ccc3e20902
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>
9342 lines
394 KiB
Diff
9342 lines
394 KiB
Diff
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)
|
||
|