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:
52
crates/voltex_renderer/src/bilateral_bloom.rs
Normal file
52
crates/voltex_renderer/src/bilateral_bloom.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
60
crates/voltex_renderer/src/half_res_ssgi.rs
Normal file
60
crates/voltex_renderer/src/half_res_ssgi.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
70
crates/voltex_renderer/src/stencil_opt.rs
Normal file
70
crates/voltex_renderer/src/stencil_opt.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user