Найти в Дзене

Оптимизация рендеринга в Vulkan: Реактивная упаковка шейдеров и растеризация

Оглавление

В своей работе я часто сталкиваюсь с задачами рендеринга и оптимизации производительности графических приложений. Vulkan — это мощный и гибкий API, который я использую для создания высококачественных графических сцен. В этой статье я хочу поделиться своим опытом оптимизации рендеринга сцены с помощью реактивной упаковки шейдеров и растеризации, а также физически обоснованного затенения. Я приведу пример кода, который я использую, и подробно разберу каждый шаг.

Введение в рендеринг на основе трассировки лучей

Рендеринг на основе трассировки лучей позволяет моделировать модели переноса света с обработкой видимости, что делает сцены более реалистичными. Однако это требует значительных вычислительных ресурсов. Поэтому важно оптимизировать процесс рендеринга, чтобы достичь баланса между качеством изображения и производительностью.

Пример программы

Ниже представлен пример кода, который демонстрирует, как я оптимизирую позиции вершин и карту нормалей на рендере с использованием физически обоснованного затенения.

class BasicRenderer { public:
BasicRenderer();

void loadScene(std::string gltf_filename, std::string target_img_filename) {
// Загружаю исходную сцену из файла и анализирую структуры CpuImage. std::tie(m_img_map["vtx_pos"], m_img_map["vtx_uv"], m_img_map["faces"],
m_img_map["world_mat"], m_img_map["view_mat"],
m_img_map["prj_mat"], m_img_map["env_img"],
m_img_map["albedo"], m_img_map["metallic"],
m_img_map["roughness"], m_img_map["normal"],
m_img_map["background"]) = LoadGltfAsCpuImages(gltf_filename);

// Загружаю целевое изображение и анализирую его структуру. m_img_map["target"] = LoadTargetImageAsCpuImage(target_img_filename);

// Создаю переменные верхнего уровня вычислительного графа. m_var_map["vtx_pos"] = {VEC3, m_img_map["vtx_pos"].getImgSize()};
m_var_map["vtx_uv"] = {VEC2, m_img_map["vtx_uv"].getImgSize()};
m_var_map["faces"] = {IVEC3, m_img_map["faces"].getImgSize()};
m_var_map["model_mat"] = {MAT4, {1, 1}};
m_var_map["view_mat"] = {MAT4, {1, 1}};
m_var_map["prj_mat"] = {MAT4, {1, 1}};
m_var_map["env_img"] = {VEC3, m_img_map["env_img"].getImgSize()};
m_var_map["albedo"] = {VEC3, m_img_map["albedo"].getImgSize()};
m_var_map["metallic"] = {FLOAT, m_img_map["metallic"].getImgSize()};
m_var_map["roughness"] = {FLOAT, m_img_map["roughness"].getImgSize()};
m_var_map["normal"] = {VEC3, m_img_map["normal"].getImgSize()};
m_var_map["background"] = {VEC3, m_img_map["background"].getImgSize()};
m_var_map["target"] = {VEC4, m_img_map["target"].getImgSize()};

// Устанавливаю флаг для отправки изображений ЦП на ГП.

m_is_sent = false;
}

void buildGraph(uint32_t K, float r, float sigma, float delta, float lr) {
// Строю вычислительный граф рендеринга.

Variable rendered_img = BuildBasicRenderGraph(
m_var_map["vtx_pos"], m_var_map["vtx_uv"], m_var_map["faces"],
m_var_map["model_mat"], m_var_map["view_mat"],
m_var_map["prj_mat"], m_var_map["env_img"],
m_var_map["albedo"], m_var_map["metallic"],
m_var_map["roughness"], m_var_map["normal"],
m_var_map["background"], K, r, sigma, delta);

// Учитываю потери.

Variable loss = F::Mean(F::Abs(m_var_map["target"] - rendered_img));

// Устанавливаю потери и оптимизатор.

m_optimizer.setLossVar(loss);
m_optimizer.setOptimizer([=](Variables xs, Variables gxs)
{
Variables updated_xs;
for (size_t i = 0; i < xs.size(); i++) {
updated_xs.push_back(xs[i] - gxs[i] * lr);
}
return updated_xs;
});
}

