Files
airdef/docs/HD-RENDERER.md
T
2026-10-29 18:38:20 +03:00

244 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HD-рендер AirDef: устройство, статус, план
Документ для передачи работы (человеку или агенту). Ведётся по ходу разработки: каждый этап, попавший в
`main`, отмечается здесь вместе с коммитом. Если что-то в коде расходится с этим текстом — прав код, а текст
надо поправить.
## Что это
HD — третий пресет графики мода (`/airdef shader off|on|hd`): отложенное освещение в стиле шейдерпаков
(BSL/Complementary-класс), но написанное с нуля внутри нашего форка Horizon (Minecraft 26.3 на Rust/wgpu).
Код шейдерпаков (BSL, Complementary, Iris/OptiFine и прочих) **не копируем**: их лицензии запрещают перенос, а в
Horizon действует правило clean-room. Исключение — Photon (sixthsurge), коммит 26c2906ee02cc484359b835fba6993d443d45966: он под MIT, из него можно
переносить идеи и код **с указанием авторства** (комментарий у кода + `gfx/THIRD_PARTY_PHOTON.md`). Остальное пишем по знанию техник (статьи, общеизвестные алгоритмы).
Пресеты должны оставаться: `off` (ваниль), `on` (пост-эффекты мода), `hd` (этот рендер), позже `rt` (аппаратная
трассировка, отдельно).
## Где код
| Что | Где |
|---|---|
| Весь HD-конвейер: цели, проходы, WGSL-шейдеры (строками), тайлы света, AO, тени, bloom, tonemap, TAA | `horizon/crates/horizon-client/src/gfx/hd.rs` |
| Атмосфера: сборка текстуры неба (WGSL), чтение неба/дымки/тона света в шейдерах, цвет солнца (`sun_transmittance`) | `horizon/crates/horizon-client/src/gfx/hd_sky.rs` (+ `THIRD_PARTY_PHOTON.md`) |
| Встраивание в кадр: G-буфер-проход, каскады теней, вызовы `hd.light` / `hd.finish`, джиттер, тайминги | `horizon/crates/horizon-client/src/gfx.rs` (`Renderer::render`, ищи `hd_on`, `AirDef fork:`) |
| G-буфер террейна: `fs_gbuffer`, `fs_shadow` | `horizon/crates/horizon-client/src/terrain.wgsl` |
| G-буфер сущностей и моделей мода: `fs_entity_gbuffer`, `fs_entity_shadow` | `horizon/crates/horizon-client/src/entity.wgsl` |
| Пост-проход мода (взрывы, дым, вспышки, свечение) и его прелюдия | `airdef-mod/shaders/effects.wgsl`, прелюдия в `horizon/.../gfx/mod_post.rs` |
| Источники света мода (пламя, ракеты, взрывы, прожекторы) | `airdef-mod/src/lib.rs`, `fn lights` |
| API мода: `render.set-lights`, `render.set-lighting` (уровень API 10) | `horizon/crates/horizon-mod-sdk/wit/horizon.wit`, `horizon/crates/horizon-mods/src/runtime.rs` |
Форк ведётся в `horizon/` (ветка `airdef`), в репозиторий мода попадает **патчами**:
```bash
cd horizon && git add -A crates && git commit -m "..." # коммит в форке
cd .. && rm -f horizon-patches/*.patch && git -C horizon format-patch -q $(cat HORIZON_COMMIT)..airdef -o ../horizon-patches
```
`tools/airdef.py fork` сверяет отпечаток `HORIZON_COMMIT` + патчей (`horizon/.git/airdef-stamp`) и пересобирает
ветку, если они изменились. После ручной перегенерации патчей у себя обнови отпечаток (см. `fork_stamp` в
`tools/airdef.py`), иначе следующий `airdef.py fork` пересоберёт ветку (безопасно: прежняя остаётся в `airdef-prev`).
## Конвейер кадра (HD включён)
Цели мира (G-буфер, AO, освещение, пост-проход мода) — в «масштабированном» разрешении мира (`world_post`; при `HORIZON_HD_SCALE` < 1 оно меньше окна), цели после TAA (история, bloom, tonemap) — в разрешении окна. HDR — `Rgba16Float`. Глубина камеры — reversed-Z
(`Depth32Float`, 1 = близко, 0 = далеко). Позиции везде относительно камеры (camera-relative), мир — в `f64`
на CPU.
0. **Небо** (`hd.sky`, проход `hd sky`, перед освещением): текстура 128×72 (`Rgba16Float`): 128 азимутов (от солнца, 0..π) × 64 высот (v = √(h/(π/2))), плюс строка 64 с «окружающим светом» (текселы 0 — зенит, 1 — среднее у горизонта). Каждый тексел — марш по лучу (32 шага) однократного рассеяния Рэлея/Ми + озон, плоский член многократного рассеяния (`MULTI_SCATTER`), солнце и луна (луна — напротив) как источники; пропускание до источника — аналитическое (Chapman, как у Photon). Зависит только от высоты солнца → перерисовывается, лишь когда она сдвинулась больше чем на 0.001. Читают её `fs_light` и композит воды (привязки 15 и 10).
1. **Каскады теней** (2 шт., `CASCADES = [40, 176]` блоков полуразмера, `SHADOW_SIZE = 2048`, массив глубины
из 2 слоёв). Источник — солнце днём, луна ночью (`light_of`). Матрицы `cascades()` привязаны к текселям
(тени не «ползут»). Во время прохода буфер глобалов мира подменяется на глобалы каскада (copy-трюк) и потом
возвращается. Рисуются секции террейна в коробке дальнего каскада (`hd_shadow_visible`) и сущности-тенеотбрасыватели
(`shadow_casters`: без оверлеев 4/9/10/40 — 40 это светящиеся модели мода).
2. **G-буфер** (свой depth): albedo `Rgba8Unorm` (a = эмиссия), нормаль `Rgba16Float` (мировая, из `dpdx/dpdy`),
уровни света `Rgba8Unorm` (x = небо, y = блоки — сырые координаты лайтмапа /240). Террейн + дальние LOD-тайлы +
сущности. Полупрозрачное в G-буфер **не** попадает (идёт ванильным путём); вода — отдельный путь, см. п. 3 и «Вода».
3. **Вода** (`water_pass`, только если на экране есть полупрозрачные секции, нет `frame.underwater` и не `HORIZON_HD_WATER=0`):
те же секции ещё раз с `fs_water` (terrain.wgsl), который отбрасывает всё, кроме слоёв спрайтов `water_still`/`water_flow`
(номера слоёв передаются в `Renderer::new` из `sprite_layers` и ставятся override-константами `WATER_STILL/WATER_FLOW`; стекло,
лёд и прочее остаются ванильными). Цели: цвет (спрайт × биомный тинт, a = свет блоков, `Rgba16Float`), нормаль грани (a = свет неба,
`Rgba8Unorm`) и собственная глубина воды (`Depth32Float`, 0 = воды нет; тест глубины только среди воды, перекрытие
землёй решают потребители). Пайплайны `gbuffer_water` (секции) и `flat.water` (flat-путь).
3a. **GTAO** (полразрешения, `Rg16Float`: окклюзия, дистанция) + depth-aware блюр 5×5. 3 среза × 5 шагов, радиус
1.8 блока, шум IGN по кадру. Сила — `HdFrame.ambient.w` (`HORIZON_HD_AO=<число>`, 0 = выкл).
4. **Освещение** `fs_light` → `lit` (HDR): пиксель неба (глубина мира 0, Оверворлд, без env-тумана) — это небо атмосферы + то, что мир нарисовал поверх ванильного неба (`max(scene − vanilla_sky(view), 0)`: спрайты луны, звёзды; квадрат ванильного солнца вырезается вокруг направления солнца и заменяется круглым диском с гало); облака (глубина > 0, нет в G-буфере, y > 8, дальше 48 блоков) на закате/рассвете окрашиваются цветом солнца. Свет неба: ванильный `lightmap` × подкраска цветом неба (зенит сверху, горизонт сбоку; яркость 1) × форма по нормали (вверх 1.1, вниз 0.66), только там, где есть небесный свет. Тени: ближний каскад — PCSS-lite (поиск блокировщиков 8 выборок, ширина полутени от расстояния до блокировщика, 12 выборок спирали Фогеля, повёрнутых IGN; сглаживает TAA), дальний — 3×3. Туман: цвет — небо над горизонтом в направлении пикселя (`haze_colour`) + воздушная перспектива `1 − exp(−d/700)` × 0.5. Далее прежнее: пиксель — вода, если `water_depth >= g` и перед водой в мировой глубине ничего нет
(`d - wd <= 0.2%`); тогда освещается **дно** по данным G-буфера (одна выборка тени, без AO), а в альфу `lit` пишется 0.5 (признак воды). `max(небо·ambient.x, блоки·ambient.y)·AO + солнце·N·L·тень + точечные
источники` × albedo + эмиссия; затем ванильный туман заново, объёмные лучи (14 шагов, Henyey-Greenstein g=0.6),
ореолы вокруг источников. Точечные источники — по тайлам 16×16 (`tile_lists`, буфер `tiles`: на тайл 4 слова —
начало/длина списка для поверхностей и для ореолов, потом индексы; переполнение → «все источники»).
4a. **Композит воды** `fs_water` (`hd.water_composite`, `lit` → `lit_water`, дальше везде `hd.lit_view()`; на пикселях без воды копия).
Волны — сумма 10 синусоид в мировых координатах (векторы — целые кратные 2π/256, скорости — целые циклы за 600 с: период 256 блоков
и 10 минут, не «плывут» с камерой; мелкие волны гасятся по размеру пикселя против мерцания); нормаль = нормаль грани + градиент
(только на горизонтальных гранях). Преломление: смещение луча относительно плоской воды, выборка `lit` по uv со сдвигом (отбраковка
по глубине: не берутся банк/сущности перед водой); толщина = расстояние от поверхности до дна; поглощение `exp(-(1-цвет)·0.8·(t+0.3))`
+ рассеяние цветом воды при свете «объёма» (лайтмап + солнце с тенью). Отражение: SSR по глубине G-буфера (24 шага, растущие,
джиттер IGN по кадру, 5 шагов бинарного уточнения, толщина `0.4+0.08·s`), при промахе — небо/туман (`sky_color`, `fog_color`, тусклее
под навесом). Волны: к 10 крупным добавлены 16 мелких (длины 0.35–3 блока, уклон до 0.05, гасятся по размеру пикселя) — густая мелкая рябь для искорок блика; блик GGX с a=0.07. Цвет воды светлее: биом × 0.85, поглощение 0.8, рассеяние 0.85 (ночью до 0.4). Небо отражения и туман — из атмосферы. Френель Шлика F0=0.02 (SSR пропускается при F<0.04: взгляд вниз), блик солнца GGX (a=0.05) × тень, туман поверх.
Подводный вид и `HORIZON_HD_WATER=0` — композит и проход воды не работают, `lit` как раньше.
5. **Пост-проход мода** `lit` → `fx` (`effects.wgsl`: взрывы, дым, вспышки, свечение; при HD его собственная
«подсветка огнём» выключена — свет даёт HD).
6. **TAA** — после пост-прохода мода (`fx`, либо `lit` без него), до bloom: история пинг-понгом, в альфе истории —
дистанция для отбраковки дизокклюзии; телепорт > 64 блоков сбрасывает историю. Джиттер добавляется к
`view_projection` только при HD (публичная матрица без джиттера); рука и HUD рисуются после tonemap и не дрожат.
**TAAU**: если мир меньше окна (`HORIZON_HD_SCALE=0.67`), тот же проход (`fs_taau`, в таймингах `hd taau`) восстанавливает
полное разрешение. История, bloom и tonemap — в разрешении окна. Для каждого пикселя выхода берётся 3×3 выборки мира
вокруг (с учётом джиттера в `taa_params.zw`), веса — полином, аппроксимирующий Lanczos-2, по расстоянию от центра
пикселя выхода до выборки; variance clip в YCoCg по тем же 3×3; глубина — ближайшая из 2×2; история (Catmull-Rom,
полное разрешение) вес 0.92, но ближе к ней там, где ближайшая выборка далеко от центра пикселя. Джиттер —
Halton(2,3) на `max(16, ceil(8·ratio²))` кадров (0.67 → 18, 0.5 → 32). При масштабе 1 работает прежний `fs_taa`
без изменений. С `HORIZON_HD_TAA=0` и масштабом < 1 остаётся простой билинейный апскейл в tonemap.
7. **Bloom** (prefilter + 6 уровней: 13-tap вниз, tent вверх) и **tonemap**: до 0.8 как есть (ванильный вид),
выше — мягкий roll-off по ярчайшему каналу; лёгкий грейд (+12% насыщенности, мягкая S-кривая).
Bind groups кэшируются и пересоздаются только при resize/смене источника (`hd_bind_groups_last_frame()` → 0 в
установившемся режиме).
### Привязки `fs_light` (group 0)
| # | Что |
|---|---|
| 0 | `HdFrame` (uniform) |
| 1, 2 | цвет и глубина мира (ванильный проход) |
| 3, 4, 5 | albedo, нормаль, глубина G-буфера |
| 6 | источники света (storage, `HdLight`: pos+radius, color+intensity) |
| 7 | уровни света (небо, блоки) |
| 8, 9 | массив карт теней, comparison-сэмплер |
| 10, 11 | лайтмап, линейный сэмплер |
| 12 | тайлы источников (storage) |
| 13 | AO |
| 14 | глубина воды (`water_depth`) |
| 15 | текстура неба (`sky_lut`) |
`HdFrame` (Rust и WGSL держать в синхроне!): `inverse_view_proj`, `resolution` (w, h, 1/w, 1/h), `params` (число
источников, экспозиция, bloom, счётчик кадров), `cascades[2]`, `sun` (направление, сила), `sun_color` (rgb, ночь),
`fog_color`, `fog`, `ambient` (тень неба, блоки, плотность воздуха, AO), `shadow` (вкл, R0, R1, тайлов в строке), `prev_view_proj`, `taa`, `taa_params`, `view_proj` (с джиттером, для лучей воды), `water` (x: эффект идёт, y: секунды волн mod 600, zw: камера x, z mod 256), `sky_color` (w: есть небо), `sky_sun` (xyz: на само солнце, w: 1 — небо рисует атмосфера), `sky_extra` (x: конец ванильного тумана неба, z: пасмурность от дождя 0..0.85), `sunrise` (ванильная полоса восхода/заката — вычитается из мира).
Композит воды (`WATER`, group 0): 0 `HdFrame`, 1 `lit`, 2 глубина G-буфера, 3 цвет воды, 4 нормаль воды, 5 глубина воды, 6–7 тени, 8 лайтмап, 9 сэмплер, 10 текстура неба. Проход неба (`SKY_PASS`, group 0): только `HdFrame`.
## Как проверять
```bash
cd horizon && cargo build --profile fast -p horizon-client --features mods # без предупреждений
cargo fmt -p horizon-client
cargo test -p horizon-client --lib hd # naga-разбор всех HD-шейдеров + тест тайлов
cd ../airdef-mod && cargo horizon-mod build && cargo test # мод и его шейдер
```
WGSL-ошибки в `hd.rs` всплывают только в рантайме — поэтому каждый новый шейдер добавляй в naga-тест
`lighting_shader_parses`.
Игра офскрин (только одна копия за раз; данные в `run-home/`, мир `Polygon`):
```bash
cd run-home && HORIZON_HOME=$PWD HORIZON_NO_AUDIO=1 ../horizon/target/fast/horizon-ctl --socket $PWD/ctl.sock \
launch --world Polygon --offscreen --wait --game ../horizon/target/fast/horizon-client \
--resources "$(ls -d "$HOME/Library/Application Support/horizon-launcher/versions"/*/.local/horizon-client/26.3 | sort -V | tail -1)"
H="../horizon/target/fast/horizon-ctl --socket $PWD/ctl.sock"
$H command "time set 4500"; $H command "tp @s 40 76 40 135 25" # полигон с техникой; z ≥ 60 — море
# команды мода — через чат: $H key t tap; $H type "/airdef shader hd"; $H key return tap
$H key f1 tap; $H screenshot out.png; $H bench 4; $H quit
```
Тайминги проходов на GPU: `HORIZON_SHADER_PACK_TIMING=1` (лог в `run-home/ctl.log`) или список F3.
Ориентиры (M5, 2560×1440): тени 0.6 + 2.85 мс (дальний каскад через кадр, ≈1.4 в среднем), G-буфер 0.8,
AO 2.9, освещение 5.2, TAA 2.7, bloom 1.1, tonemap 0.2 — кадр ≈ 15 мс GPU (замеры шумные).
TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс; 0.67 — 13.4 мс; 0.5 — 10.4 мс (освещение 6.1 → 2.7 → 1.6, AO 3.5 → 1.4 → 0.8, `hd taau` ≈ 3.5 мс на любом масштабе, дальний каскад теней 3.4 мс не зависит от масштаба).
Следующие цели по скорости: освещение (лучи 14 шагов на полном разрешении → полразрешения + апскейл) и AO.
Проверка мерцания: два скриншота с неподвижной камеры подряд и разница по пикселям (Pillow `ImageChops.difference`)
— на мире `Hills` с травой и деревьями; после исправлений ≈2% пикселей с разницей > 20/255 (дрожание TAA на пиксельных
текстурах и анимация воды), а при ошибке теней было 8.5%. Сравни с `HORIZON_HD_TAA=0` / `HORIZON_HD_AO=0` /
`HORIZON_HD_SHADOW_FAST=0`, чтобы найти виновника.
Отладка: временно выводи промежуточные величины цветом в `fs_light` (так нашли пустую карту теней и неверное
направление солнца). Помни: солнце в мире идёт по оси X (`(-sin a, cos a, 0)`), а `Globals.sun` неба — в своей
плоскости YZ.
## Статус
| Этап | Состояние | Коммит |
|---|---|---|
| 1. G-буфер, точечный свет мода в HDR, bloom, тон-кривая; API `set-lights`/`set-lighting` | готово | 0a56f56 (форк) |
| 2. Тени солнца/луны, переосвещение мира, лучи, ореолы, туман, грейд | готово | патч 12 |
| Кэш bind groups, тени только от тенеотбрасывателей (#12, #13) | готово | патч 13 |
| Тайловые списки источников (#10) | готово | патч 14 |
| Тайминги HD-проходов (#9), GTAO | готово | патч 15, ed2db85 |
| Отсечение взрывов в `effects.wgsl` (#11), порядок дыма (#16), метрика перекрытия свечения (#14) | готово | 67a2a52, ed2db85 |
| Тени: только грани к солнцу, без «закопанных» секций, дальний каскад через кадр (`HORIZON_HD_SHADOW_FAST=0` — по-старому) | готово | патч 16 |
| TAA: джиттер Halton(2,3) ×16, перепроекция по глубине и сдвигу камеры, variance clip в YCoCg, история Catmull-Rom 0.9, лёгкий шарп в tonemap (`HORIZON_HD_TAA=0` — выкл) | готово | патч 16 |
| TAAU: мир в `HORIZON_HD_SCALE` (0.25–1, по умолчанию 1 = как раньше), восстановление в TAA-проходе `fs_taau`; без LOD-bias текстур (не доступен) | готово | патч 18 |
| Исправлено мерцание теней у игрока: ближний каскад терял секции в кадры, когда дальний держится; держимая матрица дальнего каскада сдвигается на шаг камеры; у растений (наклонные квады) нормаль «вверх», а не к камере; TAA держит 94% истории в покое | готово | патч 18 |
| Вода: проход `fs_water`, преломление, SSR, Френель, блик (`HORIZON_HD_WATER=0` — выкл) | готово | патч 19 (+20: без ванильной текстуры на поверхности, цвет темнее и глубже) |
| Вода: светлее, густая мелкая рябь (26 волн), блик a=0.07 (мерцание на реке ≈1.4% пикселей >20/255) | готово | патч 21 |
| Небо и атмосфера (Рэлей/Ми/озон, текстура неба, воздушная перспектива, цвет солнца из пропускания, голубоватый ambient по нормали, PCSS-lite ближнего каскада, диск солнца, цвет облаков на закате; порт констант и Chapman из Photon, MIT) | готово | патч 22 |
| API моделей по хэндлам (#30, уровень API 11: `model-id`, `model-generation`, `set-models-by-id`) | готово | патч 17 |
## План (по порядку) и ТЗ
Каждый пункт — самостоятельная задача. Перед началом прочитай `hd.rs` целиком.
1. **TAAU — временной апскейлер** (поверх TAA) — СДЕЛАНО (патч 18, см. статус; масштаб — `HORIZON_HD_SCALE`, переопределяет «масштаб разрешения» настроек Horizon только для HD; поля в `HdSettings` нет). Осталось: LOD-bias текстур террейна, MetalFX. Исходное ТЗ: Мир и G-буфер рендерятся в `render_scale` (например 0.67),
TAA-проход восстанавливает полное разрешение: история в полном разрешении, текущий кадр семплируется с учётом
джиттера (Catmull-Rom/Lanczos-веса по субпиксельной позиции), клиппинг окрестности как в TAA. Настройка
масштаба (`HORIZON_HD_SCALE` или поле в `HdSettings`). Цель: при 0.67 картинка близка к нативной, FPS заметно
выше. Альтернатива на Mac — MetalFX через wgpu-hal (нативный код, только Metal) — отдельной опцией потом. DLSS
нереален для нашей цели (только NVIDIA через Streamline, нет в wgpu).
2. **Вода** — СДЕЛАНО (патч 19, см. «Вода» ниже). Исходное ТЗ: отдельный G-буфер-путь для воды (маска + нормаль с анимированными волнами), SSR (трассировка по
глубине, откат на небо/туман), преломление (смещение по нормали), затухание по глубине воды (цвет биома),
блики солнца (GGX-подобный), френель. Подводный вид не ломать (`frame.underwater`).
3. **Небо и атмосфера** — СДЕЛАНО (патч 22, см. «Небо и свет» ниже). Исходное ТЗ: рассеяние Рэлея/Ми (предвычисленная LUT пропускания и небесного цвета в маленькой
текстуре), солнце/луна дисками, звёзды из ванили; туман и лучи берут цвет из той же модели. Учесть дождь
(`weather`), закат, Незер/Энд (там — ванильное небо).
4. **Объёмные облака** (рэймарч слоя с шумом) + их тени на земле (в карту/маску солнца).
5. **Блики и материалы**: specular солнца для металла техники (маска по оверлею/альбедо или атрибут модели из
мода), мокрые поверхности и лужи в дождь.
6. **PCSS** (мягкость тени по расстоянию до тенеотбрасывателя) и цветные тени через стекло/листву.
7. **Ветер** листвы/травы (вершинный шейдер террейна по типу блока), рябь от дождя.
8. **Объёмный дым** пожаров и взрывов, принимающий свет и тени (сейчас дым — в `effects.wgsl`, без теней).
9. Опционально: motion blur, DoF (для кинокамеры повторов).
10. **RT-пресет**: `wgpu` `EXPERIMENTAL_RAY_QUERY` (Vulkan/DX12/Metal M3+), BLAS по секциям, ray-query тени и
отражения, затем GI. Отдельный пресет, HD остаётся.
## Вода (патч 19): что знать преемнику
- Тайминги проходов: `hd water` (геометрия воды; на Metal его метка часто 0 — работа учитывается в соседних проходах) и `hd water composite`.
Замеры 2560×1440, Hills, река на ~40% экрана, M5 (шумно: у меня GPU делят браузеры): композит 0.8–2 мс, `hd lighting` на 2–3 мс
выше, чем без воды (теперь освещается дно под водой — раньше эти пиксели были ванильными); всего вода ≈ +3 мс при большой воде
на экране, около 0.5–1 мс при малой. Цель «≤ 2 мс» при полной реке не достигнута. Идеи: отражения в полразрешения (с учётом маски воды
на границе), SSR только при F>0.04 уже есть, ужать освещение дна (без точечных источников на глубине > N блоков).
- Вода находится по слоям спрайтов. LOD-тайлы воды (`lod/mesh.rs`) идут тем же путём, если их слой — water_still; вода из resource pack с
другим именем спрайта не распознается (останется ванильной). `water_overlay` (боковые грани у стекла) не трогаем.
- Стекло/частицы/лёд перед водой: проверка `d - wd` возвращает ванильный вид пикселя (вода под стеклом без эффекта).
- Известные ограничения: отражается только то, что есть на экране (и только непрозрачное: листва — да, вода/стекло — нет); дождь на воде,
пена у берега, подводная каустика не сделаны; ночью вода слегка ярче неба (нужно привязать к модели неба — п. 4 плана «Небо»); отражение
солнечного диска — только блик GGX (без диска в SSR). Проверка мерцания на реке: разница двух скриншотов ≈1.6% пикселей >20/255 (норма ~2%).
## Небо и свет (патч 22): что знать преемнику
- Тайминги (M5, 2560×1440, Hills, река в кадре): `hd sky` ≈ 2.4–4.7 мс по таймстемпам, но кадр не вырос (GPU avg 18.4 мс против 18.1) — на Metal метка крошечного прохода ловит ожидание соседних; с кэшем по высоте солнца в покое проход не выполняется. Освещение 4.0–5.1 мс (PCSS-lite и небо почти не добавили), композит воды 0.6–2.4, TAA 2.6–2.9.
- Яркость: небо — физическая радиация → сжатие по максимальному каналу (`SKY_EXPOSURE` 0.85) → гамма 1/1.9 → насыщенность 1.2 (+0.45 у низкого солнца). Весь конвейер в гамма-пространстве, поэтому цвета неба и подкраски света — «показанные» значения; подкраска света неба имеет яркость 1 и не меняет экспозицию (яркость задаёт ванильный лайтмап × `ambient.x` = 0.62 днём, 0.95 ночью).
- Цвет солнца (`light_of`): пропускание атмосферы вдоль солнца (`hd_sky::sun_transmittance`), нормировано на 1, степень 0.6, × (1, 0.97, 0.88). Луна — холодная, как раньше.
- Дождь: `sky_extra.z` смешивает небо/дымку с ванильным градиентом (серым от ванили); Нижний мир/Энд/подводное — `sky_sun.w = 0`, всё как раньше (ванильное небо и туман).
- Облака — по-прежнему ванильные блоки; объёмные облака — п. 4 плана. Окраска облаков цветом закатного солнца — эвристика по маске (глубина есть, G-буфера нет, высота и дальность), может задеть далёкие частицы.
- Идеи дальше: многократное рассеяние по Hillaire вместо плоского члена, тени облаков, диск луны с фазой, звёзды/галактика по Photon (`sky.glsl`), розовый оттенок заката (`get_sun_tint` у Photon).
## Правила
- Код Photon (MIT) переносить можно только с атрибуцией (см. «Что это»); код других шейдерпаков — нельзя.
- Ничего не ломать в `off`/`on`; HD выключен — код HD не работает вовсе (`hd = None`).
- Без потери качества ради скорости без договорённости (шаги, разрешение, число эффектов).
- Каждый новый WGSL — в naga-тест. Каждый новый проход — в тайминги (`PassTiming`).
- Комментарии — короткие предложения о том «зачем»; в `gfx.rs` префикс `AirDef fork:`.
- Коммиты: в форке, затем патчи; подпись `Co-Authored-By: Claude <модель> <noreply@anthropic.com>`.
- Пуш в Gitea с машины без DNS: `ssh://git@100.92.6.40:2222/chexit/airdef.git` (ключ `~/Documents/claude-keys/deploy`).
- Обнови этот документ (статус + что изменилось в конвейере) в том же коммите, что и код.
## Тюнинг после проверки на полигоне (патч 22)
Голубая заливка неба по умолчанию делала белый бетон «снегом»: оттенок неба в заливке ослаблен до 0.4
(`sky_light_tint`, в `fs_light`), заливка днём `ambient.x` = 0.5 (+0.45 ночью), солнце 0.7 и теплее
(`light_of`: множитель (1, 0.93, 0.78)). Проверять баланс на `Polygon` в полдень (`time set 6000`) — белое должно
оставаться белым с тёплым оттенком.