Files
airdef/docs/HD-RENDERER.md
chexit abf3d76fdb docs: record the real MP4 acoustics check (#96)
Pairs of the same replay exported with acoustics on and off on the
scratch world (no HORIZON_NO_AUDIO): acoustics and capture ownership
both ready. The open field has no reverb send by design, so it shows
occlusion/air absorption only; the stone court's on-track is quieter
and darker, and its residual after a gain fit is low-passed and
stereo-decorrelated, i.e. the wet path. A clean reverb tail/RT60 in a
real MP4, echoes alone and listening remain open. No code changes.

Co-Authored-By: Kimi Code <noreply@moonshot.ai>
2026-11-04 20:52:11 +03:00

85 KiB
Raw Permalink Blame History

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: Hd, общие цели, буфер кадра, resize/write, сборка шейдеров (assemble: прелюдия + тело), контракты цвета/глубины/альф/времени (комментарий в начале файла) horizon/crates/horizon-client/src/gfx/hd/mod.rs
Униформ кадра HdFrame (Rust) и сцена HdScene/HdLight/HdSettings, свет солнца/луны/облаков (light_of, cloud_light); общая WGSL-прелюдия (HdFrame, FullOut, vs_full) gfx/hd/frame.rs, gfx/hd/prelude.wgsl
Тени (каскады, Shadows, cascades()); G-буфер (GBuffer, форматы) gfx/hd/shadows.rs; gfx/hd/gbuffer.rs
AO (GTAO + блюр); освещение fs_light (+ тайлы источников: tiles.rs); вода (проход геометрии и композит) gfx/hd/ao.rs + ao.wgsl, ao_blur.wgsl; gfx/hd/lighting.rs + light.wgsl, gfx/hd/tiles.rs; gfx/hd/water.rs + water.wgsl
TAA/TAAU и джиттер; bloom; tonemap (+ Hd::finish) gfx/hd/taa.rs + taa.wgsl; gfx/hd/bloom.rs + bloom.wgsl; gfx/hd/tonemap.rs + tonemap.wgsl
Объёмные облака (рэймарч слоя, тени облаков на землю) и объёмный туман, их цели и проходы; CPU-шум (2D, 3D Worley) gfx/hd/volumes.rs + cloud_common.wgsl, cloud.wgsl, cloud_shadow.wgsl, fog.wgsl; gfx/hd/noise.rs
Атмосфера: сборка текстуры неба (WGSL), чтение неба/дымки/тона света в шейдерах, цвет солнца (sun_transmittance) gfx/hd/sky.rs + sky.wgsl, sky_funcs.wgsl (+ 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
Материалы (классы спрайтов: листва, растения, металл, полированное), ветер, мокрые поверхности gfx/hd/materials.rs; terrain.wgsl (terrain_vertex/wind_offset, material_params), entity.wgsl, gfx/hd/light.wgsl (shade_surface)
Пост-проход мода (взрывы, дым, вспышки, свечение) и его прелюдия 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), render.set-spot-lights (16) horizon/crates/horizon-mod-sdk/wit/horizon.wit, horizon/crates/horizon-mods/src/runtime.rs

Форк ведётся в horizon/ (ветка airdef), в репозиторий мода попадает патчами:

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.

  1. Небо (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).
  2. Каскады теней (2 шт., CASCADES = [40, 176] блоков полуразмера, SHADOW_SIZE = 2048, массив глубины из 2 слоёв). Источник — солнце днём, луна ночью (light_of). Матрицы cascades() привязаны к текселям (тени не «ползут»). Во время прохода буфер глобалов мира подменяется на глобалы каскада (copy-трюк) и потом возвращается. Рисуются секции террейна в коробке дальнего каскада (hd_shadow_visible) и сущности-тенеотбрасыватели (shadow_casters: без оверлеев 4/9/10/40 — 40 это светящиеся модели мода).
  3. G-буфер (свой depth): albedo Rgba8Unorm (a = эмиссия), нормаль Rgba16Float (мировая, из dpdx/dpdy), уровни света Rgba8Unorm (x = небо, y = блоки — сырые координаты лайтмапа /240). Террейн + дальние LOD-тайлы + сущности. Полупрозрачное в G-буфер не попадает (идёт ванильным путём); вода — отдельный путь, см. п. 3 и «Вода».
  4. Вода (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 = выкл). 3b. Облака и туман (hd.volumes, до освещения, после прохода неба): hd cloud shadows (карта 256² оптической толщины облаков по ходу солнца, ±1200 блоков вокруг камеры), hd clouds (половина разрешения мира, Rgba16Float: свет + пропускание; рэймарч слоя 192.33…264 блока, 8–26 шагов с джиттером IGN, свет — 3 шага с удвоением, небо — 1, 4 октавы многократного рассеяния Photon; история ping-pong с репроекцией по направлению и сдвигу камеры, вес до 0.9, отбрасывается при расхождении) и hd fog (половина разрешения: 18 шагов от камеры до глубины мира, не дальше 192 блоков, плотность — высотная дымка + утренний туман у земли + дождь + шум, свет — солнце × тень каскадов × фаза Photon + свет неба; вторая цель — длина луча для апсэмплинга). Ванильные облака при этом не рисуются (hd_volumetric_clouds в gfx.rs). fs_light накладывает облака на небо (c·T + S), туман на всё (fog_at: 4 текселя с весом по близости длины луча), а тень облаков (cloud_shadow) умножает прямой солнечный свет. HORIZON_HD_CLOUDS=0 / HORIZON_HD_VOLFOG=0 выключают (облака тогда ванильные, лучи — старые 14 шагов).
  5. Освещение 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 как раньше.
  6. Пост-проход мода lit → fx (effects.wgsl: взрывы, дым, вспышки, свечение; при HD его собственная «подсветка огнём» выключена — свет даёт HD).
  7. 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.
  8. 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)
