From f43fa5ee664186c51855335a446e8089c37c51c3 Mon Sep 17 00:00:00 2001 From: chexit Date: Fri, 30 Oct 2026 03:26:10 +0300 Subject: [PATCH] Clouds occlude by depth, world-anchored noise, visual-only replay time (fork patch 29; #67, #68, #70) Co-Authored-By: Claude Opus 5.5 --- docs/HD-RENDERER.md | 4 + ...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 | 2 +- ...lla-texture-on-the-surface-the-biome.patch | 2 +- ...-a-physical-atmosphere-Rayleigh-Mie-.patch | 2 +- ...r-blue-a-little-clearer-ripples-a-li.patch | 2 +- ...sunsets-Rec.-2020-atmosphere-sun-tin.patch | 2 +- ...es-for-simulations-render.query-rays.patch | 2 +- ...vel-from-the-world-not-from-rain-geo.patch | 2 +- ...dules-gfx-hd-one-Rust-file-and-one-W.patch | 2 +- ...scales-that-divide-the-camera-s-6553.patch | 2 +- ...oke-and-missile-trails-with-clear-tu.patch | 2 +- ...ited-by-depth-so-terrain-and-vehicle.patch | 603 ++++++++++++++++++ 30 files changed, 635 insertions(+), 28 deletions(-) create mode 100644 horizon-patches/0029-HD-clouds-composited-by-depth-so-terrain-and-vehicle.patch diff --git a/docs/HD-RENDERER.md b/docs/HD-RENDERER.md index e3b67db..70afe8d 100644 --- a/docs/HD-RENDERER.md +++ b/docs/HD-RENDERER.md @@ -118,6 +118,7 @@ Bind groups кэшируются и пересоздаются только пр | 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, ночь), @@ -227,6 +228,9 @@ HORIZON_HD_TAA=0 --out run-home-bench/out/taa-off` — A/B; `--baseline run-home | Материалы и блики (план 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` (следы дронов и ракет, колонны над пожарами, дым взрывов) | готово, в рабочем дереве форка; вид не настраивался (см. «Дым и следы») | не закоммичено | ## План (по порядку) и ТЗ 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 6e2bbcb..7ae0f42 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/28] Mod API (AirDef fork): world models, client particles, +Subject: [PATCH 01/29] 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 84c671f..464f94f 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/28] cargo-horizon-mod: report particles and sounds +Subject: [PATCH 02/29] 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 446ffb3..f220605 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/28] Mod API (AirDef fork): full-screen post pass for mods +Subject: [PATCH 03/29] 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 fcae780..f971414 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/28] Mod models: draw far models as scaled impostors past +Subject: [PATCH 04/29] 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 bca1c6c..531c425 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/28] Mod cameras: render.set-views draws the world from +Subject: [PATCH 05/29] 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 c72a2c1..026116f 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/28] Mods: more fuel per tick and per event (AirDef plans +Subject: [PATCH 06/29] 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 acd7e5a..d4d636f 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/28] Mod cameras up to 1920x1080 (full-screen replays) +Subject: [PATCH 07/29] 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 ce3ce78..efd6cbc 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/28] Mod cameras: distant terrain tiles, fog measured from +Subject: [PATCH 08/29] 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 2e4f044..7a64fb4 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/28] Mods: game.world-id, so mods keep state per world (API +Subject: [PATCH 09/29] 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 2dbe9fb..556bc52 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/28] cargo-horizon-mod: trial runs behind feature full, so a +Subject: [PATCH 10/29] 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 a7263b8..e6bd48f 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/28] HD lighting: G-buffer pass, per-pixel mod point lights +Subject: [PATCH 11/29] 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 13a6dff..0463349 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/28] HD lighting stage 2: sun and moon shadow cascades, the +Subject: [PATCH 12/29] 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 cebbe94..178152f 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/28] HD: bind groups cached until a resize or a new source, +Subject: [PATCH 13/29] 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 c9c7ce3..e2060ae 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/28] HD: lights culled per 16x16 tile (surface and halo +Subject: [PATCH 14/29] 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 98a24e5..f7b8818 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/28] HD: GPU time per pass in the F3 timing list, +Subject: [PATCH 15/29] 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 3e13f05..3780c29 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/28] HD: shadows draw only sun-facing quads and skip buried +Subject: [PATCH 16/29] 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 8bc9909..6b0cb52 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/28] Mods: model handles (render.model-id, model-generation, +Subject: [PATCH 17/29] 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 7816d8e..1b8bc2a 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/28] HD: temporal upscaling (HORIZON_HD_SCALE: world, +Subject: [PATCH 18/29] 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 index a783f0a..8789c0d 100644 --- 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 @@ -1,7 +1,7 @@ From 38250c4392e27d5700842ca6ee47291dd1831b72 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 17:47:52 +0300 -Subject: [PATCH 19/28] HD water: its own pass (colour, normal, depth), the +Subject: [PATCH 19/29] 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) diff --git a/horizon-patches/0020-HD-water-no-vanilla-texture-on-the-surface-the-biome.patch b/horizon-patches/0020-HD-water-no-vanilla-texture-on-the-surface-the-biome.patch index 3ff1641..01f0068 100644 --- a/horizon-patches/0020-HD-water-no-vanilla-texture-on-the-surface-the-biome.patch +++ b/horizon-patches/0020-HD-water-no-vanilla-texture-on-the-surface-the-biome.patch @@ -1,7 +1,7 @@ From dbc6f8a6e9c2eeabe1de15f1794013c90be7faa5 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 17:56:04 +0300 -Subject: [PATCH 20/28] HD water: no vanilla texture on the surface (the biome +Subject: [PATCH 20/29] HD water: no vanilla texture on the surface (the biome colour only), a darker, deeper colour Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0021-HD-sky-and-light-a-physical-atmosphere-Rayleigh-Mie-.patch b/horizon-patches/0021-HD-sky-and-light-a-physical-atmosphere-Rayleigh-Mie-.patch index 23cb1d8..9a9cebc 100644 --- a/horizon-patches/0021-HD-sky-and-light-a-physical-atmosphere-Rayleigh-Mie-.patch +++ b/horizon-patches/0021-HD-sky-and-light-a-physical-atmosphere-Rayleigh-Mie-.patch @@ -1,7 +1,7 @@ From 13e74537dd6e63482e3613e0e416de1158944dfe Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 18:38:19 +0300 -Subject: [PATCH 21/28] HD sky and light: a physical atmosphere (Rayleigh, Mie, +Subject: [PATCH 21/29] HD sky and light: a physical atmosphere (Rayleigh, Mie, ozone; constants and transmittance after Photon, MIT) for the sky, haze, fog and water reflections; warmer sunlight from the atmosphere's transmittance, a gentle sky-coloured ambient, soft PCSS-lite shadows near the player, a haze diff --git a/horizon-patches/0022-HD-water-a-darker-blue-a-little-clearer-ripples-a-li.patch b/horizon-patches/0022-HD-water-a-darker-blue-a-little-clearer-ripples-a-li.patch index e9fc0d6..9c254a0 100644 --- a/horizon-patches/0022-HD-water-a-darker-blue-a-little-clearer-ripples-a-li.patch +++ b/horizon-patches/0022-HD-water-a-darker-blue-a-little-clearer-ripples-a-li.patch @@ -1,7 +1,7 @@ From 6277e051a89c2537570982f6ef275887193c05cf Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 19:02:16 +0300 -Subject: [PATCH 22/28] HD water: a darker blue, a little clearer, ripples a +Subject: [PATCH 22/29] HD water: a darker blue, a little clearer, ripples a little higher Co-Authored-By: Claude Opus 5.5 diff --git a/horizon-patches/0023-HD-Photon-style-sunsets-Rec.-2020-atmosphere-sun-tin.patch b/horizon-patches/0023-HD-Photon-style-sunsets-Rec.-2020-atmosphere-sun-tin.patch index 28929e3..a7642ff 100644 --- a/horizon-patches/0023-HD-Photon-style-sunsets-Rec.-2020-atmosphere-sun-tin.patch +++ b/horizon-patches/0023-HD-Photon-style-sunsets-Rec.-2020-atmosphere-sun-tin.patch @@ -1,7 +1,7 @@ From 982265d21bb9d74ec0040db03feb5a55762286ad Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 21:41:48 +0300 -Subject: [PATCH 23/28] HD: Photon-style sunsets (Rec. 2020 atmosphere, sun +Subject: [PATCH 23/29] HD: Photon-style sunsets (Rec. 2020 atmosphere, sun tint, blue hour; MIT), volumetric clouds with ground shadows instead of vanilla's in HD, volumetric fog with sun shafts and morning mist (HORIZON_HD_CLOUDS=0, HORIZON_HD_VOLFOG=0) diff --git a/horizon-patches/0024-Mods-world-queries-for-simulations-render.query-rays.patch b/horizon-patches/0024-Mods-world-queries-for-simulations-render.query-rays.patch index 3e890a3..f83e79c 100644 --- a/horizon-patches/0024-Mods-world-queries-for-simulations-render.query-rays.patch +++ b/horizon-patches/0024-Mods-world-queries-for-simulations-render.query-rays.patch @@ -1,7 +1,7 @@ From 24971b3c67d8c87673eb2e0d204cedf8a2f46d1c Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 21:43:22 +0300 -Subject: [PATCH 24/28] Mods: world queries for simulations (render.query-rays, +Subject: [PATCH 24/29] Mods: world queries for simulations (render.query-rays, query-sections, world-changes; API level 12): block-exact rays and section occupancy by collision, sight or sound, answered next tick within budgets, and a log of changed sections diff --git a/horizon-patches/0025-HD-fixes-rain-level-from-the-world-not-from-rain-geo.patch b/horizon-patches/0025-HD-fixes-rain-level-from-the-world-not-from-rain-geo.patch index 9bca46d..66c3c3c 100644 --- a/horizon-patches/0025-HD-fixes-rain-level-from-the-world-not-from-rain-geo.patch +++ b/horizon-patches/0025-HD-fixes-rain-level-from-the-world-not-from-rain-geo.patch @@ -1,7 +1,7 @@ From fdec4960983bdff82f3793460f9a5234f0cd3f49 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 22:56:15 +0300 -Subject: [PATCH 25/28] HD fixes: rain level from the world not from rain +Subject: [PATCH 25/29] HD fixes: rain level from the world not from rain geometry (#41), TAA history on the jittered grid (#38), vanilla cloud tint without a depth heuristic (#35), air fog once over water (#37), translucent things in front of HD water (#36), cheaper sunrise fan (#39), halos in thick diff --git a/horizon-patches/0026-HD-split-into-modules-gfx-hd-one-Rust-file-and-one-W.patch b/horizon-patches/0026-HD-split-into-modules-gfx-hd-one-Rust-file-and-one-W.patch index 0f99d02..46f6499 100644 --- a/horizon-patches/0026-HD-split-into-modules-gfx-hd-one-Rust-file-and-one-W.patch +++ b/horizon-patches/0026-HD-split-into-modules-gfx-hd-one-Rust-file-and-one-W.patch @@ -1,7 +1,7 @@ From 21391afb26f49505d6cdaf454a6355cd4e747503 Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 23:19:53 +0300 -Subject: [PATCH 26/28] HD split into modules (gfx/hd/: one Rust file and one +Subject: [PATCH 26/29] HD split into modules (gfx/hd/: one Rust file and one WGSL file per pass, a shared prelude), contracts for colour, depth, alpha, time and coordinates written down, tests that the frame layout and bindings match between Rust and WGSL (#45) diff --git a/horizon-patches/0027-HD-clouds-noise-scales-that-divide-the-camera-s-6553.patch b/horizon-patches/0027-HD-clouds-noise-scales-that-divide-the-camera-s-6553.patch index d18d550..258e3c5 100644 --- a/horizon-patches/0027-HD-clouds-noise-scales-that-divide-the-camera-s-6553.patch +++ b/horizon-patches/0027-HD-clouds-noise-scales-that-divide-the-camera-s-6553.patch @@ -1,7 +1,7 @@ From 8d4cadbf8534309ac847d34e229f6d764770475d Mon Sep 17 00:00:00 2001 From: chexit Date: Thu, 29 Oct 2026 23:22:19 +0300 -Subject: [PATCH 27/28] HD clouds: noise scales that divide the camera's +Subject: [PATCH 27/29] HD clouds: noise scales that divide the camera's 65536-block wrap, so the clouds and their shadows no longer jump at x or z = 0 diff --git a/horizon-patches/0028-HD-volumetric-smoke-and-missile-trails-with-clear-tu.patch b/horizon-patches/0028-HD-volumetric-smoke-and-missile-trails-with-clear-tu.patch index 96dac8e..c2782f2 100644 --- a/horizon-patches/0028-HD-volumetric-smoke-and-missile-trails-with-clear-tu.patch +++ b/horizon-patches/0028-HD-volumetric-smoke-and-missile-trails-with-clear-tu.patch @@ -1,7 +1,7 @@ From 0fb59c782abf269fe555faa3241b31acf41e26d7 Mon Sep 17 00:00:00 2001 From: chexit Date: Fri, 30 Oct 2026 03:17:11 +0300 -Subject: [PATCH 28/28] HD: volumetric smoke and missile trails with clear +Subject: [PATCH 28/29] HD: volumetric smoke and missile trails with clear tunnels through fog and cloud (render.set-smoke, API level 14; #54); sun speculars by material, wet surfaces and puddles in rain, wind in foliage (HORIZON_HD_SPECULAR=0, HORIZON_HD_WIND=0, HORIZON_HD_SMOKE=0); HD shaders diff --git a/horizon-patches/0029-HD-clouds-composited-by-depth-so-terrain-and-vehicle.patch b/horizon-patches/0029-HD-clouds-composited-by-depth-so-terrain-and-vehicle.patch new file mode 100644 index 0000000..92e6fa6 --- /dev/null +++ b/horizon-patches/0029-HD-clouds-composited-by-depth-so-terrain-and-vehicle.patch @@ -0,0 +1,603 @@ +From e4bc29da45d0d38244c6eb2ba3ffb9ab853540c0 Mon Sep 17 00:00:00 2001 +From: chexit +Date: Fri, 30 Oct 2026 03:26:09 +0300 +Subject: [PATCH 29/29] HD clouds composited by depth so terrain and vehicles + inside or behind the layer are hidden and those in front stay in front (#67); + world-anchored noise everywhere (cloud detail by height above the layer, fog, + smoke and puddle scales dividing the wrap) (#68); the replay's time is visual + only, gameplay sky darkening uses the world's time (#70) + +Co-Authored-By: Claude Opus 5.5 +--- + crates/horizon-client/src/game.rs | 37 +++-- + crates/horizon-client/src/game/render_tick.rs | 2 +- + crates/horizon-client/src/gfx/hd/cloud.wgsl | 54 ++++++- + .../src/gfx/hd/cloud_common.wgsl | 3 +- + crates/horizon-client/src/gfx/hd/fog.wgsl | 6 +- + crates/horizon-client/src/gfx/hd/light.wgsl | 42 +++++- + crates/horizon-client/src/gfx/hd/lighting.rs | 4 +- + crates/horizon-client/src/gfx/hd/volumes.rs | 135 +++++++++++++++--- + 8 files changed, 239 insertions(+), 44 deletions(-) + +diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs +index 2130d8d88..f21b3dcdd 100644 +--- a/crates/horizon-client/src/game.rs ++++ b/crates/horizon-client/src/game.rs +@@ -1838,6 +1838,8 @@ struct Game<'a> { + events: events::Events, + /// The overworld's last `sky_darken` and its inputs (time, rain, thunder). + sky_darken_memo: std::cell::Cell>, ++ /// The same for the time a mod shows (a replay's): render only. ++ shown_darken_memo: std::cell::Cell>, + persist: persist::EntityMemory, + /// Connected to a server: the world comes from the network (game/remote.rs). + remote: Option, +@@ -8020,7 +8022,7 @@ impl Game<'_> { + .flatten(); + if let Some(sprite) = shadow_sprite { + let ambient = if self.dim == Dim::Nether { 0.1 } else { 0.0 }; +- let darken = self.sky_darken(); ++ let darken = self.shown_sky_darken(); + let a = alpha; + let lerp = |p: Vec3, q: Vec3| { + glam::DVec3::new( +@@ -8378,33 +8380,43 @@ impl Game<'_> { + .map_or(self.world.day_time, |t| t as i64) + } + ++ /// The sky's darkening for the game's rules (mob AI, spawning, block behaviour, stats): always ++ /// the world's real time, never a mod's render-only override. + fn sky_darken(&self) -> i32 { ++ self.sky_darken_at(self.world.day_time, &self.sky_darken_memo) ++ } ++ ++ /// The sky's darkening as the player sees it (entity shadows, particle light): follows the mod's ++ /// time override, so a replay's night looks dark without touching the rules. ++ fn shown_sky_darken(&self) -> i32 { ++ self.sky_darken_at(self.shown_day_time(), &self.shown_darken_memo) ++ } ++ ++ fn sky_darken_at( ++ &self, ++ time: i64, ++ memo: &std::cell::Cell>, ++ ) -> i32 { + if self.dim == Dim::Nether { + return 11; + } + if self.dim == Dim::End { +- let sky = self +- .res +- .end_environment() +- .sample(self.shown_day_time(), None, None); ++ let sky = self.res.end_environment().sample(time, None, None); + return (15.0 - 15.0 * sky.sky_light_level).round() as i32; + } + let (rain, thunder) = self.world.weather.levels(1.0); + // Sampled once per (time, weather): every mob asks each tick. +- let key = (self.shown_day_time(), rain.to_bits(), thunder.to_bits()); +- if let Some((k, darken)) = self.sky_darken_memo.get() ++ let key = (time, rain.to_bits(), thunder.to_bits()); ++ if let Some((k, darken)) = memo.get() + && k == key + { + return darken; + } +- let mut sky = self +- .res +- .environment() +- .sample(self.shown_day_time(), None, None); ++ let mut sky = self.res.environment().sample(time, None, None); + horizon_client::environment::apply_weather(&mut sky, rain, thunder); + // Level.updateSkyBrightness: darkening from the sky light level. + let darken = (15.0 - 15.0 * sky.sky_light_level).round() as i32; +- self.sky_darken_memo.set(Some((key, darken))); ++ memo.set(Some((key, darken))); + darken + } + fn mob_hit(&self) -> Option<(usize, f64)> { +@@ -13954,6 +13966,7 @@ impl<'a> Game<'a> { + mods: Default::default(), + native_shaders: Default::default(), + sky_darken_memo: Default::default(), ++ shown_darken_memo: Default::default(), + persist: Default::default(), + remote: None, + lan: None, +diff --git a/crates/horizon-client/src/game/render_tick.rs b/crates/horizon-client/src/game/render_tick.rs +index 6807d7718..9912970a5 100644 +--- a/crates/horizon-client/src/game/render_tick.rs ++++ b/crates/horizon-client/src/game/render_tick.rs +@@ -241,7 +241,7 @@ impl Game<'_> { + eye[2].floor() as i32, + ]; + let packed = self.world.light(b[0], b[1], b[2]); +- let sky = i32::from(packed >> 4) - self.sky_darken(); ++ let sky = i32::from(packed >> 4) - self.shown_sky_darken(); + let raw = sky.max(i32::from(packed & 15)).clamp(0, 15) as u8; + lightmap_brightness(if self.dim == Dim::Nether { 0.1 } else { 0.0 }, raw) + } +diff --git a/crates/horizon-client/src/gfx/hd/cloud.wgsl b/crates/horizon-client/src/gfx/hd/cloud.wgsl +index fad234a46..39a239f4d 100644 +--- a/crates/horizon-client/src/gfx/hd/cloud.wgsl ++++ b/crates/horizon-client/src/gfx/hd/cloud.wgsl +@@ -1,6 +1,17 @@ + + @group(0) @binding(6) var history: texture_2d; + @group(0) @binding(7) var smoke_tex: texture_2d; ++@group(0) @binding(8) var scene_depth: texture_depth_2d; ++@group(0) @binding(9) var history_dist: texture_2d; ++ ++// What the sky pixels report as the length of their ray (nothing stops them). ++const SKY_LEN: f32 = 20000.0; ++ ++struct CloudOut { ++ @location(0) cloud: vec4, ++ // How far the ray was marched: the surface behind this texel, or SKY_LEN for sky. ++ @location(1) dist: vec4, ++} + + fn ign(px: vec2) -> f32 { + let p = px + vec2(5.588238 * frame.params.w); +@@ -65,7 +76,26 @@ fn cloud_scatter( + } + + @fragment +-fn fs_cloud(in: FullOut) -> @location(0) vec4 { ++fn fs_cloud(in: FullOut) -> CloudOut { ++ // The nearest of the four world pixels this texel stands for: the march ends at the surface ++ // there (the sky counts as infinitely far), so what stands in front of a cloud stays in front ++ // and what stands behind it is hidden by it. ++ let size = vec2(frame.resolution.xy); ++ let base = vec2(in.position.xy) * 2; ++ var zmax = 0.0; ++ var best = base; ++ for (var k = 0; k < 4; k++) { ++ let q = min(base + vec2(k & 1, k >> 1), size - 1); ++ let z = textureLoad(scene_depth, q, 0); ++ if z > zmax { ++ zmax = z; ++ best = q; ++ } ++ } ++ let suv = (vec2(best) + 0.5) * frame.resolution.zw; ++ let sndc = vec4(suv.x * 2.0 - 1.0, 1.0 - suv.y * 2.0, max(zmax, 0.000001), 1.0); ++ let sh = frame.inverse_view_proj * sndc; ++ let scene_len = select(SKY_LEN, length(sh.xyz / sh.w), zmax > 0.0); + let ndc = vec4(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, 0.5, 1.0); + let hh = frame.inverse_view_proj * ndc; + let dir = normalize(hh.xyz / hh.w); +@@ -73,11 +103,13 @@ fn fs_cloud(in: FullOut) -> @location(0) vec4 { + var hit_t = 900.0; + let y0 = frame.clouds.x; + let y1 = y0 + frame.clouds.y; +- if abs(dir.y) > 0.004 { +- let ta = y0 / dir.y; +- let tb = y1 / dir.y; ++ // A level ray stays in the slab if the camera is in it (it never leaves), else misses it. ++ let dy = select(dir.y, select(-1.0e-4, 1.0e-4, dir.y >= 0.0), abs(dir.y) < 1.0e-4); ++ { ++ let ta = y0 / dy; ++ let tb = y1 / dy; + let lo = max(min(ta, tb), 0.0); +- let hi = min(max(ta, tb), 4200.0); ++ let hi = min(max(ta, tb), min(4200.0, scene_len)); + if hi > lo { + let dither = ign(in.position.xy); + let n = clamp(i32(ceil((hi - lo) / 14.0)), 8, 26); +@@ -131,6 +163,8 @@ fn fs_cloud(in: FullOut) -> @location(0) vec4 { + cur = vec4(colour, trans); + } + } ++ var out: CloudOut; ++ out.dist = vec4(scene_len, 0.0, 0.0, 0.0); + // Reproject the history by where the clouds are (rotation and the camera's step) and blend. + if frame.cloud_cam.w > 0.5 { + let hp = frame.prev_view_proj * vec4(dir * hit_t + frame.taa.xyz, 1.0); +@@ -138,12 +172,18 @@ fn fs_cloud(in: FullOut) -> @location(0) vec4 { + let huv = vec2(hp.x / hp.w * 0.5 + 0.5, 0.5 - hp.y / hp.w * 0.5); + if all(huv > vec2(0.0)) && all(huv < vec2(1.0)) { + let old = textureSampleLevel(history, linear, huv, 0.0); ++ // A history that was marched to another surface (an edge, something that moved in ++ // front) does not describe this ray. ++ let hs = vec2(textureDimensions(history_dist)); ++ let old_len = textureLoad(history_dist, vec2(huv * hs), 0).x; ++ let same_len = 1.0 - smoothstep(0.5, 1.0, abs(old_len - scene_len) / (0.1 * min(scene_len, old_len) + 2.0)); + // Not too much of a history that disagrees by a lot (a gust, a fast turn). + let diff = length(old.rgb - cur.rgb) + abs(old.a - cur.a); +- let keep = 0.9 * (1.0 - smoothstep(0.35, 1.2, diff)); ++ let keep = 0.9 * (1.0 - smoothstep(0.35, 1.2, diff)) * same_len; + cur = mix(cur, old, keep); + } + } + } +- return cur; ++ out.cloud = cur; ++ return out; + } +diff --git a/crates/horizon-client/src/gfx/hd/cloud_common.wgsl b/crates/horizon-client/src/gfx/hd/cloud_common.wgsl +index e2352f008..8ee014420 100644 +--- a/crates/horizon-client/src/gfx/hd/cloud_common.wgsl ++++ b/crates/horizon-client/src/gfx/hd/cloud_common.wgsl +@@ -44,7 +44,8 @@ fn cloud_density(pos: vec3, detail: f32) -> f32 { + d *= smoothstep(0.0, 0.2, h); + if d < 0.002 { return 0.0; } + if detail > 0.0 { +- let p3 = vec3(w.x, pos.y, w.y); ++ // Height above the layer's base, not the camera's height: the noise stays put in the world. ++ let p3 = vec3(w.x, pos.y - frame.clouds.x, w.y); + let warp = (textureSampleLevel(noise3, repeat_s, p3 / 1024.0, 0.0).b - 0.5) * 0.4; + let w0 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 512.0 + vec3(warp), 0.0).r; + let w1 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 64.0 + vec3(warp * 3.0), 0.0).g; +diff --git a/crates/horizon-client/src/gfx/hd/fog.wgsl b/crates/horizon-client/src/gfx/hd/fog.wgsl +index 38b9098e3..d035bbf0d 100644 +--- a/crates/horizon-client/src/gfx/hd/fog.wgsl ++++ b/crates/horizon-client/src/gfx/hd/fog.wgsl +@@ -40,7 +40,7 @@ fn sun_visible(p: vec3) -> f32 { + } + + // The air's extinction per block at a camera-relative point: a haze that thins with height above +-// the sea, a morning mist hugging the ground, rain, patches from a 3D noise that drifts; none ++// the sea, a morning mist hugging the ground, rain, patches from a 3D noise that drifts (its scale divides 65536, the wrap of the camera's x, z, so it does not jump at x or z = 0); none + // deep underground. + fn fog_sigma(p: vec3) -> f32 { + let wy = p.y + frame.vfog.y; +@@ -50,7 +50,7 @@ fn fog_sigma(p: vec3) -> f32 { + s += frame.sky_extra.z * 0.003; + let w = vec3(p.x + frame.cloud_cam.x, wy, p.z + frame.cloud_cam.y) + + vec3(0.5, 0.0, 0.35) * frame.clouds.w; +- let n = textureSampleLevel(noise3, repeat_s, w / 170.0, 0.0).b; ++ let n = textureSampleLevel(noise3, repeat_s, w / 128.0, 0.0).b; + let patchy = 0.35 + 1.3 * smoothstep(0.25, 0.75, n); + // Fade out far below the sea's height: caves keep their own dark air. + return s * patchy * smoothstep(28.0, 58.0, wy); +@@ -70,7 +70,7 @@ fn smoke_detail(pos: vec3, core: f32) -> f32 { + let w = vec3(pos.x + frame.cloud_cam.x, pos.y + frame.vfog.y, pos.z + frame.cloud_cam.y) + - vec3(SMOKE_WIND.x, 0.0, SMOKE_WIND.y) * frame.smoke.z; + let n1 = textureSampleLevel(noise3, repeat_s, w / 64.0, 0.0).r; +- let n2 = textureSampleLevel(noise3, repeat_s, w / 22.0, 0.0).g; ++ let n2 = textureSampleLevel(noise3, repeat_s, w / 32.0, 0.0).g; + let n = 0.55 * n1 + 0.45 * n2; + // Thin smoke breaks up into wisps, thick smoke stays whole. + let keep = smoothstep(0.18, 0.62, n + 0.45 * min(core, 1.5) - 0.12); +diff --git a/crates/horizon-client/src/gfx/hd/light.wgsl b/crates/horizon-client/src/gfx/hd/light.wgsl +index 2534a3db8..870ae0d7d 100644 +--- a/crates/horizon-client/src/gfx/hd/light.wgsl ++++ b/crates/horizon-client/src/gfx/hd/light.wgsl +@@ -30,6 +30,9 @@ struct Light { + @group(0) @binding(17) var fog_dist_tex: texture_2d; + @group(0) @binding(18) var cloud_tex: texture_2d; + @group(0) @binding(19) var cloud_shadow_tex: texture_2d; ++// How far each cloud texel's ray was marched (the surface behind it, or SKY_RAY for sky). ++@group(0) @binding(20) var cloud_dist_tex: texture_2d; ++const SKY_RAY: f32 = 20000.0; + const CLOUD_SHADOW_R: f32 = 1200.0; + // The pixel being lit, for the shadow filter's noise. + var frag_px: vec2; +@@ -194,6 +197,34 @@ fn fog_at(px: vec2, dist: f32) -> vec4 { + return sum / total; + } + ++// The volumetric clouds between the camera and a surface `dist` away (SKY_RAY for sky): the four ++// nearest half-resolution texels weighted by closeness and by how near the length they were ++// marched to is, so a cloud in front of a mountain does not bleed onto the sky beside it. ++fn cloud_at(px: vec2, dist: f32) -> vec4 { ++ let size = vec2(textureDimensions(cloud_tex)); ++ let f = vec2(px) * 0.5 - vec2(0.25); ++ let base = vec2(floor(f)); ++ let t = f - floor(f); ++ var sum = vec4(0.0); ++ var total = 0.0; ++ for (var j = 0; j < 2; j++) { ++ for (var i = 0; i < 2; i++) { ++ let q = clamp(base + vec2(i, j), vec2(0), size - 1); ++ let s = textureLoad(cloud_tex, q, 0); ++ let sd = textureLoad(cloud_dist_tex, q, 0).x; ++ let bw = select(1.0 - t.x, t.x, i == 1) * select(1.0 - t.y, t.y, j == 1); ++ let dz = (sd - dist) / (0.06 * min(sd, dist) + 0.4); ++ let w = bw / (1.0 + dz * dz * dz * dz); ++ sum += s * w; ++ total += w; ++ } ++ } ++ if total < 0.0001 { ++ return textureLoad(cloud_tex, clamp(px / 2, vec2(0), size - 1), 0); ++ } ++ return sum / total; ++} ++ + struct LightOut { + // The lit colour; alpha 0.5 marks water (the floor under it, lit without any air), 0.6 an + // object in front of the water (the scene as drawn). +@@ -246,7 +277,7 @@ fn shade_surface(px: vec2, p: vec3, water: bool) -> vec3 { + let flat_up = smoothstep(0.9, 0.98, n.y); + let soak = wet * open_sky * (0.35 + 0.65 * flat_up); + let wp = frame.cloud_cam.xy + p.xz; +- let nz = 0.6 * wet_noise(wp * 0.45) + 0.4 * wet_noise(wp * 1.3 + vec2(17.0, 5.0)); ++ let nz = 0.6 * wet_noise(wp * 0.5) + 0.4 * wet_noise(wp * 1.25 + vec2(17.0, 5.0)); + puddle = smoothstep(0.66 - 0.16 * wet, 0.74 - 0.16 * wet, nz) * smoothstep(0.3, 0.7, wet) * open_sky * flat_up; + surface_rgb *= mix(1.0, 0.7, soak) * mix(1.0, 0.6, puddle); + rough = mix(rough, 0.12, soak * (1.0 - 0.5 * metal)); +@@ -377,7 +408,7 @@ fn fs_light(in: FullOut) -> LightOut { + c = sky + kept; + // The volumetric clouds over the sky and the sun. + if frame.clouds.z > 0.5 { +- let cl = textureSampleLevel(cloud_tex, linear, in.uv, 0.0); ++ let cl = cloud_at(px, SKY_RAY); + c = c * cl.a + cl.rgb; + } + } +@@ -421,6 +452,13 @@ fn fs_light(in: FullOut) -> LightOut { + air_t = fg.a; + air_s = fg.rgb; + } ++ // The clouds between the camera and the surface (inside the layer, or in front of a mountain ++ // or a flying thing): laid over the surface and under the fog, as the sky's are. ++ if d > 0.0 && atmosphere && frame.clouds.z > 0.5 { ++ let cl = cloud_at(px, length(ps)); ++ air_s += air_t * cl.rgb; ++ air_t *= cl.a; ++ } + if !volumetric && sun_on > 0.0 && density > 0.0 { + let steps = 14; + let jitter = ign(in.position.xy); +diff --git a/crates/horizon-client/src/gfx/hd/lighting.rs b/crates/horizon-client/src/gfx/hd/lighting.rs +index da56d00ba..d26f1e513 100644 +--- a/crates/horizon-client/src/gfx/hd/lighting.rs ++++ b/crates/horizon-client/src/gfx/hd/lighting.rs +@@ -34,7 +34,7 @@ pub const MAX_LIGHTS: usize = 512; + + pub(super) const LIGHT: &str = include_str!("light.wgsl"); + /// The lighting's group 0, binding by binding (see the contract above). +-pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 20] = [ ++pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 21] = [ + UNIFORM, + UNFILTERED, + DEPTH, +@@ -55,6 +55,7 @@ pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 20] = [ + UNFILTERED, + FLOAT, + FLOAT, ++ UNFILTERED, + ]; + + /// The outside views and sampler the lighting group reads. +@@ -219,6 +220,7 @@ impl Hd { + wgpu::BindingResource::TextureView(&self.volumes.fog_dist), + wgpu::BindingResource::TextureView(self.volumes.cloud_view()), + wgpu::BindingResource::TextureView(&self.volumes.cloud_shadow), ++ wgpu::BindingResource::TextureView(self.volumes.cloud_dist_view()), + ]; + let entries: Vec<_> = resources + .into_iter() +diff --git a/crates/horizon-client/src/gfx/hd/volumes.rs b/crates/horizon-client/src/gfx/hd/volumes.rs +index a338d756c..d3b55e2f2 100644 +--- a/crates/horizon-client/src/gfx/hd/volumes.rs ++++ b/crates/horizon-client/src/gfx/hd/volumes.rs +@@ -26,8 +26,8 @@ + + use super::noise as hd_noise; + use super::{ +- DEPTH, FLOAT, Hd, SAMPLER, SHADOW_ARRAY, SHADOW_SAMPLER, UNIFORM, entries, fullscreen, module, +- stamp, stamped_pass, target, ++ DEPTH, FLOAT, Hd, SAMPLER, SHADOW_ARRAY, SHADOW_SAMPLER, UNFILTERED, UNIFORM, entries, ++ fullscreen, module, stamp, stamped_pass, target, + }; + + pub const CLOUD_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; +@@ -86,6 +86,9 @@ pub struct HdVolumes { + shadow_sampler: wgpu::Sampler, + /// The clouds' half-resolution target and its history (read one, write the other). + pub clouds: [wgpu::TextureView; 2], ++ /// How far each cloud texel's ray was marched (ping-pong with `clouds`). ++ cloud_dist: [wgpu::TextureView; 2], ++ cloud_depth: Option, + flip: usize, + history_ready: bool, + pub cloud_shadow: wgpu::TextureView, +@@ -111,9 +114,10 @@ const D3: wgpu::BindingType = wgpu::BindingType::Texture { + }; + + /// `fs_cloud`'s group 0: frame, 3D noise, 2D noise, clamped and repeating samplers, the sky +-/// texture, the previous cloud frame. +-pub(super) const CLOUD_BINDINGS: [wgpu::BindingType; 8] = +- [UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT, FLOAT, FLOAT]; ++/// texture, the previous cloud frame, the smoke atlas, the world's depth, the previous march lengths. ++pub(super) const CLOUD_BINDINGS: [wgpu::BindingType; 10] = [ ++ UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT, FLOAT, FLOAT, DEPTH, UNFILTERED, ++]; + /// `fs_cloud_shadow`'s group 0: as the clouds' without the history. + pub(super) const CLOUD_SHADOW_BINDINGS: [wgpu::BindingType; 6] = + [UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT]; +@@ -218,15 +222,40 @@ impl HdVolumes { + let cloud_module = module(device, "HD clouds", &cloud_src); + let shadow_module = module(device, "HD cloud shadows", &shadow_src); + let fog_module = module(device, "HD fog", &fog_body()); +- let cloud_pipeline = fullscreen( +- device, +- "HD clouds", +- &cloud_layout, +- &cloud_module, +- "fs_cloud", +- CLOUD_FORMAT, +- None, +- ); ++ let cloud_pipeline = { ++ let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { ++ label: Some("HD clouds"), ++ bind_group_layouts: &[Some(&cloud_layout)], ++ ..Default::default() ++ }); ++ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { ++ label: Some("HD clouds"), ++ layout: Some(&layout), ++ vertex: wgpu::VertexState { ++ module: &cloud_module, ++ entry_point: Some("vs_full"), ++ compilation_options: Default::default(), ++ buffers: &[], ++ }, ++ fragment: Some(wgpu::FragmentState { ++ module: &cloud_module, ++ entry_point: Some("fs_cloud"), ++ compilation_options: Default::default(), ++ targets: &[CLOUD_FORMAT, FOG_DIST_FORMAT].map(|format| { ++ Some(wgpu::ColorTargetState { ++ format, ++ blend: None, ++ write_mask: wgpu::ColorWrites::ALL, ++ }) ++ }), ++ }), ++ primitive: Default::default(), ++ depth_stencil: None, ++ multisample: Default::default(), ++ multiview_mask: None, ++ cache: None, ++ }) ++ }; + let shadow_pipeline = fullscreen( + device, + "HD cloud shadows", +@@ -286,6 +315,11 @@ impl HdVolumes { + one("HD clouds", [1, 1], CLOUD_FORMAT), + one("HD clouds", [1, 1], CLOUD_FORMAT), + ], ++ cloud_dist: [ ++ one("HD cloud distance", [1, 1], FOG_DIST_FORMAT), ++ one("HD cloud distance", [1, 1], FOG_DIST_FORMAT), ++ ], ++ cloud_depth: None, + flip: 0, + history_ready: false, + cloud_shadow: one( +@@ -316,6 +350,10 @@ impl HdVolumes { + one("HD clouds", CLOUD_FORMAT), + one("HD clouds", CLOUD_FORMAT), + ]; ++ self.cloud_dist = [ ++ one("HD cloud distance", FOG_DIST_FORMAT), ++ one("HD cloud distance", FOG_DIST_FORMAT), ++ ]; + self.fog = one("HD fog", FOG_FORMAT); + self.fog_dist = one("HD fog distance", FOG_DIST_FORMAT); + self.history_ready = false; +@@ -342,6 +380,11 @@ impl HdVolumes { + &self.clouds[self.flip] + } + ++ /// The march lengths that go with `cloud_view`. ++ pub fn cloud_dist_view(&self) -> &wgpu::TextureView { ++ &self.cloud_dist[self.flip] ++ } ++ + fn group( + &mut self, + device: &wgpu::Device, +@@ -372,7 +415,12 @@ impl HdVolumes { + frame: &wgpu::Buffer, + sky_lut: &wgpu::TextureView, + atlas: &wgpu::TextureView, ++ depth: &wgpu::TextureView, + ) { ++ if self.cloud_depth.as_ref() != Some(depth) { ++ self.cloud_groups = None; ++ self.cloud_depth = Some(depth.clone()); ++ } + if self.shadow_group.is_none() { + let layout = self.shadow_layout.clone(); + let (n3, n2, clamp, repeat) = ( +@@ -405,6 +453,7 @@ impl HdVolumes { + self.repeat.clone(), + ); + let hist = self.clouds.clone(); ++ let hist_dist = self.cloud_dist.clone(); + let groups = [0, 1].map(|read| { + self.group( + device, +@@ -419,6 +468,8 @@ impl HdVolumes { + wgpu::BindingResource::TextureView(sky_lut), + wgpu::BindingResource::TextureView(&hist[read]), + wgpu::BindingResource::TextureView(atlas), ++ wgpu::BindingResource::TextureView(depth), ++ wgpu::BindingResource::TextureView(&hist_dist[read]), + ], + ) + }); +@@ -455,7 +506,7 @@ impl HdVolumes { + stamped_pass( + encoder, + "HD clouds", +- &[&self.clouds[write]], ++ &[&self.clouds[write], &self.cloud_dist[write]], + &self.cloud_pipeline, + &groups[read], + stamp, +@@ -525,8 +576,13 @@ impl Hd { + depth: &wgpu::TextureView, + ) { + if self.clouds_active { +- self.volumes +- .prepare_clouds(device, &self.frame, &self.sky.lut, &self.smoke.atlas); ++ self.volumes.prepare_clouds( ++ device, ++ &self.frame, ++ &self.sky.lut, ++ &self.smoke.atlas, ++ depth, ++ ); + self.volumes + .draw_cloud_shadow(encoder, stamp(&mut self.timing, "hd cloud shadows")); + self.volumes +@@ -570,3 +626,48 @@ pub fn shader_sources() -> Vec<(&'static str, String, &'static [wgpu::BindingTyp + ("fog", fog_body(), &FOG_BINDINGS), + ] + } ++ ++#[cfg(test)] ++mod tests { ++ use super::*; ++ ++ /// Every world-anchored noise lookup divides the world coordinate (the camera's x, z wrapped to ++ /// 65536) by a scale that divides 65536, or the pattern jumps where the wrap happens (x or z = 0). ++ #[test] ++ fn noise_scales_divide_the_camera_wrap() { ++ let mut found = 0; ++ for (name, src) in [("cloud_common", CLOUD_COMMON), ("fog", FOG)] { ++ for key in ["w / ", "p3 / "] { ++ for part in src.split(key).skip(1) { ++ let num: String = part ++ .chars() ++ .take_while(|c| c.is_ascii_digit() || *c == '.') ++ .collect(); ++ let Ok(scale) = num.parse::() else { ++ continue; ++ }; ++ found += 1; ++ assert!( ++ scale >= 1.0 && scale.fract() == 0.0 && 65536 % (scale as u64) == 0, ++ "{name}: the scale {scale} does not divide 65536" ++ ); ++ } ++ } ++ } ++ assert!(found >= 8, "found only {found} noise lookups"); ++ } ++ ++ /// The cloud noise's height is measured from the layer's base, not from the camera: the same ++ /// world point gives the same noise coordinate at any camera height. ++ #[test] ++ fn cloud_noise_height_is_anchored_to_the_layer() { ++ assert!(CLOUD_COMMON.contains("pos.y - frame.clouds.x")); ++ // frame.clouds.x = CLOUD_BASE - camera height: y_world - CLOUD_BASE however high the camera. ++ for cam in [64.0f32, 210.0, 1000.0] { ++ let world_y = 230.0f32; ++ let rel = world_y - cam; ++ let base_rel = CLOUD_BASE - cam; ++ assert!((rel - base_rel - (world_y - CLOUD_BASE)).abs() < 1e-3); ++ } ++ } ++} +-- +2.54.0 (Apple Git-157) +