use crate::renderer::{
denoise::{AtrousDenoiser, AtrousParams, GBuffers},
tonemap::tonemap_to_rgba_into,
};
use glam::Vec3;
pub fn denoise_and_tonemap_frame(
frame: FirstHitSnapshot<'_>,
scratch: &mut DenoiseScratch<'_>,
) -> Vec<u8> {
let mut rgba = vec![0u8; frame.width as usize * frame.height as usize * 4];
denoise_and_tonemap_frame_into(frame, scratch, &mut rgba);
rgba
}
pub fn denoise_and_tonemap_frame_into(
frame: FirstHitSnapshot<'_>,
scratch: &mut DenoiseScratch<'_>,
rgba: &mut [u8],
) {
denoise_and_tonemap_frame_with_params_into(frame, scratch, &AtrousParams::default(), rgba);
}
pub fn denoise_and_tonemap_frame_with_params_into(
frame: FirstHitSnapshot<'_>,
scratch: &mut DenoiseScratch<'_>,
params: &AtrousParams,
rgba: &mut [u8],
) {
if frame.current_sample_count == 0 {
scratch.filtered_buf.clear();
scratch
.filtered_buf
.resize(frame.width as usize * frame.height as usize, Vec3::ZERO);
tonemap_to_rgba_into(scratch.filtered_buf, 1.0, rgba);
return;
}
let inv_samples = 1.0 / frame.current_sample_count as f32;
scratch.avg_color_buf.clear();
scratch
.avg_color_buf
.extend(frame.accum_buffer.iter().map(|v| *v * inv_samples));
let gbuffers = GBuffers {
color: scratch.avg_color_buf,
depth: frame.first_hit_depth,
normal: frame.first_hit_normal,
facet_id: frame.first_hit_facet_id,
path_sig: (!frame.first_hit_path_sig.is_empty()).then_some(frame.first_hit_path_sig),
width: frame.width as usize,
height: frame.height as usize,
spp: frame.current_sample_count,
};
scratch
.denoiser
.denoise_into(&gbuffers, params, scratch.filtered_buf);
tonemap_to_rgba_into(scratch.filtered_buf, 1.0, rgba);
}
#[derive(Clone, Copy)]
pub struct FirstHitSnapshot<'a> {
pub width: u32,
pub height: u32,
pub current_sample_count: u32,
pub accum_buffer: &'a [Vec3],
pub first_hit_depth: &'a [f32],
pub first_hit_normal: &'a [Vec3],
pub first_hit_facet_id: &'a [i32],
pub first_hit_path_sig: &'a [u32],
}
pub struct DenoiseScratch<'a> {
pub denoiser: &'a mut AtrousDenoiser,
pub avg_color_buf: &'a mut Vec<Vec3>,
pub filtered_buf: &'a mut Vec<Vec3>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
geometry::{cuts::StandardGemCuts, plane::GpuFacetPlane},
optics::raytracer::{Camera, intersect_polyhedron, xyz_to_srgb_gamma},
renderer::{guide_pass::generate_guide_buffers, tonemap::tonemap_to_rgba},
};
fn fnv1a64(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325_u64, |hash, &b| {
(hash ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3)
})
}
const PIN_SIZE: (u32, u32) = (24, 16);
const PIN_SAMPLES: u32 = 3;
const PIN_POSE: (f32, f32, f32) = (0.60, 0.45, 2.4);
fn pin_accum() -> Vec<Vec3> {
(0..(PIN_SIZE.0 * PIN_SIZE.1) as usize)
.map(|i| {
Vec3::new(
((i * 37) % 101) as f32 * 0.03,
((i * 53) % 97) as f32 * 0.02,
((i * 71) % 89) as f32 * 0.025,
)
})
.collect()
}
fn pre_move_guides(
(width, height): (u32, u32),
camera: &Camera,
planes: &[GpuFacetPlane],
) -> (Vec<f32>, Vec<Vec3>, Vec<i32>) {
let mut depth = Vec::new();
let mut normal = Vec::new();
let mut facet_id = Vec::new();
for y in 0..height {
for x in 0..width {
let ray =
camera.generate_ray(x as f32, y as f32, width as f32, height as f32, 0.0, 0.0);
let hit = intersect_polyhedron(ray, planes);
depth.push(hit.map_or(1.0e6, |h| h.t));
normal.push(hit.map_or(Vec3::ZERO, |h| h.normal));
facet_id.push(hit.map_or(-1, |h| {
i32::try_from(h.facet_idx).expect("the facet index fits i32")
}));
}
}
(depth, normal, facet_id)
}
fn pre_move_denoise_and_tonemap(
(width, height): (u32, u32),
samples: u32,
accum: &[Vec3],
(depth, normal, facet_id): &(Vec<f32>, Vec<Vec3>, Vec<i32>),
) -> Vec<u8> {
let inv_samples = 1.0 / samples as f32;
let avg: Vec<Vec3> = accum.iter().map(|v| *v * inv_samples).collect();
let gbuffers = GBuffers {
color: &avg,
depth,
normal,
facet_id,
path_sig: None,
width: width as usize,
height: height as usize,
spp: samples,
};
let mut filtered = Vec::new();
AtrousDenoiser::new().denoise_into(&gbuffers, &AtrousParams::default(), &mut filtered);
tonemap_to_rgba(&filtered, 1.0)
}
#[test]
fn denoised_output_is_pinned_to_the_pre_move_gui_code() {
const PINNED: u64 = 0x4b0e_fd3c_897e_809c;
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(PIN_POSE.0, PIN_POSE.1, PIN_POSE.2, 42.0);
let accum = pin_accum();
let before = pre_move_denoise_and_tonemap(
PIN_SIZE,
PIN_SAMPLES,
&accum,
&pre_move_guides(PIN_SIZE, &camera, &planes),
);
let guides = generate_guide_buffers(PIN_SIZE.0, PIN_SIZE.1, &camera, &planes);
let mut denoiser = AtrousDenoiser::new();
let (mut avg_color_buf, mut filtered_buf) = (Vec::new(), Vec::new());
let after = denoise_and_tonemap_frame(
FirstHitSnapshot {
width: PIN_SIZE.0,
height: PIN_SIZE.1,
current_sample_count: PIN_SAMPLES,
accum_buffer: &accum,
first_hit_depth: &guides.depth,
first_hit_normal: &guides.normal,
first_hit_facet_id: &guides.facet_id,
first_hit_path_sig: &[],
},
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg_color_buf,
filtered_buf: &mut filtered_buf,
},
);
assert_eq!(after, before, "the moved pipeline differs from the GUI's");
assert_ne!(
after,
tonemap_to_rgba(&accum, 1.0 / PIN_SAMPLES as f32),
"the fixture must actually be filtered, not the taper's identity copy"
);
let hash = fnv1a64(&before);
assert_eq!(hash, PINNED, "pre-move hash: {hash:#018x}");
}
#[test]
fn the_into_form_matches_the_allocating_form() {
let planes = StandardGemCuts::standard_round_brilliant();
let camera = Camera::new(PIN_POSE.0, PIN_POSE.1, PIN_POSE.2, 42.0);
let accum = pin_accum();
let guides = generate_guide_buffers(PIN_SIZE.0, PIN_SIZE.1, &camera, &planes);
let snapshot = FirstHitSnapshot {
width: PIN_SIZE.0,
height: PIN_SIZE.1,
current_sample_count: PIN_SAMPLES,
accum_buffer: &accum,
first_hit_depth: &guides.depth,
first_hit_normal: &guides.normal,
first_hit_facet_id: &guides.facet_id,
first_hit_path_sig: &guides.path_sig,
};
let mut denoiser = AtrousDenoiser::new();
let (mut avg_color_buf, mut filtered_buf) = (Vec::new(), Vec::new());
let mut scratch = DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg_color_buf,
filtered_buf: &mut filtered_buf,
};
let allocated = denoise_and_tonemap_frame(snapshot, &mut scratch);
let mut reused = vec![0x5A_u8; (PIN_SIZE.0 * PIN_SIZE.1 * 4) as usize];
denoise_and_tonemap_frame_into(snapshot, &mut scratch, &mut reused);
assert_eq!(reused, allocated);
}
#[test]
fn denoise_and_tonemap_frame_is_identity_at_high_sample_counts() {
const HIGH_SAMPLE_COUNT: u32 = 50_000;
let width = 6u32;
let height = 5u32;
let pixel_count = (width * height) as usize;
let accum_buffer: Vec<Vec3> = (0..pixel_count)
.map(|i| {
let x = i % width as usize;
Vec3::new(
1.0 + x as f32,
0.5 * x as f32,
0.1f32.mul_add(-(x as f32), 2.0),
)
})
.collect();
let first_hit_depth: Vec<f32> = (0..pixel_count)
.map(|i| (i as f32).mul_add(0.01, 1.0))
.collect();
let first_hit_normal: Vec<Vec3> = vec![Vec3::Y; pixel_count];
let first_hit_facet_id: Vec<i32> = (0..pixel_count)
.map(|i| i32::try_from(i % width as usize).expect("the column fits i32"))
.collect();
let mut denoiser = AtrousDenoiser::new();
let mut avg_color_buf = Vec::new();
let mut filtered_buf = Vec::new();
let denoised_bytes = denoise_and_tonemap_frame(
FirstHitSnapshot {
width,
height,
current_sample_count: HIGH_SAMPLE_COUNT,
accum_buffer: &accum_buffer,
first_hit_depth: &first_hit_depth,
first_hit_normal: &first_hit_normal,
first_hit_facet_id: &first_hit_facet_id,
first_hit_path_sig: &[],
},
&mut DenoiseScratch {
denoiser: &mut denoiser,
avg_color_buf: &mut avg_color_buf,
filtered_buf: &mut filtered_buf,
},
);
let mut expected_bytes = vec![0u8; pixel_count * 4];
for (i, xyz) in accum_buffer.iter().enumerate() {
let rgba = xyz_to_srgb_gamma(*xyz / HIGH_SAMPLE_COUNT as f32);
expected_bytes[i * 4..i * 4 + 4].copy_from_slice(&rgba);
}
assert_eq!(
denoised_bytes, expected_bytes,
"at a converged (high) sample count, denoise_and_tonemap_frame's output must be bit-identical to tone-mapping the raw accumulation average with no filtering applied"
);
}
}