use glam::Vec3;
#[derive(Clone, Copy)]
pub struct GBuffers<'a> {
pub color: &'a [Vec3],
pub depth: &'a [f32],
pub normal: &'a [Vec3],
pub facet_id: &'a [i32],
pub width: usize,
pub height: usize,
pub spp: u32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AtrousParams {
pub num_passes: u32,
pub sigma_color: f32,
pub sigma_depth: f32,
pub normal_power: f32,
pub taper_reference_spp: f32,
pub taper_identity_epsilon: f32,
}
impl Default for AtrousParams {
fn default() -> Self {
Self {
num_passes: 5,
sigma_color: 0.35,
sigma_depth: 0.1,
normal_power: 64.0,
taper_reference_spp: 4.0,
taper_identity_epsilon: 0.02,
}
}
}
const B3_SPLINE: [f32; 5] = [1.0 / 16.0, 1.0 / 4.0, 3.0 / 8.0, 1.0 / 4.0, 1.0 / 16.0];
const SIGMA_EPS: f32 = 1.0e-8;
#[inline]
const fn sanitize(v: f32) -> f32 {
if v.is_finite() { v } else { 0.0 }
}
#[inline]
const fn sanitize_vec3(v: Vec3) -> Vec3 {
Vec3::new(sanitize(v.x), sanitize(v.y), sanitize(v.z))
}
#[must_use]
pub fn taper_strength(spp: u32, taper_reference_spp: f32) -> f32 {
let n0 = taper_reference_spp.max(SIGMA_EPS);
1.0 / (1.0 + spp as f32 / n0).sqrt()
}
#[inline]
const fn facet_weight(a: i32, b: i32) -> f32 {
if a == b { 1.0 } else { 0.0 }
}
#[inline]
fn unit_normal_or_zero(n: Vec3) -> Vec3 {
let n = sanitize_vec3(n);
let len = n.length();
if len <= SIGMA_EPS {
Vec3::ZERO
} else {
n / len
}
}
#[inline]
fn cos_pow(base: f32, power: f32) -> f32 {
if power >= 0.0 && power.fract() == 0.0 && power <= u32::MAX as f32 {
let int_power = power as u32;
if int_power.is_power_of_two() {
let squarings = int_power.trailing_zeros();
let mut v = base;
for _ in 0..squarings {
v *= v;
}
return v;
}
}
base.powf(power)
}
#[inline]
fn normal_weight_unit(a_n: Vec3, b_n: Vec3, power: f32) -> f32 {
if a_n == Vec3::ZERO || b_n == Vec3::ZERO {
return 1.0;
}
let cos_theta = a_n.dot(b_n).clamp(-1.0, 1.0).max(0.0);
cos_pow(cos_theta, power.max(0.0))
}
#[inline]
fn depth_weight(a: f32, b: f32, neg_inv_sigma_depth: f32) -> f32 {
let a = sanitize(a);
let b = sanitize(b);
((a - b).abs() * neg_inv_sigma_depth).exp()
}
#[inline]
fn color_weight(a: Vec3, b: Vec3, neg_inv_two_sigma_sq: f32) -> f32 {
let d = sanitize_vec3(a) - sanitize_vec3(b);
let dist_sq = d.length_squared();
(dist_sq * neg_inv_two_sigma_sq).exp()
}
#[cfg(target_arch = "wasm32")]
#[must_use]
const fn effective_thread_count(_threads: usize) -> usize {
1
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
fn effective_thread_count(threads: usize) -> usize {
if threads == 0 {
std::thread::available_parallelism().map_or(8, std::num::NonZero::get)
} else {
threads
}
}
struct TapConstants<'a> {
normal_n: &'a [Vec3],
neg_inv_sigma_depth: f32,
neg_inv_two_sigma_color_sq: f32,
normal_power: f32,
}
#[inline]
fn atrous_pixel(
src: &[Vec3],
g: &GBuffers<'_>,
x: usize,
y: usize,
stride: i64,
tap: &TapConstants<'_>,
) -> Vec3 {
let width = g.width;
let height = g.height;
let center_idx = y * width + x;
let c_color = src[center_idx];
let c_depth = g.depth[center_idx];
let c_normal_n = tap.normal_n[center_idx];
let c_facet = g.facet_id[center_idx];
let mut sum = Vec3::ZERO;
let mut weight_sum = 0.0f32;
for (ky, &hy) in B3_SPLINE.iter().enumerate() {
let oy = (ky as i64 - 2) * stride;
let sy = y as i64 + oy;
if sy < 0 || sy >= height as i64 {
continue;
}
for (kx, &hx) in B3_SPLINE.iter().enumerate() {
let ox = (kx as i64 - 2) * stride;
let sx = x as i64 + ox;
if sx < 0 || sx >= width as i64 {
continue;
}
let idx = sy as usize * width + sx as usize;
let kernel_w = hy * hx;
let wf = facet_weight(c_facet, g.facet_id[idx]);
if wf == 0.0 {
continue;
}
let wn = normal_weight_unit(c_normal_n, tap.normal_n[idx], tap.normal_power);
let wd = depth_weight(c_depth, g.depth[idx], tap.neg_inv_sigma_depth);
let wc = color_weight(c_color, src[idx], tap.neg_inv_two_sigma_color_sq);
let w = kernel_w * wf * wn * wd * wc;
sum += src[idx] * w;
weight_sum += w;
}
}
if weight_sum > SIGMA_EPS {
sum / weight_sum
} else {
c_color
}
}
fn atrous_pass_row_chunk(
src: &[Vec3],
dst_rows: &mut [Vec3],
g: &GBuffers<'_>,
stride: i64,
tap: &TapConstants<'_>,
start_y: usize,
) {
let width = g.width;
for (local_y, row) in dst_rows.chunks_mut(width).enumerate() {
let y = start_y + local_y;
for (x, out) in row.iter_mut().enumerate() {
*out = atrous_pixel(src, g, x, y, stride, tap);
}
}
}
fn atrous_pass(
src: &[Vec3],
dst: &mut [Vec3],
g: &GBuffers<'_>,
stride: i64,
tap: &TapConstants<'_>,
num_threads: usize,
) {
let width = g.width;
let height = g.height;
if width == 0 || height == 0 {
return;
}
let num_threads = num_threads.max(1);
let rows_per_chunk = height.div_ceil(num_threads).max(1);
if rows_per_chunk >= height {
atrous_pass_row_chunk(src, dst, g, stride, tap, 0);
return;
}
std::thread::scope(|s| {
let chunk_len = rows_per_chunk * width;
for (chunk_idx, chunk) in dst.chunks_mut(chunk_len).enumerate() {
let start_y = chunk_idx * rows_per_chunk;
s.spawn(move || {
atrous_pass_row_chunk(src, chunk, g, stride, tap, start_y);
});
}
});
}
#[derive(Default)]
pub struct AtrousDenoiser {
ping: Vec<Vec3>,
pong: Vec<Vec3>,
normal_n: Vec<Vec3>,
}
impl AtrousDenoiser {
#[must_use]
pub const fn new() -> Self {
Self {
ping: Vec::new(),
pong: Vec::new(),
normal_n: Vec::new(),
}
}
fn ensure_capacity(&mut self, len: usize) {
if self.ping.len() != len {
self.ping.clear();
self.ping.resize(len, Vec3::ZERO);
}
if self.pong.len() != len {
self.pong.clear();
self.pong.resize(len, Vec3::ZERO);
}
if self.normal_n.len() != len {
self.normal_n.clear();
self.normal_n.resize(len, Vec3::ZERO);
}
}
#[must_use]
pub fn denoise(&mut self, inputs: &GBuffers<'_>, params: &AtrousParams) -> Vec<Vec3> {
let mut output = vec![Vec3::ZERO; inputs.color.len()];
self.denoise_into(inputs, params, &mut output);
output
}
pub fn denoise_into(
&mut self,
inputs: &GBuffers<'_>,
params: &AtrousParams,
output: &mut Vec<Vec3>,
) {
self.denoise_into_with_threads(inputs, params, output, 0);
}
pub fn denoise_into_with_threads(
&mut self,
inputs: &GBuffers<'_>,
params: &AtrousParams,
output: &mut Vec<Vec3>,
threads: usize,
) {
let len = inputs.width * inputs.height;
output.clear();
output.resize(len, Vec3::ZERO);
if len == 0 {
return;
}
let buffers_ok = inputs.color.len() == len
&& inputs.depth.len() == len
&& inputs.normal.len() == len
&& inputs.facet_id.len() == len;
if !buffers_ok {
let n = inputs.color.len().min(len);
output[..n].copy_from_slice(&inputs.color[..n]);
for v in &mut output[n..] {
*v = Vec3::ZERO;
}
return;
}
let taper = taper_strength(inputs.spp, params.taper_reference_spp);
let num_passes = params.num_passes.clamp(1, 8);
if taper < params.taper_identity_epsilon {
output.copy_from_slice(inputs.color);
return;
}
let sigma_color_effective = (params.sigma_color * taper).max(0.0);
let num_threads = effective_thread_count(threads);
self.ensure_capacity(len);
self.ping.copy_from_slice(inputs.color);
for (dst, &src_n) in self.normal_n.iter_mut().zip(inputs.normal.iter()) {
*dst = unit_normal_or_zero(src_n);
}
let sigma_depth = params.sigma_depth.max(SIGMA_EPS);
let sigma_color_sq = (sigma_color_effective * sigma_color_effective).max(SIGMA_EPS);
let tap = TapConstants {
normal_n: &self.normal_n,
neg_inv_sigma_depth: -1.0 / sigma_depth,
neg_inv_two_sigma_color_sq: -1.0 / (2.0 * sigma_color_sq),
normal_power: params.normal_power,
};
for pass in 0..num_passes {
let stride = 1i64 << pass;
atrous_pass(
&self.ping,
&mut self.pong,
inputs,
stride,
&tap,
num_threads,
);
std::mem::swap(&mut self.ping, &mut self.pong);
}
output.copy_from_slice(&self.ping);
}
}
#[must_use]
pub fn atrous_denoise(inputs: &GBuffers<'_>, params: &AtrousParams) -> Vec<Vec3> {
AtrousDenoiser::new().denoise(inputs, params)
}
#[cfg(test)]
mod tests {
use super::*;
struct Xorshift32(u32);
impl Xorshift32 {
const fn new(seed: u32) -> Self {
Self(if seed == 0 { 0xdead_beef } else { seed })
}
const fn next_u32(&mut self) -> u32 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.0 = x;
x
}
fn next_f32(&mut self) -> f32 {
(f64::from(self.next_u32()) / f64::from(u32::MAX)) as f32
}
fn next_signed(&mut self) -> f32 {
self.next_f32().mul_add(2.0, -1.0)
}
}
fn irregular_scene(width: usize, height: usize, seed: u32) -> (GBuffers<'static>, Vec<Vec3>) {
let len = width * height;
let mut rng = Xorshift32::new(seed);
let mut color = vec![Vec3::ZERO; len];
let mut depth = vec![0.0f32; len];
let mut normal = vec![Vec3::Z; len];
let mut facet_id = vec![0i32; len];
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
facet_id[idx] = ((x / 5 + y / 7) % 6) as i32;
depth[idx] = rng.next_f32() * 4.0;
let jitter =
Vec3::new(rng.next_signed() * 0.3, rng.next_signed() * 0.3, 1.0).normalize();
normal[idx] = jitter;
color[idx] = Vec3::new(rng.next_f32(), rng.next_f32(), rng.next_f32());
}
}
let color_s: &'static [Vec3] = Box::leak(color.clone().into_boxed_slice());
let depth_s: &'static [f32] = Box::leak(depth.into_boxed_slice());
let normal_s: &'static [Vec3] = Box::leak(normal.into_boxed_slice());
let facet_s: &'static [i32] = Box::leak(facet_id.into_boxed_slice());
(
GBuffers {
color: color_s,
depth: depth_s,
normal: normal_s,
facet_id: facet_s,
width,
height,
spp: 1,
},
color,
)
}
fn assert_bit_identical(a: &[Vec3], b: &[Vec3], context: &str) {
assert_eq!(a.len(), b.len(), "{context}: length mismatch");
for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
assert_eq!(
x.x.to_bits(),
y.x.to_bits(),
"{context}: pixel {i} x differs"
);
assert_eq!(
x.y.to_bits(),
y.y.to_bits(),
"{context}: pixel {i} y differs"
);
assert_eq!(
x.z.to_bits(),
y.z.to_bits(),
"{context}: pixel {i} z differs"
);
}
}
#[test]
fn atrous_pass_is_thread_count_invariant() {
let width = 137;
let height = 91;
let (g, color) = irregular_scene(width, height, 0x1234_5678);
let params = AtrousParams::default();
let sigma = params.sigma_color;
let normal_n: Vec<Vec3> = g.normal.iter().map(|&n| unit_normal_or_zero(n)).collect();
let tap = TapConstants {
normal_n: &normal_n,
neg_inv_sigma_depth: -1.0 / params.sigma_depth.max(SIGMA_EPS),
neg_inv_two_sigma_color_sq: -1.0 / (2.0 * (sigma * sigma).max(SIGMA_EPS)),
normal_power: params.normal_power,
};
let mut reference = vec![Vec3::ZERO; width * height];
atrous_pass(&color, &mut reference, &g, 4, &tap, 1);
for threads in [2usize, 3, 8, 16, 200] {
let mut out = vec![Vec3::ZERO; width * height];
atrous_pass(&color, &mut out, &g, 4, &tap, threads);
assert_bit_identical(&reference, &out, &format!("threads={threads}"));
}
}
#[test]
fn cos_pow_matches_powf_within_tolerance() {
for &power in &[1.0f32, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0] {
for &base in &[0.0f32, 0.1, 0.37, 0.5, 0.9, 1.0] {
let fast = cos_pow(base, power);
let reference = base.powf(power);
let diff = (fast - reference).abs();
assert!(
diff <= reference.abs().mul_add(1.0e-5, 1.0e-6),
"cos_pow({base}, {power}) = {fast}, powf = {reference}, diff = {diff}"
);
}
}
for &power in &[5.0f32, 60.0] {
for &base in &[0.0f32, 0.3, 0.7, 1.0] {
assert_eq!(cos_pow(base, power), base.powf(power));
}
}
}
#[test]
fn denoise_into_is_thread_count_invariant() {
let width = 113;
let height = 67;
let (g, _color) = irregular_scene(width, height, 0xabcd_ef01);
let params = AtrousParams::default();
let mut denoiser = AtrousDenoiser::new();
let mut reference = Vec::new();
denoiser.denoise_into_with_threads(&g, ¶ms, &mut reference, 1);
for threads in [2usize, 4, 8, 16] {
let mut denoiser = AtrousDenoiser::new();
let mut out = Vec::new();
denoiser.denoise_into_with_threads(&g, ¶ms, &mut out, threads);
assert_bit_identical(&reference, &out, &format!("threads={threads}"));
}
let mut denoiser = AtrousDenoiser::new();
let mut out = Vec::new();
denoiser.denoise_into(&g, ¶ms, &mut out);
assert_bit_identical(&reference, &out, "auto thread count");
}
}