diff --git a/crates/voltex_renderer/src/blas_update.rs b/crates/voltex_renderer/src/blas_update.rs new file mode 100644 index 0000000..189bc75 --- /dev/null +++ b/crates/voltex_renderer/src/blas_update.rs @@ -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 { 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()); + } +} diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index dd2d452..19df023 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -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; diff --git a/crates/voltex_renderer/src/light_probes.rs b/crates/voltex_renderer/src/light_probes.rs new file mode 100644 index 0000000..193a376 --- /dev/null +++ b/crates/voltex_renderer/src/light_probes.rs @@ -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, +} + +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); + } +} diff --git a/crates/voltex_renderer/src/light_volumes.rs b/crates/voltex_renderer/src/light_volumes.rs new file mode 100644 index 0000000..9f351ff --- /dev/null +++ b/crates/voltex_renderer/src/light_volumes.rs @@ -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 + } +} diff --git a/crates/voltex_renderer/src/rt_fallback.rs b/crates/voltex_renderer/src/rt_fallback.rs new file mode 100644 index 0000000..7961940 --- /dev/null +++ b/crates/voltex_renderer/src/rt_fallback.rs @@ -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); + } +} diff --git a/crates/voltex_renderer/src/soft_rt_shadow.rs b/crates/voltex_renderer/src/soft_rt_shadow.rs new file mode 100644 index 0000000..f390d68 --- /dev/null +++ b/crates/voltex_renderer/src/soft_rt_shadow.rs @@ -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)); } +}