1use crate::kvasir::nodes::PassId;
5use crate::kvasir::{ExecutionContext, KvasirNode, ResourceId};
6use glam::Mat4;
7use wgpu::Buffer;
8
9#[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#[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#[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#[derive(Debug, Clone)]
50pub struct ShadowAtlasConfig {
51 pub size: u32,
53 pub padding: u32,
55}
56
57
58#[derive(Debug, Clone)]
60pub struct GpuMesh3d {
61 pub vertex_buffer: Buffer,
63 pub index_buffer: Buffer,
65 pub index_count: u32,
67 pub transform: Mat4,
69 pub view_depth: f32,
72 pub instance_index: u32,
74 pub skinned_buffer: Option<wgpu::Buffer>,
76}
77
78pub struct ShadowNode {
80 pub light: DirectionalLight,
81 pub shadow_map: ResourceId,
82 pub mesh_instances: Vec<GpuMesh3d>,
84 pub cascade_splits: [f32; 4],
86 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 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 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 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 let mut pass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
184 label: Some("Shadow Atlas Pass (Depth-Only)"),
185 color_attachments: &[], 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; let half_size = atlas_size * 0.5;
201
202 for (i, vp) in cascade_vps.iter().enumerate() {
204 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 pass.set_pipeline(&ctx.renderer.shadow_pipeline);
222 pass.set_bind_group(1, &ctx.renderer.berserker_bind_group, &[]);
223
224 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}