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:
2026-10-29 17:02:02 +03:00
committed by airdef
parent 37cf7ea714
commit a321460f1c
2 changed files with 457 additions and 16 deletions
+114 -12
View File
@@ -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 = [
+343 -4
View File
@@ -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];