Skip to main content

cvkg_render_gpu/passes/
shadow.rs

1//! Shadow pass types and KvasirNode — renders depth-only shadow map from
2//! light's perspective using 3D mesh data.
3
4use crate::kvasir::nodes::PassId;
5use crate::kvasir::{ExecutionContext, KvasirNode, ResourceId};
6use glam::Mat4;
7use wgpu::Buffer;
8
9/// Directional light for shadow rendering.
10#[derive(Debug, Clone, Copy)]
11pub struct DirectionalLight {
12    pub direction: glam::Vec3,
13    pub color: glam::Vec3,
14    pub intensity: f32,
15}
16
17impl Default for DirectionalLight {
18    fn default() -> Self {
19        Self {
20            direction: glam::Vec3::new(0.0, -1.0, 0.0),
21            color: glam::Vec3::ONE,
22            intensity: 1.0,
23        }
24    }
25}
26
27/// Point light source for omnidirectional shadow rendering.
28#[derive(Debug, Clone, Copy)]
29pub struct PointLight {
30    pub position: glam::Vec3,
31    pub color: glam::Vec3,
32    pub intensity: f32,
33    pub range: f32,
34}
35
36/// Spot light source with conical beam bounds.
37#[derive(Debug, Clone, Copy)]
38pub struct SpotLight {
39    pub position: glam::Vec3,
40    pub direction: glam::Vec3,
41    pub color: glam::Vec3,
42    pub intensity: f32,
43    pub range: f32,
44    pub inner_cone_angle: f32,
45    pub outer_cone_angle: f32,
46}
47
48/// Shadow atlas layout configuration managing viewport division.
49#[derive(Debug, Clone)]
50pub struct ShadowAtlasConfig {
51    /// Total width and height of the combined shadow atlas texture.
52    pub size: u32,
53    /// Viewport padding in pixels.
54    pub padding: u32,
55}
56
57
58/// GPU resources for a single 3D mesh instance ready for rendering.
59#[derive(Debug, Clone)]
60pub struct GpuMesh3d {
61    /// Vertex buffer (position, normal, UV, etc.).
62    pub vertex_buffer: Buffer,
63    /// Index buffer.
64    pub index_buffer: Buffer,
65    /// Number of indices to draw.
66    pub index_count: u32,
67    /// Per-instance model matrix.
68    pub transform: Mat4,
69    /// View depth for transparent sorting (world-space distance from camera).
70    /// Used by TransparentNode for back-to-front rendering.
71    pub view_depth: f32,
72    /// Index of this instance in the 3D instance buffer.
73    pub instance_index: u32,
74    /// Skinned vertex buffer if this mesh undergoes skeletal compute skinning.
75    pub skinned_buffer: Option<wgpu::Buffer>,
76}
77
78/// Shadow pass node — renders depth-only shadow map from light's perspective.
79pub struct ShadowNode {
80    pub light: DirectionalLight,
81    pub shadow_map: ResourceId,
82    /// GPU-ready mesh instances to render into the shadow map.
83    pub mesh_instances: Vec<GpuMesh3d>,
84    /// Cascade splits for CSM.
85    pub cascade_splits: [f32; 4],
86    /// Camera's view projection matrix.
87    pub camera_view_proj: Mat4,
88}
89
90impl KvasirNode for ShadowNode {
91    fn label(&self) -> &'static str {
92        "ShadowPass"
93    }
94
95    fn inputs(&self) -> &[ResourceId] {
96        &[]
97    }
98
99    fn outputs(&self) -> &[ResourceId] {
100        std::slice::from_ref(&self.shadow_map)
101    }
102
103    fn pass_id(&self) -> PassId {
104        PassId::Shadow
105    }
106
107    fn execute(&self, ctx: &mut ExecutionContext) {
108        let light_dir = self.light.direction.normalize();
109
110        // 1. Compute 4 cascades VP matrices
111        let inv_cam_vp = self.camera_view_proj.inverse();
112        let ndc_ranges = [
113            (0.0f32, 0.08f32),
114            (0.08f32, 0.22f32),
115            (0.22f32, 0.55f32),
116            (0.55f32, 1.0f32),
117        ];
118
119        let mut cascade_vps = [glam::Mat4::IDENTITY; 4];
120        for i in 0..4 {
121            let (near_ndc, far_ndc) = ndc_ranges[i];
122            let ndc_corners = [
123                glam::Vec3::new(-1.0, -1.0, near_ndc),
124                glam::Vec3::new(1.0, -1.0, near_ndc),
125                glam::Vec3::new(-1.0, 1.0, near_ndc),
126                glam::Vec3::new(1.0, 1.0, near_ndc),
127                glam::Vec3::new(-1.0, -1.0, far_ndc),
128                glam::Vec3::new(1.0, -1.0, far_ndc),
129                glam::Vec3::new(-1.0, 1.0, far_ndc),
130                glam::Vec3::new(1.0, 1.0, far_ndc),
131            ];
132
133            let mut world_corners = [glam::Vec3::ZERO; 8];
134            let mut center = glam::Vec3::ZERO;
135            for j in 0..8 {
136                let p = inv_cam_vp.project_point3(ndc_corners[j]);
137                world_corners[j] = p;
138                center += p;
139            }
140            center /= 8.0;
141
142            let mut radius = 0.0f32;
143            for corner in &world_corners {
144                radius = radius.max(corner.distance(center));
145            }
146
147            // Snap radius to prevent shimmering
148            radius = (radius * 16.0).round() / 16.0;
149
150            let light_pos = center - light_dir * radius * 2.0;
151            let light_view = glam::Mat4::look_at_lh(light_pos, center, glam::Vec3::Y);
152            let light_proj =
153                glam::Mat4::orthographic_lh(-radius, radius, -radius, radius, 0.0, radius * 4.0);
154
155            cascade_vps[i] = light_proj * light_view;
156        }
157
158        // 2. Update CSM buffer with the new cascade splits/VPs
159        let csm = cvkg_core::render_tier::CsmUniforms {
160            cascade_vps,
161            cascade_splits: [
162                self.cascade_splits[0],
163                self.cascade_splits[1],
164                self.cascade_splits[2],
165                self.cascade_splits[3],
166            ],
167            _pad: [0.0; 4],
168        };
169        ctx.queue
170            .write_buffer(&ctx.renderer.csm_buffer, 0, bytemuck::bytes_of(&csm));
171
172        let shadow_texture = match &ctx.renderer.shadow_map_texture {
173            Some(t) => t,
174            None => {
175                tracing::error!("ShadowNode: renderer missing shadow_map_texture");
176                return;
177            }
178        };
179
180        let shadow_view = shadow_texture.create_view(&wgpu::TextureViewDescriptor::default());
181
182        // Create a single depth-only render pass covering the entire shadow atlas
183        let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
184            label: Some("Shadow Atlas Pass (Depth-Only)"),
185            color_attachments: &[], // No color output.
186            depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
187                view: &shadow_view,
188                depth_ops: Some(wgpu::Operations {
189                    load: wgpu::LoadOp::Clear(1.0),
190                    store: wgpu::StoreOp::Store,
191                }),
192                stencil_ops: None,
193            }),
194            timestamp_writes: None,
195            occlusion_query_set: None,
196            multiview_mask: None,
197        });
198
199        let atlas_size = 1024.0f32; // Matches the shadow map texture dimensions
200        let half_size = atlas_size * 0.5;
201
202        // Render each cascade into its viewport quadrant in the atlas
203        for (i, vp) in cascade_vps.iter().enumerate() {
204            // Write cascade_vps[i] into scene_buffer's light_vp field (offset 320)
205            ctx.queue
206                .write_buffer(&ctx.renderer.scene_buffer, 320, bytemuck::bytes_of(vp));
207
208            let col = (i % 2) as f32;
209            let row = (i / 2) as f32;
210            
211            pass.set_viewport(
212                col * half_size,
213                row * half_size,
214                half_size,
215                half_size,
216                0.0,
217                1.0,
218            );
219
220            // Bind the shadow pipeline and scene uniforms.
221            pass.set_pipeline(&ctx.renderer.shadow_pipeline);
222            pass.set_bind_group(1, &ctx.renderer.berserker_bind_group, &[]);
223
224            // For each mesh, set vertex/index buffers and draw depth only.
225            for mesh in self.mesh_instances.iter() {
226                if let Some(skinned) = &mesh.skinned_buffer {
227                    pass.set_vertex_buffer(0, skinned.slice(..));
228                } else {
229                    pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
230                }
231                pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
232                pass.draw_indexed(0..mesh.index_count, 0, 0..1);
233            }
234        }
235    }
236}