Шел уже ХХ месяц как я без работы все еще ковыряю свой свой Vulkan-рендер для движка X-Ray OGSR когда я начинал делать мод я чуть меньше рабирался в втом как проще) и начитавшись про плюсы forward и так как мне хотелось живого цикла дня и ночи чтоб солнце медленно ползло по небу , а тени так же медленно ползли по стенам начал делать его .

Сразу честно, потому что это важно для всей истории. Классический рендер STALKER — это R4, deferred. У него тени уже неплохие и плавные задача нужно сделать не хуже как минимум

Но я делал свой рендер с нуля и не сознательно пошёл в forward. Не потому что это «правильнее» — а потому что хотелось наворачивать технологии, экспериментировать, и forward казался более гибким полем для этого. О том, что именно forward усложнит работу с тенями (и не только с ними), я тогда не до конца думал.
В deferred тень от солнца считается один раз — в проходе освещения, по G-буферу. В forward её приходится сэмплить в каждом шейдере геометрии. А у меня их шесть: terrain, lmap, vlit, скелетка (NPC), деревья, трава так что как только я доделал тени я понял что я проигрываю по производительности ощутимо , что делать попробовал отсекать лишнии тени , делать лоды для теней и все равно до произодительности класческого рендера не дотягивал процентов 20% что дальше? можно было бы конечно сказать включайте апскейлеры которые я доделаю и будет вам счастье но это не путь воина ) попробовал сделать кешировние, и да фпс сразу улетел в х2,5 от класического р4 но неприятный момент всё сломалось: кеш дал тик (тени замирают и скачком перерисовываются при движении солнца).

Если что речь в осноном про такие тени
Если что речь в осноном про такие тени



Но тут я понял что вот это то куда нужно копать чтоб сделать круто но как сделать и кеширование и динамику одноврменно на одном экране ?

Небольшое отсупление раз уж зашла речь про forward то , разберёмся честно: что это вообще за выбор и почему я о нём не пожалел (хотя поплатился).

Коротко про оба подхода

Deferred (как R4). Сцена сначала рисуется в «толстый» G-буфер: для каждого пикселя экрана сохраняются нормаль, альбедо, глубина, спекуляр и т.д. Потом отдельный проход освещения проходит по этому буферу и считает свет один раз на пиксель.

А я сделал именно наивный. В первой версии мой рендер тестировал 16 динамических источников света против КАЖДОГО фрагмента вообще без отбраковки. Каждый пиксель честно прогонял 16 проверок дальности и 16 затуханий — хотя реально на него влияли 1–3 лампы. В deferred такой проблемы нет by design: там свет считается один раз. Вот он, структурный разрыв с R4, который я создал себе сам.

После долгого активного гугления в лоб напрашивались два выхода:

1. Переписать в deferred. Похоронить весь forward-эксперимент, городить G-буфер. Не хотелось — это отказ от всего, ради чего я в forward и шёл.

2. Сделать forward умным. Это и есть Forward+ (clustered/tiled forward).

### Что такое Forward+ и что я для этого сделал

Идея Forward+: не проверять каждый пиксель против всех ламп, а заранее разложить источники света по ячейкам экрана (compute-проходом), чтобы каждый пиксель перебирал только те лампы, что реально достают до его ячейки. Это не уменьшение числа ламп — наоборот: становится дёшево поднять лимит и зажечь хоть сотни источников.

3D-сетка froxel'ов 16×9×24 = 3456 ячеек, разрешение-независимая (тайлы масштабируются). Срезы по глубине — экспоненциальные (схема Olsson/Doom: slice = log2(zview)*scale + bias), чтобы детализация ячеек шла по логарифму глубины, как и положено.
Один compute-диспатч, без атомиков. Один поток на froxel: строит свой AABB во view-пространстве, тестирует сферу каждого источника на пересечение (sphere-vs-AABB) и пишет индексы попавших ламп в свой фиксированный участок буфера ([ci*64 .. ci*64+64)) — участки не пересекаются, поэтому атомики не нужны.

