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 temporal_accum::TemporalAccumulation;
|
||||||
pub use taa::Taa;
|
pub use taa::Taa;
|
||||||
pub use ssr::Ssr;
|
pub use ssr::Ssr;
|
||||||
|
pub mod stencil_opt;
|
||||||
|
pub mod half_res_ssgi;
|
||||||
|
pub mod bilateral_bloom;
|
||||||
|
|
||||||
pub use png::parse_png;
|
pub use png::parse_png;
|
||||||
pub use jpg::parse_jpg;
|
pub use jpg::parse_jpg;
|
||||||
pub use gltf::{parse_gltf, GltfData, GltfMesh, GltfMaterial};
|
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