Skip to main content

proof_engine/lighting/
shadows.rs

1//! Shadow mapping subsystem for Proof Engine.
2//!
3//! Provides depth-buffer shadow maps, cascaded shadow maps for directional lights,
4//! omnidirectional shadow maps for point lights (cubemap layout), shadow atlas packing,
5//! PCF filtering, variance shadow maps, configurable bias, distance fade, and shadow
6//! caster culling.
7
8use super::lights::{Vec3, Mat4, Color, LightId, Light, CascadeShadowParams};
9use std::collections::HashMap;
10
11// ── Shadow Map ──────────────────────────────────────────────────────────────
12
13/// A single 2D depth buffer for shadow mapping.
14#[derive(Debug, Clone)]
15pub struct ShadowMap {
16    /// Width of the depth buffer in texels.
17    pub width: u32,
18    /// Height of the depth buffer in texels.
19    pub height: u32,
20    /// Depth values stored as a flat row-major array. 1.0 = far, 0.0 = near.
21    pub depth_buffer: Vec<f32>,
22    /// View-projection matrix used when rendering to this shadow map.
23    pub view_projection: Mat4,
24    /// Near plane distance.
25    pub near: f32,
26    /// Far plane distance.
27    pub far: f32,
28}
29
30impl ShadowMap {
31    /// Create a new shadow map with the given resolution.
32    pub fn new(width: u32, height: u32) -> Self {
33        let size = (width as usize) * (height as usize);
34        Self {
35            width,
36            height,
37            depth_buffer: vec![1.0; size],
38            view_projection: Mat4::IDENTITY,
39            near: 0.1,
40            far: 100.0,
41        }
42    }
43
44    /// Clear the depth buffer to the far value.
45    pub fn clear(&mut self) {
46        for d in self.depth_buffer.iter_mut() {
47            *d = 1.0;
48        }
49    }
50
51    /// Write a depth value at the given texel coordinates.
52    pub fn write_depth(&mut self, x: u32, y: u32, depth: f32) {
53        if x < self.width && y < self.height {
54            let idx = (y as usize) * (self.width as usize) + (x as usize);
55            if depth < self.depth_buffer[idx] {
56                self.depth_buffer[idx] = depth;
57            }
58        }
59    }
60
61    /// Read the depth at the given texel coordinates.
62    pub fn read_depth(&self, x: u32, y: u32) -> f32 {
63        if x < self.width && y < self.height {
64            self.depth_buffer[(y as usize) * (self.width as usize) + (x as usize)]
65        } else {
66            1.0
67        }
68    }
69
70    /// Sample depth with bilinear filtering at normalized UV coordinates.
71    pub fn sample_bilinear(&self, u: f32, v: f32) -> f32 {
72        let u = u.clamp(0.0, 1.0);
73        let v = v.clamp(0.0, 1.0);
74
75        let fx = u * (self.width as f32 - 1.0);
76        let fy = v * (self.height as f32 - 1.0);
77
78        let x0 = fx.floor() as u32;
79        let y0 = fy.floor() as u32;
80        let x1 = (x0 + 1).min(self.width - 1);
81        let y1 = (y0 + 1).min(self.height - 1);
82
83        let frac_x = fx - fx.floor();
84        let frac_y = fy - fy.floor();
85
86        let d00 = self.read_depth(x0, y0);
87        let d10 = self.read_depth(x1, y0);
88        let d01 = self.read_depth(x0, y1);
89        let d11 = self.read_depth(x1, y1);
90
91        let top = d00 + (d10 - d00) * frac_x;
92        let bottom = d01 + (d11 - d01) * frac_x;
93        top + (bottom - top) * frac_y
94    }
95
96    /// Project a world-space point into shadow map UV + depth.
97    pub fn project_point(&self, world_pos: Vec3) -> (f32, f32, f32) {
98        let clip = self.view_projection.transform_point(world_pos);
99        let u = clip.x * 0.5 + 0.5;
100        let v = clip.y * 0.5 + 0.5;
101        let depth = clip.z * 0.5 + 0.5;
102        (u, v, depth)
103    }
104
105    /// Test if a world-space point is in shadow (simple depth comparison).
106    pub fn is_in_shadow(&self, world_pos: Vec3, bias: f32) -> bool {
107        let (u, v, depth) = self.project_point(world_pos);
108        if u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 {
109            return false; // Outside shadow map
110        }
111        let stored_depth = self.sample_bilinear(u, v);
112        depth - bias > stored_depth
113    }
114
115    /// Rasterize a triangle into the shadow map depth buffer.
116    pub fn rasterize_triangle(&mut self, v0: Vec3, v1: Vec3, v2: Vec3) {
117        let p0 = self.view_projection.transform_point(v0);
118        let p1 = self.view_projection.transform_point(v1);
119        let p2 = self.view_projection.transform_point(v2);
120
121        // Convert to screen space
122        let sx0 = (p0.x * 0.5 + 0.5) * self.width as f32;
123        let sy0 = (p0.y * 0.5 + 0.5) * self.height as f32;
124        let sz0 = p0.z * 0.5 + 0.5;
125
126        let sx1 = (p1.x * 0.5 + 0.5) * self.width as f32;
127        let sy1 = (p1.y * 0.5 + 0.5) * self.height as f32;
128        let sz1 = p1.z * 0.5 + 0.5;
129
130        let sx2 = (p2.x * 0.5 + 0.5) * self.width as f32;
131        let sy2 = (p2.y * 0.5 + 0.5) * self.height as f32;
132        let sz2 = p2.z * 0.5 + 0.5;
133
134        // Compute bounding box
135        let min_x = sx0.min(sx1).min(sx2).max(0.0) as u32;
136        let max_x = sx0.max(sx1).max(sx2).min(self.width as f32 - 1.0) as u32;
137        let min_y = sy0.min(sy1).min(sy2).max(0.0) as u32;
138        let max_y = sy0.max(sy1).max(sy2).min(self.height as f32 - 1.0) as u32;
139
140        // Rasterize with barycentric coordinates
141        for y in min_y..=max_y {
142            for x in min_x..=max_x {
143                let px = x as f32 + 0.5;
144                let py = y as f32 + 0.5;
145
146                let area = edge_function(sx0, sy0, sx1, sy1, sx2, sy2);
147                if area.abs() < 1e-10 {
148                    continue;
149                }
150
151                let w0 = edge_function(sx1, sy1, sx2, sy2, px, py);
152                let w1 = edge_function(sx2, sy2, sx0, sy0, px, py);
153                let w2 = edge_function(sx0, sy0, sx1, sy1, px, py);
154
155                if (w0 >= 0.0 && w1 >= 0.0 && w2 >= 0.0) || (w0 <= 0.0 && w1 <= 0.0 && w2 <= 0.0) {
156                    let inv_area = 1.0 / area;
157                    let b0 = w0 * inv_area;
158                    let b1 = w1 * inv_area;
159                    let b2 = w2 * inv_area;
160
161                    let depth = sz0 * b0 + sz1 * b1 + sz2 * b2;
162                    self.write_depth(x, y, depth.clamp(0.0, 1.0));
163                }
164            }
165        }
166    }
167
168    /// Get the total number of texels.
169    pub fn texel_count(&self) -> usize {
170        (self.width as usize) * (self.height as usize)
171    }
172
173    /// Get memory usage in bytes (approximate).
174    pub fn memory_bytes(&self) -> usize {
175        self.depth_buffer.len() * 4
176    }
177}
178
179/// Edge function for triangle rasterization.
180fn edge_function(ax: f32, ay: f32, bx: f32, by: f32, cx: f32, cy: f32) -> f32 {
181    (cx - ax) * (by - ay) - (cy - ay) * (bx - ax)
182}
183
184// ── Cascaded Shadow Map ─────────────────────────────────────────────────────
185
186/// Shadow mapping for directional lights using cascaded shadow maps.
187/// Splits the view frustum into 4 cascades for better shadow resolution distribution.
188#[derive(Debug, Clone)]
189pub struct CascadedShadowMap {
190    /// One shadow map per cascade (up to 4).
191    pub cascades: [ShadowMap; 4],
192    /// Number of active cascades (1..=4).
193    pub cascade_count: u32,
194    /// The view-projection matrix for each cascade.
195    pub cascade_vp: [Mat4; 4],
196    /// Split distances in view space.
197    pub split_distances: [f32; 5],
198    /// Resolution per cascade.
199    pub resolution: u32,
200    /// Whether to blend between cascades.
201    pub blend_cascades: bool,
202    /// Blend band width in normalized split space.
203    pub blend_band: f32,
204}
205
206impl CascadedShadowMap {
207    pub fn new(resolution: u32, cascade_count: u32) -> Self {
208        let count = cascade_count.clamp(1, 4);
209        Self {
210            cascades: [
211                ShadowMap::new(resolution, resolution),
212                ShadowMap::new(resolution, resolution),
213                ShadowMap::new(resolution, resolution),
214                ShadowMap::new(resolution, resolution),
215            ],
216            cascade_count: count,
217            cascade_vp: [Mat4::IDENTITY; 4],
218            split_distances: [0.1, 10.0, 30.0, 80.0, 200.0],
219            resolution,
220            blend_cascades: true,
221            blend_band: 0.1,
222        }
223    }
224
225    /// Update cascade splits using the given parameters.
226    pub fn update_splits(&mut self, params: &CascadeShadowParams) {
227        self.cascade_count = params.cascade_count.min(4);
228        self.split_distances = params.split_distances;
229        self.blend_band = params.blend_band;
230    }
231
232    /// Set the view-projection matrix for a specific cascade.
233    pub fn set_cascade_vp(&mut self, cascade: usize, vp: Mat4) {
234        if cascade < 4 {
235            self.cascade_vp[cascade] = vp;
236            self.cascades[cascade].view_projection = vp;
237        }
238    }
239
240    /// Compute cascade view-projection matrices from light direction and camera frustum.
241    pub fn compute_cascade_matrices(
242        &mut self,
243        light_dir: Vec3,
244        camera_frustum_slices: &[[Vec3; 8]; 4],
245        params: &CascadeShadowParams,
246    ) {
247        self.update_splits(params);
248        let count = self.cascade_count as usize;
249        for i in 0..count {
250            let vp = params.cascade_view_projection(light_dir, &camera_frustum_slices[i]);
251            self.set_cascade_vp(i, vp);
252        }
253    }
254
255    /// Clear all cascade depth buffers.
256    pub fn clear_all(&mut self) {
257        for i in 0..self.cascade_count as usize {
258            self.cascades[i].clear();
259        }
260    }
261
262    /// Determine which cascade a view-space depth falls into.
263    pub fn select_cascade(&self, view_depth: f32) -> usize {
264        for i in 0..self.cascade_count as usize {
265            if view_depth < self.split_distances[i + 1] {
266                return i;
267            }
268        }
269        (self.cascade_count as usize).saturating_sub(1)
270    }
271
272    /// Test if a point is in shadow, using the appropriate cascade.
273    pub fn is_in_shadow(&self, world_pos: Vec3, view_depth: f32, bias: &ShadowBias) -> bool {
274        let cascade = self.select_cascade(view_depth);
275        let effective_bias = bias.compute(0.0, 0.0); // Simplified — needs surface info
276        self.cascades[cascade].is_in_shadow(world_pos, effective_bias)
277    }
278
279    /// Compute shadow factor with cascade blending (0.0 = fully shadowed, 1.0 = fully lit).
280    pub fn shadow_factor(
281        &self,
282        world_pos: Vec3,
283        view_depth: f32,
284        bias: &ShadowBias,
285    ) -> f32 {
286        let cascade = self.select_cascade(view_depth);
287        let effective_bias = bias.compute(0.0, 0.0);
288        let in_shadow = self.cascades[cascade].is_in_shadow(world_pos, effective_bias);
289
290        let base_factor = if in_shadow { 0.0 } else { 1.0 };
291
292        if !self.blend_cascades || cascade + 1 >= self.cascade_count as usize {
293            return base_factor;
294        }
295
296        // Check if we're in the blend band
297        let split_near = self.split_distances[cascade + 1];
298        let blend_start = split_near * (1.0 - self.blend_band);
299        if view_depth > blend_start {
300            let blend_t = (view_depth - blend_start) / (split_near - blend_start);
301            let next_in_shadow = self.cascades[cascade + 1].is_in_shadow(world_pos, effective_bias);
302            let next_factor = if next_in_shadow { 0.0 } else { 1.0 };
303            base_factor * (1.0 - blend_t) + next_factor * blend_t
304        } else {
305            base_factor
306        }
307    }
308
309    /// Compute frustum slice corners for a cascade given camera near/far/projection.
310    pub fn compute_frustum_slice(
311        near: f32,
312        far: f32,
313        fov_y: f32,
314        aspect: f32,
315        camera_pos: Vec3,
316        camera_forward: Vec3,
317        camera_up: Vec3,
318    ) -> [Vec3; 8] {
319        let camera_right = camera_forward.cross(camera_up).normalize();
320        let corrected_up = camera_right.cross(camera_forward).normalize();
321
322        let near_h = (fov_y * 0.5).tan() * near;
323        let near_w = near_h * aspect;
324        let far_h = (fov_y * 0.5).tan() * far;
325        let far_w = far_h * aspect;
326
327        let near_center = camera_pos + camera_forward * near;
328        let far_center = camera_pos + camera_forward * far;
329
330        [
331            near_center + corrected_up * near_h - camera_right * near_w,
332            near_center + corrected_up * near_h + camera_right * near_w,
333            near_center - corrected_up * near_h + camera_right * near_w,
334            near_center - corrected_up * near_h - camera_right * near_w,
335            far_center + corrected_up * far_h - camera_right * far_w,
336            far_center + corrected_up * far_h + camera_right * far_w,
337            far_center - corrected_up * far_h + camera_right * far_w,
338            far_center - corrected_up * far_h - camera_right * far_w,
339        ]
340    }
341
342    /// Get total memory usage in bytes.
343    pub fn memory_bytes(&self) -> usize {
344        let count = self.cascade_count as usize;
345        (0..count).map(|i| self.cascades[i].memory_bytes()).sum()
346    }
347}
348
349// ── Omnidirectional Shadow Map ──────────────────────────────────────────────
350
351/// Shadow map for point lights using a 6-face cubemap layout.
352#[derive(Debug, Clone)]
353pub struct OmniShadowMap {
354    /// Six shadow map faces: +X, -X, +Y, -Y, +Z, -Z.
355    pub faces: [ShadowMap; 6],
356    /// The light's position.
357    pub light_position: Vec3,
358    /// The light's radius (far plane).
359    pub radius: f32,
360    /// Resolution per face.
361    pub resolution: u32,
362}
363
364/// The six cube faces.
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
366pub enum CubeFace {
367    PositiveX = 0,
368    NegativeX = 1,
369    PositiveY = 2,
370    NegativeY = 3,
371    PositiveZ = 4,
372    NegativeZ = 5,
373}
374
375impl CubeFace {
376    pub const ALL: [CubeFace; 6] = [
377        CubeFace::PositiveX,
378        CubeFace::NegativeX,
379        CubeFace::PositiveY,
380        CubeFace::NegativeY,
381        CubeFace::PositiveZ,
382        CubeFace::NegativeZ,
383    ];
384
385    /// Get the forward and up directions for this cube face.
386    pub fn directions(self) -> (Vec3, Vec3) {
387        match self {
388            CubeFace::PositiveX => (Vec3::new(1.0, 0.0, 0.0), Vec3::new(0.0, -1.0, 0.0)),
389            CubeFace::NegativeX => (Vec3::new(-1.0, 0.0, 0.0), Vec3::new(0.0, -1.0, 0.0)),
390            CubeFace::PositiveY => (Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0)),
391            CubeFace::NegativeY => (Vec3::new(0.0, -1.0, 0.0), Vec3::new(0.0, 0.0, -1.0)),
392            CubeFace::PositiveZ => (Vec3::new(0.0, 0.0, 1.0), Vec3::new(0.0, -1.0, 0.0)),
393            CubeFace::NegativeZ => (Vec3::new(0.0, 0.0, -1.0), Vec3::new(0.0, -1.0, 0.0)),
394        }
395    }
396}
397
398impl OmniShadowMap {
399    pub fn new(resolution: u32, light_position: Vec3, radius: f32) -> Self {
400        let mut osm = Self {
401            faces: [
402                ShadowMap::new(resolution, resolution),
403                ShadowMap::new(resolution, resolution),
404                ShadowMap::new(resolution, resolution),
405                ShadowMap::new(resolution, resolution),
406                ShadowMap::new(resolution, resolution),
407                ShadowMap::new(resolution, resolution),
408            ],
409            light_position,
410            radius,
411            resolution,
412        };
413        osm.update_matrices();
414        osm
415    }
416
417    /// Recompute the view-projection matrices for all six faces.
418    pub fn update_matrices(&mut self) {
419        let proj = Mat4::perspective(std::f32::consts::FRAC_PI_2, 1.0, 0.1, self.radius);
420        for face in CubeFace::ALL {
421            let (forward, up) = face.directions();
422            let target = self.light_position + forward;
423            let view = Mat4::look_at(self.light_position, target, up);
424            let vp = proj.mul_mat4(view);
425            self.faces[face as usize].view_projection = vp;
426            self.faces[face as usize].near = 0.1;
427            self.faces[face as usize].far = self.radius;
428        }
429    }
430
431    /// Update the light position and recompute matrices.
432    pub fn set_position(&mut self, pos: Vec3) {
433        self.light_position = pos;
434        self.update_matrices();
435    }
436
437    /// Clear all faces.
438    pub fn clear_all(&mut self) {
439        for face in &mut self.faces {
440            face.clear();
441        }
442    }
443
444    /// Determine which cube face a direction vector maps to.
445    pub fn select_face(direction: Vec3) -> CubeFace {
446        let abs = direction.abs();
447        if abs.x >= abs.y && abs.x >= abs.z {
448            if direction.x >= 0.0 { CubeFace::PositiveX } else { CubeFace::NegativeX }
449        } else if abs.y >= abs.x && abs.y >= abs.z {
450            if direction.y >= 0.0 { CubeFace::PositiveY } else { CubeFace::NegativeY }
451        } else if direction.z >= 0.0 {
452            CubeFace::PositiveZ
453        } else {
454            CubeFace::NegativeZ
455        }
456    }
457
458    /// Test if a world-space point is in shadow.
459    pub fn is_in_shadow(&self, world_pos: Vec3, bias: f32) -> bool {
460        let dir = world_pos - self.light_position;
461        let dist = dir.length();
462        if dist > self.radius {
463            return false;
464        }
465        let face = Self::select_face(dir);
466        self.faces[face as usize].is_in_shadow(world_pos, bias)
467    }
468
469    /// Compute shadow factor (0.0 = shadowed, 1.0 = lit) with PCF.
470    pub fn shadow_factor_pcf(&self, world_pos: Vec3, bias: f32, kernel: &PcfKernel) -> f32 {
471        let dir = world_pos - self.light_position;
472        let dist = dir.length();
473        if dist > self.radius {
474            return 1.0;
475        }
476        let face = Self::select_face(dir);
477        let shadow_map = &self.faces[face as usize];
478        kernel.sample(shadow_map, world_pos, bias)
479    }
480
481    /// Get total memory usage.
482    pub fn memory_bytes(&self) -> usize {
483        self.faces.iter().map(|f| f.memory_bytes()).sum()
484    }
485}
486
487// ── Shadow Atlas ────────────────────────────────────────────────────────────
488
489/// A region within the shadow atlas.
490#[derive(Debug, Clone, Copy)]
491pub struct ShadowAtlasRegion {
492    /// Top-left X in the atlas (in texels).
493    pub x: u32,
494    /// Top-left Y in the atlas (in texels).
495    pub y: u32,
496    /// Width of this region.
497    pub width: u32,
498    /// Height of this region.
499    pub height: u32,
500    /// Which light owns this region.
501    pub light_id: Option<LightId>,
502}
503
504impl ShadowAtlasRegion {
505    pub fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
506        Self {
507            x,
508            y,
509            width,
510            height,
511            light_id: None,
512        }
513    }
514
515    /// Convert atlas-space UV to region-space UV.
516    pub fn atlas_to_region_uv(&self, atlas_width: u32, atlas_height: u32, u: f32, v: f32) -> (f32, f32) {
517        let region_u = (u * atlas_width as f32 - self.x as f32) / self.width as f32;
518        let region_v = (v * atlas_height as f32 - self.y as f32) / self.height as f32;
519        (region_u, region_v)
520    }
521
522    /// Convert region-space UV to atlas-space UV.
523    pub fn region_to_atlas_uv(&self, atlas_width: u32, atlas_height: u32, u: f32, v: f32) -> (f32, f32) {
524        let atlas_u = (self.x as f32 + u * self.width as f32) / atlas_width as f32;
525        let atlas_v = (self.y as f32 + v * self.height as f32) / atlas_height as f32;
526        (atlas_u, atlas_v)
527    }
528
529    /// Check if a point (in texels) falls within this region.
530    pub fn contains(&self, px: u32, py: u32) -> bool {
531        px >= self.x && px < self.x + self.width && py >= self.y && py < self.y + self.height
532    }
533
534    /// Area in texels.
535    pub fn area(&self) -> u32 {
536        self.width * self.height
537    }
538}
539
540/// Packs multiple shadow maps into a single atlas texture.
541#[derive(Debug, Clone)]
542pub struct ShadowAtlas {
543    /// Total atlas width in texels.
544    pub width: u32,
545    /// Total atlas height in texels.
546    pub height: u32,
547    /// The atlas depth buffer.
548    pub depth_buffer: Vec<f32>,
549    /// Allocated regions.
550    pub regions: Vec<ShadowAtlasRegion>,
551    /// Free regions available for allocation (simple shelf packing).
552    free_shelves: Vec<AtlasShelf>,
553    /// Current shelf Y position.
554    current_shelf_y: u32,
555    /// Current shelf height.
556    current_shelf_height: u32,
557    /// Current X position on the active shelf.
558    current_shelf_x: u32,
559}
560
561#[derive(Debug, Clone)]
562struct AtlasShelf {
563    y: u32,
564    height: u32,
565    remaining_width: u32,
566    x_offset: u32,
567}
568
569impl ShadowAtlas {
570    pub fn new(width: u32, height: u32) -> Self {
571        let size = (width as usize) * (height as usize);
572        Self {
573            width,
574            height,
575            depth_buffer: vec![1.0; size],
576            regions: Vec::new(),
577            free_shelves: Vec::new(),
578            current_shelf_y: 0,
579            current_shelf_height: 0,
580            current_shelf_x: 0,
581        }
582    }
583
584    /// Clear the entire atlas depth buffer.
585    pub fn clear(&mut self) {
586        for d in self.depth_buffer.iter_mut() {
587            *d = 1.0;
588        }
589    }
590
591    /// Reset all allocations (but keep the depth buffer).
592    pub fn reset_allocations(&mut self) {
593        self.regions.clear();
594        self.free_shelves.clear();
595        self.current_shelf_y = 0;
596        self.current_shelf_height = 0;
597        self.current_shelf_x = 0;
598    }
599
600    /// Allocate a region of the given size. Returns the region index or None if full.
601    pub fn allocate(&mut self, width: u32, height: u32, light_id: LightId) -> Option<usize> {
602        // Try to fit on an existing shelf
603        for shelf in &mut self.free_shelves {
604            if height <= shelf.height && width <= shelf.remaining_width {
605                let region = ShadowAtlasRegion {
606                    x: shelf.x_offset,
607                    y: shelf.y,
608                    width,
609                    height,
610                    light_id: Some(light_id),
611                };
612                shelf.x_offset += width;
613                shelf.remaining_width -= width;
614                let idx = self.regions.len();
615                self.regions.push(region);
616                return Some(idx);
617            }
618        }
619
620        // Try to fit on the current shelf
621        if self.current_shelf_x + width <= self.width && height <= self.current_shelf_height {
622            let region = ShadowAtlasRegion {
623                x: self.current_shelf_x,
624                y: self.current_shelf_y,
625                width,
626                height,
627                light_id: Some(light_id),
628            };
629            self.current_shelf_x += width;
630            let idx = self.regions.len();
631            self.regions.push(region);
632            return Some(idx);
633        }
634
635        // Start a new shelf
636        if self.current_shelf_height > 0 {
637            // Save the current shelf as a free shelf if there's remaining width
638            let remaining = self.width - self.current_shelf_x;
639            if remaining > 0 {
640                self.free_shelves.push(AtlasShelf {
641                    y: self.current_shelf_y,
642                    height: self.current_shelf_height,
643                    remaining_width: remaining,
644                    x_offset: self.current_shelf_x,
645                });
646            }
647        }
648
649        let new_y = self.current_shelf_y + self.current_shelf_height;
650        if new_y + height > self.height || width > self.width {
651            return None; // Atlas is full
652        }
653
654        self.current_shelf_y = new_y;
655        self.current_shelf_height = height;
656        self.current_shelf_x = width;
657
658        let region = ShadowAtlasRegion {
659            x: 0,
660            y: new_y,
661            width,
662            height,
663            light_id: Some(light_id),
664        };
665        let idx = self.regions.len();
666        self.regions.push(region);
667        Some(idx)
668    }
669
670    /// Write depth at a position within a region.
671    pub fn write_depth_in_region(&mut self, region_idx: usize, local_x: u32, local_y: u32, depth: f32) {
672        if let Some(region) = self.regions.get(region_idx) {
673            let ax = region.x + local_x;
674            let ay = region.y + local_y;
675            if ax < self.width && ay < self.height {
676                let idx = (ay as usize) * (self.width as usize) + (ax as usize);
677                if depth < self.depth_buffer[idx] {
678                    self.depth_buffer[idx] = depth;
679                }
680            }
681        }
682    }
683
684    /// Read depth at a position within a region.
685    pub fn read_depth_in_region(&self, region_idx: usize, local_x: u32, local_y: u32) -> f32 {
686        if let Some(region) = self.regions.get(region_idx) {
687            let ax = region.x + local_x;
688            let ay = region.y + local_y;
689            if ax < self.width && ay < self.height {
690                return self.depth_buffer[(ay as usize) * (self.width as usize) + (ax as usize)];
691            }
692        }
693        1.0
694    }
695
696    /// Sample with bilinear filtering within a region at normalized UV.
697    pub fn sample_region_bilinear(&self, region_idx: usize, u: f32, v: f32) -> f32 {
698        let region = match self.regions.get(region_idx) {
699            Some(r) => r,
700            None => return 1.0,
701        };
702
703        let u = u.clamp(0.0, 1.0);
704        let v = v.clamp(0.0, 1.0);
705
706        let fx = u * (region.width as f32 - 1.0);
707        let fy = v * (region.height as f32 - 1.0);
708
709        let x0 = fx.floor() as u32;
710        let y0 = fy.floor() as u32;
711        let x1 = (x0 + 1).min(region.width - 1);
712        let y1 = (y0 + 1).min(region.height - 1);
713
714        let frac_x = fx - fx.floor();
715        let frac_y = fy - fy.floor();
716
717        let d00 = self.read_depth_in_region(region_idx, x0, y0);
718        let d10 = self.read_depth_in_region(region_idx, x1, y0);
719        let d01 = self.read_depth_in_region(region_idx, x0, y1);
720        let d11 = self.read_depth_in_region(region_idx, x1, y1);
721
722        let top = d00 + (d10 - d00) * frac_x;
723        let bottom = d01 + (d11 - d01) * frac_x;
724        top + (bottom - top) * frac_y
725    }
726
727    /// Get the number of allocated regions.
728    pub fn region_count(&self) -> usize {
729        self.regions.len()
730    }
731
732    /// Get memory usage in bytes.
733    pub fn memory_bytes(&self) -> usize {
734        self.depth_buffer.len() * 4
735    }
736
737    /// Get utilization as a fraction (0..1).
738    pub fn utilization(&self) -> f32 {
739        let total = (self.width as u64) * (self.height as u64);
740        if total == 0 {
741            return 0.0;
742        }
743        let used: u64 = self.regions.iter().map(|r| r.area() as u64).sum();
744        used as f32 / total as f32
745    }
746}
747
748// ── PCF Filtering ───────────────────────────────────────────────────────────
749
750/// Percentage-closer filtering kernel for soft shadows.
751#[derive(Debug, Clone)]
752pub struct PcfKernel {
753    /// Sample offsets (in texels).
754    pub offsets: Vec<(f32, f32)>,
755    /// Corresponding weights (should sum to 1.0).
756    pub weights: Vec<f32>,
757    /// Texel size for the shadow map.
758    pub texel_size: f32,
759}
760
761impl PcfKernel {
762    /// Create a 3x3 PCF kernel.
763    pub fn kernel_3x3(texel_size: f32) -> Self {
764        let mut offsets = Vec::with_capacity(9);
765        let mut weights = Vec::with_capacity(9);
766        for dy in -1..=1 {
767            for dx in -1..=1 {
768                offsets.push((dx as f32, dy as f32));
769                // Gaussian-like weights
770                let dist_sq = (dx * dx + dy * dy) as f32;
771                let w = (-dist_sq * 0.5).exp();
772                weights.push(w);
773            }
774        }
775        let sum: f32 = weights.iter().sum();
776        for w in weights.iter_mut() {
777            *w /= sum;
778        }
779        Self { offsets, weights, texel_size }
780    }
781
782    /// Create a 5x5 PCF kernel.
783    pub fn kernel_5x5(texel_size: f32) -> Self {
784        let mut offsets = Vec::with_capacity(25);
785        let mut weights = Vec::with_capacity(25);
786        for dy in -2..=2 {
787            for dx in -2..=2 {
788                offsets.push((dx as f32, dy as f32));
789                let dist_sq = (dx * dx + dy * dy) as f32;
790                let w = (-dist_sq * 0.25).exp();
791                weights.push(w);
792            }
793        }
794        let sum: f32 = weights.iter().sum();
795        for w in weights.iter_mut() {
796            *w /= sum;
797        }
798        Self { offsets, weights, texel_size }
799    }
800
801    /// Create a Poisson disk PCF kernel with the given number of samples.
802    pub fn poisson_disk(sample_count: usize, texel_size: f32) -> Self {
803        // Generate a deterministic Poisson-like disk
804        let mut offsets = Vec::with_capacity(sample_count);
805        let mut weights = Vec::with_capacity(sample_count);
806
807        let golden_angle = std::f32::consts::PI * (3.0 - 5.0_f32.sqrt());
808        for i in 0..sample_count {
809            let r = ((i as f32 + 0.5) / sample_count as f32).sqrt() * 2.0;
810            let theta = i as f32 * golden_angle;
811            offsets.push((r * theta.cos(), r * theta.sin()));
812            weights.push(1.0 / sample_count as f32);
813        }
814
815        Self { offsets, weights, texel_size }
816    }
817
818    /// Sample the shadow map with PCF filtering. Returns shadow factor (0.0..1.0).
819    pub fn sample(&self, shadow_map: &ShadowMap, world_pos: Vec3, bias: f32) -> f32 {
820        let (u, v, depth) = shadow_map.project_point(world_pos);
821        if u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 {
822            return 1.0; // Outside shadow map = lit
823        }
824
825        let mut shadow_sum = 0.0f32;
826        for (i, &(dx, dy)) in self.offsets.iter().enumerate() {
827            let su = u + dx * self.texel_size;
828            let sv = v + dy * self.texel_size;
829            let stored_depth = shadow_map.sample_bilinear(su, sv);
830            let lit = if depth - bias <= stored_depth { 1.0 } else { 0.0 };
831            shadow_sum += lit * self.weights[i];
832        }
833        shadow_sum
834    }
835
836    /// Sample from a shadow atlas region.
837    pub fn sample_atlas(
838        &self,
839        atlas: &ShadowAtlas,
840        region_idx: usize,
841        u: f32,
842        v: f32,
843        depth: f32,
844        bias: f32,
845    ) -> f32 {
846        let region = match atlas.regions.get(region_idx) {
847            Some(r) => r,
848            None => return 1.0,
849        };
850
851        let texel_u = self.texel_size / region.width as f32;
852        let texel_v = self.texel_size / region.height as f32;
853
854        let mut shadow_sum = 0.0f32;
855        for (i, &(dx, dy)) in self.offsets.iter().enumerate() {
856            let su = (u + dx * texel_u).clamp(0.0, 1.0);
857            let sv = (v + dy * texel_v).clamp(0.0, 1.0);
858            let stored_depth = atlas.sample_region_bilinear(region_idx, su, sv);
859            let lit = if depth - bias <= stored_depth { 1.0 } else { 0.0 };
860            shadow_sum += lit * self.weights[i];
861        }
862        shadow_sum
863    }
864}
865
866// ── Variance Shadow Map ─────────────────────────────────────────────────────
867
868/// Variance shadow map for soft shadows using statistical analysis.
869/// Stores depth and depth-squared moments for Chebyshev's inequality test.
870#[derive(Debug, Clone)]
871pub struct VarianceShadowMap {
872    pub width: u32,
873    pub height: u32,
874    /// First moment (mean depth).
875    pub moment1: Vec<f32>,
876    /// Second moment (mean depth squared).
877    pub moment2: Vec<f32>,
878    pub view_projection: Mat4,
879    /// Minimum variance to prevent light bleeding.
880    pub min_variance: f32,
881    /// Light bleed reduction factor (0..1).
882    pub light_bleed_reduction: f32,
883}
884
885impl VarianceShadowMap {
886    pub fn new(width: u32, height: u32) -> Self {
887        let size = (width as usize) * (height as usize);
888        Self {
889            width,
890            height,
891            moment1: vec![1.0; size],
892            moment2: vec![1.0; size],
893            view_projection: Mat4::IDENTITY,
894            min_variance: 0.00002,
895            light_bleed_reduction: 0.2,
896        }
897    }
898
899    /// Clear both moment buffers.
900    pub fn clear(&mut self) {
901        for v in self.moment1.iter_mut() {
902            *v = 1.0;
903        }
904        for v in self.moment2.iter_mut() {
905            *v = 1.0;
906        }
907    }
908
909    /// Write a depth sample to the variance map (accumulates moments).
910    pub fn write_depth(&mut self, x: u32, y: u32, depth: f32) {
911        if x < self.width && y < self.height {
912            let idx = (y as usize) * (self.width as usize) + (x as usize);
913            // In a real implementation, this would be done per-pixel during rendering.
914            // Here we just store the minimum depth and its square.
915            if depth < self.moment1[idx] {
916                self.moment1[idx] = depth;
917                self.moment2[idx] = depth * depth;
918            }
919        }
920    }
921
922    /// Sample the moments at normalized UV with bilinear filtering.
923    pub fn sample_moments(&self, u: f32, v: f32) -> (f32, f32) {
924        let u = u.clamp(0.0, 1.0);
925        let v = v.clamp(0.0, 1.0);
926
927        let fx = u * (self.width as f32 - 1.0);
928        let fy = v * (self.height as f32 - 1.0);
929
930        let x0 = fx.floor() as u32;
931        let y0 = fy.floor() as u32;
932        let x1 = (x0 + 1).min(self.width - 1);
933        let y1 = (y0 + 1).min(self.height - 1);
934
935        let frac_x = fx - fx.floor();
936        let frac_y = fy - fy.floor();
937
938        let read = |buf: &[f32], x: u32, y: u32| -> f32 {
939            buf[(y as usize) * (self.width as usize) + (x as usize)]
940        };
941
942        let m1_00 = read(&self.moment1, x0, y0);
943        let m1_10 = read(&self.moment1, x1, y0);
944        let m1_01 = read(&self.moment1, x0, y1);
945        let m1_11 = read(&self.moment1, x1, y1);
946
947        let m2_00 = read(&self.moment2, x0, y0);
948        let m2_10 = read(&self.moment2, x1, y0);
949        let m2_01 = read(&self.moment2, x0, y1);
950        let m2_11 = read(&self.moment2, x1, y1);
951
952        let m1_top = m1_00 + (m1_10 - m1_00) * frac_x;
953        let m1_bot = m1_01 + (m1_11 - m1_01) * frac_x;
954        let m1 = m1_top + (m1_bot - m1_top) * frac_y;
955
956        let m2_top = m2_00 + (m2_10 - m2_00) * frac_x;
957        let m2_bot = m2_01 + (m2_11 - m2_01) * frac_x;
958        let m2 = m2_top + (m2_bot - m2_top) * frac_y;
959
960        (m1, m2)
961    }
962
963    /// Compute the shadow factor using Chebyshev's inequality.
964    pub fn shadow_factor(&self, world_pos: Vec3) -> f32 {
965        let clip = self.view_projection.transform_point(world_pos);
966        let u = clip.x * 0.5 + 0.5;
967        let v = clip.y * 0.5 + 0.5;
968        let depth = clip.z * 0.5 + 0.5;
969
970        if u < 0.0 || u > 1.0 || v < 0.0 || v > 1.0 {
971            return 1.0;
972        }
973
974        let (mean, mean_sq) = self.sample_moments(u, v);
975
976        // If fragment is closer than the mean, it's fully lit
977        if depth <= mean {
978            return 1.0;
979        }
980
981        // Chebyshev's inequality
982        let variance = (mean_sq - mean * mean).max(self.min_variance);
983        let d = depth - mean;
984        let p_max = variance / (variance + d * d);
985
986        // Light bleed reduction
987        let reduced = ((p_max - self.light_bleed_reduction) / (1.0 - self.light_bleed_reduction)).max(0.0);
988        reduced
989    }
990
991    /// Apply a box blur to the moment buffers (for smoother shadows).
992    pub fn blur(&mut self, radius: u32) {
993        let w = self.width as usize;
994        let h = self.height as usize;
995
996        // Horizontal pass
997        let mut temp1 = vec![0.0f32; w * h];
998        let mut temp2 = vec![0.0f32; w * h];
999
1000        for y in 0..h {
1001            for x in 0..w {
1002                let mut sum1 = 0.0f32;
1003                let mut sum2 = 0.0f32;
1004                let mut count = 0.0f32;
1005
1006                let x_start = x.saturating_sub(radius as usize);
1007                let x_end = (x + radius as usize + 1).min(w);
1008
1009                for sx in x_start..x_end {
1010                    sum1 += self.moment1[y * w + sx];
1011                    sum2 += self.moment2[y * w + sx];
1012                    count += 1.0;
1013                }
1014                temp1[y * w + x] = sum1 / count;
1015                temp2[y * w + x] = sum2 / count;
1016            }
1017        }
1018
1019        // Vertical pass
1020        for y in 0..h {
1021            for x in 0..w {
1022                let mut sum1 = 0.0f32;
1023                let mut sum2 = 0.0f32;
1024                let mut count = 0.0f32;
1025
1026                let y_start = y.saturating_sub(radius as usize);
1027                let y_end = (y + radius as usize + 1).min(h);
1028
1029                for sy in y_start..y_end {
1030                    sum1 += temp1[sy * w + x];
1031                    sum2 += temp2[sy * w + x];
1032                    count += 1.0;
1033                }
1034                self.moment1[y * w + x] = sum1 / count;
1035                self.moment2[y * w + x] = sum2 / count;
1036            }
1037        }
1038    }
1039
1040    /// Get memory usage in bytes.
1041    pub fn memory_bytes(&self) -> usize {
1042        (self.moment1.len() + self.moment2.len()) * 4
1043    }
1044}
1045
1046// ── Shadow Bias ─────────────────────────────────────────────────────────────
1047
1048/// Configurable shadow bias combining constant, slope-scaled, and normal offset.
1049#[derive(Debug, Clone, Copy)]
1050pub struct ShadowBias {
1051    /// Constant depth bias (added directly to the depth comparison).
1052    pub constant: f32,
1053    /// Slope-scaled bias (multiplied by the depth slope).
1054    pub slope_scale: f32,
1055    /// Normal offset (offsets the shadow lookup along the surface normal).
1056    pub normal_offset: f32,
1057}
1058
1059impl Default for ShadowBias {
1060    fn default() -> Self {
1061        Self {
1062            constant: 0.005,
1063            slope_scale: 1.5,
1064            normal_offset: 0.02,
1065        }
1066    }
1067}
1068
1069impl ShadowBias {
1070    pub fn new(constant: f32, slope_scale: f32, normal_offset: f32) -> Self {
1071        Self { constant, slope_scale, normal_offset }
1072    }
1073
1074    /// Compute the effective bias given the depth slope and surface angle.
1075    pub fn compute(&self, depth_slope: f32, _cos_angle: f32) -> f32 {
1076        self.constant + self.slope_scale * depth_slope
1077    }
1078
1079    /// Compute the world-space offset along the surface normal.
1080    pub fn normal_offset_vec(&self, normal: Vec3) -> Vec3 {
1081        normal * self.normal_offset
1082    }
1083
1084    /// Apply normal offset to a world position before shadow lookup.
1085    pub fn apply_normal_offset(&self, position: Vec3, normal: Vec3) -> Vec3 {
1086        position + self.normal_offset_vec(normal)
1087    }
1088}
1089
1090// ── Shadow Config ───────────────────────────────────────────────────────────
1091
1092/// Global shadow configuration.
1093#[derive(Debug, Clone)]
1094pub struct ShadowConfig {
1095    /// Maximum shadow distance from the camera.
1096    pub max_distance: f32,
1097    /// Distance at which shadows start fading out.
1098    pub fade_start: f32,
1099    /// Atlas resolution (width and height).
1100    pub atlas_resolution: u32,
1101    /// Default shadow map resolution per light.
1102    pub default_resolution: u32,
1103    /// Default bias settings.
1104    pub bias: ShadowBias,
1105    /// PCF kernel to use (3x3 or 5x5).
1106    pub pcf_mode: PcfMode,
1107    /// Whether to use variance shadow maps.
1108    pub use_vsm: bool,
1109    /// Maximum number of shadow-casting lights.
1110    pub max_shadow_casters: u32,
1111    /// Whether to enable shadow caster culling.
1112    pub cull_shadow_casters: bool,
1113}
1114
1115/// PCF filter mode selection.
1116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1117pub enum PcfMode {
1118    None,
1119    Pcf3x3,
1120    Pcf5x5,
1121    PoissonDisk16,
1122}
1123
1124impl Default for ShadowConfig {
1125    fn default() -> Self {
1126        Self {
1127            max_distance: 200.0,
1128            fade_start: 150.0,
1129            atlas_resolution: 4096,
1130            default_resolution: 1024,
1131            bias: ShadowBias::default(),
1132            pcf_mode: PcfMode::Pcf3x3,
1133            use_vsm: false,
1134            max_shadow_casters: 16,
1135            cull_shadow_casters: true,
1136        }
1137    }
1138}
1139
1140impl ShadowConfig {
1141    /// Compute shadow distance fade factor (1.0 = full shadow, 0.0 = faded).
1142    pub fn distance_fade(&self, distance: f32) -> f32 {
1143        if distance >= self.max_distance {
1144            return 0.0;
1145        }
1146        if distance <= self.fade_start {
1147            return 1.0;
1148        }
1149        let range = self.max_distance - self.fade_start;
1150        if range <= 0.0 {
1151            return 0.0;
1152        }
1153        1.0 - (distance - self.fade_start) / range
1154    }
1155
1156    /// Create a PCF kernel based on the current mode.
1157    pub fn create_pcf_kernel(&self) -> PcfKernel {
1158        let texel_size = 1.0 / self.default_resolution as f32;
1159        match self.pcf_mode {
1160            PcfMode::None => PcfKernel {
1161                offsets: vec![(0.0, 0.0)],
1162                weights: vec![1.0],
1163                texel_size,
1164            },
1165            PcfMode::Pcf3x3 => PcfKernel::kernel_3x3(texel_size),
1166            PcfMode::Pcf5x5 => PcfKernel::kernel_5x5(texel_size),
1167            PcfMode::PoissonDisk16 => PcfKernel::poisson_disk(16, texel_size),
1168        }
1169    }
1170}
1171
1172// ── Shadow Caster Culling ───────────────────────────────────────────────────
1173
1174/// AABB for culling shadow casters.
1175#[derive(Debug, Clone, Copy)]
1176pub struct CasterBounds {
1177    pub min: Vec3,
1178    pub max: Vec3,
1179}
1180
1181impl CasterBounds {
1182    pub fn new(min: Vec3, max: Vec3) -> Self {
1183        Self { min, max }
1184    }
1185
1186    /// Test if this AABB intersects a frustum (simplified 6-plane test).
1187    pub fn intersects_frustum(&self, frustum_planes: &[(Vec3, f32); 6]) -> bool {
1188        for &(normal, dist) in frustum_planes {
1189            let p = Vec3::new(
1190                if normal.x >= 0.0 { self.max.x } else { self.min.x },
1191                if normal.y >= 0.0 { self.max.y } else { self.min.y },
1192                if normal.z >= 0.0 { self.max.z } else { self.min.z },
1193            );
1194            if normal.dot(p) + dist < 0.0 {
1195                return false;
1196            }
1197        }
1198        true
1199    }
1200
1201    /// Test if this AABB is within a sphere (for point light culling).
1202    pub fn intersects_sphere(&self, center: Vec3, radius: f32) -> bool {
1203        let mut dist_sq = 0.0f32;
1204
1205        if center.x < self.min.x {
1206            let d = self.min.x - center.x;
1207            dist_sq += d * d;
1208        } else if center.x > self.max.x {
1209            let d = center.x - self.max.x;
1210            dist_sq += d * d;
1211        }
1212
1213        if center.y < self.min.y {
1214            let d = self.min.y - center.y;
1215            dist_sq += d * d;
1216        } else if center.y > self.max.y {
1217            let d = center.y - self.max.y;
1218            dist_sq += d * d;
1219        }
1220
1221        if center.z < self.min.z {
1222            let d = self.min.z - center.z;
1223            dist_sq += d * d;
1224        } else if center.z > self.max.z {
1225            let d = center.z - self.max.z;
1226            dist_sq += d * d;
1227        }
1228
1229        dist_sq <= radius * radius
1230    }
1231}
1232
1233/// Cull shadow casters for a specific light.
1234pub fn cull_shadow_casters(
1235    casters: &[CasterBounds],
1236    light: &Light,
1237) -> Vec<usize> {
1238    let mut visible = Vec::new();
1239
1240    match light {
1241        Light::Point(pl) => {
1242            for (i, caster) in casters.iter().enumerate() {
1243                if caster.intersects_sphere(pl.position, pl.radius) {
1244                    visible.push(i);
1245                }
1246            }
1247        }
1248        Light::Spot(sl) => {
1249            // Simplified: use sphere intersection with the spot's bounding sphere
1250            for (i, caster) in casters.iter().enumerate() {
1251                if caster.intersects_sphere(sl.position, sl.radius) {
1252                    visible.push(i);
1253                }
1254            }
1255        }
1256        Light::Directional(_) => {
1257            // Directional lights can't cull by position easily;
1258            // cull by the cascade frustum in a real implementation.
1259            // Here we just include all casters.
1260            for i in 0..casters.len() {
1261                visible.push(i);
1262            }
1263        }
1264        _ => {
1265            // Non-shadow-casting lights don't need culling
1266        }
1267    }
1268
1269    visible
1270}
1271
1272// ── Shadow System ───────────────────────────────────────────────────────────
1273
1274/// Top-level shadow system that orchestrates shadow map allocation and rendering.
1275#[derive(Debug)]
1276pub struct ShadowSystem {
1277    pub config: ShadowConfig,
1278    pub atlas: ShadowAtlas,
1279    pub cascaded_maps: HashMap<LightId, CascadedShadowMap>,
1280    pub omni_maps: HashMap<LightId, OmniShadowMap>,
1281    pub spot_maps: HashMap<LightId, usize>, // region index in atlas
1282    pub pcf_kernel: PcfKernel,
1283    pub vsm_maps: HashMap<LightId, VarianceShadowMap>,
1284    /// Stats from the last frame.
1285    pub stats: ShadowStats,
1286}
1287
1288/// Statistics for shadow rendering.
1289#[derive(Debug, Clone, Default)]
1290pub struct ShadowStats {
1291    pub shadow_casters: u32,
1292    pub cascaded_maps: u32,
1293    pub omni_maps: u32,
1294    pub spot_maps: u32,
1295    pub atlas_utilization: f32,
1296    pub total_memory_bytes: usize,
1297}
1298
1299impl ShadowSystem {
1300    pub fn new(config: ShadowConfig) -> Self {
1301        let pcf_kernel = config.create_pcf_kernel();
1302        let atlas_res = config.atlas_resolution;
1303        Self {
1304            config,
1305            atlas: ShadowAtlas::new(atlas_res, atlas_res),
1306            cascaded_maps: HashMap::new(),
1307            omni_maps: HashMap::new(),
1308            spot_maps: HashMap::new(),
1309            pcf_kernel,
1310            vsm_maps: HashMap::new(),
1311            stats: ShadowStats::default(),
1312        }
1313    }
1314
1315    /// Allocate shadow maps for the given shadow-casting lights.
1316    pub fn allocate_for_lights(&mut self, lights: &[(LightId, &Light)]) {
1317        self.atlas.reset_allocations();
1318        self.atlas.clear();
1319        self.cascaded_maps.clear();
1320        self.omni_maps.clear();
1321        self.spot_maps.clear();
1322        self.vsm_maps.clear();
1323
1324        let mut caster_count = 0u32;
1325
1326        for &(id, light) in lights {
1327            if caster_count >= self.config.max_shadow_casters {
1328                break;
1329            }
1330            if !light.is_enabled() || !light.casts_shadows() {
1331                continue;
1332            }
1333
1334            match light {
1335                Light::Directional(dl) => {
1336                    let csm = CascadedShadowMap::new(
1337                        dl.cascade_params.resolution,
1338                        dl.cascade_params.cascade_count,
1339                    );
1340                    self.cascaded_maps.insert(id, csm);
1341                    caster_count += 1;
1342                }
1343                Light::Point(pl) => {
1344                    let res = self.config.default_resolution.min(512);
1345                    let osm = OmniShadowMap::new(res, pl.position, pl.radius);
1346                    self.omni_maps.insert(id, osm);
1347                    caster_count += 1;
1348                }
1349                Light::Spot(_) => {
1350                    let res = self.config.default_resolution;
1351                    if let Some(region_idx) = self.atlas.allocate(res, res, id) {
1352                        self.spot_maps.insert(id, region_idx);
1353                    }
1354                    caster_count += 1;
1355                }
1356                _ => {}
1357            }
1358
1359            // Optionally create VSM
1360            if self.config.use_vsm {
1361                let res = self.config.default_resolution;
1362                let vsm = VarianceShadowMap::new(res, res);
1363                self.vsm_maps.insert(id, vsm);
1364            }
1365        }
1366
1367        self.update_stats();
1368    }
1369
1370    /// Compute the shadow factor for a world point from a specific light.
1371    pub fn shadow_factor(
1372        &self,
1373        light_id: LightId,
1374        world_pos: Vec3,
1375        normal: Vec3,
1376        view_depth: f32,
1377    ) -> f32 {
1378        // Distance fade
1379        let fade = self.config.distance_fade(view_depth);
1380        if fade <= 0.0 {
1381            return 1.0;
1382        }
1383
1384        let biased_pos = self.config.bias.apply_normal_offset(world_pos, normal);
1385        let effective_bias = self.config.bias.compute(0.0, 0.0);
1386
1387        // Check VSM first
1388        if self.config.use_vsm {
1389            if let Some(vsm) = self.vsm_maps.get(&light_id) {
1390                let factor = vsm.shadow_factor(biased_pos);
1391                return 1.0 - (1.0 - factor) * fade;
1392            }
1393        }
1394
1395        // Check cascaded shadow map
1396        if let Some(csm) = self.cascaded_maps.get(&light_id) {
1397            let factor = csm.shadow_factor(biased_pos, view_depth, &self.config.bias);
1398            return 1.0 - (1.0 - factor) * fade;
1399        }
1400
1401        // Check omni shadow map
1402        if let Some(osm) = self.omni_maps.get(&light_id) {
1403            let factor = osm.shadow_factor_pcf(biased_pos, effective_bias, &self.pcf_kernel);
1404            return 1.0 - (1.0 - factor) * fade;
1405        }
1406
1407        // Check spot shadow map in atlas
1408        if let Some(&region_idx) = self.spot_maps.get(&light_id) {
1409            if let Some(region) = self.atlas.regions.get(region_idx) {
1410                let _ = region; // Would project using the spot light's VP matrix
1411                // Simplified: just return lit
1412                return 1.0;
1413            }
1414        }
1415
1416        1.0 // No shadow map = fully lit
1417    }
1418
1419    /// Compute combined shadow factor from all shadow-casting lights at a point.
1420    pub fn combined_shadow_factor(
1421        &self,
1422        world_pos: Vec3,
1423        normal: Vec3,
1424        view_depth: f32,
1425    ) -> f32 {
1426        let mut min_factor = 1.0f32;
1427
1428        for &id in self.cascaded_maps.keys() {
1429            let f = self.shadow_factor(id, world_pos, normal, view_depth);
1430            min_factor = min_factor.min(f);
1431        }
1432        for &id in self.omni_maps.keys() {
1433            let f = self.shadow_factor(id, world_pos, normal, view_depth);
1434            min_factor = min_factor.min(f);
1435        }
1436        for &id in self.spot_maps.keys() {
1437            let f = self.shadow_factor(id, world_pos, normal, view_depth);
1438            min_factor = min_factor.min(f);
1439        }
1440
1441        min_factor
1442    }
1443
1444    fn update_stats(&mut self) {
1445        let mut total_mem = self.atlas.memory_bytes();
1446        for csm in self.cascaded_maps.values() {
1447            total_mem += csm.memory_bytes();
1448        }
1449        for osm in self.omni_maps.values() {
1450            total_mem += osm.memory_bytes();
1451        }
1452        for vsm in self.vsm_maps.values() {
1453            total_mem += vsm.memory_bytes();
1454        }
1455
1456        self.stats = ShadowStats {
1457            shadow_casters: (self.cascaded_maps.len() + self.omni_maps.len() + self.spot_maps.len()) as u32,
1458            cascaded_maps: self.cascaded_maps.len() as u32,
1459            omni_maps: self.omni_maps.len() as u32,
1460            spot_maps: self.spot_maps.len() as u32,
1461            atlas_utilization: self.atlas.utilization(),
1462            total_memory_bytes: total_mem,
1463        };
1464    }
1465
1466    /// Get a reference to the current stats.
1467    pub fn stats(&self) -> &ShadowStats {
1468        &self.stats
1469    }
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474    use super::*;
1475
1476    #[test]
1477    fn test_shadow_map_depth() {
1478        let mut sm = ShadowMap::new(64, 64);
1479        sm.write_depth(10, 10, 0.5);
1480        assert!((sm.read_depth(10, 10) - 0.5).abs() < 1e-5);
1481        // Closer depth should overwrite
1482        sm.write_depth(10, 10, 0.3);
1483        assert!((sm.read_depth(10, 10) - 0.3).abs() < 1e-5);
1484        // Farther depth should not overwrite
1485        sm.write_depth(10, 10, 0.8);
1486        assert!((sm.read_depth(10, 10) - 0.3).abs() < 1e-5);
1487    }
1488
1489    #[test]
1490    fn test_shadow_map_clear() {
1491        let mut sm = ShadowMap::new(16, 16);
1492        sm.write_depth(5, 5, 0.2);
1493        sm.clear();
1494        assert!((sm.read_depth(5, 5) - 1.0).abs() < 1e-5);
1495    }
1496
1497    #[test]
1498    fn test_cascaded_cascade_selection() {
1499        let csm = CascadedShadowMap::new(512, 4);
1500        assert_eq!(csm.select_cascade(5.0), 0);
1501        assert_eq!(csm.select_cascade(20.0), 1);
1502        assert_eq!(csm.select_cascade(50.0), 2);
1503        assert_eq!(csm.select_cascade(100.0), 3);
1504    }
1505
1506    #[test]
1507    fn test_omni_face_selection() {
1508        assert_eq!(
1509            OmniShadowMap::select_face(Vec3::new(1.0, 0.0, 0.0)),
1510            CubeFace::PositiveX
1511        );
1512        assert_eq!(
1513            OmniShadowMap::select_face(Vec3::new(0.0, -1.0, 0.0)),
1514            CubeFace::NegativeY
1515        );
1516        assert_eq!(
1517            OmniShadowMap::select_face(Vec3::new(0.0, 0.0, -1.0)),
1518            CubeFace::NegativeZ
1519        );
1520    }
1521
1522    #[test]
1523    fn test_shadow_atlas_allocation() {
1524        let mut atlas = ShadowAtlas::new(2048, 2048);
1525        let id1 = LightId(1);
1526        let id2 = LightId(2);
1527
1528        let r1 = atlas.allocate(512, 512, id1);
1529        assert!(r1.is_some());
1530
1531        let r2 = atlas.allocate(512, 512, id2);
1532        assert!(r2.is_some());
1533
1534        assert_eq!(atlas.region_count(), 2);
1535        assert!(atlas.utilization() > 0.0);
1536    }
1537
1538    #[test]
1539    fn test_pcf_kernel_weights() {
1540        let kernel = PcfKernel::kernel_3x3(1.0 / 512.0);
1541        let sum: f32 = kernel.weights.iter().sum();
1542        assert!((sum - 1.0).abs() < 1e-4);
1543        assert_eq!(kernel.offsets.len(), 9);
1544    }
1545
1546    #[test]
1547    fn test_vsm_shadow_factor() {
1548        let mut vsm = VarianceShadowMap::new(64, 64);
1549        vsm.view_projection = Mat4::IDENTITY;
1550        // All cleared to 1.0, so everything should be lit
1551        let factor = vsm.shadow_factor(Vec3::new(0.0, 0.0, 0.5));
1552        assert!(factor > 0.0);
1553    }
1554
1555    #[test]
1556    fn test_shadow_bias() {
1557        let bias = ShadowBias::new(0.005, 2.0, 0.03);
1558        let computed = bias.compute(0.01, 0.8);
1559        assert!(computed > 0.005); // Should be constant + slope contribution
1560    }
1561
1562    #[test]
1563    fn test_caster_bounds_sphere() {
1564        let bounds = CasterBounds::new(
1565            Vec3::new(-1.0, -1.0, -1.0),
1566            Vec3::new(1.0, 1.0, 1.0),
1567        );
1568        assert!(bounds.intersects_sphere(Vec3::ZERO, 2.0));
1569        assert!(!bounds.intersects_sphere(Vec3::new(10.0, 10.0, 10.0), 1.0));
1570    }
1571
1572    #[test]
1573    fn test_shadow_distance_fade() {
1574        let config = ShadowConfig {
1575            max_distance: 200.0,
1576            fade_start: 150.0,
1577            ..Default::default()
1578        };
1579        assert!((config.distance_fade(100.0) - 1.0).abs() < 1e-5);
1580        assert!((config.distance_fade(200.0)).abs() < 1e-5);
1581        assert!(config.distance_fade(175.0) > 0.0 && config.distance_fade(175.0) < 1.0);
1582    }
1583}