Фрагмент вычисляет свою ячейку из gl_FragCoord + view-глубины и перебирает только её список. Лимит источников поднят с 16 до 256.

Отладочный хитмап (r_clustered_debug) — раскраска по числу ламп в ячейке: костёр = 1 синяя ячейка, гроздь ламп = красно-жёлтые, фонарик = синяя сфера вокруг игрока. Очень помогает убедиться, что биннинг корректен, до того как доверять картинке.

Корректность. Кластерный путь — это надмножество правильных ламп (свободный AABB даёт пару ложных попаданий, их добивает затухание). Поэтому при ≤16 лампах картинка обязана быть идентична старому пути, просто быстрее. Так и вышло: A/B в игре показал, что r_clustered 1 и r_clustered 0 неотличимы — фонарик, костры, лампы светят одинаково.

Урок, который я вынес: forward — не «устаревший» выбор, а осознанный, как у Doom и UE5. Но он живёт только пока ты не наивен. Наивный forward проигрывает deferred структурно; Forward+ закрывает этот разрыв без переписывания в deferred. А раз так — ставка на forward оправдана, и можно навешивать на него всё остальное, включая VSM.

---

2. Почему каскады — это дорого (даже когда они плавные)

Стандартное решение для солнца — Cascaded Shadow Maps (CSM). Идея простая: близко к камере нужна высокая детализация тени, далеко — низкая. Поэтому делают несколько «каскадов» — вложенных областей разного размера, и в каждую рендерят свою карту теней. Именно так работает и R4.

И вот тут — важный момент, который я сначала недооценил. Каскады R4 плавные. Но плавные они не магией, а грубой силой: R4 перерисовывает карту теней каждый кадр с честной мировой привязкой решётки (world-anchored texel snap). Солнце сдвинулось на чуть-чуть — не беда, всё равно перерисовываем всё заново, просто с новым углом. Плавно — но дорого.

Я портировал изначально ровно этот подход. Два каскада 4096×4096 (25 м и 60 м вокруг камеры) плюс дальняя карта. Тени получились плавные и чёткие — но профайлер показал цену:

```

Shadow/Casc0 (ближний 4096²) = 3.0–3.15 мс

Shadow/Casc1 (2048²) ~ 0–1.1 мс

Shadow/CascCull (отбраковка) = 0.01 мс ← почти бесплатно!

```

Ближний каскад в одиночку стоил ~3.1 мс — это был самый дорогой проход во всём кадре, дороже, чем отрисовка всей основной сцены (~2.5 мс).

И ключевой момент: отбраковка (culling) — бесплатна (0.01 мс). Значит, дело не в том, что мы рисуем «лишние» объекты. Дело в самой растеризации: каждый кадр мы заново прогоняем всю геометрию через GPU, чтобы получить ту же самую карту глубины — хотя 99% сцены за этот кадр вообще не изменилось.

Вот это «перерисовываем одно и то же каждый кадр» — и есть главная боль. Из неё растёт всё остальное.

---

## 3. Первая идея: а давайте закешируем каскад

Логика очевидная: если сцена статична и солнце не движется — зачем перерисовывать тень? Давайте отрендерим её один раз и будем переиспользовать, а перерисовывать только когда что-то реально поменялось.

Я так и сделал — добавил кеш каскада (r_shadow_casc_cache): карта теней замораживается и перерисовывается только при достаточном смещении камеры или солнца.

Для статичной сцены это сработало великолепно. Стоишь на месте — стоимость каскада падает почти в ноль.

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

- Порог инвалидации стоит, например, на 0.05° смещения солнца.

- Солнце ползёт. Накапливается 0.05° — кеш сбрасывается — вся карта 4096² перерисовывается одним кадром.

- Casc0 подскакивает с ~0 до 3.3 мс в этот кадр.

- Глаз видит: тень замерла → дёрнулась → замерла → дёрнулась.

