96 KiB
Что перенять из графического стека Unreal Engine 5.8.2
Исследование исходников от 17 сентября 2026 года. Репозиторий: ../../../UnrealEngine. Версия проверена в Build.version; commit 16d75d84714512edfb744e1fd0a59e9c74d57873. Рабочее дерево UE при начале исследования было чистым.
Фокус — передовая графика для Faset Engine. Прочитаны выбранные C++-реализации и .usf/.ush-шейдеры, включая тела алгоритмов и места их подключения. Это выборочное исследование архитектуры, а не аудит всего UE. Движок не собирался, сцены и GPU-бенчмарки не запускались. Приоритеты и варианты прототипов ниже — инженерная оценка; чисел ускорения по результатам измерений здесь нет.
Главный вывод
Из UE стоит переносить способы ограничивать работу, а затем объединять их в систему. Nanite ограничивает и организует обработку геометрии, VSM — обновление теневых страниц, Lumen — трассировку и обновление освещения, MegaLights — число дорогих выборок света, Substrate — сложность обработки материалов. Результат зависит от хороших приближений, кэшей и проверки их актуальности.
После завершения MVP первый серьёзный графический эксперимент — GPU-driven visibility: indirect rendering + HZB + повторная проверка окклюзии. Это полезная часть подхода Nanite, которая не требует сразу строить виртуализированную геометрию. Для первого небольшого визуального эффекта — glints. Для наиболее заметного изменения освещения — гибридная GI с пространственным radiance cache, если уже есть рабочая трассировка и temporal pipeline.
Что выбрать в зависимости от цели
- Много объектов и высокая геометрическая плотность. GPU culling → indirect draw → two-pass HZB → обычный mesh LOD; затем meshlet/cluster LOD → visibility buffer и сортировка shading work. Стриминг геометрии и software rasterizer добавлять после измерений на субпиксельных треугольниках. Первые этапы полезны самостоятельно.
- Динамическое непрямое освещение. Гибридные screen/world traces, отдельное переиспользуемое представление освещения и бюджет обновления probes. Сначала ограничить прототип статической геометрией и diffuse GI. Полный Surface Cache Lumen с карточками не является обязательной первой ступенью собственной GI.
- Много динамических источников с мягкими тенями. Подход MegaLights: выбирать небольшое число важных источников, трассировать visibility и восстанавливать изображение. Начинать после clustered light lists, motion vectors и базового denoiser. При 2–4 источниках общая инфраструктура может оказаться дороже обычного освещения.
- Чёткие тени на больших расстояниях. VSM: виртуальные адреса, физический пул страниц, запросы от видимых поверхностей, кэширование и инвалидирование. Первая версия — один directional light и ограниченная сцена. Не начинать с большого числа локальных источников.
- Высокая детализация при меньшем внутреннем разрешении. Идеи TSR: реконструкция с проверкой истории, обработка вновь открывшихся поверхностей и изменения шейдинга. Сначала правильные motion vectors и обычный temporal resolve; сложные эвристики TSR — следующий этап.
- Более богатые материалы. Из Substrate взять ограниченные классы сложности и обработку по тайлам; из glints — фильтруемое распределение микробликов. Небольшой фиксированный clear-coat/двухслойный набор часто разумнее произвольного графа BSDF.
- Большие наборы текстур. Virtual Texturing полезна, когда именно residency и объём текстур стали проблемой. Стриминг целых mip-уровней проще; переход на страницы имеет смысл по измеренному рабочему набору.
Принятый план Faset — продвинутая графика после MVP
Решения обновлены 18 сентября 2026. Подробные критерии продукта — в PLAN.md, технические границы — в архитектуре. Результаты чтения UE ниже сохраняются; они не означают, что эти системы уже есть в Faset, входят в MVP или обязательно будут воспроизведены целиком.
Принято: собственный Vulkan 1.3 renderer/backend и Render Graph, SDL3, Slang → SPIR-V с совместимым HLSL; базовый путь без обязательного RT. Compiler работает в editor/cook, Player получает готовые shaders и metadata. Готовый SPIR-V не отменяет создания pipelines драйвером. Editor и Player — разные процессы; MCP доступен строго в редакторе, включая headless authoring/import/build, Play/Stop и логи редактора, без чтения или изменения runtime worlds/сессий игры.
MVP (M2, M8–M9). Две демки и измеримый direct renderer
Небольшая 2D-игра со спрайтами, прозрачностью, слоями и HUD; небольшая 3D-игра со статическими мешами, текстурами, простым PBR, солнцем и обычной shadow map. Обе проходят editor → save → Play → сборка самостоятельной игры на Linux и Windows. Начальный renderer: CPU frustum culling, direct draws, одна GPU queue, явные reads/writes, корректные barriers и отложенное освобождение ресурсов. Нужны имена проходов, validation, CPU/GPU timings и счётчики памяти; оптимизации graph culling, aliasing и async compute можно отложить.
Готовность: предсказуемые save/build/Play, корректный resize/minimize, отсутствие ошибок validation, самостоятельный Player без редактора и MCP. Direct-путь остаётся эталоном для следующих этапов. Версия API не заменяет проверки features, formats, limits и драйверов конкретных GPU.
P2. GPU visibility и обычный LOD
Resident instance buffer → GPU frustum culling → fixed indirect bins → current HZB visualization → two-pass HZB с history reset → обычный mesh LOD по экранному размеру и hysteresis. Детальный контракт разобран в исследовании 15. Начальный материал — opaque, обычная hardware rasterization; 2D/прозрачность сохраняют отдельный порядок.
Готовность: финальная depth/ID-картинка совпадает с baseline с учётом допустимых ties, нет пропавших объектов при открытии двери/camera cut, проверены пустые и предельно заполненные очереди. Измерять полный кадр и открытой, и закрытой сцены; HZB может добавить стоимость без выигрыша. Motion-vector target и TAA не являются предпосылкой первого HZB.
P3, первая часть. Освещение и тени
Расширить число локальных источников; вводить clustered light lists при подтверждённой необходимости. Для солнца — cascaded shadow maps, затем ограниченный atlas локальных теней. У shadow views собственные списки видимости: скрытый от основной камеры объект может отбрасывать видимую тень. Готовность — управляемые бюджеты, стабильность при движении камеры и понятный профиль обновлений. VSM остаётся отдельным дальнейшим экспериментом после обычных теней.
P3, вторая часть. История и temporal
Motion vectors камеры/объектов, согласованные jitter/unjitter transforms, reset при camera cut/resize, reprojection и rejection → TAA → позднее temporal upscaling. Проверять disocclusion, тонкую геометрию, движение и экспозицию, сравнивая с режимом без temporal. TSR не заменяет denoiser GI или direct lighting: сигнал и критерии доверия различаются.
P4. Непрямой свет и исследовательские ветки
Сначала запечённый непрямой свет и reflection probes, затем ограниченные screen-space дополнения с явным fallback. Динамическая GI без обязательного RT — самостоятельный исследовательский этап с бюджетами обновления/памяти, проверками света за камерой, утечек через стены и изменяющихся заслонов. Полный эквивалент Lumen не обещается.
Дальнейшие ветки выбираются по измеренной проблеме, по одной: кластерный LOD/streaming и visibility buffer; виртуальные теневые страницы; стохастический direct lighting; VT; сложные материалы. Glints допустимы как изолированный BRDF-эксперимент. Это исследовательские возможности, не дополнительные обязательства MVP. На каждом этапе фиксируются сцена, путь камеры, настройки, GPU/driver, изображение, время и память; GPU-бенчмарки ещё не выполнены.
Общие ошибки, которых помогают избежать исходники
- «Nanite — это просто mesh shaders». Его полезность складывается из preprocessing, выбора детализации, culling, rasterization, material scheduling и residency. Не нужно делать mesh shaders условием первого прототипа.
- «Lumen — один ray tracing shader». Это несколько представлений сцены, способов трассировки и кэшей, соединённых бюджетами и фильтрацией. Недостающий screen-space hit не равнозначен отсутствию геометрии в мире.
- «MegaLights делает любое число lights бесплатным». Ограничивается дорогая выборочная оценка; выбор кандидатов, scene structures, денойзинг и качество также имеют цену.
- «Виртуальная память автоматически уменьшает работу». Требуются реальные locality и повторное использование. Движущаяся геометрия и источники могут часто инвалидировать страницы, а резкое перемещение камеры создаёт всплеск запросов.
- «Temporal накопление скрывает все ошибки». Неверные velocity и слишком доверчивая история дают ghosting. Если история отвергается почти всегда, сигнал остаётся шумным. Проверять следует видео и контролируемые движения.
- «Самая сложная версия UE — лучший старт». Минимальная версия должна иметь собственный полезный результат и сравнимый baseline. Наличие множества permutations в UE показывает реальные ограничения интеграции, но не требует воспроизводить все варианты.
Как читать дальнейший разбор
В следующих разделах «в исходниках» означает подтверждённую реализацию этой ревизии, а «прототип / перенять / проверка» — предложение для собственного движка. Ссылки на исходники относятся к указанному commit; локальные checkout являются внешними необязательными материалами исследования и не входят в Faset. Термины: HZB — иерархическая пирамида глубины; closure — отдельная составляющая модели рассеяния; residency — какие страницы сейчас находятся в GPU-памяти; disocclusion — появление ранее закрытой поверхности; radiance cache — переиспользуемые оценки входящего света.
Nanite / GPU Scene: что перенести в собственный renderer
Основание: локальные тела C++/HLSL из ../../../UnrealEngine, commit 16d75d84714512edfb744e1fd0a59e9c74d57873 (UE 5.8.2). Это исследование исходников, не выполненный GPU-профиль. Приоритеты, минимальные реализации и сценарии проверки ниже — инженерная оценка; чисел ускорения не измерял. Сосредоточился на треугольном пути: в этой ревизии уже есть отдельные ветки Nanite curves и voxels, поэтому нельзя автоматически приписывать всем путям свойства классического triangle Nanite.
1. GPU-driven visibility с двухфазным HZB — начать здесь
Что интересно. Предыдущая глубина используется как предположение, которое исправляется в том же кадре. Main pass проверяет bounds в предыдущих transforms/view относительно предыдущего HZB; предполагаемо закрытые объекты отправляются в отдельную очередь. Main raster строит текущую глубину, затем новый HZB и post pass повторно проверяют отложенную очередь. В результате temporal occlusion не требует ждать CPU query и имеет путь восстановления для открывшихся объектов.
Доказательства. FBoxCull::HZB строит PrevFrustumCull из PrevLocalToTranslatedWorld и PrevTranslatedWorldToClip, записывая результат в bWasOccluded: NaniteCullingCommon.ush:623. В post permutation используется текущий rect и текущий HZB: там же:646. Запись отложенного instance через wave-aggregated atomic и чтение количества для post — NaniteInstanceCulling.usf:169, 243. Реальный порядок C++: main cull/raster NaniteCullRaster.cpp:7008, BuildHZBFurthest из scene depth и Nanite rasterized depth 7053, post cull/raster 7071. Это не просто декларация feature flag.
Не менее полезная основа — постоянная GPU Scene. Primitive/instance данные адресуются ID, изменения накапливаются без повторного добавления primitive в dirty list, затем обновляются scatter upload. Видно в FGPUScene::AddPrimitiveToUpdate GPUScene.cpp:1755, создании uploader 1218 и записи instance элементов в вычисленные scatter offsets 1358. Выигрыш архитектуры — culling, material lookup, тени и motion получают общие ID и таблицы; не нужно заново собирать данные каждого объекта на каждый draw.
Минимальный перенос. Stable instance ID; GPU tables {bounds, current/previous transform, mesh, material}; dirty-range uploads; compute frustum/HZB; append-buffer видимых ID и indirect draw arguments. Сначала использовать обычные meshes/meshlets и hardware raster. Добавить очередь rejected, rebuild HZB и второй indirect draw. Для fixed-material opaque сцены это самостоятельный проект без Nanite builder и virtual geometry.
Ловушки. Near-plane и деформация требуют консервативных bounds: сам UE пропускает HZB при пересечении near plane NaniteCullingCommon.ush:635. HZB-rect у UE привязан к центрам единственного sample; комментарий прямо предупреждает, что MSAA/conservative raster требуют другой формулы NaniteHZBCull.ush:80. Reverse-Z требует minimum reduction и соответствующего сравнения 195. Добавить overflow counters для очередей и корректное сохранение previous transforms при переиспользовании ID.
Проверка. 100 тыс. instances за крупными occluders: статичная камера, быстрый поворот, телепорт, движущаяся дверь. Сравнивать итоговую depth/ID-картинку с HZB-off reference, отдельно замерять main/post survivors, HZB build, culling и total GPU frame. Критерий успеха — отсутствие пропавшей видимой геометрии и выигрыш total frame на закрытой сцене, а не только меньше draw calls. На открытой сцене второй проход может оказаться чистой дополнительной стоимостью.
2. Visibility buffer + группировка shading по материалу — сильная архитектурная идея отдельно от Nanite
Что интересно. Геометрический проход сохраняет адрес поверхности, затем shading восстанавливает атрибуты победившего треугольника и группирует пиксели по материалам. Geometry scheduling отделяется от дорогого material evaluation. Это позволяет иметь очень много кластеров без пропорционального количества CPU material draws; потенциальный выигрыш особенно интересен при дорогих материалах и overdraw.
Доказательства. PackVisPixelX кодирует visible-cluster ID и triangle ID, UnpackVisPixel выделяет второй компонент depth: NaniteDataDecode.ush:890. WritePixel пакует ID+depth в 64-bit value и выполняет ImageInterlockedMaxUInt64, согласованно выбирая depth и идентификатор: NaniteWritePixel.ush:20. Material evaluation реально читает этот ID, получает visible cluster и page/cluster data NaniteVertexFactory.ush:1187; ветка shading читает VisBuffer64 и запрашивает пересчёт barycentrics 1240.
Binning состоит из count, reserve, scatter: wave count группирует material-bin matches NaniteShadeBinning.usf:397, выбор count/scatter shader path 861, резервирование диапазонов 959. Особенно ценная деталь: материалам без derivative ops выделяются отдельные pixels, остальным — quads; проверка bNoDerivativeOps находится 968. Это не универсальная сортировка всех пикселей независимо от требований shader.
Минимальный перенос. Обычный hardware depth+ID pass, таблицы vertex/index/material, один compute resolve для нескольких фиксированных PBR-material classes. Затем material bins и indirect compute dispatch. Для такого варианта не обязательно воспроизводить Nanite 64-bit atomics: обычные depth attachment и integer ID target подходят, если пишет только hardware path. Общая атомарная depth+ID операция нужна при независимых конкурентных software/hardware writers.
Ловушки. UV derivatives, texture LOD, tangent handedness, degenerate triangles и deformation должны восстанавливаться согласованно с geometry pass. UE вычисляет triangle barycentrics NaniteVertexFactory.ush:1043 и интерполирует UV/position/tangent данные 717. «Shading once» не означает отсутствие helper lanes или повторного alpha-test: masked/WPO поверхности усложняют raster. Полный G-buffer не обязательно исчезает: visibility может быть только первым этапом material evaluation. Не рассчитывать на экономию памяти без измерения всех вспомогательных buffers и output targets.
Проверка. Одинаковая сцена в G-buffer и visbuffer вариантах: число material classes 4/32/256, overlap слоёв 1/4/16, subpixel triangles, checker UV. Считать geometry+binning+resolve вместе; проверять mip selection и края, shader invocations, bandwidth и p95 frame time. При простых материалах расходы binning могут перевесить пользу.
3. Virtual geometry: согласованный LOD-срез, bounded residency и feedback
Что интересно. Nanite объединяет offline и runtime алгоритмы: группы соседних кластеров упрощаются вместе, runtime выбирает достаточный уровень по screen-space error, GPU запрашивает недостающую детализацию, а резидентный более грубый уровень остаётся корректным drawable fallback. Ценная мысль — streaming и LOD составляют один протокол допустимых представлений поверхности.
Доказательства. Builder объединяет children и упрощает triangles ClusterDAG.cpp:1467, заново делит результат в parent clusters 1558, назначает всем parents группы одинаковые bounds/error 1587. В simplifier позиции концов external edges блокируются Cluster.cpp:1021. Поэтому независимое упрощение каждого meshlet не воспроизводит согласованность Nanite.
В GPU traversal ShouldVisitChildInternal учитывает parent/min LOD errors и projected edge scales NaniteClusterCulling.usf:258; cluster cut использует SmallEnoughToDraw 287. Ключ к graceful streaming: кластер принимается также при NANITE_CLUSTER_FLAG_STREAMING_LEAF, даже если критерий детализации ещё не достигнут 842. Leaf traversal формирует page-range request с priority 579.
Менеджер рекурсивно добавляет page dependencies с повышенным приоритетом NaniteStreamingManager.cpp:2579, увеличивает их refcount при регистрации 950, при eviction выбирает только unreferenced LRU pages с нулевым refcount 2904. То есть простой LRU, игнорирующий зависимости, разрушает предпосылки traversal/decoding.
Минимальный перенос. Сначала meshlet LOD hierarchy целиком в памяти, hardware raster и корректный cut. Затем фиксированный page pool, постоянно доступный coarse mesh, page table, GPU feedback, budgeted async upload и переход на детей только когда готова вся требуемая группа. Compression, GPU transcoding, partial group fixups, скелеты, voxels и curves — отдельные этапы. Это высокая сложность, но очень большой эффект для сцен с тяжёлой геометрией.
Дополнительный образец. Persistent GPU workers обрабатывают общую очередь hierarchy nodes, а когда nodes ещё не готовы, берут cluster jobs: реальная ветка NaniteHierarchyTraversal.ush:274, fallback work 326. Для первой реализации послойные indirect dispatch проще отладить; MPMC scheduler оправдан только после профиля occupancy/launch overhead.
Проверка. Asset set заведомо больше page pool, flythrough с возвратом и внезапной сменой направления; искусственная задержка I/O. Требовать ограниченного VRAM, отсутствия дыр/двойной геометрии на переходах и контролируемого LOD error; измерять missed requests, page churn, upload budget, visible cluster count. Полноценная Nanite-копия — большой самостоятельный renderer/asset pipeline, не библиотека для подключения за неделю.
4. Гибрид hardware/software raster для микротреугольников — исследовательский следующий шаг
Что интересно. Один visibility target объединяет два raster пути: крупные/требующие clipping кластеры отправляются hardware rasterizer, мелкие — compute rasterizer. Это переносимая идея специализации под размер примитива, но наиболее аппаратно-зависимая из четырёх.
Доказательства. SmallEnoughToDraw отдельно устанавливает bUseHWRaster по projected edge scale NaniteClusterCulling.usf:297; near clipping принудительно включает HW 860. EmitVisibleCluster заполняет SW/HW списки с противоположных концов общего массива и отдельными atomic counters 688. Compute entry вызывает ClusterRasterize NaniteRasterizer.usf:2421, triangle path готовит edge equations и вызывает adaptive raster 718. Внутри software raster есть ещё одна специализация: rectangle traversal для узких triangles, scanline для более широких или programmable shading NaniteRasterizer.ush:292. Общая атомарная запись depth+ID описана выше.
Минимальный перенос. После стабильного hardware visbuffer добавить opaque-only compute raster для кластера с маленьким screen bound. Всё остальное оставить HW. Подбирать порог на своей GPU по total raster time; не зашивать UE threshold как универсальную константу. Programmable raster/WPO/alpha cutouts вводить отдельно, когда оба пути совпадают по coverage.
Ловушки и проверка. Нужны согласованные fill rules, subpixel precision, depth tie handling, synchronization, capability check для 64-bit image atomics. В текущем HW path есть явное предупреждение о проблемах depth precision у очень узких triangles NaniteRasterizer.usf:3300. Sweep треугольников от долей пикселя до сотен пикселей, front/back faces, shared edges и near-plane; сравнивать coverage с HW baseline, отдельно стрессировать overdraw и контенцию атомиков. Принимать только измеренный выигрыш в целевой сцене и на целевых GPU: наличие compute raster само по себе не доказывает ускорение.
Рекомендуемый порядок: GPU Scene + indirect/two-pass visibility → простой visibility shading → resident cluster LOD → page streaming → гибридный raster и persistent traversal после профилирования.
Дополнение по hardware prerequisites: mesh shaders не являются обязательным условием этой архитектуры и даже не единственным Nanite HW path. В реальном dispatch есть DispatchIndirectMeshShader для mesh-path и DrawPrimitiveIndirect в else: NaniteCullRaster.cpp:3661. Поэтому meshlet здесь означает разбиение/единицу данных, а не обязательное использование mesh-shader API. В отличие от этого, приведённый UE visibility-write path явно ограждён COMPILER_SUPPORTS_UINT64_IMAGE_ATOMICS и имеет #error в неподдерживаемой non-depth-only ветке: NaniteWritePixel.ush:27. Depth-only path использует обычный uint atomic.
Lumen и MegaLights: механизмы для собственного renderer
Проверены тела C++/HLSL в ../../../UnrealEngine, commit 16d75d84714512edfb744e1fd0a59e9c74d57873. Это статическое исследование: движок и GPU-бенчмарки не запускались. Оценки полезности, минимальные варианты и тесты ниже — мои инженерные предложения; описания UE привязаны к реализации.
1. Каскад трассировки: дешевое представление сначала, уплотнённый список незавершённых лучей затем
Что делает UE. TraceScreenProbes сначала запускает экранную трассировку, после неё выбирает HWRT либо трассировку mesh SDF/heightfields, а финальным проходом обрабатывает оставшиеся лучи и применяет Radiance Cache/sky. Это именно условные альтернативы: HWRT и mesh SDF не обязательные последовательные ступени одной конфигурации. Сборка проходов: LumenScreenProbeTracing.cpp:678, выбор HWRT: строка 795, software branch: 817, финальная compaction: 877, отключение global-SDF в HWRT permutation: 909.
Экранный hit не принимается безусловно: HZB сообщает неопределённость, bHit && !bUncertain отбрасывает сомнительный результат, край экрана отсеивается стохастически, глубина прошлого кадра проверяется перед выборкой его цвета. Эти проверки находятся в LumenScreenProbeTracing.usf:276, depth validation — 304–311, выборка radiance — 334–345. Это объясняет, почему просто добавить SSR/SSGI перед ray query недостаточно: нужна мера доверия и корректный переход между представлениями.
ScreenProbeCompactTracesCS сохраняет лучи без hit, которые ещё не дошли до Radiance Cache, с дополнительными отсечениями по дистанции. WavePrefixCountBits считает локальные offsets; на группу делается одна глобальная atomic allocation, затем пишется плотный список. GPU сам формирует indirect dispatch arguments. Тела: LumenScreenProbeTracing.usf:394, условие — 431, wave compaction — 455–481, indirect args — 508–523. Это переносимая оптимизация также для shadow rays, reflection rays и secondary bounces.
Минимум для своего движка. G-buffer + HZB → экранный луч с hit/confidence/distance → append/scan оставшихся лучей → один существующий world-space backend → resolve. Не начинать с собственной SDF-системы, если уже есть BVH/ray queries. Первую версию сделать с простым append; wave-оптимизацию добавлять после измерений. Не терять mapping исходного pixel/ray ID при compaction, правильно сохранять достигнутую дистанцию и делать небольшой pullback для следующей ступени.
Условия и риски. Нужны стабильная reprojection, motion vectors, предыдущее освещение, explicit GPU barriers и indirect dispatch. Повторное использование экранного света создаёт зависимость от видимости камерой и может образовать GI feedback; UE отдельно запрещает часть backface/foliage rays (usf:99). Качество geometry representations должно согласовываться, иначе границы экранной и мировой трассировки проявятся в движении. Compaction тоже стоит времени: при почти 100% misses она может проиграть прямой dispatch.
Проверка. На одинаковых лучах сравнить world-only и hybrid: GPU ms по ступеням, долю rays, дошедших до world, количество indirect workgroups. Камера пересекает край экрана у тонкой перегородки; свет за камерой; движущийся объект; быстрый разворот. Проверять не только среднее изображение, но покадровую ошибку и вспышки на переходе backend. Успех — измеримое сокращение дорогих rays без систематического исчезновения света/occluders.
2. Radiance Cache: кэшировать дальнее освещение по запросу и распределять обновления на GPU
Что делает UE. Потребитель помечает восемь соседних probes, необходимых для интерполяции: LumenRadianceCacheMarkCommon.ush:111. Предыдущие probes сопоставляются с новым clipmap, используются повторно либо освобождаются; новые выделяются из пула. Это demand-driven cache, а не обязательная трассировка всей регулярной 3D-сетки каждый кадр. Сопоставление: LumenRadianceCacheUpdate.usf:77; allocation: 289–333.
Самая интересная отдельная технология — гистограмма приоритетов вместо сортировки всех probes. Новые probes получают bucket 0; остальные — логарифмический bucket по времени между последним trace/use с поправкой на clipmap (GetPriorityBucketIndex, строка 221). Гистограмма суммирует trace-cost, а не только число probes (270–281). Один маленький проход выбирает порог по бюджету (399–425), последний bucket ограничивается atomic-счётчиком (455–463). Новые probes без данных имеют особый путь: они обновляются, а превышающие бюджет могут трассироваться с меньшим разрешением (466–477). Поэтому нельзя обещать математически жёсткий лимит общего GPU-времени; это бюджет вычислительной работы с fallback для заполнения пустого кэша.
Cache используется после локальной видимости. GetRadianceCacheCoverage задаёт минимальную дистанцию до интерполяции как probe TMin + cellSize * sqrt(3); контракт требует предварительно помечать позиции. LumenRadianceCacheInterpolation.ush:176. Screen probes сокращают дальнюю трассировку до этой границы и затем применяют кэш: LumenScreenProbeTracing.usf:754, применение — 825–835. Переносимая идея — переиспользовать дальнее низкочастотное освещение, сохранив локальную occlusion отдельно.
Минимум для своего движка. Одна сетка вокруг камеры, radiance+depth на probe, pool+indirection, last-used/last-updated, 8–16 priority buckets и фиксированный ray budget; добавить clipmaps после работающего proof-of-concept. Начать с diffuse GI и rough reflections. Новым probes дать начальное приближение и отдельный режим заполнения. Отдельно показывать age, residency, miss rate и trace cost; без этих debug views сложно отличать неправильную трассировку от устаревшего кэша.
Риски и проверка. Разреженные probes внутри стен, резкие перемены освещения, teleport и camera cut, забитый pool, clipmap seams. Предлагаемые benchmark-сцены: две комнаты с тонкой стеной; открывающаяся дверь; включение мощного emissive; телепорт на новую площадку. Считать число новых/повторно использованных probes, память, worst-frame cost и число кадров до восстановления заданного уровня ошибки против offline reference. Не принимать красивый статичный кадр как достаточную проверку.
3. Surface Cache: отделить стоимость материала и освещения поверхности от каждого ray hit
Что делает UE. Card capture сохраняет diffuse color, card-space normal и emissive в atlas: LumenCardBasePass.ush:134, outputs 145–147. На hit выбираются подходящие cards по направлению normal и bounds, проецируется позиция, проверяется совпадение card-depth, затем читаются Direct/Indirect/FinalLighting atlases. Реальные тела: LumenSurfaceCacheSampling.ush:235, depth weights — 279–310, atlas reads — 312–314; selection — 464–525. Нормализованная radiance возвращается в EvaluateRayHitFromCardSampleAccumulator (578–592).
Связь с HWRT явная: CalculateSurfaceCacheLighting по scene instance получает mesh-cards index и вызывает lookup, используя размер ray cone для радиуса фильтрации. Слишком близкие hits обнуляются для предотвращения self-intersection/GI feedback: LumenHardwareRayTracingCommon.ush:647. Таким образом точность ray intersection и стоимость hit lighting можно выбирать раздельно. Не следует утверждать, что весь Lumen всегда использует только это: есть отдельные hit-lighting пути.
Кэш обслуживается постепенно. Раздельные бюджеты direct/indirect; приоритет страницы учитывает возраст, расстояние до камеры, близость frustum и high-res feedback: LumenSceneLighting.usf:85, приоритет — 105–177, гистограмма — 180–205, выбор budget cutoff — 210–233. Lookup также пишет feedback и last-used page metadata: LumenSurfaceCacheSampling.ush:594–618.
Минимум для своего движка. Не воспроизводить сразу automatic card generation и virtual paging. Для статической сцены можно начать с ограниченного набора planar captures или существующей UV-развёртки lightmap: baked material attributes + динамический low-resolution lighting atlas + validity. Трассировать существующим BVH и возвращать atlas radiance. Это адаптация принципа, не описание UE. Сначала только diffuse secondary lighting; зеркальные отражения требуют другого качества.
Цена и тест. Инфраструктура ощутимая: parameterization/capture, atlas allocation, hit→surface mapping, invalidation для transform/material/light, обновление освещения. Thin geometry и вогнутые объекты дают coverage holes, view-dependent material нельзя точно представить одним nondirectional texel. Добавить debug invalid texels; UE тоже явно раскрашивает отсутствие данных (Sampling.ush:626–630). Тестировать материал с дорогой процедурной текстурой при росте secondary-ray count; сравнивать ray-hit shading ms/atlas-update ms/память. Отдельно проверять щели, окрашенный GI после движения объекта, смену emissive и старое освещение. Это более крупный проект, чем compaction или priority buckets.
4. MegaLights: фиксировать бюджет дорогой visibility, выбирать свет по вкладу и истории
Что делает UE. Для каждого shading location рассматриваются lights его clustered cell; GetLocalLightTargetPDF оценивает освещение через shading routine без реальной shadow visibility, включает IES и формирует perceptual weight log2(Lum + 1): MegaLightsLightTargetPDF.ush:23. FLightSampler хранит ограниченное число samples; AddLightSample потоково обновляет weighted selection и накопленную сумму: MegaLightsSampling.ush:47, тело — 84–114. При финализации sample weight становится WeightSum / selectedWeight: MegaLightsSampling.usf:544.
История видимых lights уменьшает вероятность выбора скрытого источника, но не обязана обнулять её; при invalid history используется другой множитель. Учитывается изменение мощности, чтобы вновь ставший значимым источник не оставался подавленным: MegaLightsSampling.usf:164, history multiplier — 203–210. Reprojected tile history и blue-noise offset — 325–387. Не называть эту конкретную цепочку автоматически «ReSTIR с temporal/spatial reservoir reuse»: здесь непосредственно подтверждены weighted sampling, visibility guiding и последующая фильтрация, не перенос reservoirs между pixels.
Важное ограничение: ограничено число выбранных expensive samples, не вся сложность независимо от количества lights. Кандидаты всё ещё обходятся циклом (cell header — 317–319; loop — 409–440). Поэтому density lights влияет на candidate evaluation, а при большом числе существенных вкладов возрастает variance.
Шум устраняется не просто TAA. Temporal denoiser использует shading confidence, neighborhood clamp отдельно для diffuse/specular и ограничивает число накопленных кадров; затем смешивает с alpha = 1 / accumulatedFrames: MegaLightsDenoiserTemporal.usf:379, clamp — 390–419, accumulation — 422–446. Spatial filter учитывает depth, normals и luminance, причём specular normal tolerance зависит от ширины lobe: MegaLightsDenoiserSpatial.usf:474.
Минимум для своего движка. Clustered light lists, 1–4 stochastic shadow samples на pixel/half-res pixel, weighted streaming sampler, shadow ray backend, correct probability compensation, temporal accumulation с rejection. Visibility history guiding добавлять после unbiased/reference baseline; затем filter. Начать с point/spot и rough opaque, area lights добавить следующим этапом. Не применять denoiser к ошибочной вероятности: он может скрыть bias.
Проверка. 10/100/1000 lights при фиксированном числе rays, но измерять отдельно candidate selection, visibility, shading, denoiser. Сопоставлять с exhaustive lighting/offline reference: яркий скрытый light, внезапное включение, движущаяся тень, glossy floor, history reset. Смотреть converged energy, variance, время восстановления после disocclusion и ghost trails. Реальный выигрыш — множество источников с предсказуемой стоимостью shadow queries; оплачивается шумом, памятью истории и сложностью устойчивого denoising.
Приоритет для отдельного renderer: сначала compaction/staged tracing и visibility-budget sampling, затем GPU cache scheduling; Surface Cache — только если измерения покажут, что повторный hit shading действительно главный bottleneck. Никаких численных обещаний ускорения без реализации и benchmark.
Продвинутая графика: VSM, virtual texturing, TSR
Исследован локальный ../../../UnrealEngine, commit 16d75d84714512edfb744e1fd0a59e9c74d57873 (UE 5.8.2). Ниже факты из прочитанных тел C++/HLSL-функций отделены от предложений для своего движка. Это статический разбор: UE не собирался, производительность на GPU не измерялась. Порядок внедрения зависит от исходного рендера: при отсутствии хорошего temporal pipeline начать с TSR-подобной реконструкции; при уже стабильном TAA и неудовлетворительных тенях — с VSM. VT окупается при реальной нехватке памяти на текстуры или дорогих материалах ландшафта.
1. Virtual Shadow Maps: кэшировать видимые страницы теней, отдельно статическую и динамическую часть
Что обнаружено. Главное заимствование — превращение shadow rendering в обслуживание разреженного кэша, востребованного пикселями камеры. В UE размер страницы 128×128, а начальная таблица содержит 128×128 страниц: виртуальная сторона получается 16384 текселя. Это следует из вычисляемых констант, а не означает физическую 16K-текстуру для каждого источника света. Константы VSM.
InitPositionData восстанавливает мировую позицию из глубины; GeneratePageFlagsFromPixels отмечает необходимые страницы directional lights через MarkPageDirectional. При этом marking предусматривает расширение на соседние страницы около границы, включая чередование диагонального направления. Это важно, потому что footprint фильтра тени может выйти за страницу, которую непосредственно запросил пиксель. Восстановление позиции, расширение запросов, запрос directional page.
В UpdatePhysicalPages уже существующие страницы переиспользуются, получают возраст и переносятся в список запрошенных. Ключевая деталь: если страница сейчас не нужна, её dirty/invalidation flags сохраняются до будущего возвращения в кадр. Иначе ушедший за камеру объект оставит устаревшую тень после возвращения камеры. Отдельные флаги позволяют обновить только динамическую часть, сохранив статическую. Управление возрастом и сохранение invalidation, раздельная инвалидация.
AllocateNewPageMappings берёт физическую страницу из available list, обязательно очищает прежнее отображение в page table, записывает новое и помечает обе части некэшированными. При переполнении пула запрос может остаться без физического backing: это реальное ограничение качества, которое должно быть видно в диагностике. SelectPagesToInitializeCS пропускает неизменённые страницы; InitializePhysicalPagesIndirectCS инициализирует обновляемую динамическую страницу сохранённой статикой. Выделение, пропуск cached pages, копирование статической основы.
Как перенять. Первый прототип: один directional light, фиксированный физический атлас, таблица virtual→physical, GPU bitset запросов, компактный список dirty pages, обычный depth raster для объектов, пересекающих страницы. Начать с одного уровня и твёрдых теней; затем добавить 3–5 clipmap уровней и привязку их координат к устойчивой сетке. UE также округляет центр каждого clipmap в пространстве света, что помогает сохранять кэш при движении камеры. Snap clipmap origin.
Nanite для самого принципа не требуется: в этом checkout существует полноценный RenderVirtualShadowMapsNonNanite; но это не обещание дешёвого рендера больших классических мешей, пересекающих много страниц. Путь обычной геометрии. Для своего варианта понадобятся стабильные instance IDs, world bounds, учёт изменения света/материалов/деформаций, compute atomics и корректные GPU barriers. Страницы надо инвалидировать по старому и новому положению переместившегося occluder — это предложение для архитектуры, а не утверждение о конкретной просмотренной функции UE.
Ловушки. Смена направления солнца способна разрушить почти весь выигрыш кэша; анимированная листва и displacement требуют явной политики. Статический и динамический depth pools увеличивают расход памяти. Camera-visible marking не покрывает автоматически все прозрачные или объёмные receivers. Не начинать со SMRT: в UE SMRTRayCast действительно шагает по shadow depth samples и экстраполирует глубину/наклон, но это отдельная апроксимация мягких теней с численной терпимостью, а не трассировка полной геометрии. SMRT.
Проверка прототипа. Сравнить кадр с режимом принудительного обновления всех страниц: статичная сцена после прогрева, локально движущийся объект, объект движется вне кадра и возвращается, camera teleport, вращение света, намеренно малый пул. Собирать requested/allocated/rendered/invalidated pages, промахи пула, долю повторно использованной статики, GPU ms marking/culling/raster/projection. Цель: после прогрева статичного вида обновления должны практически исчезнуть; выигрыш должен сохраняться в общих GPU ms, а не только в числе draw calls. Приоритет: высокий для крупной статичной сцены; сложность высокая.
2. Virtual Texturing: demand feedback, гарантированный грубый fallback и ограниченное обновление
Что обнаружено. VT содержит полезный законченный контур управления качеством: материал читает page table на нужном mip, формирует идентификатор страницы, разреженно пишет feedback; CPU обрабатывает готовый readback, объединяет запросы, обновляет LRU резидентных страниц и заказывает отсутствующие. FinalizeVirtualTextureFeedback пишет только выбранные пиксели и ограничивает длину буфера. CanMap/Map опрашивают fence и прекращают перебор на неготовом результате. Это позволяет строить поток без обязательного ожидания GPU в том же кадре. GPU feedback, page-table lookup, опрос fence.
В FeedbackAnalysisTask соседние одинаковые запросы объединяются со счётчиком. GatherRequestsTask быстро распознаёт резидентную страницу и обновляет её использование, не создавая лишний load. FTexturePagePool::Alloc берёт LRU-страницу из heap, удаляет старые mappings и назначает её новому producer. Объединение feedback, resident fast path, LRU allocation.
Наиболее ценная деталь — fallback является частью отображения страниц. UnmapPage ищет ближайшего резидентного предка и ставит в очередь update page table на более грубую страницу. Грубый корень ожидается закреплённым в памяти. Шейдер использует фактический mip из записи, вычисляет локальные UV, учитывает border и масштабирует градиенты. Поэтому eviction может снижать детализацию вместо появления пустой/чужой текстуры. Ancestor fallback, UV и derivatives.
Планировщик различает streaming pages и runtime-generated pages: SubmitThrottledRequests расходует отдельные бюджеты, так как асинхронный I/O и генерация GPU-материала имеют разный профиль стоимости. Оставшийся бюджет может идти на continuous updates. SubmitRequests различает Pending/Available и ограничивает фактическое production. Бюджеты SVT/RVT, готовность producer.
Как перенять. Практичный старт — один большой terrain albedo atlas: нарезанные offline mip tiles с border, небольшой physical atlas, page table, закреплённый самый грубый mip, feedback readback ring, CPU дедупликация, асинхронное чтение и upload budget. Добавить normal/roughness только после проверки согласованного residency нескольких слоёв. Система producer должна отвечать «pending/ready», а публикация нового mapping должна происходить после готовности данных. Для своего движка полезно ограничивать не только tiles/frame, но и bytes/frame и время генерации: одинаковое количество страниц не означает одинаковую стоимость.
RVT — следующий самостоятельный эксперимент: bake нескольких дорогих слоёв ландшафта/декалей в востребованные страницы и затем дешёвая выборка. Этот механизм уменьшает повторную работу материалов, но требует региональной invalidation после изменений и контроля устаревших страниц. Он не нужен игре с небольшим обычным набором текстур: там цена dependent texture fetch, feedback и авторского tooling может превысить выигрыш памяти.
Проверка прототипа. Teleport через terrain при пуле существенно меньше рабочего набора; медленное вращение, быстрый полёт, высокий anisotropy, переход mip и граница тайлов, намеренная задержка I/O. Условие корректности: ни одного sampling из перераспределённой чужой страницы, fallback всегда валиден, отсутствие seams. Измерять полезные загруженные bytes, residency hit rate, латентность запроса до показа, долю показа грубого mip, повторные загрузки/секунду, CPU feedback cost, GPU стоимость выборки. Приоритет: условно высокий для больших материалов/мира; средний либо низкий без давления на VRAM.
3. TSR: хранить доверие к истории и отдельно распознавать движение, disocclusion и мерцание
Что обнаружено. Самое переносимое здесь — структура информации, которую temporal reconstruction сохраняет между кадрами. В DilateVelocityCS UE читает 3×3 depth/velocity neighborhood, выбирает ближайшую глубину, вычисляет движение от этой поверхности и ошибки репроекции. Это уменьшает неправильное использование фонового motion vector на границе объекта. Одновременно существуют флаги pixel animation и temporal responsiveness. Depth-aware velocity dilation.
RejectShadingCS формирует отдельно разрешение ослабить clamp и множитель доверия; при disocclusion без resurrection оба обнуляются. UpdateHistoryCS clamp-ит цвет истории по текущему окружению, ограничивает её вес по скорости движения, затем смешивает с текущим сигналом через HDR-aware weights. Вместе с цветом записывается validity, специально квантованная под 8-битное хранение. То есть история — это цвет плюс информация, насколько на него можно опираться, а не постоянный коэффициент lerp. Выход rejection, clamp, движение, blend и validity.
Особенно интересен ComputeMoireError: он следит за сменой знака временного градиента яркости, накапливает вариацию и счётчик, сбрасывает их при disocclusion и подавляет послабление для движущихся/изменённых пикселей. Затем оценка мерцания расширяет допустимый интервал истории в MeasureRejection. Так алгоритм разрешает дополнительное усреднение там, где jitter заставляет статичную мелкую деталь чередоваться, но старается не размазать настоящую анимацию. Детектор мерцания, расширение допустимого диапазона.
UE также допускает history размером от 100% до 200% выходной стороны и отдельный resolve. Это не бесплатная резкость: удвоение обеих сторон даёт в четыре раза больше текселей соответствующих history buffers. Размер истории, resolve.
Как перенять. Сделать собственный минимальный temporal upscaler: jitter; цвет, depth, корректные motion vectors для камеры/объектов/skinning; 3×3 dilation; reprojection; depth disocclusion; цветовой neighborhood clamp; отдельный validity buffer; spatial fallback для раскрытых областей. Начать с history в выходном разрешении, стабильной экспозиции и opaque геометрии. Затем добавить коррекцию экспозиции, responsive mask для анимированных материалов/частиц и простую temporal luminance статистику. Память предыдущих деформированных вершин и дисциплина jitter conventions зачастую важнее очередного фильтра в конце пайплайна.
Не переносить сразу весь набор TSR permutations, half-precision tensor abstractions, resurrection, lens distortion и анализ тонкой геометрии. Это связанные оптимизации промышленной реализации, которые делают маленький прототип существенно сложнее. При корректном базовом upscaler можно отдельно проверить историю повышенного разрешения и antialiasing для тонких линий. Детектор мерцания принципиально допускает компромисс стабильности против ghosting: требуется управляемое ограничение его действия, а не глобальное увеличение history weight.
Проверка прототипа. Зафиксировать воспроизводимые sequence: решётка/провода в статике; медленный pan; тонкий движущийся объект перед контрастным фоном; раскрытие фона; emissive flicker; листва; particles; резкий exposure jump; camera cut; переключение render scale. Сравнивать с высокоразрешённой эталонной последовательностью и простым TAA baseline. Помимо GPU ms и bytes истории нужны temporal variance в статике, ошибка после disocclusion, длина ghost trail и сохранение контраста тонких деталей. Нельзя принимать результат только по красивому неподвижному кадру. Приоритет: высокий и наиболее универсальный из трёх; минимальный вариант средней сложности, достижение качества UE — большой отдельный проект.
Общая архитектурная идея всех трёх подсистем: тратить вычисления на востребованную часть сигнала, явно хранить состояние и уверенность кэша, проектировать отказ/устаревание как нормальный режим и визуализировать причины повторной работы. Сначала вывести в debug view pages/invalidations, fallback mip и history validity — это часть самой технологии, а не косметика после реализации.
Дополнительные механизмы: RDG, Substrate, glints
Render Dependency Graph — инфраструктура сложного рендерера
В исходниках. API требует описывать используемые ресурсы в параметрах прохода; граф выводит барьеры и время жизни: RenderGraphBuilder.h:45. Это подтверждается реализацией, а не только комментарием API: RenderGraphBuilder.cpp:1310 обходит producers от нужных выходов и снимает флаг culling; :1327 компилирует граф; :1383 описывает и обрабатывает недостижимые проходы; :1426 объединяет совместимые raster passes. В :1562 строятся fork/join для async compute, причем при отключенном async transient aliasing времена жизни расширяются на область параллельного исполнения (:1590, :1646). :3217 передает fence-ограничения transient allocator при создании и освобождении ресурсов; :3783 — компиляция барьеров.
Идея для собственного движка. Объявление pass → reads/writes → output позволяет безопасно добавлять HZB, denoise, shadow pages, history buffers. Начать с одной GPU queue, явных read/write и imported/exported ресурсов и отладочного просмотра графа. Удаление ненужных проходов и lifetime-based reuse ресурсов добавить после корректного базового пути; только потом aliasing физической памяти и async compute.
Ловушка. Два ресурса, стоящие последовательно в CPU-списке, могут одновременно использоваться GPU на разных очередях. Декларировать ресурсы следует на уровне mip/subresource, либо консервативно считать весь ресурс одним состоянием. History resources живут между кадрами и должны быть external/persistent. Экспорт и побочные эффекты — roots графа. Нельзя обещать, что сам граф ускорит каждый кадр: он имеет CPU overhead.
Проверка прототипа. После отключения эффекта все изолированные предшественники исчезают из GPU capture; attachment/compute/readback цепочка работает с validation; повторное использование памяти не меняет изображение; отдельно измерять compile CPU time, GPU pass times и peak transient bytes. При нескольких frames in flight завершение CPU кадра не означает возможность переиспользовать память.
Substrate — ограниченная сложность материала + специализированные тайлы
В исходниках. SubstrateTranslatorCommon.cpp:687 получает бюджет bytes/closures per pixel. В :695 при выходе за бюджет компилятор выбирает глубокий оператор для parameter blending; в :705 после исчерпания этого упрощения отключает optional features; :1319 проверяет сразу лимиты памяти и closures; :1433 повторяет цикл до выполнения бюджета. Это компиляционное упрощение, не измеритель GPU времени и не автоматический runtime LOD по дистанции.
SubstrateMaterialClassification.usf:172 читает компактный заголовок материала; :178 различает simple/single/complex/special; :358 пишет отдельные списки тайлов и indirect counters; :443 конвертирует их в dispatch args. SubstrateDeferredLighting.ush:112 перебирает closures и распаковывает BSDF. C++ wiring: Substrate.cpp:1746 выбирает permutation, :1774 — tile size 8 или 16, :1785 — indirect args.
Что перенять. Для начала PBR + clear coat как два фиксированных класса, компактные feature flags, экранные тайлы simple/complex и специализированные lighting shaders. Следующий шаг — максимум 2–3 closures и явный бюджет material buffer. Самостоятельно спроектировать предупреждения authoring: что было упрощено и почему. Полный граф произвольных физических слоев — отдельная большая задача.
Ловушки. Если complex-пиксели равномерно рассыпаны по экрану, сложными становятся почти все тайлы. Классификация стоит GPU времени и окупается не всегда. Несколько closures увеличивают расходы не только direct lighting, но и GI/reflections и историю. Влажность/лак можно показать обычным clear-coat BRDF, не строя весь Substrate.
Проверка. Сравнить одинаковое изображение с универсальным shader и classified shaders на сценах: только simple, локальные complex, шахматное смешение; измерять classification + lighting целиком. Отдельно проверять material compiler, что лимит bytes/closures соблюдается и упрощение видно художнику.
Glints — относительно изолированный эффект для материалов
В исходниках. GlintThirdParty.ush:13 называет исходную работу Chermain et al. 2021; :425 — f_P, который использует view/light direction и UV derivatives; :518 выбирает LOD по footprint; :543 оценивает распределения двух уровней; :553 смешивает их. В SubstrateEvaluation.ush:308 результат используется в Substrate_D_Glint, а :320 обеспечивает переход к GGX. GlintShadingLUTs.cpp:122 получает LUT texture, :140 выбирает словарь, :156 задает его параметры.
Что это дает. Материал с различимыми микробликами — например, краска с частицами, блестки или снег — вместо только гладкого среднего блика. Ключевая переносимая идея: фильтровать распределение микрофасеток с учетом footprint и масштаба, а не просто добавлять случайные яркие точки.
Прототип. Один opaque-материал, tangent basis, UV derivatives, lookup-словарь, один источник света. Обособить glint BRDF от общей системы слоев; затем добавить normal-map filtering, area lights и environment lighting. По охвату архитектуры это существенно меньше Nanite/Lumen, но математика фильтрации все равно нетривиальна.
Проверка. Плавно двигать камеру и свет, менять roughness/плотность, уменьшать объект до субпиксельного размера; проверять мерцание, устойчивость энергии и переход к обычному specular. Сравнивать с supersampled reference, измерять добавочное GPU время. Стабильность не доказывается одним скриншотом.
Публичный первоисточник. https://xavierchermain.github.io/glint_anti_aliasing/ — статья, видео и ссылка на оригинальный код. Не делать вывода о лицензии всей интеграции UE из лицензии исходной исследовательской реализации.
Публичные первоисточники для дальнейшего изучения
Результаты выше основаны на локальной ревизии. Публичные материалы помогают восстановить замысел и найти самостоятельные алгоритмические описания:
- Nanite: A Deep Dive, SIGGRAPH 2021 — оригинальный дизайн. Ограничения ранней версии из доклада не следует автоматически переносить на UE 5.8.2.
- Lumen, SIGGRAPH 2022 — устройства представлений сцены и переиспользования освещения.
- MegaLights, документация Epic — авторы отдельно отмечают влияние пересекающихся lights на выбор кандидатов и компромиссы denoising; это согласуется с просмотренными циклами shader-кода.
- MegaLights: Stochastic Direct Lighting, SIGGRAPH 2025 — материалы авторов об архитектуре прямого света.
- Geometric Glint Anti-Aliasing, страница авторов — описание фильтрации микробликов, публикация, видео и оригинальный исследовательский код.
Полный перенос исходников модулей здесь не предлагается: описаны механизмы и собственные минимальные реализации. Выбор технологий и их эффективность нужно подтвердить на будущих целевых сценах и GPU.