HD: shadows draw only sun-facing quads and skip buried sections, the far cascade every second frame; TAA (Halton jitter, reprojection by depth, YCoCg variance clipping) before bloom
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -569,6 +569,11 @@ pub struct Renderer {
|
||||
shadow_terrain: 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.
|
||||
hd_far: Option<(Mat4, glam::DVec3, Vec3, u32)>,
|
||||
/// HD's TAA: frames jittered so far, and the last one's unjittered view-projection and camera.
|
||||
taa_frame: u32,
|
||||
taa_prev: Option<(Mat4, glam::DVec3)>,
|
||||
hd: Option<hd::Hd>,
|
||||
mod_params: (Vec<f32>, f32),
|
||||
/// Time the shader pack's passes on the GPU (F3, or `HORIZON_SHADER_PACK_TIMING`).
|
||||
@@ -2815,6 +2820,9 @@ impl Renderer {
|
||||
shadow_terrain,
|
||||
shadow_entity,
|
||||
hd_shadow_visible: Vec::new(),
|
||||
hd_far: None,
|
||||
taa_frame: 0,
|
||||
taa_prev: None,
|
||||
hd: None,
|
||||
mod_params: (Vec::new(), 0.0),
|
||||
pass_timing: pack_timing::PassTiming::logging(),
|
||||
@@ -3954,7 +3962,29 @@ impl Renderer {
|
||||
frame.block_flicker,
|
||||
frame.boss_darken,
|
||||
);
|
||||
let view_proj = self.view_projection(frame);
|
||||
let hd_wanted = !frame.title
|
||||
&& frame.hd.is_some()
|
||||
&& !self.pack_post.as_ref().is_some_and(|p| p.1.is_some())
|
||||
&& !self.native.as_ref().is_some_and(|n| n.post.is_some());
|
||||
// AirDef fork: HD's temporal anti-aliasing shifts the whole projection by a sub-pixel
|
||||
// offset each frame; `unjittered` is what the next frame reprojects with.
|
||||
let unjittered = self.view_projection(frame);
|
||||
let taa = hd_wanted && hd::taa_enabled();
|
||||
let view_proj = if taa {
|
||||
let size = self
|
||||
.world_post
|
||||
.size()
|
||||
.unwrap_or([self.config.width, self.config.height]);
|
||||
let j = hd::jitter(self.taa_frame, size);
|
||||
self.taa_frame = self.taa_frame.wrapping_add(1);
|
||||
Mat4::from_translation(Vec3::new(j[0], j[1], 0.0)) * unjittered
|
||||
} else {
|
||||
unjittered
|
||||
};
|
||||
let taa_prev = std::mem::replace(
|
||||
&mut self.taa_prev,
|
||||
taa.then_some((unjittered, frame.camera)),
|
||||
);
|
||||
if frame.title {
|
||||
self.panorama.prepare(
|
||||
&self.queue,
|
||||
@@ -4158,13 +4188,12 @@ impl Renderer {
|
||||
}
|
||||
self.drawn_sections = visible.len() + visible_lods.len();
|
||||
// AirDef fork: the HD sun shadows draw every section in the farther cascade's box.
|
||||
let hd_wanted = !frame.title
|
||||
&& frame.hd.is_some()
|
||||
&& !self.pack_post.as_ref().is_some_and(|p| p.1.is_some())
|
||||
&& !self.native.as_ref().is_some_and(|n| n.post.is_some());
|
||||
let hd_scene = hd::HdScene {
|
||||
let mut hd_scene = hd::HdScene {
|
||||
cascades: [Mat4::IDENTITY; 2],
|
||||
view_proj,
|
||||
camera,
|
||||
taa: taa && taa_prev.is_some(),
|
||||
prev_view_proj: taa_prev.map_or(unjittered, |p| p.0),
|
||||
camera_delta: taa_prev.map_or(Vec3::ZERO, |p| (frame.camera - p.1).as_vec3()),
|
||||
// The world's sun: vanilla turns the sky -90° about Y, so it crosses from east to west.
|
||||
sun: Vec3::new(-sun_angle.sin(), sun_angle.cos(), 0.0),
|
||||
dark: 1.0 - frame.sky.sky_light_factor,
|
||||
@@ -4174,10 +4203,34 @@ impl Renderer {
|
||||
sky: frame.skybox && !frame.end_sky && !frame.nether_lighting && !frame.underwater,
|
||||
};
|
||||
let (shadow_dir, shadow_strength, _) = hd::light_of(&hd_scene);
|
||||
let hd_cascades = hd::cascades(camera, shadow_dir);
|
||||
let mut hd_cascades = hd::cascades(camera, shadow_dir);
|
||||
// The far cascade is redrawn every second frame (it is snapped to texels and the sun is
|
||||
// slow); between, the lighting pass gets the matrix its map was drawn with.
|
||||
let hd_far_fresh = if hd_wanted && shadow_strength > 0.0 {
|
||||
let fresh = match self.hd_far {
|
||||
Some((_, at, dir, age)) if hd_shadow_fast() => {
|
||||
age >= 1
|
||||
|| (camera - at).length_squared() > 48.0 * 48.0
|
||||
|| dir.dot(shadow_dir) < 0.99995
|
||||
|| self.hd.is_none()
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
if fresh {
|
||||
self.hd_far = Some((hd_cascades[1], camera, shadow_dir, 0));
|
||||
} else if let Some((held, _, _, age)) = &mut self.hd_far {
|
||||
hd_cascades[1] = *held;
|
||||
*age += 1;
|
||||
}
|
||||
fresh
|
||||
} else {
|
||||
self.hd_far = None;
|
||||
true
|
||||
};
|
||||
hd_scene.cascades = hd_cascades;
|
||||
let mut hd_shadow_visible = std::mem::take(&mut self.hd_shadow_visible);
|
||||
hd_shadow_visible.clear();
|
||||
if hd_wanted && shadow_strength > 0.0 {
|
||||
if hd_wanted && shadow_strength > 0.0 && hd_far_fresh {
|
||||
let radius =
|
||||
((frame.detail_distance / 16.0) as i32).min((hd::CASCADES[1] / 16.0) as i32 + 2);
|
||||
let light = Frustum {
|
||||
@@ -4191,6 +4244,11 @@ impl Renderer {
|
||||
|_, _| false,
|
||||
&mut hd_shadow_visible,
|
||||
);
|
||||
if hd_shadow_fast() {
|
||||
drop_buried(&mut hd_shadow_visible, |slot| {
|
||||
self.sections.get(slot).solid > 0
|
||||
});
|
||||
}
|
||||
}
|
||||
// Mods' cameras: their sections round them, all of them (no occlusion), after the shadow's.
|
||||
let globals_size = std::mem::size_of::<Globals>() as u64;
|
||||
@@ -5022,7 +5080,12 @@ impl Renderer {
|
||||
}
|
||||
self.queue
|
||||
.write_buffer(&hd.main_globals, 0, bytemuck::bytes_of(&globals));
|
||||
// Facing the light only: the nearest surface to it is always one that does.
|
||||
let toward = (hd_shadow_fast() && self.sides_on).then(|| shadow_dir * 1.0e5);
|
||||
for (c, cascade) in hd_cascades.iter().enumerate() {
|
||||
if c == 1 && !hd_far_fresh {
|
||||
continue;
|
||||
}
|
||||
let light = Globals {
|
||||
view_proj: cascade.to_cols_array_2d(),
|
||||
..globals
|
||||
@@ -5059,7 +5122,13 @@ impl Renderer {
|
||||
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);
|
||||
facing_runs(&s.sides, toward, v.origin, |first, quads| {
|
||||
pass.draw_indexed(
|
||||
0..quads * 6,
|
||||
s.start as i32 + first as i32 * 4,
|
||||
instance..instance + 1,
|
||||
);
|
||||
});
|
||||
}
|
||||
if !self.shadow_casters.is_empty() {
|
||||
pass.set_pipeline(&self.shadow_entity);
|
||||
@@ -5652,9 +5721,13 @@ impl Renderer {
|
||||
self.mod_params.1,
|
||||
);
|
||||
post.run(&self.device, &mut encoder, (&hd.lit, depth), &hd.fx);
|
||||
hd.finish(&self.device, &mut encoder, hd::HdSource::Fx, &view_target);
|
||||
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Fx, depth);
|
||||
hd.finish(&self.device, &mut encoder, source, &view_target);
|
||||
}
|
||||
_ => {
|
||||
let source = hd.taa(&self.device, &mut encoder, hd::HdSource::Lit, depth);
|
||||
hd.finish(&self.device, &mut encoder, source, &view_target);
|
||||
}
|
||||
_ => hd.finish(&self.device, &mut encoder, hd::HdSource::Lit, &view_target),
|
||||
}
|
||||
} else if let Some((color, depth)) = &scaled
|
||||
&& mod_post
|
||||
@@ -6055,6 +6128,35 @@ fn facing_runs(sides: &Sides, eye: Option<Vec3>, origin: [f32; 3], mut draw: imp
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
/// `HORIZON_HD_SHADOW_FAST=0` draws the HD shadow cascades the slow way (every section, both
|
||||
/// faces, the far one every frame), for comparison.
|
||||
fn hd_shadow_fast() -> bool {
|
||||
static FAST: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*FAST.get_or_init(|| !std::env::var("HORIZON_HD_SHADOW_FAST").is_ok_and(|v| v == "0"))
|
||||
}
|
||||
/// Drops the sections that sit well under the ground of their own and all eight neighbouring
|
||||
/// columns: shut in by rock, what they hold neither lights nor shades anything the sun reaches.
|
||||
fn drop_buried(list: &mut Vec<resident::Visible>, has_solid: impl Fn(u32) -> bool) {
|
||||
let mut top: std::collections::HashMap<(i32, i32), i32> = std::collections::HashMap::new();
|
||||
for v in list.iter() {
|
||||
if has_solid(v.slot) {
|
||||
let e = top.entry((v.key.0, v.key.2)).or_insert(i32::MIN);
|
||||
*e = (*e).max(v.key.1);
|
||||
}
|
||||
}
|
||||
list.retain(|v| {
|
||||
let mut lowest = i32::MAX;
|
||||
for dx in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
match top.get(&(v.key.0 + dx, v.key.2 + dz)) {
|
||||
Some(&t) => lowest = lowest.min(t),
|
||||
None => return true,
|
||||
}
|
||||
}
|
||||
}
|
||||
v.key.1 > lowest - 3
|
||||
});
|
||||
}
|
||||
fn frustum(m: Mat4) -> [Vec4; 6] {
|
||||
let r = |i| m.row(i);
|
||||
let planes = [
|
||||
|
||||
@@ -85,14 +85,26 @@ struct HdFrame {
|
||||
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).
|
||||
taa_params: [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,
|
||||
/// The camera in the world, for steady shadows.
|
||||
pub camera: glam::DVec3,
|
||||
/// Last frame's view-projection without jitter, and the camera's move since (TAA).
|
||||
pub prev_view_proj: Mat4,
|
||||
pub camera_delta: glam::Vec3,
|
||||
/// This frame is anti-aliased over time (the projection is jittered).
|
||||
pub taa: bool,
|
||||
/// Towards the sun, and the sky's darkening (0 day, 1 night).
|
||||
pub sun: glam::Vec3,
|
||||
pub dark: f32,
|
||||
@@ -119,6 +131,9 @@ struct HdFrame {
|
||||
fog: vec4<f32>,
|
||||
ambient: vec4<f32>,
|
||||
shadow: vec4<f32>,
|
||||
prev_view_proj: mat4x4<f32>,
|
||||
taa: vec4<f32>,
|
||||
taa_params: vec4<f32>,
|
||||
}
|
||||
struct FullOut {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@@ -507,7 +522,21 @@ const TONEMAP: &str = r#"
|
||||
|
||||
@fragment
|
||||
fn fs_tonemap(in: FullOut) -> @location(0) vec4<f32> {
|
||||
var c = textureSampleLevel(hdr, linear, in.uv, 0.0).rgb * frame.params.y;
|
||||
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.
|
||||
@@ -528,6 +557,130 @@ fn fs_tonemap(in: FullOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
"#;
|
||||
|
||||
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.25, lo), centre);
|
||||
let box_hi = max(min(mean + sigma * 1.25, 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;
|
||||
}
|
||||
"#;
|
||||
|
||||
struct Target {
|
||||
view: wgpu::TextureView,
|
||||
}
|
||||
@@ -570,6 +723,35 @@ type LightKey = (
|
||||
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 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
|
||||
}
|
||||
|
||||
/// Frame `frame`'s sub-pixel offset of the projection in clip space (a fraction of the
|
||||
/// screen's size in x and y), from the Halton(2, 3) sequence over 16 frames.
|
||||
pub fn jitter(frame: u32, size: [u32; 2]) -> [f32; 2] {
|
||||
let i = frame % 16 + 1;
|
||||
[
|
||||
(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 {
|
||||
@@ -589,6 +771,15 @@ pub struct Hd {
|
||||
/// The lit world, and the post pass's output.
|
||||
pub lit: wgpu::TextureView,
|
||||
pub fx: wgpu::TextureView,
|
||||
/// 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,
|
||||
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.
|
||||
@@ -902,6 +1093,44 @@ impl Hd {
|
||||
UNFILTERED,
|
||||
]),
|
||||
});
|
||||
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 = {
|
||||
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("fs_taa"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[target.clone(), target],
|
||||
}),
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let ao_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("HD ambient occlusion"),
|
||||
entries: &entries(&[UNIFORM, DEPTH, UNFILTERED]),
|
||||
@@ -1080,6 +1309,17 @@ impl Hd {
|
||||
.view,
|
||||
lit: target(device, "HD lit", [1, 1], HDR).view,
|
||||
fx: target(device, "HD post", [1, 1], HDR).view,
|
||||
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,
|
||||
taa_groups: None,
|
||||
bloom: Vec::new(),
|
||||
light_group: None,
|
||||
prefilter_group: None,
|
||||
@@ -1131,6 +1371,13 @@ 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.taa = target(device, "HD TAA", size, HDR).view;
|
||||
self.history = [
|
||||
target(device, "HD TAA history", size, HDR).view,
|
||||
target(device, "HD TAA history", size, 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;
|
||||
@@ -1206,9 +1453,22 @@ impl Hd {
|
||||
}
|
||||
// The sun by day, the moon (opposite) by night, faded out round the horizon.
|
||||
let (dir, strength, color) = light_of(scene);
|
||||
let cascades = cascades(scene.camera, dir);
|
||||
let cascades = scene.cascades;
|
||||
let night = scene.dark.clamp(0.0, 1.0);
|
||||
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 frame = HdFrame {
|
||||
prev_view_proj: scene.prev_view_proj.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 },
|
||||
],
|
||||
taa_params: [0.9, if self.taa_active { 0.12 } else { 0.0 }, 0.0, 0.0],
|
||||
inverse_view_proj: scene.view_proj.inverse().to_cols_array_2d(),
|
||||
resolution: [
|
||||
self.size[0] as f32,
|
||||
@@ -1489,6 +1749,7 @@ impl Hd {
|
||||
let source = match source {
|
||||
HdSource::Lit => self.lit.clone(),
|
||||
HdSource::Fx => self.fx.clone(),
|
||||
HdSource::Taa => self.taa.clone(),
|
||||
};
|
||||
if self
|
||||
.prefilter_group
|
||||
@@ -1572,6 +1833,70 @@ 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.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 mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("HD TAA"),
|
||||
timestamp_writes: stamp(&mut self.timing, "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(&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 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) {
|
||||
@@ -1698,6 +2023,7 @@ mod tests {
|
||||
("light", LIGHT),
|
||||
("bloom", BLOOM),
|
||||
("tonemap", TONEMAP),
|
||||
("taa", TAA),
|
||||
("ao", AO),
|
||||
("ao blur", AO_BLUR),
|
||||
] {
|
||||
@@ -1714,6 +2040,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
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 tile_lists_keep_lights_that_reach_a_tile() {
|
||||
let size = [320, 160];
|
||||
|
||||
Reference in New Issue
Block a user