
Глава 3: Рендеринг: Синтез технологий
Третья глава объединяет рассмотренные ранее концепции, показывая, как динамическое освещение и шейдерное программирование сливаются в единый конвейер рендеринга. Мы подробно разберём, как свет и материалы взаимодействуют в рамках PBR, как всё это интегрируется в игровой движок и какие передовые методы оптимизации и рендеринга определяют будущее игровой графики.
3.1 PBR: Как свет и материалы работают вместе
В основе PBR лежит идея что вместо того чтобы придумывать, как должен выглядеть материал, мы описываем его физические свойства. Шейдер, в свою очередь, использует эти свойства и формулы расчёта, чтобы рассчитать, как материал будет взаимодействовать со светом в сцене. Эта синергия создаёт предсказуемый и реалистичный результат. Как и было описанно ранее, материал предоставляет шейдеру набор текстур, описывающих его ключевые характеристики — альбедо (Albedo), металличность (Metallic) и шероховатость / гладкость (Roughness). А вот источники света предоставляют энергию, которая будет взаимодействовать с материалом. Источники света чаще всего представляют из себя направленный свет и точечный. Направленный свет такой как Direct light, чаще всего выступает в роли глобального освещения, то-есть солнце, а точечные источники это уже лампочки Point light, и прожекторы Spot light. Так же не стоит забывать об Area light.
Расчёт BRDF:
BRDF — Двулучевая функция отражательной способности (ДФОС, англ. Bidirectional reflectance distribution function — BRDF) — это функция которая, определяющая, как свет отражается от непрозрачной поверхности. Функция возвращает отношение отражённой яркости вдоль вектора направления камеры, к освещённости на поверхности направления источника света. Расчёт функции делится на две основные части: • Диффузное отражение (Diffuse): Рассеянный свет, который придаёт объекту его основной цвет. Он одинаково ярок, с какой бы стороны мы на него ни смотрели. Для металлов он равен 0. • Зеркальное отражение (Specular): Прямые отражения и блики. Их форма, яркость и цвет зависят от всех трёх свойств материала, а также от угла обзора. Именно баланс между этими двумя компонентами, скорректированный по законам сохранения энергии (объект не может отражать больше света, чем на него падает), и создаёт реалистичное изображение. Перейдём к разбору pipeline pbr шейдера на основе данных с G-Buffer:Фрагментный шейдер не работает с геометрией напрямую. Он работает с «G-буфером» — набором текстур, в которые уже была отрисована вся информация о сцене. Здесь шейдер получает всю необходимую информацию о свойствах поверхности в данной точке.
Получение набора текстур в фрагментном шейдере после рендера в G-buffer (R7Engine, GLSL).
Основной расчёт PBR в методе CalculateDeferredLighting:
Метод CalculateDeferredLighting — это сердце PBR логики. Она берёт свойства материала и рассчитывает освещение. Сначала вычисляются базовые векторы, описывающие геометрию сцены относительно пикселя:
Вычисления базовых векторов (R7Engine, GLSL).
Определение базовых свойств материала:
Здесь рассчитывается F0, базовый коэффициент отражения, используя эффект Френеля. Он определяет, сколько света отражается под прямым углом. Для диэлектриков это константа (~4%), а для металлов используется их цвет альбедо.
Расчёт базового коэффициента отражения (R7Engine, GLSL).
Модель Cook-Torrance:
Далее шейдер вычисляет три составляющие модели микрограней Cook-Torrance для создания реалистичных бликов: NDF = DistributionGGX (N, H, a): Распределение нормалей (D). Отвечает на вопрос: «Какая часть микрограней ориентирована так, чтобы отразить свет прямо в камеру. Зависит от шероховатости (roughness). G = GeometrySmith (N, V, L, a): Геометрическое затенение (G). Моделирует, как микрограни затеняют друг друга. Также зависит от шероховатости. F = FresnelSchlickRoughness (max (dot (H, V), 0.0), F0, a): Френель (F). Рассчитывает, как коэффициент отражения меняется в зависимости от угла обзора.
Три модели микрограней (R7Engine, GLSL).
Эти три компонента объединяются в единую формулу для зеркального отражения:
Единая формула зеркального отражения (R7Engine, GLSL).
Баланс между Diffuse и Specular:
Шейдер вычисляет, какая часть света уходит на диффузное отражение (kD), а какая — на зеркальное (kS). Важно, что для металлов (metallic = 1.0) диффузная часть kD становится равной нулю.
Расчёт распределения света на Ks и Kd (R7Engine, GLSL).
Расчёт итогового освещения:
В переменной типа vec3 Lo_direct объединяются диффузная и зеркальная составляющие, умножаются на цвет света (radiance) и интенсивность, зависящую от угла падения (NdotL). Также учитывается уже посчитанная бинарная тень (shadow).
Расчёт прямого света (R7Engine, GLSL).
Непрямой свет (Indirect Light / IBL) — шейдер также рассчитывает освещение от окружения (скайбокса), снова разделяя его на диффузную и зеркальную части.
Расчёт непрямого света (R7Engine, GLSL).
После того как CalculateDeferredLighting вернул цвет, main функция наслаивает на него дополнительные эффекты: • SSAO (Screen Space Ambient Occlusion): Добавляются глобальное затемнение объектов.
Добавление precompute SSAO карты в расчёт глобального света (R7Engine, GLSL).
• SSR (Screen Space Reflections): Добавляются отражения объектов, которые видны на экране.
Добавление precompute SSR карты в расчёт глобального света (R7Engine, GLSL).
• SSGI (Screen Space Global Illumination): Добавляется indirect отражения света.
Добавление precompute SSGI карты в расчёт глобального света (R7Engine, GLSL).
• Fog: Цвет смешивается с туманом в зависимости от расстояния до камеры.
Смешивание линейного тумана с конечным результатом (R7Engine, GLSL).
• God Rays: Добавляются объёмные лучи света, проходящие через сцену c учётом препятствий.
Добавление результата godrays (R7Engine, GLSL).
В итоге получается система, где художник может сосредоточиться на создании правдоподобных материалов, будучи уверенным, что они будут корректно и предсказуемо выглядеть при любом освещении, которое настроит дизайнер уровней. Это и есть главная идея PBR.
3.2 Интеграция в движок
Теоретические алгоритмы, такие как PBR или SSAO и другие, остаются просто формулами до тех пор, пока они не будут встроены в архитектуру рендер-движка. Этот процесс включает в себя создание систем для управления шейдерами, подготовки данных для рендеринга и отресовки множества проходов (passes), которые вместе формируют финальное изображение. R7Engine использует модульный и гибкий подход для решения этих задач. Движку необходим централизованный способ управления многочисленными шейдерными программами. В R7Engine эта задача решается несколькими ключевыми классами.
ShaderManager:
Этот класс действует как переключатель техник рендеринга. Вместо жесткого кодирования одного шейдера, он использует перечисление ShaderType для определения, какой алгоритм использовать. Гибкость ShaderType включает в себя не только основные PBR-шейдеры (Deferred_PBR, Forward_PBR), но и альтернативные модели (Phong), техники с тесселяцией (PBR_Tesselation) и, что важно, различные алгоритмы Ambient Occlusion (SSAO, HBAO, GTAO, RTAO).
Пример типов шейдерных программ (R7Engine, C#).
RenderSystem и RenderEffects используют этот менеджер, чтобы динамически выбирать, какой шейдер запустить. Например, в методе DrawAmbientOcclusion происходит выбор compute-шейдера для AO на основе текущего значения RenderEffects.occlusionAlgorithm.
ShaderLoader и ShaderCompiler:
Эти классы отвечают за низкоуровневую работу по загрузке, компиляции и управлению шейдерными программами. Компиляция в SPIR-V: R7Engine использует современный подход, компилируя шейдеры из формата GLSL в промежуточное представление SPIR-V. Класс ShaderCompiler использует библиотеку Glslang.NET для этой задачи. Это позволяет отделить компиляцию от рантайма и использовать более строгую проверку синтаксиса и более быструю загрузку шейдеров так как они имеют бинарный вид.
Часть метода который отвечает за компиляцию шейдера (R7Engine, C#).
Двойной режим загрузки. ShaderLoader может работать в двух режимах: загружать исходные .glsl файлы и компилировать их на лету, либо загружать уже скомпилированные .r7cs (SPIR-V) файлы. Второй режим значительно ускоряет запуск приложения.
Кусок метода который отвечает за загрузку шейдера (R7Engine, C#).
Управление ресурсами: ShaderLoader инкапсулирует хэндл программы GPU (handle), управляет аттачментом и отсоединением разных стадий шейдера (вершинный, фрагментный, compute и т. д.) и предоставляет удобные методы для передачи данных в шейдер (SetMatrix4, SetVector3 и т. д.).
Конструктор класса ShaderLoader в котором загружаются и присоединяются шейдеры (R7Engine, C#).
Вспомогательные методы которые отвечают за передачу данных в шейдер (R7Engine, C#).
Интеграция освещения:
Освещение — это не просто один шейдер, а целая система, которая подготавливает данные о свете и тенях для последующего использования. Познакомимся с классом DirectLight. Этот класс отвечает за глобальный направленный свет и, что критически важно, за создание карт теней. • Каскадные карты теней (CSM): Для качественных теней на больших расстояниях движок использует каскадные тени. DirectLight создает Texture2DArray для хранения карт глубины для нескольких каскадов.
Пример создания текстур каскадов (R7Engine, C#, OpenGL).
• Проход рендеринга теней (Shadow Pass): Метод DrawShadow является отдельным проходом рендеринга. Перед основным рендерингом он настраивает видовые и проекционные матрицы с точки зрения источника света для каждого каскада, биндит специальный Framebuffer depthMapFBO и рендерит всю видимую геометрию в этот FBO, используя очень простой шейдер, который записывает только глубину.
Рендеринг объектов в глубину (R7Engine, C#, OpenGL).
В результате после этого прохода в текстуре shadowMap содержится информация о глубине сцены с точки зрения света. Эта текстура затем используется в основном PBR-шейдере для определения, находится ли пиксель в тени. Основной цикл рендеринга в RenderSystem и VFX эффекты в RenderEffects. RenderSystem — это главный класс всего процесса. Он вызывает различные подсистемы в строгом порядке, чтобы построить кадр. Интеграция PBR в R7Engine реализована через отложенный рендеринг (Deferred Rendering). Перед расчетом освещения движок должен собрать все данные о свойствах поверхностей. Заполнение G-буфера: Метод RenderEffects.DrawSceneToGBuffer отвечает за этот этап. Он биндит gBuffer (Framebuffer с несколькими цветовыми аттачментами) и рендерит в него все непрозрачные объекты (RenderSystem._opaqueBuffer). Рендер в gBuffer был рассмотрен в «1.3Архитектуры рендеринга». После того как G-буфер готов, движок запускает серию compute-шейдеров для расчета экранных эффектов такие как расчет AO, SSGI, SSR.
Вызов расчёта экранных эффектов (R7Engine, C#, OpenGL).
Класс RenderEffects последовательно вызывает методы для расчета Ambient Occlusion (DrawAmbientOcclusion), глобального освещения (DrawSsgiComputeBuffer) и отражений (DrawSsrComputeBuffer).
Вызов выбранного алгоритма Ambient Occlusion (R7Engine, C#).
Каждый из этих проходов использует в качестве входных данных текстуры из G-буфера и записывает свой результат в отдельную текстуру (например, aoComputeResultTex, ssgiTex, ssrTex).
Вызов расчётов HBAO compute шейдера (R7Engine, C#, OpenGL).
Lighting Pass (Deferred):
Финальный Lighting Pass — это кульминация всего процесса. Здесь данные о материалах из G-буфера, тени и результаты пост-эффектов объединяются для расчета финального цвета. Метод RenderSystem.RenderMainScene выполняет эту задачу. Отложенный рендеринг плохо работает с прозрачными объектами. Поэтому R7Engine рендерит их в отдельном, прямом (Forward) проходе после основного deferred-пасса. Вызов метода RenderForwardLightingPass рендерит объекты из _transparentBuffer. Он использует отдельный PBR-шейдер (Forward_PBR), который рассчитывает освещение сразу, без использования G-буфера, и корректно смешивает результат с уже отрисованной сценой.
Финальный метод отрисовки (R7Engine, C#, OpenGL).
Метод RenderMainScene использует основные вызовы отрисовки таких как отложенный рендеринг, форвард рендеринг и рендер ландшафта. Этот метод вызывается в главном цикле рендера после прохождения всех вычислений экранных эффектов.
Часть Pipeline цикла рендера (R7Engine, C#, OpenGL).
Метод ImEditorRendering который находится в классе RenderSystem — это и есть цикл, этот метод собирает в себе абсолютно все функции которым нужно обновление каждый кадр. В этом методе находятся как дополнительные рендер функции для редактора так и для основного финальной картинки.
Начальная часть Pipeline цикла рендера (R7Engine, C#, OpenGL).
Таким образом, интеграция в R7Engine представляет собой конвейер, где каждый этап подготавливает данные для следующего, начиная от компиляции шейдеров и заканчивая попиксельным расчетом освещения с учетом множества физических эффектов.
3.3 Современная оптимизация кадра
Отрисовка одного кадра — это сложный и ресурсоёмкий процесс. Основная цель оптимизации — отправить на отрисовку в графический процессор (GPU) только те объекты, которые действительно видит камера. Отправка лишней геометрии — это пустая трата драгоценных миллисекунд, которые могли бы пойти на повышение частоты кадров (FPS). Рассмотрим три ключевых этапа фильтрации геометрии, которые присутствуют в коде: Frustum Culling, BVH и Occlusion Culling. Эти методы работают как серия фильтров, отсеивая невидимые объекты.
Frustum Culling:
Frustum Culling (отсечение по пирамиде видимости) — это первый и самый базовый этап оптимизации. Камера в 3D-мире видит не всё пространство, а только ограниченную область, имеющую форму усечённой пирамиды (frustum). Всё, что находится за пределами этой пирамиды, гарантированно не попадёт в кадр. В классе FrustumCulling присутствует метод ExtractFrustum (Matrix4 VP) который является ключевым. Метод принимает на вход матрицу View-Projection (VP) — результат перемножения матрицы вида (положение и ориентация камеры) и матрицы проекции (параметры «объектива» камеры: угол обзора, ближняя и дальняя плоскости отсечения). Из этой комбинированной матрицы код извлекает математические данные для шести плоскостей, которые образуют пирамиду видимости (левая, правая, верхняя, нижняя, ближняя и дальняя).
Извлечение данных из матрицы вида-проекции (R7Engine, C#).
Затем используются методы, такие как SphereInFrustum или BoxInFrustum. Они проверяют, находится ли ограничивающий объём объекта (сфера или параллелепипед) полностью за пределами хотя бы одной из шести плоскостей.
Проверка объекта (R7Engine, C#).
Если объект не проходит проверку, он отбрасывается и не идёт дальше по pipeline рендера.
BVH (Bounding Volume Hierarchy):
Проверять тысячи объектов по отдельности методом Frustum Culling — это долго. BVH (Иерархия ограничивающих объёмов) — это древовидная структура данных, которая группирует объекты. Представим себе большую коробку, в которой лежат две коробки поменьше, а в каждой из них — ещё несколько коробок, и так до тех пор, пока в самых маленьких коробках не окажутся сами объекты. Вместо того чтобы проверять каждый объект, сначала проверяется самая большая «коробка» (корневой узел дерева). Если вся эта большая коробка находится вне поля зрения камеры, то мгновенно отбрасывается всё её содержимое, не проверяя дочерние узлы. Это колоссальная экономия производительности. Если коробка полностью внутри, мы добавляем все объекты из неё на рендер, тоже без дальнейших проверок. Если коробка частично пересекает пирамиду видимости, мы рекурсивно спускаемся на уровень ниже и проверяем дочерние «коробки». Класс RenderSystem содержит реализацию BVH. • Разделение на статику и динамику: Код разделяет все объекты на статические и динамические, создавая для них два разных BVH-дерева (_staticBvhRoot и _dynamicBvhRoot). Это ключевая оптимизация BVH для статичных объектов (здания, скалы) строится один раз, а для динамических (персонажи, транспорт) — перестраивается по мере необходимости. • Построение Дерева: Метод RebuildBVH () вызывает BuildBVHNodeBinned. Это продвинутый алгоритм построения BVH, который использует эвристику площади поверхности (SAH) для максимально эффективного разделения объектов на группы. Он стремится создать «аккуратное» дерево, минимизируя пустое пространство в узлах. • Обход Дерева: Методе TraverseBVH рекурсивно обходит дерево и применяет Frustum Culling к узлам BVH:
Метод TraverseBVH (R7Engine, C#).
Occlusion Culling:
Объект может находиться в поле зрения камеры, но быть невидимым, потому что его полностью перекрывает другой объект (например, какой-либо объект за стеной). Frustum Culling этого не определит. Occlusion Culling (отсечение по перекрытию) решает именно эту задачу. Как это работает? Современный и эффективный способ — Occlusion Query (запрос на перекрытие) на стороне GPU. Сначала в буфер глубины рисуются все крупные объекты, которые могут перекрывать других (стены, здания). Это делается без сложных шейдеров и текстур, очень быстро. Затем для каждого объекта, прошедшего Frustum Culling, мы делаем следующее: Подаём GPU команду: «Начать считать пиксели, которые пройдут тест глубины». Рисуем не сам сложный объект, а его простой ограничивающий параллелепипед (bounding box), затем подаём GPU команду: «Закончить считать», спрашиваем у GPU: «Сколько пикселей в итоге было нарисовано?» Если результат равен 0 (или очень маленькому числу), это значит, что bounding box объекта был полностью перекрыт другими объектами. Следовательно, сам сложный объект тоже не виден, и рисовать его не нужно. В RenderSystem.cs этот механизм реализован через систему запросов.
Генерация Запросов:
Метод GenerateOcclusionQueries создаёт запросы для объектов, которые прошли этап Frustum/BVH. RenderOcclusionBBox рисует их ограничивающие объёмы (RenderOcclusionBBox) между вызовами GL.BeginQuery и GL.EndQuery.
Метод GenerateOcclusionQueries (R7Engine, C#, OpenGL).
Проверка Результата:
Результат от GPU приходит с задержкой в 1-2 кадра. Код это учитывает, используя кэш видимости (_objectVisibilityCache, _previousFrameVisibilityCache). В методе IsOcclusionVisible он проверяет, готов ли результат, и получает количество видимых пикселей (samples).
Проверка, готов ли результат (R7Engine, C#, OpenGL).
В коде функция CollectRenderables объединяет все эти подходы в единый конвейер. • Широкое отсечение: Вызывается TraverseBVHDual, который эффективно применяет Frustum Culling к BVH-деревьям, быстро отсекая огромные группы невидимых объектов. На выходе получается список visibleRenderables — потенциально видимых объектов. • Подготовка к отсечению по перекрытию: Для этого списка генерируются Occlusion Queries. • Финальная фильтрация: Перед отправкой на финальный рендеринг каждый объект из списка visibleRenderables проверяется по кэшу видимости. Если результат Occlusion Query из предыдущего кадра показал, что объект не виден, он отбрасывается. • Сортировка: Оставшиеся объекты сортируются на непрозрачные (_opaqueBuffer) и прозрачные (_transparentBuffer), которые требуют особого подхода к рендерингу.
Распределение объектов (R7Engine, C#, OpenGL).
Таким образом, из тысяч объектов, имеющихся в сцене, до дорогостоящих этапов отрисовки доходит лишь их часть. Это и есть суть современной оптимизации рендеринга, позволяющая создавать сложные и детализированные миры с высокой производительностью.
3.4 Движение к фотореализму через оптимизированный Path Tracing
Трассировка путей (Path Tracing) — это алгоритмы в компьютерной графике, которые имитируют физическое поведение света, создавая невероятно реалистичные изображения. Долгое время его сложность означала, что на рендеринг одного кадра уходили минуты или даже часы, что делало его применимым только для кино и спецэффектов. Сегодня, благодаря гибридным подходам и мощи современных GPU, можно увидеть его приход в интерактивные приложения и игры. Начнём с разбора шейдера глобального освещения SSPTGI (Screen Space Path Traced Global Illumination). Шейдер ssptgi_comp.glsl который является частью рендеринга в R7Engine и является прекрасным примером к достижению эффекта трассировки путей без специализированного оборудования. Перейдём к разбору шейдера:
Это не RTX: Расчёт на Compute Shaders:
Прежде всего, важно понять ключевое отличие метода SSPTGI от технологий типа NVIDIA RTX. NVIDIA RTX (и аналоги от AMD) — это аппаратно-ускоренная трассировка лучей. В GPU встроены специализированные ядра (RT Cores), единственная задача которых — сверхбыстро вычислять пересечение луча с треугольниками в 3D-сцене. Это позволяет выполнять трассировку в мировом пространстве (world-space) с высокой производительностью. Метод в движке R7Engine — это Screen-Space Path Traced Global Illumination (SSPTGI), реализованный как Compute Shader. Он не использует специализированные RT-ядра. Вместо этого он работает на универсальных вычислительных ядрах GPU. «Трассировка» здесь происходит не в полноценной 3D-сцене, а в экранном пространстве, используя данные, предварительно подготовленные на этапе растеризации. Это происходит потому что OpenGL не имеет расширения для аппаратно ускоренной трассировки лучей, но существуют библиотеки и инструменты, которые позволяют реализовать эту технологию с учётом возможностей графических процессоров NVIDIA. Это умный компромисс: можно получить многие визуальные преимущества трассировки путей (например, непрямое освещение), избегая при этом требования к специализированному оборудованию,которое впрочем, стало повседневностью.
Гибридная основа: G-Buffer:
Процесс начинается не с трассировки, а с классической растеризации. Вся геометрия сцены сначала отрисовывается в несколько текстур, называемых G-Buffer как и было описано в «1.3 Архитектуры рендеринга „. SSPTGI шейдер использует G-Buffer как входные данные:
Входные данные g-buffer и других текстур (R7Engine, GLSL).
Трассировка лучей в экранном пространстве (Screen-Space Ray Marching) — это сердце алгоритма. Вместо того чтобы проверять пересечение луча с миллионами треугольников в 3D, выполняется ray marching (пошаговое движение вдоль луча) в экранном пространстве. Для каждого пикселя: • Выпускается луч (например, в случайном направлении в полусфере над поверхностью). • Шейдер делает несколько шагов вдоль этого луча.
Цикл шагов вдоль луча (R7Engine, GLSL).
• На каждом шаге 3D-координата точки на луче проецируется обратно на экран, чтобы получить UV-координату в G-Buffer: vec4 clip = vec4(samplePos, 1.0) * pc.projView; • Шейдер сравнивает глубину точки на луче с глубиной, записанной в G-Buffer по этим UV. Если они достаточно близки, считается, что произошло «пересечение».
Проверка пересечения (R7Engine, GLSL).
Преимущество этого что это на порядки быстрее, чем полноценная трассировка. Недостаток алгоритма в том что он «видит» только то, что уже есть на экране. Он не сможет найти отражения объектов, находящихся за камерой или за другими объектами, что является классическим ограничением всех screen-space техник.
Пример недостатка Screen Space эффекта при повороте камеры (R7Engine).
По предоставленному выше GIF — видно что чём меньше в экранном пространстве виден свет тем менее он рассеивается, потому что есть недостаток информации о пикселях. Чтобы имитировать глобальное освещение (Global Illumination, GI), свет должен отражаться от поверхностей несколько раз. SSPTGI шейдер реализует это через цикл отскоков (bounce loop). Когда луч «попадает» в поверхность, он не останавливается, а генерируется новый луч с учётом свойств этой поверхности (например, для диффузного материала — в случайном направлении). Этот процесс повторяется u_MaxBounces раз. Именно это позволяет свету «проникать» в неосвещённые области, создавая мягкое, реалистичное непрямое освещение.
Цикл отскоков (R7Engine, GLSL).
Просто пускать лучи в случайных направлениях — неэффективно. SSPTGI шейдер использует продвинутые техники: • Последовательность Хэммерсли: Вместо простого rand () используется квазислучайная последовательность (hammersleyScrambled) для генерации направлений лучей. Это даёт более равномерное покрытие полусферы, что позволяет получить менее шумный результат при том же количестве лучей.
Пример Hammersley Scrambled для генерации направлений лучей (R7Engine, GLSL).
• Multiple Importance Sampling: Шейдер интеллектуально выбирает, как сгенерировать луч. Для глянцевых поверхностей он с большей вероятностью пустит луч в направлении зеркального отражения, а для диффузных — в случайном направлении. Затем вклады от разных стратегий взвешиваются (misWeight), чтобы уменьшить шум. Временное накопление и шумоподавление (Temporal Accumulation and Denoising). Даже со всеми оптимизациями, результат трассировки всего нескольких лучей на пиксель (numSamples) будет очень «шумным».
Пример шума от шейдера SSPTGI (R7Engine).
Борьба с этим шумом — ключ к реальному времени. SSPTGI шейдер использует также временное накопление: • Результат текущего кадра (result) смешивается с результатом из предыдущего кадра, который хранится в ssgiHistoryTex. • Используется прогрессивное усреднение: blended = (history * N + result) / (N+1). Это означает, что если камера неподвижна, каждый следующий кадр уточняет изображение, эффективно накапливая тысячи семплов с течением времени и убирая шум. • Как только камера сдвигается, накопленный буфер истории сбрасывается (accumulatedSsgiFrames = 0), чтобы избежать «шлейфов» и артефактов. В конце шейдер применяет фильтр, который анализирует соседние пиксели, чтобы найти и сгладить аномально яркие или тёмные пиксели («светлячки» и «провалы»), которые являются частыми артефактами трассировки с малым числом семплов.
Пространственное шумоподавление — (Bilateral Filtering):
Временное накопление отлично работает, когда картинка статична, но как только камера приходит в движение, история сбрасывается, и мы снова видим шумный результат трассировки. Чтобы решить эту проблему и получить чистое изображение в динамике, необходим пространственный шумоподавитель (spatial denoiser). Шейдеры ssgi_horizont_blur_comp.glsl и ssgi_vertical_blur_comp.glsl реализуют именно такой — продвинутый и оптимизированный билатеральный фильтр.
Размытие, сохраняющее края:
Простое размытие по Гауссу («Gaussian Blur») сделало бы картинку мыльной, уничтожив все детали и чёткие границы объектов. Билатеральный фильтр — это «умное» размытие. Он усредняет пиксели не только по их пространственной близости, но и по схожести их характеристик, таких как глубина и ориентация поверхности. В результате гладкие поверхности эффективно очищаются от шума, а резкие края объектов остаются нетронутыми.
Двухпроходное размытие (Separable Filter):
Реузьтат SSPTGI в движке не применяет 2D-фильтр за один проход. Вместо этого используется двухпроходный фильтр: • Горизонтальный проход (ssgi_horizont_blur_comp.glsl): Читает сырой шумный результат (ssgiRawTex) и размывает его только по горизонтали, записывая результат во временную текстуру (ssgiTempTex). • Вертикальный проход (ssgi_vertical_blur_comp.glsl): Читает временную текстуру (ssgiTempTex) и размывает её только по вертикали, записывая финальный чистый результат в ssgiBlurredTex. Тем самым не придётся сильно повышать значения количества лучей и других параметров что бы получить приемлемый результат не теряя в производительности.
Оптимизация с использованием Shared Memory:
Оба шейдера активно используют shared память GPU. Это сверхбыстрая кэш-память, доступная потокам внутри одной рабочей группы. • Тайловая обработка: Вместо того чтобы каждый поток читал из медленной глобальной памяти все необходимые ему пиксели, рабочая группа сообща загружает один «тайл» (небольшой блок) изображения с запасом (TILE_X = LOCAL_X + 2 * MAX_BLUR_RADIUS) в быструю shared память.
Пример рабочих групп (R7Engine, GLSL).
• Эффективные вычисления: Все последующие операции размытия для этого тайла происходят уже с использованием быстрой shared памяти, что многократно ускоряет процесс.
«Умные» веса (The Bilateral Weights):
Это ядро фильтра. Вклад каждого соседнего пикселя (sampleGI) в итоговый результат взвешивается на основе нескольких факторов: • Вес по глубине (wD): Рассчитывается как exp (-depthSigma * abs (centerDepth — sampleDepth)). Если разница в глубине между центральным и соседним пикселем велика (например, силуэт персонажа на фоне далёкой стены), вес wD стремится к нулю, и этот пиксель не будет влиять на результат. Это предотвращает «растекание» цвета с переднего плана на задний. • Вес по нормалям (wN): Рассчитывается как max (0.0, dot (centerNormal, sampleNormal)). Если поверхности имеют разную ориентацию (например, угол между стеной и полом), их нормали будут сильно отличаться, dot будет близок к нулю, и они не будут размываться друг с другом. • Пространственный вес (wX / wY): Это стандартный Гауссов вес, который зависит от расстояния до центрального пикселя. Он и создаёт сам эффект размытия.
Пример расчёта финального веса размытия (R7Engine, GLSL).
Благодаря этим шейдерам финальная картинка преобразуется до такого результата, что шум практически незаметен и даёт плавную картинку.


Примеры финального изображения с размытием (R7Engine).
Конвейер рендеринга R7Engine — это пример того, как можно добиться результатов в области глобального освещения, используя только универсальные вычислительные возможности GPU. Он демонстрирует все ключевые идеи, лежащие в основе современного фотореализма: гибридный подход, продвинутые алгоритмы выборки и, что самое важное, комбинацию временного накопления для статичных сцен и высокоэффективного пространственного билатерального шумоподавления для динамики.
Заключение
В ходе данного исследования был проведён комплексный анализ ключевых технологий современной real-time графики, лежащих в основе создания визуально богатых интерактивных приложений. Было установлено, что синергия между продвинутыми техниками динамического освещения и гибкостью шейдерного программирования является фундаментом, на котором строятся впечатляющие виртуальные миры. Работа последовательно раскрыла эволюцию освещения от статических, предварительно рассчитанных методов (lightmaps) до полностью динамических систем. Анализ архитектур рендеринга, в частности Deferred Shading, продемонстрировал, как отделение рендеринга геометрии от расчёта освещения позволило эффективно обрабатывать сотни источников света и облегчение создания пост процесс эффектов, что было невозможно при классическом подходе Forward Rendering. Центральная роль в реализации этих алгоритмов отводится шейдерному программированию. Исследование показало, что шейдеры являются основным инструментом для управления каждым аспектом финального изображения: от трансформации вершин и определения цвета каждого пикселя до реализации сложных визуальных эффектов. Переход на принципы физически корректного рендеринга (PBR) и использование моделей, таких как Cook-Torrance BRDF, позволил художникам оперировать интуитивно понятными физическими свойствами материалов (металличность, шероховатость), добиваясь предсказуемого и реалистичного результата при любом освещении. Кроме того, техники постобработки, такие как Bloom и God Rays, значительно повышают атмосферность и кинематографичность сцены. Наконец, была рассмотрена практическая интеграция этих систем в архитектуру игрового движка, где сложный конвейер рендеринга объединяет множество проходов: от рендеринга карт теней и заполнения G-буфера до применения экранных эффектов и финального расчёта освещения. Эффективность этого процесса напрямую зависит от современных методов оптимизации, таких как Frustum и Occlusion Culling, которые минимизируют количество избыточной работы для GPU. Движение индустрии к фотореализму было продемонстрировано на примере Screen-Space Path Traced Global Illumination (SSPTGI) — гибридной технологии, которая имитирует трассировку путей на универсальных вычислительных шейдерах, а для борьбы с шумом использует комбинацию временного накопления и продвинутых пространственных фильтров.
Таким образом, современная игровая графика представляет собой сложный баланс между физически корректными моделями света, программируемым конвейером рендеринга и многоуровневой оптимизацией, что в совокупности и позволяет достигать новых высот визуального качества в реальном времени.
NVIDIA RTX Technology [Электронный ресурс] // nvidia [nvidia.com] (https://www.nvidia.com). (2025). URL: https://www.nvidia.com/en-us/technologies/rtx (дата посещения 10.10.2025).
NVIDIA DLSS Technology [Электронный ресурс] // nvidia [nvidia.com] (https://www.nvidia.com). (2025). URL: https://www.nvidia.com/en-us/geforce/technologies/dlss (дата посещения 10.10.2025).
Khronos Group OpenGL 4.6 [Электронный ресурс] // khronos [khronos.org] (https://www.khronos.org). (2025). URL: https://www.khronos.org/opengl (дата посещения 05.09.2025).
Khronos Group OpenGL 4.6 — OpenGL Shading Language 4.60 Specification [Электронный ресурс] // khronos [khronos.org] (https://www.khronos.org). (2017). URL: https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.pdf (дата посещения 07.09.2025).
LearnOpenGL — Basic Lighting [Электронный ресурс] // learnopengl [learnopengl.com] (https://learnopengl.com). (2014). URL: https://learnopengl.com/Lighting/Basic-Lighting (дата посещения 26.10.2025).
LearnOpenGL — Advanced Lighting [Электронный ресурс] // learnopengl [learnopengl.com] (https://learnopengl.com). (2016). URL: https://learnopengl.com/Advanced-Lighting/Advanced-Lighting (дата посещения 27.10.2025).
LearnOpenGL — PBR [Электронный ресурс] // learnopengl [learnopengl.com] (https://learnopengl.com). (2016). URL: https://learnopengl.com/PBR/Theory (дата посещения 29.08.2025).
LearnOpenGL — PBR IBL [Электронный ресурс] // learnopengl [learnopengl.com] (https://learnopengl.com). (2016). URL: https://learnopengl.com/PBR/IBL/Specular-IBL (дата посещения 30.08.2025).
LearnOpenGL — Deferred Shading [Электронный ресурс] // learnopengl [learnopengl.com] (https://learnopengl.com). (2014). URL: https://learnopengl.com/Advanced-Lighting/Deferred-Shading (дата посещения 01.10.2025).
LearnOpenGL — Bloom [Электронный ресурс] // learnopengl [learnopengl.com] (https://learnopengl.com). (2025). URL: https://learnopengl.com/Advanced-Lighting/Bloom (дата посещения 01.10.2025).
R7Engine (автор Федичев Алексей).