16, 17 объёмный туман (свет+пропускание) и длина его луча
18, 19 объёмные облака, карта теней облаков
20 длина луча каждого текселя облаков (cloud_dist_tex, для апсэмплинга по глубине)

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 (ванильная полоса восхода/заката — вычитается из мира), clouds (низ слоя относительно камеры, толщина, вкл, секунды ветра), cloud_cam (x, z камеры по модулю 65536, добавка покрытия от дождя, история облаков годна), cloud_dir / cloud_sun (откуда и каким цветом светят облаку: солнце до чуть ниже горизонта, ночью луна), vfog (туман вкл, высота камеры, утренний туман).

Композит воды (WATER, group 0): 0 HdFrame, 1 lit, 2 глубина G-буфера, 3 цвет воды, 4 нормаль воды, 5 глубина воды, 6–7 тени, 8 лайтмап, 9 сэмплер, 10 текстура неба. Проход неба (SKY_PASS, group 0): только HdFrame.

Контракты (цвет, глубина, альфа, время)

Полная таблица «проход: что читает → что пишет» — в начале gfx/hd/mod.rs, подробности — в начале файла каждого прохода.

  • Цвет. От освещения до tonemap всё в «показанном» пространстве ванили (гамма, основные цвета Rec. 709), значения могут быть > 1 (Rgba16Float); tonemap не трогает ≤ 0.8. Исключения: текстура неба — линейная яркость Rec. 709 (марш в Rec. 2020; показывают читатели через sky_show); albedo G-буфера — Rgba8Unorm в том же показанном пространстве.
  • Глубина. Камера: reversed-Z, Depth32Float, 1 — близко, 0 — далеко/ничего (очистка в 0; глубина воды 0 — воды нет). Тени: обычная Z ортопроекции света (0 — у света, 1 — далеко), сравнение <=.
  • Альфа. albedo.a — эмиссия (свет добавляет rgb·a·1.5); lit.a: 1 — обычный пиксель, 0.5 — вода (в rgb дно без воздуха), 0.6 — прозрачное перед водой; air.a — пропускание воздуха; облака и туман: a — пропускание (c·a + rgb); вода: цвет.a — свет блоков, нормаль.a — свет неба; история TAA: a — дистанция; AO: x — окклюзия, y — дистанция.
  • Координаты. Позиции относительно камеры; для узоров (волны, шум облаков, ветер) мировая координата передаётся по модулю периода (256, 65536). Мировая проекция с джиттером; prev_view_proj — с джиттером для fs_taa и без для fs_taau (выбирает Hd::write).
  • Время. Три разных часа: счётчик кадров (params.w, mod 64, вращение шума), время воды (water.y, секунды стенных часов mod 600), игровое время (clouds.w, секунды HdScene.seconds mod 300000, ветер облаков). Реплей, замораживающий игровое время, не останавливает воду.
  • Истории. Две цели TAA и две цели облаков принадлежат своим проходам, создаются в resize, сбрасываются при resize, телепорте (> 64 блоков), выключенном TAA; один Hd обслуживает одну камеру, истории разных камер не смешиваются.
  • Размеры. world — масштабированная цель мира (G-буфер, AO ½, свет, вода, облака/туман ½), out — окно (TAA, bloom, tonemap).
  • Bind groups кэшируются по видам, которые читают; пересоздаются при resize и смене входного вида; нет роста в установившемся кадре.

Как проверять

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-ошибки всплывают только в рантайме — поэтому каждый новый шейдер добавляй в список shaders() в тестах gfx/hd/mod.rs: lighting_shader_parses (naga-разбор каждого итогового шейдера), every_wgsl_file_is_tested (каждый .wgsl входит в проверенный шейдер), bindings_match_the_layouts (привязки WGSL = *_BINDINGS в Rust: номера, виды, размерности), frame_layout_matches_the_prelude (поля и размер HdFrame в Rust и в прелюдии).

Игра офскрин (только одна копия за раз; данные в run-home/, мир Polygon):

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, чтобы найти виновника.

Графический полигон: python3 tools/hd_bench.py (--list — сцены; --only polygon_noon — одна сцена; --env HORIZON_HD_TAA=0 --out run-home-bench/out/taa-off — A/B; --baseline run-home-bench/out/before — diff ×6 и пороги; --budget-ms 18 — GPU-бюджет). Сам копирует Polygon/Hills в run-home-bench/home, сокет run-home-bench/c.sock, одна игра за раз; отчёт report.md с таблицей, проходами GPU и кадрами. Время и погода заморожены (gamerule advance_time/advance_weather false); вода и листва идут по часам стены, поэтому кадры сравниваются по статичным пикселям: 3 кадра подряд, пиксели с размахом > 16/255 (вода, облака, листва) не участвуют в сравнении с базой и в мерцании (маска <сцена>_motion.png); --no-freeze — живой замер производительности; --help — все пороги.

