Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5839da129a |
+17
-2
@@ -39,7 +39,7 @@ cd .. && rm -f horizon-patches/*.patch && git -C horizon format-patch -q $(cat H
|
||||
|
||||
## Конвейер кадра (HD включён)
|
||||
|
||||
Все цели в «масштабированном» разрешении мира (`world_post`), HDR — `Rgba16Float`. Глубина камеры — reversed-Z
|
||||
Цели мира (G-буфер, AO, освещение, пост-проход мода) — в «масштабированном» разрешении мира (`world_post`; при `HORIZON_HD_SCALE` < 1 оно меньше окна), цели после TAA (история, bloom, tonemap) — в разрешении окна. HDR — `Rgba16Float`. Глубина камеры — reversed-Z
|
||||
(`Depth32Float`, 1 = близко, 0 = далеко). Позиции везде относительно камеры (camera-relative), мир — в `f64`
|
||||
на CPU.
|
||||
|
||||
@@ -62,6 +62,13 @@ cd .. && rm -f horizon-patches/*.patch && git -C horizon format-patch -q $(cat H
|
||||
6. **TAA** — после пост-прохода мода (`fx`, либо `lit` без него), до bloom: история пинг-понгом, в альфе истории —
|
||||
дистанция для отбраковки дизокклюзии; телепорт > 64 блоков сбрасывает историю. Джиттер добавляется к
|
||||
`view_projection` только при HD (публичная матрица без джиттера); рука и HUD рисуются после tonemap и не дрожат.
|
||||
**TAAU**: если мир меньше окна (`HORIZON_HD_SCALE=0.67`), тот же проход (`fs_taau`, в таймингах `hd taau`) восстанавливает
|
||||
полное разрешение. История, bloom и tonemap — в разрешении окна. Для каждого пикселя выхода берётся 3×3 выборки мира
|
||||
вокруг (с учётом джиттера в `taa_params.zw`), веса — полином, аппроксимирующий Lanczos-2, по расстоянию от центра
|
||||
пикселя выхода до выборки; variance clip в YCoCg по тем же 3×3; глубина — ближайшая из 2×2; история (Catmull-Rom,
|
||||
полное разрешение) вес 0.92, но ближе к ней там, где ближайшая выборка далеко от центра пикселя. Джиттер —
|
||||
Halton(2,3) на `max(16, ceil(8·ratio²))` кадров (0.67 → 18, 0.5 → 32). При масштабе 1 работает прежний `fs_taa`
|
||||
без изменений. С `HORIZON_HD_TAA=0` и масштабом < 1 остаётся простой билинейный апскейл в tonemap.
|
||||
7. **Bloom** (prefilter + 6 уровней: 13-tap вниз, tent вверх) и **tonemap**: до 0.8 как есть (ванильный вид),
|
||||
выше — мягкий roll-off по ярчайшему каналу; лёгкий грейд (+12% насыщенности, мягкая S-кривая).
|
||||
|
||||
@@ -113,8 +120,14 @@ $H key f1 tap; $H screenshot out.png; $H bench 4; $H quit
|
||||
Тайминги проходов на GPU: `HORIZON_SHADER_PACK_TIMING=1` (лог в `run-home/ctl.log`) или список F3.
|
||||
Ориентиры (M5, 2560×1440): тени 0.6 + 2.85 мс (дальний каскад через кадр, ≈1.4 в среднем), G-буфер 0.8,
|
||||
AO 2.9, освещение 5.2, TAA 2.7, bloom 1.1, tonemap 0.2 — кадр ≈ 15 мс GPU (замеры шумные).
|
||||
TAAU (2560×1440, GPU avg кадр, сцена Polygon): scale 1.0 — 18.1 мс; 0.67 — 13.4 мс; 0.5 — 10.4 мс (освещение 6.1 → 2.7 → 1.6, AO 3.5 → 1.4 → 0.8, `hd taau` ≈ 3.5 мс на любом масштабе, дальний каскад теней 3.4 мс не зависит от масштаба).
|
||||
Следующие цели по скорости: освещение (лучи 14 шагов на полном разрешении → полразрешения + апскейл) и AO.
|
||||
|
||||
Проверка мерцания: два скриншота с неподвижной камеры подряд и разница по пикселям (Pillow `ImageChops.difference`)
|
||||
— на мире `Hills` с травой и деревьями; после исправлений ≈2% пикселей с разницей > 20/255 (дрожание TAA на пиксельных
|
||||
текстурах и анимация воды), а при ошибке теней было 8.5%. Сравни с `HORIZON_HD_TAA=0` / `HORIZON_HD_AO=0` /
|
||||
`HORIZON_HD_SHADOW_FAST=0`, чтобы найти виновника.
|
||||
|
||||
Отладка: временно выводи промежуточные величины цветом в `fs_light` (так нашли пустую карту теней и неверное
|
||||
направление солнца). Помни: солнце в мире идёт по оси X (`(-sin a, cos a, 0)`), а `Globals.sun` неба — в своей
|
||||
плоскости YZ.
|
||||
@@ -131,13 +144,15 @@ AO 2.9, освещение 5.2, TAA 2.7, bloom 1.1, tonemap 0.2 — кадр ≈
|
||||
| Отсечение взрывов в `effects.wgsl` (#11), порядок дыма (#16), метрика перекрытия свечения (#14) | готово | 67a2a52, ed2db85 |
|
||||
| Тени: только грани к солнцу, без «закопанных» секций, дальний каскад через кадр (`HORIZON_HD_SHADOW_FAST=0` — по-старому) | готово | патч 16 |
|
||||
| TAA: джиттер Halton(2,3) ×16, перепроекция по глубине и сдвигу камеры, variance clip в YCoCg, история Catmull-Rom 0.9, лёгкий шарп в tonemap (`HORIZON_HD_TAA=0` — выкл) | готово | патч 16 |
|
||||
| TAAU: мир в `HORIZON_HD_SCALE` (0.25–1, по умолчанию 1 = как раньше), восстановление в TAA-проходе `fs_taau`; без LOD-bias текстур (не доступен) | готово | патч 18 |
|
||||
| Исправлено мерцание теней у игрока: ближний каскад терял секции в кадры, когда дальний держится; держимая матрица дальнего каскада сдвигается на шаг камеры; у растений (наклонные квады) нормаль «вверх», а не к камере; TAA держит 94% истории в покое | готово | патч 18 |
|
||||
| API моделей по хэндлам (#30, уровень API 11: `model-id`, `model-generation`, `set-models-by-id`) | готово | патч 17 |
|
||||
|
||||
## План (по порядку) и ТЗ
|
||||
|
||||
Каждый пункт — самостоятельная задача. Перед началом прочитай `hd.rs` целиком.
|
||||
|
||||
1. **TAAU — временной апскейлер** (поверх TAA). Мир и G-буфер рендерятся в `render_scale` (например 0.67),
|
||||
1. **TAAU — временной апскейлер** (поверх TAA) — СДЕЛАНО (патч 18, см. статус; масштаб — `HORIZON_HD_SCALE`, переопределяет «масштаб разрешения» настроек Horizon только для HD; поля в `HdSettings` нет). Осталось: LOD-bias текстур террейна, MetalFX. Исходное ТЗ: Мир и G-буфер рендерятся в `render_scale` (например 0.67),
|
||||
TAA-проход восстанавливает полное разрешение: история в полном разрешении, текущий кадр семплируется с учётом
|
||||
джиттера (Catmull-Rom/Lanczos-веса по субпиксельной позиции), клиппинг окрестности как в TAA. Настройка
|
||||
масштаба (`HORIZON_HD_SCALE` или поле в `HdSettings`). Цель: при 0.67 картинка близка к нативной, FPS заметно
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 3de7a6875140a107e28563cc44489b55f5794517 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:37:45 +0300
|
||||
Subject: [PATCH 01/17] Mod API (AirDef fork): world models, client particles,
|
||||
Subject: [PATCH 01/18] Mod API (AirDef fork): world models, client particles,
|
||||
sounds, ground heights
|
||||
|
||||
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: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 12:39:45 +0300
|
||||
Subject: [PATCH 02/17] cargo-horizon-mod: report particles and sounds
|
||||
Subject: [PATCH 02/18] cargo-horizon-mod: report particles and sounds
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From ee79b3562feebe51959a01f1585a9e85bd53ccb5 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 13:33:10 +0300
|
||||
Subject: [PATCH 03/17] Mod API (AirDef fork): full-screen post pass for mods
|
||||
Subject: [PATCH 03/18] 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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 2b137dbbedd540daeebc7f5e387847aeb56a1355 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:09:30 +0300
|
||||
Subject: [PATCH 04/17] Mod models: draw far models as scaled impostors past
|
||||
Subject: [PATCH 04/18] Mod models: draw far models as scaled impostors past
|
||||
the far plane, cull at 4096
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From c8ebca6e0af68630e65211844b34d25c5b985c03 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 22:31:00 +0300
|
||||
Subject: [PATCH 05/17] Mod cameras: render.set-views draws the world from
|
||||
Subject: [PATCH 05/18] 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 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 8d2261fc4f8072c32e454139cfea7a8c0cd9d664 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:08:21 +0300
|
||||
Subject: [PATCH 06/17] Mods: more fuel per tick and per event (AirDef plans
|
||||
Subject: [PATCH 06/18] Mods: more fuel per tick and per event (AirDef plans
|
||||
raids)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 6c548de34def8e4a624d734243418090798bd16a Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Wed, 28 Oct 2026 23:25:29 +0300
|
||||
Subject: [PATCH 07/17] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
Subject: [PATCH 07/18] Mod cameras up to 1920x1080 (full-screen replays)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 9b9e93269d67401cc620567a884d5ce97f68d76e Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:15:11 +0300
|
||||
Subject: [PATCH 08/17] Mod cameras: distant terrain tiles, fog measured from
|
||||
Subject: [PATCH 08/18] Mod cameras: distant terrain tiles, fog measured from
|
||||
the camera, no impostors while one is shown
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 065d9fa02966238a5bd9ff460392a1a809db0b11 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 11:25:45 +0300
|
||||
Subject: [PATCH 09/17] Mods: game.world-id, so mods keep state per world (API
|
||||
Subject: [PATCH 09/18] Mods: game.world-id, so mods keep state per world (API
|
||||
level 9)
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 1bf28135fd9a31f837323dc3a8d81ed73343d816 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 12:01:08 +0300
|
||||
Subject: [PATCH 10/17] cargo-horizon-mod: trial runs behind feature full, so a
|
||||
Subject: [PATCH 10/18] cargo-horizon-mod: trial runs behind feature full, so a
|
||||
packing-only tool skips wasmtime
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 0a56f5616df4699588cbcc529d4720661baa810f Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 13:18:43 +0300
|
||||
Subject: [PATCH 11/17] HD lighting: G-buffer pass, per-pixel mod point lights
|
||||
Subject: [PATCH 11/18] 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)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 9ebf0031fe9ba6b7f02afb26e4fd3a1ba4ddb0d0 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 13:59:43 +0300
|
||||
Subject: [PATCH 12/17] HD lighting stage 2: sun and moon shadow cascades, the
|
||||
Subject: [PATCH 12/18] 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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 7a5a38f68923aec62f10bfa0df50b436474ed612 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 16:19:53 +0300
|
||||
Subject: [PATCH 13/17] HD: bind groups cached until a resize or a new source,
|
||||
Subject: [PATCH 13/18] 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 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 8020811e57d4b5a9ea1cd72d30ce859dc5095d35 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 16:25:44 +0300
|
||||
Subject: [PATCH 14/17] HD: lights culled per 16x16 tile (surface and halo
|
||||
Subject: [PATCH 14/18] 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 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 72d3fddfea329e578162656151350f0aa2efd323 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 16:43:36 +0300
|
||||
Subject: [PATCH 15/17] HD: GPU time per pass in the F3 timing list,
|
||||
Subject: [PATCH 15/18] 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 <noreply@anthropic.com>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 1fd83a821bf270879d8ece5aa583f200ed920d89 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:02:02 +0300
|
||||
Subject: [PATCH 16/17] HD: shadows draw only sun-facing quads and skip buried
|
||||
Subject: [PATCH 16/18] 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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
From 1502d359d599228fe0f44ce6b99a3fded2ac5ea0 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:02:02 +0300
|
||||
Subject: [PATCH 17/17] Mods: model handles (render.model-id, model-generation,
|
||||
Subject: [PATCH 17/18] 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
|
||||
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
From 0f9d99b8ad3e4b72e621d84c2396cf1d3d2b7a33 Mon Sep 17 00:00:00 2001
|
||||
From: chexit <chexit@duck.com>
|
||||
Date: Thu, 29 Oct 2026 17:19:57 +0300
|
||||
Subject: [PATCH 18/18] 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
|
||||
camera's step, plants' normals turned with the camera); TAA keeps more
|
||||
history when still
|
||||
|
||||
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||||
---
|
||||
crates/horizon-client/src/gfx.rs | 33 ++-
|
||||
crates/horizon-client/src/gfx/hd.rs | 211 ++++++++++++++++++--
|
||||
crates/horizon-client/src/gfx/world_post.rs | 8 +-
|
||||
crates/horizon-client/src/terrain.wgsl | 4 +
|
||||
4 files changed, 223 insertions(+), 33 deletions(-)
|
||||
|
||||
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
|
||||
index eb152667c..b85c6e269 100644
|
||||
--- a/crates/horizon-client/src/gfx.rs
|
||||
+++ b/crates/horizon-client/src/gfx.rs
|
||||
@@ -3970,13 +3970,15 @@ impl Renderer {
|
||||
// offset each frame; `unjittered` is what the next frame reprojects with.
|
||||
let unjittered = self.view_projection(frame);
|
||||
let taa = hd_wanted && hd::taa_enabled();
|
||||
+ let mut jitter_px = [0.0; 2];
|
||||
let view_proj = if taa {
|
||||
- let size = self
|
||||
- .world_post
|
||||
- .size()
|
||||
- .unwrap_or([self.config.width, self.config.height]);
|
||||
- let j = hd::jitter(self.taa_frame, size);
|
||||
+ let window = [self.config.width, self.config.height];
|
||||
+ let size = self.world_post.size().unwrap_or(window);
|
||||
+ // Upscaling (the world smaller than the window) cycles through more positions.
|
||||
+ let ratio = window[0] as f32 / size[0].max(1) as f32;
|
||||
+ let j = hd::jitter(self.taa_frame, size, hd::jitter_count(ratio));
|
||||
self.taa_frame = self.taa_frame.wrapping_add(1);
|
||||
+ jitter_px = [j[0] * size[0] as f32 * 0.5, -j[1] * size[1] as f32 * 0.5];
|
||||
Mat4::from_translation(Vec3::new(j[0], j[1], 0.0)) * unjittered
|
||||
} else {
|
||||
unjittered
|
||||
@@ -4192,6 +4194,7 @@ impl Renderer {
|
||||
cascades: [Mat4::IDENTITY; 2],
|
||||
view_proj,
|
||||
taa: taa && taa_prev.is_some(),
|
||||
+ jitter_px,
|
||||
prev_view_proj: taa_prev.map_or(unjittered, |p| p.0),
|
||||
camera_delta: taa_prev.map_or(Vec3::ZERO, |p| (frame.camera - p.1).as_vec3()),
|
||||
// The world's sun: vanilla turns the sky -90° about Y, so it crosses from east to west.
|
||||
@@ -4218,8 +4221,11 @@ impl Renderer {
|
||||
};
|
||||
if fresh {
|
||||
self.hd_far = Some((hd_cascades[1], camera, shadow_dir, 0));
|
||||
- } else if let Some((held, _, _, age)) = &mut self.hd_far {
|
||||
- hd_cascades[1] = *held;
|
||||
+ } else if let Some((held, at, _, age)) = &mut self.hd_far {
|
||||
+ // The held matrix is relative to the camera it was drawn from: shifted by the
|
||||
+ // camera's step since, so the map stays put in the world as the player walks.
|
||||
+ let step = (camera - *at).as_vec3();
|
||||
+ hd_cascades[1] = *held * Mat4::from_translation(step);
|
||||
*age += 1;
|
||||
}
|
||||
fresh
|
||||
@@ -4230,11 +4236,14 @@ impl Renderer {
|
||||
hd_scene.cascades = hd_cascades;
|
||||
let mut hd_shadow_visible = std::mem::take(&mut self.hd_shadow_visible);
|
||||
hd_shadow_visible.clear();
|
||||
- if hd_wanted && shadow_strength > 0.0 && hd_far_fresh {
|
||||
+ if hd_wanted && shadow_strength > 0.0 {
|
||||
+ // The near cascade is drawn every frame: between the far one's redraws the list holds
|
||||
+ // only the sections in the near box.
|
||||
+ let c = if hd_far_fresh { 1 } else { 0 };
|
||||
let radius =
|
||||
- ((frame.detail_distance / 16.0) as i32).min((hd::CASCADES[1] / 16.0) as i32 + 2);
|
||||
+ ((frame.detail_distance / 16.0) as i32).min((hd::CASCADES[c] / 16.0) as i32 + 2);
|
||||
let light = Frustum {
|
||||
- planes: frustum(hd_cascades[1]),
|
||||
+ planes: frustum(hd_cascades[c]),
|
||||
};
|
||||
self.sections.visible(
|
||||
camera,
|
||||
@@ -5028,6 +5037,8 @@ impl Renderer {
|
||||
.as_ref()
|
||||
.and_then(|p| p.1.as_ref())
|
||||
.is_some_and(|p| p.has_weather());
|
||||
+ // AirDef fork: HD renders the world at its own scale and upscales it in the TAA pass.
|
||||
+ self.world_post.hd_scale = hd_on.then(hd::scale_override).flatten();
|
||||
let scaled = self.world_post.prepare(
|
||||
&self.device,
|
||||
[self.config.width, self.config.height],
|
||||
@@ -5046,7 +5057,7 @@ impl Renderer {
|
||||
let hd = self
|
||||
.hd
|
||||
.get_or_insert_with(|| hd::Hd::new(device, size, format, globals_size));
|
||||
- hd.resize(device, size);
|
||||
+ hd.resize(device, size, [self.config.width, self.config.height]);
|
||||
if let Some(settings) = frame.hd {
|
||||
hd.write(&self.queue, &hd_scene, frame.hd_lights, settings);
|
||||
}
|
||||
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
||||
index 16610cb02..c54b7b961 100644
|
||||
--- a/crates/horizon-client/src/gfx/hd.rs
|
||||
+++ b/crates/horizon-client/src/gfx/hd.rs
|
||||
@@ -89,7 +89,7 @@ struct HdFrame {
|
||||
prev_view_proj: [[f32; 4]; 4],
|
||||
/// xyz: how far the camera moved since that frame; w: 1 when the history is usable.
|
||||
taa: [f32; 4],
|
||||
- /// x: the history's weight; y: the sharpening (0 without TAA).
|
||||
+ /// x: the history's weight; y: the sharpening (0 without TAA); zw: the jitter in world pixels.
|
||||
taa_params: [f32; 4],
|
||||
}
|
||||
|
||||
@@ -105,6 +105,9 @@ pub struct HdScene {
|
||||
pub camera_delta: glam::Vec3,
|
||||
/// This frame is anti-aliased over time (the projection is jittered).
|
||||
pub taa: bool,
|
||||
+ /// The jitter in world-target pixels, screen orientation (x right, y down): the world's sample
|
||||
+ /// of a pixel lies this far from its centre (the upscaler weighs samples by it).
|
||||
+ pub jitter_px: [f32; 2],
|
||||
/// Towards the sun, and the sky's darkening (0 day, 1 night).
|
||||
pub sun: glam::Vec3,
|
||||
pub dark: f32,
|
||||
@@ -662,8 +665,8 @@ fn fs_taa(in: FullOut) -> TaaOut {
|
||||
if abs(seen - expected) <= expected * 0.03 + 0.25 {
|
||||
let mean = sum / 9.0;
|
||||
let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3<f32>(0.0)));
|
||||
- let box_lo = min(max(mean - sigma * 1.25, lo), centre);
|
||||
- let box_hi = max(min(mean + sigma * 1.25, hi), centre);
|
||||
+ let box_lo = min(max(mean - sigma * 1.5, lo), centre);
|
||||
+ let box_hi = max(min(mean + sigma * 1.5, hi), centre);
|
||||
let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
|
||||
// Fast motion trusts the history less.
|
||||
let moved = length((uv - in.uv) * vec2<f32>(size));
|
||||
@@ -679,6 +682,99 @@ fn fs_taa(in: FullOut) -> TaaOut {
|
||||
out.history = vec4<f32>(color, dist);
|
||||
return out;
|
||||
}
|
||||
+
|
||||
+// The jittered samples' reconstruction filter (a polynomial fit of Lanczos-2, radius 2 in
|
||||
+// world-target pixels): 1 at 0, 0 at 1 and 2, a little negative between.
|
||||
+fn lanczos2(x: f32) -> f32 {
|
||||
+ let x2 = min(x * x, 4.0);
|
||||
+ let a = 0.4 * x2 - 1.0;
|
||||
+ let b = 0.25 * x2 - 1.0;
|
||||
+ return (1.5625 * a * a - 0.5625) * b * b;
|
||||
+}
|
||||
+
|
||||
+// Temporal upscaling: the world (and its depth) is smaller than the output. Each output pixel
|
||||
+// gathers the 3x3 world samples round it, each weighed by its distance from the pixel's centre
|
||||
+// (the samples lie off the pixel centres by the frame's jitter), clamps the reprojected full-size
|
||||
+// history to their colour box and blends: more history where the nearest sample is far away.
|
||||
+@fragment
|
||||
+fn fs_taau(in: FullOut) -> TaaOut {
|
||||
+ let low = vec2<f32>(textureDimensions(source));
|
||||
+ let lowi = vec2<i32>(low);
|
||||
+ let high = vec2<f32>(textureDimensions(history));
|
||||
+ let jit = frame.taa_params.zw;
|
||||
+ // The output pixel's place among the world's samples: sample i lies at index i - jit, and
|
||||
+ // the nearest to this pixel is `base`.
|
||||
+ let t = in.uv * low - 0.5 + jit;
|
||||
+ let base = vec2<i32>(floor(t + 0.5));
|
||||
+ var sum = vec3<f32>(0.0);
|
||||
+ var sum2 = vec3<f32>(0.0);
|
||||
+ var lo = vec3<f32>(1.0e9);
|
||||
+ var hi = vec3<f32>(-1.0e9);
|
||||
+ var weighted = vec3<f32>(0.0);
|
||||
+ var total = 0.0;
|
||||
+ for (var y = -1; y <= 1; y++) {
|
||||
+ for (var x = -1; x <= 1; x++) {
|
||||
+ let q = clamp(base + vec2<i32>(x, y), vec2<i32>(0), lowi - 1);
|
||||
+ let s = to_ycocg(squash(textureLoad(source, q, 0).rgb));
|
||||
+ sum += s;
|
||||
+ sum2 += s * s;
|
||||
+ lo = min(lo, s);
|
||||
+ hi = max(hi, s);
|
||||
+ let off = vec2<f32>(base + vec2<i32>(x, y)) - t;
|
||||
+ let w = lanczos2(off.x) * lanczos2(off.y);
|
||||
+ weighted += s * w;
|
||||
+ total += w;
|
||||
+ }
|
||||
+ }
|
||||
+ let current = weighted / max(total, 0.001);
|
||||
+ // Depth: the nearest of the 2x2 samples round the pixel (steady between frames at an edge).
|
||||
+ let corner = vec2<i32>(floor(in.uv * low - 0.5 + jit));
|
||||
+ var d = 0.0;
|
||||
+ for (var y = 0; y <= 1; y++) {
|
||||
+ for (var x = 0; x <= 1; x++) {
|
||||
+ d = max(d, textureLoad(depth_tex, clamp(corner + vec2<i32>(x, y), vec2<i32>(0), lowi - 1), 0));
|
||||
+ }
|
||||
+ }
|
||||
+ let sky = d <= 0.0;
|
||||
+ // The world was drawn shifted by the jitter in clip space; this pixel's unshifted point.
|
||||
+ let jc = vec2<f32>(jit.x * 2.0 / low.x, -jit.y * 2.0 / low.y);
|
||||
+ let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0 + jc.x, 1.0 - in.uv.y * 2.0 + jc.y, max(d, 0.000001), 1.0);
|
||||
+ let h = frame.inverse_view_proj * ndc;
|
||||
+ let p = h.xyz / h.w;
|
||||
+ let dist = select(length(p), 0.0, sky);
|
||||
+ var resolved = current;
|
||||
+ if frame.taa.w > 0.5 {
|
||||
+ let prev = frame.prev_view_proj * vec4<f32>(select(p + frame.taa.xyz, p, sky), 1.0);
|
||||
+ if prev.w > 0.0 {
|
||||
+ let pn = prev.xy / prev.w;
|
||||
+ let uv = vec2<f32>(pn.x * 0.5 + 0.5, 0.5 - pn.y * 0.5);
|
||||
+ if all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0)) {
|
||||
+ let hp = clamp(vec2<i32>(uv * high), vec2<i32>(0), vec2<i32>(high) - 1);
|
||||
+ let seen = textureLoad(history, hp, 0).a;
|
||||
+ let expected = select(length(p + frame.taa.xyz), 0.0, sky);
|
||||
+ if abs(seen - expected) <= expected * 0.03 + 0.25 {
|
||||
+ let mean = sum / 9.0;
|
||||
+ let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3<f32>(0.0)));
|
||||
+ let box_lo = min(max(mean - sigma * 1.25, lo), current);
|
||||
+ let box_hi = max(min(mean + sigma * 1.25, hi), current);
|
||||
+ let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
|
||||
+ // How far the nearest sample is from the pixel's centre (0 on it, 1 at the
|
||||
+ // corner of the world's pixel): the farther, the less the frame says.
|
||||
+ let near = clamp(length(t - floor(t + 0.5)) / 0.7071, 0.0, 1.0);
|
||||
+ let fresh = (1.0 - frame.taa_params.x) * mix(1.8, 0.5, near);
|
||||
+ let moved = length((uv - in.uv) * high);
|
||||
+ let keep = (1.0 - fresh) * mix(1.0, 0.8, clamp(moved / 4.0, 0.0, 1.0));
|
||||
+ resolved = mix(current, past, clamp(keep, 0.0, 0.98));
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ let color = unsquash(from_ycocg(resolved));
|
||||
+ var out: TaaOut;
|
||||
+ out.color = vec4<f32>(color, 1.0);
|
||||
+ out.history = vec4<f32>(color, dist);
|
||||
+ return out;
|
||||
+}
|
||||
"#;
|
||||
|
||||
struct Target {
|
||||
@@ -744,10 +840,35 @@ fn halton(mut index: u32, base: u32) -> f32 {
|
||||
r
|
||||
}
|
||||
|
||||
+/// The history's weight when upscaling (the frames each pixel gathers are sparse, so more of them
|
||||
+/// are kept than without; `fs_taau` lowers it where the frame's sample is near).
|
||||
+const UPSCALE_HISTORY: f32 = 0.92;
|
||||
+
|
||||
+/// The world's size relative to the output (`HORIZON_HD_SCALE`, per axis, 0.25..=1) while HD is on, if
|
||||
+/// set: it replaces the resolution scale of Horizon's settings for HD. 1 renders at full size.
|
||||
+pub fn scale_override() -> Option<f32> {
|
||||
+ static SCALE: std::sync::OnceLock<Option<f32>> = std::sync::OnceLock::new();
|
||||
+ *SCALE.get_or_init(|| {
|
||||
+ let v: f32 = std::env::var("HORIZON_HD_SCALE").ok()?.parse().ok()?;
|
||||
+ v.is_finite().then(|| v.clamp(0.25, 1.0))
|
||||
+ })
|
||||
+}
|
||||
+
|
||||
+/// How many jitter positions the sequence cycles through for an output `ratio` times the world's
|
||||
+/// size per axis: 16 without upscaling, else 8 per output pixel's share of a world pixel
|
||||
+/// (ratio squared), so every output pixel gets samples all over the world pixel it lies in.
|
||||
+pub fn jitter_count(ratio: f32) -> u32 {
|
||||
+ if ratio <= 1.001 {
|
||||
+ 16
|
||||
+ } else {
|
||||
+ ((8.0 * ratio * ratio).ceil() as u32).clamp(16, 64)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
/// Frame `frame`'s sub-pixel offset of the projection in clip space (a fraction of the
|
||||
-/// screen's size in x and y), from the Halton(2, 3) sequence over 16 frames.
|
||||
-pub fn jitter(frame: u32, size: [u32; 2]) -> [f32; 2] {
|
||||
- let i = frame % 16 + 1;
|
||||
+/// screen's size in x and y), from the Halton(2, 3) sequence over `count` frames.
|
||||
+pub fn jitter(frame: u32, size: [u32; 2], count: u32) -> [f32; 2] {
|
||||
+ let i = frame % count.max(1) + 1;
|
||||
[
|
||||
(halton(i, 2) - 0.5) * 2.0 / size[0].max(1) as f32,
|
||||
(halton(i, 3) - 0.5) * 2.0 / size[1].max(1) as f32,
|
||||
@@ -755,7 +876,10 @@ pub fn jitter(frame: u32, size: [u32; 2]) -> [f32; 2] {
|
||||
}
|
||||
|
||||
pub struct Hd {
|
||||
+ /// The world's size (the G-buffer, lighting, AO, post pass), and the output's (TAA, bloom,
|
||||
+ /// tone curve): bigger when the world is upscaled.
|
||||
size: [u32; 2],
|
||||
+ out: [u32; 2],
|
||||
pub albedo: wgpu::TextureView,
|
||||
pub normal: wgpu::TextureView,
|
||||
pub levels: wgpu::TextureView,
|
||||
@@ -779,6 +903,7 @@ pub struct Hd {
|
||||
taa_active: bool,
|
||||
taa_layout: wgpu::BindGroupLayout,
|
||||
taa_pipeline: wgpu::RenderPipeline,
|
||||
+ taau_pipeline: wgpu::RenderPipeline,
|
||||
taa_groups: Option<((wgpu::TextureView, wgpu::TextureView), [wgpu::BindGroup; 2])>,
|
||||
bloom: Vec<Target>,
|
||||
/// Bind groups, rebuilt only when their views change: `resize` drops all of them, the
|
||||
@@ -1098,7 +1223,7 @@ impl Hd {
|
||||
entries: &entries(&[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER]),
|
||||
});
|
||||
let taa_module = module(device, "HD TAA", TAA);
|
||||
- let taa_pipeline = {
|
||||
+ let taa_pipeline = |entry: &str| {
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("HD TAA"),
|
||||
bind_group_layouts: &[Some(&taa_layout)],
|
||||
@@ -1120,7 +1245,7 @@ impl Hd {
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &taa_module,
|
||||
- entry_point: Some("fs_taa"),
|
||||
+ entry_point: Some(entry),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[target.clone(), target],
|
||||
}),
|
||||
@@ -1131,6 +1256,7 @@ impl Hd {
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
+ let (taa_pipeline, taau_pipeline) = (taa_pipeline("fs_taa"), taa_pipeline("fs_taau"));
|
||||
let ao_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("HD ambient occlusion"),
|
||||
entries: &entries(&[UNIFORM, DEPTH, UNFILTERED]),
|
||||
@@ -1288,6 +1414,7 @@ impl Hd {
|
||||
};
|
||||
let mut hd = Hd {
|
||||
size: [0, 0],
|
||||
+ out: [0, 0],
|
||||
albedo: target(device, "HD albedo", [1, 1], ALBEDO).view,
|
||||
normal: target(device, "HD normal", [1, 1], NORMAL).view,
|
||||
levels: target(device, "HD light levels", [1, 1], LIGHT_LEVELS).view,
|
||||
@@ -1319,6 +1446,7 @@ impl Hd {
|
||||
taa_active: false,
|
||||
taa_layout,
|
||||
taa_pipeline,
|
||||
+ taau_pipeline,
|
||||
taa_groups: None,
|
||||
bloom: Vec::new(),
|
||||
light_group: None,
|
||||
@@ -1355,26 +1483,29 @@ impl Hd {
|
||||
groups_seen: 0,
|
||||
groups_last_frame: 0,
|
||||
};
|
||||
- hd.resize(device, size);
|
||||
+ hd.resize(device, size, size);
|
||||
hd
|
||||
}
|
||||
|
||||
- /// The targets, at the world target's size.
|
||||
- pub fn resize(&mut self, device: &wgpu::Device, size: [u32; 2]) {
|
||||
- if self.size == size {
|
||||
+ /// The targets: the world's at `size` (the world target's), the output's (TAA history, bloom)
|
||||
+ /// at `out`, which is bigger when the world is upscaled.
|
||||
+ pub fn resize(&mut self, device: &wgpu::Device, size: [u32; 2], out: [u32; 2]) {
|
||||
+ let out = [out[0].max(size[0]), out[1].max(size[1])];
|
||||
+ if self.size == size && self.out == out {
|
||||
return;
|
||||
}
|
||||
self.size = size;
|
||||
+ self.out = out;
|
||||
self.albedo = target(device, "HD albedo", size, ALBEDO).view;
|
||||
self.normal = target(device, "HD normal", size, NORMAL).view;
|
||||
self.levels = target(device, "HD light levels", size, LIGHT_LEVELS).view;
|
||||
self.depth = target(device, "HD depth", size, wgpu::TextureFormat::Depth32Float).view;
|
||||
self.lit = target(device, "HD lit", size, HDR).view;
|
||||
self.fx = target(device, "HD post", size, HDR).view;
|
||||
- self.taa = target(device, "HD TAA", size, HDR).view;
|
||||
+ self.taa = target(device, "HD TAA", out, HDR).view;
|
||||
self.history = [
|
||||
- target(device, "HD TAA history", size, HDR).view,
|
||||
- target(device, "HD TAA history", size, HDR).view,
|
||||
+ target(device, "HD TAA history", out, HDR).view,
|
||||
+ target(device, "HD TAA history", out, HDR).view,
|
||||
];
|
||||
self.history_ready = false;
|
||||
self.taa_groups = None;
|
||||
@@ -1404,7 +1535,7 @@ impl Hd {
|
||||
self.tiles_x = size[0].div_ceil(TILE).max(1);
|
||||
self.bloom = (0..BLOOM_LEVELS)
|
||||
.map(|k| {
|
||||
- let s = [(size[0] >> (k + 1)).max(1), (size[1] >> (k + 1)).max(1)];
|
||||
+ let s = [(out[0] >> (k + 1)).max(1), (out[1] >> (k + 1)).max(1)];
|
||||
target(device, "HD bloom", s, HDR)
|
||||
})
|
||||
.collect();
|
||||
@@ -1420,6 +1551,11 @@ impl Hd {
|
||||
.collect();
|
||||
}
|
||||
|
||||
+ /// The world is smaller than the output, so TAA upsamples it.
|
||||
+ fn upscaling(&self) -> bool {
|
||||
+ self.out != self.size
|
||||
+ }
|
||||
+
|
||||
/// This frame's camera, sun, fog, lights and settings.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
@@ -1468,7 +1604,18 @@ impl Hd {
|
||||
scene.camera_delta.z,
|
||||
if self.history_ready { 1.0 } else { 0.0 },
|
||||
],
|
||||
- taa_params: [0.9, if self.taa_active { 0.12 } else { 0.0 }, 0.0, 0.0],
|
||||
+ taa_params: [
|
||||
+ if self.upscaling() {
|
||||
+ UPSCALE_HISTORY
|
||||
+ } else {
|
||||
+ // A still picture keeps 94% of the past: the jitter's shimmer on pixel-art
|
||||
+ // textures averages out instead of flickering.
|
||||
+ 0.94
|
||||
+ },
|
||||
+ if self.taa_active { 0.12 } else { 0.0 },
|
||||
+ scene.jitter_px[0],
|
||||
+ scene.jitter_px[1],
|
||||
+ ],
|
||||
inverse_view_proj: scene.view_proj.inverse().to_cols_array_2d(),
|
||||
resolution: [
|
||||
self.size[0] as f32,
|
||||
@@ -1872,9 +2019,13 @@ impl Hd {
|
||||
return source;
|
||||
};
|
||||
let (read, write) = (self.history_flip, 1 - self.history_flip);
|
||||
+ let upscaling = self.upscaling();
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("HD TAA"),
|
||||
- timestamp_writes: stamp(&mut self.timing, "hd taa"),
|
||||
+ timestamp_writes: stamp(
|
||||
+ &mut self.timing,
|
||||
+ if upscaling { "hd taau" } else { "hd taa" },
|
||||
+ ),
|
||||
color_attachments: &[&self.taa, &self.history[write]].map(|view| {
|
||||
Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
@@ -1888,7 +2039,11 @@ impl Hd {
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
- pass.set_pipeline(&self.taa_pipeline);
|
||||
+ pass.set_pipeline(if upscaling {
|
||||
+ &self.taau_pipeline
|
||||
+ } else {
|
||||
+ &self.taa_pipeline
|
||||
+ });
|
||||
pass.set_bind_group(0, &groups[read], &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
drop(pass);
|
||||
@@ -2045,7 +2200,7 @@ mod tests {
|
||||
let size = [1920u32, 1080];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for f in 0..32 {
|
||||
- let j = jitter(f, size);
|
||||
+ let j = jitter(f, size, 16);
|
||||
assert!(j[0].abs() <= 1.0 / size[0] as f32 + 1e-9);
|
||||
assert!(j[1].abs() <= 1.0 / size[1] as f32 + 1e-9);
|
||||
seen.insert((j[0].to_bits(), j[1].to_bits()));
|
||||
@@ -2053,6 +2208,22 @@ mod tests {
|
||||
assert_eq!(seen.len(), 16);
|
||||
}
|
||||
|
||||
+ #[test]
|
||||
+ fn jitter_covers_the_upscale_ratio() {
|
||||
+ assert_eq!(jitter_count(1.0), 16);
|
||||
+ assert_eq!(jitter_count(1.0 / 0.67), 18);
|
||||
+ assert_eq!(jitter_count(2.0), 32);
|
||||
+ let size = [1280u32, 720];
|
||||
+ let count = jitter_count(2.0);
|
||||
+ let seen: std::collections::HashSet<_> = (0..count * 2)
|
||||
+ .map(|f| {
|
||||
+ let j = jitter(f, size, count);
|
||||
+ (j[0].to_bits(), j[1].to_bits())
|
||||
+ })
|
||||
+ .collect();
|
||||
+ assert_eq!(seen.len(), count as usize);
|
||||
+ }
|
||||
+
|
||||
#[test]
|
||||
fn tile_lists_keep_lights_that_reach_a_tile() {
|
||||
let size = [320, 160];
|
||||
diff --git a/crates/horizon-client/src/gfx/world_post.rs b/crates/horizon-client/src/gfx/world_post.rs
|
||||
index 17609d27d..a32bdc964 100644
|
||||
--- a/crates/horizon-client/src/gfx/world_post.rs
|
||||
+++ b/crates/horizon-client/src/gfx/world_post.rs
|
||||
@@ -18,6 +18,8 @@ pub struct Post {
|
||||
format: wgpu::TextureFormat,
|
||||
/// World resolution relative to the window (0.5..=2.0).
|
||||
pub scale: f32,
|
||||
+ /// HD's own scale for this frame (`HORIZON_HD_SCALE`), instead of `scale`.
|
||||
+ pub hd_scale: Option<f32>,
|
||||
pub antialias: bool,
|
||||
target: Option<WorldTarget>,
|
||||
}
|
||||
@@ -94,6 +96,7 @@ impl Post {
|
||||
sampler,
|
||||
format,
|
||||
scale: 1.0,
|
||||
+ hd_scale: None,
|
||||
antialias: false,
|
||||
target: None,
|
||||
}
|
||||
@@ -108,11 +111,12 @@ impl Post {
|
||||
window: [u32; 2],
|
||||
always: bool,
|
||||
) -> Option<(wgpu::TextureView, wgpu::TextureView)> {
|
||||
- if self.scale == 1.0 && !self.antialias && !always {
|
||||
+ let scale = self.hd_scale.unwrap_or(self.scale);
|
||||
+ if scale == 1.0 && !self.antialias && !always {
|
||||
self.target = None;
|
||||
return None;
|
||||
}
|
||||
- let size = window.map(|v| ((v as f32 * self.scale).round() as u32).clamp(1, 8192));
|
||||
+ let size = window.map(|v| ((v as f32 * scale).round() as u32).clamp(1, 8192));
|
||||
if self.target.as_ref().is_none_or(|t| t.size != size) {
|
||||
let extent = wgpu::Extent3d {
|
||||
width: size[0],
|
||||
diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl
|
||||
index bb5c70f63..c44449cc7 100644
|
||||
--- a/crates/horizon-client/src/terrain.wgsl
|
||||
+++ b/crates/horizon-client/src/terrain.wgsl
|
||||
@@ -166,6 +166,10 @@ struct GBufferOut {
|
||||
}
|
||||
fn face_normal(pos: vec3<f32>) -> vec3<f32> {
|
||||
var n = normalize(cross(dpdx(pos), dpdy(pos)));
|
||||
+ // Block faces are axis-aligned; only plants' crossed quads lean. Those have two
|
||||
+ // sides, so a normal turned to the camera would flip as the player walks by and
|
||||
+ // the sun and the shadow lookup with it: plants take the up normal instead.
|
||||
+ if max(abs(n.x), max(abs(n.y), abs(n.z))) < 0.9 { return vec3(0.0, 1.0, 0.0); }
|
||||
if dot(n, pos) > 0.0 { n = -n; }
|
||||
return n;
|
||||
}
|
||||
--
|
||||
2.54.0 (Apple Git-157)
|
||||
|
||||
Reference in New Issue
Block a user