Вот тут стоит остановиться и признать иронию: смоотность, которая в R4 была из коробки, я сломал собственными руками — ровно в тот момент, когда полез её оптимизировать. R4 платил полную цену каждый кадр и был плавным; я попытался не платить — и получил тик. Это и есть тик. И тут — главное прозрение всей истории:

> Кеш «всё или ничего» не может дать плавное движущееся солнце. Плавность принципиально требует перепроецирования каждый кадр. А значит, надо не выбирать между «перерисовать всё» и «не перерисовывать», а перерисовывать только то, что реально изменилось и реально видно — мелкими кусочками, каждый кадр.

Можно понизить порог инвалидации до микроскопического — но тогда мы перерисовываем 4096² почти каждый кадр и возвращаемся к исходной цене. Тупик.

Проблема не в кешировании. Проблема в гранулярности. Каскад — это монолит. А нужен механизм, который работает на уровне маленьких кусочков карты теней. Именно это и делает Virtual Shadow Maps.

---

## 4. Что такое Virtual Shadow Maps

VSM придумали в Epic для Unreal Engine 5. Базовая идея:

1. Представляем гигантскую виртуальную карту теней — настолько детальную, что на экране каждый пиксель тени получает примерно один тексель карты. У меня это клипмапа из 6 уровней, виртуальное разрешение 4096² на уровень.

2. Эту виртуальную карту режем на страницы (pages) 128×128 текселей. Всего получается 6144 страницы.

3. Физической памяти под всю виртуальную карту нет и не надо. Каждый кадр мы спрашиваем: какие страницы вообще видны хоть одному пикселю на экране? Только под них выделяем реальную память.

4. Рендерим тень только в нужные страницы. И — главное для нашей задачи — кешируем их в мировом пространстве: страница, привязанная к точке мира, остаётся валидной между кадрами, пока её содержимое не изменилось.

Что это даёт для движущегося солнца? Солнце сдвинулось → инвалидируется и перерисовывается не вся карта, а только видимые страницы, причём размазанно во времени (round-robin: ~1/N страниц за кадр). Стоишь, солнце замерло → перерисовывается ноль страниц. Плавно И дёшево.

> Важная честность. В UE5 VSM создавался под Nanite — там геометрия рендерится прямо в страницы почти бесплатно. У меня нет ни Nanite, ни mesh-шейдеров (целевая аудитория STALKER — слабое железо, а mesh-шейдеры это NVIDIA Turing+/AMD RDNA2+). Поэтому путь рендера в страницы у меня — на compute-биннинге, и это, по меркам Epic, их официальный медленный путь. Я не пытаюсь обогнать UE5+Nanite. Моя цель скромнее и конкретнее: плавное живое солнце по цене каскада на слабом железе.

---

5. Как это легло на forward-рендер X-Ray

Большинство материалов про VSM — про deferred-рендер (UE5 — deferred). У меня forward, да ещё и в движке 2007 года. Перенос был нетривиальным. Пайплайн VSM за кадр у меня выглядит так:

MARK — какие страницы видны.

Compute-проход по буферу глубины: для каждого видимого пикселя реконструирую мировую позицию → определяю уровень клипмапы и страницу → atomicOr в битмаску «нужных» страниц. Off-screen и за спиной игрока страницы-приёмники не отмечаются вообще — это бесплатный выигрыш против каскада, который рисует полный ортобокс на 360° вокруг камеры независимо от того, куда смотришь.

Forward-бонус. MARK'у нужен буфер глубины сцены — и он у меня уже был. В forward-рендере я и так делаю depth prepass (отдельный ранний проход, рисующий только глубину), чтобы early-Z убивал переотрисовку в дорогом цветовом проходе. Этот prepass я добавлял по чисто forward-причине — борьба с овердро освещения. А VSM достался он бесплатно: тот же буфер глубины, который спасает forward от овердро, оказался ровно тем, что нужно для разметки страниц. Редкий случай, когда forward-налог окупился сам собой.

