Files
airdef/horizon-patches/0026-HD-split-into-modules-gfx-hd-one-Rust-file-and-one-W.patch
T
chexit ccc3e20902
build / mod (push) Successful in 4m4s
release / windows (push) Successful in 23m13s
Playtest fixes: smoke by engine, searchlights, far launch sites that load their chunks, air defence balance, last chance and radar-cued shots, radars in smart raids, echo, Windows release build from a tag
Smoke: one rule by engine, no heat on trails, no trail on P1-SUN. Searchlights: two lamps a target, off with an empty sky. Halos fade before their reach and their sum is limited. Launch sites load chunks through the server thread, go off-map as a last resort, and reach 4000 blocks. Air defence: ranges x1.5, shots grow with urgency, claims released, /airdef lastchance, long shots on a big radar's tracks. Build: rust-toolchain.toml, airdef.py check, generator manifest and --check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:44:14 +03:00

9342 lines
394 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
From 21391afb26f49505d6cdaf454a6355cd4e747503 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 23:19:53 +0300
Subject: [PATCH 26/40] 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)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
crates/horizon-client/src/gfx.rs | 3 -
crates/horizon-client/src/gfx/hd.rs | 3414 -----------------
crates/horizon-client/src/gfx/hd/ao.rs | 159 +
crates/horizon-client/src/gfx/hd/ao.wgsl | 93 +
crates/horizon-client/src/gfx/hd/ao_blur.wgsl | 28 +
crates/horizon-client/src/gfx/hd/bloom.rs | 148 +
crates/horizon-client/src/gfx/hd/bloom.wgsl | 52 +
crates/horizon-client/src/gfx/hd/cloud.wgsl | 146 +
.../src/gfx/hd/cloud_common.wgsl | 71 +
.../src/gfx/hd/cloud_shadow.wgsl | 18 +
crates/horizon-client/src/gfx/hd/fog.wgsl | 124 +
crates/horizon-client/src/gfx/hd/frame.rs | 205 +
crates/horizon-client/src/gfx/hd/gbuffer.rs | 81 +
crates/horizon-client/src/gfx/hd/light.wgsl | 397 ++
crates/horizon-client/src/gfx/hd/lighting.rs | 265 ++
crates/horizon-client/src/gfx/hd/mod.rs | 896 +++++
.../src/gfx/{hd_noise.rs => hd/noise.rs} | 0
crates/horizon-client/src/gfx/hd/prelude.wgsl | 39 +
crates/horizon-client/src/gfx/hd/shadows.rs | 121 +
crates/horizon-client/src/gfx/hd/sky.rs | 160 +
crates/horizon-client/src/gfx/hd/sky.wgsl | 172 +
.../horizon-client/src/gfx/hd/sky_funcs.wgsl | 139 +
crates/horizon-client/src/gfx/hd/taa.rs | 299 ++
crates/horizon-client/src/gfx/hd/taa.wgsl | 215 ++
crates/horizon-client/src/gfx/hd/tiles.rs | 186 +
crates/horizon-client/src/gfx/hd/tonemap.rs | 151 +
crates/horizon-client/src/gfx/hd/tonemap.wgsl | 41 +
.../src/gfx/{hd_volumes.rs => hd/volumes.rs} | 476 +--
crates/horizon-client/src/gfx/hd/water.rs | 209 +
crates/horizon-client/src/gfx/hd/water.wgsl | 302 ++
crates/horizon-client/src/gfx/hd_sky.rs | 390 --
31 files changed, 4806 insertions(+), 4194 deletions(-)
delete mode 100644 crates/horizon-client/src/gfx/hd.rs
create mode 100644 crates/horizon-client/src/gfx/hd/ao.rs
create mode 100644 crates/horizon-client/src/gfx/hd/ao.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/ao_blur.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/bloom.rs
create mode 100644 crates/horizon-client/src/gfx/hd/bloom.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/cloud.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/cloud_common.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/cloud_shadow.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/fog.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/frame.rs
create mode 100644 crates/horizon-client/src/gfx/hd/gbuffer.rs
create mode 100644 crates/horizon-client/src/gfx/hd/light.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/lighting.rs
create mode 100644 crates/horizon-client/src/gfx/hd/mod.rs
rename crates/horizon-client/src/gfx/{hd_noise.rs => hd/noise.rs} (100%)
create mode 100644 crates/horizon-client/src/gfx/hd/prelude.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/shadows.rs
create mode 100644 crates/horizon-client/src/gfx/hd/sky.rs
create mode 100644 crates/horizon-client/src/gfx/hd/sky.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/sky_funcs.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/taa.rs
create mode 100644 crates/horizon-client/src/gfx/hd/taa.wgsl
create mode 100644 crates/horizon-client/src/gfx/hd/tiles.rs
create mode 100644 crates/horizon-client/src/gfx/hd/tonemap.rs
create mode 100644 crates/horizon-client/src/gfx/hd/tonemap.wgsl
rename crates/horizon-client/src/gfx/{hd_volumes.rs => hd/volumes.rs} (52%)
create mode 100644 crates/horizon-client/src/gfx/hd/water.rs
create mode 100644 crates/horizon-client/src/gfx/hd/water.wgsl
delete mode 100644 crates/horizon-client/src/gfx/hd_sky.rs
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
index a8031c1f6..bdc5b867a 100644
--- a/crates/horizon-client/src/gfx.rs
+++ b/crates/horizon-client/src/gfx.rs
@@ -72,9 +72,6 @@ mod cull;
mod end_sky;
mod flat;
mod hd;
-mod hd_noise;
-mod hd_sky;
-mod hd_volumes;
mod hdr;
pub mod lightning;
mod lod;
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
deleted file mode 100644
index e84a618a9..000000000
--- a/crates/horizon-client/src/gfx/hd.rs
+++ /dev/null
@@ -1,3414 +0,0 @@
-//! AirDef fork: HD lighting. Before the world pass the solid terrain and the
-//! entities (with the mods' models) are drawn once more into a G-buffer: the
-//! surface colour without light and its normal. After the world pass the
-//! mods' point lights (rocket flames, flares, blasts) light every pixel of it
-//! into a high-dynamic-range target; the mod's post pass runs on that; then
-//! bright parts bloom and a tone curve brings it to the screen — vanilla's
-//! range (up to about 0.8) left as it is, only what is brighter rolled off.
-use super::hd_sky;
-pub use super::hd_volumes::clouds_enabled;
-use super::hd_volumes::{self, HdVolumes};
-use super::pack_timing::PassTiming;
-use glam::Mat4;
-use std::sync::atomic::{AtomicU64, Ordering};
-
-/// The G-buffer's formats.
-pub const ALBEDO: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
-pub const NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
-/// Sky and block light per pixel.
-pub const LIGHT_LEVELS: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
-/// The lit world and the post pass's output.
-pub const HDR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
-/// The water pass's targets: the surface colour with the block light, and the normal with the sky light.
-pub const WATER_TINT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
-pub const WATER_NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
-/// The most lights drawn a frame.
-pub const MAX_LIGHTS: usize = 512;
-/// The side (pixels) of a tile of the screen with its own short list of lights.
-const TILE: u32 = 16;
-/// The indices all the tile lists may hold together; a tile past it loops over every light.
-const TILE_INDICES: usize = 1 << 19;
-/// A list length that means "every light": the list overflowed.
-const EVERY_LIGHT: u32 = u32::MAX;
-/// Bloom's mip levels.
-const BLOOM_LEVELS: usize = 6;
-/// The ambient occlusion's targets (half resolution): occlusion and the distance it was taken at.
-pub const AO_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rg16Float;
-/// The occlusion's default strength (`Hd::ao_strength`; 0 turns it off).
-pub const AO_STRENGTH: f32 = 1.0;
-/// The timed passes of bloom, by level (each is timed on its own; the report sums them).
-const BLOOM_NAMES: [&str; 2 * BLOOM_LEVELS] = [
- "hd bloom#0",
- "hd bloom#1",
- "hd bloom#2",
- "hd bloom#3",
- "hd bloom#4",
- "hd bloom#5",
- "hd bloom#6",
- "hd bloom#7",
- "hd bloom#8",
- "hd bloom#9",
- "hd bloom#10",
- "hd bloom#11",
-];
-
-/// A light for this frame, relative to the camera.
-#[repr(C)]
-#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
-pub struct HdLight {
- /// xyz: camera-relative position; w: radius.
- pub pos: [f32; 4],
- /// rgb: colour; w: intensity.
- pub color: [f32; 4],
-}
-
-/// The frame's HD settings.
-#[derive(Clone, Copy, Debug)]
-pub struct HdSettings {
- pub exposure: f32,
- pub bloom: f32,
-}
-
-#[repr(C)]
-#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
-struct HdFrame {
- /// Clip space to camera-relative position.
- inverse_view_proj: [[f32; 4]; 4],
- /// width, height, 1/width, 1/height.
- resolution: [f32; 4],
- /// lights, exposure, bloom, a frame counter.
- params: [f32; 4],
- /// The shadow cascades: camera-relative position to the light's clip space.
- cascades: [[[f32; 4]; 4]; 2],
- /// xyz: towards the sun (or the moon at night); w: how much it lights (0 set or hidden).
- sun: [f32; 4],
- sun_color: [f32; 4],
- fog_color: [f32; 4],
- /// Environmental start, end; render-distance start, end.
- fog: [f32; 4],
- /// x: sky light kept in the shade; y: blocks' light; z: the air's density (rays, halos);
- /// w: the ambient occlusion's strength (0 off).
- ambient: [f32; 4],
- /// x: shadows on; y, z: the cascades' half sizes; w: the light tiles per row.
- shadow: [f32; 4],
- /// The previous frame's camera-relative view-projection without jitter (for TAA).
- 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); zw: the jitter in world pixels.
- taa_params: [f32; 4],
- /// The (jittered) world-to-clip matrix the world was drawn with (rays in the water pass).
- view_proj: [[f32; 4]; 4],
- /// x: the water effect runs this frame; y: seconds for the waves (wraps at 600); zw: the
- /// camera's x and z modulo 256, the waves' period.
- water: [f32; 4],
- /// rgb: the sky's colour; w: 1 when there is a sky.
- sky_color: [f32; 4],
- /// xyz: towards the sun itself (not the moon); w: 1 when the atmosphere draws the sky.
- sky_sun: [f32; 4],
- /// x: vanilla's sky fog end; z: how overcast the sky is (rain: 0 clear, 1 vanilla's colours).
- sky_extra: [f32; 4],
- /// Vanilla's sunrise colour fan (rgb, alpha), taken out of the sky before the atmosphere is put in.
- sunrise: [f32; 4],
- /// x: the cloud slab's base height relative to the camera; y: its thickness; z: 1 when the
- /// volumetric clouds are drawn; w: seconds (the wind).
- clouds: [f32; 4],
- /// xy: the camera's x and z in the world modulo 65536; z: extra coverage (rain); w: 1 when the
- /// clouds' history is usable.
- cloud_cam: [f32; 4],
- /// xyz: the direction the clouds are lit from (the sun, or the moon at night).
- cloud_dir: [f32; 4],
- /// rgb: the colour of that light at the clouds; w: its strength.
- cloud_sun: [f32; 4],
- /// x: 1 when the volumetric fog is drawn; y: the camera's height in the world; z: the morning mist.
- vfog: [f32; 4],
-}
-
-/// What the world around looks like this frame (from the renderer's globals).
-#[derive(Clone, Copy, Debug)]
-pub struct HdScene {
- /// The shadow cascades' matrices (`cascades`; the far one may be a frame or two old).
- pub cascades: [Mat4; 2],
- /// The (jittered) projection the world was drawn with.
- pub view_proj: Mat4,
- /// Last frame's view-projection without jitter, and the camera's move since (TAA).
- pub prev_view_proj: Mat4,
- /// The same frame's matrix as it was drawn, jitter included. History kept on the jittered
- /// pixel grid (`fs_taa`, scale 1) is looked up with this; `fs_taau` resolves the history on the
- /// unjittered output grid and uses `prev_view_proj`.
- pub prev_view_proj_jittered: Mat4,
- 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,
- pub rain: f32,
- pub fog_color: [f32; 4],
- pub fog: [f32; 4],
- /// The sky has a sun at all (the Overworld).
- pub sky: bool,
- pub sky_color: [f32; 3],
- /// The camera's x and z modulo 256 (the waves' period), so the waves hold still in the world.
- pub camera_mod: [f32; 2],
- /// The water effect may run (not underwater, not disabled).
- pub water: bool,
- /// Vanilla's sunrise fan colour and its sky fog end (to subtract vanilla's sky from the world).
- pub sunrise: [f32; 4],
- pub sky_fog_end: f32,
- /// The volumetric clouds are drawn (vanilla's option is on, in the Overworld).
- pub clouds: bool,
- /// The camera's world position, x and z modulo 65536 (precision), and y; seconds of game time.
- pub camera_world: [f32; 3],
- pub seconds: f32,
-}
-
-/// The cascades' half sizes (blocks) and shadow map size.
-pub const CASCADES: [f32; 2] = [40.0, 176.0];
-pub const SHADOW_SIZE: u32 = 2048;
-
-const COMMON: &str = r#"
-struct HdFrame {
- inverse_view_proj: mat4x4<f32>,
- resolution: vec4<f32>,
- params: vec4<f32>,
- cascades: array<mat4x4<f32>, 2>,
- sun: vec4<f32>,
- sun_color: vec4<f32>,
- fog_color: vec4<f32>,
- fog: vec4<f32>,
- ambient: vec4<f32>,
- shadow: vec4<f32>,
- prev_view_proj: mat4x4<f32>,
- taa: vec4<f32>,
- taa_params: vec4<f32>,
- view_proj: mat4x4<f32>,
- water: vec4<f32>,
- sky_color: vec4<f32>,
- sky_sun: vec4<f32>,
- sky_extra: vec4<f32>,
- sunrise: vec4<f32>,
- clouds: vec4<f32>,
- cloud_cam: vec4<f32>,
- cloud_dir: vec4<f32>,
- cloud_sun: vec4<f32>,
- vfog: vec4<f32>,
-}
-struct FullOut {
- @builtin(position) position: vec4<f32>,
- @location(0) uv: vec2<f32>,
-}
-@vertex
-fn vs_full(@builtin(vertex_index) i: u32) -> FullOut {
- let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
- var out: FullOut;
- out.position = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0);
- out.uv = uv;
- return out;
-}
-"#;
-
-const LIGHT: &str = r#"
-struct Light {
- pos: vec4<f32>,
- color: vec4<f32>,
-}
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var scene_color: texture_2d<f32>;
-@group(0) @binding(2) var scene_depth: texture_depth_2d;
-@group(0) @binding(3) var albedo_tex: texture_2d<f32>;
-@group(0) @binding(4) var normal_tex: texture_2d<f32>;
-@group(0) @binding(5) var gbuffer_depth: texture_depth_2d;
-@group(0) @binding(6) var<storage, read> lights: array<Light>;
-@group(0) @binding(7) var light_tex: texture_2d<f32>;
-@group(0) @binding(8) var shadow_map: texture_depth_2d_array;
-@group(0) @binding(9) var shadow_sampler: sampler_comparison;
-@group(0) @binding(10) var lightmap: texture_2d<f32>;
-@group(0) @binding(11) var linear: sampler;
-// Per tile of 16x16 pixels: where its surface list and its halo list start in this array and
-// how long they are (4 words), then the lists of light indices. A length of 0xffffffff means
-// every light.
-@group(0) @binding(12) var<storage, read> tiles: array<u32>;
-// Ambient occlusion at half resolution: x the occlusion (1 open), y the distance it was taken at.
-@group(0) @binding(13) var ao_tex: texture_2d<f32>;
-// The water pass's depth: the nearest water surface (0 where there is none).
-@group(0) @binding(14) var water_depth: texture_depth_2d;
-//SKY15
-// The volumetric fog (half resolution: light in rgb, transmittance in a), the distance it was marched
-// to, the volumetric clouds (half resolution: light, transmittance) and the clouds' shadow map.
-@group(0) @binding(16) var fog_tex: texture_2d<f32>;
-@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>;
-const CLOUD_SHADOW_R: f32 = 1200.0;
-// The pixel being lit, for the shadow filter's noise.
-var<private> frag_px: vec2<f32>;
-
-// The occlusion at a full-resolution pixel `dist` away: the four nearest half-resolution texels
-// weighted by closeness and by how near their distance is, so it doesn't bleed across edges.
-fn ao_at(px: vec2<i32>, dist: f32) -> f32 {
- let size = vec2<i32>(textureDimensions(ao_tex));
- let f = vec2<f32>(px) * 0.5;
- let base = vec2<i32>(floor(f));
- let t = f - floor(f);
- var sum = 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(ao_tex, q, 0).xy;
- let bw = select(1.0 - t.x, t.x, i == 1) * select(1.0 - t.y, t.y, j == 1);
- let dz = (s.y - dist) / (0.03 * dist + 0.1);
- let w = select(0.0, bw / (1.0 + dz * dz * dz * dz), s.y > 0.0);
- sum += s.x * w;
- total += w;
- }
- }
- if total < 0.0001 {
- return textureLoad(ao_tex, clamp(px / 2, vec2<i32>(0), size - 1), 0).x;
- }
- return sum / total;
-}
-
-// Interleaved gradient noise: a different offset each pixel and frame, for the ray march.
-fn ign(px: vec2<f32>) -> f32 {
- let p = px + vec2<f32>(5.588238 * frame.params.w);
- return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
-}
-
-// Where a camera-relative point falls in cascade c: uv and depth.
-fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
- let s = frame.cascades[c] * vec4<f32>(p, 1.0);
- return vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
-}
-
-// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow; soft-edged.
-fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
- return shadow_taps(p, n, 1);
-}
-
-// A point of a disc of radius 1 (Vogel's spiral), turned by `angle`.
-fn vogel(i: i32, count: i32, angle: f32) -> vec2<f32> {
- let r = sqrt((f32(i) + 0.5) / f32(count));
- let a = f32(i) * 2.39996323 + angle;
- return vec2<f32>(cos(a), sin(a)) * r;
-}
-
-// The near cascade's soft shadow: a blocker search sets the penumbra's width (wider the farther
-// the shadow's caster is above the receiver), then a ring of taps turned by a per-pixel noise
-// filters it; the temporal pass averages the noise. `s`: the point in the cascade (uv, depth),
-// `texel`: the cascade's texel in blocks.
-fn soft_shadow(s: vec3<f32>, texel: f32) -> f32 {
- let angle = ign(frag_px) * 6.2831853;
- var blocker = 0.0;
- var found = 0.0;
- for (var i = 0; i < 8; i++) {
- let o = vogel(i, 8, angle) * (6.0 / 2048.0);
- let q = clamp(vec2<i32>((s.xy + o) * 2048.0), vec2<i32>(0), vec2<i32>(2047));
- let z = textureLoad(shadow_map, q, 0, 0);
- if z < s.z - 0.0006 {
- blocker += z;
- found += 1.0;
- }
- }
- if found < 0.5 { return 1.0; }
- // The caster's height above the receiver, in blocks (the cascade's depth spans 799 blocks).
- let height = (s.z - blocker / found) * 799.0;
- let radius = clamp(height * 0.18 / texel, 1.6, 8.0);
- var lit = 0.0;
- for (var i = 0; i < 12; i++) {
- let o = vogel(i, 12, angle + 1.7) * (radius / 2048.0);
- lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + o, 0, s.z - 0.0005 - 0.00005 * radius);
- }
- return lit / 12.0;
-}
-
-// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and
-// blurs it anyway).
-fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
- let dist = length(p);
- let c = select(1, 0, dist < frame.shadow.y * 0.92);
- let texel = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
- let q = p + n * texel * 1.5;
- let s = cascade_at(q, c);
- if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
- var lit = 0.0;
- let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
- if r == 1 && c == 0 {
- return mix(1.0, soft_shadow(s, texel), fade);
- }
- let step = 1.0 / 2048.0;
- for (var y = -r; y <= r; y++) {
- for (var x = -r; x <= r; x++) {
- lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
- }
- }
- return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
-}
-
-// One shadow sample, for the rays through the air.
-fn lit_at(p: vec3<f32>) -> f32 {
- let s = cascade_at(p, 1);
- if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
- return textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy, 1, s.z - 0.001);
-}
-
-fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
- if d <= start { return 0.0; }
- if d >= end { return 1.0; }
- return (d - start) / (end - start);
-}
-
-// The share of the sunlight the clouds let through at a camera-relative point: the shadow map
-// holds, for the sun's way down through the slab from a point of its base, the optical depth
-// (Photon's cloud shadow map, as a lookup rather than a pass).
-fn cloud_shadow(p: vec3<f32>) -> f32 {
- if frame.clouds.z < 0.5 { return 1.0; }
- let l = frame.cloud_dir.xyz;
- if l.y < 0.1 { return 1.0; }
- let t = (frame.clouds.x - p.y) / l.y;
- if t <= 0.0 { return 1.0; }
- let q = p.xz + l.xz * t;
- let uv = q / (2.0 * CLOUD_SHADOW_R) + vec2<f32>(0.5);
- if any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0)) { return 1.0; }
- let od = textureSampleLevel(cloud_shadow_tex, linear, uv, 0.0).r;
- // Fade out as the sun gets low (the shadow then falls far from the cloud).
- let fade = smoothstep(0.1, 0.25, l.y);
- return mix(1.0, mix(1.0, exp(-od * 0.9), 0.9), fade);
-}
-
-// The volumetric fog at a full-resolution pixel `dist` away: the four nearest half-resolution
-// texels weighted by closeness and by how near their march's length is (so it does not bleed over edges).
-fn fog_at(px: vec2<i32>, dist: f32) -> vec4<f32> {
- let size = vec2<i32>(textureDimensions(fog_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(fog_tex, q, 0);
- let sd = textureLoad(fog_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 * dist + 0.4);
- let w = bw / (1.0 + dz * dz * dz * dz);
- sum += s * w;
- total += w;
- }
- }
- if total < 0.0001 {
- return textureLoad(fog_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).
- @location(0) lit: vec4<f32>,
- // Water: the air between the camera and the surface (scattered light, transmittance);
- // in front of the water: the floor lit (rgb); else nothing.
- @location(1) air: vec4<f32>,
-}
-
-// An opaque surface lit: the ambient, the sun and the mods' lights over its albedo.
-fn shade_surface(px: vec2<i32>, p: vec3<f32>, water: bool) -> vec3<f32> {
- let sun = frame.sun.xyz;
- let sun_on = frame.sun.w;
- let atmosphere = frame.sky_sun.w > 0.5;
- let count = u32(frame.params.x);
- let tile = ((u32(px.y) >> 4u) * u32(frame.shadow.w) + (u32(px.x) >> 4u)) * 4u;
- let surface_start = tiles[tile];
- let surface_all = tiles[tile + 1u] == 0xffffffffu;
- let surface_count = select(tiles[tile + 1u], count, surface_all);
- let n = normalize(textureLoad(normal_tex, px, 0).xyz);
- let albedo = textureLoad(albedo_tex, px, 0);
- let lm = textureLoad(light_tex, px, 0).xy;
- // Vanilla's light (sky and blocks through the lightmap), shaded where the sun can't reach.
- let base = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(lm.y, lm.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
- let block = textureSampleLevel(lightmap, linear, vec2<f32>(lm.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
- let sky_reach = smoothstep(0.35, 0.95, lm.x);
- var s = 1.0;
- let ndl = dot(n, sun);
- if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow_taps(p, n, select(1, 0, water)) * cloud_shadow(p); } else { s = 0.0; }
- // Shadowed sky light (ambient) keeps vanilla's look, blocks' light stays as is; the sun adds
- // direct light by the face's angle.
- let direct = frame.sun_color.rgb * sun_on * max(ndl, 0.0) * s * sky_reach;
- // Contact shadows from the ambient occlusion: on the ambient and blocks' light fully, on
- // the mods' lights by half, not on the sun or on emission.
- var occlusion = 1.0;
- // (The water dims and blurs the floor under it: no occlusion there.)
- if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); }
- let ao_ambient = mix(1.0, occlusion, frame.ambient.w);
- let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5);
- // The sky's light is bluish from above and carries the horizon's colour from the sides;
- // faces turned up take more of it than the ones turned down.
- var sky_tint = vec3<f32>(1.0);
- if atmosphere {
- let reach = smoothstep(0.25, 0.9, lm.x);
- let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
- let shape = mix(0.66, 1.1, smoothstep(0.1, 1.0, up));
- let hue = mix(vec3<f32>(1.0), sky_light_tint(n), 0.4 * (1.0 - 0.5 * frame.sun_color.a));
- sky_tint = mix(vec3<f32>(1.0), hue * shape, reach);
- }
- var light = max(base * sky_tint * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
- // The mods' lights.
- for (var k = 0u; k < surface_count; k++) {
- let l = lights[select(tiles[surface_start + k], k, surface_all)];
- let to = l.pos.xyz - p;
- let dist = length(to);
- if dist >= l.pos.w { continue; }
- let window = pow(clamp(1.0 - pow(dist / l.pos.w, 4.0), 0.0, 1.0), 2.0);
- let falloff = l.color.w / (dist * dist + 1.0) * window;
- let k = max(dot(n, to / max(dist, 0.001)) * 0.85 + 0.15, 0.0);
- light += l.color.rgb * falloff * k * ao_lights;
- }
- return albedo.rgb * light + albedo.rgb * albedo.a * 1.5;
-}
-
-@fragment
-fn fs_light(in: FullOut) -> LightOut {
- let px = vec2<i32>(in.position.xy);
- let scene = textureLoad(scene_color, px, 0).rgb;
- let d = textureLoad(scene_depth, px, 0);
- let g = textureLoad(gbuffer_depth, px, 0);
- // Water over the opaque world (or over nothing): the water composite lays the surface over
- // the floor, which is lit here with no air. Something translucent in front of the water
- // (`front`: nearer than its surface) keeps the scene as drawn; the floor goes beside it.
- var wd = 0.0;
- var water_here = false;
- if frame.water.x > 0.5 {
- wd = textureLoad(water_depth, px, 0);
- water_here = wd > 0.0 && (g <= 0.0 || wd >= g);
- }
- let front = water_here && g > 0.0 && d - wd > d * 0.002;
- let water = water_here && !front;
- let has_floor = water && g > 0.0;
- let dd = select(d, g, has_floor);
- let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(dd, 0.000001), 1.0);
- let h = frame.inverse_view_proj * ndc;
- let p = h.xyz / h.w;
- let view = normalize(p);
- // Where the air ends: the water's surface over water, else the pixel itself.
- var ps = p;
- if water {
- let hf = frame.inverse_view_proj * vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(wd, 0.000001), 1.0);
- ps = hf.xyz / hf.w;
- }
- let view_s = normalize(ps);
- let sun = frame.sun.xyz;
- let sun_on = frame.sun.w;
- let count = u32(frame.params.x);
- let tile = ((u32(px.y) >> 4u) * u32(frame.shadow.w) + (u32(px.x) >> 4u)) * 4u;
- let halo_start = tiles[tile + 2u];
- let halo_all = tiles[tile + 3u] == 0xffffffffu;
- let halo_count = select(tiles[tile + 3u], count, halo_all);
- frag_px = in.position.xy;
- let atmosphere = frame.sky_sun.w > 0.5;
- var c = scene;
- // The sky: the atmosphere's, with what the world drew over vanilla's plain sky (sun, moon,
- // stars, clouds) kept on top.
- if d <= 0.0 && atmosphere {
- var kept = max(scene - vanilla_sky(view), vec3<f32>(0.0));
- var sky = atmosphere_sky(view);
- // The sun: vanilla's square sprite is taken out and a round disc with a soft haze round
- // it (warm, strongest low) is drawn in its place.
- let toward = dot(view, frame.sky_sun.xyz);
- kept *= 1.0 - smoothstep(0.965, 0.975, toward);
- let clear = 1.0 - frame.sky_extra.z;
- let shine = smoothstep(-0.04, 0.02, frame.sky_sun.y) * clear;
- let glow = max(toward, 0.0);
- let angle = acos(clamp(toward, -1.0, 1.0));
- sky += frame.sun_color.rgb * shine * (pow(glow, 24.0) * 0.07 + pow(glow, 400.0) * 0.4 + 6.0 * (1.0 - smoothstep(0.016, 0.022, angle)));
- 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);
- c = c * cl.a + cl.rgb;
- }
- }
- // (Vanilla's clouds, when the volumetric ones are off, take the low sun's colour in their own
- // pass: `vanilla_cloud_tint`, applied to their colour on the CPU.)
- let surface = has_floor || (!water_here && d > 0.0 && abs(d - g) <= max(d, g) * 0.002);
- if surface {
- c = shade_surface(px, p, water);
- // Vanilla's fog over it again (the water composite does it at the surface).
- if !water {
- let sph = length(p);
- let cyl = max(length(p.xz), abs(p.y));
- let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
- let haze = haze_colour(view);
- c = mix(c, haze, f * frame.fog_color.a);
- // Aerial perspective: far things take the horizon's colour a little before the fog does.
- if atmosphere && frame.fog.x > 1.0e6 {
- c = mix(c, haze, (1.0 - exp(-sph / 700.0)) * 0.5);
- }
- }
- }
- // What lies behind a translucent thing in front of the water: the floor, lit.
- var floor_lit = vec3<f32>(0.0);
- if front {
- let hg = frame.inverse_view_proj * vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(g, 0.000001), 1.0);
- floor_lit = shade_surface(px, hg.xyz / hg.w, true);
- }
- // The air between the camera and the pixel (the water's surface over water): the volumetric
- // fog (what it hides, what it scatters), the sun's rays between the shadows, and the halos
- // round the mods' lights. Over water they go to the composite, which lays them over the
- // finished water; elsewhere over the pixel here.
- var air_t = 1.0;
- var air_s = vec3<f32>(0.0);
- let far = select(min(length(ps), frame.shadow.z), frame.shadow.z, d <= 0.0);
- let density = frame.ambient.z;
- let volumetric = frame.vfog.x > 0.5 && atmosphere;
- if volumetric {
- var fog_len = 192.0;
- if d > 0.0 { fog_len = min(length(ps), 192.0); }
- let fg = fog_at(px, fog_len);
- air_t = fg.a;
- air_s = fg.rgb;
- }
- if !volumetric && sun_on > 0.0 && density > 0.0 {
- let steps = 14;
- let jitter = ign(in.position.xy);
- var acc = 0.0;
- for (var i = 0; i < steps; i++) {
- let t = (f32(i) + jitter) / f32(steps) * far;
- acc += lit_at(view_s * t);
- }
- // Henyey-Greenstein, forward scattering: brightest looking towards the sun.
- let cos_t = dot(view_s, sun);
- let gk = 0.6;
- let phase = (1.0 - gk * gk) / pow(1.0 + gk * gk - 2.0 * gk * cos_t, 1.5) * 0.08;
- air_s += frame.sun_color.rgb * sun_on * (acc / f32(steps)) * phase * (1.0 - exp(-far * density));
- }
- for (var k = 0u; k < halo_count; k++) {
- let l = lights[select(tiles[halo_start + k], k, halo_all)];
- // Nearest point of the view ray to the light, and the glow along the ray in front of it.
- let t0 = dot(l.pos.xyz, view_s);
- let hh = length(l.pos.xyz - view_s * t0) + 0.5;
- if hh > l.pos.w * 0.5 { continue; }
- let a0 = atan((0.0 - t0) / hh);
- let a1 = atan((far - t0) / hh);
- // Thicker air glows more round a light, but only so far: the mist at dusk and many fires
- // at once would otherwise wash the picture out.
- air_s += l.color.rgb * l.color.w * (a1 - a0) / hh * 0.004 * (0.3 + min(density * 300.0, 0.9));
- }
- var out: LightOut;
- if water {
- out.lit = vec4<f32>(c, 0.5);
- out.air = vec4<f32>(air_s, air_t);
- } else {
- out.lit = vec4<f32>(c * air_t + air_s, select(1.0, 0.6, front));
- out.air = vec4<f32>(floor_lit, 0.0);
- }
- return out;
-}
-"#;
-
-const WATER: &str = r#"
-const PI: f32 = 3.14159265;
-const REFLECT_STEPS: i32 = 24;
-const REFLECT_RANGE: f32 = 96.0;
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var lit_tex: texture_2d<f32>;
-@group(0) @binding(2) var gbuffer_depth: texture_depth_2d;
-@group(0) @binding(3) var water_tint: texture_2d<f32>;
-@group(0) @binding(4) var water_normal: texture_2d<f32>;
-@group(0) @binding(5) var water_depth: texture_depth_2d;
-@group(0) @binding(6) var shadow_map: texture_depth_2d_array;
-@group(0) @binding(7) var shadow_sampler: sampler_comparison;
-@group(0) @binding(8) var lightmap: texture_2d<f32>;
-@group(0) @binding(9) var linear: sampler;
-//SKY10
-// What the lighting left beside the lit world: over water the air in front of the surface (scattered
-// light, transmittance), in front of the water the lit floor.
-@group(0) @binding(11) var air_tex: texture_2d<f32>;
-
-fn ign(px: vec2<f32>) -> f32 {
- let p = px + vec2<f32>(5.588238 * frame.params.w);
- return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
-}
-
-// The camera-relative position of the screen point `uv` at depth z.
-fn position_at(uv: vec2<f32>, z: f32) -> vec3<f32> {
- let h = frame.inverse_view_proj * vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
- return h.xyz / h.w;
-}
-
-// The screen point (uv, clip w) of a camera-relative position.
-fn project(p: vec3<f32>) -> vec3<f32> {
- let c = frame.view_proj * vec4<f32>(p, 1.0);
- return vec3<f32>(c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5, c.w);
-}
-
-fn on_screen(uv: vec2<f32>) -> bool {
- return all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0));
-}
-
-fn texel(uv: vec2<f32>) -> vec2<i32> {
- return clamp(vec2<i32>(uv * frame.resolution.xy), vec2<i32>(0), vec2<i32>(frame.resolution.xy) - 1);
-}
-
-// Sine waves in world space: wave vectors that are whole multiples of 2 pi / 256 and speeds that
-// are whole cycles per 600 s, so the sum repeats every 256 blocks and 10 minutes and holds still
-// in the world as the camera moves. x, y: the vector in units of 2 pi / 256; z: the slope the
-// wave adds; w: its phase.
-const WAVES = array<vec4<f32>, 26>(
- vec4<f32>(3.0, 1.0, 0.040, 0.3),
- vec4<f32>(-2.0, 5.0, 0.045, 2.1),
- vec4<f32>(7.0, 2.0, 0.040, 4.4),
- vec4<f32>(-5.0, -9.0, 0.040, 1.2),
- vec4<f32>(11.0, -4.0, 0.035, 5.6),
- vec4<f32>(13.0, 10.0, 0.035, 3.3),
- vec4<f32>(-17.0, 7.0, 0.030, 0.9),
- vec4<f32>(9.0, 23.0, 0.030, 2.7),
- vec4<f32>(-27.0, 19.0, 0.025, 4.9),
- vec4<f32>(31.0, 30.0, 0.025, 1.8),
- // Fine ripples (wavelengths of 0.7 to 3 blocks) for the many small sun sparkles: slopes small,
- // faded out by distance and pixel size like the rest.
- vec4<f32>(83.0, 41.0, 0.042, 0.4),
- vec4<f32>(-97.0, 59.0, 0.042, 3.1),
- vec4<f32>(71.0, -113.0, 0.042, 5.0),
- vec4<f32>(131.0, 89.0, 0.040, 1.5),
- vec4<f32>(-157.0, -103.0, 0.040, 4.2),
- vec4<f32>(191.0, -67.0, 0.038, 2.4),
- vec4<f32>(-223.0, 149.0, 0.038, 0.8),
- vec4<f32>(101.0, 241.0, 0.036, 3.7),
- vec4<f32>(-263.0, -181.0, 0.034, 5.5),
- vec4<f32>(311.0, 127.0, 0.034, 2.0),
- // Finer still (0.35 to 0.6 blocks): the small sparkles.
- vec4<f32>(397.0, -263.0, 0.050, 1.1),
- vec4<f32>(-467.0, 331.0, 0.050, 4.6),
- vec4<f32>(521.0, 449.0, 0.045, 3.0),
- vec4<f32>(-613.0, -199.0, 0.045, 5.9),
- vec4<f32>(293.0, 577.0, 0.045, 0.2),
- vec4<f32>(-701.0, 263.0, 0.040, 2.8),
-);
-
-// The gradient of the water's height at world x, z: the waves that a pixel `footprint` blocks
-// wide can still resolve (finer ones fade out, or they would shimmer).
-fn wave_gradient(xz: vec2<f32>, footprint: f32) -> vec2<f32> {
- var grad = vec2<f32>(0.0);
- for (var i = 0; i < 26; i++) {
- let w = WAVES[i];
- let k = w.xy * (2.0 * PI / 256.0);
- let len = length(k);
- let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.2 - 0.6, 0.0, 1.0);
- let cycles = round(sqrt(9.8 * len) * 0.5 * 600.0 / (2.0 * PI));
- let omega = cycles * 2.0 * PI / 600.0;
- let phase = dot(k, xz) - omega * frame.water.y + w.w;
- grad += (k / len) * (w.z * fade * cos(phase));
- }
- // A little higher than the table's slopes.
- return grad * 1.25;
-}
-
-fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
- let s = frame.cascades[c] * vec4<f32>(p, 1.0);
- return vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
-}
-
-// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow (as the lighting pass has it).
-fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
- let dist = length(p);
- let c = select(1, 0, dist < frame.shadow.y * 0.92);
- let texel_size = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
- let q = p + n * texel_size * 1.5;
- let s = cascade_at(q, c);
- if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
- var lit = 0.0;
- let step = 1.0 / 2048.0;
- for (var y = -1; y <= 1; y++) {
- for (var x = -1; x <= 1; x++) {
- lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
- }
- }
- let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
- return mix(1.0, lit / 9.0, fade);
-}
-
-fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
- if d <= start { return 0.0; }
- if d >= end { return 1.0; }
- return (d - start) / (end - start);
-}
-
-// What the sky shows along r when the ray leaves the screen: the horizon's colour rising to
-// the zenith's, darker where the water lies under cover (`vis` is the sky light there).
-fn sky_along(r: vec3<f32>, vis: f32) -> vec3<f32> {
- var c = frame.fog_color.rgb;
- if frame.sky_sun.w > 0.5 {
- c = atmosphere_sky(r);
- } else if frame.sky_color.w > 0.5 {
- c = mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, r.y));
- }
- return c * mix(0.12, 1.0, vis);
-}
-
-// Marches the ray from p along r over the depth buffer (28 steps, growing, jittered so TAA
-// averages the placement; then a binary search for the crossing). xyz: the colour found,
-// w: how much to trust it (0 for a miss, less near the screen's edge).
-fn reflection(p: vec3<f32>, r: vec3<f32>, jitter: f32) -> vec4<f32> {
- var near = 0.0;
- for (var i = 0; i < REFLECT_STEPS; i++) {
- let u = (f32(i) + jitter) / f32(REFLECT_STEPS);
- let s = 0.12 + REFLECT_RANGE * u * u;
- let q = p + r * s;
- let sp = project(q);
- if sp.z <= 0.0 || !on_screen(sp.xy) { break; }
- let z = textureLoad(gbuffer_depth, texel(sp.xy), 0);
- if z > 0.0 {
- let c = frame.view_proj * vec4<f32>(q, 1.0);
- // Reversed Z: a larger depth is nearer; the ray is behind the surface when the
- // surface is nearer than it.
- if z > c.z / c.w {
- let behind = length(q) - length(position_at(sp.xy, z));
- if behind < 0.4 + 0.08 * s {
- var lo = near;
- var hi = s;
- for (var k = 0; k < 5; k++) {
- let m = 0.5 * (lo + hi);
- let mq = p + r * m;
- let mp = project(mq);
- let mc = frame.view_proj * vec4<f32>(mq, 1.0);
- let mz = textureLoad(gbuffer_depth, texel(mp.xy), 0);
- if mz > mc.z / mc.w { hi = m; } else { lo = m; }
- }
- let hp = project(p + r * hi);
- let edge = min(min(hp.x, 1.0 - hp.x), min(hp.y, 1.0 - hp.y));
- let colour = textureSampleLevel(lit_tex, linear, hp.xy, 0.0).rgb;
- return vec4<f32>(colour, smoothstep(0.0, 0.08, edge));
- }
- }
- }
- near = s;
- }
- return vec4<f32>(0.0);
-}
-
-// The sun's glint on the waves (GGX, a rough lobe so it glitters instead of aliasing).
-fn glint(n: vec3<f32>, v: vec3<f32>, l: vec3<f32>) -> f32 {
- let h = normalize(l + v);
- let a = 0.07;
- let nh = max(dot(n, h), 0.0);
- let d = a * a / (PI * pow(nh * nh * (a * a - 1.0) + 1.0, 2.0));
- let f = 0.02 + 0.98 * pow(1.0 - max(dot(v, h), 0.0), 5.0);
- return min(d * f * 0.25 * max(dot(n, l), 0.0) / max(dot(n, v), 0.1), 12.0);
-}
-
-// The water over the lit world: the world behind it (refracted by the waves, dimmed and tinted
-// by the water's depth), the sky and the world above it (a screen-space reflection) by Fresnel,
-// and the sun's glint. Pixels without water pass through.
-@fragment
-fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
- let px = vec2<i32>(in.position.xy);
- let base = textureLoad(lit_tex, px, 0);
- if base.a > 0.75 { return vec4<f32>(base.rgb, 1.0); }
- // Alpha 0.6: something translucent stands in front of the water here (base is the scene).
- let front = base.a > 0.55;
- let aux = textureLoad(air_tex, px, 0);
- let wd = textureLoad(water_depth, px, 0);
- let g = textureLoad(gbuffer_depth, px, 0);
- let tint = textureLoad(water_tint, px, 0);
- let nt = textureLoad(water_normal, px, 0);
- let p = position_at(in.uv, wd);
- let dist = length(p);
- let v = -p / dist;
- let geometric = normalize(nt.xyz * 2.0 - 1.0);
- // The waves tilt the flat parts of the surface only.
- let footprint = dist * frame.resolution.w * 1.4;
- let grad = wave_gradient(p.xz + frame.water.zw, footprint);
- let flat_part = smoothstep(0.6, 0.95, geometric.y);
- var n = normalize(geometric + vec3<f32>(-grad.x, 0.0, -grad.y) * flat_part);
- n = normalize(n + v * max(0.03 - dot(n, v), 0.0));
- let ndv = max(dot(n, v), 0.02);
-
- // Light on the water: vanilla's lightmap at the surface, and the sun where it reaches.
- let levels = vec2<f32>(nt.a, tint.a);
- let sky_reach = smoothstep(0.35, 0.95, levels.x);
- var ambient = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
- if frame.sky_sun.w > 0.5 {
- ambient *= mix(vec3<f32>(1.0), sky_light_tint(vec3<f32>(0.0, 1.0, 0.0)), 0.7 * sky_reach);
- }
- let block = textureSampleLevel(lightmap, linear, vec2<f32>(levels.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
- let sun = frame.sun.xyz;
- let sun_on = frame.sun.w;
- var sunlit = 0.0;
- if sun_on > 0.0 && sun.y > 0.0 && sky_reach > 0.0 { sunlit = shadow(p, vec3<f32>(0.0, 1.0, 0.0)) * sky_reach; }
- let volume_light = max(ambient * frame.ambient.x, block * frame.ambient.y) + frame.sun_color.rgb * sun_on * sunlit * 0.8;
-
- // Refraction: where the ray through the waves meets the world, against where the flat surface
- // would send it (so still water looks as vanilla's and only the waves distort). Not where
- // something translucent is in front of the water, nor onto texels that hold such a thing.
- let has_floor = g > 0.0;
- var uv_floor = in.uv;
- var floor_z = g;
- if has_floor && !front {
- let floor0 = position_at(in.uv, g);
- let depth0 = max(length(floor0) - dist, 0.0);
- let bent = refract(-v, n, 0.75) - refract(-v, geometric, 0.75);
- let moved = project(floor0 + bent * min(depth0, 24.0));
- if moved.z > 0.0 && on_screen(moved.xy) {
- let z = textureLoad(gbuffer_depth, texel(moved.xy), 0);
- // The four texels the filter blends: none may be a pixel with a translucent thing
- // in front of the water.
- let marks = textureGather(3, lit_tex, linear, moved.xy);
- let tainted = any((marks > vec4<f32>(0.55)) & (marks < vec4<f32>(0.75)));
- // Only what lies behind the water: not the bank or an entity in front of it.
- if z > 0.0 && z <= wd && !tainted {
- uv_floor = moved.xy;
- floor_z = z;
- }
- }
- }
- var thickness = 48.0;
- if has_floor {
- thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0);
- }
- var behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb;
- if front { behind = aux.rgb; }
- // A deep, dark water: the biome's colour darkened and a little greyed, so it reads as
- // depth rather than as bright paint.
- let biome = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
- let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.85) * vec3<f32>(0.78, 0.86, 0.95);
- // The colours the water takes up fastest are the ones it has the least of.
- let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.6 * (thickness + 0.3));
- let refracted = behind * absorb + colour * volume_light * mix(0.65, 0.3, frame.sun_color.a) * (vec3<f32>(1.0) - absorb);
-
- // Reflection: the screen where the ray stays on it, the sky where it leaves it or misses.
- var r = reflect(-v, n);
- r.y = abs(r.y);
- // Looking down onto it the reflection is a few percent of the picture: the sky alone does.
- let fresnel = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);
- var hit = vec4<f32>(0.0);
- if fresnel > 0.04 { hit = reflection(p, r, ign(in.position.xy)); }
- let sky_vis = mix(0.15, 1.0, sky_reach);
- let reflected = mix(sky_along(r, sky_vis), hit.rgb, hit.w);
-
- var c = refracted * (1.0 - fresnel) + reflected * fresnel;
- if sun_on > 0.0 && sunlit > 0.0 {
- c += frame.sun_color.rgb * sun_on * sunlit * glint(n, v, sun);
- }
- // Vanilla's fog over it, at the surface.
- let sph = dist;
- let cyl = max(length(p.xz), abs(p.y));
- let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
- let haze = haze_colour(-v);
- c = mix(c, haze, f * frame.fog_color.a);
- if frame.sky_sun.w > 0.5 && frame.fog.x > 1.0e6 {
- c = mix(c, haze, (1.0 - exp(-dist / 700.0)) * 0.5);
- }
- if front {
- // The scene as drawn, with the water's change to the floor laid under the translucent thing.
- c = base.rgb + (c - aux.rgb) * 0.7;
- } else {
- // The air between the camera and the surface: volumetric fog, rays, halos.
- c = c * aux.a + aux.rgb;
- }
- return vec4<f32>(max(c, vec3<f32>(0.0)), 1.0);
-}
-"#;
-
-const AO: &str = r#"
-const AO_RADIUS: f32 = 1.8;
-const AO_SLICES: i32 = 3;
-const AO_STEPS: i32 = 5;
-const PI: f32 = 3.14159265;
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var depth_tex: texture_depth_2d;
-@group(0) @binding(2) var normal_tex: texture_2d<f32>;
-
-// Interleaved gradient noise, shifted per frame (params.w) and per use.
-fn noise(px: vec2<f32>, shift: vec2<f32>) -> f32 {
- let p = px + shift + vec2<f32>(5.588238 * frame.params.w);
- return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
-}
-
-// The camera-relative position of a (continuous) full-resolution pixel at depth z.
-fn position_at(px: vec2<f32>, z: f32) -> vec3<f32> {
- let uv = px * frame.resolution.zw;
- let h = frame.inverse_view_proj * vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
- return h.xyz / h.w;
-}
-
-// Ground-truth ambient occlusion (horizon based): per slice through the view ray, the highest
-// horizon on either side, integrated against the normal's cosine lobe. Half resolution: one
-// pixel of each 2x2.
-@fragment
-fn fs_ao(in: FullOut) -> @location(0) vec2<f32> {
- let size = vec2<i32>(textureDimensions(depth_tex));
- let ic = min(vec2<i32>(in.position.xy) * 2, size - 1);
- let z = textureLoad(depth_tex, ic, 0);
- if z <= 0.0 { return vec2<f32>(1.0, 0.0); }
- let nrm = textureLoad(normal_tex, ic, 0).xyz;
- let c = vec2<f32>(ic) + 0.5;
- let p = position_at(c, z);
- let dist = length(p);
- if dot(nrm, nrm) < 0.01 { return vec2<f32>(1.0, dist); }
- let n = normalize(nrm);
- let v = -p / dist;
- // Blocks per pixel here (the plane of equal depth is square to the view), so how far in
- // pixels the radius reaches.
- let per_px = max(length(position_at(c + vec2<f32>(4.0, 0.0), z) - p) / 4.0, 0.00001);
- let reach = min(AO_RADIUS / per_px, 56.0);
- if reach < 2.0 { return vec2<f32>(1.0, dist); }
- let radius = reach * per_px;
- let rot = noise(in.position.xy, vec2<f32>(0.0));
- let jitter = noise(in.position.xy, vec2<f32>(37.0, 17.0));
- var visibility = 0.0;
- for (var s = 0; s < AO_SLICES; s++) {
- let phi = (f32(s) + rot) / f32(AO_SLICES) * PI;
- let d2 = vec2<f32>(cos(phi), sin(phi));
- // The slice's direction in space, square to the view ray, and its plane's axis.
- let q = position_at(c + d2 * 4.0, z) - p;
- let dir = normalize(q - v * dot(q, v));
- let axis = cross(dir, v);
- let projected = n - axis * dot(n, axis);
- let plen = length(projected);
- if plen < 0.0001 { visibility += 1.0; continue; }
- let cos_n = clamp(dot(projected, v) / plen, -1.0, 1.0);
- let nang = select(-1.0, 1.0, dot(projected, dir) >= 0.0) * acos(cos_n);
- var c_neg = -1.0;
- var c_pos = -1.0;
- for (var i = 0; i < AO_STEPS; i++) {
- let t = (f32(i) + jitter) / f32(AO_STEPS);
- let r = 1.5 + (reach - 1.5) * t * t;
- for (var side = 0; side < 2; side++) {
- let sg = select(-1.0, 1.0, side == 1);
- let ip = vec2<i32>(floor(c + d2 * (r * sg)));
- if any(ip < vec2<i32>(0)) || any(ip >= size) { continue; }
- let sz = textureLoad(depth_tex, ip, 0);
- if sz <= 0.0 { continue; }
- let to = position_at(vec2<f32>(ip) + 0.5, sz) - p;
- let len = length(to);
- // Points on (or under) the surface's own plane don't occlude it.
- if dot(to, n) < 0.1 * len { continue; }
- // Far points count less, to nothing at the radius.
- let fall = clamp((len - radius * 0.6) / (radius * 0.4), 0.0, 1.0);
- let ch = mix(dot(to / len, v), -1.0, fall);
- if side == 1 { c_pos = max(c_pos, ch); } else { c_neg = max(c_neg, ch); }
- }
- }
- var h_neg = -acos(clamp(c_neg, -1.0, 1.0));
- var h_pos = acos(clamp(c_pos, -1.0, 1.0));
- h_neg = nang + max(h_neg - nang, -0.5 * PI);
- h_pos = nang + min(h_pos - nang, 0.5 * PI);
- let sn = sin(nang);
- let arc_neg = cos_n + 2.0 * h_neg * sn - cos(2.0 * h_neg - nang);
- let arc_pos = cos_n + 2.0 * h_pos * sn - cos(2.0 * h_pos - nang);
- visibility += plen * 0.25 * (arc_neg + arc_pos);
- }
- let ao = clamp(visibility / f32(AO_SLICES), 0.0, 1.0);
- // Far away it is too coarse to show: fade out.
- return vec2<f32>(mix(1.0, ao, 1.0 - smoothstep(40.0, 96.0, dist)), dist);
-}
-"#;
-
-const AO_BLUR: &str = r#"
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var raw_ao: texture_2d<f32>;
-
-// A 5x5 blur that only takes texels at about the same distance (so not across an edge).
-@fragment
-fn fs_ao_blur(in: FullOut) -> @location(0) vec2<f32> {
- let size = vec2<i32>(textureDimensions(raw_ao));
- let ip = vec2<i32>(in.position.xy);
- let centre = textureLoad(raw_ao, ip, 0).xy;
- if centre.y <= 0.0 { return centre; }
- let tolerance = 0.03 * centre.y + 0.1;
- var sum = 0.0;
- var total = 0.0;
- for (var y = -2; y <= 2; y++) {
- for (var x = -2; x <= 2; x++) {
- let q = ip + vec2<i32>(x, y);
- if any(q < vec2<i32>(0)) || any(q >= size) { continue; }
- let s = textureLoad(raw_ao, q, 0).xy;
- if s.y <= 0.0 { continue; }
- let dz = (s.y - centre.y) / tolerance;
- let w = 1.0 / (1.0 + dz * dz * dz * dz);
- sum += s.x * w;
- total += w;
- }
- }
- return vec2<f32>(sum / max(total, 0.0001), centre.y);
-}
-"#;
-
-const BLOOM: &str = r#"
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var source: texture_2d<f32>;
-@group(0) @binding(2) var linear: sampler;
-
-// The first step down keeps only what is brighter than white (a soft knee).
-@fragment
-fn fs_prefilter(in: FullOut) -> @location(0) vec4<f32> {
- let c = textureSampleLevel(source, linear, in.uv, 0.0).rgb;
- let peak = max(c.r, max(c.g, c.b));
- let knee = 0.35;
- let soft = clamp(peak - 0.85 + knee, 0.0, 2.0 * knee);
- let weight = max(soft * soft / (4.0 * knee + 0.0001), peak - 0.85) / max(peak, 0.0001);
- return vec4<f32>(c * weight, 1.0);
-}
-// 13 taps (Call of Duty: Advanced Warfare's bloom), half size each step.
-@fragment
-fn fs_down(in: FullOut) -> @location(0) vec4<f32> {
- let t = 1.0 / vec2<f32>(textureDimensions(source));
- let s = in.uv;
- let a = textureSampleLevel(source, linear, s + t * vec2(-2.0, -2.0), 0.0).rgb;
- let b = textureSampleLevel(source, linear, s + t * vec2(0.0, -2.0), 0.0).rgb;
- let c = textureSampleLevel(source, linear, s + t * vec2(2.0, -2.0), 0.0).rgb;
- let d = textureSampleLevel(source, linear, s + t * vec2(-2.0, 0.0), 0.0).rgb;
- let e = textureSampleLevel(source, linear, s, 0.0).rgb;
- let f = textureSampleLevel(source, linear, s + t * vec2(2.0, 0.0), 0.0).rgb;
- let g = textureSampleLevel(source, linear, s + t * vec2(-2.0, 2.0), 0.0).rgb;
- let h = textureSampleLevel(source, linear, s + t * vec2(0.0, 2.0), 0.0).rgb;
- let i = textureSampleLevel(source, linear, s + t * vec2(2.0, 2.0), 0.0).rgb;
- let j = textureSampleLevel(source, linear, s + t * vec2(-1.0, -1.0), 0.0).rgb;
- let k = textureSampleLevel(source, linear, s + t * vec2(1.0, -1.0), 0.0).rgb;
- let l = textureSampleLevel(source, linear, s + t * vec2(-1.0, 1.0), 0.0).rgb;
- let m = textureSampleLevel(source, linear, s + t * vec2(1.0, 1.0), 0.0).rgb;
- var o = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
- return vec4<f32>(o, 1.0);
-}
-// A 3×3 tent, added onto the bigger level.
-@fragment
-fn fs_up(in: FullOut) -> @location(0) vec4<f32> {
- let t = 1.0 / vec2<f32>(textureDimensions(source));
- let s = in.uv;
- var o = textureSampleLevel(source, linear, s, 0.0).rgb * 4.0;
- o += (textureSampleLevel(source, linear, s + t * vec2(-1.0, 0.0), 0.0).rgb
- + textureSampleLevel(source, linear, s + t * vec2(1.0, 0.0), 0.0).rgb
- + textureSampleLevel(source, linear, s + t * vec2(0.0, -1.0), 0.0).rgb
- + textureSampleLevel(source, linear, s + t * vec2(0.0, 1.0), 0.0).rgb) * 2.0;
- o += textureSampleLevel(source, linear, s + t * vec2(-1.0, -1.0), 0.0).rgb
- + textureSampleLevel(source, linear, s + t * vec2(1.0, -1.0), 0.0).rgb
- + textureSampleLevel(source, linear, s + t * vec2(-1.0, 1.0), 0.0).rgb
- + textureSampleLevel(source, linear, s + t * vec2(1.0, 1.0), 0.0).rgb;
- return vec4<f32>(o / 16.0, 1.0);
-}
-"#;
-
-const TONEMAP: &str = r#"
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var hdr: texture_2d<f32>;
-@group(0) @binding(2) var bloom: texture_2d<f32>;
-@group(0) @binding(3) var linear: sampler;
-
-@fragment
-fn fs_tonemap(in: FullOut) -> @location(0) vec4<f32> {
- var c = textureSampleLevel(hdr, linear, in.uv, 0.0).rgb;
- // A slight sharpen after TAA (its history resampling softens): the centre against its four
- // neighbours, kept within their range so it can't ring.
- let k = frame.taa_params.y;
- if k > 0.0 {
- let t = 1.0 / vec2<f32>(textureDimensions(hdr));
- let n0 = textureSampleLevel(hdr, linear, in.uv + vec2<f32>(t.x, 0.0), 0.0).rgb;
- let n1 = textureSampleLevel(hdr, linear, in.uv - vec2<f32>(t.x, 0.0), 0.0).rgb;
- let n2 = textureSampleLevel(hdr, linear, in.uv + vec2<f32>(0.0, t.y), 0.0).rgb;
- let n3 = textureSampleLevel(hdr, linear, in.uv - vec2<f32>(0.0, t.y), 0.0).rgb;
- let lo = min(min(n0, n1), min(min(n2, n3), c));
- let hi = max(max(n0, n1), max(max(n2, n3), c));
- c = clamp(c * (1.0 + 4.0 * k) - (n0 + n1 + n2 + n3) * k, lo, hi);
- }
- c = c * frame.params.y;
- c += textureSampleLevel(bloom, linear, in.uv, 0.0).rgb * frame.params.z;
- // Up to 0.8 as it is (vanilla's look); above it, rolled off towards white by the
- // brightest channel, so hues hold instead of clipping.
- let peak = max(c.r, max(c.g, c.b));
- let t = 0.8;
- if peak > t {
- let x = (peak - t) / (1.0 - t);
- let rolled = t + (1.0 - t) * (x / (1.0 + x));
- c = c * (rolled / peak);
- // Very bright light bleaches towards white, as film does.
- c = mix(c, vec3<f32>(rolled), clamp((peak - 2.0) / 8.0, 0.0, 0.6));
- }
- // A light grade: a touch more colour, and a gentle S round the middle grey.
- let luma = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
- c = max(mix(vec3<f32>(luma), c, 1.12), vec3<f32>(0.0));
- c = mix(c, c * c * (3.0 - 2.0 * c), 0.18);
- return vec4<f32>(c, 1.0);
-}
-"#;
-
-const TAA: &str = r#"
-struct TaaOut {
- @location(0) color: vec4<f32>,
- @location(1) history: vec4<f32>,
-}
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var source: texture_2d<f32>;
-@group(0) @binding(2) var depth_tex: texture_depth_2d;
-@group(0) @binding(3) var history: texture_2d<f32>;
-@group(0) @binding(4) var linear: sampler;
-
-fn to_ycocg(c: vec3<f32>) -> vec3<f32> {
- return vec3<f32>(c.r * 0.25 + c.g * 0.5 + c.b * 0.25, c.r * 0.5 - c.b * 0.5, c.g * 0.5 - c.r * 0.25 - c.b * 0.25);
-}
-fn from_ycocg(c: vec3<f32>) -> vec3<f32> {
- let t = c.x - c.z;
- return vec3<f32>(t + c.y, c.x + c.z, t - c.y);
-}
-// Compress the range so a very bright pixel does not dominate the blend or the clamp.
-fn squash(c: vec3<f32>) -> vec3<f32> {
- return c / (1.0 + max(c.r, max(c.g, c.b)));
-}
-fn unsquash(c: vec3<f32>) -> vec3<f32> {
- return c / max(1.0 - max(c.r, max(c.g, c.b)), 0.001);
-}
-
-// The history at a uv, filtered with a Catmull-Rom spline (five bilinear taps) so that
-// resampling it every frame doesn't blur.
-fn history_color(uv: vec2<f32>) -> vec3<f32> {
- let size = vec2<f32>(textureDimensions(history));
- let pos = uv * size;
- let c1 = floor(pos - 0.5) + 0.5;
- let f = pos - c1;
- let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
- let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
- let w2 = f * (0.5 + f * (2.0 - 1.5 * f));
- let w3 = f * f * (-0.5 + 0.5 * f);
- let w12 = w1 + w2;
- let t0 = (c1 - 1.0) / size;
- let t3 = (c1 + 2.0) / size;
- let t12 = (c1 + w2 / w12) / size;
- var sum = textureSampleLevel(history, linear, vec2<f32>(t12.x, t0.y), 0.0).rgb * (w12.x * w0.y);
- var total = w12.x * w0.y;
- sum += textureSampleLevel(history, linear, vec2<f32>(t0.x, t12.y), 0.0).rgb * (w0.x * w12.y);
- total += w0.x * w12.y;
- sum += textureSampleLevel(history, linear, t12, 0.0).rgb * (w12.x * w12.y);
- total += w12.x * w12.y;
- sum += textureSampleLevel(history, linear, vec2<f32>(t3.x, t12.y), 0.0).rgb * (w3.x * w12.y);
- total += w3.x * w12.y;
- sum += textureSampleLevel(history, linear, vec2<f32>(t12.x, t3.y), 0.0).rgb * (w12.x * w3.y);
- total += w12.x * w3.y;
- return max(sum / total, vec3<f32>(0.0));
-}
-
-// Pulls `p` towards the box's centre until it is inside.
-fn clip_to_box(lo: vec3<f32>, hi: vec3<f32>, p: vec3<f32>) -> vec3<f32> {
- let centre = (lo + hi) * 0.5;
- let ext = max((hi - lo) * 0.5, vec3<f32>(0.00001));
- let off = (p - centre) / ext;
- let m = max(abs(off.x), max(abs(off.y), abs(off.z)));
- return select(p, centre + (p - centre) / m, m > 1.0);
-}
-
-// Temporal anti-aliasing: the history, reprojected by depth and the camera's move, clamped to
-// the colours round this pixel (so what moved or was uncovered can't ghost), blended in.
-@fragment
-fn fs_taa(in: FullOut) -> TaaOut {
- let size = vec2<i32>(textureDimensions(source));
- let px = vec2<i32>(in.position.xy);
- let d = textureLoad(depth_tex, px, 0);
- 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 centre = vec3<f32>(0.0);
- for (var y = -1; y <= 1; y++) {
- for (var x = -1; x <= 1; x++) {
- let q = clamp(px + vec2<i32>(x, y), vec2<i32>(0), size - 1);
- let s = to_ycocg(squash(textureLoad(source, q, 0).rgb));
- sum += s;
- sum2 += s * s;
- lo = min(lo, s);
- hi = max(hi, s);
- if x == 0 && y == 0 { centre = s; }
- }
- }
- let sky = d <= 0.0;
- let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, 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 = centre;
- 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 * vec2<f32>(size)), vec2<i32>(0), size - 1);
- let seen = textureLoad(history, hp, 0).a;
- let expected = select(length(p + frame.taa.xyz), 0.0, sky);
- // The same surface as last frame (not uncovered), by its distance.
- 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.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));
- let keep = mix(frame.taa_params.x, frame.taa_params.x * 0.75, clamp(moved / 4.0, 0.0, 1.0));
- resolved = mix(centre, past, keep);
- }
- }
- }
- }
- let color = unsquash(from_ycocg(resolved));
- var out: TaaOut;
- out.color = vec4<f32>(color, 1.0);
- 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;
-}
-"#;
-
-pub(super) struct Target {
- pub(super) view: wgpu::TextureView,
-}
-
-pub(super) fn target(
- device: &wgpu::Device,
- label: &str,
- size: [u32; 2],
- format: wgpu::TextureFormat,
-) -> Target {
- let view = device
- .create_texture(&wgpu::TextureDescriptor {
- label: Some(label),
- size: wgpu::Extent3d {
- width: size[0].max(1),
- height: size[1].max(1),
- depth_or_array_layers: 1,
- },
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
- view_formats: &[],
- })
- .create_view(&Default::default());
- Target { view }
-}
-
-/// The outside views and sampler the lighting group reads.
-type LightKey = (
- wgpu::TextureView,
- wgpu::TextureView,
- wgpu::TextureView,
- wgpu::Sampler,
-);
-
-/// What bloom and the tone curve read: the lit world, or the post pass's output.
-#[derive(Clone, Copy, PartialEq, Eq)]
-pub enum HdSource {
- Lit,
- Fx,
- /// The anti-aliased result of `taa`.
- Taa,
-}
-
-/// TAA is on unless `HORIZON_HD_TAA=0`.
-pub fn taa_enabled() -> bool {
- static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
- *ON.get_or_init(|| std::env::var("HORIZON_HD_TAA").map_or(true, |v| v != "0"))
-}
-
-/// The water effect is on unless `HORIZON_HD_WATER=0`.
-pub fn water_enabled() -> bool {
- static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
- *ON.get_or_init(|| std::env::var("HORIZON_HD_WATER").map_or(true, |v| v != "0"))
-}
-
-/// The Halton sequence's `index`-th number in `base`, in (0, 1).
-fn halton(mut index: u32, base: u32) -> f32 {
- let (mut f, mut r) = (1.0, 0.0);
- while index > 0 {
- f /= base as f32;
- r += f * (index % base) as f32;
- index /= base;
- }
- 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 `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,
- ]
-}
-
-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,
- /// The shadow cascades: the array to sample, one view per layer to draw.
- shadow_array: wgpu::TextureView,
- shadow_layers: Vec<wgpu::TextureView>,
- shadow_sampler: wgpu::Sampler,
- /// The globals the shadow passes draw with (the light's view), and the camera's to put back.
- pub shadow_globals: [wgpu::Buffer; 2],
- pub main_globals: wgpu::Buffer,
- frames: u32,
- pub depth: wgpu::TextureView,
- /// The lit world, and the post pass's output.
- pub lit: wgpu::TextureView,
- pub fx: wgpu::TextureView,
- /// Water: its surface colour and normal, its depth (the nearest water surface; 0 elsewhere),
- /// the lit world with the water over it, and what draws and composites them.
- water_tint: wgpu::TextureView,
- water_normal: wgpu::TextureView,
- water_depth: wgpu::TextureView,
- lit_water: wgpu::TextureView,
- /// The lighting's second target: the air over water, the floor beside translucent things.
- air: wgpu::TextureView,
- water_layout: wgpu::BindGroupLayout,
- water_pipeline: wgpu::RenderPipeline,
- water_group: Option<((wgpu::TextureView, wgpu::Sampler), wgpu::BindGroup)>,
- /// The water effect runs this frame (the scene allows it and there is translucent terrain on
- /// screen), and `lit_view` is the composite.
- water_active: bool,
- /// The frame's translucent terrain exists (set by the renderer before `write`).
- pub water_geometry: bool,
- water_composited: bool,
- /// The volumetric clouds and the fog run this frame.
- clouds_active: bool,
- fog_active: bool,
- /// The sky-view texture (see `hd_sky`), the pass that marches it and its bind group.
- sky_lut: wgpu::TextureView,
- sky_pipeline: wgpu::RenderPipeline,
- sky_group: wgpu::BindGroup,
- /// The sun's height the sky-view texture was marched for (it depends on nothing else), and
- /// whether this frame's sun has moved enough to march it again.
- sky_drawn_for: f32,
- sky_stale: bool,
- started: std::time::Instant,
- /// TAA: the resolved frame, and the two histories (one read, one written, alternating).
- taa: wgpu::TextureView,
- history: [wgpu::TextureView; 2],
- history_ready: bool,
- history_flip: usize,
- 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
- /// lighting and the source-reading ones are keyed by the views they read.
- light_group: [Option<(LightKey, wgpu::BindGroup)>; 2],
- /// The volumetric clouds and fog (see `hd_volumes`).
- volumes: HdVolumes,
- prefilter_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
- tone_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
- /// Reading bloom level k - 1 (for the down pass into level k, index k - 1) and level k (for
- /// the up pass onto level k - 1, index k - 1).
- down_groups: Vec<wgpu::BindGroup>,
- up_groups: Vec<wgpu::BindGroup>,
- frame: wgpu::Buffer,
- lights: wgpu::Buffer,
- /// The tiles' light lists (see `tile_lists`), sized for the screen, and the tiles per row.
- tiles: wgpu::Buffer,
- tiles_x: u32,
- tile_scratch: Vec<Vec<u16>>,
- tile_words: Vec<u32>,
- sampler: wgpu::Sampler,
- light_layout: wgpu::BindGroupLayout,
- light_pipeline: wgpu::RenderPipeline,
- bloom_layout: wgpu::BindGroupLayout,
- prefilter: wgpu::RenderPipeline,
- down: wgpu::RenderPipeline,
- up: wgpu::RenderPipeline,
- tone_layout: wgpu::BindGroupLayout,
- tone: wgpu::RenderPipeline,
- /// The ambient occlusion: its strength in the lighting, the half-resolution targets (raw, then
- /// blurred), their passes and bind groups (made at `resize`).
- pub ao_strength: f32,
- ao_raw: wgpu::TextureView,
- ao_blurred: wgpu::TextureView,
- ao_layout: wgpu::BindGroupLayout,
- ao_blur_layout: wgpu::BindGroupLayout,
- ao_pipeline: wgpu::RenderPipeline,
- ao_blur_pipeline: wgpu::RenderPipeline,
- ao_group: Option<wgpu::BindGroup>,
- ao_blur_group: Option<wgpu::BindGroup>,
- /// GPU time of the HD passes while diagnostics ask for it (`set_timing`).
- timing: Option<PassTiming>,
- /// Bind groups made so far, and in the last frame (a steady state makes none).
- groups_made: AtomicU64,
- groups_seen: u64,
- pub groups_last_frame: u64,
-}
-
-/// The tiles' header (4 words each) and index lists for a screen of `size`.
-fn tiles_buffer(device: &wgpu::Device, size: [u32; 2]) -> wgpu::Buffer {
- let tiles = (size[0].div_ceil(TILE).max(1) * size[1].div_ceil(TILE).max(1)) as usize;
- device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("HD light tiles"),
- size: ((tiles * 4 + TILE_INDICES) * std::mem::size_of::<u32>()) as u64,
- usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- })
-}
-
-/// Where a camera-relative sphere can show on the screen.
-enum Cover {
- /// Nowhere: it is wholly behind the camera.
- Nothing,
- /// Anywhere: it crosses the camera's plane, or holds the camera.
- Everywhere,
- /// Inside this pixel rectangle (min x, min y, max x, max y).
- Rect([f32; 4]),
-}
-
-/// The screen cover of the sphere at `c` of radius `r`: the projection of the 8 corners of the
-/// box round it. A box is convex and so is its projection while every corner is in front of the
-/// camera, so the rectangle holds the whole sphere. With some corners behind, the projection
-/// is not bounded and the cover is the whole screen; with all of them behind, no pixel ray
-/// (they all point forward) can meet the sphere.
-fn cover(view_proj: &Mat4, c: glam::Vec3, r: f32, size: [u32; 2]) -> Cover {
- if !c.is_finite() || !r.is_finite() {
- return Cover::Everywhere;
- }
- // A little more than the shader's radius, against rounding.
- let r = r * 1.01 + 0.01;
- let (mut lo, mut hi) = (glam::Vec2::splat(f32::MAX), glam::Vec2::splat(f32::MIN));
- let mut front = 0;
- for corner in 0..8 {
- let o = glam::Vec3::new(
- if corner & 1 == 0 { -r } else { r },
- if corner & 2 == 0 { -r } else { r },
- if corner & 4 == 0 { -r } else { r },
- );
- let h = *view_proj * (c + o).extend(1.0);
- if h.w > 0.0 {
- front += 1;
- let ndc = glam::Vec2::new(h.x, h.y) / h.w;
- let px = glam::Vec2::new(
- (ndc.x * 0.5 + 0.5) * size[0] as f32,
- (0.5 - ndc.y * 0.5) * size[1] as f32,
- );
- lo = lo.min(px);
- hi = hi.max(px);
- }
- }
- match front {
- 0 => Cover::Nothing,
- 8 => Cover::Rect([lo.x - 2.0, lo.y - 2.0, hi.x + 2.0, hi.y + 2.0]),
- _ => Cover::Everywhere,
- }
-}
-
-/// Fills `words` with each 16x16 tile's two lists of the lights that can reach it: header
-/// (surface start, surface length, halo start, halo length) per tile, then the indices; a
-/// length of `EVERY_LIGHT` stands for all of `lights` (a list past the buffer's room). The
-/// surface list holds the lights whose sphere can touch what the tile sees. The halo list
-/// holds the lights whose halo can reach a ray of the tile: the shader adds a halo for the
-/// view *line* passing within half the radius of the light, so a ray pointing away from a light
-/// behind it is lit too, and the mirrored sphere is covered as well. Lights stay in order, so
-/// the sums are the ones of the full loop.
-fn tile_lists(
- view_proj: &Mat4,
- lights: &[HdLight],
- size: [u32; 2],
- scratch: &mut Vec<Vec<u16>>,
- words: &mut Vec<u32>,
-) {
- let tx = size[0].div_ceil(TILE).max(1) as usize;
- let ty = size[1].div_ceil(TILE).max(1) as usize;
- let tiles = tx * ty;
- scratch.resize_with(tiles * 2, Vec::new);
- scratch.truncate(tiles * 2);
- scratch.iter_mut().for_each(Vec::clear);
- let mut add = |list: usize, area: Cover, light: u16| match area {
- Cover::Nothing => {}
- Cover::Everywhere => {
- for t in 0..tiles {
- scratch[t * 2 + list].push(light);
- }
- }
- Cover::Rect([x0, y0, x1, y1]) => {
- let tile = TILE as f32;
- if x1 < 0.0 || y1 < 0.0 || x0 >= size[0] as f32 || y0 >= size[1] as f32 {
- return;
- }
- let from = |v: f32, n: usize| ((v / tile).floor().max(0.0) as usize).min(n - 1);
- for y in from(y0, ty)..=from(y1, ty) {
- for x in from(x0, tx)..=from(x1, tx) {
- scratch[(y * tx + x) * 2 + list].push(light);
- }
- }
- }
- };
- for (i, l) in lights.iter().enumerate() {
- let c = glam::Vec3::new(l.pos[0], l.pos[1], l.pos[2]);
- add(0, cover(view_proj, c, l.pos[3], size), i as u16);
- // The shader's halo reach: the line within `radius / 2 - 0.5` of the light.
- let reach = l.pos[3] * 0.5 - 0.5;
- if reach > 0.0 || !reach.is_finite() {
- add(1, cover(view_proj, c, reach, size), i as u16);
- add(1, cover(view_proj, -c, reach, size), i as u16);
- }
- }
- words.clear();
- words.resize(tiles * 4, 0);
- let mut room = TILE_INDICES;
- for t in 0..tiles {
- for list in 0..2 {
- let ids = &mut scratch[t * 2 + list];
- // Both the sphere and its mirror can name a light: once is enough.
- ids.dedup();
- let at = t * 4 + list * 2;
- if ids.len() > room {
- words[at + 1] = EVERY_LIGHT;
- } else {
- room -= ids.len();
- words[at] = words.len() as u32;
- words[at + 1] = ids.len() as u32;
- words.extend(ids.iter().map(|&i| u32::from(i)));
- }
- }
- }
-}
-
-/// The timestamps of the pass `name` while HD passes are timed.
-fn stamp<'a>(
- timing: &'a mut Option<PassTiming>,
- name: &str,
-) -> Option<wgpu::RenderPassTimestampWrites<'a>> {
- timing.as_mut()?.writes(name)
-}
-
-/// `HORIZON_HD_AO=0` turns the ambient occlusion off, another number sets its strength.
-fn ao_strength_default() -> f32 {
- std::env::var("HORIZON_HD_AO")
- .ok()
- .and_then(|v| v.parse().ok())
- .unwrap_or(AO_STRENGTH)
-}
-
-/// The shader's source: the common part, then the body with the sky-reading functions where it
-/// has a marker (`//SKY15`: the sky-view texture is binding 15, `//SKY10`: binding 10).
-pub(super) fn assemble(body: &str) -> String {
- let mut body = body.to_owned();
- // Highest number first: `//SKY1` must not eat the start of `//SKY15`.
- for binding in (0..32).rev() {
- body = body.replace(
- &format!("//SKY{binding}"),
- &hd_sky::SKY_FUNCS.replace("SKYBIND", &binding.to_string()),
- );
- }
- format!("{COMMON}{body}")
-}
-
-pub(super) fn module(device: &wgpu::Device, label: &str, body: &str) -> wgpu::ShaderModule {
- // Development: with `HORIZON_HD_SHADER_DIR` set, a shader's body is read from
- // `<dir>/<label>.wgsl` (written there from the built-in one when missing), so it can be tuned
- // without rebuilding.
- let body = match std::env::var("HORIZON_HD_SHADER_DIR") {
- Ok(dir) => {
- let path = std::path::Path::new(&dir).join(format!("{}.wgsl", label.replace(' ', "_")));
- match std::fs::read_to_string(&path) {
- Ok(text) => text,
- Err(_) => {
- let _ = std::fs::write(&path, body);
- body.to_owned()
- }
- }
- }
- Err(_) => body.to_owned(),
- };
- device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some(label),
- source: wgpu::ShaderSource::Wgsl(assemble(&body).into()),
- })
-}
-
-/// One fullscreen pass over `to` (cleared), with the given pipeline and group.
-pub(super) fn stamped_pass(
- encoder: &mut wgpu::CommandEncoder,
- label: &str,
- to: &[&wgpu::TextureView],
- pipeline: &wgpu::RenderPipeline,
- group: &wgpu::BindGroup,
- timestamp_writes: Option<wgpu::RenderPassTimestampWrites<'_>>,
-) {
- let attachments: Vec<_> = to
- .iter()
- .map(|view| {
- Some(wgpu::RenderPassColorAttachment {
- view,
- resolve_target: None,
- depth_slice: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
- store: wgpu::StoreOp::Store,
- },
- })
- })
- .collect();
- let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some(label),
- timestamp_writes,
- color_attachments: &attachments,
- ..Default::default()
- });
- pass.set_pipeline(pipeline);
- pass.set_bind_group(0, group, &[]);
- pass.draw(0..3, 0..1);
-}
-
-pub(super) fn fullscreen(
- device: &wgpu::Device,
- label: &str,
- layout: &wgpu::BindGroupLayout,
- module: &wgpu::ShaderModule,
- entry: &str,
- format: wgpu::TextureFormat,
- blend: Option<wgpu::BlendState>,
-) -> wgpu::RenderPipeline {
- let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some(label),
- bind_group_layouts: &[Some(layout)],
- ..Default::default()
- });
- device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some(label),
- layout: Some(&pipeline_layout),
- vertex: wgpu::VertexState {
- module,
- entry_point: Some("vs_full"),
- compilation_options: Default::default(),
- buffers: &[],
- },
- fragment: Some(wgpu::FragmentState {
- module,
- entry_point: Some(entry),
- compilation_options: Default::default(),
- targets: &[Some(wgpu::ColorTargetState {
- format,
- blend,
- write_mask: wgpu::ColorWrites::ALL,
- })],
- }),
- primitive: Default::default(),
- depth_stencil: None,
- multisample: Default::default(),
- multiview_mask: None,
- cache: None,
- })
-}
-
-pub(super) fn entries(kinds: &[wgpu::BindingType]) -> Vec<wgpu::BindGroupLayoutEntry> {
- kinds
- .iter()
- .enumerate()
- .map(|(i, ty)| wgpu::BindGroupLayoutEntry {
- binding: i as u32,
- visibility: wgpu::ShaderStages::FRAGMENT,
- ty: *ty,
- count: None,
- })
- .collect()
-}
-
-pub(super) const UNIFORM: wgpu::BindingType = wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Uniform,
- has_dynamic_offset: false,
- min_binding_size: None,
-};
-pub(super) const FLOAT: wgpu::BindingType = wgpu::BindingType::Texture {
- sample_type: wgpu::TextureSampleType::Float { filterable: true },
- view_dimension: wgpu::TextureViewDimension::D2,
- multisampled: false,
-};
-pub(super) const UNFILTERED: wgpu::BindingType = wgpu::BindingType::Texture {
- sample_type: wgpu::TextureSampleType::Float { filterable: false },
- view_dimension: wgpu::TextureViewDimension::D2,
- multisampled: false,
-};
-pub(super) const DEPTH: wgpu::BindingType = wgpu::BindingType::Texture {
- sample_type: wgpu::TextureSampleType::Depth,
- view_dimension: wgpu::TextureViewDimension::D2,
- multisampled: false,
-};
-pub(super) const SAMPLER: wgpu::BindingType =
- wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering);
-
-impl Hd {
- pub fn new(
- device: &wgpu::Device,
- queue: &wgpu::Queue,
- size: [u32; 2],
- screen: wgpu::TextureFormat,
- globals_size: u64,
- ) -> Self {
- let light_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("HD lighting"),
- entries: &entries(&[
- UNIFORM,
- UNFILTERED,
- DEPTH,
- UNFILTERED,
- UNFILTERED,
- DEPTH,
- wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Storage { read_only: true },
- has_dynamic_offset: false,
- min_binding_size: None,
- },
- UNFILTERED,
- wgpu::BindingType::Texture {
- sample_type: wgpu::TextureSampleType::Depth,
- view_dimension: wgpu::TextureViewDimension::D2Array,
- multisampled: false,
- },
- wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
- FLOAT,
- SAMPLER,
- wgpu::BindingType::Buffer {
- ty: wgpu::BufferBindingType::Storage { read_only: true },
- has_dynamic_offset: false,
- min_binding_size: None,
- },
- UNFILTERED,
- DEPTH,
- FLOAT,
- FLOAT,
- UNFILTERED,
- FLOAT,
- FLOAT,
- ]),
- });
- let water_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("HD water"),
- entries: &entries(&[
- UNIFORM,
- FLOAT,
- DEPTH,
- UNFILTERED,
- UNFILTERED,
- DEPTH,
- wgpu::BindingType::Texture {
- sample_type: wgpu::TextureSampleType::Depth,
- view_dimension: wgpu::TextureViewDimension::D2Array,
- multisampled: false,
- },
- wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
- FLOAT,
- SAMPLER,
- FLOAT,
- UNFILTERED,
- ]),
- });
- let sky_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("HD sky"),
- entries: &entries(&[UNIFORM]),
- });
- let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("HD TAA"),
- entries: &entries(&[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER]),
- });
- let taa_module = module(device, "HD TAA", TAA);
- let taa_pipeline = |entry: &str| {
- let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("HD TAA"),
- bind_group_layouts: &[Some(&taa_layout)],
- ..Default::default()
- });
- let target = Some(wgpu::ColorTargetState {
- format: HDR,
- blend: None,
- write_mask: wgpu::ColorWrites::ALL,
- });
- device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("HD TAA"),
- layout: Some(&layout),
- vertex: wgpu::VertexState {
- module: &taa_module,
- entry_point: Some("vs_full"),
- compilation_options: Default::default(),
- buffers: &[],
- },
- fragment: Some(wgpu::FragmentState {
- module: &taa_module,
- entry_point: Some(entry),
- compilation_options: Default::default(),
- targets: &[target.clone(), target],
- }),
- primitive: Default::default(),
- depth_stencil: None,
- multisample: Default::default(),
- multiview_mask: None,
- 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]),
- });
- let ao_blur_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("HD ambient occlusion blur"),
- entries: &entries(&[UNIFORM, UNFILTERED]),
- });
- let bloom_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("HD bloom"),
- entries: &entries(&[UNIFORM, FLOAT, SAMPLER]),
- });
- let tone_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
- label: Some("HD tone mapping"),
- entries: &entries(&[UNIFORM, FLOAT, FLOAT, SAMPLER]),
- });
- let light = module(device, "HD lighting", LIGHT);
- let bloom = module(device, "HD bloom", BLOOM);
- let tone = module(device, "HD tone mapping", TONEMAP);
- let ao = module(device, "HD ambient occlusion", AO);
- let ao_blur = module(device, "HD ambient occlusion blur", AO_BLUR);
- let water = module(device, "HD water", WATER);
- let sky_module = module(device, "HD sky", hd_sky::SKY_PASS);
- let sky_pipeline = fullscreen(
- device,
- "HD sky",
- &sky_layout,
- &sky_module,
- "fs_sky_lut",
- hd_sky::SKY_LUT_FORMAT,
- None,
- );
- let water_pipeline = fullscreen(
- device,
- "HD water",
- &water_layout,
- &water,
- "fs_water",
- HDR,
- None,
- );
- let ao_pipeline = fullscreen(
- device,
- "HD ambient occlusion",
- &ao_layout,
- &ao,
- "fs_ao",
- AO_FORMAT,
- None,
- );
- let ao_blur_pipeline = fullscreen(
- device,
- "HD ambient occlusion blur",
- &ao_blur_layout,
- &ao_blur,
- "fs_ao_blur",
- AO_FORMAT,
- None,
- );
- let add = wgpu::BlendState {
- color: wgpu::BlendComponent {
- src_factor: wgpu::BlendFactor::One,
- dst_factor: wgpu::BlendFactor::One,
- operation: wgpu::BlendOperation::Add,
- },
- alpha: wgpu::BlendComponent::REPLACE,
- };
- let light_pipeline = {
- let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
- label: Some("HD lighting"),
- bind_group_layouts: &[Some(&light_layout)],
- ..Default::default()
- });
- let target = Some(wgpu::ColorTargetState {
- format: HDR,
- blend: None,
- write_mask: wgpu::ColorWrites::ALL,
- });
- device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
- label: Some("HD lighting"),
- layout: Some(&layout),
- vertex: wgpu::VertexState {
- module: &light,
- entry_point: Some("vs_full"),
- compilation_options: Default::default(),
- buffers: &[],
- },
- fragment: Some(wgpu::FragmentState {
- module: &light,
- entry_point: Some("fs_light"),
- compilation_options: Default::default(),
- targets: &[target.clone(), target],
- }),
- primitive: Default::default(),
- depth_stencil: None,
- multisample: Default::default(),
- multiview_mask: None,
- cache: None,
- })
- };
- let prefilter = fullscreen(
- device,
- "HD bloom prefilter",
- &bloom_layout,
- &bloom,
- "fs_prefilter",
- HDR,
- None,
- );
- let down = fullscreen(
- device,
- "HD bloom down",
- &bloom_layout,
- &bloom,
- "fs_down",
- HDR,
- None,
- );
- let up = fullscreen(
- device,
- "HD bloom up",
- &bloom_layout,
- &bloom,
- "fs_up",
- HDR,
- Some(add),
- );
- let tone = fullscreen(
- device,
- "HD tone mapping",
- &tone_layout,
- &tone,
- "fs_tonemap",
- screen,
- None,
- );
- let frame = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("HD frame"),
- size: std::mem::size_of::<HdFrame>() as u64,
- usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- let sky_lut = target(
- device,
- "HD sky",
- hd_sky::SKY_LUT_SIZE,
- hd_sky::SKY_LUT_FORMAT,
- )
- .view;
- let sky_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("HD sky"),
- layout: &sky_layout,
- entries: &[wgpu::BindGroupEntry {
- binding: 0,
- resource: frame.as_entire_binding(),
- }],
- });
- let lights = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("HD lights"),
- size: (MAX_LIGHTS * std::mem::size_of::<HdLight>()) as u64,
- usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
- let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
- label: Some("HD"),
- mag_filter: wgpu::FilterMode::Linear,
- min_filter: wgpu::FilterMode::Linear,
- address_mode_u: wgpu::AddressMode::ClampToEdge,
- address_mode_v: wgpu::AddressMode::ClampToEdge,
- ..Default::default()
- });
- let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
- label: Some("HD shadow cascades"),
- size: wgpu::Extent3d {
- width: SHADOW_SIZE,
- height: SHADOW_SIZE,
- depth_or_array_layers: 2,
- },
- mip_level_count: 1,
- sample_count: 1,
- dimension: wgpu::TextureDimension::D2,
- format: wgpu::TextureFormat::Depth32Float,
- usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
- view_formats: &[],
- });
- let shadow_array = shadow_texture.create_view(&wgpu::TextureViewDescriptor {
- dimension: Some(wgpu::TextureViewDimension::D2Array),
- ..Default::default()
- });
- let shadow_layers = (0..2)
- .map(|l| {
- shadow_texture.create_view(&wgpu::TextureViewDescriptor {
- dimension: Some(wgpu::TextureViewDimension::D2),
- base_array_layer: l,
- array_layer_count: Some(1),
- ..Default::default()
- })
- })
- .collect();
- let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
- label: Some("HD shadows"),
- mag_filter: wgpu::FilterMode::Linear,
- min_filter: wgpu::FilterMode::Linear,
- compare: Some(wgpu::CompareFunction::LessEqual),
- ..Default::default()
- });
- let globals = |label| {
- device.create_buffer(&wgpu::BufferDescriptor {
- label: Some(label),
- size: globals_size,
- usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- })
- };
- let volumes = HdVolumes::new(device, queue);
- 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,
- shadow_array,
- shadow_layers,
- shadow_sampler,
- shadow_globals: [
- globals("HD shadow globals 0"),
- globals("HD shadow globals 1"),
- ],
- main_globals: globals("HD camera globals"),
- frames: 0,
- depth: target(
- device,
- "HD depth",
- [1, 1],
- wgpu::TextureFormat::Depth32Float,
- )
- .view,
- lit: target(device, "HD lit", [1, 1], HDR).view,
- air: target(device, "HD air", [1, 1], HDR).view,
- fx: target(device, "HD post", [1, 1], HDR).view,
- water_tint: target(device, "HD water colour", [1, 1], WATER_TINT).view,
- water_normal: target(device, "HD water normal", [1, 1], WATER_NORMAL).view,
- water_depth: target(
- device,
- "HD water depth",
- [1, 1],
- wgpu::TextureFormat::Depth32Float,
- )
- .view,
- lit_water: target(device, "HD lit with water", [1, 1], HDR).view,
- water_layout,
- water_pipeline,
- water_group: None,
- water_active: false,
- water_geometry: false,
- water_composited: false,
- clouds_active: false,
- fog_active: false,
- sky_lut,
- sky_pipeline,
- sky_group,
- sky_drawn_for: 2.0,
- sky_stale: true,
- started: std::time::Instant::now(),
- taa: target(device, "HD TAA", [1, 1], HDR).view,
- history: [
- target(device, "HD TAA history", [1, 1], HDR).view,
- target(device, "HD TAA history", [1, 1], HDR).view,
- ],
- history_ready: false,
- history_flip: 0,
- taa_active: false,
- taa_layout,
- taa_pipeline,
- taau_pipeline,
- taa_groups: None,
- bloom: Vec::new(),
- light_group: [None, None],
- volumes,
- prefilter_group: None,
- tone_group: None,
- down_groups: Vec::new(),
- up_groups: Vec::new(),
- frame,
- lights,
- tiles: tiles_buffer(device, [1, 1]),
- tiles_x: 1,
- tile_scratch: Vec::new(),
- tile_words: Vec::new(),
- sampler,
- light_layout,
- light_pipeline,
- bloom_layout,
- prefilter,
- down,
- up,
- tone_layout,
- tone,
- ao_strength: ao_strength_default(),
- ao_raw: target(device, "HD AO", [1, 1], AO_FORMAT).view,
- ao_blurred: target(device, "HD AO blurred", [1, 1], AO_FORMAT).view,
- ao_layout,
- ao_blur_layout,
- ao_pipeline,
- ao_blur_pipeline,
- ao_group: None,
- ao_blur_group: None,
- timing: None,
- groups_made: AtomicU64::new(0),
- groups_seen: 0,
- groups_last_frame: 0,
- };
- hd.resize(device, size, size);
- hd
- }
-
- /// 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.air = target(device, "HD air", size, HDR).view;
- self.fx = target(device, "HD post", size, HDR).view;
- self.water_tint = target(device, "HD water colour", size, WATER_TINT).view;
- self.water_normal = target(device, "HD water normal", size, WATER_NORMAL).view;
- self.water_depth = target(
- device,
- "HD water depth",
- size,
- wgpu::TextureFormat::Depth32Float,
- )
- .view;
- self.lit_water = target(device, "HD lit with water", size, HDR).view;
- self.water_group = None;
- self.taa = target(device, "HD TAA", out, HDR).view;
- self.history = [
- target(device, "HD TAA history", out, HDR).view,
- target(device, "HD TAA history", out, HDR).view,
- ];
- self.history_ready = false;
- self.taa_groups = None;
- let half = [size[0].div_ceil(2), size[1].div_ceil(2)];
- self.ao_raw = target(device, "HD AO", half, AO_FORMAT).view;
- self.ao_blurred = target(device, "HD AO blurred", half, AO_FORMAT).view;
- self.ao_group = Some(self.make_group(
- device,
- "HD ambient occlusion",
- &self.ao_layout,
- &[
- wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
- wgpu::BindingResource::TextureView(&self.depth),
- wgpu::BindingResource::TextureView(&self.normal),
- ],
- ));
- self.ao_blur_group = Some(self.make_group(
- device,
- "HD ambient occlusion blur",
- &self.ao_blur_layout,
- &[
- wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
- wgpu::BindingResource::TextureView(&self.ao_raw),
- ],
- ));
- self.tiles = tiles_buffer(device, size);
- self.tiles_x = size[0].div_ceil(TILE).max(1);
- self.bloom = (0..BLOOM_LEVELS)
- .map(|k| {
- let s = [(out[0] >> (k + 1)).max(1), (out[1] >> (k + 1)).max(1)];
- target(device, "HD bloom", s, HDR)
- })
- .collect();
- // Every group names a view that was just replaced.
- self.light_group = [None, None];
- self.volumes.resize(device, size);
- self.prefilter_group = None;
- self.tone_group = None;
- self.down_groups = (1..self.bloom.len())
- .map(|k| self.bloom_group(device, &self.bloom[k - 1].view))
- .collect();
- self.up_groups = (1..self.bloom.len())
- .map(|k| self.bloom_group(device, &self.bloom[k].view))
- .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,
- queue: &wgpu::Queue,
- scene: &HdScene,
- lights: &[HdLight],
- settings: HdSettings,
- ) {
- let n = lights.len().min(MAX_LIGHTS);
- if n > 0 {
- queue.write_buffer(&self.lights, 0, bytemuck::cast_slice(&lights[..n]));
- }
- // Written every frame, empty too: the shader reads the counts, not `params.x`.
- tile_lists(
- &scene.view_proj,
- &lights[..n],
- self.size,
- &mut self.tile_scratch,
- &mut self.tile_words,
- );
- queue.write_buffer(&self.tiles, 0, bytemuck::cast_slice(&self.tile_words));
- self.frames = self.frames.wrapping_add(1);
- let made = self.groups_made.load(Ordering::Relaxed) + self.volumes.groups_made;
- self.groups_last_frame = made - self.groups_seen;
- self.groups_seen = made;
- if PassTiming::logging() && self.frames.is_multiple_of(240) {
- log::info!(
- "HD bind groups made in the last frame: {} ({made} in all)",
- self.groups_last_frame
- );
- }
- // The sun by day, the moon (opposite) by night, faded out round the horizon.
- let (dir, strength, color) = light_of(scene);
- let cascades = scene.cascades;
- let night = scene.dark.clamp(0.0, 1.0);
- self.water_active = scene.water && water_enabled() && self.water_geometry;
- self.taa_active = scene.taa && taa_enabled();
- // A long jump of the camera (a teleport) starts the history over.
- if !self.taa_active || scene.camera_delta.length() > 64.0 {
- self.history_ready = false;
- }
- let sun_height = scene.sun.normalize_or(glam::Vec3::Y).y;
- self.sky_stale = (sun_height - self.sky_drawn_for).abs() > 0.001;
- if self.sky_stale {
- self.sky_drawn_for = sun_height;
- }
- self.clouds_active = scene.sky && scene.clouds;
- self.fog_active = scene.sky && hd_volumes::fog_enabled();
- let (cloud_dir, cloud_colour, cloud_strength) = cloud_light(scene);
- // The morning mist: thick before the sun is up, gone by the time it is high.
- let mist = if sun_dir_x(scene) > 0.0 {
- smooth(-0.5, -0.1, sun_height) * (1.0 - smooth(0.05, 0.5, sun_height))
- } else {
- 0.0
- };
- let frame = HdFrame {
- prev_view_proj: if self.upscaling() {
- scene.prev_view_proj
- } else {
- scene.prev_view_proj_jittered
- }
- .to_cols_array_2d(),
- taa: [
- scene.camera_delta.x,
- scene.camera_delta.y,
- scene.camera_delta.z,
- if self.history_ready { 1.0 } else { 0.0 },
- ],
- view_proj: scene.view_proj.to_cols_array_2d(),
- water: [
- if self.water_active { 1.0 } else { 0.0 },
- (self.started.elapsed().as_secs_f64() % 600.0) as f32,
- scene.camera_mod[0],
- scene.camera_mod[1],
- ],
- sky_color: [
- scene.sky_color[0],
- scene.sky_color[1],
- scene.sky_color[2],
- if scene.sky { 1.0 } else { 0.0 },
- ],
- sky_sun: {
- let sun = scene.sun.normalize_or(glam::Vec3::Y);
- [sun.x, sun.y, sun.z, if scene.sky { 1.0 } else { 0.0 }]
- },
- sky_extra: [
- scene.sky_fog_end,
- 0.0,
- (scene.rain / 0.7).clamp(0.0, 1.0) * 0.85,
- 0.0,
- ],
- sunrise: scene.sunrise,
- clouds: [
- hd_volumes::CLOUD_BASE - scene.camera_world[2],
- hd_volumes::CLOUD_THICKNESS,
- if self.clouds_active { 1.0 } else { 0.0 },
- scene.seconds.rem_euclid(300_000.0),
- ],
- cloud_cam: [
- scene.camera_world[0],
- scene.camera_world[1],
- (scene.rain / 0.7).clamp(0.0, 1.0) * 0.18,
- if self.volumes.history_ready()
- && self.taa_active
- && scene.camera_delta.length() < 64.0
- {
- 1.0
- } else {
- 0.0
- },
- ],
- cloud_dir: [cloud_dir.x, cloud_dir.y, cloud_dir.z, 0.0],
- cloud_sun: [
- cloud_colour.x,
- cloud_colour.y,
- cloud_colour.z,
- cloud_strength,
- ],
- vfog: [
- if self.fog_active { 1.0 } else { 0.0 },
- scene.camera_world[2],
- mist,
- 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,
- self.size[1] as f32,
- 1.0 / self.size[0] as f32,
- 1.0 / self.size[1] as f32,
- ],
- params: [
- n as f32,
- settings.exposure,
- settings.bloom,
- (self.frames % 64) as f32,
- ],
- cascades: cascades.map(|m| m.to_cols_array_2d()),
- sun: [dir.x, dir.y, dir.z, strength],
- sun_color: [color.x, color.y, color.z, night],
- fog_color: scene.fog_color,
- fog: scene.fog,
- // In the shade by day a surface keeps half its sky light; at night almost all.
- ambient: [
- 0.5 + 0.45 * night,
- 1.0,
- 0.0018 + scene.rain * 0.006,
- self.ao_strength.max(0.0),
- ],
- shadow: [
- if strength > 0.0 { 1.0 } else { 0.0 },
- CASCADES[0],
- CASCADES[1],
- self.tiles_x as f32,
- ],
- };
- queue.write_buffer(&self.frame, 0, bytemuck::bytes_of(&frame));
- }
-
- /// The shadow pass of cascade `c`: its depth layer cleared, nothing else.
- pub fn shadow_pass<'e>(
- &mut self,
- encoder: &'e mut wgpu::CommandEncoder,
- c: usize,
- ) -> wgpu::RenderPass<'e> {
- encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("HD shadow cascade"),
- timestamp_writes: stamp(&mut self.timing, ["hd shadow 0", "hd shadow 1"][c.min(1)]),
- color_attachments: &[],
- depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
- view: &self.shadow_layers[c],
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Clear(1.0),
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- }),
- ..Default::default()
- })
- .forget_lifetime()
- }
-
- /// The G-buffer pass's attachments: albedo and normal cleared, its own depth.
- pub fn gbuffer_pass<'e>(
- &mut self,
- encoder: &'e mut wgpu::CommandEncoder,
- ) -> wgpu::RenderPass<'e> {
- let clear = |view| {
- Some(wgpu::RenderPassColorAttachment {
- view,
- resolve_target: None,
- depth_slice: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
- store: wgpu::StoreOp::Store,
- },
- })
- };
- encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("HD G-buffer"),
- timestamp_writes: stamp(&mut self.timing, "hd g-buffer"),
- color_attachments: &[
- clear(&self.albedo),
- clear(&self.normal),
- clear(&self.levels),
- ],
- depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
- view: &self.depth,
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Clear(0.0),
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- }),
- ..Default::default()
- })
- .forget_lifetime()
- }
-
- fn draw(
- encoder: &mut wgpu::CommandEncoder,
- label: &str,
- to: &wgpu::TextureView,
- load: bool,
- pipeline: &wgpu::RenderPipeline,
- group: &wgpu::BindGroup,
- timestamp_writes: Option<wgpu::RenderPassTimestampWrites<'_>>,
- ) {
- let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some(label),
- timestamp_writes,
- color_attachments: &[Some(wgpu::RenderPassColorAttachment {
- view: to,
- resolve_target: None,
- depth_slice: None,
- ops: wgpu::Operations {
- load: if load {
- wgpu::LoadOp::Load
- } else {
- wgpu::LoadOp::Clear(wgpu::Color::BLACK)
- },
- store: wgpu::StoreOp::Store,
- },
- })],
- ..Default::default()
- });
- pass.set_pipeline(pipeline);
- pass.set_bind_group(0, group, &[]);
- pass.draw(0..3, 0..1);
- }
-
- /// Marches the sky-view texture for this frame's sun (before the lighting and the water); skipped
- /// while the sun's height has moved less than a thousandth since the last march.
- pub fn sky(&mut self, encoder: &mut wgpu::CommandEncoder) {
- if !self.sky_stale {
- return;
- }
- Self::draw(
- encoder,
- "HD sky",
- &self.sky_lut,
- false,
- &self.sky_pipeline,
- &self.sky_group,
- stamp(&mut self.timing, "hd sky"),
- );
- }
-
- /// The volumetric clouds (with their shadow map) and the fog, before the lighting reads them.
- pub fn volumes(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- depth: &wgpu::TextureView,
- ) {
- if self.clouds_active {
- self.volumes
- .prepare_clouds(device, &self.frame, &self.sky_lut);
- self.volumes
- .draw_cloud_shadow(encoder, stamp(&mut self.timing, "hd cloud shadows"));
- self.volumes
- .draw_clouds(encoder, stamp(&mut self.timing, "hd clouds"));
- } else {
- self.volumes.reset_history();
- }
- if self.fog_active {
- self.volumes.draw_fog(
- device,
- encoder,
- &self.frame,
- depth,
- &self.shadow_array,
- &self.sky_lut,
- stamp(&mut self.timing, "hd fog"),
- );
- }
- }
-
- /// Lights the world (its colour and depth) into `lit`.
- pub fn light(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- world: (&wgpu::TextureView, &wgpu::TextureView),
- lightmap: (&wgpu::TextureView, &wgpu::Sampler),
- ) {
- let key = (
- world.0.clone(),
- world.1.clone(),
- lightmap.0.clone(),
- lightmap.1.clone(),
- );
- let flip = self.volumes.flip_index();
- if self.light_group[flip]
- .as_ref()
- .is_none_or(|(k, _)| *k != key)
- {
- self.groups_made.fetch_add(1, Ordering::Relaxed);
- let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("HD lighting"),
- layout: &self.light_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: self.frame.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::TextureView(world.0),
- },
- wgpu::BindGroupEntry {
- binding: 2,
- resource: wgpu::BindingResource::TextureView(world.1),
- },
- wgpu::BindGroupEntry {
- binding: 3,
- resource: wgpu::BindingResource::TextureView(&self.albedo),
- },
- wgpu::BindGroupEntry {
- binding: 4,
- resource: wgpu::BindingResource::TextureView(&self.normal),
- },
- wgpu::BindGroupEntry {
- binding: 5,
- resource: wgpu::BindingResource::TextureView(&self.depth),
- },
- wgpu::BindGroupEntry {
- binding: 6,
- resource: self.lights.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 7,
- resource: wgpu::BindingResource::TextureView(&self.levels),
- },
- wgpu::BindGroupEntry {
- binding: 8,
- resource: wgpu::BindingResource::TextureView(&self.shadow_array),
- },
- wgpu::BindGroupEntry {
- binding: 9,
- resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
- },
- wgpu::BindGroupEntry {
- binding: 10,
- resource: wgpu::BindingResource::TextureView(lightmap.0),
- },
- wgpu::BindGroupEntry {
- binding: 11,
- resource: wgpu::BindingResource::Sampler(lightmap.1),
- },
- wgpu::BindGroupEntry {
- binding: 12,
- resource: self.tiles.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 13,
- resource: wgpu::BindingResource::TextureView(&self.ao_blurred),
- },
- wgpu::BindGroupEntry {
- binding: 14,
- resource: wgpu::BindingResource::TextureView(&self.water_depth),
- },
- wgpu::BindGroupEntry {
- binding: 15,
- resource: wgpu::BindingResource::TextureView(&self.sky_lut),
- },
- wgpu::BindGroupEntry {
- binding: 16,
- resource: wgpu::BindingResource::TextureView(&self.volumes.fog),
- },
- wgpu::BindGroupEntry {
- binding: 17,
- resource: wgpu::BindingResource::TextureView(&self.volumes.fog_dist),
- },
- wgpu::BindGroupEntry {
- binding: 18,
- resource: wgpu::BindingResource::TextureView(self.volumes.cloud_view()),
- },
- wgpu::BindGroupEntry {
- binding: 19,
- resource: wgpu::BindingResource::TextureView(&self.volumes.cloud_shadow),
- },
- ],
- });
- self.light_group[flip] = Some((key, group));
- }
- let Some((_, group)) = &self.light_group[flip] else {
- return;
- };
- self.water_composited = false;
- let clear = |view| {
- Some(wgpu::RenderPassColorAttachment {
- view,
- resolve_target: None,
- depth_slice: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
- store: wgpu::StoreOp::Store,
- },
- })
- };
- {
- let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("HD lighting"),
- timestamp_writes: stamp(&mut self.timing, "hd lighting"),
- color_attachments: &[clear(&self.lit), clear(&self.air)],
- ..Default::default()
- });
- pass.set_pipeline(&self.light_pipeline);
- pass.set_bind_group(0, group, &[]);
- pass.draw(0..3, 0..1);
- }
- }
-
- /// A bind group of consecutive bindings 0.., counted (`groups_last_frame`).
- fn make_group(
- &self,
- device: &wgpu::Device,
- label: &str,
- layout: &wgpu::BindGroupLayout,
- resources: &[wgpu::BindingResource<'_>],
- ) -> wgpu::BindGroup {
- self.groups_made.fetch_add(1, Ordering::Relaxed);
- let entries: Vec<_> = resources
- .iter()
- .enumerate()
- .map(|(i, r)| wgpu::BindGroupEntry {
- binding: i as u32,
- resource: r.clone(),
- })
- .collect();
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some(label),
- layout,
- entries: &entries,
- })
- }
-
- fn bloom_group(&self, device: &wgpu::Device, source: &wgpu::TextureView) -> wgpu::BindGroup {
- self.groups_made.fetch_add(1, Ordering::Relaxed);
- device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("HD bloom"),
- layout: &self.bloom_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: self.frame.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::TextureView(source),
- },
- wgpu::BindGroupEntry {
- binding: 2,
- resource: wgpu::BindingResource::Sampler(&self.sampler),
- },
- ],
- })
- }
-
- /// Bloom from `source` (the lit world or the post pass's output), then the tone curve onto `screen`.
- pub fn finish(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- source: HdSource,
- screen: &wgpu::TextureView,
- ) {
- let source = match source {
- HdSource::Lit => self.lit_view().clone(),
- HdSource::Fx => self.fx.clone(),
- HdSource::Taa => self.taa.clone(),
- };
- if self
- .prefilter_group
- .as_ref()
- .is_none_or(|(v, _)| *v != source)
- {
- let group = self.bloom_group(device, &source);
- self.prefilter_group = Some((source.clone(), group));
- }
- if self.tone_group.as_ref().is_none_or(|(v, _)| *v != source) {
- self.groups_made.fetch_add(1, Ordering::Relaxed);
- let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("HD tone mapping"),
- layout: &self.tone_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: self.frame.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::TextureView(&source),
- },
- wgpu::BindGroupEntry {
- binding: 2,
- resource: wgpu::BindingResource::TextureView(&self.bloom[0].view),
- },
- wgpu::BindGroupEntry {
- binding: 3,
- resource: wgpu::BindingResource::Sampler(&self.sampler),
- },
- ],
- });
- self.tone_group = Some((source, group));
- }
- let (Some((_, prefilter)), Some((_, tone))) = (&self.prefilter_group, &self.tone_group)
- else {
- return;
- };
- // Down: the prefilter into the first level, then each level from the one above.
- Self::draw(
- encoder,
- "HD bloom prefilter",
- &self.bloom[0].view,
- false,
- &self.prefilter,
- prefilter,
- stamp(&mut self.timing, BLOOM_NAMES[0]),
- );
- for k in 1..self.bloom.len() {
- Self::draw(
- encoder,
- "HD bloom down",
- &self.bloom[k].view,
- false,
- &self.down,
- &self.down_groups[k - 1],
- stamp(&mut self.timing, BLOOM_NAMES[k]),
- );
- }
- // Up: each level added onto the bigger one.
- for k in (1..self.bloom.len()).rev() {
- Self::draw(
- encoder,
- "HD bloom up",
- &self.bloom[k - 1].view,
- true,
- &self.up,
- &self.up_groups[k - 1],
- stamp(&mut self.timing, BLOOM_NAMES[BLOOM_LEVELS + k - 1]),
- );
- }
- Self::draw(
- encoder,
- "HD tone mapping",
- screen,
- false,
- &self.tone,
- tone,
- stamp(&mut self.timing, "hd tonemap"),
- );
- }
-
- /// Temporal anti-aliasing of `source` (with the world's `depth`): the source it left, to hand
- /// to `finish` — the same one when TAA is off this frame.
- pub fn taa(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- source: HdSource,
- depth: &wgpu::TextureView,
- ) -> HdSource {
- if !self.taa_active {
- return source;
- }
- let input = match source {
- HdSource::Lit => self.lit_view().clone(),
- HdSource::Fx => self.fx.clone(),
- HdSource::Taa => return source,
- };
- let key = (input, depth.clone());
- if self.taa_groups.as_ref().is_none_or(|(k, _)| *k != key) {
- let groups = [0, 1].map(|read| {
- self.make_group(
- device,
- "HD TAA",
- &self.taa_layout,
- &[
- wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
- wgpu::BindingResource::TextureView(&key.0),
- wgpu::BindingResource::TextureView(depth),
- wgpu::BindingResource::TextureView(&self.history[read]),
- wgpu::BindingResource::Sampler(&self.sampler),
- ],
- )
- });
- self.taa_groups = Some((key, groups));
- }
- let Some((_, groups)) = &self.taa_groups else {
- 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,
- if upscaling { "hd taau" } else { "hd taa" },
- ),
- color_attachments: &[&self.taa, &self.history[write]].map(|view| {
- Some(wgpu::RenderPassColorAttachment {
- view,
- resolve_target: None,
- depth_slice: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
- store: wgpu::StoreOp::Store,
- },
- })
- }),
- ..Default::default()
- });
- 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);
- self.history_flip = write;
- self.history_ready = true;
- HdSource::Taa
- }
-
- /// The lit world as later passes see it: with the water over it when the effect ran.
- pub fn lit_view(&self) -> &wgpu::TextureView {
- if self.water_composited {
- &self.lit_water
- } else {
- &self.lit
- }
- }
-
- /// The water effect runs this frame (the renderer draws the water quads with `water_pass`).
- pub fn water_active(&self) -> bool {
- self.water_active
- }
-
- /// The water pass's attachments: the surface colour and normal, and the water's own depth,
- /// all cleared. The caller draws the translucent terrain with `fs_water`.
- pub fn water_pass<'e>(
- &mut self,
- encoder: &'e mut wgpu::CommandEncoder,
- ) -> wgpu::RenderPass<'e> {
- let clear = |view| {
- Some(wgpu::RenderPassColorAttachment {
- view,
- resolve_target: None,
- depth_slice: None,
- ops: wgpu::Operations {
- load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
- store: wgpu::StoreOp::Store,
- },
- })
- };
- encoder
- .begin_render_pass(&wgpu::RenderPassDescriptor {
- label: Some("HD water"),
- timestamp_writes: stamp(&mut self.timing, "hd water"),
- color_attachments: &[clear(&self.water_tint), clear(&self.water_normal)],
- depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
- view: &self.water_depth,
- depth_ops: Some(wgpu::Operations {
- load: wgpu::LoadOp::Clear(0.0),
- store: wgpu::StoreOp::Store,
- }),
- stencil_ops: None,
- }),
- ..Default::default()
- })
- .forget_lifetime()
- }
-
- /// Lays the water over the lit world (refraction, reflection, glint) into a target of its
- /// own; `lit_view` is that target after this.
- pub fn water_composite(
- &mut self,
- device: &wgpu::Device,
- encoder: &mut wgpu::CommandEncoder,
- lightmap: (&wgpu::TextureView, &wgpu::Sampler),
- ) {
- if !self.water_active {
- return;
- }
- let key = (lightmap.0.clone(), lightmap.1.clone());
- if self.water_group.as_ref().is_none_or(|(k, _)| *k != key) {
- let group = self.make_group(
- device,
- "HD water",
- &self.water_layout,
- &[
- wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
- wgpu::BindingResource::TextureView(&self.lit),
- wgpu::BindingResource::TextureView(&self.depth),
- wgpu::BindingResource::TextureView(&self.water_tint),
- wgpu::BindingResource::TextureView(&self.water_normal),
- wgpu::BindingResource::TextureView(&self.water_depth),
- wgpu::BindingResource::TextureView(&self.shadow_array),
- wgpu::BindingResource::Sampler(&self.shadow_sampler),
- wgpu::BindingResource::TextureView(lightmap.0),
- wgpu::BindingResource::Sampler(&self.sampler),
- wgpu::BindingResource::TextureView(&self.sky_lut),
- wgpu::BindingResource::TextureView(&self.air),
- ],
- );
- self.water_group = Some((key, group));
- }
- let Some((_, group)) = &self.water_group else {
- return;
- };
- Self::draw(
- encoder,
- "HD water composite",
- &self.lit_water,
- false,
- &self.water_pipeline,
- group,
- stamp(&mut self.timing, "hd water composite"),
- );
- self.water_composited = true;
- }
-
- /// The ambient occlusion from the G-buffer's depth and normals: the half-resolution pass, then
- /// its blur. Run after the G-buffer pass; nothing when the strength is 0.
- pub fn ao(&mut self, encoder: &mut wgpu::CommandEncoder) {
- if self.ao_strength <= 0.0 {
- return;
- }
- let (Some(raw), Some(blur)) = (&self.ao_group, &self.ao_blur_group) else {
- return;
- };
- Self::draw(
- encoder,
- "HD ambient occlusion",
- &self.ao_raw,
- false,
- &self.ao_pipeline,
- raw,
- stamp(&mut self.timing, "hd ao"),
- );
- Self::draw(
- encoder,
- "HD ambient occlusion blur",
- &self.ao_blurred,
- false,
- &self.ao_blur_pipeline,
- blur,
- stamp(&mut self.timing, "hd ao blur"),
- );
- }
-
- /// Times the HD passes on the GPU while `on` (and the device can), else not.
- pub fn set_timing(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, on: bool) {
- if on != self.timing.is_some() {
- self.timing = on.then(|| PassTiming::new(device, queue)).flatten();
- }
- }
-
- /// At the start of the frame's passes, before the first of them.
- pub fn timing_begin(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
- if let Some(t) = &mut self.timing {
- t.begin(device, queue);
- }
- }
-
- /// After the frame is submitted.
- pub fn timing_submitted(&mut self, queue: &wgpu::Queue) {
- if let Some(t) = &mut self.timing {
- t.submitted(queue);
- }
- }
-
- /// Each HD pass's median GPU time in milliseconds (bloom's passes summed into one).
- pub fn pass_times(&self) -> Vec<(String, f32)> {
- let Some(t) = &self.timing else {
- return Vec::new();
- };
- let mut out: Vec<(String, f32)> = Vec::new();
- for (name, ms) in t.medians() {
- let name = name.split('#').next().unwrap_or(&name).to_owned();
- match out.iter_mut().find(|o| o.0 == name) {
- Some(o) => o.1 += ms,
- None => out.push((name, ms)),
- }
- }
- out
- }
-}
-
-/// The light that casts the shadows: towards the sun by day, the moon by night; how strong
-/// (0 below the horizon or without a sky) and its colour (warm low, white high, cold moon).
-pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
- if !scene.sky {
- return (glam::Vec3::Y, 0.0, glam::Vec3::ZERO);
- }
- let sun = scene.sun.normalize_or(glam::Vec3::Y);
- let clear = 1.0 - scene.rain * 0.85;
- if sun.y > -0.02 {
- let up = smooth(-0.02, 0.18, sun.y);
- // The atmosphere's transmittance along the sun's ray (orange when low, a clean warm white
- // high), kept at full brightness and tempered, with a slight warm tint on top.
- let t = hd_sky::sun_transmittance(sun.y);
- let hue = t / t.max_element().max(1e-4);
- let color = glam::Vec3::new(hue.x.powf(0.6), hue.y.powf(0.6), hue.z.powf(0.6))
- * glam::Vec3::new(1.0, 0.93, 0.78)
- * hd_sky::sun_tint(sun.y);
- (sun, 0.7 * up * clear, color)
- } else {
- let moon = -sun;
- let up = smooth(-0.02, 0.18, moon.y);
- (moon, 0.15 * up * clear, glam::Vec3::new(0.55, 0.66, 1.0))
- }
-}
-
-/// The factor on vanilla's cloud colour that lights the clouds with the low sun's colour at dusk
-/// and dawn instead of a flat white. It is the clouds' own colour (the cloud pass gets it), not a
-/// guess from the depth buffer, so what stands behind a cloud cannot change it.
-pub fn vanilla_cloud_tint(scene: &HdScene) -> [f32; 3] {
- let sun = scene.sun.normalize_or(glam::Vec3::Y);
- let low = 1.0 - smooth(0.05, 0.5, sun.y);
- let overcast = (scene.rain / 0.7).clamp(0.0, 1.0) * 0.85;
- let (_, _, color) = light_of(scene);
- let warm = color.lerp(glam::Vec3::ONE, 0.45) * 1.1;
- let t = low * 0.7 * (1.0 - overcast);
- let tint = glam::Vec3::ONE.lerp(warm, t);
- [tint.x, tint.y, tint.z]
-}
-
-/// The x of the sun's direction: positive in the morning, negative in the evening.
-fn sun_dir_x(scene: &HdScene) -> f32 {
- scene.sun.normalize_or(glam::Vec3::Y).x
-}
-
-/// What lights the clouds: the direction, the colour and the strength. The sun's colour from the
-/// atmosphere's transmittance (as `light_of`, with Photon's evening tint), kept lighting the clouds
-/// a little past the sunset (the clouds stand high), the moon's cold light at night.
-pub fn cloud_light(scene: &HdScene) -> (glam::Vec3, glam::Vec3, f32) {
- let sun = scene.sun.normalize_or(glam::Vec3::Y);
- let clear = 1.0 - scene.rain * 0.85;
- if sun.y > -0.2 {
- let t = hd_sky::sun_transmittance(sun.y.max(0.01));
- let hue = t / t.max_element().max(1e-4);
- let color = glam::Vec3::new(hue.x.powf(0.6), hue.y.powf(0.6), hue.z.powf(0.6))
- * hd_sky::sun_tint(sun.y);
- (sun, color, smooth(-0.2, 0.08, sun.y) * clear)
- } else {
- (
- -sun,
- glam::Vec3::new(0.55, 0.66, 1.0),
- 0.22 * smooth(-0.2, -0.45, sun.y) * clear,
- )
- }
-}
-
-fn smooth(a: f32, b: f32, x: f32) -> f32 {
- let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
- t * t * (3.0 - 2.0 * t)
-}
-
-/// The cascades' light view-projections (camera-relative), each square snapped to its texels in
-/// the world so the shadows don't crawl as the camera moves.
-pub fn cascades(camera: glam::DVec3, light: glam::Vec3) -> [Mat4; 2] {
- let l = light.normalize_or(glam::Vec3::Y).as_dvec3();
- let forward = -l;
- let reference = if l.y.abs() > 0.99 {
- glam::DVec3::Z
- } else {
- glam::DVec3::Y
- };
- let right = forward.cross(reference).normalize();
- let up = right.cross(forward);
- CASCADES.map(|r| {
- let texel = f64::from(r) * 2.0 / f64::from(SHADOW_SIZE);
- let snap = |v: f64| (v / texel).floor() * texel - v;
- let centre = right * snap(camera.dot(right)) + up * snap(camera.dot(up));
- let eye = centre + l * 400.0;
- let view = glam::DMat4::look_at_rh(eye, centre, up);
- let r = f64::from(r);
- let projection = glam::DMat4::orthographic_rh(-r, r, -r, r, 1.0, 800.0);
- (projection * view).as_mat4()
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn lighting_shader_parses() {
- for (name, body) in [
- ("light", LIGHT),
- ("bloom", BLOOM),
- ("tonemap", TONEMAP),
- ("taa", TAA),
- ("ao", AO),
- ("ao blur", AO_BLUR),
- ("water", WATER),
- ("sky", hd_sky::SKY_PASS),
- ]
- .into_iter()
- .chain(
- hd_volumes::shader_sources()
- .iter()
- .map(|(n, b)| (*n, b.as_str())),
- ) {
- let source = assemble(body);
- let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| {
- panic!("{name}: {}", e.emit_to_string(&source));
- });
- naga::valid::Validator::new(
- naga::valid::ValidationFlags::all(),
- naga::valid::Capabilities::all(),
- )
- .validate(&module)
- .unwrap_or_else(|e| panic!("{name}: {e:?}"));
- }
- }
-
- #[test]
- fn jitter_stays_within_a_pixel() {
- let size = [1920u32, 1080];
- let mut seen = std::collections::HashSet::new();
- for f in 0..32 {
- 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()));
- }
- assert_eq!(seen.len(), 16);
- }
-
- #[test]
- fn taa_history_lookup_follows_the_jittered_grid() {
- // A static point under two frames' different Halton jitters: fs_taa finds this frame's
- // point in the history by the previous frame's matrix *as drawn*, which lands on the pixel
- // the previous frame wrote it to; the unjittered one is off by that frame's jitter.
- let size = [1920u32, 1080];
- let unjittered = Mat4::perspective_rh(1.0, 16.0 / 9.0, 0.05, 1000.0);
- let to_uv = |m: Mat4, p: glam::Vec3| {
- let c = m * p.extend(1.0);
- [c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5]
- };
- let drawn = |frame: u32| {
- let j = jitter(frame, size, 16);
- Mat4::from_translation(glam::Vec3::new(j[0], j[1], 0.0)) * unjittered
- };
- let p = glam::Vec3::new(0.0, 0.0, -20.0);
- for frame in 1..16 {
- let prev = drawn(frame - 1);
- // The pixel (in uv) the previous frame drew the point at: what the history holds there.
- let wrote = to_uv(prev, p);
- let old = to_uv(unjittered, p);
- let err = [
- (old[0] - wrote[0]).abs() * size[0] as f32,
- (old[1] - wrote[1]).abs() * size[1] as f32,
- ];
- let pj = jitter(frame - 1, size, 16);
- assert!((err[0] - pj[0].abs() * size[0] as f32 * 0.5).abs() < 1e-3);
- assert!((err[1] - pj[1].abs() * size[1] as f32 * 0.5).abs() < 1e-3);
- }
- }
-
- #[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];
- let proj = Mat4::perspective_rh(1.0, 2.0, 0.05, 1000.0);
- let light = |x: f32, y: f32, z: f32, r: f32| HdLight {
- pos: [x, y, z, r],
- color: [1.0; 4],
- };
- let lights = [
- // Dead ahead and small; far to the side; behind the camera; holding the camera.
- light(0.0, 0.0, -20.0, 4.0),
- light(30.0, 0.0, -5.0, 4.0),
- light(0.0, 0.0, 10.0, 8.0),
- light(1.0, 0.0, 0.0, 6.0),
- ];
- let (mut scratch, mut words) = (Vec::new(), Vec::new());
- tile_lists(&proj, &lights, size, &mut scratch, &mut words);
- let list = |tile: usize, which: usize| -> Vec<u32> {
- let (start, len) = (words[tile * 4 + which * 2], words[tile * 4 + which * 2 + 1]);
- words[start as usize..(start + len) as usize].to_vec()
- };
- // The centre tile sees the first light and the one at the camera, in both lists.
- let centre = 5 * 20 + 10;
- assert_eq!(list(centre, 0), vec![0, 3]);
- assert!(list(centre, 1).contains(&0) && list(centre, 1).contains(&3));
- // A far corner sees only the one at the camera.
- assert_eq!(list(0, 0), vec![3]);
- // The light behind the camera shows in the halo list where the view points away.
- assert!(list(centre, 1).contains(&2));
- assert!(!list(0, 0).contains(&2));
- }
-}
diff --git a/crates/horizon-client/src/gfx/hd/ao.rs b/crates/horizon-client/src/gfx/hd/ao.rs
new file mode 100644
index 000000000..c6c5f8b2f
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/ao.rs
@@ -0,0 +1,159 @@
+//! Ambient occlusion (GTAO) at half resolution, then a depth-aware blur.
+//!
+//! Contract:
+//! - Reads (world size): the G-buffer's depth (reversed-Z, `Depth32Float`) and normal; `HdFrame`
+//! for `inverse_view_proj` (camera-relative positions), `resolution` and the frame counter
+//! `params.w` (IGN noise rotation). Strength is `HdFrame.ambient.w` (`HORIZON_HD_AO`, 0 = off and
+//! the pass is skipped).
+//! - Writes `raw`, then `blurred` (`Rg16Float`, half the world size, rounded up): x = occlusion
+//! (1 open, 0 closed), y = the camera distance (blocks) the sample was taken at, for the
+//! blur's and the lighting's depth-aware reads. Sky pixels write (1, 0).
+//! - Reader: `fs_light` (binding 13) reads `blurred`.
+//! - Bind groups name the G-buffer's views, so they are remade at `resize` only.
+use super::gbuffer::GBuffer;
+use super::{DEPTH, Hd, UNFILTERED, UNIFORM, entries, fullscreen, make_group, module, stamp};
+use std::sync::atomic::AtomicU64;
+
+/// The ambient occlusion's targets (half resolution): occlusion and the distance it was taken at.
+pub const AO_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rg16Float;
+/// The occlusion's default strength (`Ao::strength`; 0 turns it off).
+pub const AO_STRENGTH: f32 = 1.0;
+
+pub(super) const AO: &str = include_str!("ao.wgsl");
+pub(super) const AO_BLUR: &str = include_str!("ao_blur.wgsl");
+
+/// `fs_ao`'s group 0: frame, G-buffer depth, G-buffer normal.
+pub(super) const AO_BINDINGS: [wgpu::BindingType; 3] = [UNIFORM, DEPTH, UNFILTERED];
+/// `fs_ao_blur`'s group 0: frame, the raw occlusion.
+pub(super) const AO_BLUR_BINDINGS: [wgpu::BindingType; 2] = [UNIFORM, UNFILTERED];
+
+/// `HORIZON_HD_AO=0` turns the ambient occlusion off, another number sets its strength.
+fn strength_default() -> f32 {
+ std::env::var("HORIZON_HD_AO")
+ .ok()
+ .and_then(|v| v.parse().ok())
+ .unwrap_or(AO_STRENGTH)
+}
+
+pub(super) struct Ao {
+ /// Its strength in the lighting.
+ pub(super) strength: f32,
+ raw: wgpu::TextureView,
+ pub(super) blurred: wgpu::TextureView,
+ layout: wgpu::BindGroupLayout,
+ blur_layout: wgpu::BindGroupLayout,
+ pipeline: wgpu::RenderPipeline,
+ blur_pipeline: wgpu::RenderPipeline,
+ group: Option<wgpu::BindGroup>,
+ blur_group: Option<wgpu::BindGroup>,
+}
+
+impl Ao {
+ pub(super) fn new(device: &wgpu::Device) -> Self {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD ambient occlusion"),
+ entries: &entries(&AO_BINDINGS),
+ });
+ let blur_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD ambient occlusion blur"),
+ entries: &entries(&AO_BLUR_BINDINGS),
+ });
+ let ao = module(device, "HD ambient occlusion", AO);
+ let ao_blur = module(device, "HD ambient occlusion blur", AO_BLUR);
+ let pipeline = fullscreen(
+ device,
+ "HD ambient occlusion",
+ &layout,
+ &ao,
+ "fs_ao",
+ AO_FORMAT,
+ None,
+ );
+ let blur_pipeline = fullscreen(
+ device,
+ "HD ambient occlusion blur",
+ &blur_layout,
+ &ao_blur,
+ "fs_ao_blur",
+ AO_FORMAT,
+ None,
+ );
+ Self {
+ strength: strength_default(),
+ raw: super::target(device, "HD AO", [1, 1], AO_FORMAT).view,
+ blurred: super::target(device, "HD AO blurred", [1, 1], AO_FORMAT).view,
+ layout,
+ blur_layout,
+ pipeline,
+ blur_pipeline,
+ group: None,
+ blur_group: None,
+ }
+ }
+
+ /// New half-resolution targets for a world of `size`, and the groups that read the G-buffer.
+ pub(super) fn resize(
+ &mut self,
+ device: &wgpu::Device,
+ size: [u32; 2],
+ frame: &wgpu::Buffer,
+ gbuffer: &GBuffer,
+ groups: &AtomicU64,
+ ) {
+ let half = [size[0].div_ceil(2), size[1].div_ceil(2)];
+ self.raw = super::target(device, "HD AO", half, AO_FORMAT).view;
+ self.blurred = super::target(device, "HD AO blurred", half, AO_FORMAT).view;
+ self.group = Some(make_group(
+ groups,
+ device,
+ "HD ambient occlusion",
+ &self.layout,
+ &[
+ wgpu::BindingResource::Buffer(frame.as_entire_buffer_binding()),
+ wgpu::BindingResource::TextureView(&gbuffer.depth),
+ wgpu::BindingResource::TextureView(&gbuffer.normal),
+ ],
+ ));
+ self.blur_group = Some(make_group(
+ groups,
+ device,
+ "HD ambient occlusion blur",
+ &self.blur_layout,
+ &[
+ wgpu::BindingResource::Buffer(frame.as_entire_buffer_binding()),
+ wgpu::BindingResource::TextureView(&self.raw),
+ ],
+ ));
+ }
+}
+
+impl Hd {
+ /// The ambient occlusion from the G-buffer's depth and normals: the half-resolution pass, then
+ /// its blur. Run after the G-buffer pass; nothing when the strength is 0.
+ pub fn ao(&mut self, encoder: &mut wgpu::CommandEncoder) {
+ if self.ao.strength <= 0.0 {
+ return;
+ }
+ let (Some(raw), Some(blur)) = (&self.ao.group, &self.ao.blur_group) else {
+ return;
+ };
+ Self::draw(
+ encoder,
+ "HD ambient occlusion",
+ &self.ao.raw,
+ false,
+ &self.ao.pipeline,
+ raw,
+ stamp(&mut self.timing, "hd ao"),
+ );
+ Self::draw(
+ encoder,
+ "HD ambient occlusion blur",
+ &self.ao.blurred,
+ false,
+ &self.ao.blur_pipeline,
+ blur,
+ stamp(&mut self.timing, "hd ao blur"),
+ );
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/ao.wgsl b/crates/horizon-client/src/gfx/hd/ao.wgsl
new file mode 100644
index 000000000..fc4b2a07c
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/ao.wgsl
@@ -0,0 +1,93 @@
+
+const AO_RADIUS: f32 = 1.8;
+const AO_SLICES: i32 = 3;
+const AO_STEPS: i32 = 5;
+const PI: f32 = 3.14159265;
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var depth_tex: texture_depth_2d;
+@group(0) @binding(2) var normal_tex: texture_2d<f32>;
+
+// Interleaved gradient noise, shifted per frame (params.w) and per use.
+fn noise(px: vec2<f32>, shift: vec2<f32>) -> f32 {
+ let p = px + shift + vec2<f32>(5.588238 * frame.params.w);
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
+}
+
+// The camera-relative position of a (continuous) full-resolution pixel at depth z.
+fn position_at(px: vec2<f32>, z: f32) -> vec3<f32> {
+ let uv = px * frame.resolution.zw;
+ let h = frame.inverse_view_proj * vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
+ return h.xyz / h.w;
+}
+
+// Ground-truth ambient occlusion (horizon based): per slice through the view ray, the highest
+// horizon on either side, integrated against the normal's cosine lobe. Half resolution: one
+// pixel of each 2x2.
+@fragment
+fn fs_ao(in: FullOut) -> @location(0) vec2<f32> {
+ let size = vec2<i32>(textureDimensions(depth_tex));
+ let ic = min(vec2<i32>(in.position.xy) * 2, size - 1);
+ let z = textureLoad(depth_tex, ic, 0);
+ if z <= 0.0 { return vec2<f32>(1.0, 0.0); }
+ let nrm = textureLoad(normal_tex, ic, 0).xyz;
+ let c = vec2<f32>(ic) + 0.5;
+ let p = position_at(c, z);
+ let dist = length(p);
+ if dot(nrm, nrm) < 0.01 { return vec2<f32>(1.0, dist); }
+ let n = normalize(nrm);
+ let v = -p / dist;
+ // Blocks per pixel here (the plane of equal depth is square to the view), so how far in
+ // pixels the radius reaches.
+ let per_px = max(length(position_at(c + vec2<f32>(4.0, 0.0), z) - p) / 4.0, 0.00001);
+ let reach = min(AO_RADIUS / per_px, 56.0);
+ if reach < 2.0 { return vec2<f32>(1.0, dist); }
+ let radius = reach * per_px;
+ let rot = noise(in.position.xy, vec2<f32>(0.0));
+ let jitter = noise(in.position.xy, vec2<f32>(37.0, 17.0));
+ var visibility = 0.0;
+ for (var s = 0; s < AO_SLICES; s++) {
+ let phi = (f32(s) + rot) / f32(AO_SLICES) * PI;
+ let d2 = vec2<f32>(cos(phi), sin(phi));
+ // The slice's direction in space, square to the view ray, and its plane's axis.
+ let q = position_at(c + d2 * 4.0, z) - p;
+ let dir = normalize(q - v * dot(q, v));
+ let axis = cross(dir, v);
+ let projected = n - axis * dot(n, axis);
+ let plen = length(projected);
+ if plen < 0.0001 { visibility += 1.0; continue; }
+ let cos_n = clamp(dot(projected, v) / plen, -1.0, 1.0);
+ let nang = select(-1.0, 1.0, dot(projected, dir) >= 0.0) * acos(cos_n);
+ var c_neg = -1.0;
+ var c_pos = -1.0;
+ for (var i = 0; i < AO_STEPS; i++) {
+ let t = (f32(i) + jitter) / f32(AO_STEPS);
+ let r = 1.5 + (reach - 1.5) * t * t;
+ for (var side = 0; side < 2; side++) {
+ let sg = select(-1.0, 1.0, side == 1);
+ let ip = vec2<i32>(floor(c + d2 * (r * sg)));
+ if any(ip < vec2<i32>(0)) || any(ip >= size) { continue; }
+ let sz = textureLoad(depth_tex, ip, 0);
+ if sz <= 0.0 { continue; }
+ let to = position_at(vec2<f32>(ip) + 0.5, sz) - p;
+ let len = length(to);
+ // Points on (or under) the surface's own plane don't occlude it.
+ if dot(to, n) < 0.1 * len { continue; }
+ // Far points count less, to nothing at the radius.
+ let fall = clamp((len - radius * 0.6) / (radius * 0.4), 0.0, 1.0);
+ let ch = mix(dot(to / len, v), -1.0, fall);
+ if side == 1 { c_pos = max(c_pos, ch); } else { c_neg = max(c_neg, ch); }
+ }
+ }
+ var h_neg = -acos(clamp(c_neg, -1.0, 1.0));
+ var h_pos = acos(clamp(c_pos, -1.0, 1.0));
+ h_neg = nang + max(h_neg - nang, -0.5 * PI);
+ h_pos = nang + min(h_pos - nang, 0.5 * PI);
+ let sn = sin(nang);
+ let arc_neg = cos_n + 2.0 * h_neg * sn - cos(2.0 * h_neg - nang);
+ let arc_pos = cos_n + 2.0 * h_pos * sn - cos(2.0 * h_pos - nang);
+ visibility += plen * 0.25 * (arc_neg + arc_pos);
+ }
+ let ao = clamp(visibility / f32(AO_SLICES), 0.0, 1.0);
+ // Far away it is too coarse to show: fade out.
+ return vec2<f32>(mix(1.0, ao, 1.0 - smoothstep(40.0, 96.0, dist)), dist);
+}
diff --git a/crates/horizon-client/src/gfx/hd/ao_blur.wgsl b/crates/horizon-client/src/gfx/hd/ao_blur.wgsl
new file mode 100644
index 000000000..598e1e1af
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/ao_blur.wgsl
@@ -0,0 +1,28 @@
+
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var raw_ao: texture_2d<f32>;
+
+// A 5x5 blur that only takes texels at about the same distance (so not across an edge).
+@fragment
+fn fs_ao_blur(in: FullOut) -> @location(0) vec2<f32> {
+ let size = vec2<i32>(textureDimensions(raw_ao));
+ let ip = vec2<i32>(in.position.xy);
+ let centre = textureLoad(raw_ao, ip, 0).xy;
+ if centre.y <= 0.0 { return centre; }
+ let tolerance = 0.03 * centre.y + 0.1;
+ var sum = 0.0;
+ var total = 0.0;
+ for (var y = -2; y <= 2; y++) {
+ for (var x = -2; x <= 2; x++) {
+ let q = ip + vec2<i32>(x, y);
+ if any(q < vec2<i32>(0)) || any(q >= size) { continue; }
+ let s = textureLoad(raw_ao, q, 0).xy;
+ if s.y <= 0.0 { continue; }
+ let dz = (s.y - centre.y) / tolerance;
+ let w = 1.0 / (1.0 + dz * dz * dz * dz);
+ sum += s.x * w;
+ total += w;
+ }
+ }
+ return vec2<f32>(sum / max(total, 0.0001), centre.y);
+}
diff --git a/crates/horizon-client/src/gfx/hd/bloom.rs b/crates/horizon-client/src/gfx/hd/bloom.rs
new file mode 100644
index 000000000..6bebaa7c7
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/bloom.rs
@@ -0,0 +1,148 @@
+//! Bloom: a prefilter into level 0, a 13-tap down chain, a tent up chain added back.
+//!
+//! Contract:
+//! - Reads the frame's final HDR colour (`HdSource`: lit/water, the mod's post output or the TAA
+//! result; display-space values, may exceed 1) at the *output* size; `HdFrame.params.z` is the
+//! strength applied later by the tone curve, not here.
+//! - Writes `levels` (`Rgba16Float`), level k at half the output size, shifted k + 1 times, 6
+//! levels. Alpha is unused. The tone curve reads level 0.
+//! - Bind groups: the prefilter's names the source (remade when it changes), the others name the
+//! chain's own levels (remade at `resize`).
+use super::{FLOAT, HDR, SAMPLER, UNIFORM, entries, fullscreen, make_group, module};
+use std::sync::atomic::AtomicU64;
+
+/// Bloom's mip levels.
+pub(super) const BLOOM_LEVELS: usize = 6;
+/// The timed passes of bloom, by level (each is timed on its own; the report sums them).
+pub(super) const BLOOM_NAMES: [&str; 2 * BLOOM_LEVELS] = [
+ "hd bloom#0",
+ "hd bloom#1",
+ "hd bloom#2",
+ "hd bloom#3",
+ "hd bloom#4",
+ "hd bloom#5",
+ "hd bloom#6",
+ "hd bloom#7",
+ "hd bloom#8",
+ "hd bloom#9",
+ "hd bloom#10",
+ "hd bloom#11",
+];
+
+pub(super) const BLOOM: &str = include_str!("bloom.wgsl");
+/// Group 0 of every bloom pass: frame, the source, the linear sampler.
+pub(super) const BLOOM_BINDINGS: [wgpu::BindingType; 3] = [UNIFORM, FLOAT, SAMPLER];
+
+pub(super) struct Bloom {
+ pub(super) levels: Vec<super::Target>,
+ layout: wgpu::BindGroupLayout,
+ pub(super) prefilter: wgpu::RenderPipeline,
+ pub(super) down: wgpu::RenderPipeline,
+ pub(super) up: wgpu::RenderPipeline,
+ pub(super) prefilter_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
+ /// Reading level k - 1 (for the down pass into level k, index k - 1) and level k (for
+ /// the up pass onto level k - 1, index k - 1).
+ pub(super) down_groups: Vec<wgpu::BindGroup>,
+ pub(super) up_groups: Vec<wgpu::BindGroup>,
+}
+
+impl Bloom {
+ pub(super) fn new(device: &wgpu::Device) -> Self {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD bloom"),
+ entries: &entries(&BLOOM_BINDINGS),
+ });
+ let module = module(device, "HD bloom", BLOOM);
+ let add = wgpu::BlendState {
+ color: wgpu::BlendComponent {
+ src_factor: wgpu::BlendFactor::One,
+ dst_factor: wgpu::BlendFactor::One,
+ operation: wgpu::BlendOperation::Add,
+ },
+ alpha: wgpu::BlendComponent::REPLACE,
+ };
+ let prefilter = fullscreen(
+ device,
+ "HD bloom prefilter",
+ &layout,
+ &module,
+ "fs_prefilter",
+ HDR,
+ None,
+ );
+ let down = fullscreen(
+ device,
+ "HD bloom down",
+ &layout,
+ &module,
+ "fs_down",
+ HDR,
+ None,
+ );
+ let up = fullscreen(
+ device,
+ "HD bloom up",
+ &layout,
+ &module,
+ "fs_up",
+ HDR,
+ Some(add),
+ );
+ Self {
+ levels: Vec::new(),
+ layout,
+ prefilter,
+ down,
+ up,
+ prefilter_group: None,
+ down_groups: Vec::new(),
+ up_groups: Vec::new(),
+ }
+ }
+
+ /// A bind group reading `source`, counted in `groups`.
+ pub(super) fn group(
+ &self,
+ device: &wgpu::Device,
+ groups: &AtomicU64,
+ frame: &wgpu::Buffer,
+ sampler: &wgpu::Sampler,
+ source: &wgpu::TextureView,
+ ) -> wgpu::BindGroup {
+ make_group(
+ groups,
+ device,
+ "HD bloom",
+ &self.layout,
+ &[
+ wgpu::BindingResource::Buffer(frame.as_entire_buffer_binding()),
+ wgpu::BindingResource::TextureView(source),
+ wgpu::BindingResource::Sampler(sampler),
+ ],
+ )
+ }
+
+ /// The chain for an output of `out` and the groups that read its levels.
+ pub(super) fn resize(
+ &mut self,
+ device: &wgpu::Device,
+ out: [u32; 2],
+ frame: &wgpu::Buffer,
+ sampler: &wgpu::Sampler,
+ groups: &AtomicU64,
+ ) {
+ self.levels = (0..BLOOM_LEVELS)
+ .map(|k| {
+ let s = [(out[0] >> (k + 1)).max(1), (out[1] >> (k + 1)).max(1)];
+ super::target(device, "HD bloom", s, HDR)
+ })
+ .collect();
+ self.prefilter_group = None;
+ self.down_groups = (1..self.levels.len())
+ .map(|k| self.group(device, groups, frame, sampler, &self.levels[k - 1].view))
+ .collect();
+ self.up_groups = (1..self.levels.len())
+ .map(|k| self.group(device, groups, frame, sampler, &self.levels[k].view))
+ .collect();
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/bloom.wgsl b/crates/horizon-client/src/gfx/hd/bloom.wgsl
new file mode 100644
index 000000000..5db8405a2
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/bloom.wgsl
@@ -0,0 +1,52 @@
+
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var source: texture_2d<f32>;
+@group(0) @binding(2) var linear: sampler;
+
+// The first step down keeps only what is brighter than white (a soft knee).
+@fragment
+fn fs_prefilter(in: FullOut) -> @location(0) vec4<f32> {
+ let c = textureSampleLevel(source, linear, in.uv, 0.0).rgb;
+ let peak = max(c.r, max(c.g, c.b));
+ let knee = 0.35;
+ let soft = clamp(peak - 0.85 + knee, 0.0, 2.0 * knee);
+ let weight = max(soft * soft / (4.0 * knee + 0.0001), peak - 0.85) / max(peak, 0.0001);
+ return vec4<f32>(c * weight, 1.0);
+}
+// 13 taps (Call of Duty: Advanced Warfare's bloom), half size each step.
+@fragment
+fn fs_down(in: FullOut) -> @location(0) vec4<f32> {
+ let t = 1.0 / vec2<f32>(textureDimensions(source));
+ let s = in.uv;
+ let a = textureSampleLevel(source, linear, s + t * vec2(-2.0, -2.0), 0.0).rgb;
+ let b = textureSampleLevel(source, linear, s + t * vec2(0.0, -2.0), 0.0).rgb;
+ let c = textureSampleLevel(source, linear, s + t * vec2(2.0, -2.0), 0.0).rgb;
+ let d = textureSampleLevel(source, linear, s + t * vec2(-2.0, 0.0), 0.0).rgb;
+ let e = textureSampleLevel(source, linear, s, 0.0).rgb;
+ let f = textureSampleLevel(source, linear, s + t * vec2(2.0, 0.0), 0.0).rgb;
+ let g = textureSampleLevel(source, linear, s + t * vec2(-2.0, 2.0), 0.0).rgb;
+ let h = textureSampleLevel(source, linear, s + t * vec2(0.0, 2.0), 0.0).rgb;
+ let i = textureSampleLevel(source, linear, s + t * vec2(2.0, 2.0), 0.0).rgb;
+ let j = textureSampleLevel(source, linear, s + t * vec2(-1.0, -1.0), 0.0).rgb;
+ let k = textureSampleLevel(source, linear, s + t * vec2(1.0, -1.0), 0.0).rgb;
+ let l = textureSampleLevel(source, linear, s + t * vec2(-1.0, 1.0), 0.0).rgb;
+ let m = textureSampleLevel(source, linear, s + t * vec2(1.0, 1.0), 0.0).rgb;
+ var o = e * 0.125 + (a + c + g + i) * 0.03125 + (b + d + f + h) * 0.0625 + (j + k + l + m) * 0.125;
+ return vec4<f32>(o, 1.0);
+}
+// A 3×3 tent, added onto the bigger level.
+@fragment
+fn fs_up(in: FullOut) -> @location(0) vec4<f32> {
+ let t = 1.0 / vec2<f32>(textureDimensions(source));
+ let s = in.uv;
+ var o = textureSampleLevel(source, linear, s, 0.0).rgb * 4.0;
+ o += (textureSampleLevel(source, linear, s + t * vec2(-1.0, 0.0), 0.0).rgb
+ + textureSampleLevel(source, linear, s + t * vec2(1.0, 0.0), 0.0).rgb
+ + textureSampleLevel(source, linear, s + t * vec2(0.0, -1.0), 0.0).rgb
+ + textureSampleLevel(source, linear, s + t * vec2(0.0, 1.0), 0.0).rgb) * 2.0;
+ o += textureSampleLevel(source, linear, s + t * vec2(-1.0, -1.0), 0.0).rgb
+ + textureSampleLevel(source, linear, s + t * vec2(1.0, -1.0), 0.0).rgb
+ + textureSampleLevel(source, linear, s + t * vec2(-1.0, 1.0), 0.0).rgb
+ + textureSampleLevel(source, linear, s + t * vec2(1.0, 1.0), 0.0).rgb;
+ return vec4<f32>(o / 16.0, 1.0);
+}
diff --git a/crates/horizon-client/src/gfx/hd/cloud.wgsl b/crates/horizon-client/src/gfx/hd/cloud.wgsl
new file mode 100644
index 000000000..a3c0fffae
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/cloud.wgsl
@@ -0,0 +1,146 @@
+
+@group(0) @binding(6) var history: texture_2d<f32>;
+
+fn ign(px: vec2<f32>) -> f32 {
+ let p = px + vec2<f32>(5.588238 * frame.params.w);
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
+}
+
+fn hg(c: f32, g: f32) -> f32 {
+ let gg = g * g;
+ return (1.0 - gg) / (4.0 * CPI * pow(1.0 + gg - 2.0 * g * c, 1.5));
+}
+fn klein_nishina(nu: f32, e: f32) -> f32 {
+ return e / (2.0 * CPI * (e - e * nu + 1.0) * log(2.0 * e + 1.0));
+}
+// Photon's single scattering phase: a glow close to the sun, a forward lobe and a back lobe.
+fn phase_single(c: f32) -> f32 {
+ let a = klein_nishina(c, 2600.0);
+ let b = hg(c, 0.8);
+ return 0.8 * max(a, b) + 0.2 * hg(c, -0.2);
+}
+fn phase_multi(c: f32, g: vec3<f32>) -> f32 {
+ return 0.65 * hg(c, g.x) + 0.10 * hg(c, g.y) + 0.25 * hg(c, -g.z);
+}
+fn powder_effect(density: f32, c: f32) -> f32 {
+ let p = CPI * density / (density + 0.15);
+ let k = c * 0.5 + 0.5;
+ return mix(p, 1.0, 0.8 * k * k);
+}
+
+// Photon's multiple scattering octaves: each one scatters a part of the light again with a wider
+// phase, less extinction and the powder effect easing off. x: sunlight, y: skylight.
+fn cloud_scatter(
+ density: f32,
+ light_od: f32,
+ sky_od: f32,
+ ground_od: f32,
+ ext: f32,
+ sca: f32,
+ step_t: f32,
+ cos_theta: f32,
+ bounced: f32,
+) -> vec2<f32> {
+ var s = vec2<f32>(0.0);
+ var scatter_amount = sca;
+ var extinct_amount = ext;
+ let integral = (1.0 - step_t) / ext;
+ var powder = powder_effect(density, cos_theta);
+ var phase = phase_single(cos_theta);
+ var phase_g = pow(vec3<f32>(0.6, 0.9, 0.3), vec3<f32>(1.0 + light_od));
+ let iso = 1.0 / (4.0 * CPI);
+ for (var i = 0; i < 4; i++) {
+ s.x += scatter_amount * exp(-extinct_amount * light_od) * phase;
+ s.x += scatter_amount * exp(-extinct_amount * ground_od) * iso * bounced;
+ s.y += scatter_amount * exp(-extinct_amount * sky_od) * iso;
+ let lift = pow(clamp(sca / 0.1, 0.0, 1.0), 0.33);
+ scatter_amount *= 0.55 * mix(lift, 1.0, cos_theta * 0.5 + 0.5) * powder;
+ extinct_amount *= 0.4;
+ phase_g *= 0.8;
+ powder = mix(powder, sqrt(powder), 0.5);
+ phase = phase_multi(cos_theta, phase_g);
+ }
+ return s * integral;
+}
+
+@fragment
+fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
+ 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);
+ var cur = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ 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;
+ let lo = max(min(ta, tb), 0.0);
+ let hi = min(max(ta, tb), 4200.0);
+ if hi > lo {
+ let dither = ign(in.position.xy);
+ let n = clamp(i32(ceil((hi - lo) / 14.0)), 8, 26);
+ let step_len = (hi - lo) / f32(n);
+ let light = frame.cloud_dir.xyz;
+ let cos_theta = dot(dir, light);
+ let overcast = frame.sky_extra.z;
+ let ext = CLOUD_EXT * (1.0 - 0.25 * overcast);
+ let sca = ext;
+ let bounced = 0.4 * max(light.y, 0.0) / CPI;
+ var scatter = vec2<f32>(0.0);
+ var t_acc = 1.0;
+ var dist_sum = 0.0;
+ var weight_sum = 0.0;
+ for (var i = 0; i < n; i++) {
+ if t_acc < 0.075 { break; }
+ let t = lo + step_len * (f32(i) + dither);
+ let pos = dir * t;
+ var d = cloud_density(pos, 1.0);
+ if d < 0.002 { continue; }
+ d *= 1.0 - smoothstep(0.8, 1.0, t / 4200.0);
+ let step_od = d * ext * step_len;
+ let step_t = exp(-step_od);
+ let h = clamp((pos.y - y0) / frame.clouds.y, 0.0, 1.0);
+ let hash = fract(dither * 7.13 + f32(i) * 0.618);
+ let light_od = cloud_light_depth(pos, light, 3, hash, 1.0);
+ let sky_od = cloud_light_depth(pos, vec3<f32>(0.0, 1.0, 0.0), 1, 1.0 - hash, 0.0);
+ // A guess of the optical depth to the ground from the altitude and this density.
+ let ground_od = mix(d, 1.0, clamp(h * 2.0 - 1.0, 0.0, 1.0)) * h * frame.clouds.y;
+ scatter += cloud_scatter(d, light_od, sky_od, ground_od, ext, sca, step_t, cos_theta, bounced) * t_acc;
+ t_acc *= step_t;
+ dist_sum += t * d;
+ weight_sum += d;
+ }
+ if weight_sum > 0.0 { hit_t = dist_sum / weight_sum; }
+ // The sunlight's colour at this hour (Photon: the light's colour from the transmittance),
+ // the sky's own light from the zenith and the horizon.
+ let sun_light = frame.cloud_sun.rgb * frame.cloud_sun.w * (1.0 - overcast);
+ let zenith = sky_show(textureLoad(sky_lut, vec2<i32>(0, 64), 0).rgb);
+ let horizon = sky_show(textureLoad(sky_lut, vec2<i32>(1, 64), 0).rgb);
+ let ambient = mix(horizon, zenith, 0.55);
+ var colour = scatter.x * sun_light * 3.2 + scatter.y * ambient * 5.0;
+ // The remap of Photon: the transmittance's floor is 0.
+ let trans = linear_step(0.075, 1.0, t_acc);
+ // Aerial perspective: far clouds fade into the haze.
+ let haze = haze_colour(dir);
+ let air = exp(-hit_t / 3600.0);
+ colour = mix((1.0 - trans) * haze, colour, air);
+ cur = vec4<f32>(colour, trans);
+ }
+ }
+ // 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);
+ if hp.w > 0.0 {
+ 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);
+ // 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));
+ cur = mix(cur, old, keep);
+ }
+ }
+ }
+ return cur;
+}
diff --git a/crates/horizon-client/src/gfx/hd/cloud_common.wgsl b/crates/horizon-client/src/gfx/hd/cloud_common.wgsl
new file mode 100644
index 000000000..393ea564b
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/cloud_common.wgsl
@@ -0,0 +1,71 @@
+
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var noise3: texture_3d<f32>;
+@group(0) @binding(2) var noise2: texture_2d<f32>;
+@group(0) @binding(3) var linear: sampler;
+@group(0) @binding(4) var repeat_s: sampler;
+//SKY5
+
+const CPI: f32 = 3.14159265;
+// Extinction per block at full density, how fast the wind blows (blocks per second).
+const CLOUD_EXT: f32 = 0.22;
+const CLOUD_WIND: f32 = 3.0;
+
+fn linear_step(a: f32, b: f32, x: f32) -> f32 {
+ return clamp((x - a) / (b - a), 0.0, 1.0);
+}
+
+fn dampen(x: f32) -> f32 {
+ let t = clamp(x, 0.0, 1.0);
+ return t * (2.0 - t);
+}
+
+// Where a camera-relative point lies in the world's cloud pattern (the wind moves the pattern).
+fn cloud_xz(pos: vec3<f32>) -> vec2<f32> {
+ return vec2<f32>(pos.x + frame.cloud_cam.x, pos.z + frame.cloud_cam.y)
+ + vec2<f32>(0.8, 0.6) * (CLOUD_WIND * frame.clouds.w);
+}
+
+// The cloud density at a camera-relative point (0 outside the slab), in 0..1. Photon's cumulus
+// shaping: a coverage field and a shape field cut by a threshold, an egg shape over the altitude,
+// Worley erosion (large and small) and sharper edges at the top than at the bottom.
+fn cloud_density(pos: vec3<f32>, detail: f32) -> f32 {
+ let h = (pos.y - frame.clouds.x) / frame.clouds.y;
+ if h <= 0.0 || h >= 1.0 { return 0.0; }
+ let w = cloud_xz(pos);
+ let cover = textureSampleLevel(noise2, repeat_s, w / 16000.0, 0.0).r;
+ let shape = textureSampleLevel(noise2, repeat_s, w / 3000.0, 0.0).g;
+ let cu = mix(0.56, 0.80, cover) + frame.cloud_cam.z;
+ var d = linear_step(1.0 - cu, 1.0, shape);
+ // Carve an egg shape, thin out the bottom.
+ d -= smoothstep(0.2, 1.0, h) * 0.6;
+ 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);
+ let warp = (textureSampleLevel(noise3, repeat_s, p3 / 1100.0, 0.0).b - 0.5) * 0.4;
+ let w0 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 420.0 + vec3<f32>(warp), 0.0).r;
+ let w1 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 90.0 + vec3<f32>(warp * 3.0), 0.0).g;
+ let fade = 0.20 * smoothstep(0.85, 1.0, 1.0 - h) - 0.35 * smoothstep(0.05, 0.5, h) + 0.6;
+ d -= 0.55 * w0 * w0 * dampen(1.0 - d);
+ d -= 0.45 * w1 * w1 * dampen(1.0 - d) * fade;
+ }
+ d = max(d, 0.0);
+ d = 1.0 - pow(1.0 - d, mix(2.2, 5.0, h));
+ d *= 0.1 + 0.9 * smoothstep(0.2, 0.7, h);
+ return d;
+}
+
+// The optical depth from `p` along `dir` through the slab: a few steps that double in length
+// (Photon's light march), the sample dithered inside its step.
+fn cloud_light_depth(p: vec3<f32>, dir: vec3<f32>, steps: i32, dither: f32, detail: f32) -> f32 {
+ var len = 0.1 * frame.clouds.y / f32(steps);
+ var t = 0.0;
+ var od = 0.0;
+ for (var i = 0; i < steps; i++) {
+ len *= 2.0;
+ od += cloud_density(p + dir * (t + len * dither), detail) * len;
+ t += len;
+ }
+ return od;
+}
diff --git a/crates/horizon-client/src/gfx/hd/cloud_shadow.wgsl b/crates/horizon-client/src/gfx/hd/cloud_shadow.wgsl
new file mode 100644
index 000000000..8fca87fba
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/cloud_shadow.wgsl
@@ -0,0 +1,18 @@
+
+@fragment
+fn fs_cloud_shadow(in: FullOut) -> @location(0) vec4<f32> {
+ // A square of the cloud base plane round the camera; a texel holds the optical depth of the
+ // sunlight's way through the slab from that point.
+ let q = (in.uv - vec2<f32>(0.5)) * (2.0 * SHADOW_R);
+ let light = frame.cloud_dir.xyz;
+ let ly = max(light.y, 0.15);
+ let base = vec3<f32>(q.x, frame.clouds.x + 0.5, q.y);
+ let len = frame.clouds.y / ly;
+ var od = 0.0;
+ for (var i = 0; i < 4; i++) {
+ let t = (f32(i) + 0.5) / 4.0 * len;
+ od += cloud_density(base + light * t, 0.0);
+ }
+ od *= CLOUD_EXT * len / 4.0 * (1.0 - 0.25 * frame.sky_extra.z);
+ return vec4<f32>(od, 0.0, 0.0, 1.0);
+}
diff --git a/crates/horizon-client/src/gfx/hd/fog.wgsl b/crates/horizon-client/src/gfx/hd/fog.wgsl
new file mode 100644
index 000000000..8979534d7
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/fog.wgsl
@@ -0,0 +1,124 @@
+
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var scene_depth: texture_depth_2d;
+@group(0) @binding(2) var shadow_map: texture_depth_2d_array;
+@group(0) @binding(3) var shadow_sampler: sampler_comparison;
+@group(0) @binding(4) var noise3: texture_3d<f32>;
+@group(0) @binding(5) var repeat_s: sampler;
+@group(0) @binding(6) var linear: sampler;
+//SKY7
+
+const FOG_MAX: f32 = 192.0;
+const FOG_STEPS: i32 = 18;
+
+struct FogOut {
+ @location(0) fog: vec4<f32>,
+ @location(1) dist: vec4<f32>,
+}
+
+fn ign(px: vec2<f32>) -> f32 {
+ let p = px + vec2<f32>(5.588238 * frame.params.w);
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
+}
+
+// Henyey-Greenstein, normalised to 1 for an isotropic one (so the light is its strength times it).
+fn hgn(c: f32, g: f32) -> f32 {
+ let gg = g * g;
+ return (1.0 - gg) / pow(1.0 + gg - 2.0 * g * c, 1.5);
+}
+
+// The sun's light reaching a camera-relative point: 0 in shadow, 1 lit (the near cascade close
+// by, the far one beyond; outside both, lit).
+fn sun_visible(p: vec3<f32>) -> f32 {
+ let near = length(p) < frame.shadow.y * 0.9;
+ let c = select(1, 0, near);
+ let s = frame.cascades[c] * vec4<f32>(p, 1.0);
+ let uv = vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
+ if any(uv.xy < vec2<f32>(0.0)) || any(uv.xy > vec2<f32>(1.0)) || uv.z > 1.0 { return 1.0; }
+ return textureSampleCompareLevel(shadow_map, shadow_sampler, uv.xy, c, uv.z - select(0.0012, 0.0007, near));
+}
+
+// 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
+// deep underground.
+fn fog_sigma(p: vec3<f32>) -> f32 {
+ let wy = p.y + frame.vfog.y;
+ let h = wy - 62.0;
+ var s = 0.0016 * exp(clamp(-h / 42.0, -6.0, 1.3));
+ s += frame.vfog.z * 0.011 * exp(clamp(-h / 15.0, -6.0, 1.0));
+ 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 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);
+}
+
+@fragment
+fn fs_fog(in: FullOut) -> FogOut {
+ // The nearest of the four world pixels this texel stands for (so edges keep the near depth).
+ let size = vec2<i32>(frame.resolution.xy);
+ let base = vec2<i32>(in.position.xy) * 2;
+ var d = 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 > d {
+ d = z;
+ best = q;
+ }
+ }
+ let uv = (vec2<f32>(best) + 0.5) * frame.resolution.zw;
+ let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, max(d, 0.000001), 1.0);
+ let h = frame.inverse_view_proj * ndc;
+ let p = h.xyz / h.w;
+ let dir = normalize(p);
+ let len = select(FOG_MAX, min(length(p), FOG_MAX), d > 0.0);
+ var out: FogOut;
+ out.dist = vec4<f32>(len, 0.0, 0.0, 0.0);
+ if frame.vfog.x < 0.5 {
+ out.fog = vec4<f32>(0.0, 0.0, 0.0, 1.0);
+ return out;
+ }
+ let sun_on = frame.sun.w;
+ let sun_light = frame.sun_color.rgb * sun_on;
+ let cos_v = dot(dir, frame.sun.xyz);
+ // Photon's mix of two phase functions for the aerosols, as its air fog does.
+ let phase = 0.7 * hgn(cos_v, 0.55) + 0.3 * hgn(cos_v, -0.2);
+ let haze = haze_colour(dir);
+ let jitter = ign(in.position.xy);
+ var trans = 1.0;
+ var light = vec3<f32>(0.0);
+ var lit_sum = 0.0;
+ for (var i = 0; i < FOG_STEPS; i++) {
+ let u0 = (f32(i) + jitter) / f32(FOG_STEPS);
+ let u1 = (f32(i) + 1.0 + jitter) / f32(FOG_STEPS);
+ let f0 = 0.5 * u0 + 0.5 * u0 * u0;
+ let f1 = 0.5 * u1 + 0.5 * u1 * u1;
+ let t = len * f0;
+ let ds = len * (f1 - f0);
+ let pos = dir * (t + 0.5 * ds);
+ let sigma = fog_sigma(pos);
+ let a = 1.0 - exp(-sigma * ds);
+ var vis = 1.0;
+ if sun_on > 0.0 { vis = sun_visible(pos); }
+ lit_sum += vis;
+ // The sky's light fills the shade less than the open.
+ let ambient = haze * (0.2 + 0.8 * (0.35 + 0.65 * vis));
+ light += trans * a * (sun_light * vis * phase * FOG_SUN + ambient * FOG_AMBIENT);
+ trans *= 1.0 - a;
+ }
+ // Photon brightens the fog a little in the early morning and the evening.
+ let sy = frame.sky_sun.y + 0.02;
+ let glow = 1.0 + 2.0 * linear_step(0.05, 1.0, exp(-300.0 * sy * sy));
+ out.fog = vec4<f32>(light * glow, trans);
+ return out;
+}
+
+const FOG_SUN: f32 = 1.1;
+const FOG_AMBIENT: f32 = 0.9;
+fn linear_step(a: f32, b: f32, x: f32) -> f32 {
+ return clamp((x - a) / (b - a), 0.0, 1.0);
+}
diff --git a/crates/horizon-client/src/gfx/hd/frame.rs b/crates/horizon-client/src/gfx/hd/frame.rs
new file mode 100644
index 000000000..9d4dfabdb
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/frame.rs
@@ -0,0 +1,205 @@
+//! The frame's data every pass reads: the uniform block (`HdFrame`, mirrored by the WGSL prelude),
+//! what the renderer hands HD each frame (`HdScene`, `HdLight`, `HdSettings`) and the light
+//! the scene implies (sun or moon, cloud light).
+//!
+//! Contract of `HdFrame` (Rust and `prelude.wgsl` must list the same fields in the same order; a
+//! test checks the size):
+//! - Positions are camera-relative (the camera is the origin; the world is `f64` on the CPU).
+//! `inverse_view_proj` maps the *jittered* clip space of the world target to such a position.
+//! - Depth is reversed-Z (`Depth32Float`, 1 near, 0 far / nothing).
+//! - Colour: the whole pipeline works in vanilla's display (gamma-encoded, Rec. 709 primaries) space,
+//! as "HDR" in the sense that values may exceed 1 (`Rgba16Float`); the tone curve leaves up to ~0.8
+//! untouched. The sky texture is the exception: linear Rec. 709 radiance (marched in Rec. 2020),
+//! put into display space by the readers (`sky.rs`, `sky_funcs.wgsl`).
+//! - Time: `params.w` is a frame counter modulo 64 (noise rotation), `water.y` is wall-clock seconds
+//! modulo 600 (waves), `clouds.w` is game seconds modulo 300000 (cloud wind). They never mix.
+
+use glam::Mat4;
+
+/// The shared WGSL prelude: `HdFrame`, `FullOut` and the full-screen vertex shader. Every HD shader
+/// is this followed by its own body (see `assemble`).
+pub(super) const PRELUDE: &str = include_str!("prelude.wgsl");
+
+/// A light for this frame, relative to the camera.
+#[repr(C)]
+#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct HdLight {
+ /// xyz: camera-relative position; w: radius.
+ pub pos: [f32; 4],
+ /// rgb: colour; w: intensity.
+ pub color: [f32; 4],
+}
+
+/// The frame's HD settings.
+#[derive(Clone, Copy, Debug)]
+pub struct HdSettings {
+ pub exposure: f32,
+ pub bloom: f32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+pub(super) struct HdFrame {
+ /// Clip space to camera-relative position.
+ pub(super) inverse_view_proj: [[f32; 4]; 4],
+ /// width, height, 1/width, 1/height.
+ pub(super) resolution: [f32; 4],
+ /// lights, exposure, bloom, a frame counter.
+ pub(super) params: [f32; 4],
+ /// The shadow cascades: camera-relative position to the light's clip space.
+ pub(super) cascades: [[[f32; 4]; 4]; 2],
+ /// xyz: towards the sun (or the moon at night); w: how much it lights (0 set or hidden).
+ pub(super) sun: [f32; 4],
+ pub(super) sun_color: [f32; 4],
+ pub(super) fog_color: [f32; 4],
+ /// Environmental start, end; render-distance start, end.
+ pub(super) fog: [f32; 4],
+ /// x: sky light kept in the shade; y: blocks' light; z: the air's density (rays, halos);
+ /// w: the ambient occlusion's strength (0 off).
+ pub(super) ambient: [f32; 4],
+ /// x: shadows on; y, z: the cascades' half sizes; w: the light tiles per row.
+ pub(super) shadow: [f32; 4],
+ /// The previous frame's camera-relative view-projection without jitter (for TAA).
+ pub(super) prev_view_proj: [[f32; 4]; 4],
+ /// xyz: how far the camera moved since that frame; w: 1 when the history is usable.
+ pub(super) taa: [f32; 4],
+ /// x: the history's weight; y: the sharpening (0 without TAA); zw: the jitter in world pixels.
+ pub(super) taa_params: [f32; 4],
+ /// The (jittered) world-to-clip matrix the world was drawn with (rays in the water pass).
+ pub(super) view_proj: [[f32; 4]; 4],
+ /// x: the water effect runs this frame; y: seconds for the waves (wraps at 600); zw: the
+ /// camera's x and z modulo 256, the waves' period.
+ pub(super) water: [f32; 4],
+ /// rgb: the sky's colour; w: 1 when there is a sky.
+ pub(super) sky_color: [f32; 4],
+ /// xyz: towards the sun itself (not the moon); w: 1 when the atmosphere draws the sky.
+ pub(super) sky_sun: [f32; 4],
+ /// x: vanilla's sky fog end; z: how overcast the sky is (rain: 0 clear, 1 vanilla's colours).
+ pub(super) sky_extra: [f32; 4],
+ /// Vanilla's sunrise colour fan (rgb, alpha), taken out of the sky before the atmosphere is put in.
+ pub(super) sunrise: [f32; 4],
+ /// x: the cloud slab's base height relative to the camera; y: its thickness; z: 1 when the
+ /// volumetric clouds are drawn; w: seconds (the wind).
+ pub(super) clouds: [f32; 4],
+ /// xy: the camera's x and z in the world modulo 65536; z: extra coverage (rain); w: 1 when the
+ /// clouds' history is usable.
+ pub(super) cloud_cam: [f32; 4],
+ /// xyz: the direction the clouds are lit from (the sun, or the moon at night).
+ pub(super) cloud_dir: [f32; 4],
+ /// rgb: the colour of that light at the clouds; w: its strength.
+ pub(super) cloud_sun: [f32; 4],
+ /// x: 1 when the volumetric fog is drawn; y: the camera's height in the world; z: the morning mist.
+ pub(super) vfog: [f32; 4],
+}
+
+/// What the world around looks like this frame (from the renderer's globals).
+#[derive(Clone, Copy, Debug)]
+pub struct HdScene {
+ /// The shadow cascades' matrices (`cascades`; the far one may be a frame or two old).
+ pub cascades: [Mat4; 2],
+ /// The (jittered) projection the world was drawn with.
+ pub view_proj: Mat4,
+ /// Last frame's view-projection without jitter, and the camera's move since (TAA).
+ pub prev_view_proj: Mat4,
+ /// The same frame's matrix as it was drawn, jitter included. History kept on the jittered
+ /// pixel grid (`fs_taa`, scale 1) is looked up with this; `fs_taau` resolves the history on the
+ /// unjittered output grid and uses `prev_view_proj`.
+ pub prev_view_proj_jittered: Mat4,
+ 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,
+ pub rain: f32,
+ pub fog_color: [f32; 4],
+ pub fog: [f32; 4],
+ /// The sky has a sun at all (the Overworld).
+ pub sky: bool,
+ pub sky_color: [f32; 3],
+ /// The camera's x and z modulo 256 (the waves' period), so the waves hold still in the world.
+ pub camera_mod: [f32; 2],
+ /// The water effect may run (not underwater, not disabled).
+ pub water: bool,
+ /// Vanilla's sunrise fan colour and its sky fog end (to subtract vanilla's sky from the world).
+ pub sunrise: [f32; 4],
+ pub sky_fog_end: f32,
+ /// The volumetric clouds are drawn (vanilla's option is on, in the Overworld).
+ pub clouds: bool,
+ /// The camera's world position, x and z modulo 65536 (precision), and y; seconds of game time.
+ pub camera_world: [f32; 3],
+ pub seconds: f32,
+}
+
+/// The light that casts the shadows: towards the sun by day, the moon by night; how strong
+/// (0 below the horizon or without a sky) and its colour (warm low, white high, cold moon).
+pub fn light_of(scene: &HdScene) -> (glam::Vec3, f32, glam::Vec3) {
+ if !scene.sky {
+ return (glam::Vec3::Y, 0.0, glam::Vec3::ZERO);
+ }
+ let sun = scene.sun.normalize_or(glam::Vec3::Y);
+ let clear = 1.0 - scene.rain * 0.85;
+ if sun.y > -0.02 {
+ let up = smooth(-0.02, 0.18, sun.y);
+ // The atmosphere's transmittance along the sun's ray (orange when low, a clean warm white
+ // high), kept at full brightness and tempered, with a slight warm tint on top.
+ let t = super::sky::sun_transmittance(sun.y);
+ let hue = t / t.max_element().max(1e-4);
+ let color = glam::Vec3::new(hue.x.powf(0.6), hue.y.powf(0.6), hue.z.powf(0.6))
+ * glam::Vec3::new(1.0, 0.93, 0.78)
+ * super::sky::sun_tint(sun.y);
+ (sun, 0.7 * up * clear, color)
+ } else {
+ let moon = -sun;
+ let up = smooth(-0.02, 0.18, moon.y);
+ (moon, 0.15 * up * clear, glam::Vec3::new(0.55, 0.66, 1.0))
+ }
+}
+
+/// The factor on vanilla's cloud colour that lights the clouds with the low sun's colour at dusk
+/// and dawn instead of a flat white. It is the clouds' own colour (the cloud pass gets it), not a
+/// guess from the depth buffer, so what stands behind a cloud cannot change it.
+pub fn vanilla_cloud_tint(scene: &HdScene) -> [f32; 3] {
+ let sun = scene.sun.normalize_or(glam::Vec3::Y);
+ let low = 1.0 - smooth(0.05, 0.5, sun.y);
+ let overcast = (scene.rain / 0.7).clamp(0.0, 1.0) * 0.85;
+ let (_, _, color) = light_of(scene);
+ let warm = color.lerp(glam::Vec3::ONE, 0.45) * 1.1;
+ let t = low * 0.7 * (1.0 - overcast);
+ let tint = glam::Vec3::ONE.lerp(warm, t);
+ [tint.x, tint.y, tint.z]
+}
+
+/// The x of the sun's direction: positive in the morning, negative in the evening.
+pub(super) fn sun_dir_x(scene: &HdScene) -> f32 {
+ scene.sun.normalize_or(glam::Vec3::Y).x
+}
+
+/// What lights the clouds: the direction, the colour and the strength. The sun's colour from the
+/// atmosphere's transmittance (as `light_of`, with Photon's evening tint), kept lighting the clouds
+/// a little past the sunset (the clouds stand high), the moon's cold light at night.
+pub fn cloud_light(scene: &HdScene) -> (glam::Vec3, glam::Vec3, f32) {
+ let sun = scene.sun.normalize_or(glam::Vec3::Y);
+ let clear = 1.0 - scene.rain * 0.85;
+ if sun.y > -0.2 {
+ let t = super::sky::sun_transmittance(sun.y.max(0.01));
+ let hue = t / t.max_element().max(1e-4);
+ let color = glam::Vec3::new(hue.x.powf(0.6), hue.y.powf(0.6), hue.z.powf(0.6))
+ * super::sky::sun_tint(sun.y);
+ (sun, color, smooth(-0.2, 0.08, sun.y) * clear)
+ } else {
+ (
+ -sun,
+ glam::Vec3::new(0.55, 0.66, 1.0),
+ 0.22 * smooth(-0.2, -0.45, sun.y) * clear,
+ )
+ }
+}
+
+pub(super) fn smooth(a: f32, b: f32, x: f32) -> f32 {
+ let t = ((x - a) / (b - a)).clamp(0.0, 1.0);
+ t * t * (3.0 - 2.0 * t)
+}
diff --git a/crates/horizon-client/src/gfx/hd/gbuffer.rs b/crates/horizon-client/src/gfx/hd/gbuffer.rs
new file mode 100644
index 000000000..16db359f5
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/gbuffer.rs
@@ -0,0 +1,81 @@
+//! The G-buffer: the solid world (terrain, far LOD tiles, entities and mod models) drawn once more
+//! before the world pass, with its own depth. Translucent things do not enter it.
+//!
+//! Contract (all at the world size, jitter included in the projection):
+//! - `albedo` (`Rgba8Unorm`): rgb surface colour without light, in the display (gamma) space the
+//! vanilla textures use; **a = emission** (0 none; the lighting adds `rgb * a * 1.5` on top).
+//! - `normal` (`Rgba16Float`): rgb world-space unit normal (from `dpdx/dpdy`; plants use up), a unused.
+//! - `levels` (`Rgba8Unorm`): x = sky light, y = block light, both the raw lightmap coordinate / 240
+//! (0..1); zw unused.
+//! - `depth` (`Depth32Float`): camera depth, **reversed-Z** (cleared to 0 = far / nothing, 1 = near).
+//! An untouched pixel (depth 0) is sky.
+//! - Readers: AO, lighting, water composite, TAA/TAAU (depth). Written by `gbuffer_pass`; the caller
+//! draws into it with `fs_gbuffer` / `fs_entity_gbuffer`.
+use super::{Hd, stamp, target};
+
+/// The G-buffer's formats.
+pub const ALBEDO: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
+pub const NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
+/// Sky and block light per pixel.
+pub const LIGHT_LEVELS: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
+/// The camera depth formats (the G-buffer's and the water's own).
+pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
+
+pub(super) struct GBuffer {
+ pub(super) albedo: wgpu::TextureView,
+ pub(super) normal: wgpu::TextureView,
+ pub(super) levels: wgpu::TextureView,
+ pub(super) depth: wgpu::TextureView,
+}
+
+impl GBuffer {
+ /// The targets at `size` (the world's).
+ pub(super) fn new(device: &wgpu::Device, size: [u32; 2]) -> Self {
+ Self {
+ albedo: target(device, "HD albedo", size, ALBEDO).view,
+ normal: target(device, "HD normal", size, NORMAL).view,
+ levels: target(device, "HD light levels", size, LIGHT_LEVELS).view,
+ depth: target(device, "HD depth", size, DEPTH_FORMAT).view,
+ }
+ }
+}
+
+impl Hd {
+ /// The G-buffer pass's attachments: albedo and normal cleared, its own depth.
+ pub fn gbuffer_pass<'e>(
+ &mut self,
+ encoder: &'e mut wgpu::CommandEncoder,
+ ) -> wgpu::RenderPass<'e> {
+ let clear = |view| {
+ Some(wgpu::RenderPassColorAttachment {
+ view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+ store: wgpu::StoreOp::Store,
+ },
+ })
+ };
+ encoder
+ .begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("HD G-buffer"),
+ timestamp_writes: stamp(&mut self.timing, "hd g-buffer"),
+ color_attachments: &[
+ clear(&self.gbuffer.albedo),
+ clear(&self.gbuffer.normal),
+ clear(&self.gbuffer.levels),
+ ],
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+ view: &self.gbuffer.depth,
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(0.0),
+ store: wgpu::StoreOp::Store,
+ }),
+ stencil_ops: None,
+ }),
+ ..Default::default()
+ })
+ .forget_lifetime()
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/light.wgsl b/crates/horizon-client/src/gfx/hd/light.wgsl
new file mode 100644
index 000000000..034a04607
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/light.wgsl
@@ -0,0 +1,397 @@
+
+struct Light {
+ pos: vec4<f32>,
+ color: vec4<f32>,
+}
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var scene_color: texture_2d<f32>;
+@group(0) @binding(2) var scene_depth: texture_depth_2d;
+@group(0) @binding(3) var albedo_tex: texture_2d<f32>;
+@group(0) @binding(4) var normal_tex: texture_2d<f32>;
+@group(0) @binding(5) var gbuffer_depth: texture_depth_2d;
+@group(0) @binding(6) var<storage, read> lights: array<Light>;
+@group(0) @binding(7) var light_tex: texture_2d<f32>;
+@group(0) @binding(8) var shadow_map: texture_depth_2d_array;
+@group(0) @binding(9) var shadow_sampler: sampler_comparison;
+@group(0) @binding(10) var lightmap: texture_2d<f32>;
+@group(0) @binding(11) var linear: sampler;
+// Per tile of 16x16 pixels: where its surface list and its halo list start in this array and
+// how long they are (4 words), then the lists of light indices. A length of 0xffffffff means
+// every light.
+@group(0) @binding(12) var<storage, read> tiles: array<u32>;
+// Ambient occlusion at half resolution: x the occlusion (1 open), y the distance it was taken at.
+@group(0) @binding(13) var ao_tex: texture_2d<f32>;
+// The water pass's depth: the nearest water surface (0 where there is none).
+@group(0) @binding(14) var water_depth: texture_depth_2d;
+//SKY15
+// The volumetric fog (half resolution: light in rgb, transmittance in a), the distance it was marched
+// to, the volumetric clouds (half resolution: light, transmittance) and the clouds' shadow map.
+@group(0) @binding(16) var fog_tex: texture_2d<f32>;
+@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>;
+const CLOUD_SHADOW_R: f32 = 1200.0;
+// The pixel being lit, for the shadow filter's noise.
+var<private> frag_px: vec2<f32>;
+
+// The occlusion at a full-resolution pixel `dist` away: the four nearest half-resolution texels
+// weighted by closeness and by how near their distance is, so it doesn't bleed across edges.
+fn ao_at(px: vec2<i32>, dist: f32) -> f32 {
+ let size = vec2<i32>(textureDimensions(ao_tex));
+ let f = vec2<f32>(px) * 0.5;
+ let base = vec2<i32>(floor(f));
+ let t = f - floor(f);
+ var sum = 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(ao_tex, q, 0).xy;
+ let bw = select(1.0 - t.x, t.x, i == 1) * select(1.0 - t.y, t.y, j == 1);
+ let dz = (s.y - dist) / (0.03 * dist + 0.1);
+ let w = select(0.0, bw / (1.0 + dz * dz * dz * dz), s.y > 0.0);
+ sum += s.x * w;
+ total += w;
+ }
+ }
+ if total < 0.0001 {
+ return textureLoad(ao_tex, clamp(px / 2, vec2<i32>(0), size - 1), 0).x;
+ }
+ return sum / total;
+}
+
+// Interleaved gradient noise: a different offset each pixel and frame, for the ray march.
+fn ign(px: vec2<f32>) -> f32 {
+ let p = px + vec2<f32>(5.588238 * frame.params.w);
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
+}
+
+// Where a camera-relative point falls in cascade c: uv and depth.
+fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
+ let s = frame.cascades[c] * vec4<f32>(p, 1.0);
+ return vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
+}
+
+// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow; soft-edged.
+fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
+ return shadow_taps(p, n, 1);
+}
+
+// A point of a disc of radius 1 (Vogel's spiral), turned by `angle`.
+fn vogel(i: i32, count: i32, angle: f32) -> vec2<f32> {
+ let r = sqrt((f32(i) + 0.5) / f32(count));
+ let a = f32(i) * 2.39996323 + angle;
+ return vec2<f32>(cos(a), sin(a)) * r;
+}
+
+// The near cascade's soft shadow: a blocker search sets the penumbra's width (wider the farther
+// the shadow's caster is above the receiver), then a ring of taps turned by a per-pixel noise
+// filters it; the temporal pass averages the noise. `s`: the point in the cascade (uv, depth),
+// `texel`: the cascade's texel in blocks.
+fn soft_shadow(s: vec3<f32>, texel: f32) -> f32 {
+ let angle = ign(frag_px) * 6.2831853;
+ var blocker = 0.0;
+ var found = 0.0;
+ for (var i = 0; i < 8; i++) {
+ let o = vogel(i, 8, angle) * (6.0 / 2048.0);
+ let q = clamp(vec2<i32>((s.xy + o) * 2048.0), vec2<i32>(0), vec2<i32>(2047));
+ let z = textureLoad(shadow_map, q, 0, 0);
+ if z < s.z - 0.0006 {
+ blocker += z;
+ found += 1.0;
+ }
+ }
+ if found < 0.5 { return 1.0; }
+ // The caster's height above the receiver, in blocks (the cascade's depth spans 799 blocks).
+ let height = (s.z - blocker / found) * 799.0;
+ let radius = clamp(height * 0.18 / texel, 1.6, 8.0);
+ var lit = 0.0;
+ for (var i = 0; i < 12; i++) {
+ let o = vogel(i, 12, angle + 1.7) * (radius / 2048.0);
+ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + o, 0, s.z - 0.0005 - 0.00005 * radius);
+ }
+ return lit / 12.0;
+}
+
+// `shadow` with a (2 * r + 1) square of taps: the floor under water takes one (the water dims and
+// blurs it anyway).
+fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
+ let dist = length(p);
+ let c = select(1, 0, dist < frame.shadow.y * 0.92);
+ let texel = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
+ let q = p + n * texel * 1.5;
+ let s = cascade_at(q, c);
+ if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
+ var lit = 0.0;
+ let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
+ if r == 1 && c == 0 {
+ return mix(1.0, soft_shadow(s, texel), fade);
+ }
+ let step = 1.0 / 2048.0;
+ for (var y = -r; y <= r; y++) {
+ for (var x = -r; x <= r; x++) {
+ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
+ }
+ }
+ return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
+}
+
+// One shadow sample, for the rays through the air.
+fn lit_at(p: vec3<f32>) -> f32 {
+ let s = cascade_at(p, 1);
+ if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
+ return textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy, 1, s.z - 0.001);
+}
+
+fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
+ if d <= start { return 0.0; }
+ if d >= end { return 1.0; }
+ return (d - start) / (end - start);
+}
+
+// The share of the sunlight the clouds let through at a camera-relative point: the shadow map
+// holds, for the sun's way down through the slab from a point of its base, the optical depth
+// (Photon's cloud shadow map, as a lookup rather than a pass).
+fn cloud_shadow(p: vec3<f32>) -> f32 {
+ if frame.clouds.z < 0.5 { return 1.0; }
+ let l = frame.cloud_dir.xyz;
+ if l.y < 0.1 { return 1.0; }
+ let t = (frame.clouds.x - p.y) / l.y;
+ if t <= 0.0 { return 1.0; }
+ let q = p.xz + l.xz * t;
+ let uv = q / (2.0 * CLOUD_SHADOW_R) + vec2<f32>(0.5);
+ if any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0)) { return 1.0; }
+ let od = textureSampleLevel(cloud_shadow_tex, linear, uv, 0.0).r;
+ // Fade out as the sun gets low (the shadow then falls far from the cloud).
+ let fade = smoothstep(0.1, 0.25, l.y);
+ return mix(1.0, mix(1.0, exp(-od * 0.9), 0.9), fade);
+}
+
+// The volumetric fog at a full-resolution pixel `dist` away: the four nearest half-resolution
+// texels weighted by closeness and by how near their march's length is (so it does not bleed over edges).
+fn fog_at(px: vec2<i32>, dist: f32) -> vec4<f32> {
+ let size = vec2<i32>(textureDimensions(fog_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(fog_tex, q, 0);
+ let sd = textureLoad(fog_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 * dist + 0.4);
+ let w = bw / (1.0 + dz * dz * dz * dz);
+ sum += s * w;
+ total += w;
+ }
+ }
+ if total < 0.0001 {
+ return textureLoad(fog_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).
+ @location(0) lit: vec4<f32>,
+ // Water: the air between the camera and the surface (scattered light, transmittance);
+ // in front of the water: the floor lit (rgb); else nothing.
+ @location(1) air: vec4<f32>,
+}
+
+// An opaque surface lit: the ambient, the sun and the mods' lights over its albedo.
+fn shade_surface(px: vec2<i32>, p: vec3<f32>, water: bool) -> vec3<f32> {
+ let sun = frame.sun.xyz;
+ let sun_on = frame.sun.w;
+ let atmosphere = frame.sky_sun.w > 0.5;
+ let count = u32(frame.params.x);
+ let tile = ((u32(px.y) >> 4u) * u32(frame.shadow.w) + (u32(px.x) >> 4u)) * 4u;
+ let surface_start = tiles[tile];
+ let surface_all = tiles[tile + 1u] == 0xffffffffu;
+ let surface_count = select(tiles[tile + 1u], count, surface_all);
+ let n = normalize(textureLoad(normal_tex, px, 0).xyz);
+ let albedo = textureLoad(albedo_tex, px, 0);
+ let lm = textureLoad(light_tex, px, 0).xy;
+ // Vanilla's light (sky and blocks through the lightmap), shaded where the sun can't reach.
+ let base = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(lm.y, lm.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
+ let block = textureSampleLevel(lightmap, linear, vec2<f32>(lm.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
+ let sky_reach = smoothstep(0.35, 0.95, lm.x);
+ var s = 1.0;
+ let ndl = dot(n, sun);
+ if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow_taps(p, n, select(1, 0, water)) * cloud_shadow(p); } else { s = 0.0; }
+ // Shadowed sky light (ambient) keeps vanilla's look, blocks' light stays as is; the sun adds
+ // direct light by the face's angle.
+ let direct = frame.sun_color.rgb * sun_on * max(ndl, 0.0) * s * sky_reach;
+ // Contact shadows from the ambient occlusion: on the ambient and blocks' light fully, on
+ // the mods' lights by half, not on the sun or on emission.
+ var occlusion = 1.0;
+ // (The water dims and blurs the floor under it: no occlusion there.)
+ if frame.ambient.w > 0.0 && !water { occlusion = ao_at(px, length(p)); }
+ let ao_ambient = mix(1.0, occlusion, frame.ambient.w);
+ let ao_lights = mix(1.0, occlusion, frame.ambient.w * 0.5);
+ // The sky's light is bluish from above and carries the horizon's colour from the sides;
+ // faces turned up take more of it than the ones turned down.
+ var sky_tint = vec3<f32>(1.0);
+ if atmosphere {
+ let reach = smoothstep(0.25, 0.9, lm.x);
+ let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
+ let shape = mix(0.66, 1.1, smoothstep(0.1, 1.0, up));
+ let hue = mix(vec3<f32>(1.0), sky_light_tint(n), 0.4 * (1.0 - 0.5 * frame.sun_color.a));
+ sky_tint = mix(vec3<f32>(1.0), hue * shape, reach);
+ }
+ var light = max(base * sky_tint * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
+ // The mods' lights.
+ for (var k = 0u; k < surface_count; k++) {
+ let l = lights[select(tiles[surface_start + k], k, surface_all)];
+ let to = l.pos.xyz - p;
+ let dist = length(to);
+ if dist >= l.pos.w { continue; }
+ let window = pow(clamp(1.0 - pow(dist / l.pos.w, 4.0), 0.0, 1.0), 2.0);
+ let falloff = l.color.w / (dist * dist + 1.0) * window;
+ let k = max(dot(n, to / max(dist, 0.001)) * 0.85 + 0.15, 0.0);
+ light += l.color.rgb * falloff * k * ao_lights;
+ }
+ return albedo.rgb * light + albedo.rgb * albedo.a * 1.5;
+}
+
+@fragment
+fn fs_light(in: FullOut) -> LightOut {
+ let px = vec2<i32>(in.position.xy);
+ let scene = textureLoad(scene_color, px, 0).rgb;
+ let d = textureLoad(scene_depth, px, 0);
+ let g = textureLoad(gbuffer_depth, px, 0);
+ // Water over the opaque world (or over nothing): the water composite lays the surface over
+ // the floor, which is lit here with no air. Something translucent in front of the water
+ // (`front`: nearer than its surface) keeps the scene as drawn; the floor goes beside it.
+ var wd = 0.0;
+ var water_here = false;
+ if frame.water.x > 0.5 {
+ wd = textureLoad(water_depth, px, 0);
+ water_here = wd > 0.0 && (g <= 0.0 || wd >= g);
+ }
+ let front = water_here && g > 0.0 && d - wd > d * 0.002;
+ let water = water_here && !front;
+ let has_floor = water && g > 0.0;
+ let dd = select(d, g, has_floor);
+ let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(dd, 0.000001), 1.0);
+ let h = frame.inverse_view_proj * ndc;
+ let p = h.xyz / h.w;
+ let view = normalize(p);
+ // Where the air ends: the water's surface over water, else the pixel itself.
+ var ps = p;
+ if water {
+ let hf = frame.inverse_view_proj * vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(wd, 0.000001), 1.0);
+ ps = hf.xyz / hf.w;
+ }
+ let view_s = normalize(ps);
+ let sun = frame.sun.xyz;
+ let sun_on = frame.sun.w;
+ let count = u32(frame.params.x);
+ let tile = ((u32(px.y) >> 4u) * u32(frame.shadow.w) + (u32(px.x) >> 4u)) * 4u;
+ let halo_start = tiles[tile + 2u];
+ let halo_all = tiles[tile + 3u] == 0xffffffffu;
+ let halo_count = select(tiles[tile + 3u], count, halo_all);
+ frag_px = in.position.xy;
+ let atmosphere = frame.sky_sun.w > 0.5;
+ var c = scene;
+ // The sky: the atmosphere's, with what the world drew over vanilla's plain sky (sun, moon,
+ // stars, clouds) kept on top.
+ if d <= 0.0 && atmosphere {
+ var kept = max(scene - vanilla_sky(view), vec3<f32>(0.0));
+ var sky = atmosphere_sky(view);
+ // The sun: vanilla's square sprite is taken out and a round disc with a soft haze round
+ // it (warm, strongest low) is drawn in its place.
+ let toward = dot(view, frame.sky_sun.xyz);
+ kept *= 1.0 - smoothstep(0.965, 0.975, toward);
+ let clear = 1.0 - frame.sky_extra.z;
+ let shine = smoothstep(-0.04, 0.02, frame.sky_sun.y) * clear;
+ let glow = max(toward, 0.0);
+ let angle = acos(clamp(toward, -1.0, 1.0));
+ sky += frame.sun_color.rgb * shine * (pow(glow, 24.0) * 0.07 + pow(glow, 400.0) * 0.4 + 6.0 * (1.0 - smoothstep(0.016, 0.022, angle)));
+ 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);
+ c = c * cl.a + cl.rgb;
+ }
+ }
+ // (Vanilla's clouds, when the volumetric ones are off, take the low sun's colour in their own
+ // pass: `vanilla_cloud_tint`, applied to their colour on the CPU.)
+ let surface = has_floor || (!water_here && d > 0.0 && abs(d - g) <= max(d, g) * 0.002);
+ if surface {
+ c = shade_surface(px, p, water);
+ // Vanilla's fog over it again (the water composite does it at the surface).
+ if !water {
+ let sph = length(p);
+ let cyl = max(length(p.xz), abs(p.y));
+ let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
+ let haze = haze_colour(view);
+ c = mix(c, haze, f * frame.fog_color.a);
+ // Aerial perspective: far things take the horizon's colour a little before the fog does.
+ if atmosphere && frame.fog.x > 1.0e6 {
+ c = mix(c, haze, (1.0 - exp(-sph / 700.0)) * 0.5);
+ }
+ }
+ }
+ // What lies behind a translucent thing in front of the water: the floor, lit.
+ var floor_lit = vec3<f32>(0.0);
+ if front {
+ let hg = frame.inverse_view_proj * vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, max(g, 0.000001), 1.0);
+ floor_lit = shade_surface(px, hg.xyz / hg.w, true);
+ }
+ // The air between the camera and the pixel (the water's surface over water): the volumetric
+ // fog (what it hides, what it scatters), the sun's rays between the shadows, and the halos
+ // round the mods' lights. Over water they go to the composite, which lays them over the
+ // finished water; elsewhere over the pixel here.
+ var air_t = 1.0;
+ var air_s = vec3<f32>(0.0);
+ let far = select(min(length(ps), frame.shadow.z), frame.shadow.z, d <= 0.0);
+ let density = frame.ambient.z;
+ let volumetric = frame.vfog.x > 0.5 && atmosphere;
+ if volumetric {
+ var fog_len = 192.0;
+ if d > 0.0 { fog_len = min(length(ps), 192.0); }
+ let fg = fog_at(px, fog_len);
+ air_t = fg.a;
+ air_s = fg.rgb;
+ }
+ if !volumetric && sun_on > 0.0 && density > 0.0 {
+ let steps = 14;
+ let jitter = ign(in.position.xy);
+ var acc = 0.0;
+ for (var i = 0; i < steps; i++) {
+ let t = (f32(i) + jitter) / f32(steps) * far;
+ acc += lit_at(view_s * t);
+ }
+ // Henyey-Greenstein, forward scattering: brightest looking towards the sun.
+ let cos_t = dot(view_s, sun);
+ let gk = 0.6;
+ let phase = (1.0 - gk * gk) / pow(1.0 + gk * gk - 2.0 * gk * cos_t, 1.5) * 0.08;
+ air_s += frame.sun_color.rgb * sun_on * (acc / f32(steps)) * phase * (1.0 - exp(-far * density));
+ }
+ for (var k = 0u; k < halo_count; k++) {
+ let l = lights[select(tiles[halo_start + k], k, halo_all)];
+ // Nearest point of the view ray to the light, and the glow along the ray in front of it.
+ let t0 = dot(l.pos.xyz, view_s);
+ let hh = length(l.pos.xyz - view_s * t0) + 0.5;
+ if hh > l.pos.w * 0.5 { continue; }
+ let a0 = atan((0.0 - t0) / hh);
+ let a1 = atan((far - t0) / hh);
+ // Thicker air glows more round a light, but only so far: the mist at dusk and many fires
+ // at once would otherwise wash the picture out.
+ air_s += l.color.rgb * l.color.w * (a1 - a0) / hh * 0.004 * (0.3 + min(density * 300.0, 0.9));
+ }
+ var out: LightOut;
+ if water {
+ out.lit = vec4<f32>(c, 0.5);
+ out.air = vec4<f32>(air_s, air_t);
+ } else {
+ out.lit = vec4<f32>(c * air_t + air_s, select(1.0, 0.6, front));
+ out.air = vec4<f32>(floor_lit, 0.0);
+ }
+ return out;
+}
diff --git a/crates/horizon-client/src/gfx/hd/lighting.rs b/crates/horizon-client/src/gfx/hd/lighting.rs
new file mode 100644
index 000000000..da56d00ba
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/lighting.rs
@@ -0,0 +1,265 @@
+//! Deferred lighting: the G-buffer, the shadows, the sky, the clouds, the fog and the mods' point
+//! lights combined into the HDR colour of the world (`fs_light`).
+//!
+//! Contract (world size, camera-relative positions rebuilt from the world depth and the jittered
+//! `inverse_view_proj`; depth is reversed-Z):
+//! - Reads: group 0 bindings (see `LIGHT_BINDINGS`): 0 `HdFrame`; 1-2 the vanilla world pass's
+//! colour and depth (`d`; 0 = sky); 3-5 the G-buffer albedo, normal and depth (`g`); 6 the lights;
+//! 7 light levels; 8-9 shadow cascades and comparison sampler; 10-11 lightmap and sampler;
+//! 12 the light tiles; 13 AO (half size); 14 the water depth; 15 the sky texture; 16-17 the
+//! volumetric fog and its ray length (half size); 18-19 the volumetric clouds and the clouds'
+//! shadow map.
+//! - Writes two targets (`Rgba16Float`, display space, may exceed 1):
+//! - `lit`: rgb the lit colour; **alpha is a marker**: 1 an ordinary pixel, **0.5 water** (rgb is
+//! the *floor* under it, lit without any air), **0.6 something translucent in front of the
+//! water** (rgb the scene as drawn).
+//! - `air`: over water, rgb the scattered air light up to the surface and **a the transmittance**
+//! (the composite applies it once after absorption); in front of the water, rgb the lit floor;
+//! else 0.
+//! - Which pixel is what: `g == 0` means the G-buffer holds nothing (sky, or something the world pass
+//! drew that the G-buffer did not: translucent, particles); the vanilla colour is kept with
+//! the air applied. Water is where `water_depth >= g` and nothing sits in front (`d - wd` within
+//! 0.2%).
+//! - The sun is the light `frame::light_of` picks (moon at night), in `HdFrame.sun`.
+//! - Bind groups are cached per (world views, lightmap) and per volumetric-history flip.
+use super::frame::HdLight;
+use super::tiles::{TILE, tile_lists, tiles_buffer};
+use super::{
+ DEPTH, FLOAT, HDR, Hd, SAMPLER, SHADOW_ARRAY, SHADOW_SAMPLER, STORAGE, UNFILTERED, UNIFORM,
+ entries, module, stamp, target,
+};
+
+/// The most lights drawn a frame.
+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] = [
+ UNIFORM,
+ UNFILTERED,
+ DEPTH,
+ UNFILTERED,
+ UNFILTERED,
+ DEPTH,
+ STORAGE,
+ UNFILTERED,
+ SHADOW_ARRAY,
+ SHADOW_SAMPLER,
+ FLOAT,
+ SAMPLER,
+ STORAGE,
+ UNFILTERED,
+ DEPTH,
+ FLOAT,
+ FLOAT,
+ UNFILTERED,
+ FLOAT,
+ FLOAT,
+];
+
+/// The outside views and sampler the lighting group reads.
+type LightKey = (
+ wgpu::TextureView,
+ wgpu::TextureView,
+ wgpu::TextureView,
+ wgpu::Sampler,
+);
+
+pub(super) struct Lighting {
+ /// The lit world, and the second target (the air over water, the floor beside translucent things).
+ pub(super) lit: wgpu::TextureView,
+ pub(super) air: wgpu::TextureView,
+ layout: wgpu::BindGroupLayout,
+ pipeline: wgpu::RenderPipeline,
+ /// Bind groups, keyed by the views they read; one per volumetric-history flip.
+ group: [Option<(LightKey, wgpu::BindGroup)>; 2],
+ lights: wgpu::Buffer,
+ /// The tiles' light lists (see `tile_lists`), sized for the screen, and the tiles per row.
+ tiles: wgpu::Buffer,
+ pub(super) tiles_x: u32,
+ tile_scratch: Vec<Vec<u16>>,
+ tile_words: Vec<u32>,
+}
+
+impl Lighting {
+ pub(super) fn new(device: &wgpu::Device) -> Self {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD lighting"),
+ entries: &entries(&LIGHT_BINDINGS),
+ });
+ let light = module(device, "HD lighting", LIGHT);
+ let pipeline = {
+ let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("HD lighting"),
+ bind_group_layouts: &[Some(&layout)],
+ ..Default::default()
+ });
+ let target = Some(wgpu::ColorTargetState {
+ format: HDR,
+ blend: None,
+ write_mask: wgpu::ColorWrites::ALL,
+ });
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("HD lighting"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &light,
+ entry_point: Some("vs_full"),
+ compilation_options: Default::default(),
+ buffers: &[],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &light,
+ entry_point: Some("fs_light"),
+ compilation_options: Default::default(),
+ targets: &[target.clone(), target],
+ }),
+ primitive: Default::default(),
+ depth_stencil: None,
+ multisample: Default::default(),
+ multiview_mask: None,
+ cache: None,
+ })
+ };
+ let lights = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("HD lights"),
+ size: (MAX_LIGHTS * std::mem::size_of::<HdLight>()) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+ Self {
+ lit: target(device, "HD lit", [1, 1], HDR).view,
+ air: target(device, "HD air", [1, 1], HDR).view,
+ layout,
+ pipeline,
+ group: [None, None],
+ lights,
+ tiles: tiles_buffer(device, [1, 1]),
+ tiles_x: 1,
+ tile_scratch: Vec::new(),
+ tile_words: Vec::new(),
+ }
+ }
+
+ /// New world-size targets and tile buffer; every group names a view that was just replaced.
+ pub(super) fn resize(&mut self, device: &wgpu::Device, size: [u32; 2]) {
+ self.lit = target(device, "HD lit", size, HDR).view;
+ self.air = target(device, "HD air", size, HDR).view;
+ self.tiles = tiles_buffer(device, size);
+ self.tiles_x = size[0].div_ceil(TILE).max(1);
+ self.group = [None, None];
+ }
+
+ /// Uploads this frame's lights and their tile lists; returns how many lights are drawn.
+ pub(super) fn upload(
+ &mut self,
+ queue: &wgpu::Queue,
+ view_proj: &glam::Mat4,
+ lights: &[HdLight],
+ size: [u32; 2],
+ ) -> usize {
+ let n = lights.len().min(MAX_LIGHTS);
+ if n > 0 {
+ queue.write_buffer(&self.lights, 0, bytemuck::cast_slice(&lights[..n]));
+ }
+ // Written every frame, empty too: the shader reads the counts, not `params.x`.
+ tile_lists(
+ view_proj,
+ &lights[..n],
+ size,
+ &mut self.tile_scratch,
+ &mut self.tile_words,
+ );
+ queue.write_buffer(&self.tiles, 0, bytemuck::cast_slice(&self.tile_words));
+ n
+ }
+}
+
+impl Hd {
+ /// Lights the world (its colour and depth) into `lit`.
+ pub fn light(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ world: (&wgpu::TextureView, &wgpu::TextureView),
+ lightmap: (&wgpu::TextureView, &wgpu::Sampler),
+ ) {
+ use std::sync::atomic::Ordering;
+ let key = (
+ world.0.clone(),
+ world.1.clone(),
+ lightmap.0.clone(),
+ lightmap.1.clone(),
+ );
+ let flip = self.volumes.flip_index();
+ if self.lighting.group[flip]
+ .as_ref()
+ .is_none_or(|(k, _)| *k != key)
+ {
+ self.groups_made.fetch_add(1, Ordering::Relaxed);
+ let lighting = &self.lighting;
+ let resources = [
+ wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
+ wgpu::BindingResource::TextureView(world.0),
+ wgpu::BindingResource::TextureView(world.1),
+ wgpu::BindingResource::TextureView(&self.gbuffer.albedo),
+ wgpu::BindingResource::TextureView(&self.gbuffer.normal),
+ wgpu::BindingResource::TextureView(&self.gbuffer.depth),
+ lighting.lights.as_entire_binding(),
+ wgpu::BindingResource::TextureView(&self.gbuffer.levels),
+ wgpu::BindingResource::TextureView(&self.shadows.array),
+ wgpu::BindingResource::Sampler(&self.shadows.sampler),
+ wgpu::BindingResource::TextureView(lightmap.0),
+ wgpu::BindingResource::Sampler(lightmap.1),
+ lighting.tiles.as_entire_binding(),
+ wgpu::BindingResource::TextureView(&self.ao.blurred),
+ wgpu::BindingResource::TextureView(self.water.depth_view()),
+ wgpu::BindingResource::TextureView(&self.sky.lut),
+ wgpu::BindingResource::TextureView(&self.volumes.fog),
+ wgpu::BindingResource::TextureView(&self.volumes.fog_dist),
+ wgpu::BindingResource::TextureView(self.volumes.cloud_view()),
+ wgpu::BindingResource::TextureView(&self.volumes.cloud_shadow),
+ ];
+ let entries: Vec<_> = resources
+ .into_iter()
+ .enumerate()
+ .map(|(i, resource)| wgpu::BindGroupEntry {
+ binding: i as u32,
+ resource,
+ })
+ .collect();
+ let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("HD lighting"),
+ layout: &lighting.layout,
+ entries: &entries,
+ });
+ self.lighting.group[flip] = Some((key, group));
+ }
+ let Some((_, group)) = &self.lighting.group[flip] else {
+ return;
+ };
+ self.water.new_frame();
+ let clear = |view| {
+ Some(wgpu::RenderPassColorAttachment {
+ view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
+ store: wgpu::StoreOp::Store,
+ },
+ })
+ };
+ {
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("HD lighting"),
+ timestamp_writes: stamp(&mut self.timing, "hd lighting"),
+ color_attachments: &[clear(&self.lighting.lit), clear(&self.lighting.air)],
+ ..Default::default()
+ });
+ pass.set_pipeline(&self.lighting.pipeline);
+ pass.set_bind_group(0, group, &[]);
+ pass.draw(0..3, 0..1);
+ }
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/mod.rs b/crates/horizon-client/src/gfx/hd/mod.rs
new file mode 100644
index 000000000..ed336692d
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/mod.rs
@@ -0,0 +1,896 @@
+//! AirDef fork: HD lighting. Before the world pass the solid terrain and the
+//! entities (with the mods' models) are drawn once more into a G-buffer: the
+//! surface colour without light and its normal. After the world pass the
+//! mods' point lights (rocket flames, flares, blasts) light every pixel of it
+//! into a high-dynamic-range target; the mod's post pass runs on that; then
+//! bright parts bloom and a tone curve brings it to the screen — vanilla's
+//! range (up to about 0.8) left as it is, only what is brighter rolled off.
+//!
+//! # Layout
+//!
+//! This file owns `Hd` (the targets shared between passes, the frame buffer, group counting,
+//! `resize`, `write`) and the helpers every pass uses. One file per pass owns that pass's pipelines,
+//! layouts, bind groups and targets, and its WGSL (`<pass>.wgsl`, next to it):
+//! `shadows`, `gbuffer`, `ao`, `sky`, `volumes` (clouds, fog), `lighting` (+ `tiles`), `water`,
+//! `taa`, `bloom`, `tonemap`; `frame` holds the uniform block and the scene types, `noise` the
+//! CPU-made noise textures. Every shader is `frame::PRELUDE` + its body (`assemble`).
+//!
+//! # Contracts (the same words are repeated at the top of each pass file)
+//!
+//! Frame order and what each pass reads (R) and writes (W). Sizes: *world* = the scaled world target
+//! (`HORIZON_HD_SCALE`), *out* = the window.
+//!
+//! | # | Pass | R | W |
+//! |---|---|---|---|
+//! | 1 | `sky` | frame | sky texture 128x72 (linear Rec. 709 radiance) |
+//! | 2 | `shadow_pass` x2 (caller draws) | - | shadow array (normal Z, 1 = far) |
+//! | 3 | `gbuffer_pass` (caller draws) | - | albedo (a = emission), normal, light levels, depth |
+//! | 3a | `water_pass` (caller draws) | - | water colour (a = block light), normal (a = sky light), water depth |
+//! | 3b | `ao` | G-buffer depth, normal | AO half size (x occlusion, y distance) |
+//! | 3c | `volumes` | frame, sky, world depth, shadows, noise | clouds (rgb, a = transmittance), cloud shadow map, fog (rgb, a = transmittance) + ray length |
+//! | 4 | `light` | world colour + depth, G-buffer, shadows, sky, AO, volumes, water depth | `lit` (a: 1 normal / 0.5 water / 0.6 in front of water), `air` |
+//! | 4a | `water_composite` | `lit`, `air`, G-buffer depth, water targets, shadows, sky | `lit_water` |
+//! | 5 | mod post pass (caller) | `lit_view()` + depth | `fx` |
+//! | 6 | `taa` | `lit_view()` or `fx`, world depth, history | `taa` + next history (out) |
+//! | 7 | `finish` | colour source (`HdSource`) | bloom chain, then the screen |
+//!
+//! - **Colour.** Everything between the lighting and the tone curve is in vanilla's display space
+//! (gamma-encoded, Rec. 709 primaries) with values allowed above 1 (`Rgba16Float`, "HDR");
+//! the tone curve leaves <= 0.8 untouched. Exceptions: the sky texture (linear Rec. 709 radiance,
+//! marched in Rec. 2020; readers apply `sky_show`) and the cloud/fog light accumulators (added in
+//! the same display space as the lit colour). G-buffer albedo is display-space `Rgba8Unorm`.
+//! - **Depth.** The camera's depth is reversed-Z, `Depth32Float`: 1 near, 0 far; cleared to 0, so 0
+//! means "nothing" (sky, or no water). The shadow maps use ordinary Z from the light's orthographic
+//! projection (0 near, 1 far; compared `<=`).
+//! - **Alpha channels.** G-buffer albedo.a = emission; `lit.a` = 1 / 0.5 (water) / 0.6 (in front of
+//! water); `air.a` = transmittance; cloud and fog `.a` = transmittance; water colour.a = block
+//! light, water normal.a = sky light; TAA history.a = camera distance; AO.y = distance.
+//! - **Coordinates.** Positions are camera-relative (the camera is the origin; the world is `f64`
+//! on the CPU and only differences reach the GPU). Where a world coordinate is needed for a
+//! pattern (waves, cloud noise, wind) it is passed modulo a period (256, 65536) so it stays exact
+//! in `f32`. The world projection carries the TAA jitter; `HdScene::prev_view_proj*` says which.
+//! - **Time.** `frame counter` (`HdFrame.params.w`, modulo 64, only for noise rotation); *water time*
+//! (`water.y`, wall-clock seconds since HD started modulo 600); *game time* (`clouds.w`, seconds
+//! from `HdScene::seconds` modulo 300000, drives cloud wind). They are separate on purpose:
+//! replays that freeze or scrub game time still get running water and a changing noise pattern.
+//! - **Histories.** TAA's two history targets and the clouds' two are owned by their pass, sized at
+//! `resize` and reset on resize, a teleport (> 64 blocks), TAA off, or the pass being off; one `Hd`
+//! serves one camera, so histories of different cameras never mix.
+//! - **Bind groups** are cached by the views they read and remade only on `resize` or a changed
+//! input view (`groups_last_frame` is 0 in a steady state).
+
+mod ao;
+mod bloom;
+mod frame;
+mod gbuffer;
+mod lighting;
+mod noise;
+mod shadows;
+mod sky;
+mod taa;
+mod tiles;
+mod tonemap;
+mod volumes;
+mod water;
+
+use crate::gfx::pack_timing::PassTiming;
+use ao::Ao;
+use bloom::Bloom;
+use gbuffer::GBuffer;
+use lighting::Lighting;
+use shadows::Shadows;
+use sky::Sky;
+use std::sync::atomic::{AtomicU64, Ordering};
+use taa::Taa;
+use tonemap::Tonemap;
+use volumes::HdVolumes;
+use water::Water;
+
+pub use frame::{HdLight, HdScene, HdSettings, light_of, vanilla_cloud_tint};
+pub use gbuffer::{ALBEDO, LIGHT_LEVELS, NORMAL};
+pub use shadows::{CASCADES, cascades};
+pub use taa::{jitter, jitter_count, scale_override, taa_enabled};
+pub use volumes::clouds_enabled;
+pub use water::{WATER_NORMAL, WATER_TINT};
+
+use frame::{HdFrame, cloud_light, smooth, sun_dir_x};
+use taa::UPSCALE_HISTORY;
+use water::water_enabled;
+
+/// The lit world and the post pass's output.
+pub const HDR: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
+
+pub(super) struct Target {
+ pub(super) view: wgpu::TextureView,
+}
+
+pub(super) fn target(
+ device: &wgpu::Device,
+ label: &str,
+ size: [u32; 2],
+ format: wgpu::TextureFormat,
+) -> Target {
+ let view = device
+ .create_texture(&wgpu::TextureDescriptor {
+ label: Some(label),
+ size: wgpu::Extent3d {
+ width: size[0].max(1),
+ height: size[1].max(1),
+ depth_or_array_layers: 1,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ })
+ .create_view(&Default::default());
+ Target { view }
+}
+
+/// What bloom and the tone curve read: the lit world, or the post pass's output.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum HdSource {
+ Lit,
+ Fx,
+ /// The anti-aliased result of `taa`.
+ Taa,
+}
+
+/// The timestamps of the pass `name` while HD passes are timed.
+fn stamp<'a>(
+ timing: &'a mut Option<PassTiming>,
+ name: &str,
+) -> Option<wgpu::RenderPassTimestampWrites<'a>> {
+ timing.as_mut()?.writes(name)
+}
+
+/// The shader's source: the common part, then the body with the sky-reading functions where it
+/// has a marker (`//SKY15`: the sky-view texture is binding 15, `//SKY10`: binding 10).
+pub(super) fn assemble(body: &str) -> String {
+ let mut body = body.to_owned();
+ // Highest number first: `//SKY1` must not eat the start of `//SKY15`.
+ for binding in (0..32).rev() {
+ body = body.replace(
+ &format!("//SKY{binding}"),
+ &sky::SKY_FUNCS.replace("SKYBIND", &binding.to_string()),
+ );
+ }
+ format!("{}{body}", frame::PRELUDE)
+}
+
+pub(super) fn module(device: &wgpu::Device, label: &str, body: &str) -> wgpu::ShaderModule {
+ // Development: with `HORIZON_HD_SHADER_DIR` set, a shader's body is read from
+ // `<dir>/<label>.wgsl` (written there from the built-in one when missing), so it can be tuned
+ // without rebuilding.
+ let body = match std::env::var("HORIZON_HD_SHADER_DIR") {
+ Ok(dir) => {
+ let path = std::path::Path::new(&dir).join(format!("{}.wgsl", label.replace(' ', "_")));
+ match std::fs::read_to_string(&path) {
+ Ok(text) => text,
+ Err(_) => {
+ let _ = std::fs::write(&path, body);
+ body.to_owned()
+ }
+ }
+ }
+ Err(_) => body.to_owned(),
+ };
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some(label),
+ source: wgpu::ShaderSource::Wgsl(assemble(&body).into()),
+ })
+}
+
+/// One fullscreen pass over `to` (cleared), with the given pipeline and group.
+pub(super) fn stamped_pass(
+ encoder: &mut wgpu::CommandEncoder,
+ label: &str,
+ to: &[&wgpu::TextureView],
+ pipeline: &wgpu::RenderPipeline,
+ group: &wgpu::BindGroup,
+ timestamp_writes: Option<wgpu::RenderPassTimestampWrites<'_>>,
+) {
+ let attachments: Vec<_> = to
+ .iter()
+ .map(|view| {
+ Some(wgpu::RenderPassColorAttachment {
+ view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
+ store: wgpu::StoreOp::Store,
+ },
+ })
+ })
+ .collect();
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some(label),
+ timestamp_writes,
+ color_attachments: &attachments,
+ ..Default::default()
+ });
+ pass.set_pipeline(pipeline);
+ pass.set_bind_group(0, group, &[]);
+ pass.draw(0..3, 0..1);
+}
+
+pub(super) fn fullscreen(
+ device: &wgpu::Device,
+ label: &str,
+ layout: &wgpu::BindGroupLayout,
+ module: &wgpu::ShaderModule,
+ entry: &str,
+ format: wgpu::TextureFormat,
+ blend: Option<wgpu::BlendState>,
+) -> wgpu::RenderPipeline {
+ let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some(label),
+ bind_group_layouts: &[Some(layout)],
+ ..Default::default()
+ });
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some(label),
+ layout: Some(&pipeline_layout),
+ vertex: wgpu::VertexState {
+ module,
+ entry_point: Some("vs_full"),
+ compilation_options: Default::default(),
+ buffers: &[],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module,
+ entry_point: Some(entry),
+ compilation_options: Default::default(),
+ targets: &[Some(wgpu::ColorTargetState {
+ format,
+ blend,
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ }),
+ primitive: Default::default(),
+ depth_stencil: None,
+ multisample: Default::default(),
+ multiview_mask: None,
+ cache: None,
+ })
+}
+
+pub(super) fn entries(kinds: &[wgpu::BindingType]) -> Vec<wgpu::BindGroupLayoutEntry> {
+ kinds
+ .iter()
+ .enumerate()
+ .map(|(i, ty)| wgpu::BindGroupLayoutEntry {
+ binding: i as u32,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ ty: *ty,
+ count: None,
+ })
+ .collect()
+}
+
+pub(super) const UNIFORM: wgpu::BindingType = wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+};
+pub(super) const FLOAT: wgpu::BindingType = wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float { filterable: true },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+};
+pub(super) const UNFILTERED: wgpu::BindingType = wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float { filterable: false },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+};
+pub(super) const DEPTH: wgpu::BindingType = wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Depth,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+};
+pub(super) const SAMPLER: wgpu::BindingType =
+ wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering);
+
+/// A read-only storage buffer binding.
+pub(super) const STORAGE: wgpu::BindingType = wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Storage { read_only: true },
+ has_dynamic_offset: false,
+ min_binding_size: None,
+};
+/// The shadow cascades: a depth array.
+pub(super) const SHADOW_ARRAY: wgpu::BindingType = wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Depth,
+ view_dimension: wgpu::TextureViewDimension::D2Array,
+ multisampled: false,
+};
+/// The comparison sampler of the shadow cascades.
+pub(super) const SHADOW_SAMPLER: wgpu::BindingType =
+ wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison);
+
+/// A bind group of consecutive bindings 0.., counted in `groups` (`Hd::groups_last_frame`).
+pub(super) fn make_group(
+ groups: &AtomicU64,
+ device: &wgpu::Device,
+ label: &str,
+ layout: &wgpu::BindGroupLayout,
+ resources: &[wgpu::BindingResource<'_>],
+) -> wgpu::BindGroup {
+ groups.fetch_add(1, Ordering::Relaxed);
+ let entries: Vec<_> = resources
+ .iter()
+ .enumerate()
+ .map(|(i, r)| wgpu::BindGroupEntry {
+ binding: i as u32,
+ resource: r.clone(),
+ })
+ .collect();
+ device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some(label),
+ layout,
+ entries: &entries,
+ })
+}
+
+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],
+ gbuffer: GBuffer,
+ shadows: Shadows,
+ /// The globals the shadow passes draw with (the light's view), and the camera's to put back.
+ pub shadow_globals: [wgpu::Buffer; 2],
+ pub main_globals: wgpu::Buffer,
+ frames: u32,
+ /// The lit world (`lighting.lit`) goes through the water composite; this is the mod post
+ /// pass's output.
+ pub fx: wgpu::TextureView,
+ lighting: Lighting,
+ water: Water,
+ /// The frame's translucent terrain exists (set by the renderer before `write`).
+ pub water_geometry: bool,
+ /// The volumetric clouds and the fog run this frame.
+ clouds_active: bool,
+ fog_active: bool,
+ sky: Sky,
+ started: std::time::Instant,
+ taa: Taa,
+ bloom: Bloom,
+ tonemap: Tonemap,
+ /// The volumetric clouds and fog (see `volumes`).
+ volumes: HdVolumes,
+ frame: wgpu::Buffer,
+ sampler: wgpu::Sampler,
+ ao: Ao,
+ /// GPU time of the HD passes while diagnostics ask for it (`set_timing`).
+ timing: Option<PassTiming>,
+ /// Bind groups made so far, and in the last frame (a steady state makes none).
+ groups_made: AtomicU64,
+ groups_seen: u64,
+ pub groups_last_frame: u64,
+}
+
+impl Hd {
+ pub fn new(
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ size: [u32; 2],
+ screen: wgpu::TextureFormat,
+ globals_size: u64,
+ ) -> Self {
+ let frame = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("HD frame"),
+ size: std::mem::size_of::<HdFrame>() as u64,
+ usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("HD"),
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ address_mode_u: wgpu::AddressMode::ClampToEdge,
+ address_mode_v: wgpu::AddressMode::ClampToEdge,
+ ..Default::default()
+ });
+ let globals = |label| {
+ device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some(label),
+ size: globals_size,
+ usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ })
+ };
+ let mut hd = Hd {
+ size: [0, 0],
+ out: [0, 0],
+ gbuffer: GBuffer::new(device, [1, 1]),
+ shadows: Shadows::new(device),
+ shadow_globals: [
+ globals("HD shadow globals 0"),
+ globals("HD shadow globals 1"),
+ ],
+ main_globals: globals("HD camera globals"),
+ frames: 0,
+ fx: target(device, "HD post", [1, 1], HDR).view,
+ lighting: Lighting::new(device),
+ water: Water::new(device),
+ water_geometry: false,
+ clouds_active: false,
+ fog_active: false,
+ sky: Sky::new(device, &frame),
+ started: std::time::Instant::now(),
+ taa: Taa::new(device),
+ bloom: Bloom::new(device),
+ tonemap: Tonemap::new(device, screen),
+ volumes: HdVolumes::new(device, queue),
+ frame,
+ sampler,
+ ao: Ao::new(device),
+ timing: None,
+ groups_made: AtomicU64::new(0),
+ groups_seen: 0,
+ groups_last_frame: 0,
+ };
+ hd.resize(device, size, size);
+ hd
+ }
+
+ /// 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.gbuffer = GBuffer::new(device, size);
+ self.lighting.resize(device, size);
+ self.fx = target(device, "HD post", size, HDR).view;
+ self.water.resize(device, size);
+ self.taa.resize(device, out);
+ self.ao
+ .resize(device, size, &self.frame, &self.gbuffer, &self.groups_made);
+ // Every group names a view that was just replaced.
+ self.volumes.resize(device, size);
+ self.tonemap.reset();
+ self.bloom
+ .resize(device, out, &self.frame, &self.sampler, &self.groups_made);
+ }
+
+ /// 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,
+ queue: &wgpu::Queue,
+ scene: &HdScene,
+ lights: &[HdLight],
+ settings: HdSettings,
+ ) {
+ let n = self
+ .lighting
+ .upload(queue, &scene.view_proj, lights, self.size);
+ self.frames = self.frames.wrapping_add(1);
+ let made = self.groups_made.load(Ordering::Relaxed) + self.volumes.groups_made;
+ self.groups_last_frame = made - self.groups_seen;
+ self.groups_seen = made;
+ if PassTiming::logging() && self.frames.is_multiple_of(240) {
+ log::info!(
+ "HD bind groups made in the last frame: {} ({made} in all)",
+ self.groups_last_frame
+ );
+ }
+ // The sun by day, the moon (opposite) by night, faded out round the horizon.
+ let (dir, strength, color) = light_of(scene);
+ let cascades = scene.cascades;
+ let night = scene.dark.clamp(0.0, 1.0);
+ self.water.active = scene.water && water_enabled() && self.water_geometry;
+ self.taa.active = scene.taa && taa_enabled();
+ // A long jump of the camera (a teleport) starts the history over.
+ if !self.taa.active || scene.camera_delta.length() > 64.0 {
+ self.taa.history_ready = false;
+ }
+ let sun_height = scene.sun.normalize_or(glam::Vec3::Y).y;
+ self.sky.stale = (sun_height - self.sky.drawn_for).abs() > 0.001;
+ if self.sky.stale {
+ self.sky.drawn_for = sun_height;
+ }
+ self.clouds_active = scene.sky && scene.clouds;
+ self.fog_active = scene.sky && volumes::fog_enabled();
+ let (cloud_dir, cloud_colour, cloud_strength) = cloud_light(scene);
+ // The morning mist: thick before the sun is up, gone by the time it is high.
+ let mist = if sun_dir_x(scene) > 0.0 {
+ smooth(-0.5, -0.1, sun_height) * (1.0 - smooth(0.05, 0.5, sun_height))
+ } else {
+ 0.0
+ };
+ let frame = HdFrame {
+ prev_view_proj: if self.upscaling() {
+ scene.prev_view_proj
+ } else {
+ scene.prev_view_proj_jittered
+ }
+ .to_cols_array_2d(),
+ taa: [
+ scene.camera_delta.x,
+ scene.camera_delta.y,
+ scene.camera_delta.z,
+ if self.taa.history_ready { 1.0 } else { 0.0 },
+ ],
+ view_proj: scene.view_proj.to_cols_array_2d(),
+ water: [
+ if self.water.active { 1.0 } else { 0.0 },
+ (self.started.elapsed().as_secs_f64() % 600.0) as f32,
+ scene.camera_mod[0],
+ scene.camera_mod[1],
+ ],
+ sky_color: [
+ scene.sky_color[0],
+ scene.sky_color[1],
+ scene.sky_color[2],
+ if scene.sky { 1.0 } else { 0.0 },
+ ],
+ sky_sun: {
+ let sun = scene.sun.normalize_or(glam::Vec3::Y);
+ [sun.x, sun.y, sun.z, if scene.sky { 1.0 } else { 0.0 }]
+ },
+ sky_extra: [
+ scene.sky_fog_end,
+ 0.0,
+ (scene.rain / 0.7).clamp(0.0, 1.0) * 0.85,
+ 0.0,
+ ],
+ sunrise: scene.sunrise,
+ clouds: [
+ volumes::CLOUD_BASE - scene.camera_world[2],
+ volumes::CLOUD_THICKNESS,
+ if self.clouds_active { 1.0 } else { 0.0 },
+ scene.seconds.rem_euclid(300_000.0),
+ ],
+ cloud_cam: [
+ scene.camera_world[0],
+ scene.camera_world[1],
+ (scene.rain / 0.7).clamp(0.0, 1.0) * 0.18,
+ if self.volumes.history_ready()
+ && self.taa.active
+ && scene.camera_delta.length() < 64.0
+ {
+ 1.0
+ } else {
+ 0.0
+ },
+ ],
+ cloud_dir: [cloud_dir.x, cloud_dir.y, cloud_dir.z, 0.0],
+ cloud_sun: [
+ cloud_colour.x,
+ cloud_colour.y,
+ cloud_colour.z,
+ cloud_strength,
+ ],
+ vfog: [
+ if self.fog_active { 1.0 } else { 0.0 },
+ scene.camera_world[2],
+ mist,
+ 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,
+ self.size[1] as f32,
+ 1.0 / self.size[0] as f32,
+ 1.0 / self.size[1] as f32,
+ ],
+ params: [
+ n as f32,
+ settings.exposure,
+ settings.bloom,
+ (self.frames % 64) as f32,
+ ],
+ cascades: cascades.map(|m| m.to_cols_array_2d()),
+ sun: [dir.x, dir.y, dir.z, strength],
+ sun_color: [color.x, color.y, color.z, night],
+ fog_color: scene.fog_color,
+ fog: scene.fog,
+ // In the shade by day a surface keeps half its sky light; at night almost all.
+ ambient: [
+ 0.5 + 0.45 * night,
+ 1.0,
+ 0.0018 + scene.rain * 0.006,
+ self.ao.strength.max(0.0),
+ ],
+ shadow: [
+ if strength > 0.0 { 1.0 } else { 0.0 },
+ CASCADES[0],
+ CASCADES[1],
+ self.lighting.tiles_x as f32,
+ ],
+ };
+ queue.write_buffer(&self.frame, 0, bytemuck::bytes_of(&frame));
+ }
+
+ fn draw(
+ encoder: &mut wgpu::CommandEncoder,
+ label: &str,
+ to: &wgpu::TextureView,
+ load: bool,
+ pipeline: &wgpu::RenderPipeline,
+ group: &wgpu::BindGroup,
+ timestamp_writes: Option<wgpu::RenderPassTimestampWrites<'_>>,
+ ) {
+ let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some(label),
+ timestamp_writes,
+ color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+ view: to,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: if load {
+ wgpu::LoadOp::Load
+ } else {
+ wgpu::LoadOp::Clear(wgpu::Color::BLACK)
+ },
+ store: wgpu::StoreOp::Store,
+ },
+ })],
+ ..Default::default()
+ });
+ pass.set_pipeline(pipeline);
+ pass.set_bind_group(0, group, &[]);
+ pass.draw(0..3, 0..1);
+ }
+
+ /// A bind group of consecutive bindings 0.., counted (`groups_last_frame`).
+ fn make_group(
+ &self,
+ device: &wgpu::Device,
+ label: &str,
+ layout: &wgpu::BindGroupLayout,
+ resources: &[wgpu::BindingResource<'_>],
+ ) -> wgpu::BindGroup {
+ make_group(&self.groups_made, device, label, layout, resources)
+ }
+
+ /// Times the HD passes on the GPU while `on` (and the device can), else not.
+ pub fn set_timing(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, on: bool) {
+ if on != self.timing.is_some() {
+ self.timing = on.then(|| PassTiming::new(device, queue)).flatten();
+ }
+ }
+
+ /// At the start of the frame's passes, before the first of them.
+ pub fn timing_begin(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
+ if let Some(t) = &mut self.timing {
+ t.begin(device, queue);
+ }
+ }
+
+ /// After the frame is submitted.
+ pub fn timing_submitted(&mut self, queue: &wgpu::Queue) {
+ if let Some(t) = &mut self.timing {
+ t.submitted(queue);
+ }
+ }
+
+ /// Each HD pass's median GPU time in milliseconds (bloom's passes summed into one).
+ pub fn pass_times(&self) -> Vec<(String, f32)> {
+ let Some(t) = &self.timing else {
+ return Vec::new();
+ };
+ let mut out: Vec<(String, f32)> = Vec::new();
+ for (name, ms) in t.medians() {
+ let name = name.split('#').next().unwrap_or(&name).to_owned();
+ match out.iter_mut().find(|o| o.0 == name) {
+ Some(o) => o.1 += ms,
+ None => out.push((name, ms)),
+ }
+ }
+ out
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use naga::{AddressSpace, ImageClass, ImageDimension, TypeInner};
+
+ /// Every HD shader: its name, its source (before the prelude), and the bind group 0 its
+ /// Rust layout declares.
+ fn shaders() -> Vec<(&'static str, String, &'static [wgpu::BindingType])> {
+ let mut all: Vec<(&'static str, String, &'static [wgpu::BindingType])> = vec![
+ (
+ "light",
+ lighting::LIGHT.to_owned(),
+ &lighting::LIGHT_BINDINGS,
+ ),
+ ("bloom", bloom::BLOOM.to_owned(), &bloom::BLOOM_BINDINGS),
+ (
+ "tonemap",
+ tonemap::TONEMAP.to_owned(),
+ &tonemap::TONEMAP_BINDINGS,
+ ),
+ ("taa", taa::TAA.to_owned(), &taa::TAA_BINDINGS),
+ ("ao", ao::AO.to_owned(), &ao::AO_BINDINGS),
+ ("ao blur", ao::AO_BLUR.to_owned(), &ao::AO_BLUR_BINDINGS),
+ ("water", water::WATER.to_owned(), &water::WATER_BINDINGS),
+ ("sky", sky::SKY_PASS.to_owned(), &sky::SKY_BINDINGS),
+ ];
+ all.extend(volumes::shader_sources());
+ all
+ }
+
+ fn parse(name: &str, body: &str) -> naga::Module {
+ let source = assemble(body);
+ let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| {
+ panic!("{name}: {}", e.emit_to_string(&source));
+ });
+ naga::valid::Validator::new(
+ naga::valid::ValidationFlags::all(),
+ naga::valid::Capabilities::all(),
+ )
+ .validate(&module)
+ .unwrap_or_else(|e| panic!("{name}: {e:?}"));
+ module
+ }
+
+ #[test]
+ fn lighting_shader_parses() {
+ for (name, body, _) in shaders() {
+ parse(name, &body);
+ }
+ }
+
+ /// Each shader's file is part of some tested shader, so a new `.wgsl` cannot be forgotten.
+ #[test]
+ fn every_wgsl_file_is_tested() {
+ let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/gfx/hd");
+ let sources: Vec<String> = shaders().into_iter().map(|s| s.1).collect();
+ let mut seen = 0;
+ for entry in std::fs::read_dir(dir).unwrap() {
+ let path = entry.unwrap().path();
+ if path.extension().is_none_or(|e| e != "wgsl") {
+ continue;
+ }
+ let text = std::fs::read_to_string(&path).unwrap();
+ let is_prelude = text == frame::PRELUDE;
+ assert!(
+ is_prelude || {
+ let shown = assemble(text.trim());
+ let shown = &shown[frame::PRELUDE.len()..];
+ sources.iter().any(|s| {
+ let s = assemble(s);
+ // The sky snippet's binding number is filled in by `assemble`.
+ shown.split("SKYBIND").all(|part| s.contains(part))
+ })
+ },
+ "{} is not part of any tested shader",
+ path.display()
+ );
+ seen += 1;
+ }
+ assert!(seen >= 14, "found only {seen} shader files");
+ }
+
+ /// Rust's binding types and each shader's declarations agree (count, kind, dimension).
+ #[test]
+ fn bindings_match_the_layouts() {
+ for (name, body, bindings) in shaders() {
+ let module = parse(name, &body);
+ let mut declared: Vec<(u32, &naga::GlobalVariable)> = module
+ .global_variables
+ .iter()
+ .filter_map(|(_, g)| g.binding.as_ref().map(|b| (b, g)))
+ .map(|(b, g)| {
+ assert_eq!(b.group, 0, "{name}: only group 0 is used");
+ (b.binding, g)
+ })
+ .collect();
+ declared.sort_by_key(|d| d.0);
+ let numbers: Vec<u32> = declared.iter().map(|d| d.0).collect();
+ assert_eq!(
+ numbers,
+ (0..bindings.len() as u32).collect::<Vec<_>>(),
+ "{name}: bindings"
+ );
+ for ((binding, global), kind) in declared.iter().zip(bindings) {
+ let inner = &module.types[global.ty].inner;
+ let ok = match (kind, &global.space, inner) {
+ (
+ wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ ..
+ },
+ AddressSpace::Uniform,
+ _,
+ ) => true,
+ (
+ wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Storage { .. },
+ ..
+ },
+ AddressSpace::Storage { .. },
+ _,
+ ) => true,
+ (
+ wgpu::BindingType::Texture {
+ sample_type,
+ view_dimension,
+ ..
+ },
+ AddressSpace::Handle,
+ TypeInner::Image {
+ dim,
+ arrayed,
+ class,
+ },
+ ) => {
+ let dimension = match (dim, arrayed) {
+ (ImageDimension::D2, false) => wgpu::TextureViewDimension::D2,
+ (ImageDimension::D2, true) => wgpu::TextureViewDimension::D2Array,
+ (ImageDimension::D3, false) => wgpu::TextureViewDimension::D3,
+ _ => panic!("{name}: binding {binding}: unexpected image"),
+ };
+ let depth = matches!(class, ImageClass::Depth { .. });
+ dimension == *view_dimension
+ && depth == matches!(sample_type, wgpu::TextureSampleType::Depth)
+ }
+ (
+ wgpu::BindingType::Sampler(ty),
+ AddressSpace::Handle,
+ TypeInner::Sampler { comparison },
+ ) => *comparison == matches!(ty, wgpu::SamplerBindingType::Comparison),
+ _ => false,
+ };
+ assert!(ok, "{name}: binding {binding} disagrees with its layout");
+ }
+ }
+ }
+
+ /// `HdFrame` in Rust and in the prelude list the same fields and take the same room.
+ #[test]
+ fn frame_layout_matches_the_prelude() {
+ let module = parse("prelude", "");
+ let (_, ty) = module
+ .types
+ .iter()
+ .find(|(_, t)| t.name.as_deref() == Some("HdFrame"))
+ .expect("HdFrame in the prelude");
+ let TypeInner::Struct { members, span } = &ty.inner else {
+ panic!("HdFrame is not a struct");
+ };
+ assert_eq!(*span as usize, std::mem::size_of::<HdFrame>());
+ let rust: Vec<&str> = include_str!("frame.rs")
+ .split("pub(super) struct HdFrame {")
+ .nth(1)
+ .unwrap()
+ .split("\n}")
+ .next()
+ .unwrap()
+ .lines()
+ .filter_map(|l| l.trim().strip_prefix("pub(super) "))
+ .filter_map(|l| l.split(':').next())
+ .collect();
+ let wgsl: Vec<&str> = members.iter().map(|m| m.name.as_deref().unwrap()).collect();
+ assert_eq!(rust, wgsl);
+ assert_eq!(std::mem::size_of::<HdFrame>() % 16, 0);
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd_noise.rs b/crates/horizon-client/src/gfx/hd/noise.rs
similarity index 100%
rename from crates/horizon-client/src/gfx/hd_noise.rs
rename to crates/horizon-client/src/gfx/hd/noise.rs
diff --git a/crates/horizon-client/src/gfx/hd/prelude.wgsl b/crates/horizon-client/src/gfx/hd/prelude.wgsl
new file mode 100644
index 000000000..bd43a7c75
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/prelude.wgsl
@@ -0,0 +1,39 @@
+
+struct HdFrame {
+ inverse_view_proj: mat4x4<f32>,
+ resolution: vec4<f32>,
+ params: vec4<f32>,
+ cascades: array<mat4x4<f32>, 2>,
+ sun: vec4<f32>,
+ sun_color: vec4<f32>,
+ fog_color: vec4<f32>,
+ fog: vec4<f32>,
+ ambient: vec4<f32>,
+ shadow: vec4<f32>,
+ prev_view_proj: mat4x4<f32>,
+ taa: vec4<f32>,
+ taa_params: vec4<f32>,
+ view_proj: mat4x4<f32>,
+ water: vec4<f32>,
+ sky_color: vec4<f32>,
+ sky_sun: vec4<f32>,
+ sky_extra: vec4<f32>,
+ sunrise: vec4<f32>,
+ clouds: vec4<f32>,
+ cloud_cam: vec4<f32>,
+ cloud_dir: vec4<f32>,
+ cloud_sun: vec4<f32>,
+ vfog: vec4<f32>,
+}
+struct FullOut {
+ @builtin(position) position: vec4<f32>,
+ @location(0) uv: vec2<f32>,
+}
+@vertex
+fn vs_full(@builtin(vertex_index) i: u32) -> FullOut {
+ let uv = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
+ var out: FullOut;
+ out.position = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0);
+ out.uv = uv;
+ return out;
+}
diff --git a/crates/horizon-client/src/gfx/hd/shadows.rs b/crates/horizon-client/src/gfx/hd/shadows.rs
new file mode 100644
index 000000000..daa34b3c6
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/shadows.rs
@@ -0,0 +1,121 @@
+//! Sun and moon shadows: two cascades in one `Depth32Float` array.
+//!
+//! Contract:
+//! - Writes: `Shadows::array` (2 layers, `SHADOW_SIZE` squared), one pass per layer, cleared to 1.
+//! The terrain and entity shadow shaders (`terrain.wgsl`, `entity.wgsl`) draw into it with the
+//! world globals replaced by the cascade's globals (`Hd::shadow_globals`), then restored.
+//! - Depth convention: the light's orthographic projection is *normal* Z (0 near the light,
+//! 1 far), unlike the camera's reversed-Z. A texel is lit when its depth is `<=` the stored one
+//! (comparison sampler, `LessEqual`).
+//! - Space: `cascades()` returns camera-relative position to the light's clip space; each
+//! cascade's square is snapped to its texels in the world, so shadows do not crawl.
+//! - Readers: lighting (`fs_light`, bindings 8-9), water composite (6-7) and the volumetric fog.
+//! - The light is the sun by day and the moon at night (`frame::light_of`).
+use super::{Hd, stamp};
+use glam::Mat4;
+
+/// The cascades' half sizes (blocks) and shadow map size.
+pub const CASCADES: [f32; 2] = [40.0, 176.0];
+pub const SHADOW_SIZE: u32 = 2048;
+
+/// The cascade array, a view per layer to draw, and the comparison sampler.
+pub(super) struct Shadows {
+ pub(super) array: wgpu::TextureView,
+ layers: Vec<wgpu::TextureView>,
+ pub(super) sampler: wgpu::Sampler,
+}
+
+impl Shadows {
+ pub(super) fn new(device: &wgpu::Device) -> Self {
+ let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("HD shadow cascades"),
+ size: wgpu::Extent3d {
+ width: SHADOW_SIZE,
+ height: SHADOW_SIZE,
+ depth_or_array_layers: 2,
+ },
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Depth32Float,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+ let array = shadow_texture.create_view(&wgpu::TextureViewDescriptor {
+ dimension: Some(wgpu::TextureViewDimension::D2Array),
+ ..Default::default()
+ });
+ let layers: Vec<wgpu::TextureView> = (0..2)
+ .map(|l| {
+ shadow_texture.create_view(&wgpu::TextureViewDescriptor {
+ dimension: Some(wgpu::TextureViewDimension::D2),
+ base_array_layer: l,
+ array_layer_count: Some(1),
+ ..Default::default()
+ })
+ })
+ .collect();
+ let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+ label: Some("HD shadows"),
+ mag_filter: wgpu::FilterMode::Linear,
+ min_filter: wgpu::FilterMode::Linear,
+ compare: Some(wgpu::CompareFunction::LessEqual),
+ ..Default::default()
+ });
+ Self {
+ array,
+ layers,
+ sampler,
+ }
+ }
+}
+
+impl Hd {
+ /// The shadow pass of cascade `c`: its depth layer cleared, nothing else.
+ pub fn shadow_pass<'e>(
+ &mut self,
+ encoder: &'e mut wgpu::CommandEncoder,
+ c: usize,
+ ) -> wgpu::RenderPass<'e> {
+ encoder
+ .begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("HD shadow cascade"),
+ timestamp_writes: stamp(&mut self.timing, ["hd shadow 0", "hd shadow 1"][c.min(1)]),
+ color_attachments: &[],
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+ view: &self.shadows.layers[c],
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(1.0),
+ store: wgpu::StoreOp::Store,
+ }),
+ stencil_ops: None,
+ }),
+ ..Default::default()
+ })
+ .forget_lifetime()
+ }
+}
+
+/// The cascades' light view-projections (camera-relative), each square snapped to its texels in
+/// the world so the shadows don't crawl as the camera moves.
+pub fn cascades(camera: glam::DVec3, light: glam::Vec3) -> [Mat4; 2] {
+ let l = light.normalize_or(glam::Vec3::Y).as_dvec3();
+ let forward = -l;
+ let reference = if l.y.abs() > 0.99 {
+ glam::DVec3::Z
+ } else {
+ glam::DVec3::Y
+ };
+ let right = forward.cross(reference).normalize();
+ let up = right.cross(forward);
+ CASCADES.map(|r| {
+ let texel = f64::from(r) * 2.0 / f64::from(SHADOW_SIZE);
+ let snap = |v: f64| (v / texel).floor() * texel - v;
+ let centre = right * snap(camera.dot(right)) + up * snap(camera.dot(up));
+ let eye = centre + l * 400.0;
+ let view = glam::DMat4::look_at_rh(eye, centre, up);
+ let r = f64::from(r);
+ let projection = glam::DMat4::orthographic_rh(-r, r, -r, r, 1.0, 800.0);
+ (projection * view).as_mat4()
+ })
+}
diff --git a/crates/horizon-client/src/gfx/hd/sky.rs b/crates/horizon-client/src/gfx/hd/sky.rs
new file mode 100644
index 000000000..7211ffda2
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/sky.rs
@@ -0,0 +1,160 @@
+//! AirDef fork: the HD renderer's atmosphere. A small sky-view texture (azimuth from the sun by
+//! elevation, plus a row of ambient samples) is marched each frame from single scattering of
+//! Rayleigh and Mie particles and ozone absorption with a cheap multiple-scattering term; the
+//! lighting and water shaders read it for the sky, the aerial-perspective haze, the water's
+//! reflection fallback and the colour of the sky's light. The sunlight's colour is the same
+//! atmosphere's transmittance at the sun's elevation (`sun_transmittance`).
+//!
+//! Contract:
+//! - Reads only `HdFrame` (`sky_sun`, `sun`, ...): the texture depends on the sun's height alone,
+//! so it is marched again only when that moved more than 0.001 (`stale`).
+//! - Writes `lut` (`Rgba16Float`, 128 x 72): 128 azimuths from the sun (0..pi) x 64 elevations
+//! (v = sqrt(h / (pi / 2))), then row 64 for the ambient samples (texel 0 zenith, 1 the horizon's
+//! average). Texels hold **linear physical radiance in Rec. 709** (marched in Rec. 2020 with
+//! Photon's coefficients and converted); alpha 1 where written. It is *shown* only by readers
+//! (`sky_show`: compress by the brightest channel with `SKY_EXPOSURE`, gamma 1/1.9, saturation
+//! boost), which puts it in the pipeline's display space.
+//! - Readers: `fs_light` (binding 15) and the water composite (binding 10) through the snippets of
+//! `sky_funcs.wgsl` that `assemble` pastes at a `//SKY<n>` marker.
+//!
+//! The coefficients, the density profiles (scale heights, the ozone layer), the Henyey-Greenstein
+//! Mie parameters and the Chapman-function transmittance come from Photon by sixthsurge (MIT
+//! licence, see THIRD_PARTY_PHOTON.md), shaders/include/sky/atmosphere.glsl at commit 26c2906.
+use super::{Hd, UNIFORM, entries, fullscreen, module, stamp, target};
+
+/// The sky-view texture: 128 azimuths by 64 elevations, then 8 rows for the ambient samples
+/// (row 64: texel 0 the zenith, texel 1 the horizon's average).
+pub const SKY_LUT_SIZE: [u32; 2] = [128, 72];
+pub const SKY_LUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
+
+/// The atmosphere's constants and the pass that marches the sky-view texture (after `COMMON`).
+pub(super) const SKY_PASS: &str = include_str!("sky.wgsl");
+
+/// Reading the sky-view texture, in the lighting and the water shaders (`SKYBIND` is replaced by
+/// the binding of the texture; `frame` and the `linear` sampler come before it).
+pub(super) const SKY_FUNCS: &str = include_str!("sky_funcs.wgsl");
+
+/// The sky pass's group 0: only the frame.
+pub(super) const SKY_BINDINGS: [wgpu::BindingType; 1] = [UNIFORM];
+
+/// The sky-view texture, the pass that marches it and its bind group.
+pub(super) struct Sky {
+ pub(super) lut: wgpu::TextureView,
+ pipeline: wgpu::RenderPipeline,
+ group: wgpu::BindGroup,
+ /// The sun's height the texture was marched for (it depends on nothing else).
+ pub(super) drawn_for: f32,
+ /// This frame's sun has moved enough to march it again.
+ pub(super) stale: bool,
+}
+
+impl Sky {
+ pub(super) fn new(device: &wgpu::Device, frame: &wgpu::Buffer) -> Self {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD sky"),
+ entries: &entries(&SKY_BINDINGS),
+ });
+ let sky_module = module(device, "HD sky", SKY_PASS);
+ let pipeline = fullscreen(
+ device,
+ "HD sky",
+ &layout,
+ &sky_module,
+ "fs_sky_lut",
+ SKY_LUT_FORMAT,
+ None,
+ );
+ let lut = target(device, "HD sky", SKY_LUT_SIZE, SKY_LUT_FORMAT).view;
+ let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("HD sky"),
+ layout: &layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: frame.as_entire_binding(),
+ }],
+ });
+ Self {
+ lut,
+ pipeline,
+ group,
+ drawn_for: 2.0,
+ stale: true,
+ }
+ }
+}
+
+impl Hd {
+ /// Marches the sky-view texture for this frame's sun (before the lighting and the water); skipped
+ /// while the sun's height has moved less than a thousandth since the last march.
+ pub fn sky(&mut self, encoder: &mut wgpu::CommandEncoder) {
+ if !self.sky.stale {
+ return;
+ }
+ Self::draw(
+ encoder,
+ "HD sky",
+ &self.sky.lut,
+ false,
+ &self.sky.pipeline,
+ &self.sky.group,
+ stamp(&mut self.timing, "hd sky"),
+ );
+ }
+}
+
+/// The sun's light at the ground: the atmosphere's transmittance along the sun's direction
+/// (`sin_elevation`, the y of the unit vector towards it), linear, as a fraction of the light
+/// outside the air. Same Chapman model as the sky pass.
+pub fn sun_transmittance(sin_elevation: f32) -> glam::Vec3 {
+ const PLANET: f32 = 6_371_000.0;
+ const SCALE: [f32; 2] = [8400.0, 1250.0];
+ // The same Rec. 2020 coefficients as the sky pass; the result is moved back to Rec. 709.
+ const RAYLEIGH: glam::Vec3 = glam::Vec3::new(1.232_851_7e-5, 1.638_796_1e-5, 3.814_816_5e-5);
+ const MIE: glam::Vec3 = glam::Vec3::new(1.727_386_2e-6, 1.804_192_8e-6, 1.941_423_4e-6);
+ const OZONE: glam::Vec3 = glam::Vec3::new(9.563_766e-7, 1.267_105_3e-6, 1.780_727e-7);
+ // A sun below the horizon keeps the horizon's colour (its strength is faded elsewhere).
+ let mu = sin_elevation.clamp(0.0, 1.0);
+ let chapman = |x: f32, c: f32| {
+ let k = (std::f32::consts::FRAC_PI_2 * x).sqrt();
+ k / ((k - 1.0) * c + 1.0)
+ };
+ let air = [
+ SCALE[0] * chapman(PLANET / SCALE[0], mu),
+ SCALE[1] * chapman(PLANET / SCALE[1], mu),
+ ];
+ let tau = RAYLEIGH * air[0] + MIE * air[1] + OZONE * air[0];
+ let t = glam::Vec3::new((-tau.x).exp(), (-tau.y).exp(), (-tau.z).exp());
+ let to_709 = glam::Mat3::from_cols(
+ glam::Vec3::new(1.660_344, -0.124_378_68, -0.018_112_72),
+ glam::Vec3::new(-0.587_571_6, 1.132_837, -0.100_583_79),
+ glam::Vec3::new(-0.072_880_24, -0.008_358_68, 1.118_621_3),
+ );
+ (to_709 * t).max(glam::Vec3::ZERO)
+}
+
+/// Photon's tint of the sunlight (`get_sun_tint`, light_color.glsl): pink when the sun is low, violet
+/// at the blue hour. `y`: the sun's height (the y of the unit vector towards it).
+pub fn sun_tint(y: f32) -> glam::Vec3 {
+ let blue_hour =
+ (((-190.0 * (y + 0.096_04) * (y + 0.096_04)).exp() - 0.05) / 0.95).clamp(0.0, 1.0);
+ let pulse = {
+ let t = ((y - 0.17).abs() / 0.40).min(1.0);
+ 1.0 - t * t * (3.0 - 2.0 * t)
+ };
+ let evening = glam::Vec3::ONE.lerp(glam::Vec3::new(1.05, 0.84, 0.93) * 1.2, pulse * pulse);
+ let blue = glam::Vec3::ONE.lerp(glam::Vec3::new(0.95, 0.80, 1.0), blue_hour);
+ evening * blue
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn the_low_sun_is_redder_than_the_high_one() {
+ let noon = sun_transmittance(1.0);
+ let low = sun_transmittance(0.1);
+ assert!(noon.x > 0.8 && noon.z > 0.6, "{noon:?}");
+ assert!(low.z / low.x < noon.z / noon.x * 0.6, "{low:?} vs {noon:?}");
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/sky.wgsl b/crates/horizon-client/src/gfx/hd/sky.wgsl
new file mode 100644
index 000000000..8fdbe8b55
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/sky.wgsl
@@ -0,0 +1,172 @@
+
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+
+// Photon's atmosphere (sixthsurge, MIT): metres, Earth's size; the camera stands on the surface.
+const PI: f32 = 3.14159265;
+const PLANET_R: f32 = 6371000.0;
+const ATMOSPHERE_TOP: f32 = 6481000.0;
+const GROUND_R: f32 = 6370000.0;
+const SCALE_HEIGHTS: vec2<f32> = vec2<f32>(8400.0, 1250.0);
+// Photon marches in Rec. 2020 (its working colour space): the coefficients are the Rec. 709 ones
+// moved to it, and the radiance goes back to Rec. 709 at the end. This keeps the sunset's reds and
+// purples apart where the narrower primaries would mix them into yellow and green.
+const RAYLEIGH: vec3<f32> = vec3<f32>(1.232851742e-05, 1.638796144e-05, 3.814816455e-05);
+const MIE: vec3<f32> = vec3<f32>(1.727386213e-06, 1.804192789e-06, 1.941423374e-06);
+const OZONE: vec3<f32> = vec3<f32>(9.563766055e-07, 1.267105338e-06, 1.780727031e-07);
+const REC2020_TO_709 = mat3x3<f32>(
+ vec3<f32>(1.66034406, -0.12437868, -0.01811272),
+ vec3<f32>(-0.58757158, 1.13283707, -0.10058379),
+ vec3<f32>(-0.07288024, -0.00835868, 1.11862127));
+const MIE_KN_ENERGY: f32 = 3000.0;
+const MIE_ALBEDO: f32 = 0.9;
+const MIE_G: f32 = 0.77;
+// Sunlight in space (Photon's AM0 colour) and how bright the sun and the moon shine here.
+const SUN_I: vec3<f32> = vec3<f32>(1.051, 0.985, 0.940) * 24.0;
+const MOON_I: vec3<f32> = vec3<f32>(0.62, 0.78, 1.0) * 0.07;
+// The share of the light scattered a second time, spread evenly (the haze's lift).
+const MULTI_SCATTER: f32 = 0.6;
+const STEPS: i32 = 32;
+
+// Photon's density: Rayleigh, Mie (exponential) and the ozone layer, at an altitude in metres.
+fn density_at(altitude: f32) -> vec3<f32> {
+ let rm = exp(-max(altitude, 0.0) / SCALE_HEIGHTS);
+ let km = altitude * 1e-3;
+ let o1 = 12.5 * exp(-km / 8.0);
+ let o2 = 30.0 * exp((18.0 - km) * (km - 18.0) / 80.0);
+ let o3 = 75.0 * exp((23.5 - km) * (km - 23.5) / 50.0);
+ let o4 = 50.0 * exp((30.0 - km) * (km - 30.0) / 150.0);
+ return vec3<f32>(rm, 7.428e-3 * (o1 + o2 + o3 + o4));
+}
+
+// Chapman function approximation (thetenthplanet.de/archives/4519, as Photon uses it).
+fn chapman(x: f32, cos_theta: f32) -> f32 {
+ let c = sqrt(1.5707963 * x);
+ if cos_theta >= 0.0 {
+ return c / ((c - 1.0) * cos_theta + 1.0);
+ }
+ let sin_theta = sqrt(clamp(1.0 - cos_theta * cos_theta, 0.0, 1.0));
+ return c / ((c - 1.0) * cos_theta - 1.0) + 2.0 * c * exp(min(x - x * sin_theta, 80.0)) * sqrt(sin_theta);
+}
+
+// The light reaching a point r from the planet's centre (altitude h, whose sky direction makes
+// cos mu with the up there) from the sun or the moon; 0 when the planet is in the way.
+fn light_transmittance(mu: f32, r: f32, h: f32) -> vec3<f32> {
+ if mu < 0.0 && r * r * (1.0 - mu * mu) < PLANET_R * PLANET_R { return vec3<f32>(0.0); }
+ let dens = exp(-max(h, 0.0) / SCALE_HEIGHTS);
+ var air = SCALE_HEIGHTS * dens;
+ air.x *= chapman(r / SCALE_HEIGHTS.x, mu);
+ air.y *= chapman(r / SCALE_HEIGHTS.y, mu);
+ return clamp(exp(-(RAYLEIGH * air.x + MIE * air.y + OZONE * air.x)), vec3<f32>(0.0), vec3<f32>(1.0));
+}
+
+// Photon's Rayleigh phase with the depolarisation per channel (the sky's slight tint).
+fn rayleigh_phase(nu: f32) -> vec3<f32> {
+ let depolarization = vec3<f32>(2.786, 2.842, 2.899) * 1e-2;
+ let gamma = depolarization / (vec3<f32>(2.0) - depolarization);
+ let k = 3.0 / (16.0 * PI * (vec3<f32>(1.0) + 2.0 * gamma));
+ return k * ((vec3<f32>(1.0) + 3.0 * gamma) + (vec3<f32>(1.0) - gamma) * nu * nu);
+}
+
+// Photon's Klein-Nishina phase for the aerosols ("far closer to an actual aerosol phase function
+// than Henyey-Greenstein"): a tight, bright glow round the sun.
+fn mie_phase(nu: f32) -> f32 {
+ let e = MIE_KN_ENERGY;
+ return e / (2.0 * PI * (e - e * nu + 1.0) * log(2.0 * e + 1.0));
+}
+
+fn linear_step(a: f32, b: f32, x: f32) -> f32 {
+ return clamp((x - a) / (b - a), 0.0, 1.0);
+}
+// Photon's pulse: 1 at the centre, 0 at the width, smooth between.
+fn pulse(x: f32, centre: f32, width: f32) -> f32 {
+ let t = abs(x - centre) / width;
+ if t > 1.0 { return 0.0; }
+ return 1.0 - t * t * (3.0 - 2.0 * t);
+}
+
+// The sunlight's tint and extra strength round sunrise and sunset, from Photon's
+// get_sun_tint / get_sun_exposure (light_color.glsl): pinkish when the sun is low, violet at the
+// blue hour, and a strong lift then so the twilight sky does not go dark. `y`: the sun's height.
+fn sun_light_tint(y: f32) -> vec3<f32> {
+ let blue_hour = linear_step(0.05, 1.0, exp(-190.0 * (y + 0.09604) * (y + 0.09604)));
+ let p = pulse(y, 0.17, 0.40);
+ let morning_evening = mix(vec3<f32>(1.0), vec3<f32>(1.05, 0.84, 0.93) * 1.2, p * p);
+ let blue_tint = mix(vec3<f32>(1.0), vec3<f32>(0.95, 0.80, 1.0), blue_hour);
+ let low = 1.0 - smoothstep(0.0, 0.3, abs(y));
+ return morning_evening * blue_tint * (1.0 + 0.5 * low + 40.0 * blue_hour);
+}
+
+// The radiance the camera sees along `view` (y up) with the sun and the moon in the directions
+// given: single scattering plus a flat multiple-scattering term.
+fn march(view: vec3<f32>, sun: vec3<f32>, moon: vec3<f32>) -> vec3<f32> {
+ let b = PLANET_R * view.y;
+ let t_max = -b + sqrt(b * b + ATMOSPHERE_TOP * ATMOSPHERE_TOP - PLANET_R * PLANET_R);
+ let nu_sun = dot(view, sun);
+ let nu_moon = dot(view, moon);
+ let sun_i = SUN_I * sun_light_tint(frame.sky_sun.y);
+ let phase_r_sun = rayleigh_phase(nu_sun);
+ let phase_m_sun = mie_phase(nu_sun);
+ let phase_r_moon = rayleigh_phase(nu_moon);
+ let phase_m_moon = mie_phase(nu_moon);
+ var od = vec3<f32>(0.0);
+ var sum = vec3<f32>(0.0);
+ var t0 = 0.0;
+ for (var i = 0; i < STEPS; i++) {
+ let u = (f32(i) + 1.0) / f32(STEPS);
+ let t1 = t_max * u * u;
+ let dt = t1 - t0;
+ let t = 0.5 * (t0 + t1);
+ t0 = t1;
+ let r2 = PLANET_R * PLANET_R + 2.0 * PLANET_R * t * view.y + t * t;
+ let r = sqrt(r2);
+ // The altitude without the cancellation of r - planet radius.
+ let altitude = (2.0 * PLANET_R * t * view.y + t * t) / (r + PLANET_R);
+ let dens = density_at(altitude);
+ let extinction = RAYLEIGH * dens.x + MIE * dens.y + OZONE * dens.z;
+ let view_t = exp(-(od + 0.5 * extinction * dt));
+ od += extinction * dt;
+ let mu_sun = (PLANET_R * sun.y + t * nu_sun) / r;
+ let mu_moon = (PLANET_R * moon.y + t * nu_moon) / r;
+ let sun_t = light_transmittance(mu_sun, r, altitude);
+ let moon_t = light_transmittance(mu_moon, r, altitude);
+ let scatter_r = RAYLEIGH * dens.x;
+ let scatter_m = MIE * MIE_ALBEDO * dens.y;
+ let single = (scatter_r * phase_r_sun + scatter_m * phase_m_sun) * sun_t * sun_i
+ + (scatter_r * phase_r_moon + scatter_m * phase_m_moon) * moon_t * MOON_I;
+ let multi = (scatter_r + scatter_m) * (1.0 / (4.0 * PI)) * MULTI_SCATTER
+ * (sun_t * sun_i + moon_t * MOON_I) * 0.5;
+ sum += view_t * (single + multi) * dt;
+ }
+ return max(REC2020_TO_709 * sum, vec3<f32>(0.0));
+}
+
+@fragment
+fn fs_sky_lut(in: FullOut) -> @location(0) vec4<f32> {
+ let px = vec2<i32>(in.position.xy);
+ // The texture's frame: the sun in the xy plane, the moon opposite.
+ let sin_e = clamp(frame.sky_sun.y, -1.0, 1.0);
+ let sun = vec3<f32>(sqrt(1.0 - sin_e * sin_e), sin_e, 0.0);
+ let moon = -sun;
+ // A faint night glow, so the night sky is not black.
+ let night = vec3<f32>(0.0006, 0.0011, 0.0026);
+ if px.y < 64 {
+ let phi = f32(px.x) / 127.0 * PI;
+ let v = f32(px.y) / 63.0;
+ let l = v * v * PI * 0.5;
+ let dir = vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi));
+ return vec4<f32>(march(dir, sun, moon) + night, 1.0);
+ }
+ if px.y == 64 && px.x == 0 {
+ return vec4<f32>(march(vec3<f32>(0.0, 1.0, 0.0), sun, moon) + night, 1.0);
+ }
+ if px.y == 64 && px.x == 1 {
+ var sum = vec3<f32>(0.0);
+ for (var k = 0; k < 6; k++) {
+ let phi = (f32(k) + 0.5) / 6.0 * PI;
+ let l = 0.3;
+ sum += march(vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi)), sun, moon);
+ }
+ return vec4<f32>(sum / 6.0 + night, 1.0);
+ }
+ return vec4<f32>(0.0);
+}
diff --git a/crates/horizon-client/src/gfx/hd/sky_funcs.wgsl b/crates/horizon-client/src/gfx/hd/sky_funcs.wgsl
new file mode 100644
index 000000000..bea0230f1
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/sky_funcs.wgsl
@@ -0,0 +1,139 @@
+
+@group(0) @binding(SKYBIND) var sky_lut: texture_2d<f32>;
+const SKY_PI: f32 = 3.14159265;
+// How bright the sky is shown (the exposure of the physical radiance) and its gamma.
+const SKY_EXPOSURE: f32 = 0.85;
+
+// Where the direction `dir` (world, y up) lies in the sky-view texture.
+fn sky_uv(dir: vec3<f32>) -> vec2<f32> {
+ let s = frame.sky_sun.xyz;
+ let l = asin(clamp(dir.y, 0.0, 1.0));
+ var cphi = 1.0;
+ let dl = length(dir.xz);
+ let sl = length(s.xz);
+ if dl > 1e-4 && sl > 1e-4 { cphi = dot(dir.xz / dl, s.xz / sl); }
+ let u = acos(clamp(cphi, -1.0, 1.0)) / SKY_PI;
+ let v = sqrt(l / (SKY_PI * 0.5));
+ return vec2<f32>((u * 127.0 + 0.5) / 128.0, (v * 63.0 + 0.5) / 72.0);
+}
+
+// The physical sky's colour as it is shown: a soft shoulder, then the display's gamma.
+fn sky_show(radiance: vec3<f32>) -> vec3<f32> {
+ // Compressed by the brightest channel, so the hue stays as the atmosphere made it.
+ let peak = max(max(radiance.r, radiance.g), max(radiance.b, 1e-6));
+ let c = pow(radiance * ((1.0 - exp(-peak * SKY_EXPOSURE)) / peak), vec3<f32>(1.0 / 1.9));
+ // A little more colour (Photon boosts the atmosphere's saturation too).
+ // The low sun gets more, for orange and pink in place of pastel.
+ let boost = 1.2 + 0.8 * (1.0 - smoothstep(0.0, 0.35, frame.sky_sun.y));
+ return max(mix(vec3<f32>(dot(c, vec3<f32>(0.2126, 0.7152, 0.0722))), c, boost), vec3<f32>(0.0));
+}
+
+// The vanilla sky's own gradient (the overcast and the rain keep it).
+fn vanilla_gradient(dir: vec3<f32>) -> vec3<f32> {
+ return mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, max(dir.y, 0.0)));
+}
+
+// The sky along `dir`: the atmosphere, greyed towards vanilla's colours in rain.
+fn atmosphere_sky(dir: vec3<f32>) -> vec3<f32> {
+ let lin = textureSampleLevel(sky_lut, linear, sky_uv(dir), 0.0).rgb;
+ var c = sky_show(lin);
+ let overcast = frame.sky_extra.z;
+ if overcast > 0.0 { c = mix(c, vanilla_gradient(dir), overcast); }
+ return c;
+}
+
+// The colour distant things fade to: the sky just above the horizon in their direction (so the
+// haze leans towards the sun), or vanilla's fog colour where the environment sets its own fog.
+fn haze_colour(dir: vec3<f32>) -> vec3<f32> {
+ if frame.sky_sun.w < 0.5 || frame.fog.x < 1.0e6 { return frame.fog_color.rgb; }
+ let up = max(dir.y, 0.0) * 0.5 + 0.035;
+ return atmosphere_sky(normalize(vec3<f32>(dir.x, 0.0, dir.z) + vec3<f32>(0.0, up, 0.0)));
+}
+
+// The sky's light for a surface of normal n: the horizon's colour from the sides, the zenith's
+// from above, as a hue (luminance 1) to tint vanilla's sky light with.
+fn sky_light_tint(n: vec3<f32>) -> vec3<f32> {
+ let zenith = sky_show(textureLoad(sky_lut, vec2<i32>(0, 64), 0).rgb);
+ let horizon = sky_show(textureLoad(sky_lut, vec2<i32>(1, 64), 0).rgb);
+ let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
+ let c = mix(horizon, zenith, smoothstep(0.35, 1.0, up));
+ let luma = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
+ return c / max(luma, 0.003);
+}
+
+// vanilla's sky (terrain.wgsl: fs_sky), to tell what the world drew on the sky pixel (the sun and
+// moon sprites, the stars, the clouds) from its plain gradient.
+fn vanilla_disc_distance(d: vec3<f32>, y: f32) -> vec2<f32> {
+ if d.y * y <= 0.0 { return vec2<f32>(-1.0); }
+ let p = d * (y / d.y);
+ let h = length(p.xz);
+ let phi = atan2(p.z, p.x);
+ let sector = SKY_PI / 4.0;
+ let delta = phi - (floor(phi / sector) * sector + sector * 0.5);
+ let edge = 512.0 * cos(sector * 0.5) / cos(delta);
+ if h > edge { return vec2<f32>(-1.0); }
+ let s = h / edge;
+ let ay = abs(y);
+ let rim = sqrt(512.0 * 512.0 + y * y);
+ return vec2<f32>(ay + s * (rim - ay), ay + s * (512.0 - ay));
+}
+fn vanilla_linear_fog(d: f32, start: f32, end: f32) -> f32 {
+ if d <= start { return 0.0; }
+ if d >= end { return 1.0; }
+ return (d - start) / (end - start);
+}
+fn vanilla_rotate_x90(v: vec3<f32>) -> vec3<f32> { return vec3<f32>(v.x, -v.z, v.y); }
+fn vanilla_sunrise_weight(d: vec3<f32>) -> f32 {
+ let alpha = frame.sunrise.a;
+ if alpha <= 0.001 { return 0.0; }
+ let flip = select(0.0, 180.0, frame.sky_sun.x > 0.0);
+ let theta = radians(flip + 90.0);
+ let c = cos(theta);
+ let s = sin(theta);
+ // Above the horizon the fan reaches only the sun's side (x of the same sign as the apex's, -s)
+ // and rises at most alpha/3 per block of that distance (its rim stands 40*alpha high over a
+ // 120-block radius and the points above the horizon lie at least 120 * the share of the rim
+ // from the axis); anything else cannot hit it, so most of the sky skips the 16 triangles.
+ if d.y > 0.0 {
+ let side = -s * d.x;
+ if side <= 0.0 || d.y > alpha * (1.0 / 3.0) * side * 1.02 + 0.001 { return 0.0; }
+ }
+ var best = 0.0;
+ let apex = vanilla_rotate_x90(vec3<f32>(-100.0 * s, 100.0 * c, 0.0));
+ for (var i = 0; i < 16; i++) {
+ let a0 = f32(i) * 2.0 * SKY_PI / 16.0;
+ let a1 = f32(i + 1) * 2.0 * SKY_PI / 16.0;
+ let l0 = vec3<f32>(sin(a0) * 120.0, cos(a0) * 120.0, -cos(a0) * 40.0 * alpha);
+ let l1 = vec3<f32>(sin(a1) * 120.0, cos(a1) * 120.0, -cos(a1) * 40.0 * alpha);
+ let r0 = vanilla_rotate_x90(vec3<f32>(l0.x * c - l0.y * s, l0.x * s + l0.y * c, l0.z));
+ let r1 = vanilla_rotate_x90(vec3<f32>(l1.x * c - l1.y * s, l1.x * s + l1.y * c, l1.z));
+ let e1 = r0 - apex;
+ let e2 = r1 - apex;
+ let pv = cross(d, e2);
+ let det = dot(e1, pv);
+ if abs(det) < 1e-6 { continue; }
+ let tv = -apex;
+ let u = dot(tv, pv) / det;
+ let qv = cross(tv, e1);
+ let v = dot(d, qv) / det;
+ let t = dot(e2, qv) / det;
+ if u >= 0.0 && v >= 0.0 && u + v <= 1.0 && t > 0.0 {
+ best = max(best, 1.0 - u - v);
+ }
+ }
+ return best;
+}
+fn vanilla_sky(d: vec3<f32>) -> vec3<f32> {
+ var color = frame.fog_color.rgb;
+ let dist = vanilla_disc_distance(d, 16.0);
+ if dist.x >= 0.0 {
+ let end = frame.sky_extra.x;
+ let f = max(vanilla_linear_fog(dist.x, 0.0, end), vanilla_linear_fog(dist.y, end, end));
+ color = mix(frame.sky_color.rgb, frame.fog_color.rgb, f * frame.fog_color.a);
+ }
+ let w = vanilla_sunrise_weight(d);
+ if w > 0.0 {
+ color = mix(color, frame.sunrise.rgb, w * frame.sunrise.a);
+ }
+ return color;
+}
diff --git a/crates/horizon-client/src/gfx/hd/taa.rs b/crates/horizon-client/src/gfx/hd/taa.rs
new file mode 100644
index 000000000..c21c92482
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/taa.rs
@@ -0,0 +1,299 @@
+//! Temporal anti-aliasing and, when the world is smaller than the output, temporal upscaling
+//! (TAAU). Also the jitter sequence and the HD environment switches.
+//!
+//! Contract:
+//! - Reads: the colour source (`HdSource::Lit`/`Fx`, the lit world with water, or the mod's post
+//! output; display space, world size), the *world* depth (reversed-Z, the pass's own depth from the
+//! world pass, not the G-buffer's), and the history (output size, `Rgba16Float`). `HdFrame`:
+//! `prev_view_proj` (last frame's matrix; **with** jitter for `fs_taa`, **without** for `fs_taau`,
+//! chosen in `Hd::write`), `taa` (xyz the camera's move since, w history usable), `taa_params`
+//! (x history weight, y sharpening, zw jitter in world pixels).
+//! - Writes `target` (the resolved frame, output size) and the next history together (two
+//! targets); history alpha holds the camera distance for disocclusion. The histories ping-pong
+//! (`flip`); a teleport > 64 blocks, `resize` or TAA off starts them over.
+//! - Jitter: Halton(2,3) in clip space (`jitter`), added to the world `view_projection` only while HD
+//! draws; the public matrix stays unjittered. The hand and the HUD are drawn after tonemap.
+//! - A history belongs to one camera and one output size: do not feed one history from two cameras.
+use super::{
+ DEPTH, FLOAT, HDR, Hd, HdSource, SAMPLER, UNFILTERED, UNIFORM, entries, module, stamp,
+};
+
+pub(super) const TAA: &str = include_str!("taa.wgsl");
+/// Group 0 of `fs_taa` / `fs_taau`: frame, the colour, the world depth, the history, the sampler.
+pub(super) const TAA_BINDINGS: [wgpu::BindingType; 5] =
+ [UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER];
+
+/// TAA is on unless `HORIZON_HD_TAA=0`.
+pub fn taa_enabled() -> bool {
+ static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
+ *ON.get_or_init(|| std::env::var("HORIZON_HD_TAA").map_or(true, |v| v != "0"))
+}
+
+/// The Halton sequence's `index`-th number in `base`, in (0, 1).
+fn halton(mut index: u32, base: u32) -> f32 {
+ let (mut f, mut r) = (1.0, 0.0);
+ while index > 0 {
+ f /= base as f32;
+ r += f * (index % base) as f32;
+ index /= base;
+ }
+ 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).
+pub(super) 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 `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,
+ ]
+}
+
+pub(super) struct Taa {
+ /// The resolved frame (output size), and the two histories (one read, one written, alternating).
+ pub(super) target: wgpu::TextureView,
+ history: [wgpu::TextureView; 2],
+ pub(super) history_ready: bool,
+ history_flip: usize,
+ /// This frame is anti-aliased (set by `write`).
+ pub(super) active: bool,
+ layout: wgpu::BindGroupLayout,
+ pipeline: wgpu::RenderPipeline,
+ taau_pipeline: wgpu::RenderPipeline,
+ groups: Option<((wgpu::TextureView, wgpu::TextureView), [wgpu::BindGroup; 2])>,
+}
+
+impl Taa {
+ pub(super) fn new(device: &wgpu::Device) -> Self {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD TAA"),
+ entries: &entries(&TAA_BINDINGS),
+ });
+ let taa_module = module(device, "HD TAA", TAA);
+ let pipeline = |entry: &str| {
+ let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("HD TAA"),
+ bind_group_layouts: &[Some(&layout)],
+ ..Default::default()
+ });
+ let target = Some(wgpu::ColorTargetState {
+ format: HDR,
+ blend: None,
+ write_mask: wgpu::ColorWrites::ALL,
+ });
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("HD TAA"),
+ layout: Some(&layout),
+ vertex: wgpu::VertexState {
+ module: &taa_module,
+ entry_point: Some("vs_full"),
+ compilation_options: Default::default(),
+ buffers: &[],
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &taa_module,
+ entry_point: Some(entry),
+ compilation_options: Default::default(),
+ targets: &[target.clone(), target],
+ }),
+ primitive: Default::default(),
+ depth_stencil: None,
+ multisample: Default::default(),
+ multiview_mask: None,
+ cache: None,
+ })
+ };
+ let (pipeline, taau_pipeline) = (pipeline("fs_taa"), pipeline("fs_taau"));
+ Self {
+ target: super::target(device, "HD TAA", [1, 1], HDR).view,
+ history: [
+ super::target(device, "HD TAA history", [1, 1], HDR).view,
+ super::target(device, "HD TAA history", [1, 1], HDR).view,
+ ],
+ history_ready: false,
+ history_flip: 0,
+ active: false,
+ layout,
+ pipeline,
+ taau_pipeline,
+ groups: None,
+ }
+ }
+
+ /// New output-size targets; the history starts over.
+ pub(super) fn resize(&mut self, device: &wgpu::Device, out: [u32; 2]) {
+ self.target = super::target(device, "HD TAA", out, HDR).view;
+ self.history = [
+ super::target(device, "HD TAA history", out, HDR).view,
+ super::target(device, "HD TAA history", out, HDR).view,
+ ];
+ self.history_ready = false;
+ self.groups = None;
+ }
+}
+
+impl Hd {
+ /// Temporal anti-aliasing of `source` (with the world's `depth`): the source it left, to hand
+ /// to `finish` — the same one when TAA is off this frame.
+ pub fn taa(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ source: HdSource,
+ depth: &wgpu::TextureView,
+ ) -> HdSource {
+ if !self.taa.active {
+ return source;
+ }
+ let input = match source {
+ HdSource::Lit => self.lit_view().clone(),
+ HdSource::Fx => self.fx.clone(),
+ HdSource::Taa => return source,
+ };
+ let key = (input, depth.clone());
+ if self.taa.groups.as_ref().is_none_or(|(k, _)| *k != key) {
+ let groups = [0, 1].map(|read| {
+ self.make_group(
+ device,
+ "HD TAA",
+ &self.taa.layout,
+ &[
+ wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
+ wgpu::BindingResource::TextureView(&key.0),
+ wgpu::BindingResource::TextureView(depth),
+ wgpu::BindingResource::TextureView(&self.taa.history[read]),
+ wgpu::BindingResource::Sampler(&self.sampler),
+ ],
+ )
+ });
+ self.taa.groups = Some((key, groups));
+ }
+ let Some((_, groups)) = &self.taa.groups else {
+ return source;
+ };
+ let (read, write) = (self.taa.history_flip, 1 - self.taa.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,
+ if upscaling { "hd taau" } else { "hd taa" },
+ ),
+ color_attachments: &[&self.taa.target, &self.taa.history[write]].map(|view| {
+ Some(wgpu::RenderPassColorAttachment {
+ view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
+ store: wgpu::StoreOp::Store,
+ },
+ })
+ }),
+ ..Default::default()
+ });
+ pass.set_pipeline(if upscaling {
+ &self.taa.taau_pipeline
+ } else {
+ &self.taa.pipeline
+ });
+ pass.set_bind_group(0, &groups[read], &[]);
+ pass.draw(0..3, 0..1);
+ drop(pass);
+ self.taa.history_flip = write;
+ self.taa.history_ready = true;
+ HdSource::Taa
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use glam::Mat4;
+
+ #[test]
+ fn jitter_stays_within_a_pixel() {
+ let size = [1920u32, 1080];
+ let mut seen = std::collections::HashSet::new();
+ for f in 0..32 {
+ 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()));
+ }
+ assert_eq!(seen.len(), 16);
+ }
+
+ #[test]
+ fn taa_history_lookup_follows_the_jittered_grid() {
+ // A static point under two frames' different Halton jitters: fs_taa finds this frame's
+ // point in the history by the previous frame's matrix *as drawn*, which lands on the pixel
+ // the previous frame wrote it to; the unjittered one is off by that frame's jitter.
+ let size = [1920u32, 1080];
+ let unjittered = Mat4::perspective_rh(1.0, 16.0 / 9.0, 0.05, 1000.0);
+ let to_uv = |m: Mat4, p: glam::Vec3| {
+ let c = m * p.extend(1.0);
+ [c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5]
+ };
+ let drawn = |frame: u32| {
+ let j = jitter(frame, size, 16);
+ Mat4::from_translation(glam::Vec3::new(j[0], j[1], 0.0)) * unjittered
+ };
+ let p = glam::Vec3::new(0.0, 0.0, -20.0);
+ for frame in 1..16 {
+ let prev = drawn(frame - 1);
+ // The pixel (in uv) the previous frame drew the point at: what the history holds there.
+ let wrote = to_uv(prev, p);
+ let old = to_uv(unjittered, p);
+ let err = [
+ (old[0] - wrote[0]).abs() * size[0] as f32,
+ (old[1] - wrote[1]).abs() * size[1] as f32,
+ ];
+ let pj = jitter(frame - 1, size, 16);
+ assert!((err[0] - pj[0].abs() * size[0] as f32 * 0.5).abs() < 1e-3);
+ assert!((err[1] - pj[1].abs() * size[1] as f32 * 0.5).abs() < 1e-3);
+ }
+ }
+
+ #[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);
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/taa.wgsl b/crates/horizon-client/src/gfx/hd/taa.wgsl
new file mode 100644
index 000000000..fa87025f0
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/taa.wgsl
@@ -0,0 +1,215 @@
+
+struct TaaOut {
+ @location(0) color: vec4<f32>,
+ @location(1) history: vec4<f32>,
+}
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var source: texture_2d<f32>;
+@group(0) @binding(2) var depth_tex: texture_depth_2d;
+@group(0) @binding(3) var history: texture_2d<f32>;
+@group(0) @binding(4) var linear: sampler;
+
+fn to_ycocg(c: vec3<f32>) -> vec3<f32> {
+ return vec3<f32>(c.r * 0.25 + c.g * 0.5 + c.b * 0.25, c.r * 0.5 - c.b * 0.5, c.g * 0.5 - c.r * 0.25 - c.b * 0.25);
+}
+fn from_ycocg(c: vec3<f32>) -> vec3<f32> {
+ let t = c.x - c.z;
+ return vec3<f32>(t + c.y, c.x + c.z, t - c.y);
+}
+// Compress the range so a very bright pixel does not dominate the blend or the clamp.
+fn squash(c: vec3<f32>) -> vec3<f32> {
+ return c / (1.0 + max(c.r, max(c.g, c.b)));
+}
+fn unsquash(c: vec3<f32>) -> vec3<f32> {
+ return c / max(1.0 - max(c.r, max(c.g, c.b)), 0.001);
+}
+
+// The history at a uv, filtered with a Catmull-Rom spline (five bilinear taps) so that
+// resampling it every frame doesn't blur.
+fn history_color(uv: vec2<f32>) -> vec3<f32> {
+ let size = vec2<f32>(textureDimensions(history));
+ let pos = uv * size;
+ let c1 = floor(pos - 0.5) + 0.5;
+ let f = pos - c1;
+ let w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
+ let w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
+ let w2 = f * (0.5 + f * (2.0 - 1.5 * f));
+ let w3 = f * f * (-0.5 + 0.5 * f);
+ let w12 = w1 + w2;
+ let t0 = (c1 - 1.0) / size;
+ let t3 = (c1 + 2.0) / size;
+ let t12 = (c1 + w2 / w12) / size;
+ var sum = textureSampleLevel(history, linear, vec2<f32>(t12.x, t0.y), 0.0).rgb * (w12.x * w0.y);
+ var total = w12.x * w0.y;
+ sum += textureSampleLevel(history, linear, vec2<f32>(t0.x, t12.y), 0.0).rgb * (w0.x * w12.y);
+ total += w0.x * w12.y;
+ sum += textureSampleLevel(history, linear, t12, 0.0).rgb * (w12.x * w12.y);
+ total += w12.x * w12.y;
+ sum += textureSampleLevel(history, linear, vec2<f32>(t3.x, t12.y), 0.0).rgb * (w3.x * w12.y);
+ total += w3.x * w12.y;
+ sum += textureSampleLevel(history, linear, vec2<f32>(t12.x, t3.y), 0.0).rgb * (w12.x * w3.y);
+ total += w12.x * w3.y;
+ return max(sum / total, vec3<f32>(0.0));
+}
+
+// Pulls `p` towards the box's centre until it is inside.
+fn clip_to_box(lo: vec3<f32>, hi: vec3<f32>, p: vec3<f32>) -> vec3<f32> {
+ let centre = (lo + hi) * 0.5;
+ let ext = max((hi - lo) * 0.5, vec3<f32>(0.00001));
+ let off = (p - centre) / ext;
+ let m = max(abs(off.x), max(abs(off.y), abs(off.z)));
+ return select(p, centre + (p - centre) / m, m > 1.0);
+}
+
+// Temporal anti-aliasing: the history, reprojected by depth and the camera's move, clamped to
+// the colours round this pixel (so what moved or was uncovered can't ghost), blended in.
+@fragment
+fn fs_taa(in: FullOut) -> TaaOut {
+ let size = vec2<i32>(textureDimensions(source));
+ let px = vec2<i32>(in.position.xy);
+ let d = textureLoad(depth_tex, px, 0);
+ 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 centre = vec3<f32>(0.0);
+ for (var y = -1; y <= 1; y++) {
+ for (var x = -1; x <= 1; x++) {
+ let q = clamp(px + vec2<i32>(x, y), vec2<i32>(0), size - 1);
+ let s = to_ycocg(squash(textureLoad(source, q, 0).rgb));
+ sum += s;
+ sum2 += s * s;
+ lo = min(lo, s);
+ hi = max(hi, s);
+ if x == 0 && y == 0 { centre = s; }
+ }
+ }
+ let sky = d <= 0.0;
+ let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0, 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 = centre;
+ 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 * vec2<f32>(size)), vec2<i32>(0), size - 1);
+ let seen = textureLoad(history, hp, 0).a;
+ let expected = select(length(p + frame.taa.xyz), 0.0, sky);
+ // The same surface as last frame (not uncovered), by its distance.
+ 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.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));
+ let keep = mix(frame.taa_params.x, frame.taa_params.x * 0.75, clamp(moved / 4.0, 0.0, 1.0));
+ resolved = mix(centre, past, keep);
+ }
+ }
+ }
+ }
+ let color = unsquash(from_ycocg(resolved));
+ var out: TaaOut;
+ out.color = vec4<f32>(color, 1.0);
+ 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;
+}
diff --git a/crates/horizon-client/src/gfx/hd/tiles.rs b/crates/horizon-client/src/gfx/hd/tiles.rs
new file mode 100644
index 000000000..0b069e0be
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/tiles.rs
@@ -0,0 +1,186 @@
+//! Light tiles: for every 16x16 pixel tile of the world target, the short lists of the mods' point
+//! lights that can reach it, so `fs_light` loops over a few lights instead of all of them.
+//!
+//! Contract: a storage buffer of `u32`, per tile 4 words (surface list start, surface length, halo
+//! list start, halo length), then the index lists. A length of `EVERY_LIGHT` (0xffffffff) means
+//! "all lights" (the list overflowed `TILE_INDICES`). Light positions are camera-relative and the
+//! cover test uses the *jittered* view-projection the world was drawn with. Rebuilt on the CPU
+//! every frame (an empty one too: the shader reads the counts, not `params.x`) and sized at
+//! `resize`.
+use super::frame::HdLight;
+use glam::Mat4;
+
+/// The side (pixels) of a tile of the screen with its own short list of lights.
+pub(super) const TILE: u32 = 16;
+/// The indices all the tile lists may hold together; a tile past it loops over every light.
+const TILE_INDICES: usize = 1 << 19;
+/// A list length that means "every light": the list overflowed.
+const EVERY_LIGHT: u32 = u32::MAX;
+
+/// The tiles' header (4 words each) and index lists for a screen of `size`.
+pub(super) fn tiles_buffer(device: &wgpu::Device, size: [u32; 2]) -> wgpu::Buffer {
+ let tiles = (size[0].div_ceil(TILE).max(1) * size[1].div_ceil(TILE).max(1)) as usize;
+ device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("HD light tiles"),
+ size: ((tiles * 4 + TILE_INDICES) * std::mem::size_of::<u32>()) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ })
+}
+
+/// Where a camera-relative sphere can show on the screen.
+enum Cover {
+ /// Nowhere: it is wholly behind the camera.
+ Nothing,
+ /// Anywhere: it crosses the camera's plane, or holds the camera.
+ Everywhere,
+ /// Inside this pixel rectangle (min x, min y, max x, max y).
+ Rect([f32; 4]),
+}
+
+/// The screen cover of the sphere at `c` of radius `r`: the projection of the 8 corners of the
+/// box round it. A box is convex and so is its projection while every corner is in front of the
+/// camera, so the rectangle holds the whole sphere. With some corners behind, the projection
+/// is not bounded and the cover is the whole screen; with all of them behind, no pixel ray
+/// (they all point forward) can meet the sphere.
+fn cover(view_proj: &Mat4, c: glam::Vec3, r: f32, size: [u32; 2]) -> Cover {
+ if !c.is_finite() || !r.is_finite() {
+ return Cover::Everywhere;
+ }
+ // A little more than the shader's radius, against rounding.
+ let r = r * 1.01 + 0.01;
+ let (mut lo, mut hi) = (glam::Vec2::splat(f32::MAX), glam::Vec2::splat(f32::MIN));
+ let mut front = 0;
+ for corner in 0..8 {
+ let o = glam::Vec3::new(
+ if corner & 1 == 0 { -r } else { r },
+ if corner & 2 == 0 { -r } else { r },
+ if corner & 4 == 0 { -r } else { r },
+ );
+ let h = *view_proj * (c + o).extend(1.0);
+ if h.w > 0.0 {
+ front += 1;
+ let ndc = glam::Vec2::new(h.x, h.y) / h.w;
+ let px = glam::Vec2::new(
+ (ndc.x * 0.5 + 0.5) * size[0] as f32,
+ (0.5 - ndc.y * 0.5) * size[1] as f32,
+ );
+ lo = lo.min(px);
+ hi = hi.max(px);
+ }
+ }
+ match front {
+ 0 => Cover::Nothing,
+ 8 => Cover::Rect([lo.x - 2.0, lo.y - 2.0, hi.x + 2.0, hi.y + 2.0]),
+ _ => Cover::Everywhere,
+ }
+}
+
+/// Fills `words` with each 16x16 tile's two lists of the lights that can reach it: header
+/// (surface start, surface length, halo start, halo length) per tile, then the indices; a
+/// length of `EVERY_LIGHT` stands for all of `lights` (a list past the buffer's room). The
+/// surface list holds the lights whose sphere can touch what the tile sees. The halo list
+/// holds the lights whose halo can reach a ray of the tile: the shader adds a halo for the
+/// view *line* passing within half the radius of the light, so a ray pointing away from a light
+/// behind it is lit too, and the mirrored sphere is covered as well. Lights stay in order, so
+/// the sums are the ones of the full loop.
+pub(super) fn tile_lists(
+ view_proj: &Mat4,
+ lights: &[HdLight],
+ size: [u32; 2],
+ scratch: &mut Vec<Vec<u16>>,
+ words: &mut Vec<u32>,
+) {
+ let tx = size[0].div_ceil(TILE).max(1) as usize;
+ let ty = size[1].div_ceil(TILE).max(1) as usize;
+ let tiles = tx * ty;
+ scratch.resize_with(tiles * 2, Vec::new);
+ scratch.truncate(tiles * 2);
+ scratch.iter_mut().for_each(Vec::clear);
+ let mut add = |list: usize, area: Cover, light: u16| match area {
+ Cover::Nothing => {}
+ Cover::Everywhere => {
+ for t in 0..tiles {
+ scratch[t * 2 + list].push(light);
+ }
+ }
+ Cover::Rect([x0, y0, x1, y1]) => {
+ let tile = TILE as f32;
+ if x1 < 0.0 || y1 < 0.0 || x0 >= size[0] as f32 || y0 >= size[1] as f32 {
+ return;
+ }
+ let from = |v: f32, n: usize| ((v / tile).floor().max(0.0) as usize).min(n - 1);
+ for y in from(y0, ty)..=from(y1, ty) {
+ for x in from(x0, tx)..=from(x1, tx) {
+ scratch[(y * tx + x) * 2 + list].push(light);
+ }
+ }
+ }
+ };
+ for (i, l) in lights.iter().enumerate() {
+ let c = glam::Vec3::new(l.pos[0], l.pos[1], l.pos[2]);
+ add(0, cover(view_proj, c, l.pos[3], size), i as u16);
+ // The shader's halo reach: the line within `radius / 2 - 0.5` of the light.
+ let reach = l.pos[3] * 0.5 - 0.5;
+ if reach > 0.0 || !reach.is_finite() {
+ add(1, cover(view_proj, c, reach, size), i as u16);
+ add(1, cover(view_proj, -c, reach, size), i as u16);
+ }
+ }
+ words.clear();
+ words.resize(tiles * 4, 0);
+ let mut room = TILE_INDICES;
+ for t in 0..tiles {
+ for list in 0..2 {
+ let ids = &mut scratch[t * 2 + list];
+ // Both the sphere and its mirror can name a light: once is enough.
+ ids.dedup();
+ let at = t * 4 + list * 2;
+ if ids.len() > room {
+ words[at + 1] = EVERY_LIGHT;
+ } else {
+ room -= ids.len();
+ words[at] = words.len() as u32;
+ words[at + 1] = ids.len() as u32;
+ words.extend(ids.iter().map(|&i| u32::from(i)));
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn tile_lists_keep_lights_that_reach_a_tile() {
+ let size = [320, 160];
+ let proj = Mat4::perspective_rh(1.0, 2.0, 0.05, 1000.0);
+ let light = |x: f32, y: f32, z: f32, r: f32| HdLight {
+ pos: [x, y, z, r],
+ color: [1.0; 4],
+ };
+ let lights = [
+ // Dead ahead and small; far to the side; behind the camera; holding the camera.
+ light(0.0, 0.0, -20.0, 4.0),
+ light(30.0, 0.0, -5.0, 4.0),
+ light(0.0, 0.0, 10.0, 8.0),
+ light(1.0, 0.0, 0.0, 6.0),
+ ];
+ let (mut scratch, mut words) = (Vec::new(), Vec::new());
+ tile_lists(&proj, &lights, size, &mut scratch, &mut words);
+ let list = |tile: usize, which: usize| -> Vec<u32> {
+ let (start, len) = (words[tile * 4 + which * 2], words[tile * 4 + which * 2 + 1]);
+ words[start as usize..(start + len) as usize].to_vec()
+ };
+ // The centre tile sees the first light and the one at the camera, in both lists.
+ let centre = 5 * 20 + 10;
+ assert_eq!(list(centre, 0), vec![0, 3]);
+ assert!(list(centre, 1).contains(&0) && list(centre, 1).contains(&3));
+ // A far corner sees only the one at the camera.
+ assert_eq!(list(0, 0), vec![3]);
+ // The light behind the camera shows in the halo list where the view points away.
+ assert!(list(centre, 1).contains(&2));
+ assert!(!list(0, 0).contains(&2));
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/tonemap.rs b/crates/horizon-client/src/gfx/hd/tonemap.rs
new file mode 100644
index 000000000..da79c43da
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/tonemap.rs
@@ -0,0 +1,151 @@
+//! The tone curve onto the screen, and `Hd::finish` (bloom, then the curve).
+//!
+//! Contract:
+//! - Reads the final HDR colour (`HdSource`) and bloom level 0, both at the output size, plus
+//! `HdFrame`: `params.y` exposure, `params.z` bloom strength, `taa_params.y` the sharpening.
+//! - Writes the screen target (`screen` format, display space): up to 0.8 as it is (vanilla's look),
+//! brighter rolled off towards white by the brightest channel, a light grade on top.
+//! The hand and HUD are drawn after it and are not jittered or tone-mapped.
+use super::bloom::{BLOOM_LEVELS, BLOOM_NAMES};
+use super::{
+ FLOAT, Hd, HdSource, SAMPLER, UNIFORM, entries, fullscreen, make_group, module, stamp,
+};
+
+pub(super) const TONEMAP: &str = include_str!("tonemap.wgsl");
+/// `fs_tonemap`'s group 0: frame, the colour, bloom level 0, the linear sampler.
+pub(super) const TONEMAP_BINDINGS: [wgpu::BindingType; 4] = [UNIFORM, FLOAT, FLOAT, SAMPLER];
+
+pub(super) struct Tonemap {
+ layout: wgpu::BindGroupLayout,
+ pipeline: wgpu::RenderPipeline,
+ group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
+}
+
+impl Tonemap {
+ pub(super) fn new(device: &wgpu::Device, screen: wgpu::TextureFormat) -> Self {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD tone mapping"),
+ entries: &entries(&TONEMAP_BINDINGS),
+ });
+ let tone = module(device, "HD tone mapping", TONEMAP);
+ let pipeline = fullscreen(
+ device,
+ "HD tone mapping",
+ &layout,
+ &tone,
+ "fs_tonemap",
+ screen,
+ None,
+ );
+ Self {
+ layout,
+ pipeline,
+ group: None,
+ }
+ }
+
+ /// The bind group names the colour source and bloom level 0: dropped at `resize`.
+ pub(super) fn reset(&mut self) {
+ self.group = None;
+ }
+}
+
+impl Hd {
+ /// Bloom from `source` (the lit world or the post pass's output), then the tone curve onto `screen`.
+ pub fn finish(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ source: HdSource,
+ screen: &wgpu::TextureView,
+ ) {
+ let source = match source {
+ HdSource::Lit => self.lit_view().clone(),
+ HdSource::Fx => self.fx.clone(),
+ HdSource::Taa => self.taa.target.clone(),
+ };
+ if self
+ .bloom
+ .prefilter_group
+ .as_ref()
+ .is_none_or(|(v, _)| *v != source)
+ {
+ let group = self.bloom.group(
+ device,
+ &self.groups_made,
+ &self.frame,
+ &self.sampler,
+ &source,
+ );
+ self.bloom.prefilter_group = Some((source.clone(), group));
+ }
+ if self
+ .tonemap
+ .group
+ .as_ref()
+ .is_none_or(|(v, _)| *v != source)
+ {
+ let group = make_group(
+ &self.groups_made,
+ device,
+ "HD tone mapping",
+ &self.tonemap.layout,
+ &[
+ wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
+ wgpu::BindingResource::TextureView(&source),
+ wgpu::BindingResource::TextureView(&self.bloom.levels[0].view),
+ wgpu::BindingResource::Sampler(&self.sampler),
+ ],
+ );
+ self.tonemap.group = Some((source, group));
+ }
+ let (Some((_, prefilter)), Some((_, tone))) =
+ (&self.bloom.prefilter_group, &self.tonemap.group)
+ else {
+ return;
+ };
+ let bloom = &self.bloom;
+ // Down: the prefilter into the first level, then each level from the one above.
+ Self::draw(
+ encoder,
+ "HD bloom prefilter",
+ &bloom.levels[0].view,
+ false,
+ &bloom.prefilter,
+ prefilter,
+ stamp(&mut self.timing, BLOOM_NAMES[0]),
+ );
+ for k in 1..bloom.levels.len() {
+ Self::draw(
+ encoder,
+ "HD bloom down",
+ &bloom.levels[k].view,
+ false,
+ &bloom.down,
+ &bloom.down_groups[k - 1],
+ stamp(&mut self.timing, BLOOM_NAMES[k]),
+ );
+ }
+ // Up: each level added onto the bigger one.
+ for k in (1..bloom.levels.len()).rev() {
+ Self::draw(
+ encoder,
+ "HD bloom up",
+ &bloom.levels[k - 1].view,
+ true,
+ &bloom.up,
+ &bloom.up_groups[k - 1],
+ stamp(&mut self.timing, BLOOM_NAMES[BLOOM_LEVELS + k - 1]),
+ );
+ }
+ Self::draw(
+ encoder,
+ "HD tone mapping",
+ screen,
+ false,
+ &self.tonemap.pipeline,
+ tone,
+ stamp(&mut self.timing, "hd tonemap"),
+ );
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/tonemap.wgsl b/crates/horizon-client/src/gfx/hd/tonemap.wgsl
new file mode 100644
index 000000000..2ede55e3d
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/tonemap.wgsl
@@ -0,0 +1,41 @@
+
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var hdr: texture_2d<f32>;
+@group(0) @binding(2) var bloom: texture_2d<f32>;
+@group(0) @binding(3) var linear: sampler;
+
+@fragment
+fn fs_tonemap(in: FullOut) -> @location(0) vec4<f32> {
+ var c = textureSampleLevel(hdr, linear, in.uv, 0.0).rgb;
+ // A slight sharpen after TAA (its history resampling softens): the centre against its four
+ // neighbours, kept within their range so it can't ring.
+ let k = frame.taa_params.y;
+ if k > 0.0 {
+ let t = 1.0 / vec2<f32>(textureDimensions(hdr));
+ let n0 = textureSampleLevel(hdr, linear, in.uv + vec2<f32>(t.x, 0.0), 0.0).rgb;
+ let n1 = textureSampleLevel(hdr, linear, in.uv - vec2<f32>(t.x, 0.0), 0.0).rgb;
+ let n2 = textureSampleLevel(hdr, linear, in.uv + vec2<f32>(0.0, t.y), 0.0).rgb;
+ let n3 = textureSampleLevel(hdr, linear, in.uv - vec2<f32>(0.0, t.y), 0.0).rgb;
+ let lo = min(min(n0, n1), min(min(n2, n3), c));
+ let hi = max(max(n0, n1), max(max(n2, n3), c));
+ c = clamp(c * (1.0 + 4.0 * k) - (n0 + n1 + n2 + n3) * k, lo, hi);
+ }
+ c = c * frame.params.y;
+ c += textureSampleLevel(bloom, linear, in.uv, 0.0).rgb * frame.params.z;
+ // Up to 0.8 as it is (vanilla's look); above it, rolled off towards white by the
+ // brightest channel, so hues hold instead of clipping.
+ let peak = max(c.r, max(c.g, c.b));
+ let t = 0.8;
+ if peak > t {
+ let x = (peak - t) / (1.0 - t);
+ let rolled = t + (1.0 - t) * (x / (1.0 + x));
+ c = c * (rolled / peak);
+ // Very bright light bleaches towards white, as film does.
+ c = mix(c, vec3<f32>(rolled), clamp((peak - 2.0) / 8.0, 0.0, 0.6));
+ }
+ // A light grade: a touch more colour, and a gentle S round the middle grey.
+ let luma = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
+ c = max(mix(vec3<f32>(luma), c, 1.12), vec3<f32>(0.0));
+ c = mix(c, c * c * (3.0 - 2.0 * c), 0.18);
+ return vec4<f32>(c, 1.0);
+}
diff --git a/crates/horizon-client/src/gfx/hd_volumes.rs b/crates/horizon-client/src/gfx/hd/volumes.rs
similarity index 52%
rename from crates/horizon-client/src/gfx/hd_volumes.rs
rename to crates/horizon-client/src/gfx/hd/volumes.rs
index 5de1f2337..a49c58f86 100644
--- a/crates/horizon-client/src/gfx/hd_volumes.rs
+++ b/crates/horizon-client/src/gfx/hd/volumes.rs
@@ -4,14 +4,31 @@
//! and Photon's multiple-scattering approximation, drawn at half resolution with a jittered march
//! and reprojected history; plus a small map of the clouds' shadows for the ground. Fog: a half
//! resolution march from the camera to the surface through height fog, sampling the sun's shadow
-//! cascades for the light shafts. `fs_light` composites both (see `hd.rs`).
+//! cascades for the light shafts. `fs_light` composites both (see `lighting.rs`).
+//!
+//! Contract:
+//! - Clouds read `HdFrame` (`clouds`, `cloud_cam`, `cloud_dir`, `cloud_sun`), the sky texture, the
+//! CPU-made noise (`noise.rs`) and their own history. Positions are camera-relative; the noise is
+//! addressed by the camera's x/z modulo 65536 so it stays put in the world. Time is *game*
+//! seconds modulo 300000 (`clouds.w`, the wind), unlike the water's wall-clock time.
+//! Writes (half the world size, `Rgba16Float`): rgb light, **a transmittance** (apply as
+//! `c * a + rgb`); two textures ping-pong as history (reprojected by direction and camera shift),
+//! `flip_index` says which one `fs_light` reads. The history is reset by resize, teleport, no TAA.
+//! - Cloud shadows: a 256 x 256 `R16Float` map of optical depth along the sun, +-1200 blocks around
+//! the camera, read by `fs_light` to dim direct sun.
+//! - Fog reads the world depth (reversed-Z, the lighting's input), the shadow cascades and the sky,
+//! and writes (half size) the in-scattered light with transmittance in alpha, and the length of the
+//! ray (`R16Float`) for the depth-aware upsample.
//!
//! The cloud shaping and lighting (altitude shaping, the powder effect, the phase functions, the
//! multiple-scattering octaves, the light march with growing steps) and the fog's phase mix and
//! evening glow are ported from Photon by sixthsurge (MIT, see THIRD_PARTY_PHOTON.md), shaders/include/sky/clouds/{common,cumulus}.glsl, shaders/include/lighting/cloud_shadows.glsl and shaders/include/fog/air_fog_vl.glsl at commit 26c2906.
-use super::hd::{DEPTH, FLOAT, SAMPLER, UNIFORM, entries, module, stamped_pass, target};
-use super::hd_noise;
+use super::noise as hd_noise;
+use super::{
+ DEPTH, FLOAT, Hd, SAMPLER, SHADOW_ARRAY, SHADOW_SAMPLER, UNIFORM, entries, fullscreen, module,
+ stamp, stamped_pass, target,
+};
pub const CLOUD_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
pub const SHADOW_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R16Float;
@@ -39,376 +56,17 @@ pub fn fog_enabled() -> bool {
/// Shared by the cloud passes: the bindings, the density of the slab, the light march and the
/// phase functions. Bindings: 0 frame, 1 3D noise, 2 2D noise, 3 clamped sampler, 4 repeating
/// sampler, 5 the sky texture.
-const CLOUD_COMMON: &str = r#"
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var noise3: texture_3d<f32>;
-@group(0) @binding(2) var noise2: texture_2d<f32>;
-@group(0) @binding(3) var linear: sampler;
-@group(0) @binding(4) var repeat_s: sampler;
-//SKY5
-
-const CPI: f32 = 3.14159265;
-// Extinction per block at full density, how fast the wind blows (blocks per second).
-const CLOUD_EXT: f32 = 0.22;
-const CLOUD_WIND: f32 = 3.0;
-
-fn linear_step(a: f32, b: f32, x: f32) -> f32 {
- return clamp((x - a) / (b - a), 0.0, 1.0);
-}
-
-fn dampen(x: f32) -> f32 {
- let t = clamp(x, 0.0, 1.0);
- return t * (2.0 - t);
-}
-
-// Where a camera-relative point lies in the world's cloud pattern (the wind moves the pattern).
-fn cloud_xz(pos: vec3<f32>) -> vec2<f32> {
- return vec2<f32>(pos.x + frame.cloud_cam.x, pos.z + frame.cloud_cam.y)
- + vec2<f32>(0.8, 0.6) * (CLOUD_WIND * frame.clouds.w);
-}
+const CLOUD_COMMON: &str = include_str!("cloud_common.wgsl");
-// The cloud density at a camera-relative point (0 outside the slab), in 0..1. Photon's cumulus
-// shaping: a coverage field and a shape field cut by a threshold, an egg shape over the altitude,
-// Worley erosion (large and small) and sharper edges at the top than at the bottom.
-fn cloud_density(pos: vec3<f32>, detail: f32) -> f32 {
- let h = (pos.y - frame.clouds.x) / frame.clouds.y;
- if h <= 0.0 || h >= 1.0 { return 0.0; }
- let w = cloud_xz(pos);
- let cover = textureSampleLevel(noise2, repeat_s, w / 16000.0, 0.0).r;
- let shape = textureSampleLevel(noise2, repeat_s, w / 3000.0, 0.0).g;
- let cu = mix(0.56, 0.80, cover) + frame.cloud_cam.z;
- var d = linear_step(1.0 - cu, 1.0, shape);
- // Carve an egg shape, thin out the bottom.
- d -= smoothstep(0.2, 1.0, h) * 0.6;
- 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);
- let warp = (textureSampleLevel(noise3, repeat_s, p3 / 1100.0, 0.0).b - 0.5) * 0.4;
- let w0 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 420.0 + vec3<f32>(warp), 0.0).r;
- let w1 = 1.0 - textureSampleLevel(noise3, repeat_s, p3 / 90.0 + vec3<f32>(warp * 3.0), 0.0).g;
- let fade = 0.20 * smoothstep(0.85, 1.0, 1.0 - h) - 0.35 * smoothstep(0.05, 0.5, h) + 0.6;
- d -= 0.55 * w0 * w0 * dampen(1.0 - d);
- d -= 0.45 * w1 * w1 * dampen(1.0 - d) * fade;
- }
- d = max(d, 0.0);
- d = 1.0 - pow(1.0 - d, mix(2.2, 5.0, h));
- d *= 0.1 + 0.9 * smoothstep(0.2, 0.7, h);
- return d;
-}
-
-// The optical depth from `p` along `dir` through the slab: a few steps that double in length
-// (Photon's light march), the sample dithered inside its step.
-fn cloud_light_depth(p: vec3<f32>, dir: vec3<f32>, steps: i32, dither: f32, detail: f32) -> f32 {
- var len = 0.1 * frame.clouds.y / f32(steps);
- var t = 0.0;
- var od = 0.0;
- for (var i = 0; i < steps; i++) {
- len *= 2.0;
- od += cloud_density(p + dir * (t + len * dither), detail) * len;
- t += len;
- }
- return od;
-}
-"#;
+const CLOUD: &str = include_str!("cloud.wgsl");
-const CLOUD: &str = r#"
-@group(0) @binding(6) var history: texture_2d<f32>;
-
-fn ign(px: vec2<f32>) -> f32 {
- let p = px + vec2<f32>(5.588238 * frame.params.w);
- return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
-}
-
-fn hg(c: f32, g: f32) -> f32 {
- let gg = g * g;
- return (1.0 - gg) / (4.0 * CPI * pow(1.0 + gg - 2.0 * g * c, 1.5));
-}
-fn klein_nishina(nu: f32, e: f32) -> f32 {
- return e / (2.0 * CPI * (e - e * nu + 1.0) * log(2.0 * e + 1.0));
-}
-// Photon's single scattering phase: a glow close to the sun, a forward lobe and a back lobe.
-fn phase_single(c: f32) -> f32 {
- let a = klein_nishina(c, 2600.0);
- let b = hg(c, 0.8);
- return 0.8 * max(a, b) + 0.2 * hg(c, -0.2);
-}
-fn phase_multi(c: f32, g: vec3<f32>) -> f32 {
- return 0.65 * hg(c, g.x) + 0.10 * hg(c, g.y) + 0.25 * hg(c, -g.z);
-}
-fn powder_effect(density: f32, c: f32) -> f32 {
- let p = CPI * density / (density + 0.15);
- let k = c * 0.5 + 0.5;
- return mix(p, 1.0, 0.8 * k * k);
-}
-
-// Photon's multiple scattering octaves: each one scatters a part of the light again with a wider
-// phase, less extinction and the powder effect easing off. x: sunlight, y: skylight.
-fn cloud_scatter(
- density: f32,
- light_od: f32,
- sky_od: f32,
- ground_od: f32,
- ext: f32,
- sca: f32,
- step_t: f32,
- cos_theta: f32,
- bounced: f32,
-) -> vec2<f32> {
- var s = vec2<f32>(0.0);
- var scatter_amount = sca;
- var extinct_amount = ext;
- let integral = (1.0 - step_t) / ext;
- var powder = powder_effect(density, cos_theta);
- var phase = phase_single(cos_theta);
- var phase_g = pow(vec3<f32>(0.6, 0.9, 0.3), vec3<f32>(1.0 + light_od));
- let iso = 1.0 / (4.0 * CPI);
- for (var i = 0; i < 4; i++) {
- s.x += scatter_amount * exp(-extinct_amount * light_od) * phase;
- s.x += scatter_amount * exp(-extinct_amount * ground_od) * iso * bounced;
- s.y += scatter_amount * exp(-extinct_amount * sky_od) * iso;
- let lift = pow(clamp(sca / 0.1, 0.0, 1.0), 0.33);
- scatter_amount *= 0.55 * mix(lift, 1.0, cos_theta * 0.5 + 0.5) * powder;
- extinct_amount *= 0.4;
- phase_g *= 0.8;
- powder = mix(powder, sqrt(powder), 0.5);
- phase = phase_multi(cos_theta, phase_g);
- }
- return s * integral;
-}
-
-@fragment
-fn fs_cloud(in: FullOut) -> @location(0) vec4<f32> {
- 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);
- var cur = vec4<f32>(0.0, 0.0, 0.0, 1.0);
- 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;
- let lo = max(min(ta, tb), 0.0);
- let hi = min(max(ta, tb), 4200.0);
- if hi > lo {
- let dither = ign(in.position.xy);
- let n = clamp(i32(ceil((hi - lo) / 14.0)), 8, 26);
- let step_len = (hi - lo) / f32(n);
- let light = frame.cloud_dir.xyz;
- let cos_theta = dot(dir, light);
- let overcast = frame.sky_extra.z;
- let ext = CLOUD_EXT * (1.0 - 0.25 * overcast);
- let sca = ext;
- let bounced = 0.4 * max(light.y, 0.0) / CPI;
- var scatter = vec2<f32>(0.0);
- var t_acc = 1.0;
- var dist_sum = 0.0;
- var weight_sum = 0.0;
- for (var i = 0; i < n; i++) {
- if t_acc < 0.075 { break; }
- let t = lo + step_len * (f32(i) + dither);
- let pos = dir * t;
- var d = cloud_density(pos, 1.0);
- if d < 0.002 { continue; }
- d *= 1.0 - smoothstep(0.8, 1.0, t / 4200.0);
- let step_od = d * ext * step_len;
- let step_t = exp(-step_od);
- let h = clamp((pos.y - y0) / frame.clouds.y, 0.0, 1.0);
- let hash = fract(dither * 7.13 + f32(i) * 0.618);
- let light_od = cloud_light_depth(pos, light, 3, hash, 1.0);
- let sky_od = cloud_light_depth(pos, vec3<f32>(0.0, 1.0, 0.0), 1, 1.0 - hash, 0.0);
- // A guess of the optical depth to the ground from the altitude and this density.
- let ground_od = mix(d, 1.0, clamp(h * 2.0 - 1.0, 0.0, 1.0)) * h * frame.clouds.y;
- scatter += cloud_scatter(d, light_od, sky_od, ground_od, ext, sca, step_t, cos_theta, bounced) * t_acc;
- t_acc *= step_t;
- dist_sum += t * d;
- weight_sum += d;
- }
- if weight_sum > 0.0 { hit_t = dist_sum / weight_sum; }
- // The sunlight's colour at this hour (Photon: the light's colour from the transmittance),
- // the sky's own light from the zenith and the horizon.
- let sun_light = frame.cloud_sun.rgb * frame.cloud_sun.w * (1.0 - overcast);
- let zenith = sky_show(textureLoad(sky_lut, vec2<i32>(0, 64), 0).rgb);
- let horizon = sky_show(textureLoad(sky_lut, vec2<i32>(1, 64), 0).rgb);
- let ambient = mix(horizon, zenith, 0.55);
- var colour = scatter.x * sun_light * 3.2 + scatter.y * ambient * 5.0;
- // The remap of Photon: the transmittance's floor is 0.
- let trans = linear_step(0.075, 1.0, t_acc);
- // Aerial perspective: far clouds fade into the haze.
- let haze = haze_colour(dir);
- let air = exp(-hit_t / 3600.0);
- colour = mix((1.0 - trans) * haze, colour, air);
- cur = vec4<f32>(colour, trans);
- }
- }
- // 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);
- if hp.w > 0.0 {
- 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);
- // 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));
- cur = mix(cur, old, keep);
- }
- }
- }
- return cur;
-}
-"#;
-
-const CLOUD_SHADOW: &str = r#"
-@fragment
-fn fs_cloud_shadow(in: FullOut) -> @location(0) vec4<f32> {
- // A square of the cloud base plane round the camera; a texel holds the optical depth of the
- // sunlight's way through the slab from that point.
- let q = (in.uv - vec2<f32>(0.5)) * (2.0 * SHADOW_R);
- let light = frame.cloud_dir.xyz;
- let ly = max(light.y, 0.15);
- let base = vec3<f32>(q.x, frame.clouds.x + 0.5, q.y);
- let len = frame.clouds.y / ly;
- var od = 0.0;
- for (var i = 0; i < 4; i++) {
- let t = (f32(i) + 0.5) / 4.0 * len;
- od += cloud_density(base + light * t, 0.0);
- }
- od *= CLOUD_EXT * len / 4.0 * (1.0 - 0.25 * frame.sky_extra.z);
- return vec4<f32>(od, 0.0, 0.0, 1.0);
-}
-"#;
+const CLOUD_SHADOW: &str = include_str!("cloud_shadow.wgsl");
/// The volumetric fog: bindings 0 frame, 1 the world's depth, 2 and 3 the shadow cascades and
/// their sampler, 4 the 3D noise, 5 the repeating sampler, 6 the clamped sampler, 7 the sky
/// texture. Targets: the in-scattered light (rgb) with the transmittance (a), and the distance the
/// ray was marched (for the depth-aware upsample in `fs_light`).
-const FOG: &str = r#"
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-@group(0) @binding(1) var scene_depth: texture_depth_2d;
-@group(0) @binding(2) var shadow_map: texture_depth_2d_array;
-@group(0) @binding(3) var shadow_sampler: sampler_comparison;
-@group(0) @binding(4) var noise3: texture_3d<f32>;
-@group(0) @binding(5) var repeat_s: sampler;
-@group(0) @binding(6) var linear: sampler;
-//SKY7
-
-const FOG_MAX: f32 = 192.0;
-const FOG_STEPS: i32 = 18;
-
-struct FogOut {
- @location(0) fog: vec4<f32>,
- @location(1) dist: vec4<f32>,
-}
-
-fn ign(px: vec2<f32>) -> f32 {
- let p = px + vec2<f32>(5.588238 * frame.params.w);
- return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
-}
-
-// Henyey-Greenstein, normalised to 1 for an isotropic one (so the light is its strength times it).
-fn hgn(c: f32, g: f32) -> f32 {
- let gg = g * g;
- return (1.0 - gg) / pow(1.0 + gg - 2.0 * g * c, 1.5);
-}
-
-// The sun's light reaching a camera-relative point: 0 in shadow, 1 lit (the near cascade close
-// by, the far one beyond; outside both, lit).
-fn sun_visible(p: vec3<f32>) -> f32 {
- let near = length(p) < frame.shadow.y * 0.9;
- let c = select(1, 0, near);
- let s = frame.cascades[c] * vec4<f32>(p, 1.0);
- let uv = vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
- if any(uv.xy < vec2<f32>(0.0)) || any(uv.xy > vec2<f32>(1.0)) || uv.z > 1.0 { return 1.0; }
- return textureSampleCompareLevel(shadow_map, shadow_sampler, uv.xy, c, uv.z - select(0.0012, 0.0007, near));
-}
-
-// 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
-// deep underground.
-fn fog_sigma(p: vec3<f32>) -> f32 {
- let wy = p.y + frame.vfog.y;
- let h = wy - 62.0;
- var s = 0.0016 * exp(clamp(-h / 42.0, -6.0, 1.3));
- s += frame.vfog.z * 0.011 * exp(clamp(-h / 15.0, -6.0, 1.0));
- 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 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);
-}
-
-@fragment
-fn fs_fog(in: FullOut) -> FogOut {
- // The nearest of the four world pixels this texel stands for (so edges keep the near depth).
- let size = vec2<i32>(frame.resolution.xy);
- let base = vec2<i32>(in.position.xy) * 2;
- var d = 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 > d {
- d = z;
- best = q;
- }
- }
- let uv = (vec2<f32>(best) + 0.5) * frame.resolution.zw;
- let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, max(d, 0.000001), 1.0);
- let h = frame.inverse_view_proj * ndc;
- let p = h.xyz / h.w;
- let dir = normalize(p);
- let len = select(FOG_MAX, min(length(p), FOG_MAX), d > 0.0);
- var out: FogOut;
- out.dist = vec4<f32>(len, 0.0, 0.0, 0.0);
- if frame.vfog.x < 0.5 {
- out.fog = vec4<f32>(0.0, 0.0, 0.0, 1.0);
- return out;
- }
- let sun_on = frame.sun.w;
- let sun_light = frame.sun_color.rgb * sun_on;
- let cos_v = dot(dir, frame.sun.xyz);
- // Photon's mix of two phase functions for the aerosols, as its air fog does.
- let phase = 0.7 * hgn(cos_v, 0.55) + 0.3 * hgn(cos_v, -0.2);
- let haze = haze_colour(dir);
- let jitter = ign(in.position.xy);
- var trans = 1.0;
- var light = vec3<f32>(0.0);
- var lit_sum = 0.0;
- for (var i = 0; i < FOG_STEPS; i++) {
- let u0 = (f32(i) + jitter) / f32(FOG_STEPS);
- let u1 = (f32(i) + 1.0 + jitter) / f32(FOG_STEPS);
- let f0 = 0.5 * u0 + 0.5 * u0 * u0;
- let f1 = 0.5 * u1 + 0.5 * u1 * u1;
- let t = len * f0;
- let ds = len * (f1 - f0);
- let pos = dir * (t + 0.5 * ds);
- let sigma = fog_sigma(pos);
- let a = 1.0 - exp(-sigma * ds);
- var vis = 1.0;
- if sun_on > 0.0 { vis = sun_visible(pos); }
- lit_sum += vis;
- // The sky's light fills the shade less than the open.
- let ambient = haze * (0.2 + 0.8 * (0.35 + 0.65 * vis));
- light += trans * a * (sun_light * vis * phase * FOG_SUN + ambient * FOG_AMBIENT);
- trans *= 1.0 - a;
- }
- // Photon brightens the fog a little in the early morning and the evening.
- let sy = frame.sky_sun.y + 0.02;
- let glow = 1.0 + 2.0 * linear_step(0.05, 1.0, exp(-300.0 * sy * sy));
- out.fog = vec4<f32>(light * glow, trans);
- return out;
-}
-
-const FOG_SUN: f32 = 1.1;
-const FOG_AMBIENT: f32 = 0.9;
-fn linear_step(a: f32, b: f32, x: f32) -> f32 {
- return clamp((x - a) / (b - a), 0.0, 1.0);
-}
-"#;
+const FOG: &str = include_str!("fog.wgsl");
fn cloud_body(extra: &str) -> String {
format!("{CLOUD_COMMON}{extra}")
@@ -447,6 +105,26 @@ const D3: wgpu::BindingType = wgpu::BindingType::Texture {
multisampled: false,
};
+/// `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; 7] =
+ [UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT, FLOAT];
+/// `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];
+/// `fs_fog`'s group 0: frame, the world's depth, the shadow cascades and their sampler, 3D noise,
+/// repeating and clamped samplers, the sky texture.
+pub(super) const FOG_BINDINGS: [wgpu::BindingType; 8] = [
+ UNIFORM,
+ DEPTH,
+ SHADOW_ARRAY,
+ SHADOW_SAMPLER,
+ D3,
+ SAMPLER,
+ SAMPLER,
+ FLOAT,
+];
+
impl HdVolumes {
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
let upload = |label: &str, dimension, size: wgpu::Extent3d, data: Vec<u8>| {
@@ -516,28 +194,15 @@ impl HdVolumes {
});
let cloud_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD clouds"),
- entries: &entries(&[UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT, FLOAT]),
+ entries: &entries(&CLOUD_BINDINGS),
});
let shadow_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD cloud shadows"),
- entries: &entries(&[UNIFORM, D3, FLOAT, SAMPLER, SAMPLER, FLOAT]),
+ entries: &entries(&CLOUD_SHADOW_BINDINGS),
});
let fog_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("HD fog"),
- entries: &entries(&[
- UNIFORM,
- DEPTH,
- wgpu::BindingType::Texture {
- sample_type: wgpu::TextureSampleType::Depth,
- view_dimension: wgpu::TextureViewDimension::D2Array,
- multisampled: false,
- },
- wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
- D3,
- SAMPLER,
- SAMPLER,
- FLOAT,
- ]),
+ entries: &entries(&FOG_BINDINGS),
});
let cloud_src = format!("{}{CLOUD}", cloud_body(""));
let shadow_src = format!(
@@ -547,7 +212,7 @@ 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);
- let cloud_pipeline = super::hd::fullscreen(
+ let cloud_pipeline = fullscreen(
device,
"HD clouds",
&cloud_layout,
@@ -556,7 +221,7 @@ impl HdVolumes {
CLOUD_FORMAT,
None,
);
- let shadow_pipeline = super::hd::fullscreen(
+ let shadow_pipeline = fullscreen(
device,
"HD cloud shadows",
&shadow_layout,
@@ -841,17 +506,54 @@ impl HdVolumes {
}
}
+impl Hd {
+ /// The volumetric clouds (with their shadow map) and the fog, before the lighting reads them.
+ pub fn volumes(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ depth: &wgpu::TextureView,
+ ) {
+ if self.clouds_active {
+ self.volumes
+ .prepare_clouds(device, &self.frame, &self.sky.lut);
+ self.volumes
+ .draw_cloud_shadow(encoder, stamp(&mut self.timing, "hd cloud shadows"));
+ self.volumes
+ .draw_clouds(encoder, stamp(&mut self.timing, "hd clouds"));
+ } else {
+ self.volumes.reset_history();
+ }
+ if self.fog_active {
+ self.volumes.draw_fog(
+ device,
+ encoder,
+ &self.frame,
+ depth,
+ &self.shadows.array,
+ &self.sky.lut,
+ stamp(&mut self.timing, "hd fog"),
+ );
+ }
+ }
+}
+
#[cfg(test)]
-pub fn shader_sources() -> Vec<(&'static str, String)> {
+pub fn shader_sources() -> Vec<(&'static str, String, &'static [wgpu::BindingType])> {
vec![
- ("clouds", format!("{}{CLOUD}", cloud_body(""))),
+ (
+ "clouds",
+ format!("{}{CLOUD}", cloud_body("")),
+ &CLOUD_BINDINGS,
+ ),
(
"cloud shadows",
format!(
"const SHADOW_R: f32 = 1200.0;\n{}{CLOUD_SHADOW}",
cloud_body("")
),
+ &CLOUD_SHADOW_BINDINGS,
),
- ("fog", FOG.to_owned()),
+ ("fog", FOG.to_owned(), &FOG_BINDINGS),
]
}
diff --git a/crates/horizon-client/src/gfx/hd/water.rs b/crates/horizon-client/src/gfx/hd/water.rs
new file mode 100644
index 000000000..da972c5f2
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/water.rs
@@ -0,0 +1,209 @@
+//! Water: a geometry pass over the translucent terrain that keeps only the water sprites, then a
+//! composite that lays refraction, SSR/sky reflection, glint and the air over the lit world.
+//!
+//! Contract:
+//! - Geometry pass (`water_pass`, the caller draws `fs_water` of `terrain.wgsl`; world size, same
+//! jittered projection as the world): `tint` (`Rgba16Float`) = surface colour (sprite x biome
+//! tint, display space) with **a = block light**; `normal` (`Rgba8Unorm`) = the face normal
+//! encoded `n * 0.5 + 0.5` with **a = sky light**; `depth` (`Depth32Float`, reversed-Z, cleared to
+//! 0 = no water; depth-tested among water only, so land in front is the consumers' job).
+//! - Composite (`water_composite`) reads: `lit` (alpha 1 normal, **0.5 water** = the floor lit
+//! without air, **0.6 in front of the water** = the scene as drawn), the lighting's `air` target
+//! (over water: scattered air light rgb + transmittance a, applied once after absorption; in
+//! front of the water: the lit floor rgb), the G-buffer depth, `tint`, `normal`, `depth`, the
+//! shadow cascades, the lightmap and the sky texture. Writes `composite` (`Rgba16Float`); `lit_view`
+//! is that target afterwards.
+//! - Time: the waves use `HdFrame.water.y`, wall-clock seconds since HD started modulo 600, and the
+//! camera's x/z modulo 256 (`water.zw`) so they stand still in the world (period 256 blocks,
+//! whole cycles per 600 s). It is not game time: a replay paused in game time still has moving
+//! water.
+//! - Off underwater, without translucent terrain on screen and with `HORIZON_HD_WATER=0`.
+use super::gbuffer::DEPTH_FORMAT;
+use super::{
+ DEPTH, FLOAT, HDR, Hd, SAMPLER, SHADOW_ARRAY, SHADOW_SAMPLER, UNFILTERED, UNIFORM, entries,
+ fullscreen, module, stamp, target,
+};
+
+/// The water pass's targets: the surface colour with the block light, and the normal with the sky light.
+pub const WATER_TINT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
+pub const WATER_NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
+
+pub(super) const WATER: &str = include_str!("water.wgsl");
+/// The composite's group 0: frame, lit, G-buffer depth, water colour, normal, depth, shadow array
+/// and sampler, lightmap, linear sampler, sky texture, air.
+pub(super) const WATER_BINDINGS: [wgpu::BindingType; 12] = [
+ UNIFORM,
+ FLOAT,
+ DEPTH,
+ UNFILTERED,
+ UNFILTERED,
+ DEPTH,
+ SHADOW_ARRAY,
+ SHADOW_SAMPLER,
+ FLOAT,
+ SAMPLER,
+ FLOAT,
+ UNFILTERED,
+];
+
+/// The water effect is on unless `HORIZON_HD_WATER=0`.
+pub fn water_enabled() -> bool {
+ static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
+ *ON.get_or_init(|| std::env::var("HORIZON_HD_WATER").map_or(true, |v| v != "0"))
+}
+
+pub(super) struct Water {
+ tint: wgpu::TextureView,
+ normal: wgpu::TextureView,
+ depth: wgpu::TextureView,
+ /// The lit world with the water over it.
+ composite: wgpu::TextureView,
+ layout: wgpu::BindGroupLayout,
+ pipeline: wgpu::RenderPipeline,
+ group: Option<((wgpu::TextureView, wgpu::Sampler), wgpu::BindGroup)>,
+ /// The water effect runs this frame (the scene allows it and there is translucent terrain on
+ /// screen), and `lit_view` is the composite.
+ pub(super) active: bool,
+ composited: bool,
+}
+
+impl Water {
+ pub(super) fn new(device: &wgpu::Device) -> Self {
+ let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("HD water"),
+ entries: &entries(&WATER_BINDINGS),
+ });
+ let water = module(device, "HD water", WATER);
+ let pipeline = fullscreen(device, "HD water", &layout, &water, "fs_water", HDR, None);
+ Self {
+ tint: target(device, "HD water colour", [1, 1], WATER_TINT).view,
+ normal: target(device, "HD water normal", [1, 1], WATER_NORMAL).view,
+ depth: target(device, "HD water depth", [1, 1], DEPTH_FORMAT).view,
+ composite: target(device, "HD lit with water", [1, 1], HDR).view,
+ layout,
+ pipeline,
+ group: None,
+ active: false,
+ composited: false,
+ }
+ }
+
+ /// New world-size targets; the group names the old ones.
+ pub(super) fn resize(&mut self, device: &wgpu::Device, size: [u32; 2]) {
+ self.tint = target(device, "HD water colour", size, WATER_TINT).view;
+ self.normal = target(device, "HD water normal", size, WATER_NORMAL).view;
+ self.depth = target(device, "HD water depth", size, DEPTH_FORMAT).view;
+ self.composite = target(device, "HD lit with water", size, HDR).view;
+ self.group = None;
+ }
+
+ /// The nearest water surface's depth (0 where there is none), for the lighting.
+ pub(super) fn depth_view(&self) -> &wgpu::TextureView {
+ &self.depth
+ }
+
+ /// A new frame's lighting starts: the composite is stale until `water_composite` runs.
+ pub(super) fn new_frame(&mut self) {
+ self.composited = false;
+ }
+}
+
+impl Hd {
+ /// The lit world as later passes see it: with the water over it when the effect ran.
+ pub fn lit_view(&self) -> &wgpu::TextureView {
+ if self.water.composited {
+ &self.water.composite
+ } else {
+ &self.lighting.lit
+ }
+ }
+
+ /// The water effect runs this frame (the renderer draws the water quads with `water_pass`).
+ pub fn water_active(&self) -> bool {
+ self.water.active
+ }
+
+ /// The water pass's attachments: the surface colour and normal, and the water's own depth,
+ /// all cleared. The caller draws the translucent terrain with `fs_water`.
+ pub fn water_pass<'e>(
+ &mut self,
+ encoder: &'e mut wgpu::CommandEncoder,
+ ) -> wgpu::RenderPass<'e> {
+ let clear = |view| {
+ Some(wgpu::RenderPassColorAttachment {
+ view,
+ resolve_target: None,
+ depth_slice: None,
+ ops: wgpu::Operations {
+ load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+ store: wgpu::StoreOp::Store,
+ },
+ })
+ };
+ encoder
+ .begin_render_pass(&wgpu::RenderPassDescriptor {
+ label: Some("HD water"),
+ timestamp_writes: stamp(&mut self.timing, "hd water"),
+ color_attachments: &[clear(&self.water.tint), clear(&self.water.normal)],
+ depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+ view: &self.water.depth,
+ depth_ops: Some(wgpu::Operations {
+ load: wgpu::LoadOp::Clear(0.0),
+ store: wgpu::StoreOp::Store,
+ }),
+ stencil_ops: None,
+ }),
+ ..Default::default()
+ })
+ .forget_lifetime()
+ }
+
+ /// Lays the water over the lit world (refraction, reflection, glint) into a target of its
+ /// own; `lit_view` is that target after this.
+ pub fn water_composite(
+ &mut self,
+ device: &wgpu::Device,
+ encoder: &mut wgpu::CommandEncoder,
+ lightmap: (&wgpu::TextureView, &wgpu::Sampler),
+ ) {
+ if !self.water.active {
+ return;
+ }
+ let key = (lightmap.0.clone(), lightmap.1.clone());
+ if self.water.group.as_ref().is_none_or(|(k, _)| *k != key) {
+ let group = self.make_group(
+ device,
+ "HD water",
+ &self.water.layout,
+ &[
+ wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
+ wgpu::BindingResource::TextureView(&self.lighting.lit),
+ wgpu::BindingResource::TextureView(&self.gbuffer.depth),
+ wgpu::BindingResource::TextureView(&self.water.tint),
+ wgpu::BindingResource::TextureView(&self.water.normal),
+ wgpu::BindingResource::TextureView(&self.water.depth),
+ wgpu::BindingResource::TextureView(&self.shadows.array),
+ wgpu::BindingResource::Sampler(&self.shadows.sampler),
+ wgpu::BindingResource::TextureView(lightmap.0),
+ wgpu::BindingResource::Sampler(&self.sampler),
+ wgpu::BindingResource::TextureView(&self.sky.lut),
+ wgpu::BindingResource::TextureView(&self.lighting.air),
+ ],
+ );
+ self.water.group = Some((key, group));
+ }
+ let Some((_, group)) = &self.water.group else {
+ return;
+ };
+ Self::draw(
+ encoder,
+ "HD water composite",
+ &self.water.composite,
+ false,
+ &self.water.pipeline,
+ group,
+ stamp(&mut self.timing, "hd water composite"),
+ );
+ self.water.composited = true;
+ }
+}
diff --git a/crates/horizon-client/src/gfx/hd/water.wgsl b/crates/horizon-client/src/gfx/hd/water.wgsl
new file mode 100644
index 000000000..a1a30897d
--- /dev/null
+++ b/crates/horizon-client/src/gfx/hd/water.wgsl
@@ -0,0 +1,302 @@
+
+const PI: f32 = 3.14159265;
+const REFLECT_STEPS: i32 = 24;
+const REFLECT_RANGE: f32 = 96.0;
+@group(0) @binding(0) var<uniform> frame: HdFrame;
+@group(0) @binding(1) var lit_tex: texture_2d<f32>;
+@group(0) @binding(2) var gbuffer_depth: texture_depth_2d;
+@group(0) @binding(3) var water_tint: texture_2d<f32>;
+@group(0) @binding(4) var water_normal: texture_2d<f32>;
+@group(0) @binding(5) var water_depth: texture_depth_2d;
+@group(0) @binding(6) var shadow_map: texture_depth_2d_array;
+@group(0) @binding(7) var shadow_sampler: sampler_comparison;
+@group(0) @binding(8) var lightmap: texture_2d<f32>;
+@group(0) @binding(9) var linear: sampler;
+//SKY10
+// What the lighting left beside the lit world: over water the air in front of the surface (scattered
+// light, transmittance), in front of the water the lit floor.
+@group(0) @binding(11) var air_tex: texture_2d<f32>;
+
+fn ign(px: vec2<f32>) -> f32 {
+ let p = px + vec2<f32>(5.588238 * frame.params.w);
+ return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
+}
+
+// The camera-relative position of the screen point `uv` at depth z.
+fn position_at(uv: vec2<f32>, z: f32) -> vec3<f32> {
+ let h = frame.inverse_view_proj * vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
+ return h.xyz / h.w;
+}
+
+// The screen point (uv, clip w) of a camera-relative position.
+fn project(p: vec3<f32>) -> vec3<f32> {
+ let c = frame.view_proj * vec4<f32>(p, 1.0);
+ return vec3<f32>(c.x / c.w * 0.5 + 0.5, 0.5 - c.y / c.w * 0.5, c.w);
+}
+
+fn on_screen(uv: vec2<f32>) -> bool {
+ return all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0));
+}
+
+fn texel(uv: vec2<f32>) -> vec2<i32> {
+ return clamp(vec2<i32>(uv * frame.resolution.xy), vec2<i32>(0), vec2<i32>(frame.resolution.xy) - 1);
+}
+
+// Sine waves in world space: wave vectors that are whole multiples of 2 pi / 256 and speeds that
+// are whole cycles per 600 s, so the sum repeats every 256 blocks and 10 minutes and holds still
+// in the world as the camera moves. x, y: the vector in units of 2 pi / 256; z: the slope the
+// wave adds; w: its phase.
+const WAVES = array<vec4<f32>, 26>(
+ vec4<f32>(3.0, 1.0, 0.040, 0.3),
+ vec4<f32>(-2.0, 5.0, 0.045, 2.1),
+ vec4<f32>(7.0, 2.0, 0.040, 4.4),
+ vec4<f32>(-5.0, -9.0, 0.040, 1.2),
+ vec4<f32>(11.0, -4.0, 0.035, 5.6),
+ vec4<f32>(13.0, 10.0, 0.035, 3.3),
+ vec4<f32>(-17.0, 7.0, 0.030, 0.9),
+ vec4<f32>(9.0, 23.0, 0.030, 2.7),
+ vec4<f32>(-27.0, 19.0, 0.025, 4.9),
+ vec4<f32>(31.0, 30.0, 0.025, 1.8),
+ // Fine ripples (wavelengths of 0.7 to 3 blocks) for the many small sun sparkles: slopes small,
+ // faded out by distance and pixel size like the rest.
+ vec4<f32>(83.0, 41.0, 0.042, 0.4),
+ vec4<f32>(-97.0, 59.0, 0.042, 3.1),
+ vec4<f32>(71.0, -113.0, 0.042, 5.0),
+ vec4<f32>(131.0, 89.0, 0.040, 1.5),
+ vec4<f32>(-157.0, -103.0, 0.040, 4.2),
+ vec4<f32>(191.0, -67.0, 0.038, 2.4),
+ vec4<f32>(-223.0, 149.0, 0.038, 0.8),
+ vec4<f32>(101.0, 241.0, 0.036, 3.7),
+ vec4<f32>(-263.0, -181.0, 0.034, 5.5),
+ vec4<f32>(311.0, 127.0, 0.034, 2.0),
+ // Finer still (0.35 to 0.6 blocks): the small sparkles.
+ vec4<f32>(397.0, -263.0, 0.050, 1.1),
+ vec4<f32>(-467.0, 331.0, 0.050, 4.6),
+ vec4<f32>(521.0, 449.0, 0.045, 3.0),
+ vec4<f32>(-613.0, -199.0, 0.045, 5.9),
+ vec4<f32>(293.0, 577.0, 0.045, 0.2),
+ vec4<f32>(-701.0, 263.0, 0.040, 2.8),
+);
+
+// The gradient of the water's height at world x, z: the waves that a pixel `footprint` blocks
+// wide can still resolve (finer ones fade out, or they would shimmer).
+fn wave_gradient(xz: vec2<f32>, footprint: f32) -> vec2<f32> {
+ var grad = vec2<f32>(0.0);
+ for (var i = 0; i < 26; i++) {
+ let w = WAVES[i];
+ let k = w.xy * (2.0 * PI / 256.0);
+ let len = length(k);
+ let fade = clamp((2.0 * PI / len) / max(footprint, 0.0001) * 0.2 - 0.6, 0.0, 1.0);
+ let cycles = round(sqrt(9.8 * len) * 0.5 * 600.0 / (2.0 * PI));
+ let omega = cycles * 2.0 * PI / 600.0;
+ let phase = dot(k, xz) - omega * frame.water.y + w.w;
+ grad += (k / len) * (w.z * fade * cos(phase));
+ }
+ // A little higher than the table's slopes.
+ return grad * 1.25;
+}
+
+fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
+ let s = frame.cascades[c] * vec4<f32>(p, 1.0);
+ return vec3<f32>(s.x * 0.5 + 0.5, 0.5 - s.y * 0.5, s.z);
+}
+
+// The sun's (or moon's) light reaching p: 1 lit, 0 in shadow (as the lighting pass has it).
+fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
+ let dist = length(p);
+ let c = select(1, 0, dist < frame.shadow.y * 0.92);
+ let texel_size = select(frame.shadow.z, frame.shadow.y, c == 0) * 2.0 / 2048.0;
+ let q = p + n * texel_size * 1.5;
+ let s = cascade_at(q, c);
+ if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return 1.0; }
+ var lit = 0.0;
+ let step = 1.0 / 2048.0;
+ for (var y = -1; y <= 1; y++) {
+ for (var x = -1; x <= 1; x++) {
+ lit += textureSampleCompareLevel(shadow_map, shadow_sampler, s.xy + vec2<f32>(f32(x), f32(y)) * step, c, s.z - 0.0005);
+ }
+ }
+ let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
+ return mix(1.0, lit / 9.0, fade);
+}
+
+fn linear_fog(d: f32, start: f32, end: f32) -> f32 {
+ if d <= start { return 0.0; }
+ if d >= end { return 1.0; }
+ return (d - start) / (end - start);
+}
+
+// What the sky shows along r when the ray leaves the screen: the horizon's colour rising to
+// the zenith's, darker where the water lies under cover (`vis` is the sky light there).
+fn sky_along(r: vec3<f32>, vis: f32) -> vec3<f32> {
+ var c = frame.fog_color.rgb;
+ if frame.sky_sun.w > 0.5 {
+ c = atmosphere_sky(r);
+ } else if frame.sky_color.w > 0.5 {
+ c = mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, r.y));
+ }
+ return c * mix(0.12, 1.0, vis);
+}
+
+// Marches the ray from p along r over the depth buffer (28 steps, growing, jittered so TAA
+// averages the placement; then a binary search for the crossing). xyz: the colour found,
+// w: how much to trust it (0 for a miss, less near the screen's edge).
+fn reflection(p: vec3<f32>, r: vec3<f32>, jitter: f32) -> vec4<f32> {
+ var near = 0.0;
+ for (var i = 0; i < REFLECT_STEPS; i++) {
+ let u = (f32(i) + jitter) / f32(REFLECT_STEPS);
+ let s = 0.12 + REFLECT_RANGE * u * u;
+ let q = p + r * s;
+ let sp = project(q);
+ if sp.z <= 0.0 || !on_screen(sp.xy) { break; }
+ let z = textureLoad(gbuffer_depth, texel(sp.xy), 0);
+ if z > 0.0 {
+ let c = frame.view_proj * vec4<f32>(q, 1.0);
+ // Reversed Z: a larger depth is nearer; the ray is behind the surface when the
+ // surface is nearer than it.
+ if z > c.z / c.w {
+ let behind = length(q) - length(position_at(sp.xy, z));
+ if behind < 0.4 + 0.08 * s {
+ var lo = near;
+ var hi = s;
+ for (var k = 0; k < 5; k++) {
+ let m = 0.5 * (lo + hi);
+ let mq = p + r * m;
+ let mp = project(mq);
+ let mc = frame.view_proj * vec4<f32>(mq, 1.0);
+ let mz = textureLoad(gbuffer_depth, texel(mp.xy), 0);
+ if mz > mc.z / mc.w { hi = m; } else { lo = m; }
+ }
+ let hp = project(p + r * hi);
+ let edge = min(min(hp.x, 1.0 - hp.x), min(hp.y, 1.0 - hp.y));
+ let colour = textureSampleLevel(lit_tex, linear, hp.xy, 0.0).rgb;
+ return vec4<f32>(colour, smoothstep(0.0, 0.08, edge));
+ }
+ }
+ }
+ near = s;
+ }
+ return vec4<f32>(0.0);
+}
+
+// The sun's glint on the waves (GGX, a rough lobe so it glitters instead of aliasing).
+fn glint(n: vec3<f32>, v: vec3<f32>, l: vec3<f32>) -> f32 {
+ let h = normalize(l + v);
+ let a = 0.07;
+ let nh = max(dot(n, h), 0.0);
+ let d = a * a / (PI * pow(nh * nh * (a * a - 1.0) + 1.0, 2.0));
+ let f = 0.02 + 0.98 * pow(1.0 - max(dot(v, h), 0.0), 5.0);
+ return min(d * f * 0.25 * max(dot(n, l), 0.0) / max(dot(n, v), 0.1), 12.0);
+}
+
+// The water over the lit world: the world behind it (refracted by the waves, dimmed and tinted
+// by the water's depth), the sky and the world above it (a screen-space reflection) by Fresnel,
+// and the sun's glint. Pixels without water pass through.
+@fragment
+fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
+ let px = vec2<i32>(in.position.xy);
+ let base = textureLoad(lit_tex, px, 0);
+ if base.a > 0.75 { return vec4<f32>(base.rgb, 1.0); }
+ // Alpha 0.6: something translucent stands in front of the water here (base is the scene).
+ let front = base.a > 0.55;
+ let aux = textureLoad(air_tex, px, 0);
+ let wd = textureLoad(water_depth, px, 0);
+ let g = textureLoad(gbuffer_depth, px, 0);
+ let tint = textureLoad(water_tint, px, 0);
+ let nt = textureLoad(water_normal, px, 0);
+ let p = position_at(in.uv, wd);
+ let dist = length(p);
+ let v = -p / dist;
+ let geometric = normalize(nt.xyz * 2.0 - 1.0);
+ // The waves tilt the flat parts of the surface only.
+ let footprint = dist * frame.resolution.w * 1.4;
+ let grad = wave_gradient(p.xz + frame.water.zw, footprint);
+ let flat_part = smoothstep(0.6, 0.95, geometric.y);
+ var n = normalize(geometric + vec3<f32>(-grad.x, 0.0, -grad.y) * flat_part);
+ n = normalize(n + v * max(0.03 - dot(n, v), 0.0));
+ let ndv = max(dot(n, v), 0.02);
+
+ // Light on the water: vanilla's lightmap at the surface, and the sun where it reaches.
+ let levels = vec2<f32>(nt.a, tint.a);
+ let sky_reach = smoothstep(0.35, 0.95, levels.x);
+ var ambient = textureSampleLevel(lightmap, linear, clamp(vec2<f32>(levels.y, levels.x) * (15.0 / 16.0) + 0.5 / 16.0, vec2<f32>(0.5 / 16.0), vec2<f32>(15.5 / 16.0)), 0.0).rgb;
+ if frame.sky_sun.w > 0.5 {
+ ambient *= mix(vec3<f32>(1.0), sky_light_tint(vec3<f32>(0.0, 1.0, 0.0)), 0.7 * sky_reach);
+ }
+ let block = textureSampleLevel(lightmap, linear, vec2<f32>(levels.y * (15.0 / 16.0) + 0.5 / 16.0, 0.5 / 16.0), 0.0).rgb;
+ let sun = frame.sun.xyz;
+ let sun_on = frame.sun.w;
+ var sunlit = 0.0;
+ if sun_on > 0.0 && sun.y > 0.0 && sky_reach > 0.0 { sunlit = shadow(p, vec3<f32>(0.0, 1.0, 0.0)) * sky_reach; }
+ let volume_light = max(ambient * frame.ambient.x, block * frame.ambient.y) + frame.sun_color.rgb * sun_on * sunlit * 0.8;
+
+ // Refraction: where the ray through the waves meets the world, against where the flat surface
+ // would send it (so still water looks as vanilla's and only the waves distort). Not where
+ // something translucent is in front of the water, nor onto texels that hold such a thing.
+ let has_floor = g > 0.0;
+ var uv_floor = in.uv;
+ var floor_z = g;
+ if has_floor && !front {
+ let floor0 = position_at(in.uv, g);
+ let depth0 = max(length(floor0) - dist, 0.0);
+ let bent = refract(-v, n, 0.75) - refract(-v, geometric, 0.75);
+ let moved = project(floor0 + bent * min(depth0, 24.0));
+ if moved.z > 0.0 && on_screen(moved.xy) {
+ let z = textureLoad(gbuffer_depth, texel(moved.xy), 0);
+ // The four texels the filter blends: none may be a pixel with a translucent thing
+ // in front of the water.
+ let marks = textureGather(3, lit_tex, linear, moved.xy);
+ let tainted = any((marks > vec4<f32>(0.55)) & (marks < vec4<f32>(0.75)));
+ // Only what lies behind the water: not the bank or an entity in front of it.
+ if z > 0.0 && z <= wd && !tainted {
+ uv_floor = moved.xy;
+ floor_z = z;
+ }
+ }
+ }
+ var thickness = 48.0;
+ if has_floor {
+ thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0);
+ }
+ var behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb;
+ if front { behind = aux.rgb; }
+ // A deep, dark water: the biome's colour darkened and a little greyed, so it reads as
+ // depth rather than as bright paint.
+ let biome = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
+ let colour = mix(vec3<f32>(dot(biome, vec3<f32>(0.333))), biome, 0.85) * vec3<f32>(0.78, 0.86, 0.95);
+ // The colours the water takes up fastest are the ones it has the least of.
+ let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.6 * (thickness + 0.3));
+ let refracted = behind * absorb + colour * volume_light * mix(0.65, 0.3, frame.sun_color.a) * (vec3<f32>(1.0) - absorb);
+
+ // Reflection: the screen where the ray stays on it, the sky where it leaves it or misses.
+ var r = reflect(-v, n);
+ r.y = abs(r.y);
+ // Looking down onto it the reflection is a few percent of the picture: the sky alone does.
+ let fresnel = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);
+ var hit = vec4<f32>(0.0);
+ if fresnel > 0.04 { hit = reflection(p, r, ign(in.position.xy)); }
+ let sky_vis = mix(0.15, 1.0, sky_reach);
+ let reflected = mix(sky_along(r, sky_vis), hit.rgb, hit.w);
+
+ var c = refracted * (1.0 - fresnel) + reflected * fresnel;
+ if sun_on > 0.0 && sunlit > 0.0 {
+ c += frame.sun_color.rgb * sun_on * sunlit * glint(n, v, sun);
+ }
+ // Vanilla's fog over it, at the surface.
+ let sph = dist;
+ let cyl = max(length(p.xz), abs(p.y));
+ let f = max(linear_fog(sph, frame.fog.x, frame.fog.y), linear_fog(cyl, frame.fog.z, frame.fog.w));
+ let haze = haze_colour(-v);
+ c = mix(c, haze, f * frame.fog_color.a);
+ if frame.sky_sun.w > 0.5 && frame.fog.x > 1.0e6 {
+ c = mix(c, haze, (1.0 - exp(-dist / 700.0)) * 0.5);
+ }
+ if front {
+ // The scene as drawn, with the water's change to the floor laid under the translucent thing.
+ c = base.rgb + (c - aux.rgb) * 0.7;
+ } else {
+ // The air between the camera and the surface: volumetric fog, rays, halos.
+ c = c * aux.a + aux.rgb;
+ }
+ return vec4<f32>(max(c, vec3<f32>(0.0)), 1.0);
+}
diff --git a/crates/horizon-client/src/gfx/hd_sky.rs b/crates/horizon-client/src/gfx/hd_sky.rs
deleted file mode 100644
index dbe8c8853..000000000
--- a/crates/horizon-client/src/gfx/hd_sky.rs
+++ /dev/null
@@ -1,390 +0,0 @@
-//! AirDef fork: the HD renderer's atmosphere. A small sky-view texture (azimuth from the sun by
-//! elevation, plus a row of ambient samples) is marched each frame from single scattering of
-//! Rayleigh and Mie particles and ozone absorption with a cheap multiple-scattering term; the
-//! lighting and water shaders read it for the sky, the aerial-perspective haze, the water's
-//! reflection fallback and the colour of the sky's light. The sunlight's colour is the same
-//! atmosphere's transmittance at the sun's elevation (`sun_transmittance`).
-//!
-//! The coefficients, the density profiles (scale heights, the ozone layer), the Henyey-Greenstein
-//! Mie parameters and the Chapman-function transmittance come from Photon by sixthsurge (MIT
-//! licence, see THIRD_PARTY_PHOTON.md), shaders/include/sky/atmosphere.glsl at commit 26c2906.
-
-/// The sky-view texture: 128 azimuths by 64 elevations, then 8 rows for the ambient samples
-/// (row 64: texel 0 the zenith, texel 1 the horizon's average).
-pub const SKY_LUT_SIZE: [u32; 2] = [128, 72];
-pub const SKY_LUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
-
-/// The atmosphere's constants and the pass that marches the sky-view texture (after `COMMON`).
-pub const SKY_PASS: &str = r#"
-@group(0) @binding(0) var<uniform> frame: HdFrame;
-
-// Photon's atmosphere (sixthsurge, MIT): metres, Earth's size; the camera stands on the surface.
-const PI: f32 = 3.14159265;
-const PLANET_R: f32 = 6371000.0;
-const ATMOSPHERE_TOP: f32 = 6481000.0;
-const GROUND_R: f32 = 6370000.0;
-const SCALE_HEIGHTS: vec2<f32> = vec2<f32>(8400.0, 1250.0);
-// Photon marches in Rec. 2020 (its working colour space): the coefficients are the Rec. 709 ones
-// moved to it, and the radiance goes back to Rec. 709 at the end. This keeps the sunset's reds and
-// purples apart where the narrower primaries would mix them into yellow and green.
-const RAYLEIGH: vec3<f32> = vec3<f32>(1.232851742e-05, 1.638796144e-05, 3.814816455e-05);
-const MIE: vec3<f32> = vec3<f32>(1.727386213e-06, 1.804192789e-06, 1.941423374e-06);
-const OZONE: vec3<f32> = vec3<f32>(9.563766055e-07, 1.267105338e-06, 1.780727031e-07);
-const REC2020_TO_709 = mat3x3<f32>(
- vec3<f32>(1.66034406, -0.12437868, -0.01811272),
- vec3<f32>(-0.58757158, 1.13283707, -0.10058379),
- vec3<f32>(-0.07288024, -0.00835868, 1.11862127));
-const MIE_KN_ENERGY: f32 = 3000.0;
-const MIE_ALBEDO: f32 = 0.9;
-const MIE_G: f32 = 0.77;
-// Sunlight in space (Photon's AM0 colour) and how bright the sun and the moon shine here.
-const SUN_I: vec3<f32> = vec3<f32>(1.051, 0.985, 0.940) * 24.0;
-const MOON_I: vec3<f32> = vec3<f32>(0.62, 0.78, 1.0) * 0.07;
-// The share of the light scattered a second time, spread evenly (the haze's lift).
-const MULTI_SCATTER: f32 = 0.6;
-const STEPS: i32 = 32;
-
-// Photon's density: Rayleigh, Mie (exponential) and the ozone layer, at an altitude in metres.
-fn density_at(altitude: f32) -> vec3<f32> {
- let rm = exp(-max(altitude, 0.0) / SCALE_HEIGHTS);
- let km = altitude * 1e-3;
- let o1 = 12.5 * exp(-km / 8.0);
- let o2 = 30.0 * exp((18.0 - km) * (km - 18.0) / 80.0);
- let o3 = 75.0 * exp((23.5 - km) * (km - 23.5) / 50.0);
- let o4 = 50.0 * exp((30.0 - km) * (km - 30.0) / 150.0);
- return vec3<f32>(rm, 7.428e-3 * (o1 + o2 + o3 + o4));
-}
-
-// Chapman function approximation (thetenthplanet.de/archives/4519, as Photon uses it).
-fn chapman(x: f32, cos_theta: f32) -> f32 {
- let c = sqrt(1.5707963 * x);
- if cos_theta >= 0.0 {
- return c / ((c - 1.0) * cos_theta + 1.0);
- }
- let sin_theta = sqrt(clamp(1.0 - cos_theta * cos_theta, 0.0, 1.0));
- return c / ((c - 1.0) * cos_theta - 1.0) + 2.0 * c * exp(min(x - x * sin_theta, 80.0)) * sqrt(sin_theta);
-}
-
-// The light reaching a point r from the planet's centre (altitude h, whose sky direction makes
-// cos mu with the up there) from the sun or the moon; 0 when the planet is in the way.
-fn light_transmittance(mu: f32, r: f32, h: f32) -> vec3<f32> {
- if mu < 0.0 && r * r * (1.0 - mu * mu) < PLANET_R * PLANET_R { return vec3<f32>(0.0); }
- let dens = exp(-max(h, 0.0) / SCALE_HEIGHTS);
- var air = SCALE_HEIGHTS * dens;
- air.x *= chapman(r / SCALE_HEIGHTS.x, mu);
- air.y *= chapman(r / SCALE_HEIGHTS.y, mu);
- return clamp(exp(-(RAYLEIGH * air.x + MIE * air.y + OZONE * air.x)), vec3<f32>(0.0), vec3<f32>(1.0));
-}
-
-// Photon's Rayleigh phase with the depolarisation per channel (the sky's slight tint).
-fn rayleigh_phase(nu: f32) -> vec3<f32> {
- let depolarization = vec3<f32>(2.786, 2.842, 2.899) * 1e-2;
- let gamma = depolarization / (vec3<f32>(2.0) - depolarization);
- let k = 3.0 / (16.0 * PI * (vec3<f32>(1.0) + 2.0 * gamma));
- return k * ((vec3<f32>(1.0) + 3.0 * gamma) + (vec3<f32>(1.0) - gamma) * nu * nu);
-}
-
-// Photon's Klein-Nishina phase for the aerosols ("far closer to an actual aerosol phase function
-// than Henyey-Greenstein"): a tight, bright glow round the sun.
-fn mie_phase(nu: f32) -> f32 {
- let e = MIE_KN_ENERGY;
- return e / (2.0 * PI * (e - e * nu + 1.0) * log(2.0 * e + 1.0));
-}
-
-fn linear_step(a: f32, b: f32, x: f32) -> f32 {
- return clamp((x - a) / (b - a), 0.0, 1.0);
-}
-// Photon's pulse: 1 at the centre, 0 at the width, smooth between.
-fn pulse(x: f32, centre: f32, width: f32) -> f32 {
- let t = abs(x - centre) / width;
- if t > 1.0 { return 0.0; }
- return 1.0 - t * t * (3.0 - 2.0 * t);
-}
-
-// The sunlight's tint and extra strength round sunrise and sunset, from Photon's
-// get_sun_tint / get_sun_exposure (light_color.glsl): pinkish when the sun is low, violet at the
-// blue hour, and a strong lift then so the twilight sky does not go dark. `y`: the sun's height.
-fn sun_light_tint(y: f32) -> vec3<f32> {
- let blue_hour = linear_step(0.05, 1.0, exp(-190.0 * (y + 0.09604) * (y + 0.09604)));
- let p = pulse(y, 0.17, 0.40);
- let morning_evening = mix(vec3<f32>(1.0), vec3<f32>(1.05, 0.84, 0.93) * 1.2, p * p);
- let blue_tint = mix(vec3<f32>(1.0), vec3<f32>(0.95, 0.80, 1.0), blue_hour);
- let low = 1.0 - smoothstep(0.0, 0.3, abs(y));
- return morning_evening * blue_tint * (1.0 + 0.5 * low + 40.0 * blue_hour);
-}
-
-// The radiance the camera sees along `view` (y up) with the sun and the moon in the directions
-// given: single scattering plus a flat multiple-scattering term.
-fn march(view: vec3<f32>, sun: vec3<f32>, moon: vec3<f32>) -> vec3<f32> {
- let b = PLANET_R * view.y;
- let t_max = -b + sqrt(b * b + ATMOSPHERE_TOP * ATMOSPHERE_TOP - PLANET_R * PLANET_R);
- let nu_sun = dot(view, sun);
- let nu_moon = dot(view, moon);
- let sun_i = SUN_I * sun_light_tint(frame.sky_sun.y);
- let phase_r_sun = rayleigh_phase(nu_sun);
- let phase_m_sun = mie_phase(nu_sun);
- let phase_r_moon = rayleigh_phase(nu_moon);
- let phase_m_moon = mie_phase(nu_moon);
- var od = vec3<f32>(0.0);
- var sum = vec3<f32>(0.0);
- var t0 = 0.0;
- for (var i = 0; i < STEPS; i++) {
- let u = (f32(i) + 1.0) / f32(STEPS);
- let t1 = t_max * u * u;
- let dt = t1 - t0;
- let t = 0.5 * (t0 + t1);
- t0 = t1;
- let r2 = PLANET_R * PLANET_R + 2.0 * PLANET_R * t * view.y + t * t;
- let r = sqrt(r2);
- // The altitude without the cancellation of r - planet radius.
- let altitude = (2.0 * PLANET_R * t * view.y + t * t) / (r + PLANET_R);
- let dens = density_at(altitude);
- let extinction = RAYLEIGH * dens.x + MIE * dens.y + OZONE * dens.z;
- let view_t = exp(-(od + 0.5 * extinction * dt));
- od += extinction * dt;
- let mu_sun = (PLANET_R * sun.y + t * nu_sun) / r;
- let mu_moon = (PLANET_R * moon.y + t * nu_moon) / r;
- let sun_t = light_transmittance(mu_sun, r, altitude);
- let moon_t = light_transmittance(mu_moon, r, altitude);
- let scatter_r = RAYLEIGH * dens.x;
- let scatter_m = MIE * MIE_ALBEDO * dens.y;
- let single = (scatter_r * phase_r_sun + scatter_m * phase_m_sun) * sun_t * sun_i
- + (scatter_r * phase_r_moon + scatter_m * phase_m_moon) * moon_t * MOON_I;
- let multi = (scatter_r + scatter_m) * (1.0 / (4.0 * PI)) * MULTI_SCATTER
- * (sun_t * sun_i + moon_t * MOON_I) * 0.5;
- sum += view_t * (single + multi) * dt;
- }
- return max(REC2020_TO_709 * sum, vec3<f32>(0.0));
-}
-
-@fragment
-fn fs_sky_lut(in: FullOut) -> @location(0) vec4<f32> {
- let px = vec2<i32>(in.position.xy);
- // The texture's frame: the sun in the xy plane, the moon opposite.
- let sin_e = clamp(frame.sky_sun.y, -1.0, 1.0);
- let sun = vec3<f32>(sqrt(1.0 - sin_e * sin_e), sin_e, 0.0);
- let moon = -sun;
- // A faint night glow, so the night sky is not black.
- let night = vec3<f32>(0.0006, 0.0011, 0.0026);
- if px.y < 64 {
- let phi = f32(px.x) / 127.0 * PI;
- let v = f32(px.y) / 63.0;
- let l = v * v * PI * 0.5;
- let dir = vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi));
- return vec4<f32>(march(dir, sun, moon) + night, 1.0);
- }
- if px.y == 64 && px.x == 0 {
- return vec4<f32>(march(vec3<f32>(0.0, 1.0, 0.0), sun, moon) + night, 1.0);
- }
- if px.y == 64 && px.x == 1 {
- var sum = vec3<f32>(0.0);
- for (var k = 0; k < 6; k++) {
- let phi = (f32(k) + 0.5) / 6.0 * PI;
- let l = 0.3;
- sum += march(vec3<f32>(cos(l) * cos(phi), sin(l), cos(l) * sin(phi)), sun, moon);
- }
- return vec4<f32>(sum / 6.0 + night, 1.0);
- }
- return vec4<f32>(0.0);
-}
-"#;
-
-/// Reading the sky-view texture, in the lighting and the water shaders (`SKYBIND` is replaced by
-/// the binding of the texture; `frame` and the `linear` sampler come before it).
-pub const SKY_FUNCS: &str = r#"
-@group(0) @binding(SKYBIND) var sky_lut: texture_2d<f32>;
-const SKY_PI: f32 = 3.14159265;
-// How bright the sky is shown (the exposure of the physical radiance) and its gamma.
-const SKY_EXPOSURE: f32 = 0.85;
-
-// Where the direction `dir` (world, y up) lies in the sky-view texture.
-fn sky_uv(dir: vec3<f32>) -> vec2<f32> {
- let s = frame.sky_sun.xyz;
- let l = asin(clamp(dir.y, 0.0, 1.0));
- var cphi = 1.0;
- let dl = length(dir.xz);
- let sl = length(s.xz);
- if dl > 1e-4 && sl > 1e-4 { cphi = dot(dir.xz / dl, s.xz / sl); }
- let u = acos(clamp(cphi, -1.0, 1.0)) / SKY_PI;
- let v = sqrt(l / (SKY_PI * 0.5));
- return vec2<f32>((u * 127.0 + 0.5) / 128.0, (v * 63.0 + 0.5) / 72.0);
-}
-
-// The physical sky's colour as it is shown: a soft shoulder, then the display's gamma.
-fn sky_show(radiance: vec3<f32>) -> vec3<f32> {
- // Compressed by the brightest channel, so the hue stays as the atmosphere made it.
- let peak = max(max(radiance.r, radiance.g), max(radiance.b, 1e-6));
- let c = pow(radiance * ((1.0 - exp(-peak * SKY_EXPOSURE)) / peak), vec3<f32>(1.0 / 1.9));
- // A little more colour (Photon boosts the atmosphere's saturation too).
- // The low sun gets more, for orange and pink in place of pastel.
- let boost = 1.2 + 0.8 * (1.0 - smoothstep(0.0, 0.35, frame.sky_sun.y));
- return max(mix(vec3<f32>(dot(c, vec3<f32>(0.2126, 0.7152, 0.0722))), c, boost), vec3<f32>(0.0));
-}
-
-// The vanilla sky's own gradient (the overcast and the rain keep it).
-fn vanilla_gradient(dir: vec3<f32>) -> vec3<f32> {
- return mix(frame.fog_color.rgb, frame.sky_color.rgb, smoothstep(0.0, 0.5, max(dir.y, 0.0)));
-}
-
-// The sky along `dir`: the atmosphere, greyed towards vanilla's colours in rain.
-fn atmosphere_sky(dir: vec3<f32>) -> vec3<f32> {
- let lin = textureSampleLevel(sky_lut, linear, sky_uv(dir), 0.0).rgb;
- var c = sky_show(lin);
- let overcast = frame.sky_extra.z;
- if overcast > 0.0 { c = mix(c, vanilla_gradient(dir), overcast); }
- return c;
-}
-
-// The colour distant things fade to: the sky just above the horizon in their direction (so the
-// haze leans towards the sun), or vanilla's fog colour where the environment sets its own fog.
-fn haze_colour(dir: vec3<f32>) -> vec3<f32> {
- if frame.sky_sun.w < 0.5 || frame.fog.x < 1.0e6 { return frame.fog_color.rgb; }
- let up = max(dir.y, 0.0) * 0.5 + 0.035;
- return atmosphere_sky(normalize(vec3<f32>(dir.x, 0.0, dir.z) + vec3<f32>(0.0, up, 0.0)));
-}
-
-// The sky's light for a surface of normal n: the horizon's colour from the sides, the zenith's
-// from above, as a hue (luminance 1) to tint vanilla's sky light with.
-fn sky_light_tint(n: vec3<f32>) -> vec3<f32> {
- let zenith = sky_show(textureLoad(sky_lut, vec2<i32>(0, 64), 0).rgb);
- let horizon = sky_show(textureLoad(sky_lut, vec2<i32>(1, 64), 0).rgb);
- let up = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
- let c = mix(horizon, zenith, smoothstep(0.35, 1.0, up));
- let luma = dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
- return c / max(luma, 0.003);
-}
-
-// vanilla's sky (terrain.wgsl: fs_sky), to tell what the world drew on the sky pixel (the sun and
-// moon sprites, the stars, the clouds) from its plain gradient.
-fn vanilla_disc_distance(d: vec3<f32>, y: f32) -> vec2<f32> {
- if d.y * y <= 0.0 { return vec2<f32>(-1.0); }
- let p = d * (y / d.y);
- let h = length(p.xz);
- let phi = atan2(p.z, p.x);
- let sector = SKY_PI / 4.0;
- let delta = phi - (floor(phi / sector) * sector + sector * 0.5);
- let edge = 512.0 * cos(sector * 0.5) / cos(delta);
- if h > edge { return vec2<f32>(-1.0); }
- let s = h / edge;
- let ay = abs(y);
- let rim = sqrt(512.0 * 512.0 + y * y);
- return vec2<f32>(ay + s * (rim - ay), ay + s * (512.0 - ay));
-}
-fn vanilla_linear_fog(d: f32, start: f32, end: f32) -> f32 {
- if d <= start { return 0.0; }
- if d >= end { return 1.0; }
- return (d - start) / (end - start);
-}
-fn vanilla_rotate_x90(v: vec3<f32>) -> vec3<f32> { return vec3<f32>(v.x, -v.z, v.y); }
-fn vanilla_sunrise_weight(d: vec3<f32>) -> f32 {
- let alpha = frame.sunrise.a;
- if alpha <= 0.001 { return 0.0; }
- let flip = select(0.0, 180.0, frame.sky_sun.x > 0.0);
- let theta = radians(flip + 90.0);
- let c = cos(theta);
- let s = sin(theta);
- // Above the horizon the fan reaches only the sun's side (x of the same sign as the apex's, -s)
- // and rises at most alpha/3 per block of that distance (its rim stands 40*alpha high over a
- // 120-block radius and the points above the horizon lie at least 120 * the share of the rim
- // from the axis); anything else cannot hit it, so most of the sky skips the 16 triangles.
- if d.y > 0.0 {
- let side = -s * d.x;
- if side <= 0.0 || d.y > alpha * (1.0 / 3.0) * side * 1.02 + 0.001 { return 0.0; }
- }
- var best = 0.0;
- let apex = vanilla_rotate_x90(vec3<f32>(-100.0 * s, 100.0 * c, 0.0));
- for (var i = 0; i < 16; i++) {
- let a0 = f32(i) * 2.0 * SKY_PI / 16.0;
- let a1 = f32(i + 1) * 2.0 * SKY_PI / 16.0;
- let l0 = vec3<f32>(sin(a0) * 120.0, cos(a0) * 120.0, -cos(a0) * 40.0 * alpha);
- let l1 = vec3<f32>(sin(a1) * 120.0, cos(a1) * 120.0, -cos(a1) * 40.0 * alpha);
- let r0 = vanilla_rotate_x90(vec3<f32>(l0.x * c - l0.y * s, l0.x * s + l0.y * c, l0.z));
- let r1 = vanilla_rotate_x90(vec3<f32>(l1.x * c - l1.y * s, l1.x * s + l1.y * c, l1.z));
- let e1 = r0 - apex;
- let e2 = r1 - apex;
- let pv = cross(d, e2);
- let det = dot(e1, pv);
- if abs(det) < 1e-6 { continue; }
- let tv = -apex;
- let u = dot(tv, pv) / det;
- let qv = cross(tv, e1);
- let v = dot(d, qv) / det;
- let t = dot(e2, qv) / det;
- if u >= 0.0 && v >= 0.0 && u + v <= 1.0 && t > 0.0 {
- best = max(best, 1.0 - u - v);
- }
- }
- return best;
-}
-fn vanilla_sky(d: vec3<f32>) -> vec3<f32> {
- var color = frame.fog_color.rgb;
- let dist = vanilla_disc_distance(d, 16.0);
- if dist.x >= 0.0 {
- let end = frame.sky_extra.x;
- let f = max(vanilla_linear_fog(dist.x, 0.0, end), vanilla_linear_fog(dist.y, end, end));
- color = mix(frame.sky_color.rgb, frame.fog_color.rgb, f * frame.fog_color.a);
- }
- let w = vanilla_sunrise_weight(d);
- if w > 0.0 {
- color = mix(color, frame.sunrise.rgb, w * frame.sunrise.a);
- }
- return color;
-}
-"#;
-
-/// The sun's light at the ground: the atmosphere's transmittance along the sun's direction
-/// (`sin_elevation`, the y of the unit vector towards it), linear, as a fraction of the light
-/// outside the air. Same Chapman model as the sky pass.
-pub fn sun_transmittance(sin_elevation: f32) -> glam::Vec3 {
- const PLANET: f32 = 6_371_000.0;
- const SCALE: [f32; 2] = [8400.0, 1250.0];
- // The same Rec. 2020 coefficients as the sky pass; the result is moved back to Rec. 709.
- const RAYLEIGH: glam::Vec3 = glam::Vec3::new(1.232_851_7e-5, 1.638_796_1e-5, 3.814_816_5e-5);
- const MIE: glam::Vec3 = glam::Vec3::new(1.727_386_2e-6, 1.804_192_8e-6, 1.941_423_4e-6);
- const OZONE: glam::Vec3 = glam::Vec3::new(9.563_766e-7, 1.267_105_3e-6, 1.780_727e-7);
- // A sun below the horizon keeps the horizon's colour (its strength is faded elsewhere).
- let mu = sin_elevation.clamp(0.0, 1.0);
- let chapman = |x: f32, c: f32| {
- let k = (std::f32::consts::FRAC_PI_2 * x).sqrt();
- k / ((k - 1.0) * c + 1.0)
- };
- let air = [
- SCALE[0] * chapman(PLANET / SCALE[0], mu),
- SCALE[1] * chapman(PLANET / SCALE[1], mu),
- ];
- let tau = RAYLEIGH * air[0] + MIE * air[1] + OZONE * air[0];
- let t = glam::Vec3::new((-tau.x).exp(), (-tau.y).exp(), (-tau.z).exp());
- let to_709 = glam::Mat3::from_cols(
- glam::Vec3::new(1.660_344, -0.124_378_68, -0.018_112_72),
- glam::Vec3::new(-0.587_571_6, 1.132_837, -0.100_583_79),
- glam::Vec3::new(-0.072_880_24, -0.008_358_68, 1.118_621_3),
- );
- (to_709 * t).max(glam::Vec3::ZERO)
-}
-
-/// Photon's tint of the sunlight (`get_sun_tint`, light_color.glsl): pink when the sun is low, violet
-/// at the blue hour. `y`: the sun's height (the y of the unit vector towards it).
-pub fn sun_tint(y: f32) -> glam::Vec3 {
- let blue_hour =
- (((-190.0 * (y + 0.096_04) * (y + 0.096_04)).exp() - 0.05) / 0.95).clamp(0.0, 1.0);
- let pulse = {
- let t = ((y - 0.17).abs() / 0.40).min(1.0);
- 1.0 - t * t * (3.0 - 2.0 * t)
- };
- let evening = glam::Vec3::ONE.lerp(glam::Vec3::new(1.05, 0.84, 0.93) * 1.2, pulse * pulse);
- let blue = glam::Vec3::ONE.lerp(glam::Vec3::new(0.95, 0.80, 1.0), blue_hour);
- evening * blue
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn the_low_sun_is_redder_than_the_high_one() {
- let noon = sun_transmittance(1.0);
- let low = sun_transmittance(0.1);
- assert!(noon.x > 0.8 && noon.z > 0.6, "{noon:?}");
- assert!(low.z / low.x < noon.z / noon.x * 0.6, "{low:?} vs {noon:?}");
- }
-}
--
2.54.0 (Apple Git-157)