void setRequiresGrad(std::string name) {
// Устанавливаю флаг требуемого градиента. m_var_map[name].setRequiresGradRecursively();
}

void execStep() {
// При необходимости отправляю изображения ЦП на ГП.

if (!m_is_sent)
{
for (auto [name, var]: m_var_map)
{
m_optimizer.sendImg(var, m_img_map[name]);
}
m_is_sent = false;
}
// Выполняю одну итерацию оптимизации. m_optimizer.execStep();
}

void saveScene(std::string gltf_filename) {
// Получаю все изображения с графического процессора.

for (auto [name, var]: m_var_map)
{
m_img_map[name] = m_optimizer.recv(var);
}
// Сохраняю оптимизированную сцену в файл GLTF. SaveGltfFromCpuImages(m_img_map);
}

private:
Optimizer m_optimizer;
// Карта данных сцены для CpuImage.

std::map<std::string, CpuImage> m_img_map;
// Карта данных сцены для переменной, соответствующей CpuImage. std::map<std::string, Variable> m_var_map;
// Внутренние флаги.

bool m_is_sent = false;
};

void OptimizeVertexPostionAndNormalTexture();

int main(int argc, char *argv[]) {
OptimizeVertexPostionAndNormalTexture();
return 0;
}

void OptimizeVertexPostionAndNormalTexture() {
BasicRenderer renderer;
// Загрузим данные сцены на процессор. renderer.loadScene("initial_scene.gltf", "target_img.png");
// Построим вычислительный граф.

uint32_t K = 2; // Количество пилинга.

float r = 0.01f; // Параметр радиуса.

float sigma = r / 7.f; // Параметр смешивания.

float delta = r; // Ширина края силуэта.

float lr = 0.01f; // Скорость обучения для оптимизатора.

renderer.buildGraph(K, r, sigma, delta, lr);
// Отметим позиции вершин и текстуру нормалей как цели оптимизации. renderer.setRequiresGrad("vtx_pos");
renderer.setRequiresGrad("normal");
// Итерации оптимизации. for (int iter = 0; iter < 1000; iter++)
{
renderer.execStep();
}
// Сохраним оптимизированные данные в файл. renderer.saveScene("optimized_scene.gltf");
}

Объяснение кода

Загрузка сцены и целевого изображения

В методе loadScene я загружаю данные сцены из файла GLTF и целевое изображение. Эти данные хранятся в виде структур CpuImage, и я создаю соответствующие переменные для вычислительного графа.

Построение вычислительного графа

Метод buildGraph строит вычислительный граф рендеринга с заданными параметрами. В этом графе я учитываю потери, которые представляют собой среднее абсолютное отклонение между целевым и рендеренным изображением. Оптимизатор обновляет переменные на каждой итерации, уменьшая потери.

Установление флага требуемого градиента

Метод setRequiresGrad устанавливает флаг требуемого градиента для переменных, которые я буду оптимизировать. В данном случае это позиции вершин и текстура нормалей.

Выполнение итераций оптимизации

В методе execStep я выполняю итерации оптимизации. Если изображения еще не были отправлены на графический процессор, я отправляю их в начале каждой итерации. Затем выполняется одна итерация оптимизации.

Сохранение оптимизированной сцены

Метод saveScene получает все изображения с графического процессора и сохраняет оптимизированную сцену в файл GLTF.

Заключение

В данной статье я рассказал, как можно оптимизировать рендеринг сцены с использованием Vulkan, реактивной упаковки шейдеров и растеризации. Приведенный пример кода демонстрирует, как загрузить сцену, построить вычислительный граф, выполнить оптимизацию и сохранить результаты. Надеюсь, что этот материал будет полезен для разработчиков, стремящихся улучшить производительность и качество рендеринга в своих приложениях.