HD: temporal upscaling (HORIZON_HD_SCALE: world, G-buffer and lighting at a lower size, rebuilt to the window in the TAA pass); fix flickering shadows near the player (the near cascade lost its sections on frames the far one was held, the held far matrix ignored the camera's step, plants' normals turned with the camera); TAA keeps more history when still
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3970,13 +3970,15 @@ impl Renderer {
|
||||
// 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 mut jitter_px = [0.0; 2];
|
||||
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);
|
||||
let window = [self.config.width, self.config.height];
|
||||
let size = self.world_post.size().unwrap_or(window);
|
||||
// Upscaling (the world smaller than the window) cycles through more positions.
|
||||
let ratio = window[0] as f32 / size[0].max(1) as f32;
|
||||
let j = hd::jitter(self.taa_frame, size, hd::jitter_count(ratio));
|
||||
self.taa_frame = self.taa_frame.wrapping_add(1);
|
||||
jitter_px = [j[0] * size[0] as f32 * 0.5, -j[1] * size[1] as f32 * 0.5];
|
||||
Mat4::from_translation(Vec3::new(j[0], j[1], 0.0)) * unjittered
|
||||
} else {
|
||||
unjittered
|
||||
@@ -4192,6 +4194,7 @@ impl Renderer {
|
||||
cascades: [Mat4::IDENTITY; 2],
|
||||
view_proj,
|
||||
taa: taa && taa_prev.is_some(),
|
||||
jitter_px,
|
||||
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.
|
||||
@@ -4218,8 +4221,11 @@ impl Renderer {
|
||||
};
|
||||
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;
|
||||
} else if let Some((held, at, _, age)) = &mut self.hd_far {
|
||||
// The held matrix is relative to the camera it was drawn from: shifted by the
|
||||
// camera's step since, so the map stays put in the world as the player walks.
|
||||
let step = (camera - *at).as_vec3();
|
||||
hd_cascades[1] = *held * Mat4::from_translation(step);
|
||||
*age += 1;
|
||||
}
|
||||
fresh
|
||||
@@ -4230,11 +4236,14 @@ impl Renderer {
|
||||
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 && hd_far_fresh {
|
||||
if hd_wanted && shadow_strength > 0.0 {
|
||||
// The near cascade is drawn every frame: between the far one's redraws the list holds
|
||||
// only the sections in the near box.
|
||||
let c = if hd_far_fresh { 1 } else { 0 };
|
||||
let radius =
|
||||
((frame.detail_distance / 16.0) as i32).min((hd::CASCADES[1] / 16.0) as i32 + 2);
|
||||
((frame.detail_distance / 16.0) as i32).min((hd::CASCADES[c] / 16.0) as i32 + 2);
|
||||
let light = Frustum {
|
||||
planes: frustum(hd_cascades[1]),
|
||||
planes: frustum(hd_cascades[c]),
|
||||
};
|
||||
self.sections.visible(
|
||||
camera,
|
||||
@@ -5028,6 +5037,8 @@ impl Renderer {
|
||||
.as_ref()
|
||||
.and_then(|p| p.1.as_ref())
|
||||
.is_some_and(|p| p.has_weather());
|
||||
// AirDef fork: HD renders the world at its own scale and upscales it in the TAA pass.
|
||||
self.world_post.hd_scale = hd_on.then(hd::scale_override).flatten();
|
||||
let scaled = self.world_post.prepare(
|
||||
&self.device,
|
||||
[self.config.width, self.config.height],
|
||||
@@ -5046,7 +5057,7 @@ impl Renderer {
|
||||
let hd = self
|
||||
.hd
|
||||
.get_or_insert_with(|| hd::Hd::new(device, size, format, globals_size));
|
||||
hd.resize(device, size);
|
||||
hd.resize(device, size, [self.config.width, self.config.height]);
|
||||
if let Some(settings) = frame.hd {
|
||||
hd.write(&self.queue, &hd_scene, frame.hd_lights, settings);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ struct HdFrame {
|
||||
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).
|
||||
/// x: the history's weight; y: the sharpening (0 without TAA); zw: the jitter in world pixels.
|
||||
taa_params: [f32; 4],
|
||||
}
|
||||
|
||||
@@ -105,6 +105,9 @@ pub struct HdScene {
|
||||
pub camera_delta: glam::Vec3,
|
||||
/// This frame is anti-aliased over time (the projection is jittered).
|
||||
pub taa: bool,
|
||||
/// The jitter in world-target pixels, screen orientation (x right, y down): the world's sample
|
||||
/// of a pixel lies this far from its centre (the upscaler weighs samples by it).
|
||||
pub jitter_px: [f32; 2],
|
||||
/// Towards the sun, and the sky's darkening (0 day, 1 night).
|
||||
pub sun: glam::Vec3,
|
||||
pub dark: f32,
|
||||
@@ -662,8 +665,8 @@ fn fs_taa(in: FullOut) -> TaaOut {
|
||||
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 box_lo = min(max(mean - sigma * 1.5, lo), centre);
|
||||
let box_hi = max(min(mean + sigma * 1.5, hi), centre);
|
||||
let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
|
||||
// Fast motion trusts the history less.
|
||||
let moved = length((uv - in.uv) * vec2<f32>(size));
|
||||
@@ -679,6 +682,99 @@ fn fs_taa(in: FullOut) -> TaaOut {
|
||||
out.history = vec4<f32>(color, dist);
|
||||
return out;
|
||||
}
|
||||
|
||||
// The jittered samples' reconstruction filter (a polynomial fit of Lanczos-2, radius 2 in
|
||||
// world-target pixels): 1 at 0, 0 at 1 and 2, a little negative between.
|
||||
fn lanczos2(x: f32) -> f32 {
|
||||
let x2 = min(x * x, 4.0);
|
||||
let a = 0.4 * x2 - 1.0;
|
||||
let b = 0.25 * x2 - 1.0;
|
||||
return (1.5625 * a * a - 0.5625) * b * b;
|
||||
}
|
||||
|
||||
// Temporal upscaling: the world (and its depth) is smaller than the output. Each output pixel
|
||||
// gathers the 3x3 world samples round it, each weighed by its distance from the pixel's centre
|
||||
// (the samples lie off the pixel centres by the frame's jitter), clamps the reprojected full-size
|
||||
// history to their colour box and blends: more history where the nearest sample is far away.
|
||||
@fragment
|
||||
fn fs_taau(in: FullOut) -> TaaOut {
|
||||
let low = vec2<f32>(textureDimensions(source));
|
||||
let lowi = vec2<i32>(low);
|
||||
let high = vec2<f32>(textureDimensions(history));
|
||||
let jit = frame.taa_params.zw;
|
||||
// The output pixel's place among the world's samples: sample i lies at index i - jit, and
|
||||
// the nearest to this pixel is `base`.
|
||||
let t = in.uv * low - 0.5 + jit;
|
||||
let base = vec2<i32>(floor(t + 0.5));
|
||||
var sum = vec3<f32>(0.0);
|
||||
var sum2 = vec3<f32>(0.0);
|
||||
var lo = vec3<f32>(1.0e9);
|
||||
var hi = vec3<f32>(-1.0e9);
|
||||
var weighted = vec3<f32>(0.0);
|
||||
var total = 0.0;
|
||||
for (var y = -1; y <= 1; y++) {
|
||||
for (var x = -1; x <= 1; x++) {
|
||||
let q = clamp(base + vec2<i32>(x, y), vec2<i32>(0), lowi - 1);
|
||||
let s = to_ycocg(squash(textureLoad(source, q, 0).rgb));
|
||||
sum += s;
|
||||
sum2 += s * s;
|
||||
lo = min(lo, s);
|
||||
hi = max(hi, s);
|
||||
let off = vec2<f32>(base + vec2<i32>(x, y)) - t;
|
||||
let w = lanczos2(off.x) * lanczos2(off.y);
|
||||
weighted += s * w;
|
||||
total += w;
|
||||
}
|
||||
}
|
||||
let current = weighted / max(total, 0.001);
|
||||
// Depth: the nearest of the 2x2 samples round the pixel (steady between frames at an edge).
|
||||
let corner = vec2<i32>(floor(in.uv * low - 0.5 + jit));
|
||||
var d = 0.0;
|
||||
for (var y = 0; y <= 1; y++) {
|
||||
for (var x = 0; x <= 1; x++) {
|
||||
d = max(d, textureLoad(depth_tex, clamp(corner + vec2<i32>(x, y), vec2<i32>(0), lowi - 1), 0));
|
||||
}
|
||||
}
|
||||
let sky = d <= 0.0;
|
||||
// The world was drawn shifted by the jitter in clip space; this pixel's unshifted point.
|
||||
let jc = vec2<f32>(jit.x * 2.0 / low.x, -jit.y * 2.0 / low.y);
|
||||
let ndc = vec4<f32>(in.uv.x * 2.0 - 1.0 + jc.x, 1.0 - in.uv.y * 2.0 + jc.y, max(d, 0.000001), 1.0);
|
||||
let h = frame.inverse_view_proj * ndc;
|
||||
let p = h.xyz / h.w;
|
||||
let dist = select(length(p), 0.0, sky);
|
||||
var resolved = current;
|
||||
if frame.taa.w > 0.5 {
|
||||
let prev = frame.prev_view_proj * vec4<f32>(select(p + frame.taa.xyz, p, sky), 1.0);
|
||||
if prev.w > 0.0 {
|
||||
let pn = prev.xy / prev.w;
|
||||
let uv = vec2<f32>(pn.x * 0.5 + 0.5, 0.5 - pn.y * 0.5);
|
||||
if all(uv > vec2<f32>(0.0)) && all(uv < vec2<f32>(1.0)) {
|
||||
let hp = clamp(vec2<i32>(uv * high), vec2<i32>(0), vec2<i32>(high) - 1);
|
||||
let seen = textureLoad(history, hp, 0).a;
|
||||
let expected = select(length(p + frame.taa.xyz), 0.0, sky);
|
||||
if abs(seen - expected) <= expected * 0.03 + 0.25 {
|
||||
let mean = sum / 9.0;
|
||||
let sigma = sqrt(max(sum2 / 9.0 - mean * mean, vec3<f32>(0.0)));
|
||||
let box_lo = min(max(mean - sigma * 1.25, lo), current);
|
||||
let box_hi = max(min(mean + sigma * 1.25, hi), current);
|
||||
let past = clip_to_box(box_lo, box_hi, to_ycocg(squash(history_color(uv))));
|
||||
// How far the nearest sample is from the pixel's centre (0 on it, 1 at the
|
||||
// corner of the world's pixel): the farther, the less the frame says.
|
||||
let near = clamp(length(t - floor(t + 0.5)) / 0.7071, 0.0, 1.0);
|
||||
let fresh = (1.0 - frame.taa_params.x) * mix(1.8, 0.5, near);
|
||||
let moved = length((uv - in.uv) * high);
|
||||
let keep = (1.0 - fresh) * mix(1.0, 0.8, clamp(moved / 4.0, 0.0, 1.0));
|
||||
resolved = mix(current, past, clamp(keep, 0.0, 0.98));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let color = unsquash(from_ycocg(resolved));
|
||||
var out: TaaOut;
|
||||
out.color = vec4<f32>(color, 1.0);
|
||||
out.history = vec4<f32>(color, dist);
|
||||
return out;
|
||||
}
|
||||
"#;
|
||||
|
||||
struct Target {
|
||||
@@ -744,10 +840,35 @@ fn halton(mut index: u32, base: u32) -> f32 {
|
||||
r
|
||||
}
|
||||
|
||||
/// The history's weight when upscaling (the frames each pixel gathers are sparse, so more of them
|
||||
/// are kept than without; `fs_taau` lowers it where the frame's sample is near).
|
||||
const UPSCALE_HISTORY: f32 = 0.92;
|
||||
|
||||
/// The world's size relative to the output (`HORIZON_HD_SCALE`, per axis, 0.25..=1) while HD is on, if
|
||||
/// set: it replaces the resolution scale of Horizon's settings for HD. 1 renders at full size.
|
||||
pub fn scale_override() -> Option<f32> {
|
||||
static SCALE: std::sync::OnceLock<Option<f32>> = std::sync::OnceLock::new();
|
||||
*SCALE.get_or_init(|| {
|
||||
let v: f32 = std::env::var("HORIZON_HD_SCALE").ok()?.parse().ok()?;
|
||||
v.is_finite().then(|| v.clamp(0.25, 1.0))
|
||||
})
|
||||
}
|
||||
|
||||
/// How many jitter positions the sequence cycles through for an output `ratio` times the world's
|
||||
/// size per axis: 16 without upscaling, else 8 per output pixel's share of a world pixel
|
||||
/// (ratio squared), so every output pixel gets samples all over the world pixel it lies in.
|
||||
pub fn jitter_count(ratio: f32) -> u32 {
|
||||
if ratio <= 1.001 {
|
||||
16
|
||||
} else {
|
||||
((8.0 * ratio * ratio).ceil() as u32).clamp(16, 64)
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame `frame`'s sub-pixel offset of the projection in clip space (a fraction of the
|
||||
/// screen's size in x and y), from the Halton(2, 3) sequence over 16 frames.
|
||||
pub fn jitter(frame: u32, size: [u32; 2]) -> [f32; 2] {
|
||||
let i = frame % 16 + 1;
|
||||
/// screen's size in x and y), from the Halton(2, 3) sequence over `count` frames.
|
||||
pub fn jitter(frame: u32, size: [u32; 2], count: u32) -> [f32; 2] {
|
||||
let i = frame % count.max(1) + 1;
|
||||
[
|
||||
(halton(i, 2) - 0.5) * 2.0 / size[0].max(1) as f32,
|
||||
(halton(i, 3) - 0.5) * 2.0 / size[1].max(1) as f32,
|
||||
@@ -755,7 +876,10 @@ pub fn jitter(frame: u32, size: [u32; 2]) -> [f32; 2] {
|
||||
}
|
||||
|
||||
pub struct Hd {
|
||||
/// The world's size (the G-buffer, lighting, AO, post pass), and the output's (TAA, bloom,
|
||||
/// tone curve): bigger when the world is upscaled.
|
||||
size: [u32; 2],
|
||||
out: [u32; 2],
|
||||
pub albedo: wgpu::TextureView,
|
||||
pub normal: wgpu::TextureView,
|
||||
pub levels: wgpu::TextureView,
|
||||
@@ -779,6 +903,7 @@ pub struct Hd {
|
||||
taa_active: bool,
|
||||
taa_layout: wgpu::BindGroupLayout,
|
||||
taa_pipeline: wgpu::RenderPipeline,
|
||||
taau_pipeline: wgpu::RenderPipeline,
|
||||
taa_groups: Option<((wgpu::TextureView, wgpu::TextureView), [wgpu::BindGroup; 2])>,
|
||||
bloom: Vec<Target>,
|
||||
/// Bind groups, rebuilt only when their views change: `resize` drops all of them, the
|
||||
@@ -1098,7 +1223,7 @@ impl Hd {
|
||||
entries: &entries(&[UNIFORM, UNFILTERED, DEPTH, FLOAT, SAMPLER]),
|
||||
});
|
||||
let taa_module = module(device, "HD TAA", TAA);
|
||||
let taa_pipeline = {
|
||||
let taa_pipeline = |entry: &str| {
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("HD TAA"),
|
||||
bind_group_layouts: &[Some(&taa_layout)],
|
||||
@@ -1120,7 +1245,7 @@ impl Hd {
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &taa_module,
|
||||
entry_point: Some("fs_taa"),
|
||||
entry_point: Some(entry),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[target.clone(), target],
|
||||
}),
|
||||
@@ -1131,6 +1256,7 @@ impl Hd {
|
||||
cache: None,
|
||||
})
|
||||
};
|
||||
let (taa_pipeline, taau_pipeline) = (taa_pipeline("fs_taa"), taa_pipeline("fs_taau"));
|
||||
let ao_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("HD ambient occlusion"),
|
||||
entries: &entries(&[UNIFORM, DEPTH, UNFILTERED]),
|
||||
@@ -1288,6 +1414,7 @@ impl Hd {
|
||||
};
|
||||
let mut hd = Hd {
|
||||
size: [0, 0],
|
||||
out: [0, 0],
|
||||
albedo: target(device, "HD albedo", [1, 1], ALBEDO).view,
|
||||
normal: target(device, "HD normal", [1, 1], NORMAL).view,
|
||||
levels: target(device, "HD light levels", [1, 1], LIGHT_LEVELS).view,
|
||||
@@ -1319,6 +1446,7 @@ impl Hd {
|
||||
taa_active: false,
|
||||
taa_layout,
|
||||
taa_pipeline,
|
||||
taau_pipeline,
|
||||
taa_groups: None,
|
||||
bloom: Vec::new(),
|
||||
light_group: None,
|
||||
@@ -1355,26 +1483,29 @@ impl Hd {
|
||||
groups_seen: 0,
|
||||
groups_last_frame: 0,
|
||||
};
|
||||
hd.resize(device, size);
|
||||
hd.resize(device, size, size);
|
||||
hd
|
||||
}
|
||||
|
||||
/// The targets, at the world target's size.
|
||||
pub fn resize(&mut self, device: &wgpu::Device, size: [u32; 2]) {
|
||||
if self.size == size {
|
||||
/// The targets: the world's at `size` (the world target's), the output's (TAA history, bloom)
|
||||
/// at `out`, which is bigger when the world is upscaled.
|
||||
pub fn resize(&mut self, device: &wgpu::Device, size: [u32; 2], out: [u32; 2]) {
|
||||
let out = [out[0].max(size[0]), out[1].max(size[1])];
|
||||
if self.size == size && self.out == out {
|
||||
return;
|
||||
}
|
||||
self.size = size;
|
||||
self.out = out;
|
||||
self.albedo = target(device, "HD albedo", size, ALBEDO).view;
|
||||
self.normal = target(device, "HD normal", size, NORMAL).view;
|
||||
self.levels = target(device, "HD light levels", size, LIGHT_LEVELS).view;
|
||||
self.depth = target(device, "HD depth", size, wgpu::TextureFormat::Depth32Float).view;
|
||||
self.lit = target(device, "HD lit", size, HDR).view;
|
||||
self.fx = target(device, "HD post", size, HDR).view;
|
||||
self.taa = target(device, "HD TAA", size, HDR).view;
|
||||
self.taa = target(device, "HD TAA", out, HDR).view;
|
||||
self.history = [
|
||||
target(device, "HD TAA history", size, HDR).view,
|
||||
target(device, "HD TAA history", size, HDR).view,
|
||||
target(device, "HD TAA history", out, HDR).view,
|
||||
target(device, "HD TAA history", out, HDR).view,
|
||||
];
|
||||
self.history_ready = false;
|
||||
self.taa_groups = None;
|
||||
@@ -1404,7 +1535,7 @@ impl Hd {
|
||||
self.tiles_x = size[0].div_ceil(TILE).max(1);
|
||||
self.bloom = (0..BLOOM_LEVELS)
|
||||
.map(|k| {
|
||||
let s = [(size[0] >> (k + 1)).max(1), (size[1] >> (k + 1)).max(1)];
|
||||
let s = [(out[0] >> (k + 1)).max(1), (out[1] >> (k + 1)).max(1)];
|
||||
target(device, "HD bloom", s, HDR)
|
||||
})
|
||||
.collect();
|
||||
@@ -1420,6 +1551,11 @@ impl Hd {
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// The world is smaller than the output, so TAA upsamples it.
|
||||
fn upscaling(&self) -> bool {
|
||||
self.out != self.size
|
||||
}
|
||||
|
||||
/// This frame's camera, sun, fog, lights and settings.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
@@ -1468,7 +1604,18 @@ impl Hd {
|
||||
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],
|
||||
taa_params: [
|
||||
if self.upscaling() {
|
||||
UPSCALE_HISTORY
|
||||
} else {
|
||||
// A still picture keeps 94% of the past: the jitter's shimmer on pixel-art
|
||||
// textures averages out instead of flickering.
|
||||
0.94
|
||||
},
|
||||
if self.taa_active { 0.12 } else { 0.0 },
|
||||
scene.jitter_px[0],
|
||||
scene.jitter_px[1],
|
||||
],
|
||||
inverse_view_proj: scene.view_proj.inverse().to_cols_array_2d(),
|
||||
resolution: [
|
||||
self.size[0] as f32,
|
||||
@@ -1872,9 +2019,13 @@ impl Hd {
|
||||
return source;
|
||||
};
|
||||
let (read, write) = (self.history_flip, 1 - self.history_flip);
|
||||
let upscaling = self.upscaling();
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("HD TAA"),
|
||||
timestamp_writes: stamp(&mut self.timing, "hd taa"),
|
||||
timestamp_writes: stamp(
|
||||
&mut self.timing,
|
||||
if upscaling { "hd taau" } else { "hd taa" },
|
||||
),
|
||||
color_attachments: &[&self.taa, &self.history[write]].map(|view| {
|
||||
Some(wgpu::RenderPassColorAttachment {
|
||||
view,
|
||||
@@ -1888,7 +2039,11 @@ impl Hd {
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
pass.set_pipeline(&self.taa_pipeline);
|
||||
pass.set_pipeline(if upscaling {
|
||||
&self.taau_pipeline
|
||||
} else {
|
||||
&self.taa_pipeline
|
||||
});
|
||||
pass.set_bind_group(0, &groups[read], &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
drop(pass);
|
||||
@@ -2045,7 +2200,7 @@ mod tests {
|
||||
let size = [1920u32, 1080];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for f in 0..32 {
|
||||
let j = jitter(f, size);
|
||||
let j = jitter(f, size, 16);
|
||||
assert!(j[0].abs() <= 1.0 / size[0] as f32 + 1e-9);
|
||||
assert!(j[1].abs() <= 1.0 / size[1] as f32 + 1e-9);
|
||||
seen.insert((j[0].to_bits(), j[1].to_bits()));
|
||||
@@ -2053,6 +2208,22 @@ mod tests {
|
||||
assert_eq!(seen.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jitter_covers_the_upscale_ratio() {
|
||||
assert_eq!(jitter_count(1.0), 16);
|
||||
assert_eq!(jitter_count(1.0 / 0.67), 18);
|
||||
assert_eq!(jitter_count(2.0), 32);
|
||||
let size = [1280u32, 720];
|
||||
let count = jitter_count(2.0);
|
||||
let seen: std::collections::HashSet<_> = (0..count * 2)
|
||||
.map(|f| {
|
||||
let j = jitter(f, size, count);
|
||||
(j[0].to_bits(), j[1].to_bits())
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(seen.len(), count as usize);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tile_lists_keep_lights_that_reach_a_tile() {
|
||||
let size = [320, 160];
|
||||
|
||||
@@ -18,6 +18,8 @@ pub struct Post {
|
||||
format: wgpu::TextureFormat,
|
||||
/// World resolution relative to the window (0.5..=2.0).
|
||||
pub scale: f32,
|
||||
/// HD's own scale for this frame (`HORIZON_HD_SCALE`), instead of `scale`.
|
||||
pub hd_scale: Option<f32>,
|
||||
pub antialias: bool,
|
||||
target: Option<WorldTarget>,
|
||||
}
|
||||
@@ -94,6 +96,7 @@ impl Post {
|
||||
sampler,
|
||||
format,
|
||||
scale: 1.0,
|
||||
hd_scale: None,
|
||||
antialias: false,
|
||||
target: None,
|
||||
}
|
||||
@@ -108,11 +111,12 @@ impl Post {
|
||||
window: [u32; 2],
|
||||
always: bool,
|
||||
) -> Option<(wgpu::TextureView, wgpu::TextureView)> {
|
||||
if self.scale == 1.0 && !self.antialias && !always {
|
||||
let scale = self.hd_scale.unwrap_or(self.scale);
|
||||
if scale == 1.0 && !self.antialias && !always {
|
||||
self.target = None;
|
||||
return None;
|
||||
}
|
||||
let size = window.map(|v| ((v as f32 * self.scale).round() as u32).clamp(1, 8192));
|
||||
let size = window.map(|v| ((v as f32 * scale).round() as u32).clamp(1, 8192));
|
||||
if self.target.as_ref().is_none_or(|t| t.size != size) {
|
||||
let extent = wgpu::Extent3d {
|
||||
width: size[0],
|
||||
|
||||
@@ -166,6 +166,10 @@ struct GBufferOut {
|
||||
}
|
||||
fn face_normal(pos: vec3<f32>) -> vec3<f32> {
|
||||
var n = normalize(cross(dpdx(pos), dpdy(pos)));
|
||||
// Block faces are axis-aligned; only plants' crossed quads lean. Those have two
|
||||
// sides, so a normal turned to the camera would flip as the player walks by and
|
||||
// the sun and the shadow lookup with it: plants take the up normal instead.
|
||||
if max(abs(n.x), max(abs(n.y), abs(n.z))) < 0.9 { return vec3(0.0, 1.0, 0.0); }
|
||||
if dot(n, pos) > 0.0 { n = -n; }
|
||||
return n;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user