From 428713d4bc893fd92e3978cacf6cb905d73fac40 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 17:47:52 +0300 Subject: [PATCH] HD water: reflections, refraction, depth colour, sun glint (fork patch 19); doc updated Co-Authored-By: Claude Opus 5.5 --- docs/HD-RENDERER.md | 44 +- ...ork-world-models-client-particles-so.patch | 2 +- ...izon-mod-report-particles-and-sounds.patch | 2 +- ...-fork-full-screen-post-pass-for-mods.patch | 2 +- ...far-models-as-scaled-impostors-past-.patch | 2 +- ...er.set-views-draws-the-world-from-ot.patch | 2 +- ...er-tick-and-per-event-AirDef-plans-r.patch | 2 +- ...-up-to-1920x1080-full-screen-replays.patch | 2 +- ...ant-terrain-tiles-fog-measured-from-.patch | 2 +- ...id-so-mods-keep-state-per-world-API-.patch | 2 +- ...d-trial-runs-behind-feature-full-so-.patch | 2 +- ...ffer-pass-per-pixel-mod-point-lights.patch | 2 +- ...e-2-sun-and-moon-shadow-cascades-the.patch | 2 +- ...ached-until-a-resize-or-a-new-source.patch | 2 +- ...-per-16x16-tile-surface-and-halo-lis.patch | 2 +- ...pass-in-the-F3-timing-list-half-reso.patch | 2 +- ...only-sun-facing-quads-and-skip-burie.patch | 2 +- ...es-render.model-id-model-generation-.patch | 2 +- ...aling-HORIZON_HD_SCALE-world-G-buffe.patch | 2 +- ...-pass-colour-normal-depth-the-floor-.patch | 982 ++++++++++++++++++ 20 files changed, 1039 insertions(+), 23 deletions(-) create mode 100644 horizon-patches/0019-HD-water-its-own-pass-colour-normal-depth-the-floor-.patch diff --git a/docs/HD-RENDERER.md b/docs/HD-RENDERER.md index c09031b..16900f8 100644 --- a/docs/HD-RENDERER.md +++ b/docs/HD-RENDERER.md @@ -50,13 +50,29 @@ cd .. && rm -f horizon-patches/*.patch && git -C horizon format-patch -q $(cat H (`shadow_casters`: без оверлеев 4/9/10/40 — 40 это светящиеся модели мода). 2. **G-буфер** (свой depth): albedo `Rgba8Unorm` (a = эмиссия), нормаль `Rgba16Float` (мировая, из `dpdx/dpdy`), уровни света `Rgba8Unorm` (x = небо, y = блоки — сырые координаты лайтмапа /240). Террейн + дальние LOD-тайлы + - сущности. Вода/полупрозрачное в G-буфер **не** попадает (идёт ванильным путём) — см. план. -3. **GTAO** (полразрешения, `Rg16Float`: окклюзия, дистанция) + depth-aware блюр 5×5. 3 среза × 5 шагов, радиус + сущности. Полупрозрачное в 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): `max(небо·ambient.x, блоки·ambient.y)·AO + солнце·N·L·тень + точечные +4. **Освещение** `fs_light` → `lit` (HDR): пиксель — вода, если `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`, тусклее + под навесом). Френель Шлика 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: история пинг-понгом, в альфе истории — @@ -88,10 +104,13 @@ Bind groups кэшируются и пересоздаются только пр | 10, 11 | лайтмап, линейный сэмплер | | 12 | тайлы источников (storage) | | 13 | AO | +| 14 | глубина воды (`water_depth`) | `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, тайлов в строке). +`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: есть небо). + +Композит воды (`WATER`, group 0): 0 `HdFrame`, 1 `lit`, 2 глубина G-буфера, 3 цвет воды, 4 нормаль воды, 5 глубина воды, 6–7 тени, 8 лайтмап, 9 сэмплер. ## Как проверять @@ -146,6 +165,7 @@ TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс | 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 | | API моделей по хэндлам (#30, уровень API 11: `model-id`, `model-generation`, `set-models-by-id`) | готово | патч 17 | ## План (по порядку) и ТЗ @@ -158,7 +178,7 @@ TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс масштаба (`HORIZON_HD_SCALE` или поле в `HdSettings`). Цель: при 0.67 картинка близка к нативной, FPS заметно выше. Альтернатива на Mac — MetalFX через wgpu-hal (нативный код, только Metal) — отдельной опцией потом. DLSS нереален для нашей цели (только NVIDIA через Streamline, нет в wgpu). -2. **Вода**: отдельный G-буфер-путь для воды (маска + нормаль с анимированными волнами), SSR (трассировка по +2. **Вода** — СДЕЛАНО (патч 19, см. «Вода» ниже). Исходное ТЗ: отдельный G-буфер-путь для воды (маска + нормаль с анимированными волнами), SSR (трассировка по глубине, откат на небо/туман), преломление (смещение по нормали), затухание по глубине воды (цвет биома), блики солнца (GGX-подобный), френель. Подводный вид не ломать (`frame.underwater`). 3. **Небо и атмосфера**: рассеяние Рэлея/Ми (предвычисленная LUT пропускания и небесного цвета в маленькой @@ -174,6 +194,20 @@ TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс 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%). + ## Правила - Ничего не ломать в `off`/`on`; HD выключен — код HD не работает вовсе (`hd = None`). diff --git a/horizon-patches/0001-Mod-API-AirDef-fork-world-models-client-particles-so.patch b/horizon-patches/0001-Mod-API-AirDef-fork-world-models-client-particles-so.patch index 4b64670..c63ed4d 100644 --- a/horizon-patches/0001-Mod-API-AirDef-fork-world-models-client-particles-so.patch +++ b/horizon-patches/0001-Mod-API-AirDef-fork-world-models-client-particles-so.patch @@ -1,7 +1,7 @@ From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 12:37:45 +0300 -Subject: [PATCH 01/18] Mod API (AirDef fork): world models, client particles, +Subject: [PATCH 01/19] Mod API (AirDef fork): world models, client particles, sounds, ground heights render.set-models draws block models with an interpolated pose per id; diff --git a/horizon-patches/0002-cargo-horizon-mod-report-particles-and-sounds.patch b/horizon-patches/0002-cargo-horizon-mod-report-particles-and-sounds.patch index dfaf85b..6cb120b 100644 --- a/horizon-patches/0002-cargo-horizon-mod-report-particles-and-sounds.patch +++ b/horizon-patches/0002-cargo-horizon-mod-report-particles-and-sounds.patch @@ -1,7 +1,7 @@ From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 12:39:45 +0300 -Subject: [PATCH 02/18] cargo-horizon-mod: report particles and sounds +Subject: [PATCH 02/19] cargo-horizon-mod: report particles and sounds Co-Authored-By: Claude Opus 5.5 --- diff --git a/horizon-patches/0003-Mod-API-AirDef-fork-full-screen-post-pass-for-mods.patch b/horizon-patches/0003-Mod-API-AirDef-fork-full-screen-post-pass-for-mods.patch index 8131e6c..c515e69 100644 --- a/horizon-patches/0003-Mod-API-AirDef-fork-full-screen-post-pass-for-mods.patch +++ b/horizon-patches/0003-Mod-API-AirDef-fork-full-screen-post-pass-for-mods.patch @@ -1,7 +1,7 @@ From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 13:33:10 +0300 -Subject: [PATCH 03/18] Mod API (AirDef fork): full-screen post pass for mods +Subject: [PATCH 03/19] Mod API (AirDef fork): full-screen post pass for mods render.set-post compiles a mod's WGSL after a prelude (camera matrices, time, 256 vec4 of params, the world's colour and depth) and runs it after diff --git a/horizon-patches/0004-Mod-models-draw-far-models-as-scaled-impostors-past-.patch b/horizon-patches/0004-Mod-models-draw-far-models-as-scaled-impostors-past-.patch index cfd7b36..00cc772 100644 --- a/horizon-patches/0004-Mod-models-draw-far-models-as-scaled-impostors-past-.patch +++ b/horizon-patches/0004-Mod-models-draw-far-models-as-scaled-impostors-past-.patch @@ -1,7 +1,7 @@ From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 22:09:30 +0300 -Subject: [PATCH 04/18] Mod models: draw far models as scaled impostors past +Subject: [PATCH 04/19] Mod models: draw far models as scaled impostors past the far plane, cull at 4096 --- diff --git a/horizon-patches/0005-Mod-cameras-render.set-views-draws-the-world-from-ot.patch b/horizon-patches/0005-Mod-cameras-render.set-views-draws-the-world-from-ot.patch index 2013ab6..8bbc5e0 100644 --- a/horizon-patches/0005-Mod-cameras-render.set-views-draws-the-world-from-ot.patch +++ b/horizon-patches/0005-Mod-cameras-render.set-views-draws-the-world-from-ot.patch @@ -1,7 +1,7 @@ From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 22:31:00 +0300 -Subject: [PATCH 05/18] Mod cameras: render.set-views draws the world from +Subject: [PATCH 05/19] Mod cameras: render.set-views draws the world from other points into pictures that HUD view shapes show (API level 9) Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0006-Mods-more-fuel-per-tick-and-per-event-AirDef-plans-r.patch b/horizon-patches/0006-Mods-more-fuel-per-tick-and-per-event-AirDef-plans-r.patch index 2483e12..714f195 100644 --- a/horizon-patches/0006-Mods-more-fuel-per-tick-and-per-event-AirDef-plans-r.patch +++ b/horizon-patches/0006-Mods-more-fuel-per-tick-and-per-event-AirDef-plans-r.patch @@ -1,7 +1,7 @@ From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 23:08:21 +0300 -Subject: [PATCH 06/18] Mods: more fuel per tick and per event (AirDef plans +Subject: [PATCH 06/19] Mods: more fuel per tick and per event (AirDef plans raids) Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0007-Mod-cameras-up-to-1920x1080-full-screen-replays.patch b/horizon-patches/0007-Mod-cameras-up-to-1920x1080-full-screen-replays.patch index 5cb0143..785d7e8 100644 --- a/horizon-patches/0007-Mod-cameras-up-to-1920x1080-full-screen-replays.patch +++ b/horizon-patches/0007-Mod-cameras-up-to-1920x1080-full-screen-replays.patch @@ -1,7 +1,7 @@ From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 28 Oct 2026 23:25:29 +0300 -Subject: [PATCH 07/18] Mod cameras up to 1920x1080 (full-screen replays) +Subject: [PATCH 07/19] Mod cameras up to 1920x1080 (full-screen replays) Co-Authored-By: Claude Opus 5.5 --- diff --git a/horizon-patches/0008-Mod-cameras-distant-terrain-tiles-fog-measured-from-.patch b/horizon-patches/0008-Mod-cameras-distant-terrain-tiles-fog-measured-from-.patch index 88c8c40..dd9f5da 100644 --- a/horizon-patches/0008-Mod-cameras-distant-terrain-tiles-fog-measured-from-.patch +++ b/horizon-patches/0008-Mod-cameras-distant-terrain-tiles-fog-measured-from-.patch @@ -1,7 +1,7 @@ From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 11:15:11 +0300 -Subject: [PATCH 08/18] Mod cameras: distant terrain tiles, fog measured from +Subject: [PATCH 08/19] Mod cameras: distant terrain tiles, fog measured from the camera, no impostors while one is shown Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0009-Mods-game.world-id-so-mods-keep-state-per-world-API-.patch b/horizon-patches/0009-Mods-game.world-id-so-mods-keep-state-per-world-API-.patch index 5ba1a4b..3b3cf14 100644 --- a/horizon-patches/0009-Mods-game.world-id-so-mods-keep-state-per-world-API-.patch +++ b/horizon-patches/0009-Mods-game.world-id-so-mods-keep-state-per-world-API-.patch @@ -1,7 +1,7 @@ From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 11:25:45 +0300 -Subject: [PATCH 09/18] Mods: game.world-id, so mods keep state per world (API +Subject: [PATCH 09/19] Mods: game.world-id, so mods keep state per world (API level 9) Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0010-cargo-horizon-mod-trial-runs-behind-feature-full-so-.patch b/horizon-patches/0010-cargo-horizon-mod-trial-runs-behind-feature-full-so-.patch index fc6429c..3e98903 100644 --- a/horizon-patches/0010-cargo-horizon-mod-trial-runs-behind-feature-full-so-.patch +++ b/horizon-patches/0010-cargo-horizon-mod-trial-runs-behind-feature-full-so-.patch @@ -1,7 +1,7 @@ From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 12:01:08 +0300 -Subject: [PATCH 10/18] cargo-horizon-mod: trial runs behind feature full, so a +Subject: [PATCH 10/19] cargo-horizon-mod: trial runs behind feature full, so a packing-only tool skips wasmtime Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0011-HD-lighting-G-buffer-pass-per-pixel-mod-point-lights.patch b/horizon-patches/0011-HD-lighting-G-buffer-pass-per-pixel-mod-point-lights.patch index bf2f5a1..0a2c2bd 100644 --- a/horizon-patches/0011-HD-lighting-G-buffer-pass-per-pixel-mod-point-lights.patch +++ b/horizon-patches/0011-HD-lighting-G-buffer-pass-per-pixel-mod-point-lights.patch @@ -1,7 +1,7 @@ From 0a56f5616df4699588cbcc529d4720661baa810f Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 13:18:43 +0300 -Subject: [PATCH 11/18] HD lighting: G-buffer pass, per-pixel mod point lights +Subject: [PATCH 11/19] HD lighting: G-buffer pass, per-pixel mod point lights in HDR, bloom and tone curve; render.set-lights and render.set-lighting (API level 10) diff --git a/horizon-patches/0012-HD-lighting-stage-2-sun-and-moon-shadow-cascades-the.patch b/horizon-patches/0012-HD-lighting-stage-2-sun-and-moon-shadow-cascades-the.patch index 2afa4e5..2c549c3 100644 --- a/horizon-patches/0012-HD-lighting-stage-2-sun-and-moon-shadow-cascades-the.patch +++ b/horizon-patches/0012-HD-lighting-stage-2-sun-and-moon-shadow-cascades-the.patch @@ -1,7 +1,7 @@ From 9ebf0031fe9ba6b7f02afb26e4fd3a1ba4ddb0d0 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 13:59:43 +0300 -Subject: [PATCH 12/18] HD lighting stage 2: sun and moon shadow cascades, the +Subject: [PATCH 12/19] HD lighting stage 2: sun and moon shadow cascades, the world relit (sun by face angle and shadow, sky and block light through the lightmap), light shafts and halos, fog over it again, a light colour grade diff --git a/horizon-patches/0013-HD-bind-groups-cached-until-a-resize-or-a-new-source.patch b/horizon-patches/0013-HD-bind-groups-cached-until-a-resize-or-a-new-source.patch index 9d68036..4cadab6 100644 --- a/horizon-patches/0013-HD-bind-groups-cached-until-a-resize-or-a-new-source.patch +++ b/horizon-patches/0013-HD-bind-groups-cached-until-a-resize-or-a-new-source.patch @@ -1,7 +1,7 @@ From 7a5a38f68923aec62f10bfa0df50b436474ed612 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 16:19:53 +0300 -Subject: [PATCH 13/18] HD: bind groups cached until a resize or a new source, +Subject: [PATCH 13/19] HD: bind groups cached until a resize or a new source, shadow passes draw only the entities that cast shadows Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0014-HD-lights-culled-per-16x16-tile-surface-and-halo-lis.patch b/horizon-patches/0014-HD-lights-culled-per-16x16-tile-surface-and-halo-lis.patch index 50618c8..13c84d3 100644 --- a/horizon-patches/0014-HD-lights-culled-per-16x16-tile-surface-and-halo-lis.patch +++ b/horizon-patches/0014-HD-lights-culled-per-16x16-tile-surface-and-halo-lis.patch @@ -1,7 +1,7 @@ From 8020811e57d4b5a9ea1cd72d30ce859dc5095d35 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 16:25:44 +0300 -Subject: [PATCH 14/18] HD: lights culled per 16x16 tile (surface and halo +Subject: [PATCH 14/19] HD: lights culled per 16x16 tile (surface and halo lists built on the CPU), a naga test for the lighting shaders Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0015-HD-GPU-time-per-pass-in-the-F3-timing-list-half-reso.patch b/horizon-patches/0015-HD-GPU-time-per-pass-in-the-F3-timing-list-half-reso.patch index 7209aea..0cdd362 100644 --- a/horizon-patches/0015-HD-GPU-time-per-pass-in-the-F3-timing-list-half-reso.patch +++ b/horizon-patches/0015-HD-GPU-time-per-pass-in-the-F3-timing-list-half-reso.patch @@ -1,7 +1,7 @@ From 72d3fddfea329e578162656151350f0aa2efd323 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 16:43:36 +0300 -Subject: [PATCH 15/18] HD: GPU time per pass in the F3 timing list, +Subject: [PATCH 15/19] HD: GPU time per pass in the F3 timing list, half-resolution GTAO with a depth-aware blur Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0016-HD-shadows-draw-only-sun-facing-quads-and-skip-burie.patch b/horizon-patches/0016-HD-shadows-draw-only-sun-facing-quads-and-skip-burie.patch index 1cd4390..6e246ab 100644 --- a/horizon-patches/0016-HD-shadows-draw-only-sun-facing-quads-and-skip-burie.patch +++ b/horizon-patches/0016-HD-shadows-draw-only-sun-facing-quads-and-skip-burie.patch @@ -1,7 +1,7 @@ From 1fd83a821bf270879d8ece5aa583f200ed920d89 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 17:02:02 +0300 -Subject: [PATCH 16/18] HD: shadows draw only sun-facing quads and skip buried +Subject: [PATCH 16/19] HD: shadows draw only sun-facing quads and skip buried sections, the far cascade every second frame; TAA (Halton jitter, reprojection by depth, YCoCg variance clipping) before bloom diff --git a/horizon-patches/0017-Mods-model-handles-render.model-id-model-generation-.patch b/horizon-patches/0017-Mods-model-handles-render.model-id-model-generation-.patch index 4bf4c90..927dc0c 100644 --- a/horizon-patches/0017-Mods-model-handles-render.model-id-model-generation-.patch +++ b/horizon-patches/0017-Mods-model-handles-render.model-id-model-generation-.patch @@ -1,7 +1,7 @@ From 1502d359d599228fe0f44ce6b99a3fded2ac5ea0 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 17:02:02 +0300 -Subject: [PATCH 17/18] Mods: model handles (render.model-id, model-generation, +Subject: [PATCH 17/19] Mods: model handles (render.model-id, model-generation, set-models-by-id; API level 11) so mods send no model names each tick; clear-post and set-lighting documented diff --git a/horizon-patches/0018-HD-temporal-upscaling-HORIZON_HD_SCALE-world-G-buffe.patch b/horizon-patches/0018-HD-temporal-upscaling-HORIZON_HD_SCALE-world-G-buffe.patch index f3cc23c..6bf16b7 100644 --- a/horizon-patches/0018-HD-temporal-upscaling-HORIZON_HD_SCALE-world-G-buffe.patch +++ b/horizon-patches/0018-HD-temporal-upscaling-HORIZON_HD_SCALE-world-G-buffe.patch @@ -1,7 +1,7 @@ From 0f9d99b8ad3e4b72e621d84c2396cf1d3d2b7a33 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 17:19:57 +0300 -Subject: [PATCH 18/18] HD: temporal upscaling (HORIZON_HD_SCALE: world, +Subject: [PATCH 18/19] HD: temporal upscaling (HORIZON_HD_SCALE: world, G-buffer and lighting at a lower size, rebuilt to the window in the TAA pass); fix flickering shadows near the player (the near cascade lost its sections on frames the far one was held, the held far matrix ignored the diff --git a/horizon-patches/0019-HD-water-its-own-pass-colour-normal-depth-the-floor-.patch b/horizon-patches/0019-HD-water-its-own-pass-colour-normal-depth-the-floor-.patch new file mode 100644 index 0000000..323d929 --- /dev/null +++ b/horizon-patches/0019-HD-water-its-own-pass-colour-normal-depth-the-floor-.patch @@ -0,0 +1,982 @@ +From 38250c4392e27d5700842ca6ee47291dd1831b72 Mon Sep 17 00:00:00 2001 +From: chexit +Date: Thu, 29 Oct 2026 17:47:52 +0300 +Subject: [PATCH 19/19] HD water: its own pass (colour, normal, depth), the + floor under it lit, a composite with world-space waves, refraction with depth + absorption in the biome colour, screen-space reflections, Fresnel and a + shadowed sun glint (HORIZON_HD_WATER=0 to switch off) + +Co-Authored-By: Claude Opus 5.5 +--- + crates/horizon-client/src/game.rs | 9 + + crates/horizon-client/src/gfx.rs | 109 +++++- + crates/horizon-client/src/gfx/flat.rs | 4 + + crates/horizon-client/src/gfx/hd.rs | 497 ++++++++++++++++++++++++- + crates/horizon-client/src/terrain.wgsl | 19 + + 5 files changed, 627 insertions(+), 11 deletions(-) + +diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs +index 3a11ee283..5a173977f 100644 +--- a/crates/horizon-client/src/game.rs ++++ b/crates/horizon-client/src/game.rs +@@ -14658,6 +14658,14 @@ fn build_renderer( + prepared, + previous, + api, ++ // AirDef fork, HD water: the layers of the water sprites. ++ ["water_still", "water_flow"].map(|name| { ++ assets ++ .sprite_layers ++ .get(&format!("minecraft:block/{name}")) ++ .copied() ++ .unwrap_or(0xffff) ++ }), + ))?; + renderer.hud.sprite_animations = atlas + .sprite_animations +@@ -14717,6 +14725,7 @@ fn loading_renderer( + None, + previous, + api, ++ [0xffff; 2], + ))?; + if let Some(atlas) = atlas { + renderer.hud.sprite_animations = atlas +diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs +index b85c6e269..f9dab8abf 100644 +--- a/crates/horizon-client/src/gfx.rs ++++ b/crates/horizon-client/src/gfx.rs +@@ -564,6 +564,8 @@ pub struct Renderer { + mod_views: Option, + /// AirDef fork: the HD lighting's G-buffer pipelines (terrain, entities) and its targets. + gbuffer_terrain: wgpu::RenderPipeline, ++ /// The water quads alone into the HD water targets (section draws; `flat.water` for flat ones). ++ gbuffer_water: wgpu::RenderPipeline, + gbuffer_entity: wgpu::RenderPipeline, + /// The HD sun shadows: depth only, from the light, and the sections in its farther cascade. + shadow_terrain: wgpu::RenderPipeline, +@@ -1083,6 +1085,7 @@ impl Renderer { + prepared: Option, + previous: Option<&Self>, + api: GraphicsApi, ++ water_layers: [u32; 2], + ) -> Result { + let split = sprite_split(); + if layers > split * SPRITE_ARRAYS { +@@ -1205,6 +1208,13 @@ impl Renderer { + ("SPRITE_ARRAYS", f64::from(sprite_arrays)), + ("SPRITE_SPLIT", f64::from(split)), + ]; ++ // AirDef fork, HD water: the sprite layers terrain.wgsl's fs_water takes for water. ++ let water_constants = [ ++ ("SPRITE_ARRAYS", f64::from(sprite_arrays)), ++ ("SPRITE_SPLIT", f64::from(split)), ++ ("WATER_STILL", f64::from(water_layers[0])), ++ ("WATER_FLOW", f64::from(water_layers[1])), ++ ]; + let bindings = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Horizon terrain bindings"), + entries: &[ +@@ -1420,6 +1430,55 @@ impl Renderer { + multiview_mask: None, + cache: None, + }); ++ // AirDef fork, HD water: only the water quads (`fs_water`), into the colour and normal ++ // targets and the water's own depth; `vertex` is the section or the flat vertex stage. ++ let water_targets = [ ++ Some(wgpu::ColorTargetState { ++ format: hd::WATER_TINT, ++ blend: None, ++ write_mask: wgpu::ColorWrites::ALL, ++ }), ++ Some(wgpu::ColorTargetState { ++ format: hd::WATER_NORMAL, ++ blend: None, ++ write_mask: wgpu::ColorWrites::ALL, ++ }), ++ ]; ++ let water_pipeline = ++ |layout: &wgpu::PipelineLayout, ++ label: &str, ++ vertex: &str, ++ buffers: &[Option>]| { ++ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { ++ label: Some(label), ++ layout: Some(layout), ++ vertex: wgpu::VertexState { ++ module: &shader, ++ entry_point: Some(vertex), ++ compilation_options: Default::default(), ++ buffers, ++ }, ++ primitive: wgpu::PrimitiveState { ++ cull_mode: Some(wgpu::Face::Back), ++ ..Default::default() ++ }, ++ depth_stencil: gbuffer_depth.clone(), ++ multisample: Default::default(), ++ fragment: Some(wgpu::FragmentState { ++ module: &shader, ++ entry_point: Some("fs_water"), ++ compilation_options: wgpu::PipelineCompilationOptions { ++ constants: &water_constants, ++ ..Default::default() ++ }, ++ targets: &water_targets, ++ }), ++ multiview_mask: None, ++ cache: None, ++ }) ++ }; ++ let gbuffer_water = ++ water_pipeline(&layout, "HD water terrain", "vs_terrain", &terrain_buffers); + let shadow_terrain = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("HD shadows terrain"), + layout: Some(&layout), +@@ -2597,6 +2656,7 @@ impl Renderer { + &device, + &bindings, + arena.page_vertices(), ++ |layout, label| water_pipeline(layout, label, "vs_terrain_flat", &buffers), + |layout, label, fragment, blend| { + pipeline_in( + layout, +@@ -2816,6 +2876,7 @@ impl Renderer { + mod_post_hdr: false, + mod_views: None, + gbuffer_terrain, ++ gbuffer_water, + gbuffer_entity, + shadow_terrain, + shadow_entity, +@@ -4204,6 +4265,16 @@ impl Renderer { + fog_color, + fog, + sky: frame.skybox && !frame.end_sky && !frame.nether_lighting && !frame.underwater, ++ sky_color: [ ++ frame.sky.sky_color[0], ++ frame.sky.sky_color[1], ++ frame.sky.sky_color[2], ++ ], ++ camera_mod: [ ++ camera.x.rem_euclid(256.0) as f32, ++ camera.z.rem_euclid(256.0) as f32, ++ ], ++ water: !frame.underwater, + }; + let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene); + let mut hd_cascades = hd::cascades(camera, shadow_dir); +@@ -5058,6 +5129,11 @@ impl Renderer { + .hd + .get_or_insert_with(|| hd::Hd::new(device, size, format, globals_size)); + hd.resize(device, size, [self.config.width, self.config.height]); ++ // Translucent terrain on screen: the water pass has something to draw. ++ hd.water_geometry = match self.flat.as_ref().filter(|_| flat_frame) { ++ Some(flat) => !flat.translucent_calls().is_empty(), ++ None => translucent_draws > 0, ++ }; + if let Some(settings) = frame.hd { + hd.write(&self.queue, &hd_scene, frame.hd_lights, settings); + } +@@ -5186,6 +5262,32 @@ impl Renderer { + drop(pass); + // Contact shadows from the G-buffer's depth and normals, for the lighting. + hd.ao(&mut encoder); ++ // The water quads alone, with their own depth: the lighting finds the water by it and ++ // the composite after the lighting lays the surface over the lit floor. ++ if hd.water_active() { ++ let mut pass = hd.water_pass(&mut encoder); ++ pass.set_bind_group(0, &self.group, &[]); ++ match self.flat.as_ref().filter(|_| flat_frame) { ++ Some(flat) => { ++ pass.set_pipeline(&flat.water); ++ flat.draw(&mut pass, flat.translucent_calls(), |p| { ++ self.arena.buffer(p) ++ }); ++ } ++ None => { ++ pass.set_pipeline(&self.gbuffer_water); ++ pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32); ++ pass.set_vertex_buffer(1, self.instances.slice(..)); ++ self.draw_sections( ++ &mut pass, ++ &self.indirect, ++ 0, ++ solid_draws, ++ translucent_draws, ++ ); ++ } ++ } ++ } + } else if !hd_on { + self.hd = None; + } +@@ -5721,6 +5823,11 @@ impl Renderer { + (color, depth), + (&self.lightmap_view, &self.lightmap_sampler), + ); ++ hd.water_composite( ++ &self.device, ++ &mut encoder, ++ (&self.lightmap_view, &self.lightmap_sampler), ++ ); + match &mut self.mod_post { + Some(post) if mod_post => { + let size = self.world_post.size().unwrap_or([1, 1]); +@@ -5731,7 +5838,7 @@ impl Renderer { + &self.mod_params.0, + self.mod_params.1, + ); +- post.run(&self.device, &mut encoder, (&hd.lit, depth), &hd.fx); ++ post.run(&self.device, &mut encoder, (hd.lit_view(), depth), &hd.fx); + let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Fx, depth); + hd.finish(&self.device, &mut encoder, source, &view_target); + } +diff --git a/crates/horizon-client/src/gfx/flat.rs b/crates/horizon-client/src/gfx/flat.rs +index 5d86888da..7e2e20cad 100644 +--- a/crates/horizon-client/src/gfx/flat.rs ++++ b/crates/horizon-client/src/gfx/flat.rs +@@ -64,6 +64,8 @@ pub struct Call { + pub struct Flat { + pub solid: wgpu::RenderPipeline, + pub translucent: wgpu::RenderPipeline, ++ /// The water quads alone, into HD's water targets. ++ pub water: wgpu::RenderPipeline, + flatten: wgpu::ComputePipeline, + compute_layout: wgpu::BindGroupLayout, + flatten_seen: wgpu::ComputePipeline, +@@ -254,6 +256,7 @@ impl Flat { + device: &wgpu::Device, + terrain_layout: &wgpu::BindGroupLayout, + page_vertices: u64, ++ water: impl Fn(&wgpu::PipelineLayout, &str) -> wgpu::RenderPipeline, + pipeline: impl Fn( + &wgpu::PipelineLayout, + &str, +@@ -320,6 +323,7 @@ impl Flat { + "fs_translucent", + Some(wgpu::BlendState::ALPHA_BLENDING), + ), ++ water: water(&layout, "HD water terrain (flat)"), + flatten, + compute_layout, + flatten_seen, +diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs +index c54b7b961..f5caa2d0b 100644 +--- a/crates/horizon-client/src/gfx/hd.rs ++++ b/crates/horizon-client/src/gfx/hd.rs +@@ -16,6 +16,9 @@ pub const NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; + pub const LIGHT_LEVELS: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; + /// The lit world and the post pass's output. + pub const HDR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; ++/// The water pass's targets: the surface colour with the block light, and the normal with the sky light. ++pub const WATER_TINT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; ++pub const WATER_NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; + /// The most lights drawn a frame. + pub const MAX_LIGHTS: usize = 512; + /// The side (pixels) of a tile of the screen with its own short list of lights. +@@ -91,6 +94,13 @@ struct HdFrame { + taa: [f32; 4], + /// x: the history's weight; y: the sharpening (0 without TAA); zw: the jitter in world pixels. + taa_params: [f32; 4], ++ /// The (jittered) world-to-clip matrix the world was drawn with (rays in the water pass). ++ view_proj: [[f32; 4]; 4], ++ /// x: the water effect runs this frame; y: seconds for the waves (wraps at 600); zw: the ++ /// camera's x and z modulo 256, the waves' period. ++ water: [f32; 4], ++ /// rgb: the sky's colour; w: 1 when there is a sky. ++ sky_color: [f32; 4], + } + + /// What the world around looks like this frame (from the renderer's globals). +@@ -116,6 +126,11 @@ pub struct HdScene { + pub fog: [f32; 4], + /// The sky has a sun at all (the Overworld). + pub sky: bool, ++ pub sky_color: [f32; 3], ++ /// The camera's x and z modulo 256 (the waves' period), so the waves hold still in the world. ++ pub camera_mod: [f32; 2], ++ /// The water effect may run (not underwater, not disabled). ++ pub water: bool, + } + + /// The cascades' half sizes (blocks) and shadow map size. +@@ -137,6 +152,9 @@ struct HdFrame { + prev_view_proj: mat4x4, + taa: vec4, + taa_params: vec4, ++ view_proj: mat4x4, ++ water: vec4, ++ sky_color: vec4, + } + struct FullOut { + @builtin(position) position: vec4, +@@ -175,6 +193,8 @@ struct Light { + @group(0) @binding(12) var tiles: array; + // Ambient occlusion at half resolution: x the occlusion (1 open), y the distance it was taken at. + @group(0) @binding(13) var ao_tex: texture_2d; ++// The water pass's depth: the nearest water surface (0 where there is none). ++@group(0) @binding(14) var water_depth: texture_depth_2d; + + // The occlusion at a full-resolution pixel `dist` away: the four nearest half-resolution texels + // weighted by closeness and by how near their distance is, so it doesn't bleed across edges. +@@ -216,6 +236,12 @@ fn cascade_at(p: vec3, c: i32) -> vec3 { + + // The sun's (or moon's) light reaching p: 1 lit, 0 in shadow; soft-edged. + fn shadow(p: vec3, n: vec3) -> f32 { ++ return shadow_taps(p, n, 1); ++} ++ ++// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and ++// blurs it anyway). ++fn shadow_taps(p: vec3, n: vec3, r: i32) -> f32 { + let dist = length(p); + let c = select(1, 0, dist < frame.shadow.y * 0.92); + let texel = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0; +@@ -224,13 +250,13 @@ fn shadow(p: vec3, n: vec3) -> f32 { + if any(s.xy < vec2(0.0)) || any(s.xy > vec2(1.0)) || s.z > 1.0 { return 1.0; } + var lit = 0.0; + let step = 1.0 / 2048.0; +- for (var y = -1; y <= 1; y++) { +- for (var x = -1; x <= 1; x++) { ++ for (var y = -r; y <= r; y++) { ++ for (var x = -r; x <= r; x++) { + lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2(f32(x), f32(y)) * step, c, s.z - 0.0005); + } + } + let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0); +- return mix(1.0, lit / 9.0, fade); ++ return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade); + } + + // One shadow sample, for the rays through the air. +@@ -252,7 +278,15 @@ fn fs_light(in: FullOut) -> @location(0) vec4 { + let scene = textureLoad(scene_color, px, 0).rgb; + let d = textureLoad(scene_depth, px, 0); + let g = textureLoad(gbuffer_depth, px, 0); +- let ndc = vec4(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(d, 0.000001), 1.0); ++ // Water in front of the opaque world with nothing translucent over it: here the floor is lit ++ // and the water composite lays the surface over it (it knows the pixel by the alpha 0.5). ++ var water = false; ++ if frame.water.x > 0.5 && g > 0.0 { ++ let wd = textureLoad(water_depth, px, 0); ++ water = wd > 0.0 && wd >= g && d - wd <= d * 0.002; ++ } ++ let dd = select(d, g, water); ++ let ndc = vec4(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(dd, 0.000001), 1.0); + let h = frame.inverse_view_proj * ndc; + let p = h.xyz / h.w; + let view = normalize(p); +@@ -267,7 +301,7 @@ fn fs_light(in: FullOut) -> @location(0) vec4 { + let halo_all = tiles[tile + 3u] == 0xffffffffu; + let halo_count = select(tiles[tile + 3u], count, halo_all); + var c = scene; +- let surface = d > 0.0 && abs(d - g) <= max(d, g) * 0.002; ++ let surface = water || (d > 0.0 && abs(d - g) <= max(d, g) * 0.002); + if surface { + let n = normalize(textureLoad(normal_tex, px, 0).xyz); + let albedo = textureLoad(albedo_tex, px, 0); +@@ -278,14 +312,15 @@ fn fs_light(in: FullOut) -> @location(0) vec4 { + let sky_reach = smoothstep(0.35, 0.95, lm.x); + var s = 1.0; + let ndl = dot(n, sun); +- if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow(p, n); } else { s = 0.0; } ++ if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow_taps(p, n, select(1, 0, water)); } else { s = 0.0; } + // Shadowed sky light (ambient) keeps vanilla's look, blocks' light stays as is; the sun adds + // direct light by the face's angle. + let direct = frame.sun_color.rgb * sun_on * max(ndl, 0.0) * s * sky_reach; + // Contact shadows from the ambient occlusion: on the ambient and blocks' light fully, on + // the mods' lights by half, not on the sun or on emission. + var occlusion = 1.0; +- if frame.ambient.w > 0.0 { occlusion = ao_at(px, length(p)); } ++ // (The water dims and blurs the floor under it: no occlusion there.) ++ if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); } + let ao_ambient = mix(1.0, occlusion, frame.ambient.w); + let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5); + var light = max(base * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct; +@@ -334,7 +369,253 @@ fn fs_light(in: FullOut) -> @location(0) vec4 { + let a1 = atan((far - t0) / hh); + c += l.color.rgb * l.color.w * (a1 - a0) / hh * 0.004 * (0.3 + density * 300.0); + } +- return vec4(c, 1.0); ++ return vec4(c, select(1.0, 0.5, water)); ++} ++"#; ++ ++const WATER: &str = r#" ++const PI: f32 = 3.14159265; ++const REFLECT_STEPS: i32 = 24; ++const REFLECT_RANGE: f32 = 96.0; ++@group(0) @binding(0) var frame: HdFrame; ++@group(0) @binding(1) var lit_tex: texture_2d; ++@group(0) @binding(2) var gbuffer_depth: texture_depth_2d; ++@group(0) @binding(3) var water_tint: texture_2d; ++@group(0) @binding(4) var water_normal: texture_2d; ++@group(0) @binding(5) var water_depth: texture_depth_2d; ++@group(0) @binding(6) var shadow_map: texture_depth_2d_array; ++@group(0) @binding(7) var shadow_sampler: sampler_comparison; ++@group(0) @binding(8) var lightmap: texture_2d; ++@group(0) @binding(9) var linear: sampler; ++ ++fn ign(px: vec2) -> f32 { ++ let p = px + vec2(5.588238 * frame.params.w); ++ return fract(52.9829189 * fract(dot(p, vec2(0.06711056, 0.00583715)))); ++} ++ ++// The camera-relative position of the screen point `uv` at depth z. ++fn position_at(uv: vec2, z: f32) -> vec3 { ++ let h = frame.inverse_view_proj * vec4(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0); ++ return h.xyz / h.w; ++} ++ ++// The screen point (uv, clip w) of a camera-relative position. ++fn project(p: vec3) -> vec3 { ++ let c = frame.view_proj * vec4(p, 1.0); ++ return vec3(c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5, c.w); ++} ++ ++fn on_screen(uv: vec2) -> bool { ++ return all(uv > vec2(0.0)) && all(uv < vec2(1.0)); ++} ++ ++fn texel(uv: vec2) -> vec2 { ++ return clamp(vec2(uv * frame.resolution.xy), vec2(0), vec2(frame.resolution.xy) - 1); ++} ++ ++// Sine waves in world space: wave vectors that are whole multiples of 2 pi / 256 and speeds that ++// are whole cycles per 600 s, so the sum repeats every 256 blocks and 10 minutes and holds still ++// in the world as the camera moves. x, y: the vector in units of 2 pi / 256; z: the slope the ++// wave adds; w: its phase. ++const WAVES = array, 10>( ++ vec4(3.0, 1.0, 0.040, 0.3), ++ vec4(-2.0, 5.0, 0.045, 2.1), ++ vec4(7.0, 2.0, 0.040, 4.4), ++ vec4(-5.0, -9.0, 0.040, 1.2), ++ vec4(11.0, -4.0, 0.035, 5.6), ++ vec4(13.0, 10.0, 0.035, 3.3), ++ vec4(-17.0, 7.0, 0.030, 0.9), ++ vec4(9.0, 23.0, 0.030, 2.7), ++ vec4(-27.0, 19.0, 0.025, 4.9), ++ vec4(31.0, 30.0, 0.025, 1.8), ++); ++ ++// The gradient of the water's height at world x, z: the waves that a pixel `footprint` blocks ++// wide can still resolve (finer ones fade out, or they would shimmer). ++fn wave_gradient(xz: vec2, footprint: f32) -> vec2 { ++ var grad = vec2(0.0); ++ for (var i = 0; i < 10; i++) { ++ let w = WAVES[i]; ++ let k = w.xy * (2.0 * PI / 256.0); ++ let len = length(k); ++ let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.25 - 0.5, 0.0, 1.0); ++ let cycles = round(sqrt(9.8 * len) * 0.5 * 600.0 / (2.0 * PI)); ++ let omega = cycles * 2.0 * PI / 600.0; ++ let phase = dot(k, xz) - omega * frame.water.y + w.w; ++ grad += (k / len) * (w.z * fade * cos(phase)); ++ } ++ return grad; ++} ++ ++fn cascade_at(p: vec3, c: i32) -> vec3 { ++ let s = frame.cascades[c] * vec4(p, 1.0); ++ return vec3(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z); ++} ++ ++// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow (as the lighting pass has it). ++fn shadow(p: vec3, n: vec3) -> f32 { ++ let dist = length(p); ++ let c = select(1, 0, dist < frame.shadow.y * 0.92); ++ let texel_size = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0; ++ let q = p + n * texel_size * 1.5; ++ let s = cascade_at(q, c); ++ if any(s.xy < vec2(0.0)) || any(s.xy > vec2(1.0)) || s.z > 1.0 { return 1.0; } ++ var lit = 0.0; ++ let step = 1.0 / 2048.0; ++ for (var y = -1; y <= 1; y++) { ++ for (var x = -1; x <= 1; x++) { ++ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2(f32(x), f32(y)) * step, c, s.z - 0.0005); ++ } ++ } ++ let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0); ++ return mix(1.0, lit / 9.0, fade); ++} ++ ++fn linear_fog(d: f32, start: f32, end: f32) -> f32 { ++ if d <= start { return 0.0; } ++ if d >= end { return 1.0; } ++ return (d - start) / (end - start); ++} ++ ++// What the sky shows along r when the ray leaves the screen: the horizon's colour rising to ++// the zenith's, darker where the water lies under cover (`vis` is the sky light there). ++fn sky_along(r: vec3, vis: f32) -> vec3 { ++ var c = frame.fog_color.rgb; ++ if frame.sky_color.w > 0.5 { ++ c = mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, r.y)); ++ } ++ return c * mix(0.12, 1.0, vis); ++} ++ ++// Marches the ray from p along r over the depth buffer (28 steps, growing, jittered so TAA ++// averages the placement; then a binary search for the crossing). xyz: the colour found, ++// w: how much to trust it (0 for a miss, less near the screen's edge). ++fn reflection(p: vec3, r: vec3, jitter: f32) -> vec4 { ++ var near = 0.0; ++ for (var i = 0; i < REFLECT_STEPS; i++) { ++ let u = (f32(i) + jitter) / f32(REFLECT_STEPS); ++ let s = 0.12 + REFLECT_RANGE * u * u; ++ let q = p + r * s; ++ let sp = project(q); ++ if sp.z <= 0.0 || !on_screen(sp.xy) { break; } ++ let z = textureLoad(gbuffer_depth, texel(sp.xy), 0); ++ if z > 0.0 { ++ let c = frame.view_proj * vec4(q, 1.0); ++ // Reversed Z: a larger depth is nearer; the ray is behind the surface when the ++ // surface is nearer than it. ++ if z > c.z / c.w { ++ let behind = length(q) - length(position_at(sp.xy, z)); ++ if behind < 0.4 + 0.08 * s { ++ var lo = near; ++ var hi = s; ++ for (var k = 0; k < 5; k++) { ++ let m = 0.5 * (lo + hi); ++ let mq = p + r * m; ++ let mp = project(mq); ++ let mc = frame.view_proj * vec4(mq, 1.0); ++ let mz = textureLoad(gbuffer_depth, texel(mp.xy), 0); ++ if mz > mc.z / mc.w { hi = m; } else { lo = m; } ++ } ++ let hp = project(p + r * hi); ++ let edge = min(min(hp.x, 1.0 - hp.x), min(hp.y, 1.0 - hp.y)); ++ let colour = textureSampleLevel(lit_tex, linear, hp.xy, 0.0).rgb; ++ return vec4(colour, smoothstep(0.0, 0.08, edge)); ++ } ++ } ++ } ++ near = s; ++ } ++ return vec4(0.0); ++} ++ ++// The sun's glint on the waves (GGX, a rough lobe so it glitters instead of aliasing). ++fn glint(n: vec3, v: vec3, l: vec3) -> f32 { ++ let h = normalize(l + v); ++ let a = 0.05; ++ let nh = max(dot(n, h), 0.0); ++ let d = a * a / (PI * pow(nh * nh * (a * a - 1.0) + 1.0, 2.0)); ++ let f = 0.02 + 0.98 * pow(1.0 - max(dot(v, h), 0.0), 5.0); ++ return min(d * f * 0.25 * max(dot(n, l), 0.0) / max(dot(n, v), 0.1), 12.0); ++} ++ ++// The water over the lit world: the world behind it (refracted by the waves, dimmed and tinted ++// by the water's depth), the sky and the world above it (a screen-space reflection) by Fresnel, ++// and the sun's glint. Pixels without water pass through. ++@fragment ++fn fs_water(in: FullOut) -> @location(0) vec4 { ++ let px = vec2(in.position.xy); ++ let base = textureLoad(lit_tex, px, 0); ++ if base.a > 0.75 { return vec4(base.rgb, 1.0); } ++ let wd = textureLoad(water_depth, px, 0); ++ let g = textureLoad(gbuffer_depth, px, 0); ++ let tint = textureLoad(water_tint, px, 0); ++ let nt = textureLoad(water_normal, px, 0); ++ let p = position_at(in.uv, wd); ++ let dist = length(p); ++ let v = -p / dist; ++ let geometric = normalize(nt.xyz * 2.0 - 1.0); ++ // The waves tilt the flat parts of the surface only. ++ let footprint = dist * frame.resolution.w * 1.4; ++ let grad = wave_gradient(p.xz + frame.water.zw, footprint); ++ let flat_part = smoothstep(0.6, 0.95, geometric.y); ++ var n = normalize(geometric + vec3(-grad.x, 0.0, -grad.y) * flat_part); ++ n = normalize(n + v * max(0.03 - dot(n, v), 0.0)); ++ let ndv = max(dot(n, v), 0.02); ++ ++ // Light on the water: vanilla's lightmap at the surface, and the sun where it reaches. ++ let levels = vec2(nt.a, tint.a); ++ let sky_reach = smoothstep(0.35, 0.95, levels.x); ++ let ambient = textureSampleLevel(lightmap, linear, clamp(vec2(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2(0.5 / 16.0), vec2(15.5 / 16.0)), 0.0).rgb; ++ let block = textureSampleLevel(lightmap, linear, vec2(levels.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb; ++ let sun = frame.sun.xyz; ++ let sun_on = frame.sun.w; ++ var sunlit = 0.0; ++ if sun_on > 0.0 && sun.y > 0.0 && sky_reach > 0.0 { sunlit = shadow(p, vec3(0.0, 1.0, 0.0)) * sky_reach; } ++ let volume_light = max(ambient * frame.ambient.x, block * frame.ambient.y) + frame.sun_color.rgb * sun_on * sunlit * 0.8; ++ ++ // Refraction: where the ray through the waves meets the world, against where the flat surface ++ // would send it (so still water looks as vanilla's and only the waves distort). ++ let floor0 = position_at(in.uv, g); ++ let depth0 = max(length(floor0) - dist, 0.0); ++ let bent = refract(-v, n, 0.75) - refract(-v, geometric, 0.75); ++ var uv_floor = in.uv; ++ var floor_z = g; ++ let moved = project(floor0 + bent * min(depth0, 24.0)); ++ if moved.z > 0.0 && on_screen(moved.xy) { ++ let z = textureLoad(gbuffer_depth, texel(moved.xy), 0); ++ // Only what lies behind the water: not the bank or an entity in front of it. ++ if z > 0.0 && z <= wd { ++ uv_floor = moved.xy; ++ floor_z = z; ++ } ++ } ++ let thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0); ++ let behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb; ++ let colour = clamp(tint.rgb, vec3(0.03), vec3(1.0)); ++ // The colours the water takes up fastest are the ones it has the least of. ++ let absorb = exp(-(vec3(1.0) - colour) * 0.8 * (thickness + 0.3)); ++ let refracted = behind * absorb + colour * volume_light * 0.9 * (vec3(1.0) - absorb); ++ ++ // Reflection: the screen where the ray stays on it, the sky where it leaves it or misses. ++ var r = reflect(-v, n); ++ r.y = abs(r.y); ++ // Looking down onto it the reflection is a few percent of the picture: the sky alone does. ++ let fresnel = 0.02 + 0.98 * pow(1.0 - ndv, 5.0); ++ var hit = vec4(0.0); ++ if fresnel > 0.04 { hit = reflection(p, r, ign(in.position.xy)); } ++ let sky_vis = mix(0.15, 1.0, sky_reach); ++ let reflected = mix(sky_along(r, sky_vis), hit.rgb, hit.w); ++ ++ var c = refracted * (1.0 - fresnel) + reflected * fresnel; ++ if sun_on > 0.0 && sunlit > 0.0 { ++ c += frame.sun_color.rgb * sun_on * sunlit * glint(n, v, sun); ++ } ++ // Vanilla's fog over it, at the surface. ++ let sph = dist; ++ let cyl = max(length(p.xz), abs(p.y)); ++ let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w)); ++ c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a); ++ return vec4(max(c, vec3(0.0)), 1.0); + } + "#; + +@@ -829,6 +1110,12 @@ pub fn taa_enabled() -> bool { + *ON.get_or_init(|| std::env::var("HORIZON_HD_TAA").map_or(true, |v| v != "0")) + } + ++/// The water effect is on unless `HORIZON_HD_WATER=0`. ++pub fn water_enabled() -> bool { ++ static ON: std::sync::OnceLock = std::sync::OnceLock::new(); ++ *ON.get_or_init(|| std::env::var("HORIZON_HD_WATER").map_or(true, |v| v != "0")) ++} ++ + /// The Halton sequence's `index`-th number in `base`, in (0, 1). + fn halton(mut index: u32, base: u32) -> f32 { + let (mut f, mut r) = (1.0, 0.0); +@@ -895,6 +1182,22 @@ pub struct Hd { + /// The lit world, and the post pass's output. + pub lit: wgpu::TextureView, + pub fx: wgpu::TextureView, ++ /// Water: its surface colour and normal, its depth (the nearest water surface; 0 elsewhere), ++ /// the lit world with the water over it, and what draws and composites them. ++ water_tint: wgpu::TextureView, ++ water_normal: wgpu::TextureView, ++ water_depth: wgpu::TextureView, ++ lit_water: wgpu::TextureView, ++ water_layout: wgpu::BindGroupLayout, ++ water_pipeline: wgpu::RenderPipeline, ++ water_group: Option<((wgpu::TextureView, wgpu::Sampler), wgpu::BindGroup)>, ++ /// The water effect runs this frame (the scene allows it and there is translucent terrain on ++ /// screen), and `lit_view` is the composite. ++ water_active: bool, ++ /// The frame's translucent terrain exists (set by the renderer before `write`). ++ pub water_geometry: bool, ++ water_composited: bool, ++ started: std::time::Instant, + /// TAA: the resolved frame, and the two histories (one read, one written, alternating). + taa: wgpu::TextureView, + history: [wgpu::TextureView; 2], +@@ -1216,6 +1519,26 @@ impl Hd { + min_binding_size: None, + }, + UNFILTERED, ++ DEPTH, ++ ]), ++ }); ++ let water_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { ++ label: Some("HD water"), ++ entries: &entries(&[ ++ UNIFORM, ++ FLOAT, ++ DEPTH, ++ UNFILTERED, ++ UNFILTERED, ++ DEPTH, ++ wgpu::BindingType::Texture { ++ sample_type: wgpu::TextureSampleType::Depth, ++ view_dimension: wgpu::TextureViewDimension::D2Array, ++ multisampled: false, ++ }, ++ wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), ++ FLOAT, ++ SAMPLER, + ]), + }); + let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { +@@ -1278,6 +1601,16 @@ impl Hd { + let tone = module(device, "HD tone mapping", TONEMAP); + let ao = module(device, "HD ambient occlusion", AO); + let ao_blur = module(device, "HD ambient occlusion blur", AO_BLUR); ++ let water = module(device, "HD water", WATER); ++ let water_pipeline = fullscreen( ++ device, ++ "HD water", ++ &water_layout, ++ &water, ++ "fs_water", ++ HDR, ++ None, ++ ); + let ao_pipeline = fullscreen( + device, + "HD ambient occlusion", +@@ -1436,6 +1769,23 @@ impl Hd { + .view, + lit: target(device, "HD lit", [1, 1], HDR).view, + fx: target(device, "HD post", [1, 1], HDR).view, ++ water_tint: target(device, "HD water colour", [1, 1], WATER_TINT).view, ++ water_normal: target(device, "HD water normal", [1, 1], WATER_NORMAL).view, ++ water_depth: target( ++ device, ++ "HD water depth", ++ [1, 1], ++ wgpu::TextureFormat::Depth32Float, ++ ) ++ .view, ++ lit_water: target(device, "HD lit with water", [1, 1], HDR).view, ++ water_layout, ++ water_pipeline, ++ water_group: None, ++ water_active: false, ++ water_geometry: false, ++ water_composited: false, ++ started: std::time::Instant::now(), + taa: target(device, "HD TAA", [1, 1], HDR).view, + history: [ + target(device, "HD TAA history", [1, 1], HDR).view, +@@ -1502,6 +1852,17 @@ impl Hd { + self.depth = target(device, "HD depth", size, wgpu::TextureFormat::Depth32Float).view; + self.lit = target(device, "HD lit", size, HDR).view; + self.fx = target(device, "HD post", size, HDR).view; ++ self.water_tint = target(device, "HD water colour", size, WATER_TINT).view; ++ self.water_normal = target(device, "HD water normal", size, WATER_NORMAL).view; ++ self.water_depth = target( ++ device, ++ "HD water depth", ++ size, ++ wgpu::TextureFormat::Depth32Float, ++ ) ++ .view; ++ self.lit_water = target(device, "HD lit with water", size, HDR).view; ++ self.water_group = None; + self.taa = target(device, "HD TAA", out, HDR).view; + self.history = [ + target(device, "HD TAA history", out, HDR).view, +@@ -1591,6 +1952,7 @@ impl Hd { + let (dir, strength, color) = light_of(scene); + let cascades = scene.cascades; + let night = scene.dark.clamp(0.0, 1.0); ++ self.water_active = scene.water && water_enabled() && self.water_geometry; + self.taa_active = scene.taa && taa_enabled(); + // A long jump of the camera (a teleport) starts the history over. + if !self.taa_active || scene.camera_delta.length() > 64.0 { +@@ -1604,6 +1966,19 @@ impl Hd { + scene.camera_delta.z, + if self.history_ready { 1.0 } else { 0.0 }, + ], ++ view_proj: scene.view_proj.to_cols_array_2d(), ++ water: [ ++ if self.water_active { 1.0 } else { 0.0 }, ++ (self.started.elapsed().as_secs_f64() % 600.0) as f32, ++ scene.camera_mod[0], ++ scene.camera_mod[1], ++ ], ++ sky_color: [ ++ scene.sky_color[0], ++ scene.sky_color[1], ++ scene.sky_color[2], ++ if scene.sky { 1.0 } else { 0.0 }, ++ ], + taa_params: [ + if self.upscaling() { + UPSCALE_HISTORY +@@ -1821,6 +2196,10 @@ impl Hd { + binding: 13, + resource: wgpu::BindingResource::TextureView(&self.ao_blurred), + }, ++ wgpu::BindGroupEntry { ++ binding: 14, ++ resource: wgpu::BindingResource::TextureView(&self.water_depth), ++ }, + ], + }); + self.light_group = Some((key, group)); +@@ -1828,6 +2207,7 @@ impl Hd { + let Some((_, group)) = &self.light_group else { + return; + }; ++ self.water_composited = false; + Self::draw( + encoder, + "HD lighting", +@@ -1894,7 +2274,7 @@ impl Hd { + screen: &wgpu::TextureView, + ) { + let source = match source { +- HdSource::Lit => self.lit.clone(), ++ HdSource::Lit => self.lit_view().clone(), + HdSource::Fx => self.fx.clone(), + HdSource::Taa => self.taa.clone(), + }; +@@ -1993,7 +2373,7 @@ impl Hd { + return source; + } + let input = match source { +- HdSource::Lit => self.lit.clone(), ++ HdSource::Lit => self.lit_view().clone(), + HdSource::Fx => self.fx.clone(), + HdSource::Taa => return source, + }; +@@ -2052,6 +2432,102 @@ impl Hd { + HdSource::Taa + } + ++ /// The lit world as later passes see it: with the water over it when the effect ran. ++ pub fn lit_view(&self) -> &wgpu::TextureView { ++ if self.water_composited { ++ &self.lit_water ++ } else { ++ &self.lit ++ } ++ } ++ ++ /// The water effect runs this frame (the renderer draws the water quads with `water_pass`). ++ pub fn water_active(&self) -> bool { ++ self.water_active ++ } ++ ++ /// The water pass's attachments: the surface colour and normal, and the water's own depth, ++ /// all cleared. The caller draws the translucent terrain with `fs_water`. ++ pub fn water_pass<'e>( ++ &mut self, ++ encoder: &'e mut wgpu::CommandEncoder, ++ ) -> wgpu::RenderPass<'e> { ++ let clear = |view| { ++ Some(wgpu::RenderPassColorAttachment { ++ view, ++ resolve_target: None, ++ depth_slice: None, ++ ops: wgpu::Operations { ++ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), ++ store: wgpu::StoreOp::Store, ++ }, ++ }) ++ }; ++ encoder ++ .begin_render_pass(&wgpu::RenderPassDescriptor { ++ label: Some("HD water"), ++ timestamp_writes: stamp(&mut self.timing, "hd water"), ++ color_attachments: &[clear(&self.water_tint), clear(&self.water_normal)], ++ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { ++ view: &self.water_depth, ++ depth_ops: Some(wgpu::Operations { ++ load: wgpu::LoadOp::Clear(0.0), ++ store: wgpu::StoreOp::Store, ++ }), ++ stencil_ops: None, ++ }), ++ ..Default::default() ++ }) ++ .forget_lifetime() ++ } ++ ++ /// Lays the water over the lit world (refraction, reflection, glint) into a target of its ++ /// own; `lit_view` is that target after this. ++ pub fn water_composite( ++ &mut self, ++ device: &wgpu::Device, ++ encoder: &mut wgpu::CommandEncoder, ++ lightmap: (&wgpu::TextureView, &wgpu::Sampler), ++ ) { ++ if !self.water_active { ++ return; ++ } ++ let key = (lightmap.0.clone(), lightmap.1.clone()); ++ if self.water_group.as_ref().is_none_or(|(k, _)| *k != key) { ++ let group = self.make_group( ++ device, ++ "HD water", ++ &self.water_layout, ++ &[ ++ wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()), ++ wgpu::BindingResource::TextureView(&self.lit), ++ wgpu::BindingResource::TextureView(&self.depth), ++ wgpu::BindingResource::TextureView(&self.water_tint), ++ wgpu::BindingResource::TextureView(&self.water_normal), ++ wgpu::BindingResource::TextureView(&self.water_depth), ++ wgpu::BindingResource::TextureView(&self.shadow_array), ++ wgpu::BindingResource::Sampler(&self.shadow_sampler), ++ wgpu::BindingResource::TextureView(lightmap.0), ++ wgpu::BindingResource::Sampler(&self.sampler), ++ ], ++ ); ++ self.water_group = Some((key, group)); ++ } ++ let Some((_, group)) = &self.water_group else { ++ return; ++ }; ++ Self::draw( ++ encoder, ++ "HD water composite", ++ &self.lit_water, ++ false, ++ &self.water_pipeline, ++ group, ++ stamp(&mut self.timing, "hd water composite"), ++ ); ++ self.water_composited = true; ++ } ++ + /// The ambient occlusion from the G-buffer's depth and normals: the half-resolution pass, then + /// its blur. Run after the G-buffer pass; nothing when the strength is 0. + pub fn ao(&mut self, encoder: &mut wgpu::CommandEncoder) { +@@ -2181,6 +2657,7 @@ mod tests { + ("taa", TAA), + ("ao", AO), + ("ao blur", AO_BLUR), ++ ("water", WATER), + ] { + let source = format!("{COMMON}{body}"); + let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| { +diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl +index c44449cc7..0d5d2fc89 100644 +--- a/crates/horizon-client/src/terrain.wgsl ++++ b/crates/horizon-client/src/terrain.wgsl +@@ -182,6 +182,25 @@ fn face_normal(pos: vec3) -> vec3 { + out.light = vec4(clamp(in.lm, vec2(0.0), vec2(1.0)), 0.0, 1.0); + return out; + } ++// AirDef fork, HD water: the water quads once more, after the G-buffer, into small targets of ++// their own: the surface colour (the water sprite times the biome tint) with the block light, and ++// the face normal with the sky light. The layers are the water sprites' (gfx.rs fills them in; ++// nothing else is water: glass, ice and the rest stay as vanilla draws them). ++override WATER_STILL: u32 = 0xffffu; ++override WATER_FLOW: u32 = 0xffffu; ++struct WaterOut { ++ @location(0) tint: vec4, ++ @location(1) normal: vec4, ++} ++@fragment fn fs_water(in: TerrainOut) -> WaterOut { ++ let layer = in.layer & 0x7fffu; ++ if layer != WATER_STILL && layer != WATER_FLOW { discard; } ++ let color = sprite(in.uv, layer) * in.tint; ++ var out: WaterOut; ++ out.tint = vec4(color.rgb, clamp(in.lm.y, 0.0, 1.0)); ++ out.normal = vec4(face_normal(in.pos) * 0.5 + 0.5, clamp(in.lm.x, 0.0, 1.0)); ++ return out; ++} + // AirDef fork: the sun's shadow map — depth only, cut-out leaves leave holes. + @fragment fn fs_shadow(in: TerrainOut) { + let a = sprite(in.uv, in.layer & 0x7fffu).a; +-- +2.54.0 (Apple Git-157) +