HD: coloured sun through glass and leaves, rain ripples on water, film motion blur and depth of field
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -595,6 +595,11 @@ pub struct Renderer {
|
||||
gbuffer_entity: wgpu::RenderPipeline,
|
||||
/// The HD sun shadows: depth only, from the light, and the sections in its farther cascade.
|
||||
shadow_terrain: wgpu::RenderPipeline,
|
||||
/// The near cascade's depth without leaves, and its colour map's leaves and glass
|
||||
/// (coloured shadows, `HORIZON_HD_COLOR_SHADOW`).
|
||||
shadow_terrain_near: wgpu::RenderPipeline,
|
||||
shadow_tint_leaves: wgpu::RenderPipeline,
|
||||
shadow_tint_glass: wgpu::RenderPipeline,
|
||||
shadow_entity: wgpu::RenderPipeline,
|
||||
hd_shadow_visible: Vec<resident::Visible>,
|
||||
/// The far cascade as last drawn (matrix, camera, light) and the frames since.
|
||||
@@ -1534,6 +1539,84 @@ impl Renderer {
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
// Coloured shadows: the near cascade's depth leaves the leaves out, a second pass writes
|
||||
// what leaves and glass do to the light (see `fs_shadow_tint`).
|
||||
let shadow_terrain_near = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("HD shadows terrain near"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_terrain"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &terrain_buffers,
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: shadow_depth.clone(),
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_shadow_near"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &sprite_constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &[],
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let tint_target = [Some(wgpu::ColorTargetState {
|
||||
format: hd::SHADOW_TINT,
|
||||
// Colours multiply; the alpha (distance from the light) keeps the nearest.
|
||||
blend: Some(wgpu::BlendState {
|
||||
color: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::Dst,
|
||||
dst_factor: wgpu::BlendFactor::Zero,
|
||||
operation: wgpu::BlendOperation::Add,
|
||||
},
|
||||
alpha: wgpu::BlendComponent {
|
||||
src_factor: wgpu::BlendFactor::One,
|
||||
dst_factor: wgpu::BlendFactor::One,
|
||||
operation: wgpu::BlendOperation::Min,
|
||||
},
|
||||
}),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})];
|
||||
let tint_pipeline = |label: &str, leaves: bool| {
|
||||
let 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])),
|
||||
("SHADOW_SOLID", if leaves { 1.0 } else { 0.0 }),
|
||||
];
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_terrain"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &terrain_buffers,
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_shadow_tint"),
|
||||
compilation_options: wgpu::PipelineCompilationOptions {
|
||||
constants: &constants,
|
||||
..Default::default()
|
||||
},
|
||||
targets: &tint_target,
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let shadow_tint_leaves = tint_pipeline("HD shadow colour leaves", true);
|
||||
let shadow_tint_glass = tint_pipeline("HD shadow colour glass", false);
|
||||
let solid = pipeline(
|
||||
"Terrain solid",
|
||||
"vs_terrain",
|
||||
@@ -2911,6 +2994,9 @@ impl Renderer {
|
||||
gbuffer_water,
|
||||
gbuffer_entity,
|
||||
shadow_terrain,
|
||||
shadow_terrain_near,
|
||||
shadow_tint_leaves,
|
||||
shadow_tint_glass,
|
||||
shadow_entity,
|
||||
hd_shadow_visible: Vec::new(),
|
||||
hd_far: None,
|
||||
@@ -5342,7 +5428,13 @@ impl Renderer {
|
||||
};
|
||||
let mut pass = hd.shadow_pass(&mut encoder, c);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_pipeline(&self.shadow_terrain);
|
||||
// The near cascade leaves the leaves to the colour map.
|
||||
let tinted = c == 0 && hd::color_shadows_enabled();
|
||||
pass.set_pipeline(if tinted {
|
||||
&self.shadow_terrain_near
|
||||
} else {
|
||||
&self.shadow_terrain
|
||||
});
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
let mut page = None;
|
||||
@@ -5376,6 +5468,62 @@ impl Renderer {
|
||||
pass.draw_indexed(first * 6..end * 6, 0, 0..1);
|
||||
}
|
||||
}
|
||||
drop(pass);
|
||||
if tinted {
|
||||
// What leaves and glass do to the sunlight in the near box: colours
|
||||
// multiplied, the nearest one's distance in alpha (cleared to white, far).
|
||||
let mut pass = hd.shadow_tint_pass(&mut encoder);
|
||||
pass.set_bind_group(0, &self.group, &[]);
|
||||
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
|
||||
pass.set_vertex_buffer(1, self.instances.slice(..));
|
||||
pass.set_pipeline(&self.shadow_tint_leaves);
|
||||
let mut page = None;
|
||||
for (k, v) in hd_shadow_visible.iter().enumerate() {
|
||||
let s = self.sections.get(v.slot);
|
||||
let min = Vec3::from(v.origin);
|
||||
if s.solid == 0 || !box_.intersects(min, min + Vec3::splat(16.0)) {
|
||||
continue;
|
||||
}
|
||||
if page != Some(s.page) {
|
||||
pass.set_vertex_buffer(0, self.arena.buffer(s.page).slice(..));
|
||||
page = Some(s.page);
|
||||
}
|
||||
let instance = hd_shadow_first + k as u32;
|
||||
pass.draw_indexed(
|
||||
0..s.solid * 6,
|
||||
s.start as i32,
|
||||
instance..instance + 1,
|
||||
);
|
||||
}
|
||||
pass.set_pipeline(&self.shadow_tint_glass);
|
||||
let mut page = None;
|
||||
for (k, v) in hd_shadow_visible.iter().enumerate() {
|
||||
let s = self.sections.get(v.slot);
|
||||
let min = Vec3::from(v.origin);
|
||||
let Some((index_page, index_start)) = s.sorted_indices else {
|
||||
continue;
|
||||
};
|
||||
if s.translucent == 0 || !box_.intersects(min, min + Vec3::splat(16.0))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if page != Some((s.page, index_page)) {
|
||||
pass.set_vertex_buffer(0, self.arena.buffer(s.page).slice(..));
|
||||
pass.set_index_buffer(
|
||||
self.sorted_indices.buffer(index_page).slice(..),
|
||||
wgpu::IndexFormat::Uint32,
|
||||
);
|
||||
page = Some((s.page, index_page));
|
||||
}
|
||||
let instance = hd_shadow_first + k as u32;
|
||||
let first = index_start as u32;
|
||||
pass.draw_indexed(
|
||||
first..first + s.translucent * 6,
|
||||
(s.start + u64::from(s.solid) * 4) as i32,
|
||||
instance..instance + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
encoder.copy_buffer_to_buffer(&hd.main_globals, 0, &self.globals, 0, globals_size);
|
||||
}
|
||||
@@ -5994,10 +6142,12 @@ impl Renderer {
|
||||
);
|
||||
post.run(&self.device, &mut encoder, (hd.lit_view(), depth), &hd.fx);
|
||||
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Fx, depth);
|
||||
let source = hd.film(&self.device, &mut encoder, source, depth);
|
||||
hd.finish(&self.device, &mut encoder, source, &view_target);
|
||||
}
|
||||
_ => {
|
||||
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Lit, depth);
|
||||
let source = hd.film(&self.device, &mut encoder, source, depth);
|
||||
hd.finish(&self.device, &mut encoder, source, &view_target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
//! The film camera's motion blur and depth of field. Only while film quality is on (a film is
|
||||
//! playing or exported, `set_film_quality`); normal play never runs these passes.
|
||||
//!
|
||||
//! Contract:
|
||||
//! - Runs after `taa`, before bloom: reads the colour source (display space, may exceed 1, output
|
||||
//! size) and the world depth (reversed-Z; world size, equal to the output's in film quality),
|
||||
//! plus `HdFrame`: `film` (x: motion blur on; y: depth of field on; z: focus distance in blocks,
|
||||
//! 0 = automatic; w: depth of field strength), `inverse_view_proj`, `prev_view_proj` and `taa.xyz`
|
||||
//! (the camera's move), `taa.w` (the previous frame is usable: not after a cut or a teleport).
|
||||
//! - Writes `motion` (`fs_motion`: a blur along the camera's motion on screen, rebuilt from depth;
|
||||
//! things that move by themselves are not blurred) and then `dof` (`fs_dof`: a disc gather whose
|
||||
//! radius follows the circle of confusion of each pixel's distance from the focus). Either pass
|
||||
//! is skipped when its toggle is off; `film` returns the source the next pass reads.
|
||||
//! - Deterministic: the only time the shaders read is `HdFrame.water.y`, the film clock's seconds
|
||||
//! (`film_clock.rs`), for the sample noise; nothing reads wall time or keeps a history.
|
||||
//! - Toggles: `HORIZON_HD_MOTION_BLUR=0`, `HORIZON_HD_DOF=0`; `HORIZON_HD_DOF_FOCUS=<blocks>`
|
||||
//! fixes the focus, `HORIZON_HD_DOF_STRENGTH=<number>` scales the blur (default 0.8).
|
||||
use super::{
|
||||
DEPTH, FLOAT, HDR, Hd, HdSource, SAMPLER, UNIFORM, entries, film_quality, fullscreen,
|
||||
make_group, module, stamp, target,
|
||||
};
|
||||
|
||||
pub(super) const FILM: &str = include_str!("film.wgsl");
|
||||
/// Group 0 of both passes: frame, the colour, the world depth, the linear sampler.
|
||||
pub(super) const FILM_BINDINGS: [wgpu::BindingType; 4] = [UNIFORM, FLOAT, DEPTH, SAMPLER];
|
||||
|
||||
fn flag(name: &str) -> bool {
|
||||
std::env::var(name).map_or(true, |v| v != "0")
|
||||
}
|
||||
|
||||
/// Motion blur is allowed (on, unless `HORIZON_HD_MOTION_BLUR=0`); it still needs film quality.
|
||||
pub fn motion_blur_enabled() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| flag("HORIZON_HD_MOTION_BLUR"))
|
||||
}
|
||||
|
||||
/// Depth of field is allowed (on, unless `HORIZON_HD_DOF=0`); it still needs film quality.
|
||||
pub fn dof_enabled() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| flag("HORIZON_HD_DOF"))
|
||||
}
|
||||
|
||||
/// The focus distance in blocks (0 = automatic) and the blur strength.
|
||||
pub(super) fn dof_settings() -> (f32, f32) {
|
||||
static SETTINGS: std::sync::OnceLock<(f32, f32)> = std::sync::OnceLock::new();
|
||||
*SETTINGS.get_or_init(|| {
|
||||
let read = |name: &str, default: f32| {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<f32>().ok())
|
||||
.filter(|v| v.is_finite() && *v >= 0.0)
|
||||
.unwrap_or(default)
|
||||
};
|
||||
(
|
||||
read("HORIZON_HD_DOF_FOCUS", 0.0),
|
||||
read("HORIZON_HD_DOF_STRENGTH", 0.8),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
type Key = (wgpu::TextureView, wgpu::TextureView);
|
||||
|
||||
pub(super) struct Film {
|
||||
layout: wgpu::BindGroupLayout,
|
||||
motion_pipeline: wgpu::RenderPipeline,
|
||||
dof_pipeline: wgpu::RenderPipeline,
|
||||
pub(super) motion: wgpu::TextureView,
|
||||
pub(super) dof: wgpu::TextureView,
|
||||
motion_group: Option<(Key, wgpu::BindGroup)>,
|
||||
dof_group: Option<(Key, wgpu::BindGroup)>,
|
||||
/// The target the last frame's passes ended in, for `HdSource::Film`.
|
||||
pub(super) result: wgpu::TextureView,
|
||||
/// The targets' size (0 until a film plays: normal play never pays for them).
|
||||
size: [u32; 2],
|
||||
}
|
||||
|
||||
impl Film {
|
||||
pub(super) fn new(device: &wgpu::Device) -> Self {
|
||||
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("HD film"),
|
||||
entries: &entries(&FILM_BINDINGS),
|
||||
});
|
||||
let shader = module(device, "HD film", FILM);
|
||||
let pipeline =
|
||||
|entry: &str| fullscreen(device, "HD film", &layout, &shader, entry, HDR, None);
|
||||
let motion_pipeline = pipeline("fs_motion");
|
||||
let dof_pipeline = pipeline("fs_dof");
|
||||
let motion = target(device, "HD film motion", [1, 1], HDR).view;
|
||||
Self {
|
||||
layout,
|
||||
motion_pipeline,
|
||||
dof_pipeline,
|
||||
dof: target(device, "HD film depth of field", [1, 1], HDR).view,
|
||||
result: motion.clone(),
|
||||
motion,
|
||||
motion_group: None,
|
||||
dof_group: None,
|
||||
size: [0, 0],
|
||||
}
|
||||
}
|
||||
|
||||
/// The output size changed: the targets are made again when a film next needs them.
|
||||
pub(super) fn resize(&mut self) {
|
||||
self.size = [0, 0];
|
||||
}
|
||||
|
||||
/// Output-size targets, made when a film first plays (or after a resize).
|
||||
fn ensure(&mut self, device: &wgpu::Device, out: [u32; 2]) {
|
||||
if self.size == out {
|
||||
return;
|
||||
}
|
||||
self.size = out;
|
||||
self.motion = target(device, "HD film motion", out, HDR).view;
|
||||
self.dof = target(device, "HD film depth of field", out, HDR).view;
|
||||
self.result = self.motion.clone();
|
||||
self.motion_group = None;
|
||||
self.dof_group = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl Hd {
|
||||
/// Whether the film passes run this frame, and which (motion blur, depth of field).
|
||||
pub(super) fn film_passes(&self) -> (bool, bool) {
|
||||
let on = film_quality();
|
||||
(on && motion_blur_enabled(), on && dof_enabled())
|
||||
}
|
||||
|
||||
/// Motion blur and depth of field over `source` (after TAA) with the world's `depth`; returns
|
||||
/// the source `finish` reads: the same one when film quality is off.
|
||||
pub fn film(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
source: HdSource,
|
||||
depth: &wgpu::TextureView,
|
||||
) -> HdSource {
|
||||
let (blur, dof) = self.film_passes();
|
||||
if !blur && !dof {
|
||||
return source;
|
||||
}
|
||||
self.film.ensure(device, self.out);
|
||||
let mut input = match source {
|
||||
HdSource::Lit => self.lit_view().clone(),
|
||||
HdSource::Fx => self.fx.clone(),
|
||||
HdSource::Taa => self.taa.target.clone(),
|
||||
HdSource::Film => return source,
|
||||
};
|
||||
let mut passes: Vec<(&str, bool)> = Vec::new();
|
||||
if blur {
|
||||
passes.push(("hd film motion", true));
|
||||
}
|
||||
if dof {
|
||||
passes.push(("hd film dof", false));
|
||||
}
|
||||
for (name, is_motion) in passes {
|
||||
let key = (input.clone(), depth.clone());
|
||||
let slot = if is_motion {
|
||||
&mut self.film.motion_group
|
||||
} else {
|
||||
&mut self.film.dof_group
|
||||
};
|
||||
if slot.as_ref().is_none_or(|(k, _)| *k != key) {
|
||||
let group = make_group(
|
||||
&self.groups_made,
|
||||
device,
|
||||
"HD film",
|
||||
&self.film.layout,
|
||||
&[
|
||||
wgpu::BindingResource::Buffer(self.frame.as_entire_buffer_binding()),
|
||||
wgpu::BindingResource::TextureView(&key.0),
|
||||
wgpu::BindingResource::TextureView(depth),
|
||||
wgpu::BindingResource::Sampler(&self.sampler),
|
||||
],
|
||||
);
|
||||
*slot = Some((key, group));
|
||||
}
|
||||
let group = if is_motion {
|
||||
&self.film.motion_group
|
||||
} else {
|
||||
&self.film.dof_group
|
||||
};
|
||||
let Some((_, group)) = group else {
|
||||
return source;
|
||||
};
|
||||
let (to, pipeline) = if is_motion {
|
||||
(&self.film.motion, &self.film.motion_pipeline)
|
||||
} else {
|
||||
(&self.film.dof, &self.film.dof_pipeline)
|
||||
};
|
||||
Self::draw(
|
||||
encoder,
|
||||
name,
|
||||
to,
|
||||
false,
|
||||
pipeline,
|
||||
group,
|
||||
stamp(&mut self.timing, name),
|
||||
);
|
||||
input = to.clone();
|
||||
}
|
||||
self.film.result = input;
|
||||
HdSource::Film
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// The film camera: motion blur from the camera's own move, then depth of field (see film.rs).
|
||||
@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 linear: sampler;
|
||||
|
||||
// Where the sky counts as being, in blocks.
|
||||
const FAR: f32 = 3000.0;
|
||||
// The shutter stays open for half the frame (a 180 degree shutter).
|
||||
const SHUTTER: f32 = 0.5;
|
||||
// The longest blur, and the widest circle of confusion at strength 1, as shares of the screen's height.
|
||||
const MAX_BLUR: f32 = 0.045;
|
||||
const MAX_COC: f32 = 0.007;
|
||||
const MOTION_TAPS: i32 = 14;
|
||||
const DOF_TAPS: i32 = 24;
|
||||
|
||||
// Noise for the taps: a function of the film clock's time only, so the same film frame gets the
|
||||
// same noise whenever and however fast it is rendered.
|
||||
fn film_noise(px: vec2<f32>) -> f32 {
|
||||
let step = floor(fract(frame.water.y * 1.618034) * 64.0);
|
||||
let p = px + vec2<f32>(5.588238 * step);
|
||||
return fract(52.9829189 * fract(dot(p, vec2<f32>(0.06711056, 0.00583715))));
|
||||
}
|
||||
|
||||
fn depth_load(uv: vec2<f32>) -> f32 {
|
||||
let size = vec2<i32>(textureDimensions(depth_tex));
|
||||
return textureLoad(depth_tex, clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), size - 1), 0);
|
||||
}
|
||||
|
||||
// The camera-relative point a depth value at uv stands for.
|
||||
fn world_at(uv: vec2<f32>, d: f32) -> vec3<f32> {
|
||||
let h = frame.inverse_view_proj * vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, max(d, 0.000001), 1.0);
|
||||
return h.xyz / h.w;
|
||||
}
|
||||
|
||||
// The depth along the view axis in blocks (the inverse matrix's w row gives 1 / w of the clip
|
||||
// position: one dot product, not a whole reconstruction).
|
||||
fn planar(uv: vec2<f32>, d: f32) -> f32 {
|
||||
let m = frame.inverse_view_proj;
|
||||
let w = m[0][3] * (uv.x * 2.0 - 1.0) + m[1][3] * (1.0 - uv.y * 2.0) + m[2][3] * d + m[3][3];
|
||||
return select(FAR, clamp(1.0 / w, 0.05, FAR), d > 0.0 && w > 0.0);
|
||||
}
|
||||
|
||||
fn distance_at(uv: vec2<f32>) -> f32 {
|
||||
return planar(uv, depth_load(uv));
|
||||
}
|
||||
|
||||
// Motion blur: each pixel is smeared along where the camera moved it on screen during the shutter,
|
||||
// found by projecting its point with last frame's matrix. Samples much nearer than the pixel are
|
||||
// weighed down, so a close object does not smear over the far scene behind it.
|
||||
@fragment
|
||||
fn fs_motion(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let uv = in.uv;
|
||||
let base = textureSampleLevel(source, linear, uv, 0.0);
|
||||
if frame.film.x < 0.5 || frame.taa.w < 0.5 { return vec4<f32>(base.rgb, 1.0); }
|
||||
let d = depth_load(uv);
|
||||
let sky = d <= 0.0;
|
||||
let p = world_at(uv, d);
|
||||
let prev = frame.prev_view_proj * vec4<f32>(select(p + frame.taa.xyz, p, sky), 1.0);
|
||||
if prev.w <= 0.0 { return vec4<f32>(base.rgb, 1.0); }
|
||||
let before = vec2<f32>(prev.x / prev.w * 0.5 + 0.5, 0.5 - prev.y / prev.w * 0.5);
|
||||
let size = vec2<f32>(textureDimensions(source));
|
||||
let moved = (uv - before) * size;
|
||||
let len = length(moved);
|
||||
// Under about a pixel is the jitter's noise, not motion.
|
||||
let amount = smoothstep(1.0, 2.5, len);
|
||||
if amount <= 0.0 { return vec4<f32>(base.rgb, 1.0); }
|
||||
let blur = moved * (SHUTTER * amount * min(1.0, MAX_BLUR * size.y / (len * SHUTTER)));
|
||||
let here = planar(uv, d);
|
||||
let n = film_noise(in.position.xy);
|
||||
var sum = base.rgb;
|
||||
var total = 1.0;
|
||||
for (var i = 0; i < MOTION_TAPS; i++) {
|
||||
let t = (f32(i) + n) / f32(MOTION_TAPS) - 0.5;
|
||||
let suv = uv + blur * t / size;
|
||||
let nearer = (here - distance_at(suv)) / (0.25 * here + 1.0);
|
||||
let w = 1.0 - clamp(nearer, 0.0, 1.0);
|
||||
sum += textureSampleLevel(source, linear, suv, 0.0).rgb * w;
|
||||
total += w;
|
||||
}
|
||||
return vec4<f32>(sum / total, 1.0);
|
||||
}
|
||||
|
||||
// Where the lens is focused, in blocks: the setting, or what the middle of the picture shows (the
|
||||
// harmonic mean of nine distances, the middle counting thrice, so near things win).
|
||||
fn focus_distance() -> f32 {
|
||||
if frame.film.z > 0.0 { return frame.film.z; }
|
||||
var inverse = 0.0;
|
||||
var weight = 0.0;
|
||||
for (var j = -1; j <= 1; j++) {
|
||||
for (var i = -1; i <= 1; i++) {
|
||||
let uv = vec2<f32>(0.5) + vec2<f32>(f32(i) * 0.012, f32(j) * 0.02);
|
||||
let d = depth_load(uv);
|
||||
if d <= 0.0 { continue; }
|
||||
let w = select(1.0, 3.0, i == 0 && j == 0);
|
||||
inverse += w / planar(uv, d);
|
||||
weight += w;
|
||||
}
|
||||
}
|
||||
return select(400.0, weight / inverse, weight > 0.0);
|
||||
}
|
||||
|
||||
// The signed circle of confusion in pixels (far positive, near negative): nothing within a few
|
||||
// per cent of the focus, then growing as the reciprocal distances differ.
|
||||
fn circle_of_confusion(dist: f32, focus: f32, height: f32) -> f32 {
|
||||
let a = 1.0 - focus / max(dist, 0.1);
|
||||
return sign(a) * smoothstep(0.03, 0.5, abs(a)) * frame.film.w * MAX_COC * height;
|
||||
}
|
||||
|
||||
// Depth of field: a disc gather. Each tap counts if its own circle reaches this pixel; a tap behind
|
||||
// a sharper pixel is limited to that pixel's circle, so far blur does not creep over the subject.
|
||||
@fragment
|
||||
fn fs_dof(in: FullOut) -> @location(0) vec4<f32> {
|
||||
let uv = in.uv;
|
||||
let base = textureSampleLevel(source, linear, uv, 0.0);
|
||||
if frame.film.y < 0.5 { return vec4<f32>(base.rgb, 1.0); }
|
||||
let size = vec2<f32>(textureDimensions(source));
|
||||
let focus = focus_distance();
|
||||
let here = distance_at(uv);
|
||||
let coc = circle_of_confusion(here, focus, size.y);
|
||||
let radius = MAX_COC * size.y * max(frame.film.w, 0.25);
|
||||
let angle = film_noise(in.position.xy) * 6.2831853;
|
||||
var sum = base.rgb / max(coc * coc, 1.0);
|
||||
var total = 1.0 / max(coc * coc, 1.0);
|
||||
for (var i = 0; i < DOF_TAPS; i++) {
|
||||
let r = sqrt((f32(i) + 0.5) / f32(DOF_TAPS)) * radius;
|
||||
let a = f32(i) * 2.39996323 + angle;
|
||||
let suv = uv + vec2<f32>(cos(a), sin(a)) * r / size;
|
||||
let there = distance_at(suv);
|
||||
var reach = abs(circle_of_confusion(there, focus, size.y));
|
||||
if there > here * 1.05 + 0.5 {
|
||||
reach = min(reach, max(abs(coc), 1.0));
|
||||
}
|
||||
let w = clamp(reach - r + 1.0, 0.0, 1.0) / max(reach * reach, 1.0);
|
||||
sum += textureSampleLevel(source, linear, suv, 0.0).rgb * w;
|
||||
total += w;
|
||||
}
|
||||
return vec4<f32>(sum / total, 1.0);
|
||||
}
|
||||
@@ -111,7 +111,8 @@ pub(super) struct HdFrame {
|
||||
/// w: the number of spot lights.
|
||||
pub(super) vfog: [f32; 4],
|
||||
/// x: how wet the surfaces are (0 dry .. 1 soaked; follows the rain, dries slowly); y: 1 when
|
||||
/// the specular and the wet look are on.
|
||||
/// the specular and the wet look are on; z: film quality; w: 1 when glass and leaves colour the
|
||||
/// sunlight (`shadows.rs`).
|
||||
pub(super) surface: [f32; 4],
|
||||
/// Smoke (see `smoke.rs`). x: 1 when the smoke grid is drawn; y: the grid's cell size in blocks;
|
||||
/// z: the smoke clock in seconds modulo 86400 (the noise rides the wind with it); w: 1 when the far grid has smoke (x: the near one has).
|
||||
@@ -126,6 +127,9 @@ pub(super) struct HdFrame {
|
||||
/// Up to `MAX_SPOTS` spot lights, four vec4 each (`HdSpot`: pos+range, dir+cos outer,
|
||||
/// colour+intensity, cone); their number is `vfog.w`.
|
||||
pub(super) spots: [[f32; 4]; 64],
|
||||
/// The film camera (`film.rs`). x: motion blur on; y: depth of field on; z: focus distance in
|
||||
/// blocks (0 = automatic); w: depth of field strength.
|
||||
pub(super) film: [f32; 4],
|
||||
}
|
||||
|
||||
/// What the world around looks like this frame (from the renderer's globals).
|
||||
|
||||
@@ -33,6 +33,8 @@ struct Light {
|
||||
// How far each cloud texel's ray was marched (the surface behind it, or SKY_RAY for sky).
|
||||
@group(0) @binding(20) var cloud_dist_tex: texture_2d<f32>;
|
||||
//PANO21
|
||||
// The near cascade's colour map: glass and leaves (`shadow_tint`).
|
||||
@group(0) @binding(22) var tint_map: texture_2d<f32>;
|
||||
const SKY_RAY: f32 = 20000.0;
|
||||
const CLOUD_SHADOW_R: f32 = 1200.0;
|
||||
// The pixel being lit, for the shadow filter's noise.
|
||||
@@ -140,6 +142,32 @@ fn shadow_taps(p: vec3<f32>, n: vec3<f32>, r: i32) -> f32 {
|
||||
return mix(1.0, lit / f32((2 * r + 1) * (2 * r + 1)), fade);
|
||||
}
|
||||
|
||||
// Coloured shadows: the colour the near cascade's glass and leaves give the sunlight that reaches
|
||||
// p (white where it is clear). The map (`shadows.rs`) holds, per light-space texel, the colour
|
||||
// multiplier of the quads there and the nearest one's distance from the light; a tap counts when
|
||||
// that quad is nearer the light than p. A ring of taps turned by the pixel's noise softens the
|
||||
// leaves into dappled light; the temporal pass averages it.
|
||||
fn shadow_tint(p: vec3<f32>, n: vec3<f32>) -> vec3<f32> {
|
||||
let dist = length(p);
|
||||
let reach = frame.shadow.y * 0.92;
|
||||
if frame.surface.w < 0.5 || dist >= reach { return vec3<f32>(1.0); }
|
||||
let texel = frame.shadow.y * 2.0 / 2048.0;
|
||||
let s = cascade_at(p + n * texel * 1.5, 0);
|
||||
if any(s.xy < vec2<f32>(0.0)) || any(s.xy > vec2<f32>(1.0)) || s.z > 1.0 { return vec3<f32>(1.0); }
|
||||
// The receiver's distance in the map's units (see fs_shadow_tint).
|
||||
let local = ((s.z * 799.0 + 1.0) - 320.0) / 160.0;
|
||||
let angle = ign(frag_px) * 6.2831853;
|
||||
var sum = vec3<f32>(0.0);
|
||||
for (var i = 0; i < 8; i++) {
|
||||
let o = vogel(i, 8, angle) * (0.4 / (frame.shadow.y * 2.0));
|
||||
let t = textureSampleLevel(tint_map, linear, s.xy + o, 0.0);
|
||||
// About half a block nearer the light than the receiver counts as in front of it.
|
||||
let in_front = smoothstep(0.0, 0.004, local - t.a - 0.003);
|
||||
sum += mix(vec3<f32>(1.0), t.rgb, in_front);
|
||||
}
|
||||
return mix(vec3<f32>(1.0), sum / 8.0, clamp((reach - dist) / 6.0, 0.0, 1.0));
|
||||
}
|
||||
|
||||
// One shadow sample, for the rays through the air.
|
||||
fn lit_at(p: vec3<f32>) -> f32 {
|
||||
let s = cascade_at(p, 1);
|
||||
@@ -304,7 +332,9 @@ fn shade_surface(px: vec2<i32>, p: vec3<f32>, water: bool) -> vec3<f32> {
|
||||
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;
|
||||
var sun_rgb = frame.sun_color.rgb;
|
||||
if s > 0.0 && !water { sun_rgb *= shadow_tint(p, n); }
|
||||
let direct = sun_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;
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
//! 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.
|
||||
//! shadow map; 21 the cloud panorama; 22 the near cascade's colour map (glass and leaves,
|
||||
//! `shadows.rs`).
|
||||
//! - 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
|
||||
@@ -34,7 +35,7 @@ pub const MAX_LIGHTS: usize = 512;
|
||||
|
||||
pub(super) const LIGHT: &str = include_str!("light.wgsl");
|
||||
/// The lighting's group 0, binding by binding (see the contract above).
|
||||
pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 22] = [
|
||||
pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 23] = [
|
||||
UNIFORM,
|
||||
UNFILTERED,
|
||||
DEPTH,
|
||||
@@ -57,6 +58,7 @@ pub(super) const LIGHT_BINDINGS: [wgpu::BindingType; 22] = [
|
||||
FLOAT,
|
||||
UNFILTERED,
|
||||
FLOAT,
|
||||
FLOAT,
|
||||
];
|
||||
|
||||
/// The outside views and sampler the lighting group reads.
|
||||
@@ -225,6 +227,7 @@ impl Hd {
|
||||
wgpu::BindingResource::TextureView(&self.volumes.cloud_shadow),
|
||||
wgpu::BindingResource::TextureView(self.volumes.cloud_dist_view()),
|
||||
wgpu::BindingResource::TextureView(self.volumes.pano_view()),
|
||||
wgpu::BindingResource::TextureView(&self.shadows.tint),
|
||||
];
|
||||
let entries: Vec<_> = resources
|
||||
.into_iter()
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
//! | 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) |
|
||||
//! | 6a | `film` (film quality only) | the colour source, world depth | motion blur, then depth of field (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
|
||||
@@ -61,6 +62,7 @@
|
||||
|
||||
mod ao;
|
||||
mod bloom;
|
||||
mod film;
|
||||
mod frame;
|
||||
mod gbuffer;
|
||||
mod ground;
|
||||
@@ -79,6 +81,7 @@ mod water;
|
||||
use crate::gfx::pack_timing::PassTiming;
|
||||
use ao::Ao;
|
||||
use bloom::Bloom;
|
||||
use film::Film;
|
||||
use gbuffer::GBuffer;
|
||||
use lighting::Lighting;
|
||||
use shadows::Shadows;
|
||||
@@ -93,7 +96,7 @@ use water::Water;
|
||||
pub use frame::{HdLight, HdScene, HdSettings, HdSpot, MAX_SPOTS, light_of, vanilla_cloud_tint};
|
||||
pub use gbuffer::{ALBEDO, LIGHT_LEVELS, NORMAL};
|
||||
pub use materials::{Table as MaterialTable, table as material_table, wind_enabled, wind_seconds};
|
||||
pub use shadows::{CASCADES, cascades};
|
||||
pub use shadows::{CASCADES, SHADOW_TINT, cascades, color_shadows_enabled};
|
||||
pub use smoke::{HdSmoke, smoke_enabled};
|
||||
pub use taa::{film_quality, jitter, jitter_count, scale_override, set_film_quality, taa_enabled};
|
||||
pub use volumes::clouds_enabled;
|
||||
@@ -148,6 +151,8 @@ pub enum HdSource {
|
||||
Fx,
|
||||
/// The anti-aliased result of `taa`.
|
||||
Taa,
|
||||
/// The film camera's motion blur and depth of field over that (`film`).
|
||||
Film,
|
||||
}
|
||||
|
||||
/// The timestamps of the pass `name` while HD passes are timed.
|
||||
@@ -389,6 +394,7 @@ pub struct Hd {
|
||||
/// The surfaces' wetness and when it was last updated (see `wetness`).
|
||||
wet: (f32, std::time::Instant),
|
||||
taa: Taa,
|
||||
film: Film,
|
||||
bloom: Bloom,
|
||||
tonemap: Tonemap,
|
||||
/// The volumetric clouds and fog (see `volumes`).
|
||||
@@ -457,6 +463,7 @@ impl Hd {
|
||||
wet: (0.0, std::time::Instant::now()),
|
||||
started: std::time::Instant::now(),
|
||||
taa: Taa::new(device),
|
||||
film: Film::new(device),
|
||||
bloom: Bloom::new(device),
|
||||
tonemap: Tonemap::new(device, screen),
|
||||
volumes: HdVolumes::new(device, queue),
|
||||
@@ -486,6 +493,7 @@ impl Hd {
|
||||
self.fx = target(device, "HD post", size, HDR).view;
|
||||
self.water.resize(device, size);
|
||||
self.taa.resize(device, out);
|
||||
self.film.resize();
|
||||
self.ao
|
||||
.resize(device, size, &self.frame, &self.gbuffer, &self.groups_made);
|
||||
// Every group names a view that was just replaced.
|
||||
@@ -598,7 +606,12 @@ impl Hd {
|
||||
scene.sky_fog_end,
|
||||
0.0,
|
||||
(scene.rain / 0.7).clamp(0.0, 1.0) * 0.85,
|
||||
0.0,
|
||||
// Rain ripples on the water: the rain level, 0 when off or dry.
|
||||
if scene.sky && water::ripples_enabled() {
|
||||
(scene.rain / 0.7).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
},
|
||||
],
|
||||
sunrise: scene.sunrise,
|
||||
clouds: [
|
||||
@@ -637,6 +650,16 @@ impl Hd {
|
||||
smoke_min: self.smoke.frame.min,
|
||||
smoke_far: self.smoke.frame.far,
|
||||
smoke_lights: self.smoke.frame.lights,
|
||||
film: {
|
||||
let (blur, dof) = self.film_passes();
|
||||
let (focus, strength) = film::dof_settings();
|
||||
[
|
||||
if blur { 1.0 } else { 0.0 },
|
||||
if dof { 1.0 } else { 0.0 },
|
||||
focus,
|
||||
strength,
|
||||
]
|
||||
},
|
||||
spots: {
|
||||
let mut all = [[0.0; 4]; 64];
|
||||
for (i, s) in self.spots.iter().enumerate() {
|
||||
@@ -653,7 +676,8 @@ impl Hd {
|
||||
},
|
||||
// Film quality: more steps for the clouds and the fog.
|
||||
if film_quality() { 1.0 } else { 0.0 },
|
||||
0.0,
|
||||
// Coloured shadows: the lighting reads the near cascade's colour map.
|
||||
if color_shadows_enabled() { 1.0 } else { 0.0 },
|
||||
],
|
||||
taa_params: [
|
||||
if self.upscaling() {
|
||||
@@ -836,6 +860,7 @@ mod tests {
|
||||
("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),
|
||||
("film", film::FILM.to_owned(), &film::FILM_BINDINGS),
|
||||
];
|
||||
all.extend(volumes::shader_sources());
|
||||
all.extend(smoke::shader_sources());
|
||||
|
||||
@@ -30,6 +30,7 @@ struct HdFrame {
|
||||
smoke_far: vec4<f32>,
|
||||
smoke_lights: array<vec4<f32>, 32>,
|
||||
spots: array<vec4<f32>, 64>,
|
||||
film: vec4<f32>,
|
||||
}
|
||||
struct FullOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
//! (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.
|
||||
//! - Coloured shadows (`HORIZON_HD_COLOR_SHADOW`, on by default): the near cascade's depth leaves the
|
||||
//! leaves out; `shadow_tint_pass` writes `Shadows::tint` (`SHADOW_TINT`, `TINT_SIZE` squared, the
|
||||
//! same light matrix): rgb the sunlight's multiplier after glass and leaves (cleared to 1), alpha
|
||||
//! the nearest such quad's distance from the light, `(distance - 320) / 160` (cleared to 1; the
|
||||
//! light sits 400 blocks from the cascade's middle). `fs_light` tints a surface lit by the sun
|
||||
//! where that distance is nearer to the light than the surface's own (`shadow_tint`).
|
||||
//! - 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};
|
||||
@@ -17,11 +23,22 @@ 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 near cascade's colour map: its format and size.
|
||||
pub const SHADOW_TINT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
|
||||
pub const TINT_SIZE: u32 = 1024;
|
||||
|
||||
/// Glass and leaves colour the sunlight unless `HORIZON_HD_COLOR_SHADOW=0`.
|
||||
pub fn color_shadows_enabled() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var("HORIZON_HD_COLOR_SHADOW").map_or(true, |v| v != "0"))
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
/// The near cascade's colour map (see the contract).
|
||||
pub(super) tint: wgpu::TextureView,
|
||||
pub(super) sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
@@ -62,9 +79,11 @@ impl Shadows {
|
||||
compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
..Default::default()
|
||||
});
|
||||
let tint = super::target(device, "HD shadow colour", [TINT_SIZE; 2], SHADOW_TINT).view;
|
||||
Self {
|
||||
array,
|
||||
layers,
|
||||
tint,
|
||||
sampler,
|
||||
}
|
||||
}
|
||||
@@ -96,6 +115,31 @@ impl Hd {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hd {
|
||||
/// The colour map's pass: cleared to white, far; the caller draws the leaves and the glass.
|
||||
pub fn shadow_tint_pass<'e>(
|
||||
&mut self,
|
||||
encoder: &'e mut wgpu::CommandEncoder,
|
||||
) -> wgpu::RenderPass<'e> {
|
||||
encoder
|
||||
.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("HD shadow colour"),
|
||||
timestamp_writes: stamp(&mut self.timing, "hd shadow colour"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &self.shadows.tint,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::WHITE),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
..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] {
|
||||
|
||||
@@ -68,7 +68,10 @@ pub fn set_film_quality(on: bool) {
|
||||
|
||||
/// Whether film quality is on.
|
||||
pub fn film_quality() -> bool {
|
||||
FILM.load(std::sync::atomic::Ordering::Relaxed)
|
||||
// `HORIZON_HD_FILM_QUALITY=1` forces it on, to try the film passes without a film.
|
||||
static FORCED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*FORCED.get_or_init(|| std::env::var("HORIZON_HD_FILM_QUALITY").is_ok_and(|v| v == "1"))
|
||||
|| FILM.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// How many jitter positions the sequence cycles through for an output `ratio` times the world's
|
||||
@@ -191,7 +194,7 @@ impl Hd {
|
||||
let input = match source {
|
||||
HdSource::Lit => self.lit_view().clone(),
|
||||
HdSource::Fx => self.fx.clone(),
|
||||
HdSource::Taa => return source,
|
||||
HdSource::Taa | HdSource::Film => return source,
|
||||
};
|
||||
let key = (input, depth.clone());
|
||||
if self.taa.groups.as_ref().is_none_or(|(k, _)| *k != key) {
|
||||
|
||||
@@ -63,6 +63,7 @@ impl Hd {
|
||||
HdSource::Lit => self.lit_view().clone(),
|
||||
HdSource::Fx => self.fx.clone(),
|
||||
HdSource::Taa => self.taa.target.clone(),
|
||||
HdSource::Film => self.film.result.clone(),
|
||||
};
|
||||
if self
|
||||
.bloom
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
//! 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
|
||||
//! - Time: the waves use `HdFrame.water.y`, film-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.
|
||||
@@ -53,6 +53,12 @@ pub fn water_enabled() -> bool {
|
||||
*ON.get_or_init(|| std::env::var("HORIZON_HD_WATER").map_or(true, |v| v != "0"))
|
||||
}
|
||||
|
||||
/// Rain makes ripples on the water unless `HORIZON_HD_RAIN_RIPPLES=0`.
|
||||
pub fn ripples_enabled() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var("HORIZON_HD_RAIN_RIPPLES").map_or(true, |v| v != "0"))
|
||||
}
|
||||
|
||||
pub(super) struct Water {
|
||||
tint: wgpu::TextureView,
|
||||
normal: wgpu::TextureView,
|
||||
|
||||
@@ -97,6 +97,49 @@ fn wave_gradient(xz: vec2<f32>, footprint: f32) -> vec2<f32> {
|
||||
return grad * 1.25;
|
||||
}
|
||||
|
||||
fn hash21(p: vec2<f32>) -> f32 {
|
||||
var q = fract(p * vec2<f32>(0.1031, 0.1030));
|
||||
q += dot(q, q.yx + 33.33);
|
||||
return fract((q.x + q.y) * q.x);
|
||||
}
|
||||
|
||||
// Rain ripples: a drop falls in some cells of a 1.6-block grid (160 per 256 blocks, so the pattern
|
||||
// repeats with the waves), and its ring widens and fades over about a second. The cell's drop
|
||||
// repeats every 600/n seconds with n a whole number of cycles per 600 s, so water time wrapping at
|
||||
// 600 does not pop. `rain` (0..1) sets how many cells have a drop; `footprint` fades rings the
|
||||
// pixel cannot resolve. Returns the surface's slope.
|
||||
fn ripple_gradient(xz: vec2<f32>, rain: f32, footprint: f32) -> vec2<f32> {
|
||||
let fade = clamp(1.0 - footprint * 6.0, 0.0, 1.0);
|
||||
if rain <= 0.0 || fade <= 0.0 { return vec2<f32>(0.0); }
|
||||
let cell_size = 1.6;
|
||||
let cell = floor(xz / cell_size);
|
||||
var grad = vec2<f32>(0.0);
|
||||
for (var j = -1; j <= 1; j++) {
|
||||
for (var i = -1; i <= 1; i++) {
|
||||
let c = cell + vec2<f32>(f32(i), f32(j));
|
||||
let id = c - 160.0 * floor(c / 160.0);
|
||||
let h0 = hash21(id);
|
||||
let h1 = hash21(id + vec2<f32>(37.0, 91.0));
|
||||
let h2 = hash21(id + vec2<f32>(11.0, 53.0));
|
||||
let n = 600.0 + 150.0 * floor(h1 * 3.0);
|
||||
let x = frame.water.y * (n / 600.0) + h0;
|
||||
let t = fract(x);
|
||||
let cycle = x - t - n * floor((x - t) / n);
|
||||
if hash21(id + vec2<f32>(cycle * 0.37, cycle * 0.11)) >= rain * 0.8 { continue; }
|
||||
let centre = (c + vec2<f32>(0.2 + 0.6 * h1, 0.2 + 0.6 * h2)) * cell_size;
|
||||
let to = xz - centre;
|
||||
let d = max(length(to), 0.0001);
|
||||
let width = 0.1;
|
||||
let u = (d - t * 0.9) / width;
|
||||
// d/dd of cos(2.4 u) * exp(-u^2 / 2), scaled by how young the ring is.
|
||||
let slope = -(2.4 * sin(2.4 * u) + u * cos(2.4 * u)) * exp(-0.5 * u * u) / width;
|
||||
let young = (1.0 - t) * (1.0 - t);
|
||||
grad += (to / d) * (slope * 0.012 * young);
|
||||
}
|
||||
}
|
||||
return grad * fade;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -213,7 +256,9 @@ fn fs_water(in: FullOut) -> @location(0) vec4<f32> {
|
||||
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);
|
||||
// Rain: rings on the water that is open to the sky.
|
||||
let ripples = ripple_gradient(p.xz + frame.water.zw, frame.sky_extra.w, footprint) * smoothstep(0.7, 0.95, nt.a);
|
||||
var n = normalize(geometric + vec3<f32>(-(grad.x + ripples.x), 0.0, -(grad.y + ripples.y)) * flat_part);
|
||||
n = normalize(n + v * max(0.03 - dot(n, v), 0.0));
|
||||
let ndv = max(dot(n, v), 0.02);
|
||||
|
||||
|
||||
@@ -273,6 +273,35 @@ struct WaterOut {
|
||||
let a = sprite(in.uv, in.layer & 0x7fffu).a;
|
||||
if a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
|
||||
}
|
||||
// AirDef fork, coloured shadows: the near cascade's depth map without the leaves (they are
|
||||
// filtered by `fs_shadow_tint` instead, so their shade is soft and green, not a hard cut-out).
|
||||
@fragment fn fs_shadow_near(in: TerrainOut) {
|
||||
if material_class(in.layer) == MAT_LEAVES { discard; }
|
||||
let a = sprite(in.uv, in.layer & 0x7fffu).a;
|
||||
if a < 0.5 && (in.layer & 0x8000u) == 0u { discard; }
|
||||
}
|
||||
// The colour map of the near cascade: what the sunlight is multiplied by after glass (translucent
|
||||
// quads) or a leaf (solid quads, SHADOW_SOLID), and in alpha the nearest such quad's distance from
|
||||
// the light, remapped to the cascade's middle +-80 blocks (0..1) so half floats hold it finely.
|
||||
// Blending multiplies the colours and keeps the smallest alpha. Water is left to the HD water.
|
||||
override SHADOW_SOLID: bool = false;
|
||||
@fragment fn fs_shadow_tint(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
let layer = in.layer & 0x7fffu;
|
||||
let s = sprite(in.uv, layer);
|
||||
var t: vec3<f32>;
|
||||
if SHADOW_SOLID {
|
||||
if material_class(in.layer) != MAT_LEAVES || s.a < 0.5 { discard; }
|
||||
// A leaf lets a little of its own green through.
|
||||
let c = max(s.rgb * in.tint.rgb, vec3(0.02));
|
||||
t = 0.55 * pow(c / max(max(c.r, c.g), c.b), vec3(0.8));
|
||||
} else {
|
||||
if layer == WATER_STILL || layer == WATER_FLOW || s.a < 0.05 { discard; }
|
||||
// Stained glass filters the light by its colour as much as it is opaque.
|
||||
t = pow(max(s.rgb * in.tint.rgb, vec3(0.02)), vec3(s.a * 0.9));
|
||||
}
|
||||
let dist = in.clip.z * 799.0 + 1.0;
|
||||
return vec4(t, clamp((dist - 320.0) / 160.0, 0.0, 1.0));
|
||||
}
|
||||
@fragment fn fs_translucent(in: TerrainOut) -> @location(0) vec4<f32> {
|
||||
let color = sprite(in.uv, in.layer & 0x7fffu) * in.color;
|
||||
if color.a < 0.01 { discard; }
|
||||
|
||||
Reference in New Issue
Block a user