Clouds occlude by depth, world-anchored noise, visual-only replay time (fork patch 29; #67, #68, #70)
build / mod (push) Successful in 3m29s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-30 03:26:10 +03:00
parent 336ae9f7cc
commit f43fa5ee66
30 changed files with 635 additions and 28 deletions
+4
View File
@@ -118,6 +118,7 @@ Bind groups кэшируются и пересоздаются только пр
| 15 | текстура неба (`sky_lut`) | | 15 | текстура неба (`sky_lut`) |
| 16, 17 | объёмный туман (свет+пропускание) и длина его луча | | 16, 17 | объёмный туман (свет+пропускание) и длина его луча |
| 18, 19 | объёмные облака, карта теней облаков | | 18, 19 | объёмные облака, карта теней облаков |
| 20 | длина луча каждого текселя облаков (`cloud_dist_tex`, для апсэмплинга по глубине) |
`HdFrame` (Rust и WGSL держать в синхроне!): `inverse_view_proj`, `resolution` (w, h, 1/w, 1/h), `params` (число `HdFrame` (Rust и WGSL держать в синхроне!): `inverse_view_proj`, `resolution` (w, h, 1/w, 1/h), `params` (число
источников, экспозиция, bloom, счётчик кадров), `cascades[2]`, `sun` (направление, сила), `sun_color` (rgb, ночь), источников, экспозиция, 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` — выкл. | готово | в рабочем дереве форка | | Материалы и блики (план 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` — выкл. | готово | в рабочем дереве форка | | Ветер (план 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` (следы дронов и ракет, колонны над пожарами, дым взрывов) | готово, в рабочем дереве форка; вид не настраивался (см. «Дым и следы») | не закоммичено | | Объёмный дым и следы ракет (#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` (следы дронов и ракет, колонны над пожарами, дым взрывов) | готово, в рабочем дереве форка; вид не настраивался (см. «Дым и следы») | не закоммичено |
## План (по порядку) и ТЗ ## План (по порядку) и ТЗ
@@ -1,7 +1,7 @@
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001 From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 12:37:45 +0300 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 sounds, ground heights
render.set-models draws block models with an interpolated pose per id; render.set-models draws block models with an interpolated pose per id;
@@ -1,7 +1,7 @@
From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001 From a6d64e6415bf618539af464e521091e72b72d72f Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 12:39:45 +0300 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 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--- ---
@@ -1,7 +1,7 @@
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001 From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 13:33:10 +0300 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, 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 time, 256 vec4 of params, the world's colour and depth) and runs it after
@@ -1,7 +1,7 @@
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001 From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 22:09:30 +0300 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 the far plane, cull at 4096
--- ---
@@ -1,7 +1,7 @@
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001 From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 22:31:00 +0300 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) other points into pictures that HUD view shapes show (API level 9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001 From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 23:08:21 +0300 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) raids)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001 From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Wed, 28 Oct 2026 23:25:29 +0300 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 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--- ---
@@ -1,7 +1,7 @@
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001 From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 11:15:11 +0300 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 the camera, no impostors while one is shown
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001 From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 11:25:45 +0300 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) level 9)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001 From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 12:01:08 +0300 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 packing-only tool skips wasmtime
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 0a56f5616df4699588cbcc529d4720661baa810f Mon Sep 17 00:00:00 2001 From 0a56f5616df4699588cbcc529d4720661baa810f Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 13:18:43 +0300 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 in HDR, bloom and tone curve; render.set-lights and render.set-lighting (API
level 10) level 10)
@@ -1,7 +1,7 @@
From 9ebf0031fe9ba6b7f02afb26e4fd3a1ba4ddb0d0 Mon Sep 17 00:00:00 2001 From 9ebf0031fe9ba6b7f02afb26e4fd3a1ba4ddb0d0 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 13:59:43 +0300 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 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 lightmap), light shafts and halos, fog over it again, a light colour grade
@@ -1,7 +1,7 @@
From 7a5a38f68923aec62f10bfa0df50b436474ed612 Mon Sep 17 00:00:00 2001 From 7a5a38f68923aec62f10bfa0df50b436474ed612 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 16:19:53 +0300 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 shadow passes draw only the entities that cast shadows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 8020811e57d4b5a9ea1cd72d30ce859dc5095d35 Mon Sep 17 00:00:00 2001 From 8020811e57d4b5a9ea1cd72d30ce859dc5095d35 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 16:25:44 +0300 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 lists built on the CPU), a naga test for the lighting shaders
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 72d3fddfea329e578162656151350f0aa2efd323 Mon Sep 17 00:00:00 2001 From 72d3fddfea329e578162656151350f0aa2efd323 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 16:43:36 +0300 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 half-resolution GTAO with a depth-aware blur
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 1fd83a821bf270879d8ece5aa583f200ed920d89 Mon Sep 17 00:00:00 2001 From 1fd83a821bf270879d8ece5aa583f200ed920d89 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 17:02:02 +0300 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, sections, the far cascade every second frame; TAA (Halton jitter,
reprojection by depth, YCoCg variance clipping) before bloom reprojection by depth, YCoCg variance clipping) before bloom
@@ -1,7 +1,7 @@
From 1502d359d599228fe0f44ce6b99a3fded2ac5ea0 Mon Sep 17 00:00:00 2001 From 1502d359d599228fe0f44ce6b99a3fded2ac5ea0 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 17:02:02 +0300 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; set-models-by-id; API level 11) so mods send no model names each tick;
clear-post and set-lighting documented clear-post and set-lighting documented
@@ -1,7 +1,7 @@
From 0f9d99b8ad3e4b72e621d84c2396cf1d3d2b7a33 Mon Sep 17 00:00:00 2001 From 0f9d99b8ad3e4b72e621d84c2396cf1d3d2b7a33 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 17:19:57 +0300 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 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 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 sections on frames the far one was held, the held far matrix ignored the
@@ -1,7 +1,7 @@
From 38250c4392e27d5700842ca6ee47291dd1831b72 Mon Sep 17 00:00:00 2001 From 38250c4392e27d5700842ca6ee47291dd1831b72 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 17:47:52 +0300 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 floor under it lit, a composite with world-space waves, refraction with depth
absorption in the biome colour, screen-space reflections, Fresnel and a absorption in the biome colour, screen-space reflections, Fresnel and a
shadowed sun glint (HORIZON_HD_WATER=0 to switch off) shadowed sun glint (HORIZON_HD_WATER=0 to switch off)
@@ -1,7 +1,7 @@
From dbc6f8a6e9c2eeabe1de15f1794013c90be7faa5 Mon Sep 17 00:00:00 2001 From dbc6f8a6e9c2eeabe1de15f1794013c90be7faa5 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 17:56:04 +0300 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 colour only), a darker, deeper colour
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 13e74537dd6e63482e3613e0e416de1158944dfe Mon Sep 17 00:00:00 2001 From 13e74537dd6e63482e3613e0e416de1158944dfe Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 18:38:19 +0300 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 ozone; constants and transmittance after Photon, MIT) for the sky, haze, fog
and water reflections; warmer sunlight from the atmosphere's transmittance, a and water reflections; warmer sunlight from the atmosphere's transmittance, a
gentle sky-coloured ambient, soft PCSS-lite shadows near the player, a haze gentle sky-coloured ambient, soft PCSS-lite shadows near the player, a haze
@@ -1,7 +1,7 @@
From 6277e051a89c2537570982f6ef275887193c05cf Mon Sep 17 00:00:00 2001 From 6277e051a89c2537570982f6ef275887193c05cf Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 19:02:16 +0300 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 little higher
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,7 +1,7 @@
From 982265d21bb9d74ec0040db03feb5a55762286ad Mon Sep 17 00:00:00 2001 From 982265d21bb9d74ec0040db03feb5a55762286ad Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 21:41:48 +0300 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 tint, blue hour; MIT), volumetric clouds with ground shadows instead of
vanilla's in HD, volumetric fog with sun shafts and morning mist vanilla's in HD, volumetric fog with sun shafts and morning mist
(HORIZON_HD_CLOUDS=0, HORIZON_HD_VOLFOG=0) (HORIZON_HD_CLOUDS=0, HORIZON_HD_VOLFOG=0)
@@ -1,7 +1,7 @@
From 24971b3c67d8c87673eb2e0d204cedf8a2f46d1c Mon Sep 17 00:00:00 2001 From 24971b3c67d8c87673eb2e0d204cedf8a2f46d1c Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 21:43:22 +0300 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 query-sections, world-changes; API level 12): block-exact rays and section
occupancy by collision, sight or sound, answered next tick within budgets, occupancy by collision, sight or sound, answered next tick within budgets,
and a log of changed sections and a log of changed sections
@@ -1,7 +1,7 @@
From fdec4960983bdff82f3793460f9a5234f0cd3f49 Mon Sep 17 00:00:00 2001 From fdec4960983bdff82f3793460f9a5234f0cd3f49 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 22:56:15 +0300 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 geometry (#41), TAA history on the jittered grid (#38), vanilla cloud tint
without a depth heuristic (#35), air fog once over water (#37), translucent 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 things in front of HD water (#36), cheaper sunrise fan (#39), halos in thick
@@ -1,7 +1,7 @@
From 21391afb26f49505d6cdaf454a6355cd4e747503 Mon Sep 17 00:00:00 2001 From 21391afb26f49505d6cdaf454a6355cd4e747503 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 23:19:53 +0300 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, WGSL file per pass, a shared prelude), contracts for colour, depth, alpha,
time and coordinates written down, tests that the frame layout and bindings time and coordinates written down, tests that the frame layout and bindings
match between Rust and WGSL (#45) match between Rust and WGSL (#45)
@@ -1,7 +1,7 @@
From 8d4cadbf8534309ac847d34e229f6d764770475d Mon Sep 17 00:00:00 2001 From 8d4cadbf8534309ac847d34e229f6d764770475d Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 23:22:19 +0300 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 = 65536-block wrap, so the clouds and their shadows no longer jump at x or z =
0 0
@@ -1,7 +1,7 @@
From 0fb59c782abf269fe555faa3241b31acf41e26d7 Mon Sep 17 00:00:00 2001 From 0fb59c782abf269fe555faa3241b31acf41e26d7 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com> From: chexit <chexit@duck.com>
Date: Fri, 30 Oct 2026 03:17:11 +0300 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 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 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 (HORIZON_HD_SPECULAR=0, HORIZON_HD_WIND=0, HORIZON_HD_SMOKE=0); HD shaders
@@ -0,0 +1,603 @@
From e4bc29da45d0d38244c6eb2ba3ffb9ab853540c0 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
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 <noreply@anthropic.com>
---
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<Option<((i64, u32, u32), i32)>>,
+ /// The same for the time a mod shows (a replay's): render only.
+ shown_darken_memo: std::cell::Cell<Option<((i64, u32, u32), i32)>>,
persist: persist::EntityMemory,
/// Connected to a server: the world comes from the network (game/remote.rs).
remote: Option<remote::Remote>,
@@ -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<Option<((i64, u32, u32), i32)>>,
+ ) -> 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<f32>;
@group(0) @binding(7) var smoke_tex: texture_2d<f32>;
+@group(0) @binding(8) var scene_depth: texture_depth_2d;
+@group(0) @binding(9) var history_dist: texture_2d<f32>;
+
+// 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<f32>,
+ // How far the ray was marched: the surface behind this texel, or SKY_LEN for sky.
+ @location(1) dist: vec4<f32>,
+}
fn ign(px: vec2<f32>) -> f32 {
let p = px + vec2<f32>(5.588238 * frame.params.w);
@@ -65,7 +76,26 @@ fn cloud_scatter(
}
@fragment
-fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
+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<i32>(frame.resolution.xy);
+ let base = vec2<i32>(in.position.xy) * 2;
+ var zmax = 0.0;
+ var best = base;
+ for (var k = 0; k < 4; k++) {
+ let q = min(base + vec2<i32>(k & 1, k >> 1), size - 1);
+ let z = textureLoad(scene_depth, q, 0);
+ if z > zmax {
+ zmax = z;
+ best = q;
+ }
+ }
+ let suv = (vec2<f32>(best) + 0.5) * frame.resolution.zw;
+ let sndc = vec4<f32>(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<f32>(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<f32> {
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<f32> {
cur = vec4<f32>(colour, trans);
}
}
+ var out: CloudOut;
+ out.dist = vec4<f32>(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<f32>(dir * hit_t + frame.taa.xyz, 1.0);
@@ -138,12 +172,18 @@ fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
let huv = vec2<f32>(hp.x / hp.w * 0.5 + 0.5, 0.5 - hp.y / hp.w * 0.5);
if all(huv > vec2<f32>(0.0)) && all(huv < vec2<f32>(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<f32>(textureDimensions(history_dist));
+ let old_len = textureLoad(history_dist, vec2<i32>(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<f32>, detail: f32) -> f32 {
d *= smoothstep(0.0, 0.2, h);
if d < 0.002 { return 0.0; }
if detail > 0.0 {
- let p3 = vec3<f32>(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<f32>(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<f32>(warp), 0.0).r;
let w1 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 64.0 + vec3<f32>(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>) -> 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>) -> f32 {
let wy = p.y + frame.vfog.y;
@@ -50,7 +50,7 @@ fn fog_sigma(p: vec3<f32>) -> f32 {
s += frame.sky_extra.z * 0.003;
let w = vec3<f32>(p.x + frame.cloud_cam.x, wy, p.z + frame.cloud_cam.y)
+ vec3<f32>(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<f32>, core: f32) -> f32 {
let w = vec3<f32>(pos.x + frame.cloud_cam.x, pos.y + frame.vfog.y, pos.z + frame.cloud_cam.y)
- vec3<f32>(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<f32>;
@group(0) @binding(18) var cloud_tex: texture_2d<f32>;
@group(0) @binding(19) var cloud_shadow_tex: texture_2d<f32>;
+// 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<f32>;
+const SKY_RAY: f32 = 20000.0;
const CLOUD_SHADOW_R: f32 = 1200.0;
// The pixel being lit, for the shadow filter's noise.
var<private> frag_px: vec2<f32>;
@@ -194,6 +197,34 @@ fn fog_at(px: vec2<i32>, dist: f32) -> vec4<f32> {
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<i32>, dist: f32) -> vec4<f32> {
+ let size = vec2<i32>(textureDimensions(cloud_tex));
+ let f = vec2<f32>(px) * 0.5 - vec2<f32>(0.25);
+ let base = vec2<i32>(floor(f));
+ let t = f - floor(f);
+ var sum = vec4<f32>(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<i32>(i, j), vec2<i32>(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<i32>(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<i32>, p: vec3<f32>, water: bool) -> vec3<f32> {
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<f32>(17.0, 5.0));
+ let nz = 0.6 * wet_noise(wp * 0.5) + 0.4 * wet_noise(wp * 1.25 + vec2<f32>(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<wgpu::TextureView>,
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::<f64>() 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)