ALLOC — выделить физику.

Один поток на виртуальную страницу: если страница нужна — атомарно резервируем физический слот в атласе, пишем pageTable[virtual] → slot.

BIN — кто отбрасывает тень в какую страницу.

Для каждого кастера (объекта-тенеотбрасывателя) ищем, в какие резидентные страницы попадает его проекция в свете солнца, и формируем списки + indirect-команды отрисовки. Это и есть «наш Nanite» на compute.

RENDER — рисуем только грязные страницы.

Растеризуем геометрию только в те страницы, что изменились (loadOp = LOAD, очистка только грязных). Кешированные страницы не трогаем.

RESOLVE — собираем экранную маску.

Тут архитектурный сдвиг: вместо того чтобы каждый шейдер-приёмник лез в атлас, отдельный compute-проход разрешает тень для всего экрана в screen-space маску (RGBA16F) и применяет к ней temporal-сглаживание. После этого каждый из 6 шейдеров-приёмников (terrain / lmap / vlit / skinned / tree / grass) делает буквально одну строчку: texture(uVsmMask, screenUV).r.

Forward-бонус (тот самый возврат долга). Помните, во вступлении forward отомстил тем, что тень надо сэмплить в шести шейдерах вместо одного deferred-прохода? Вот здесь я долг и вернул. RESOLVE считает тень один раз на весь экран — ровно как deferred-проход освещения по G-буферу. Шесть шейдеров после этого не считают тень, а просто читают готовую маску одной строкой. По сути я воспроизвёл deferred-удобство точечно для теней, не заводя G-буфер.

Бонусом — temporal-сглаживание тут делается без motion vectors, которых у моего forward-рендера нет этого пока что нет ) . Репроекция истории идёт через глубину + матрицу прошлого кадра (prevViewProj), а не через буфер скоростей. (Кстати, ★самый дорогой баг всего VSM был именно тут: «дрожащие шафты» оказались не из-за теней, а из-за того, что temporal репроецировал дрожащий от джиттера мир; лечится репроекцией нежиттеренного центра, а джиттерится только сэмпл.)

Мировая привязка (world-anchored) — фундамент всего кеша. Матрицу вида солнца я строю с «глазом» в начале координат, а не на камере. Тогда XY в световом пространстве не зависит от камеры → страницы привязаны к точкам мира → их можно кешировать между кадрами. Резидентность — тороидальная, вообще без хеш-таблиц и free-list:

Модуло само работает как вытеснение — коллизий нет, пока окно 32 страницы шириной. Дёшево и сердито.

Атлас разделён на статический (статичная геометрия + деревья) и динамический (NPC-скелетка + трава): статика кешируется, динамика перерисовывается. Resolve сэмплит оба и берёт min(глубин) — результат идентичен одному общему атласу.

---

6. Война за производительность: с +6 мс до +0.5 мс

Когда VSM впервые заработал «честно» (всё перерисовываем каждый кадр), он был на ~6 мс тяжелее каскада. Это приговор — никто не включит фичу, которая роняет fps в полтора раза. Дальше — список оптимизаций, каждая со своим «зачем» и «на сколько».

### 6.1. Перестать платить дважды за каскад (−2.7 мс)

Первый и самый стыдный баг: при включённом VSM каскад продолжал рендериться каждый кадр. Приёмники читали VSM-маску, а старый каскадный проход всё равно молотил впустую. Добавил гейт vsmActive вокруг растеризации солнца:

```

SunShadow: 3.29 мс → 0.55 мс

```

Минус 2.7 мс просто за то, что перестал делать двойную работу.

