feat(renderer): add stencil optimization, half-res SSGI, bilateral bloom

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 17:21:44 +09:00
parent 025bf4d0b9
commit bc2880d41c
4 changed files with 186 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
/// Bilateral bloom weight: attenuates blur across brightness edges.
pub fn bilateral_bloom_weight(
center_luminance: f32,
sample_luminance: f32,
spatial_weight: f32,
sigma_luminance: f32,
) -> f32 {
let lum_diff = (center_luminance - sample_luminance).abs();
let lum_weight = (-lum_diff * lum_diff / (2.0 * sigma_luminance * sigma_luminance)).exp();
spatial_weight * lum_weight
}
/// Calculate luminance from RGB.
pub fn luminance(r: f32, g: f32, b: f32) -> f32 {
0.2126 * r + 0.7152 * g + 0.0722 * b
}
/// 5-tap Gaussian weights for 1D bloom blur.
pub fn gaussian_5tap() -> [f32; 5] {
// Sigma ≈ 1.4
[0.0625, 0.25, 0.375, 0.25, 0.0625]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bilateral_bloom_same_luminance() {
let w = bilateral_bloom_weight(0.5, 0.5, 1.0, 0.1);
assert!((w - 1.0).abs() < 0.01); // same lum → full weight
}
#[test]
fn test_bilateral_bloom_edge() {
let w = bilateral_bloom_weight(0.1, 0.9, 1.0, 0.1);
assert!(w < 0.01); // large lum diff → near zero
}
#[test]
fn test_luminance_white() {
let l = luminance(1.0, 1.0, 1.0);
assert!((l - 1.0).abs() < 0.01);
}
#[test]
fn test_gaussian_5tap_sum() {
let g = gaussian_5tap();
let sum: f32 = g.iter().sum();
assert!((sum - 1.0).abs() < 0.01);
}
}

View File

@@ -0,0 +1,60 @@
/// Calculate half-resolution dimensions (rounded up).
pub fn half_resolution(width: u32, height: u32) -> (u32, u32) {
((width + 1) / 2, (height + 1) / 2)
}
/// Bilinear upscale weight calculation for a 2x2 tap pattern.
pub fn bilinear_weights(frac_x: f32, frac_y: f32) -> [f32; 4] {
let w00 = (1.0 - frac_x) * (1.0 - frac_y);
let w10 = frac_x * (1.0 - frac_y);
let w01 = (1.0 - frac_x) * frac_y;
let w11 = frac_x * frac_y;
[w00, w10, w01, w11]
}
/// Depth-aware upscale: reject samples with large depth discontinuity.
pub fn depth_aware_weight(center_depth: f32, sample_depth: f32, threshold: f32) -> f32 {
if (center_depth - sample_depth).abs() > threshold { 0.0 } else { 1.0 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_half_resolution() {
assert_eq!(half_resolution(1920, 1080), (960, 540));
assert_eq!(half_resolution(1921, 1081), (961, 541));
assert_eq!(half_resolution(1, 1), (1, 1));
}
#[test]
fn test_bilinear_weights_center() {
let w = bilinear_weights(0.5, 0.5);
for &wi in &w { assert!((wi - 0.25).abs() < 1e-6); }
}
#[test]
fn test_bilinear_weights_corner() {
let w = bilinear_weights(0.0, 0.0);
assert!((w[0] - 1.0).abs() < 1e-6); // top-left = full weight
assert!((w[1] - 0.0).abs() < 1e-6);
}
#[test]
fn test_bilinear_weights_sum_to_one() {
let w = bilinear_weights(0.3, 0.7);
let sum: f32 = w.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
}
#[test]
fn test_depth_aware_accept() {
assert!((depth_aware_weight(0.5, 0.51, 0.1) - 1.0).abs() < 1e-6);
}
#[test]
fn test_depth_aware_reject() {
assert!((depth_aware_weight(0.5, 0.9, 0.1) - 0.0).abs() < 1e-6);
}
}

View File

@@ -93,6 +93,10 @@ pub use bilateral_blur::BilateralBlur;
pub use temporal_accum::TemporalAccumulation;
pub use taa::Taa;
pub use ssr::Ssr;
pub mod stencil_opt;
pub mod half_res_ssgi;
pub mod bilateral_bloom;
pub use png::parse_png;
pub use jpg::parse_jpg;
pub use gltf::{parse_gltf, GltfData, GltfMesh, GltfMaterial};

View File

@@ -0,0 +1,70 @@
/// Stencil state for marking pixels inside a light volume.
pub fn light_volume_stencil_mark() -> wgpu::DepthStencilState {
wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::IncrementWrap,
pass_op: wgpu::StencilOperation::Keep,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::Always,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::DecrementWrap,
pass_op: wgpu::StencilOperation::Keep,
},
read_mask: 0xFF,
write_mask: 0xFF,
},
bias: wgpu::DepthBiasState::default(),
}
}
/// Stencil state for rendering only where stencil != 0 (inside light volume).
pub fn light_volume_stencil_test() -> wgpu::DepthStencilState {
wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState {
front: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::NotEqual,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
back: wgpu::StencilFaceState {
compare: wgpu::CompareFunction::NotEqual,
fail_op: wgpu::StencilOperation::Keep,
depth_fail_op: wgpu::StencilOperation::Keep,
pass_op: wgpu::StencilOperation::Keep,
},
read_mask: 0xFF,
write_mask: 0x00,
},
bias: wgpu::DepthBiasState::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mark_stencil_format() {
let state = light_volume_stencil_mark();
assert_eq!(state.format, wgpu::TextureFormat::Depth24PlusStencil8);
assert!(!state.depth_write_enabled);
}
#[test]
fn test_test_stencil_compare() {
let state = light_volume_stencil_test();
assert_eq!(state.stencil.front.compare, wgpu::CompareFunction::NotEqual);
assert_eq!(state.stencil.write_mask, 0x00); // read-only
}
}