Отладка: временно выводи промежуточные величины цветом в 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
Закат как у Photon: атмосфера в Rec. 2020 (коэффициенты и матрицы Photon), фаза Клейна-Нишины, тинт и усиление солнца на закате/рассвете/синем часе (get_sun_tint), насыщенность неба у низкого солнца готово патч 23
Объёмные облака (рэймарч, мультирассеяние Photon, тени на землю, TAA-история, ванильные скрыты) готово патч 24
Объёмный туман (лучи через тени каскадов, высотная дымка, утренний туман, полразрешения) готово патч 25
API моделей по хэндлам (#30, уровень API 11: model-id, model-generation, set-models-by-id) готово патч 17
#41: пасмурность неба и сила солнца берутся из мирового уровня дождя (Sky.rain, ставит apply_weather), а не из наличия геометрии осадков готово в рабочем дереве форка
#38: TAA без апскейла ищет историю по матрице прошлого кадра с джиттером (HdScene.prev_view_proj_jittered); fs_taau по-прежнему берёт матрицу без джиттера готово в рабочем дереве форка
#35: ванильные облака (HD-облака выключены) красятся закатным цветом в собственном проходе (hd::vanilla_cloud_tint → цвет в clouds::Input), эвристика по глубине G-буфера убрана из fs_light готово в рабочем дереве форка
#37: воздух (ванильный туман, aerial, объёмный туман, лучи, ореолы) на воде считается до поверхности и кладётся один раз поверх готовой воды: fs_light пишет во вторую цель air (рассеяние + пропускание), fs_water применяет её после поглощения готово в рабочем дереве форка
#36: ванильная вода не рисует цвет в HD (fs_translucent при Globals.hd_flags.x, глубину оставляет); прозрачное перед водой (d ближе wd) не отключает HD-воду: вода считается на освещённом дне и добавляется к сцене с весом 0.7; преломление не берёт тексели с таким прозрачным (textureGather по альфе) готово (проверены стекло; частицы и scale 0.67 не проверялись) в рабочем дереве форка
#45: gfx/hd.rs разделён на модуль gfx/hd/ (по файлу на проход: shadows, gbuffer, ao, sky, volumes, lighting+tiles, water, taa, bloom, tonemap, frame), WGSL вынесен в .wgsl (include_str!, общая прелюдия), у каждого прохода своя структура ресурсов/пайплайнов; контракты цвета, глубины, альф и времени записаны в hd/mod.rs и в начале файлов проходов; тесты привязок и размера HdFrame. Вид не менялся (сверка tools/hd_bench.py) готово в рабочем дереве форка
#39: веер восхода vanilla_sunrise_weight отсекается до цикла по 16 треугольникам выше горизонта (знак x стороны солнца и наклон ≤ α/3; проверено перебором 200 тыс. лучей без пропусков); hd lighting на рассвете 5.3 → 3.3 мс (M-серия, 2560×1440) готово в рабочем дереве форка

| Облака в воде, перистые, туман по рельефу, лучи, тень облаков при низком солнце, дальний дым (768 блоков) | готово, в рабочем дереве форка; тайминги не сняты чисто | не закоммичено | | Материалы и блики (план 5): G-буфер levels.zw = шероховатость, металличность; GGX-блик солнца с Френелем и отражение неба (sky_along/atmosphere_sky) в shade_surface; мокрые поверхности и лужи в дождь (HdFrame.surface); HORIZON_HD_SPECULAR=0 — выкл. | готово | в рабочем дереве форка | | Ветер (план 7): листва и растения качаются в вершинной стадии террейна (Globals.hd_wind, hd_materials), те же смещения в тенях; HORIZON_HD_WIND=0 — выкл. | готово | в рабочем дереве форка | | #67: облака марш идут до ближайшей поверхности (fs_cloud читает глубину мира, вторая цель — длина луча, история отбрасывается при другой длине); fs_light кладёт облака на все пиксели (cloud_at: 4 текселя с весом по близости длины луча), под туман и над поверхностью, для воды — в air; луч внутри слоя вдоль горизонта больше не пропускается | готово; проверено только пустой полигон (нет гор/техники в слое, снимки scratchpad/c*.png), бюджет GPU на Mac не мерился | в рабочем дереве форка | | #68: шум облаков по Y — от низа слоя, а не от камеры; шкалы тумана 170→128, дыма 22→32, луж 0.45/1.3→0.5/1.25 делят период обёртки 65536/256; тест noise_scales_divide_the_camera_wrap | готово | в рабочем дереве форка | | #70: Game::sky_darken — всегда реальное время мира (ИИ, спавн, блоки, статистика); shown_sky_darken (время реплея) — только тени сущностей и свет глаза; небо/луна/цвета остаются по показанному времени | готово (собрано, игровой проверки день/ночь реплея нет) | в рабочем дереве форка | | Объёмный дым и следы ракет (#54, п. 8 плана): API уровня 14 render.set-smoke, сетка дыма 128×64×128 по 3 блока (gfx/hd/smoke.rs, smoke_grid.wgsl), марш в проходе тумана, «туннель» в тумане и облаках, HORIZON_HD_SMOKE=0; мод: airdef-mod/src/smoke.rs (следы дронов и ракет, колонны над пожарами, дым взрывов) | готово, в рабочем дереве форка; вид не настраивался (см. «Дым и следы») | не закоммичено | | Цветные тени (план 6): ближний каскад без листвы в глубине + карта цвета 1024² (Rgba16Float, shadow_tint_pass): стекло/лёд умножают свет на цвет, листва пропускает ×0.55 зелёного; в альфе расстояние до источника света (min-blend); fs_light берёт 8 отсчётов (shadow_tint); HORIZON_HD_COLOR_SHADOW=0 — выкл. | готово; радуга от цветного стекла видна в игре, листва — слабым зеленоватым пятном; без A/B с выключенным | в рабочем дереве форка | | Рябь от дождя на воде (остаток плана 7): кольца в сетке 1.6 блока (ripple_gradient, water.wgsl), уровень дождя в sky_extra.w; HORIZON_HD_RAIN_RIPPLES=0 — выкл. | готово; в игре едва различимы дуги в бликах (частицы дождя закрывают), не настраивалось | в рабочем дереве форка | | Motion blur и DoF кинокамеры (план 9): gfx/hd/film.rs + film.wgsl, проход после TAA, только при film_quality(); HORIZON_HD_MOTION_BLUR=0, HORIZON_HD_DOF=0, HORIZON_HD_DOF_FOCUS, HORIZON_HD_DOF_STRENGTH, HORIZON_HD_FILM_QUALITY=1 (принудительно, для проб) | DoF виден в игре (до правки стоил 14–17 мс); motion blur в игре не подтверждён; правка стоимости DoF (планарная глубина, 24 отсчёта, радиус 0.7%) собрана и прошла тесты, но глазами не проверена | в рабочем дереве форка |

План (по порядку) и ТЗ

Каждый пункт — самостоятельная задача. Перед началом прочитай gfx/hd/mod.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. Объёмные облака — СДЕЛАНО (патч 24, см. «Закат, облака и туман»). Перистые и облака в воде сделаны (см. «Облака, перистые, туман по рельефу»).
  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) — белое должно оставаться белым с тёплым оттенком.

