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>
983 lines
43 KiB
Diff
983 lines
43 KiB
Diff
From 38250c4392e27d5700842ca6ee47291dd1831b72 Mon Sep 17 00:00:00 2001
|
|
From: chexit <chexit@duck.com>
|
|
Date: Thu, 29 Oct 2026 17:47:52 +0300
|
|
Subject: [PATCH 19/40] HD water: its own pass (colour, normal, depth), the
|
|
floor under it lit, a composite with world-space waves, refraction with depth
|
|
absorption in the biome colour, screen-space reflections, Fresnel and a
|
|
shadowed sun glint (HORIZON_HD_WATER=0 to switch off)
|
|
|
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
---
|
|
crates/horizon-client/src/game.rs | 9 +
|
|
crates/horizon-client/src/gfx.rs | 109 +++++-
|
|
crates/horizon-client/src/gfx/flat.rs | 4 +
|
|
crates/horizon-client/src/gfx/hd.rs | 497 ++++++++++++++++++++++++-
|
|
crates/horizon-client/src/terrain.wgsl | 19 +
|
|
5 files changed, 627 insertions(+), 11 deletions(-)
|
|
|
|
diff --git a/crates/horizon-client/src/game.rs b/crates/horizon-client/src/game.rs
|
|
index 3a11ee283..5a173977f 100644
|
|
--- a/crates/horizon-client/src/game.rs
|
|
+++ b/crates/horizon-client/src/game.rs
|
|
@@ -14658,6 +14658,14 @@ fn build_renderer(
|
|
prepared,
|
|
previous,
|
|
api,
|
|
+ // AirDef fork, HD water: the layers of the water sprites.
|
|
+ ["water_still", "water_flow"].map(|name| {
|
|
+ assets
|
|
+ .sprite_layers
|
|
+ .get(&format!("minecraft:block/{name}"))
|
|
+ .copied()
|
|
+ .unwrap_or(0xffff)
|
|
+ }),
|
|
))?;
|
|
renderer.hud.sprite_animations = atlas
|
|
.sprite_animations
|
|
@@ -14717,6 +14725,7 @@ fn loading_renderer(
|
|
None,
|
|
previous,
|
|
api,
|
|
+ [0xffff; 2],
|
|
))?;
|
|
if let Some(atlas) = atlas {
|
|
renderer.hud.sprite_animations = atlas
|
|
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
|
|
index b85c6e269..f9dab8abf 100644
|
|
--- a/crates/horizon-client/src/gfx.rs
|
|
+++ b/crates/horizon-client/src/gfx.rs
|
|
@@ -564,6 +564,8 @@ pub struct Renderer {
|
|
mod_views: Option<mod_views::Views>,
|
|
/// AirDef fork: the HD lighting's G-buffer pipelines (terrain, entities) and its targets.
|
|
gbuffer_terrain: wgpu::RenderPipeline,
|
|
+ /// The water quads alone into the HD water targets (section draws; `flat.water` for flat ones).
|
|
+ gbuffer_water: wgpu::RenderPipeline,
|
|
gbuffer_entity: wgpu::RenderPipeline,
|
|
/// The HD sun shadows: depth only, from the light, and the sections in its farther cascade.
|
|
shadow_terrain: wgpu::RenderPipeline,
|
|
@@ -1083,6 +1085,7 @@ impl Renderer {
|
|
prepared: Option<PreparedTextures>,
|
|
previous: Option<&Self>,
|
|
api: GraphicsApi,
|
|
+ water_layers: [u32; 2],
|
|
) -> Result<Self> {
|
|
let split = sprite_split();
|
|
if layers > split * SPRITE_ARRAYS {
|
|
@@ -1205,6 +1208,13 @@ impl Renderer {
|
|
("SPRITE_ARRAYS", f64::from(sprite_arrays)),
|
|
("SPRITE_SPLIT", f64::from(split)),
|
|
];
|
|
+ // AirDef fork, HD water: the sprite layers terrain.wgsl's fs_water takes for water.
|
|
+ let water_constants = [
|
|
+ ("SPRITE_ARRAYS", f64::from(sprite_arrays)),
|
|
+ ("SPRITE_SPLIT", f64::from(split)),
|
|
+ ("WATER_STILL", f64::from(water_layers[0])),
|
|
+ ("WATER_FLOW", f64::from(water_layers[1])),
|
|
+ ];
|
|
let bindings = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: Some("Horizon terrain bindings"),
|
|
entries: &[
|
|
@@ -1420,6 +1430,55 @@ impl Renderer {
|
|
multiview_mask: None,
|
|
cache: None,
|
|
});
|
|
+ // AirDef fork, HD water: only the water quads (`fs_water`), into the colour and normal
|
|
+ // targets and the water's own depth; `vertex` is the section or the flat vertex stage.
|
|
+ let water_targets = [
|
|
+ Some(wgpu::ColorTargetState {
|
|
+ format: hd::WATER_TINT,
|
|
+ blend: None,
|
|
+ write_mask: wgpu::ColorWrites::ALL,
|
|
+ }),
|
|
+ Some(wgpu::ColorTargetState {
|
|
+ format: hd::WATER_NORMAL,
|
|
+ blend: None,
|
|
+ write_mask: wgpu::ColorWrites::ALL,
|
|
+ }),
|
|
+ ];
|
|
+ let water_pipeline =
|
|
+ |layout: &wgpu::PipelineLayout,
|
|
+ label: &str,
|
|
+ vertex: &str,
|
|
+ buffers: &[Option<wgpu::VertexBufferLayout<'_>>]| {
|
|
+ device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
+ label: Some(label),
|
|
+ layout: Some(layout),
|
|
+ vertex: wgpu::VertexState {
|
|
+ module: &shader,
|
|
+ entry_point: Some(vertex),
|
|
+ compilation_options: Default::default(),
|
|
+ buffers,
|
|
+ },
|
|
+ primitive: wgpu::PrimitiveState {
|
|
+ cull_mode: Some(wgpu::Face::Back),
|
|
+ ..Default::default()
|
|
+ },
|
|
+ depth_stencil: gbuffer_depth.clone(),
|
|
+ multisample: Default::default(),
|
|
+ fragment: Some(wgpu::FragmentState {
|
|
+ module: &shader,
|
|
+ entry_point: Some("fs_water"),
|
|
+ compilation_options: wgpu::PipelineCompilationOptions {
|
|
+ constants: &water_constants,
|
|
+ ..Default::default()
|
|
+ },
|
|
+ targets: &water_targets,
|
|
+ }),
|
|
+ multiview_mask: None,
|
|
+ cache: None,
|
|
+ })
|
|
+ };
|
|
+ let gbuffer_water =
|
|
+ water_pipeline(&layout, "HD water terrain", "vs_terrain", &terrain_buffers);
|
|
let shadow_terrain = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
label: Some("HD shadows terrain"),
|
|
layout: Some(&layout),
|
|
@@ -2597,6 +2656,7 @@ impl Renderer {
|
|
&device,
|
|
&bindings,
|
|
arena.page_vertices(),
|
|
+ |layout, label| water_pipeline(layout, label, "vs_terrain_flat", &buffers),
|
|
|layout, label, fragment, blend| {
|
|
pipeline_in(
|
|
layout,
|
|
@@ -2816,6 +2876,7 @@ impl Renderer {
|
|
mod_post_hdr: false,
|
|
mod_views: None,
|
|
gbuffer_terrain,
|
|
+ gbuffer_water,
|
|
gbuffer_entity,
|
|
shadow_terrain,
|
|
shadow_entity,
|
|
@@ -4204,6 +4265,16 @@ impl Renderer {
|
|
fog_color,
|
|
fog,
|
|
sky: frame.skybox && !frame.end_sky && !frame.nether_lighting && !frame.underwater,
|
|
+ sky_color: [
|
|
+ frame.sky.sky_color[0],
|
|
+ frame.sky.sky_color[1],
|
|
+ frame.sky.sky_color[2],
|
|
+ ],
|
|
+ camera_mod: [
|
|
+ camera.x.rem_euclid(256.0) as f32,
|
|
+ camera.z.rem_euclid(256.0) as f32,
|
|
+ ],
|
|
+ water: !frame.underwater,
|
|
};
|
|
let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene);
|
|
let mut hd_cascades = hd::cascades(camera, shadow_dir);
|
|
@@ -5058,6 +5129,11 @@ impl Renderer {
|
|
.hd
|
|
.get_or_insert_with(|| hd::Hd::new(device, size, format, globals_size));
|
|
hd.resize(device, size, [self.config.width, self.config.height]);
|
|
+ // Translucent terrain on screen: the water pass has something to draw.
|
|
+ hd.water_geometry = match self.flat.as_ref().filter(|_| flat_frame) {
|
|
+ Some(flat) => !flat.translucent_calls().is_empty(),
|
|
+ None => translucent_draws > 0,
|
|
+ };
|
|
if let Some(settings) = frame.hd {
|
|
hd.write(&self.queue, &hd_scene, frame.hd_lights, settings);
|
|
}
|
|
@@ -5186,6 +5262,32 @@ impl Renderer {
|
|
drop(pass);
|
|
// Contact shadows from the G-buffer's depth and normals, for the lighting.
|
|
hd.ao(&mut encoder);
|
|
+ // The water quads alone, with their own depth: the lighting finds the water by it and
|
|
+ // the composite after the lighting lays the surface over the lit floor.
|
|
+ if hd.water_active() {
|
|
+ let mut pass = hd.water_pass(&mut encoder);
|
|
+ pass.set_bind_group(0, &self.group, &[]);
|
|
+ match self.flat.as_ref().filter(|_| flat_frame) {
|
|
+ Some(flat) => {
|
|
+ pass.set_pipeline(&flat.water);
|
|
+ flat.draw(&mut pass, flat.translucent_calls(), |p| {
|
|
+ self.arena.buffer(p)
|
|
+ });
|
|
+ }
|
|
+ None => {
|
|
+ pass.set_pipeline(&self.gbuffer_water);
|
|
+ pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
|
+ pass.set_vertex_buffer(1, self.instances.slice(..));
|
|
+ self.draw_sections(
|
|
+ &mut pass,
|
|
+ &self.indirect,
|
|
+ 0,
|
|
+ solid_draws,
|
|
+ translucent_draws,
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ }
|
|
} else if !hd_on {
|
|
self.hd = None;
|
|
}
|
|
@@ -5721,6 +5823,11 @@ impl Renderer {
|
|
(color, depth),
|
|
(&self.lightmap_view, &self.lightmap_sampler),
|
|
);
|
|
+ hd.water_composite(
|
|
+ &self.device,
|
|
+ &mut encoder,
|
|
+ (&self.lightmap_view, &self.lightmap_sampler),
|
|
+ );
|
|
match &mut self.mod_post {
|
|
Some(post) if mod_post => {
|
|
let size = self.world_post.size().unwrap_or([1, 1]);
|
|
@@ -5731,7 +5838,7 @@ impl Renderer {
|
|
&self.mod_params.0,
|
|
self.mod_params.1,
|
|
);
|
|
- post.run(&self.device, &mut encoder, (&hd.lit, depth), &hd.fx);
|
|
+ post.run(&self.device, &mut encoder, (hd.lit_view(), depth), &hd.fx);
|
|
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Fx, depth);
|
|
hd.finish(&self.device, &mut encoder, source, &view_target);
|
|
}
|
|
diff --git a/crates/horizon-client/src/gfx/flat.rs b/crates/horizon-client/src/gfx/flat.rs
|
|
index 5d86888da..7e2e20cad 100644
|
|
--- a/crates/horizon-client/src/gfx/flat.rs
|
|
+++ b/crates/horizon-client/src/gfx/flat.rs
|
|
@@ -64,6 +64,8 @@ pub struct Call {
|
|
pub struct Flat {
|
|
pub solid: wgpu::RenderPipeline,
|
|
pub translucent: wgpu::RenderPipeline,
|
|
+ /// The water quads alone, into HD's water targets.
|
|
+ pub water: wgpu::RenderPipeline,
|
|
flatten: wgpu::ComputePipeline,
|
|
compute_layout: wgpu::BindGroupLayout,
|
|
flatten_seen: wgpu::ComputePipeline,
|
|
@@ -254,6 +256,7 @@ impl Flat {
|
|
device: &wgpu::Device,
|
|
terrain_layout: &wgpu::BindGroupLayout,
|
|
page_vertices: u64,
|
|
+ water: impl Fn(&wgpu::PipelineLayout, &str) -> wgpu::RenderPipeline,
|
|
pipeline: impl Fn(
|
|
&wgpu::PipelineLayout,
|
|
&str,
|
|
@@ -320,6 +323,7 @@ impl Flat {
|
|
"fs_translucent",
|
|
Some(wgpu::BlendState::ALPHA_BLENDING),
|
|
),
|
|
+ water: water(&layout, "HD water terrain (flat)"),
|
|
flatten,
|
|
compute_layout,
|
|
flatten_seen,
|
|
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
|
|
index c54b7b961..f5caa2d0b 100644
|
|
--- a/crates/horizon-client/src/gfx/hd.rs
|
|
+++ b/crates/horizon-client/src/gfx/hd.rs
|
|
@@ -16,6 +16,9 @@ pub const NORMAL: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
|
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.
|
|
@@ -91,6 +94,13 @@ struct HdFrame {
|
|
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],
|
|
}
|
|
|
|
/// What the world around looks like this frame (from the renderer's globals).
|
|
@@ -116,6 +126,11 @@ pub struct HdScene {
|
|
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,
|
|
}
|
|
|
|
/// The cascades' half sizes (blocks) and shadow map size.
|
|
@@ -137,6 +152,9 @@ struct HdFrame {
|
|
prev_view_proj: mat4x4<f32>,
|
|
taa: vec4<f32>,
|
|
taa_params: vec4<f32>,
|
|
+ view_proj: mat4x4<f32>,
|
|
+ water: vec4<f32>,
|
|
+ sky_color: vec4<f32>,
|
|
}
|
|
struct FullOut {
|
|
@builtin(position) position: vec4<f32>,
|
|
@@ -175,6 +193,8 @@ struct 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;
|
|
|
|
// 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.
|
|
@@ -216,6 +236,12 @@ fn cascade_at(p: vec3<f32>, c: i32) -> vec3<f32> {
|
|
|
|
// 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);
|
|
+}
|
|
+
|
|
+// `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;
|
|
@@ -224,13 +250,13 @@ fn shadow(p: vec3<f32>, n: vec3<f32>) -> f32 {
|
|
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++) {
|
|
+ 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);
|
|
}
|
|
}
|
|
let fade = clamp((frame.shadow.z - dist) / 16.0, 0.0, 1.0);
|
|
- return mix(1.0, lit / 9.0, fade);
|
|
+ return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
|
|
}
|
|
|
|
// One shadow sample, for the rays through the air.
|
|
@@ -252,7 +278,15 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
let scene = textureLoad(scene_color, px, 0).rgb;
|
|
let d = textureLoad(scene_depth, px, 0);
|
|
let g = textureLoad(gbuffer_depth, px, 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);
|
|
+ // Water in front of the opaque world with nothing translucent over it: here the floor is lit
|
|
+ // and the water composite lays the surface over it (it knows the pixel by the alpha 0.5).
|
|
+ var water = false;
|
|
+ if frame.water.x > 0.5 && g > 0.0 {
|
|
+ let wd = textureLoad(water_depth, px, 0);
|
|
+ water = wd > 0.0 && wd >= g && d - wd <= d * 0.002;
|
|
+ }
|
|
+ let dd = select(d, g, water);
|
|
+ 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);
|
|
@@ -267,7 +301,7 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
let halo_all = tiles[tile + 3u] == 0xffffffffu;
|
|
let halo_count = select(tiles[tile + 3u], count, halo_all);
|
|
var c = scene;
|
|
- let surface = d > 0.0 && abs(d - g) <= max(d, g) * 0.002;
|
|
+ let surface = water || (d > 0.0 && abs(d - g) <= max(d, g) * 0.002);
|
|
if surface {
|
|
let n = normalize(textureLoad(normal_tex, px, 0).xyz);
|
|
let albedo = textureLoad(albedo_tex, px, 0);
|
|
@@ -278,14 +312,15 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
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(p, n); } else { s = 0.0; }
|
|
+ if sun_on > 0.0 && ndl > 0.0 && sky_reach > 0.0 { s = shadow_taps(p, n, select(1, 0, water)); } 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;
|
|
- if frame.ambient.w > 0.0 { occlusion = ao_at(px, length(p)); }
|
|
+ // (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);
|
|
var light = max(base * frame.ambient.x, block * frame.ambient.y) * ao_ambient + direct;
|
|
@@ -334,7 +369,253 @@ fn fs_light(in: FullOut) -> @location(0) vec4<f32> {
|
|
let a1 = atan((far - t0) / hh);
|
|
c += l.color.rgb * l.color.w * (a1 - a0) / hh * 0.004 * (0.3 + density * 300.0);
|
|
}
|
|
- return vec4<f32>(c, 1.0);
|
|
+ return vec4<f32>(c, select(1.0, 0.5, water));
|
|
+}
|
|
+"#;
|
|
+
|
|
+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;
|
|
+
|
|
+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>, 10>(
|
|
+ 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),
|
|
+);
|
|
+
|
|
+// 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 < 10; 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.25 - 0.5, 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));
|
|
+ }
|
|
+ return grad;
|
|
+}
|
|
+
|
|
+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_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.05;
|
|
+ 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); }
|
|
+ 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);
|
|
+ let 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;
|
|
+ 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).
|
|
+ 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);
|
|
+ var uv_floor = in.uv;
|
|
+ var floor_z = g;
|
|
+ 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);
|
|
+ // Only what lies behind the water: not the bank or an entity in front of it.
|
|
+ if z > 0.0 && z <= wd {
|
|
+ uv_floor = moved.xy;
|
|
+ floor_z = z;
|
|
+ }
|
|
+ }
|
|
+ let thickness = clamp(length(position_at(uv_floor, floor_z)) - dist, 0.0, 48.0);
|
|
+ let behind = textureSampleLevel(lit_tex, linear, uv_floor, 0.0).rgb;
|
|
+ let colour = clamp(tint.rgb, vec3<f32>(0.03), vec3<f32>(1.0));
|
|
+ // The colours the water takes up fastest are the ones it has the least of.
|
|
+ let absorb = exp(-(vec3<f32>(1.0) - colour) * 0.8 * (thickness + 0.3));
|
|
+ let refracted = behind * absorb + colour * volume_light * 0.9 * (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));
|
|
+ c = mix(c, frame.fog_color.rgb, f * frame.fog_color.a);
|
|
+ return vec4<f32>(max(c, vec3<f32>(0.0)), 1.0);
|
|
}
|
|
"#;
|
|
|
|
@@ -829,6 +1110,12 @@ pub fn taa_enabled() -> bool {
|
|
*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);
|
|
@@ -895,6 +1182,22 @@ pub struct Hd {
|
|
/// 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,
|
|
+ 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,
|
|
+ started: std::time::Instant,
|
|
/// TAA: the resolved frame, and the two histories (one read, one written, alternating).
|
|
taa: wgpu::TextureView,
|
|
history: [wgpu::TextureView; 2],
|
|
@@ -1216,6 +1519,26 @@ impl Hd {
|
|
min_binding_size: None,
|
|
},
|
|
UNFILTERED,
|
|
+ DEPTH,
|
|
+ ]),
|
|
+ });
|
|
+ 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,
|
|
]),
|
|
});
|
|
let taa_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
@@ -1278,6 +1601,16 @@ impl Hd {
|
|
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 water_pipeline = fullscreen(
|
|
+ device,
|
|
+ "HD water",
|
|
+ &water_layout,
|
|
+ &water,
|
|
+ "fs_water",
|
|
+ HDR,
|
|
+ None,
|
|
+ );
|
|
let ao_pipeline = fullscreen(
|
|
device,
|
|
"HD ambient occlusion",
|
|
@@ -1436,6 +1769,23 @@ impl Hd {
|
|
.view,
|
|
lit: target(device, "HD lit", [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,
|
|
+ started: std::time::Instant::now(),
|
|
taa: target(device, "HD TAA", [1, 1], HDR).view,
|
|
history: [
|
|
target(device, "HD TAA history", [1, 1], HDR).view,
|
|
@@ -1502,6 +1852,17 @@ impl Hd {
|
|
self.depth = target(device, "HD depth", size, wgpu::TextureFormat::Depth32Float).view;
|
|
self.lit = target(device, "HD lit", size, HDR).view;
|
|
self.fx = target(device, "HD post", size, HDR).view;
|
|
+ self.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,
|
|
@@ -1591,6 +1952,7 @@ impl Hd {
|
|
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 {
|
|
@@ -1604,6 +1966,19 @@ impl Hd {
|
|
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 },
|
|
+ ],
|
|
taa_params: [
|
|
if self.upscaling() {
|
|
UPSCALE_HISTORY
|
|
@@ -1821,6 +2196,10 @@ impl Hd {
|
|
binding: 13,
|
|
resource: wgpu::BindingResource::TextureView(&self.ao_blurred),
|
|
},
|
|
+ wgpu::BindGroupEntry {
|
|
+ binding: 14,
|
|
+ resource: wgpu::BindingResource::TextureView(&self.water_depth),
|
|
+ },
|
|
],
|
|
});
|
|
self.light_group = Some((key, group));
|
|
@@ -1828,6 +2207,7 @@ impl Hd {
|
|
let Some((_, group)) = &self.light_group else {
|
|
return;
|
|
};
|
|
+ self.water_composited = false;
|
|
Self::draw(
|
|
encoder,
|
|
"HD lighting",
|
|
@@ -1894,7 +2274,7 @@ impl Hd {
|
|
screen: &wgpu::TextureView,
|
|
) {
|
|
let source = match source {
|
|
- HdSource::Lit => self.lit.clone(),
|
|
+ HdSource::Lit => self.lit_view().clone(),
|
|
HdSource::Fx => self.fx.clone(),
|
|
HdSource::Taa => self.taa.clone(),
|
|
};
|
|
@@ -1993,7 +2373,7 @@ impl Hd {
|
|
return source;
|
|
}
|
|
let input = match source {
|
|
- HdSource::Lit => self.lit.clone(),
|
|
+ HdSource::Lit => self.lit_view().clone(),
|
|
HdSource::Fx => self.fx.clone(),
|
|
HdSource::Taa => return source,
|
|
};
|
|
@@ -2052,6 +2432,102 @@ impl Hd {
|
|
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),
|
|
+ ],
|
|
+ );
|
|
+ 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) {
|
|
@@ -2181,6 +2657,7 @@ mod tests {
|
|
("taa", TAA),
|
|
("ao", AO),
|
|
("ao blur", AO_BLUR),
|
|
+ ("water", WATER),
|
|
] {
|
|
let source = format!("{COMMON}{body}");
|
|
let module = naga::front::wgsl::parse_str(&source).unwrap_or_else(|e| {
|
|
diff --git a/crates/horizon-client/src/terrain.wgsl b/crates/horizon-client/src/terrain.wgsl
|
|
index c44449cc7..0d5d2fc89 100644
|
|
--- a/crates/horizon-client/src/terrain.wgsl
|
|
+++ b/crates/horizon-client/src/terrain.wgsl
|
|
@@ -182,6 +182,25 @@ fn face_normal(pos: vec3<f32>) -> vec3<f32> {
|
|
out.light = vec4(clamp(in.lm, vec2(0.0), vec2(1.0)), 0.0, 1.0);
|
|
return out;
|
|
}
|
|
+// AirDef fork, HD water: the water quads once more, after the G-buffer, into small targets of
|
|
+// their own: the surface colour (the water sprite times the biome tint) with the block light, and
|
|
+// the face normal with the sky light. The layers are the water sprites' (gfx.rs fills them in;
|
|
+// nothing else is water: glass, ice and the rest stay as vanilla draws them).
|
|
+override WATER_STILL: u32 = 0xffffu;
|
|
+override WATER_FLOW: u32 = 0xffffu;
|
|
+struct WaterOut {
|
|
+ @location(0) tint: vec4<f32>,
|
|
+ @location(1) normal: vec4<f32>,
|
|
+}
|
|
+@fragment fn fs_water(in: TerrainOut) -> WaterOut {
|
|
+ let layer = in.layer & 0x7fffu;
|
|
+ if layer != WATER_STILL && layer != WATER_FLOW { discard; }
|
|
+ let color = sprite(in.uv, layer) * in.tint;
|
|
+ var out: WaterOut;
|
|
+ out.tint = vec4(color.rgb, clamp(in.lm.y, 0.0, 1.0));
|
|
+ out.normal = vec4(face_normal(in.pos) * 0.5 + 0.5, clamp(in.lm.x, 0.0, 1.0));
|
|
+ return out;
|
|
+}
|
|
// AirDef fork: the sun's shadow map — depth only, cut-out leaves leave holes.
|
|
@fragment fn fs_shadow(in: TerrainOut) {
|
|
let a = sprite(in.uv, in.layer & 0x7fffu).a;
|
|
--
|
|
2.54.0 (Apple Git-157)
|
|
|