> Forward-налог: оружие в руках. Один читатель карт солнца всё-таки остался — модель оружия в руках (HUD weapon). И это чисто forward-специфичная заноза. В forward оружие рендерится тем же пайплайном, что и мир, но в своей искажённой проекции (его «мировые» координаты — фейковые, чтобы ствол не упирался в стены и красиво держался у камеры). Из-за этого screen-space VSM-маска для него считалась бы неправильно — её мировая реконструкция к фейковым координатам оружия не применима. Поэтому оружие я оставил на старом каскадном сэмплинге, а под VSM оно получает слегка устаревший «замороженный» каскад — на глаз незаметно. В deferred такого спецслучая, скорее всего, не возникло бы: там тень читается из общего экранного результата, а не пересчитывается из координат меша. Мелочь, но показательная — forward постоянно подсовывает такие частные случаи там, где deferred обходится одним общим путём.

6.2. Барьер depth → COMPUTE

Resolve сэмплит глубину сцены и атлас в compute, а стандартный барьер движка переводил depth в SHADER_READ только для fragment-стадии. Пришлось руками прописать dstStage = FRAGMENT | COMPUTE. Классическая Vulkan-засада, которую находишь только через валидационные слои и чёрный экран.

### 6.3. Деревья: из динамики в статический кеш (−3 мс)

Деревья у меня не качаются на ветру (пока), то есть геометрически статичны — но рендерились в динамический атлас каждый кадр. Чистая потеря. Перевёл их в статический кешируемый атлас с фильтром по грязным страницам:

```

VSMrender (пик при движении): 5.40 мс → 2.41 мс

Стоишь на месте: грязных страниц 1–2 из 6144

```

6.4. Деревья: один multi-draw на группу вместо 6288 вызовов (fps 167 → 250)

Оказалось, кадр был CPU-bound на записи команд отрисовки: 6288 отдельных vkCmdDrawIndexedIndirect, по одному на дерево. Заменил на один vkCmdDrawIndexedIndirect с drawCount на всю группу (деревья группы лежат подряд в indirect-буфере). Пустые деревья просто имеют instanceCount = 0 — на GPU бесплатно.

```

CPU: 6 мс → 4–5 мс

FPS: 167 → 200–250 ← самый большой скачок, CPU был стеной

```

Урок: на старом движке узкое место часто не там, где красивая математика, а в банальной записи тысяч драв-коллов.

### 6.5. Отбраковка теней NPC по дистанции

Сборщик кастеров-скелеток не имел вообще никакого culling — скинил и бинил всех NPC независимо от расстояния, упираясь в кап в 256 «листьев». Добавил тест по дистанции (r_vsm_npc_dist, по умолчанию 50 м). В STALKER в кадре обычно 1–5 NPC, так что это почти всегда чистый выигрыш.

### 6.6. Caster-LOD в биннинге (оказалось — мимо)

Попробовал выбирать грубый LOD кастера для далёких объектов прямо в vsm_bin. Маргинально — в пределах шума. Ровно тот же урок, что и с каскадом: ближние кастеры в грязных страницах не «далёкие», а у дальних LOD-0 уже мелкий. Оставил как опцию для открытых карт (r_vsm_lod_dist, по умолчанию 0 = выкл).

### 6.7. Half-res MARK (−0.5 мс)

Проход MARK можно гонять в полразрешения: один поток на блок 2×2. Соседние пиксели всё равно ложатся в одну страницу, так что «промах» 1 из 4 почти не случается, а потоков и атомиков — вчетверо меньше.

```

VSMmark: 1.06 мс → 0.53 мс

```

### 6.8. Вернуть копии каскада под VSM

Под VSM никто не сэмплит собранные карты солнца (маска рулит всем). Завернул vkCmdCopyImage каскадов в if (!vsmActive) — ещё ~0.4 мс.

Итог войны

A/B на одной локации, движущееся солнце:

```

Каскад VSM

gpu_total ~6.4 мс ~6.9 мс → +0.5 мс «в нагрузке»

calm (стоишь) ~6 мс ~5 мс → VSM ДЕШЕВЛЕ

Casc0 на redraw ПИК 3.3 мс нет пика → это и есть «тик»

```

