feat(renderer): add soft shadows, BLAS tracker, RT fallback, light probes, light volumes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
34
crates/voltex_renderer/src/blas_update.rs
Normal file
34
crates/voltex_renderer/src/blas_update.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
/// Tracks which meshes need BLAS rebuild.
|
||||
pub struct BlasTracker {
|
||||
dirty: Vec<(u32, bool)>, // (mesh_id, needs_rebuild)
|
||||
}
|
||||
|
||||
impl BlasTracker {
|
||||
pub fn new() -> Self { BlasTracker { dirty: Vec::new() } }
|
||||
pub fn register(&mut self, mesh_id: u32) { self.dirty.push((mesh_id, false)); }
|
||||
pub fn mark_dirty(&mut self, mesh_id: u32) {
|
||||
if let Some(entry) = self.dirty.iter_mut().find(|(id, _)| *id == mesh_id) { entry.1 = true; }
|
||||
}
|
||||
pub fn dirty_meshes(&self) -> Vec<u32> { self.dirty.iter().filter(|(_, d)| *d).map(|(id, _)| *id).collect() }
|
||||
pub fn clear_dirty(&mut self) { for entry in &mut self.dirty { entry.1 = false; } }
|
||||
pub fn mesh_count(&self) -> usize { self.dirty.len() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_register_and_dirty() {
|
||||
let mut t = BlasTracker::new();
|
||||
t.register(1); t.register(2);
|
||||
t.mark_dirty(1);
|
||||
assert_eq!(t.dirty_meshes(), vec![1]);
|
||||
}
|
||||
#[test]
|
||||
fn test_clear_dirty() {
|
||||
let mut t = BlasTracker::new();
|
||||
t.register(1); t.mark_dirty(1);
|
||||
t.clear_dirty();
|
||||
assert!(t.dirty_meshes().is_empty());
|
||||
}
|
||||
}
|
||||
@@ -100,3 +100,12 @@ pub mod bilateral_bloom;
|
||||
pub use png::parse_png;
|
||||
pub use jpg::parse_jpg;
|
||||
pub use gltf::{parse_gltf, GltfData, GltfMesh, GltfMaterial};
|
||||
pub mod soft_rt_shadow;
|
||||
pub mod blas_update;
|
||||
pub use blas_update::BlasTracker;
|
||||
pub mod rt_fallback;
|
||||
pub use rt_fallback::{RtCapabilities, RenderMode};
|
||||
pub mod light_probes;
|
||||
pub use light_probes::{LightProbe, LightProbeGrid};
|
||||
pub mod light_volumes;
|
||||
pub use light_volumes::LightVolume;
|
||||
|
||||
63
crates/voltex_renderer/src/light_probes.rs
Normal file
63
crates/voltex_renderer/src/light_probes.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
pub struct LightProbe {
|
||||
pub position: [f32; 3],
|
||||
pub sh_coefficients: [[f32; 3]; 9], // L2 SH, 9 RGB coefficients
|
||||
}
|
||||
|
||||
impl LightProbe {
|
||||
pub fn new(position: [f32; 3]) -> Self {
|
||||
LightProbe { position, sh_coefficients: [[0.0; 3]; 9] }
|
||||
}
|
||||
pub fn evaluate_irradiance(&self, normal: [f32; 3]) -> [f32; 3] {
|
||||
// L0
|
||||
let mut result = [self.sh_coefficients[0][0], self.sh_coefficients[0][1], self.sh_coefficients[0][2]];
|
||||
// L1
|
||||
let (nx, ny, nz) = (normal[0], normal[1], normal[2]);
|
||||
for c in 0..3 {
|
||||
result[c] += self.sh_coefficients[1][c] * ny;
|
||||
result[c] += self.sh_coefficients[2][c] * nz;
|
||||
result[c] += self.sh_coefficients[3][c] * nx;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LightProbeGrid {
|
||||
probes: Vec<LightProbe>,
|
||||
}
|
||||
|
||||
impl LightProbeGrid {
|
||||
pub fn new() -> Self { LightProbeGrid { probes: Vec::new() } }
|
||||
pub fn add(&mut self, probe: LightProbe) { self.probes.push(probe); }
|
||||
pub fn nearest(&self, pos: [f32; 3]) -> Option<&LightProbe> {
|
||||
self.probes.iter().min_by(|a, b| {
|
||||
let da = dist_sq(a.position, pos);
|
||||
let db = dist_sq(b.position, pos);
|
||||
da.partial_cmp(&db).unwrap()
|
||||
})
|
||||
}
|
||||
pub fn len(&self) -> usize { self.probes.len() }
|
||||
}
|
||||
|
||||
fn dist_sq(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||||
let dx = a[0]-b[0]; let dy = a[1]-b[1]; let dz = a[2]-b[2]; dx*dx+dy*dy+dz*dz
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_probe_evaluate() {
|
||||
let mut p = LightProbe::new([0.0; 3]);
|
||||
p.sh_coefficients[0] = [0.5, 0.5, 0.5]; // ambient
|
||||
let irr = p.evaluate_irradiance([0.0, 1.0, 0.0]);
|
||||
assert!(irr[0] > 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn test_grid_nearest() {
|
||||
let mut grid = LightProbeGrid::new();
|
||||
grid.add(LightProbe::new([0.0, 0.0, 0.0]));
|
||||
grid.add(LightProbe::new([10.0, 0.0, 0.0]));
|
||||
let nearest = grid.nearest([1.0, 0.0, 0.0]).unwrap();
|
||||
assert!((nearest.position[0] - 0.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
54
crates/voltex_renderer/src/light_volumes.rs
Normal file
54
crates/voltex_renderer/src/light_volumes.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
/// Light volume shapes for deferred lighting optimization.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LightVolume {
|
||||
Sphere { center: [f32; 3], radius: f32 },
|
||||
Cone { apex: [f32; 3], direction: [f32; 3], angle: f32, range: f32 },
|
||||
Fullscreen,
|
||||
}
|
||||
|
||||
impl LightVolume {
|
||||
pub fn point_light(center: [f32; 3], radius: f32) -> Self { LightVolume::Sphere { center, radius } }
|
||||
pub fn spot_light(apex: [f32; 3], dir: [f32; 3], angle: f32, range: f32) -> Self {
|
||||
LightVolume::Cone { apex, direction: dir, angle, range }
|
||||
}
|
||||
pub fn directional() -> Self { LightVolume::Fullscreen }
|
||||
|
||||
pub fn contains_point(&self, point: [f32; 3]) -> bool {
|
||||
match self {
|
||||
LightVolume::Sphere { center, radius } => {
|
||||
let dx = point[0]-center[0]; let dy = point[1]-center[1]; let dz = point[2]-center[2];
|
||||
dx*dx + dy*dy + dz*dz <= radius * radius
|
||||
}
|
||||
LightVolume::Fullscreen => true,
|
||||
LightVolume::Cone { apex, direction, angle, range } => {
|
||||
let dx = point[0]-apex[0]; let dy = point[1]-apex[1]; let dz = point[2]-apex[2];
|
||||
let dist = (dx*dx+dy*dy+dz*dz).sqrt();
|
||||
if dist > *range { return false; }
|
||||
if dist < 1e-6 { return true; }
|
||||
let dot = (dx*direction[0]+dy*direction[1]+dz*direction[2]) / dist;
|
||||
dot >= angle.cos()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_sphere_contains() {
|
||||
let v = LightVolume::point_light([0.0; 3], 5.0);
|
||||
assert!(v.contains_point([3.0, 0.0, 0.0]));
|
||||
assert!(!v.contains_point([6.0, 0.0, 0.0]));
|
||||
}
|
||||
#[test]
|
||||
fn test_fullscreen() {
|
||||
assert!(LightVolume::directional().contains_point([999.0, 999.0, 999.0]));
|
||||
}
|
||||
#[test]
|
||||
fn test_cone_contains() {
|
||||
let v = LightVolume::spot_light([0.0;3], [0.0,0.0,-1.0], 0.5, 10.0);
|
||||
assert!(v.contains_point([0.0, 0.0, -5.0])); // on axis
|
||||
assert!(!v.contains_point([0.0, 0.0, 5.0])); // behind
|
||||
}
|
||||
}
|
||||
57
crates/voltex_renderer/src/rt_fallback.rs
Normal file
57
crates/voltex_renderer/src/rt_fallback.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
/// Check if a wgpu adapter supports features needed for RT.
|
||||
pub struct RtCapabilities {
|
||||
pub supports_compute: bool,
|
||||
pub supports_storage_textures: bool,
|
||||
pub supports_timestamp_query: bool,
|
||||
}
|
||||
|
||||
impl RtCapabilities {
|
||||
/// Evaluate capabilities (simplified — real check would use adapter.features()).
|
||||
pub fn evaluate(max_storage_buffers: u32, max_compute_workgroup_size: u32) -> Self {
|
||||
RtCapabilities {
|
||||
supports_compute: max_compute_workgroup_size >= 256,
|
||||
supports_storage_textures: max_storage_buffers >= 4,
|
||||
supports_timestamp_query: false, // opt-in feature
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_use_rt(&self) -> bool { self.supports_compute && self.supports_storage_textures }
|
||||
}
|
||||
|
||||
/// Fallback rendering mode when RT is not available.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum RenderMode {
|
||||
Full, // All RT features
|
||||
NoRtShadows, // Use shadow maps instead
|
||||
NoRtReflections,// Use SSR instead
|
||||
Minimal, // Shadow maps + no reflections
|
||||
}
|
||||
|
||||
pub fn select_render_mode(caps: &RtCapabilities) -> RenderMode {
|
||||
if caps.can_use_rt() { RenderMode::Full }
|
||||
else if caps.supports_compute { RenderMode::NoRtShadows }
|
||||
else { RenderMode::Minimal }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_full_caps() {
|
||||
let c = RtCapabilities::evaluate(8, 256);
|
||||
assert!(c.can_use_rt());
|
||||
assert_eq!(select_render_mode(&c), RenderMode::Full);
|
||||
}
|
||||
#[test]
|
||||
fn test_no_compute() {
|
||||
let c = RtCapabilities::evaluate(8, 64);
|
||||
assert!(!c.can_use_rt());
|
||||
assert_eq!(select_render_mode(&c), RenderMode::Minimal);
|
||||
}
|
||||
#[test]
|
||||
fn test_limited_storage() {
|
||||
let c = RtCapabilities::evaluate(2, 256);
|
||||
assert!(!c.can_use_rt());
|
||||
assert_eq!(select_render_mode(&c), RenderMode::NoRtShadows);
|
||||
}
|
||||
}
|
||||
34
crates/voltex_renderer/src/soft_rt_shadow.rs
Normal file
34
crates/voltex_renderer/src/soft_rt_shadow.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Pod, Zeroable)]
|
||||
pub struct SoftShadowParams {
|
||||
pub light_dir: [f32; 3],
|
||||
pub num_rays: u32,
|
||||
pub light_radius: f32,
|
||||
pub max_distance: f32,
|
||||
pub _pad: [f32; 2],
|
||||
}
|
||||
|
||||
impl SoftShadowParams {
|
||||
pub fn new() -> Self {
|
||||
SoftShadowParams { light_dir: [0.0, -1.0, 0.0], num_rays: 16, light_radius: 0.02, max_distance: 50.0, _pad: [0.0; 2] }
|
||||
}
|
||||
}
|
||||
|
||||
/// Penumbra estimation: wider penumbra for objects farther from occluder.
|
||||
pub fn penumbra_size(light_radius: f32, blocker_dist: f32, receiver_dist: f32) -> f32 {
|
||||
if blocker_dist < 1e-6 { return 0.0; }
|
||||
light_radius * (receiver_dist - blocker_dist) / blocker_dist
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_params_default() { let p = SoftShadowParams::new(); assert_eq!(p.num_rays, 16); }
|
||||
#[test]
|
||||
fn test_penumbra_close() { assert!((penumbra_size(0.02, 1.0, 1.1) - 0.002).abs() < 0.001); }
|
||||
#[test]
|
||||
fn test_penumbra_far() { assert!(penumbra_size(0.02, 1.0, 5.0) > penumbra_size(0.02, 1.0, 2.0)); }
|
||||
}
|
||||
Reference in New Issue
Block a user