feat: add shadow demo with directional light shadow mapping and 3x3 PCF

- Add Mat4::orthographic() to voltex_math for light projection
- Fix pbr_demo and multi_light_demo to provide shadow bind group (group 3)
  required by updated PBR pipeline (dummy shadow with size=0 disables it)
- Create shadow_demo with two-pass rendering: shadow depth pass using
  orthographic light projection, then PBR color pass with shadow sampling
- Scene: ground plane, 3 spheres, 2 cubes with directional light

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 21:09:27 +09:00
parent 8f962368e9
commit 5f962f376e
7 changed files with 726 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ use voltex_platform::{VoltexWindow, WindowConfig, InputState, GameTimer};
use voltex_renderer::{
GpuContext, Camera, FpsController, CameraUniform, LightsUniform, LightData,
Mesh, GpuTexture, MaterialUniform, generate_sphere, create_pbr_pipeline, obj,
ShadowMap, ShadowUniform,
};
use wgpu::util::DeviceExt;
@@ -33,6 +34,8 @@ struct AppState {
camera_light_bind_group: wgpu::BindGroup,
_texture: GpuTexture,
material_bind_group: wgpu::BindGroup,
shadow_bind_group: wgpu::BindGroup,
_shadow_map: ShadowMap,
input: InputState,
timer: GameTimer,
cam_aligned_size: u32,
@@ -186,6 +189,26 @@ impl ApplicationHandler for MultiLightApp {
}],
});
// Shadow resources (dummy — shadows disabled)
let shadow_map = ShadowMap::new(&gpu.device);
let shadow_layout = ShadowMap::bind_group_layout(&gpu.device);
let shadow_uniform = ShadowUniform {
light_view_proj: [[0.0; 4]; 4],
shadow_map_size: 0.0,
shadow_bias: 0.0,
_padding: [0.0; 2],
};
let shadow_uniform_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Shadow Uniform Buffer"),
contents: bytemuck::cast_slice(&[shadow_uniform]),
usage: wgpu::BufferUsages::UNIFORM,
});
let shadow_bind_group = shadow_map.create_bind_group(
&gpu.device,
&shadow_layout,
&shadow_uniform_buffer,
);
// PBR pipeline
let pipeline = create_pbr_pipeline(
&gpu.device,
@@ -193,6 +216,7 @@ impl ApplicationHandler for MultiLightApp {
&cl_layout,
&tex_layout,
&mat_layout,
&shadow_layout,
);
self.state = Some(AppState {
@@ -209,6 +233,8 @@ impl ApplicationHandler for MultiLightApp {
camera_light_bind_group,
_texture: texture,
material_bind_group,
shadow_bind_group,
_shadow_map: shadow_map,
input: InputState::new(),
timer: GameTimer::new(60),
cam_aligned_size,
@@ -493,6 +519,7 @@ impl ApplicationHandler for MultiLightApp {
render_pass.set_pipeline(&state.pipeline);
render_pass.set_bind_group(1, &state._texture.bind_group, &[]);
render_pass.set_bind_group(3, &state.shadow_bind_group, &[]);
// Draw 5 spheres (objects 0..4)
render_pass.set_vertex_buffer(0, state.sphere_mesh.vertex_buffer.slice(..));