VSM стоит +0.5 мс в худшем случае, дешевле в спокойном кадре, и главное — он плавный там, где кешированый каскад дёргается. Разрыв в 6 мс схлопнулся в полмиллисекунды.

7. «Выглядело как баг VSM, но это был не VSM»

Самое весёлое в графике — это артефакты. Несколько детективных историй.

Реконструкция мира через аффинную инверсию. Все реконструированные точки схлопывались в уровень L0 клипмапы. Причина: в X-Ray Fmatrix::invert() — это аффинная инверсия 4×3, она даёт мусор для проективной viewProj. Нужна была invert_44. Симптом-подсказка: гистограмма страниц по уровням — L1..L5 все по нулям, всё в L0. (Стоило цикла отладки.)

Полоски на стыках страниц. Билинейная фильтрация подтягивала глубину из соседней страницы атласа → сетка швов. Лечится half-texel inset (вставкой на полтекселя внутрь страницы).

Краулинг тени при вращении солнца. Решётка теней «ползла», пока солнце поворачивалось. Лечится мировой привязкой фазы решётки к точке мира рядом с камерой (R4-трюк world-anchored texel snap) — световое пространство вращается, а тексель якоря стоит на месте.

Мораль раздела: при работе с тенями половина времени уходит не на алгоритм, а на то, чтобы понять, какой именно из десятка взаимодействующих механизмов даёт рябь.

8. Что дальше

Две «конфетки» в работе:

1. Shadow-HZB (перф-гем). Двухфазная occlusion-отбраковка кастеров против HZB, построенного из атласа прошлого кадра (тороидальный кеш делает его практически бесплатным источником). Цель — добить главную оставшуюся стоимость (overdraw при растеризации в грязные страницы) и сделать так, чтобы VSM обгонял каскад даже в нагрузке — тогда дефолт можно будет честно переключить.

2. SMRT-lite (визуальный гем). Заменить 3×3 PCF в resolve на трассировку лучей по карте теней (как в UE5 SMRT, но без RT-железа — просто марш по глубине атласа): мягкие контактные тени, которые ужесточаются вблизи объекта и размываются вдали. AAA-картинка на DX11-классе железа.

Заключение

Я начал с простого желания — чтобы солнце красиво садилось. Это привело меня через тупик кеширования каскада к Virtual Shadow Maps — и к пониманию, почему Epic вообще это придумали. Главный вывод не технический, а инженерный:

Когда фича упирается в «дорого перерисовывать всё каждый кадр», ответ редко в том, чтобы «перерисовывать реже». Чаще — в том, чтобы измельчить гранулярность и перерисовывать только то, что правда изменилось.

А ещё это вся история — про цену осознанно выбранного forward. Я насчитал по ходу свои налоги (тень надо сэмплить в шести шейдерах, спецслучай оружия в руках, наивный свет в 16 ламп на пиксель) и свои бонусы (готовый depth prepass, лёгкость, гибкость, дружба со сглаживанием). И главное — каждый налог закрывается умным решением, а не капитуляцией в deferred: по свету ответил Forward+, по теням — resolve-маска (вернувшая forward'у deferred-удобство) и VSM. Forward — это не «бесплатно проще», это «дороже, но управляемо, если не лениться».

VSM в forward+ рендере старого движка то, чего каскад не даёт в принципе: солнце, которое движется плавно и множество истоничнок света в сцене которые подругому невоможны ,

ну и немного скриншотов того как сейчс выглядит рендер , про туман ао и пом расскау может потом

ну так чисто эксперемент динамическая смена сезона )
ну так чисто эксперемент динамическая смена сезона )

видосы буду открыто загружать сюда https://boosty.to/babaiiia как и ссылки

PSS на данный момент производительность кратно больше или равна класическу р4 при условивии запуска того с похожими настройками без урезания , но есть еще огромный кусок куда можно оптимизировать а так же никто не отменял DLSS , FSR , FG итд