Закат, облака и туман (патчи 23–25): что знать преемнику

  • Закат (23). Небо марширует в Rec. 2020 (коэффициенты Photon сдвинуты матрицей rec709_to_rec2020, результат возвращается в 709): это убрало жёлто-зелёный горизонт. Добавлены фаза Клейна-Нишины для Ми и деполяризованная Рэлея, sun_light_tint (розоватый тинт и подъём яркости на закате, фиолетовый на синем часе, из get_sun_tint), сильнее насыщенность у низкого солнца (sky_show). Цвет прямого света (light_of) умножен на тот же тинт. Сверка: 12000/12500/13000 (запад) и 23000/23500 (восток) — оранжевый/розовый/пурпурный; полдень Polygon (6000) прежний.
  • Облака (24). Слой 192.33…264.33 блока (CLOUD_BASE/CLOUD_THICKNESS), ветер 3 блока/с (CLOUD_WIND), шум — текстуры, сгенерированные на CPU при создании HD (hd_noise.rs: 2D 256² покрытие/форма, 3D 64³ Worley). Плотность — по clouds_cumulus_density Photon (порог покрытия × форма, «яйцо» по высоте, эрозия Worley, резкий верх), рассеяние — clouds_cumulus_scattering (октавы, пудра, фазы). Цвет света — cloud_light (Rust): пропускание атмосферы на высоте солнца; солнце светит облакам до −0.2 по высоте, дальше луна. Воздушная перспектива exp(−d/3600) к цвету дымки. История сбрасывается при resize, телепорте и без TAA. Опция ванильных облаков «выкл» → frame.clouds пуст → нет ни облаков, ни теней. Тень: cloud_shadow в fs_light (карта оптической толщины; гаснет при солнце ниже ~0.1–0.25 по высоте).
  • Туман (25). fs_fog: 18 шагов, плотность fog_sigma (высотная дымка от y=62, масштаб 42; утренний туман — пока солнце на «утренней» стороне и низко; пятна из 3D-шума; дождь; затухание ниже y≈28–58, чтобы не светить в пещерах), свет — Photon-смесь двух HG, «вечернее свечение» ×1…3. Старые 14 шагов лучей остаются при HORIZON_HD_VOLFOG=0; ореолы вокруг источников мода — всегда.
  • Тайминги. Метки Metal (HORIZON_SHADER_PACK_TIMING=1, Hills, 2560×1440): hd clouds ≈ 6 мс (завышено: метка ловит ожидание соседних проходов), hd cloud shadows 0.2–3 (шум), hd fog ≈ 1.3. По среднему кадру GPU «оба выкл → оба вкл»: +0.2…3.7 мс (замеры шумные: рядом шла чужая игра на том же GPU). Как удешевить: у облаков — длина шага (14) и потолок шагов (26) в fs_cloud, 3 шага света, 4 октавы, облака в четверть разрешения; у тумана — FOG_STEPS.
  • Разработка шейдеров. HORIZON_HD_SHADER_DIR=<папка>: каждый HD-шейдер читается из <папка>/<метка>.wgsl (записывается из встроенного, если файла нет; это тело без прелюдии, она добавляется всегда) — правки видны после перезапуска игры без пересборки; готовое переносится в gfx/hd/*.wgsl вручную.
  • Слабые места (после доработки облаков и тумана). Облака не закрывают далёкую землю выше слоя (только небо); карта теней облаков теперь центрируется на проекции камеры вдоль солнца (работает и при низком солнце, гаснет плавно при высоте солнца 0.03..0.2), но текстель карты ~9 блоков; перистые облака и панорама — без тени на землю; туман знает рельеф только в 512 блоках вокруг камеры (карта 8-блоковых клеток, деревья считаются минимумом из 4 проб); история облаков на быстром повороте держится сильнее (порог расхождения шире), но на краю экрана свежие пиксели остаются шумными; тайминги Metal в этой работе не сняты чисто (на машине шли другие игры, load average 65).

Облака, перистые, туман по рельефу, дальний дым (доработка)

  • Облака в отражениях. Проход hd cloud sky (fs_cloud_sky в cloud.wgsl): панорама облаков 256×64 (Rgba16Float, азимут от +x по кругу × v = √(высота/(π/2))), марш кучевых «lite» (14 шагов, 2 шага света, без тоннелей дыма) + перистые, смешивается с прошлым кадром (0.8, без репроекции: история в мировых направлениях). Пинг-понг идёт тем же flip, что у облаков. Читает её sky_with_clouds (pano_funcs.wgsl, маркер //PANO<n>): привязка 21 в fs_light (отражение неба на мокром/металле) и 12 в композите воды (sky_along). Групп воды теперь две (по flip).
  • Перистые. cirrus() в cloud.wgsl: плоский слой на 900 блоков (CIRRUS_ABOVE над основанием кучевых), 2D-шум (каналы noise2, вытянут вдоль ветра, масштабы делят 65536), без марша света: прямой свет = цвет солнца облаков × (0.5 + 3·HG(0.7)) плюс небо; на закате цвет берётся из cloud_sun; дымка по расстоянию, гаснет у горизонта; дождь повышает покрытие (cloud_cam.z) и затемняет небесную долю. Складывается за кучевыми (behind), только на небо. У кучевых при дожде темнее небесный свет.
  • Туман по рельефу. gfx/hd/ground.rs: карта 64×64 клетки по 8 блоков (Rg16Float: высота земли, глубина котловины = среднее в ±48 блоков минус высота), тороидальная (индекс = клетка mod 64, sampler Repeat, uv = мир/512). CPU обновляет по 256 клеток за кадр (4 пробы world_surface, минимум), при телепорте — всю карту; размытие и загрузка — раз в 4 кадра при изменениях. Данные: Frame::hd_ground (замыкание, game.rs). fog_sigma: утренний туман льнёт к земле и заполняет котловину до 75% глубины, немного дневной дымки в долинах, затухание ниже земли (g−14..g−3) вместо абсолютного y 28–58. Привязка 9 тумана.
  • Лучи. FOG_SUN 1.1→1.35 (у дыма осталось 1.1) и плотность ×(1…1.9) при низком солнце (low_sun); в лесу Hills утром лучи заметны.
  • Дальний дым (#54). Второй каскад сетки дыма: те же 128×64×128 клеток по 12 блоков (FAR_CELL): 768 блоков по горизонтали (192 вниз, 576 вверх), трубки не тоньше 7.2 блока, до 6144 сегментов (FAR_MAX_SEGMENTS); ближний — как раньше (192 блока, 3072). Проход hd smoke grid far; привязка 10 тумана; HdFrame.smoke_far, smoke.w = «дальняя сетка не пуста». В fog.wgsl два марша (march_smoke(..., far)): ближний до 200 блоков, дальний (шаг 12, до 72 шагов) до 800; вес ближнего — smoke_near_cover (плавно 1→0 на последних 8 клетках), дальнего — остаток: сумма 1, шва нет; дальний гаснет на последних 6 клетках. Игра (mod_models.rs: mod_smoke) шлёт сегменты до 900 блоков и до 8192. Не проверено глазами: полёт дронов на 700 блоков (/airdef wave shahed 8 700 за время ожидания трасс не показал).

Заметки по исправлениям #35–#41 (преемнику)

  • Цели света. fs_light пишет две цели: lit и air (Rgba16Float). Альфа lit: 1 — обычный пиксель, 0.5 — вода (в lit дно без воздуха, в air рассеяние воздуха до поверхности и пропускание в альфе), 0.6 — прозрачное перед водой (в lit сцена как нарисована, в air.rgb освещённое дно). Привязка 11 композита воды — air.
  • Ванильная вода в HD. Пока hd.water_active(), fs_translucent не рисует цвет воды (альфа 0), но пишет её глубину; флаг — Globals.hd_flags.x (поле добавлено в Rust-Globals, terrain.wgsl, entity.wgsl; пишется после hd.write и в hd.main_globals). Вода без дна (g = 0) считается водой толщиной 48.
  • Не сделано. Полупрозрачное перед водой берёт вес 0.7 на «вклад воды» (приближение: настоящий порядок «вода → прозрачное» потребовал бы разделить проход сцены). Частицы перед водой, HORIZON_HD_SCALE=0.67, поиск мерцания на рябь после этих правок не прогонялись.
  • HORIZON_HD_TAA=0. На Polygon в полдень (tp @s 40 76 40 135 15) HD с выключенным TAA выглядит так же (дымка, небо, тени на месте), GPU 16.5 мс против 18.1 мс с TAA (минус ≈ проход TAA); потери вида и падения до 4.8 мс не воспроизвелись. В коде HORIZON_HD_TAA читают только taa_enabled() (джиттер, проход taa, история), остальные проходы от него не зависят. Если проявится снова — проверь, что до шота прошёл /airdef shader hd (при неготовой команде кадр без HD ~5 мс).

Материалы, мокрое, ветер: что знать преемнику

  • Класс спрайта. gfx/hd/materials.rs по имени спрайта (minecraft:block/<имя>) даёт класс 0..8 (обычный, листва, растение, нижняя/верхняя половина двойного растения, металл, гладкое, глянец, дерево). Таблица — по 4 бита на слой, первые 2048 слоёв (Globals.hd_materials, 64 × vec4<u32>, считается в game.rs и кладётся в Renderer); спрайты модов дальше 2048 — обычные. Числа MAT_* в terrain.wgsl держать в синхроне. Добавить блок в класс — список имён в classify.
  • G-буфер. levels.zw (было не занято) = шероховатость и металличность (material_params в terrain.wgsl; для сущностей — по оверлею в fs_entity_gbuffer: обычные 0.9/0; модели мода 41 — крашеный металл 0.42/0.3 (по умолчанию для всех моделей мода, game/mod_models.rs), 42 — голый металл 0.28/1 (мод пока не умеет выбирать), 40 — свечение 0.5/0). Вода своим проходом, без материала.
  • Блик. В shade_surface (light.wgsl): GGX (α = rough², не ниже 0.03) × Смит × Френель Шлика, у шероховатых Френель почти не растёт (max(gloss, F0)); цвет — солнце × тень × sky_reach; потолок 6. Отражение неба: atmosphere_sky(reflect) × Френель × gloss² × доступ к небесному свету × AO, без SSR на террейне. Металл сохраняет 35% диффузного (карты окружения нет). Дно под водой блеск не получает.
  • Мокро. HdFrame.surface.x — влажность 0..1 (CPU, Hd::wetness): растёт за ~15 с дождя к уровню дождя мира, сохнет ~150 с. Под открытым небом (lm.x > 0.8) поверхности темнеют до ×0.7 (боковые грани слабее) и их шероховатость падает к 0.12; лужи — на плоских гранях вверх, по 2-октавному value-шуму в мировых координатах (wet_noise, период 256 блоков), с влажности ~0.3, альбедо ×0.6 и шероховатость 0.03, отражают небо.
  • Ветер. terrain_vertex смещает вершины листвы (±0.04 блока, непрерывно по мировой позиции, чтобы блоки не расходились) и растений (до 0.14 блока вверху спрайта; низ — по uv.y; двойные растения стыкуются на середине). Волны — целые циклы на 256 блоков и 600 секунд (как у воды). Данные ветра — в Globals.hd_wind (сила 1 + 1.2·дождь, секунды, камера mod 256); они же в глобалах каскадов теней, поэтому смещение одно и то же во всех проходах (ванильный, G-буфер, тени, глубина совпадают). Вне HD сила 0 — вершинный шейдер не меняется. TAA не знает о движении листвы (малые амплитуды).
  • Переключатели. HORIZON_HD_WIND=0, HORIZON_HD_SPECULAR=0 (блик, отражения и мокрое).

Проверка материалов/мокрого/ветра (что видно и что нет)

  • Увидено в игре (Hills, run-home-bench/out/mat1, out/rain): ветер работает, листва и трава двигаются, артефактов швов нет; в дождь сцена заметно темнее (мокрое), небо серое. Не проверено глазами: блики на технике (в Polygon сцена бенча сейчас без техники, не сопоставима с базой before-mat), отдельные лужи (кадр дождя смотрит в небо), высыхание (out/rain/drying_30s.png снят, не разобран), ночь.
  • Мерцание: hills_river_noon 9% без ветра (out/nowind) против 31% с ветром — это движение листвы вблизи камеры, не шум; амплитуда листвы снижена с 0.04 до 0.025 после этого замера (повторно не мерилось).
  • Тайминги не сняты: параллельно шли игры других агентов (числа 25–155 мс недействительны). Перед замером брать run-home/.gpu-lock.
  • Известно: shaders_translate_to_hlsl_for_directx_12 падает на cloud.wgsl (не мой файл).

Камеры мода: полный вид и мониторы (#56, #51)

  • Главный вид (render.set-main-view, API 15): мир экрана рисуется из камеры мода (set-views) полным конвейером — HD, небо, облака, туман, объёмный дым, TAA, пост-проход мода, разрешение окна. Руку, контур блока и ванильный HUD не рисуем (game.rs: mod_main_view, show_hud). Так показываются повторы Full/Reel/моменты; чанки остаются вокруг игрока, слушатель — игрок.
  • Малые виды (мониторы, gfx/mod_views.rs) остаются экономными: небо, твёрдая земля, сущности и вторым проходом пост-эффект мода с камерой этого вида (ModPost::write_to/run_view, режим params[0].w = 0: пост сам освещает землю и рисует дым). Бюджет по площади картинки (post_budget: <4096 px без пост-прохода, до 150k px — 4 взрыва/12 огней, до 600k — 12/32, больше — всё). HD-освещения, облаков и TAA у малых видов нет.
  • Мод в повторе берёт взрывы из записи (record::replay_blasts, чистая функция времени), время идёт по часам повтора (params[255].x — доля частичного тика, которую эффекты не используют при паузе/замедлении).
  • #51: на перехвате слои звука (cinema.rs), свет-«удар» (белая вспышка в HD-освещении, радиус до 256), пик экспозиции/bloom (set-lighting), в effects.wgsl широкое гало, подсветка неба/облаков и яркое кольцо ударной волны на экране.

Прожекторы: направленный свет и объёмный луч (#60)

  • API 16 render.set-spot-lights(list<spot-light>) -> bool: апекс, направление, цвет, интенсивность, range (до 1024), внутренний/внешний полуугол (рад), beam (видимость луча 0..1). До 32 от мода, рисуются ближайшие 16 (MAX_SPOTS). Возвращает, рисует ли HD (HORIZON_HD_SPOTS=0 выключает), иначе мод держит свой запасной луч. Игра интерполирует по id между двумя вызовами (mod_models.rs: mod_spots) и переводит в HdSpot (gfx/hd/frame.rs): координаты камеры, косинусы углов.
  • Данные в шейдере: HdFrame.spots (4 vec4 на источник: апекс+range, ось+cos внешнего, цвет+интенсивность, cos внутреннего+beam), число в vfog.w. Новых привязок нет.
  • Тайлы: заголовок тайла 5 слов (TILE_WORDS), пятое: маска конусов, бит j стоит, если выпуклая оболочка конуса (апекс и квадрат вокруг дальнего круга, tiles.rs: spot_hull, cover_points) на экране накрывает тайл. Маска служит и поверхностям, и лучу. Индекс тайла в WGSL: * 5u.
  • Поверхности и цели: spot_light (spot.wgsl, вставляется маркером //SPOT в assemble): конус с мягкой кромкой (smoothstep по косинусам), I/(d²+1), окно в последние 3 блока до range, угол падения как у точечных; в shade_surface (земля, стены, дроны: они в G-буфере). Теней нет: свет обрывается по range, который мод ставит по броску оси на землю (ground.world.probe, раз в 4 тика).
  • Луч: spot_beams (spot.wgsl): пересечение луча взгляда с конусом (spot_span: квадратное уравнение конуса, обрезка по оси и range); внутри 8 шагов с джиттером IGN; яркое ядро, мягкая периферия; затухание 1/(1+d/70); плотность воздуха max(spot_air, 0.0009) (в чистом воздухе слабый луч, в дымке/тумане ярче и короче); фаза Henyey-Greenstein вперёд (на лампу ярче). Складывается (прозрачный), ограничен глубиной сцены: за стеной не продолжается; камера внутри конуса работает. Туман вкл: считается в fs_fog (spot_air = fog_sigma, длина до 1100 блоков, в небо тоже, не 192 как у тумана); туман выкл или нет атмосферы: тот же код в fs_light с постоянной плотностью ambient.z.
  • Мод: sim/lights.rs: голова прожектора шлёт один SpotLight (Sam::spot_beam, lib.rs: spots; конус 0.03/0.075 рад, range = свободная длина оси + 3.5). Цель «освещена» (lit) только в угле 0.05 от оси и если она не дальше свободной длины (за горой бонуса нет). Старые airdef:beam рисуются, только когда HD не рисует споты и эффекты включены (тоньше), при эффектах off луча нет. Точечный «шар» на цели остаётся лишь без спотов. В реплеях споты не шлются (реплей не пишет голову прожектора; #56/#58 отдельно).
  • Ограничения: тень рельефа вне оси (боковая часть конуса за гребнем светит), запасной луч непрозрачен (текстура модели без альфы), GPU-цена на массовой сцене не снята.

Кинорежиссёр и экспорт MP4 (#58, API 17)

  • Качество фильма (render.set-film-quality, включается и на время захвата): мир HD в полном размере окна (hd::scale_override → 1.0), шаги облаков ×2 (шаг 7 блоков, 16–52 шага), шаги тумана ×2; флаг идёт в HdFrame.surface.z. Смена set-main-view id (склейка) сбрасывает историю TAA и облаков (Renderer::cut).
  • Захват (render.capture-start/stop/status, game/film.rs, Renderer::capture_*): часы игры идут ровно 1/fps секунды за кадр (тики, интерполяция, tick мода), кадр копируется с GPU через очередь из ≤3 буферов и уходит в поток-писатель → ffmpeg (libx264, иначе h264_videotoolbox; иначе PNG-последовательность). Звук: микшер OpenAL-подобный (audio.rs) работает офлайн (film_begin/film_mix), слышны только звуки мода, ориентация — по камере; дорожка вмуксивается AAC. После склейки рисуются warmup кадров без записи, чтобы сошлись TAA и объёмы. Размер: offscreen-игра ресайзится, окно просится (request_inner_size).
  • Мод-HUD (set-hud) теперь рисуется и при set-main-view (титры, леттербокс фильма); ванильный HUD по-прежнему скрыт.
  • Режиссура: airdef-mod/src/film.rs (план по записи, варианты ракурсов, проверка рельефа по высотам Ground::land_state, ускорение/замедление времени).

Проверенный жизненный цикл MP4 (4 ноября, pin 57f8b2853, очистка ошибки энкодера — 73b9d3070, API 29)

Реальный короткий экспорт на scratch HORIZON_HOME/мире (не на сохранениях игрока), Apple M5, Metal, ffmpeg 9.0.1. /airdef replay export с записанного налёта (4 цели, 4 перехвата), 640×360:

  • готовый файл: h264 + AAC 48 кГц stereo, длительность видео = длительность звука; звук не пустой (mean −29.4 dB, max −18.0 dB); 2 с при 30 fps — 62 кадра, sec=1 — 32, noaudio — 33 кадра без звуковой дорожки, sec=0.01 — 3 кадра; 1920×1080 sec=1 — 32 кадра. Число кадров в отчёте мода всегда совпадает с ffprobe nb_frames: очередь не теряет кадры на остановке.
  • 2–3 кадра сверх sec — это кадры, уже находившиеся в полёте на момент остановки (движок их дописывает); это не потеря хвоста. Отчёт и файл сходятся.
  • отмена во время записи (/airdef replay stop): сообщение «Экспорт отменён», файла и процессов ffmpeg не остаётся; сразу после отмены новый экспорт проходит.
  • отказ SDK (size=10000x10000): сообщение об ошибке, захват не остаётся, следующий экспорт проходит.
  • ошибка энкодера (подставной HORIZON_FFMPEG, отвечающий на -encoders, но падающий при записи): «Экспорт не удался: … Broken pipe …: fake encoder failed: no such codec»; временных файлов и процесса нет, а последние строки лога теперь в самом тексте ошибки (сам лог удалён). Готовый keep.mp4 прежнего экспорта с тем же именем остался байт-в-байт (sha256), как и беззвучный keepnone.mp4 после неудачного noaudio-экспорта с тем же именем. Движок 73b9d3070 всегда пишет <имя>.video.mp4; <имя>.mp4 появляется только после успешного mux/переименования. Контрольный ctrl на итоговой сборке 73b9d3070 — h264 + AAC, 33 кадра, временных файлов нет; keep (h264 + AAC, 32 кадра) и keepnone (только h264, 32 кадра) проверялись на той же правке.
  • акустика в MP4: пары on/off одного повтора (кнопка мода) со статусами Акустика … ready и Захват … ready; в открытом поле send реверба нулевой по построению, в каменном дворе ON тише и глуше, а остаток после вычитания масштабированного сухого сигнала низкочастотный и декоррелированный (wet-путь). Цифры, статусы и ограничения — ACOUSTICS.md.
  • выход в заголовок во время экспорта (до исправления): игра перестаёт тикать на экране заголовка, но продолжает кадры, поэтому захват писал заголовок; запрос sec=10 дал 3898 кадров (129.9 с), временные video.mp4/audio.raw и ffmpeg оставались, а после возврата в тот же мир экспорт продолжался. Мод получает кадры и вне мира (Mod::frame), поэтому Session::world_left завершает показ с ExitCause::WorldChange и одним CancelCapture: после исправления при выходе в заголовок ffmpeg и временные файлы исчезают, повторный вход и следующий экспорт проходят. Пропуск мира виден по game.world-id = none; тик мода вне мира не вызывается.

Цветные тени, рябь, кино-пост: что знать преемнику

  • Цветные тени. Ближний каскад (c = 0) рисуется fs_shadow_near (листва MAT_LEAVES отброшена), потом проход hd shadow colour (Hd::shadow_tint_pass, цель Shadows::tint, очистка в белое) рисует листву (shadow_tint_leaves, твёрдые квады, SHADOW_SOLID) и полупрозрачные квады секций (shadow_tint_glass, по отсортированным индексам; вода не рисуется) тем же fs_shadow_tint: rgb умножаются (blend Dst*src), альфа = (расстояние от света − 320)/160 с blend Min — ближайший такой квад. Приёмник в shadow_tint (light.wgsl, привязка 22) окрашивается, если квад ближе к свету, чем он; 8 точек Vogel радиусом 0.4 блока размывают листву в «пятна». Дальний каскад по-прежнему рисует листву полностью (на границе 36 блоков шов: ближе листва светлее). Цвет не доходит до лучей тумана, воды и дальнего каскада. Цена: hd shadow colour 0.25–0.35 мс (Mac, малая сцена).
  • Рябь. ripple_gradient берёт часы воды (water.y, часы фильма): частоты — целые циклы на 600 с, сетка 160 клеток на 256 блоков, поэтому рисунок не прыгает на обороте. Гаснет по размеру пикселя, только под открытым небом (nt.a). Амплитуда 0.012 не подбиралась.
  • Кино-пост. Hd::film между taa и finish (HdSource::Film), только при film_quality(); цели создаются при первом фильме. Motion blur — только от движения камеры (глубина + prev_view_proj, затвор 180°, мёртвая зона 1–2.5 px из-за джиттера; движущиеся объекты не размываются; нет после склейки/телепорта: taa.w). DoF — диск 24 отсчёта, фокус авто (гармоническое среднее 9 глубин у центра) или HORIZON_HD_DOF_FOCUS. Детерминизм: единственное время в шейдере — water.y (часы фильма) для шума. Проверять на реальном фильме /airdef replay film; HORIZON_HD_FILM_QUALITY=1 принудительно включает качество фильма для проб. DoF в игре был слишком сильным и дорогим; параметры уменьшены без повторного снимка. Motion blur на снимке (поворот 40° за 1.5 с) не разобран.
  • Из Photon (MIT) ничего нового не переносилось: THIRD_PARTY_PHOTON.md не менялся.