From 8f962368e963c44c7edc31fb81ef4534b0c3d99c Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Tue, 24 Mar 2026 21:03:31 +0900 Subject: [PATCH] feat(renderer): integrate shadow map sampling with 3x3 PCF into PBR shader Co-Authored-By: Claude Sonnet 4.6 --- crates/voltex_renderer/src/pbr_pipeline.rs | 3 +- crates/voltex_renderer/src/pbr_shader.wgsl | 57 +++++++++++++++++++++- crates/voltex_renderer/src/shadow.rs | 2 +- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/crates/voltex_renderer/src/pbr_pipeline.rs b/crates/voltex_renderer/src/pbr_pipeline.rs index 7a2b95f..1cd82f3 100644 --- a/crates/voltex_renderer/src/pbr_pipeline.rs +++ b/crates/voltex_renderer/src/pbr_pipeline.rs @@ -7,6 +7,7 @@ pub fn create_pbr_pipeline( camera_light_layout: &wgpu::BindGroupLayout, texture_layout: &wgpu::BindGroupLayout, material_layout: &wgpu::BindGroupLayout, + shadow_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("PBR Shader"), @@ -15,7 +16,7 @@ pub fn create_pbr_pipeline( let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("PBR Pipeline Layout"), - bind_group_layouts: &[camera_light_layout, texture_layout, material_layout], + bind_group_layouts: &[camera_light_layout, texture_layout, material_layout, shadow_layout], immediate_size: 0, }); diff --git a/crates/voltex_renderer/src/pbr_shader.wgsl b/crates/voltex_renderer/src/pbr_shader.wgsl index 70c81f9..ffb280d 100644 --- a/crates/voltex_renderer/src/pbr_shader.wgsl +++ b/crates/voltex_renderer/src/pbr_shader.wgsl @@ -37,6 +37,16 @@ struct MaterialUniform { @group(2) @binding(0) var material: MaterialUniform; +struct ShadowUniform { + light_view_proj: mat4x4, + shadow_map_size: f32, + shadow_bias: f32, +}; + +@group(3) @binding(0) var t_shadow: texture_depth_2d; +@group(3) @binding(1) var s_shadow: sampler_comparison; +@group(3) @binding(2) var shadow: ShadowUniform; + struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @@ -48,6 +58,7 @@ struct VertexOutput { @location(0) world_normal: vec3, @location(1) world_pos: vec3, @location(2) uv: vec2, + @location(3) light_space_pos: vec4, }; @vertex @@ -58,6 +69,7 @@ fn vs_main(model_v: VertexInput) -> VertexOutput { out.world_normal = (camera.model * vec4(model_v.normal, 0.0)).xyz; out.clip_position = camera.view_proj * world_pos; out.uv = model_v.uv; + out.light_space_pos = shadow.light_view_proj * world_pos; return out; } @@ -165,6 +177,44 @@ fn compute_light_contribution( return (kd * albedo / 3.14159265358979 + specular) * radiance * NdotL; } +fn calculate_shadow(light_space_pos: vec4) -> f32 { + // If shadow_map_size == 0, shadow is disabled + if shadow.shadow_map_size == 0.0 { + return 1.0; + } + + let proj_coords = light_space_pos.xyz / light_space_pos.w; + + // wgpu NDC: x,y [-1,1], z [0,1] + let shadow_uv = vec2( + proj_coords.x * 0.5 + 0.5, + -proj_coords.y * 0.5 + 0.5, + ); + let current_depth = proj_coords.z; + + if shadow_uv.x < 0.0 || shadow_uv.x > 1.0 || shadow_uv.y < 0.0 || shadow_uv.y > 1.0 { + return 1.0; + } + if current_depth > 1.0 || current_depth < 0.0 { + return 1.0; + } + + // 3x3 PCF + let texel_size = 1.0 / shadow.shadow_map_size; + var shadow_val = 0.0; + for (var x = -1; x <= 1; x++) { + for (var y = -1; y <= 1; y++) { + let offset = vec2(f32(x), f32(y)) * texel_size; + shadow_val += textureSampleCompare( + t_shadow, s_shadow, + shadow_uv + offset, + current_depth - shadow.shadow_bias, + ); + } + } + return shadow_val / 9.0; +} + @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let tex_color = textureSample(t_diffuse, s_diffuse, in.uv); @@ -180,13 +230,18 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let F0 = mix(vec3(0.04, 0.04, 0.04), albedo, metallic); // Accumulate contribution from all active lights + let shadow_factor = calculate_shadow(in.light_space_pos); var Lo = vec3(0.0); let light_count = min(lights_uniform.count, 16u); for (var i = 0u; i < light_count; i++) { - Lo += compute_light_contribution( + var contribution = compute_light_contribution( lights_uniform.lights[i], N, V, in.world_pos, F0, albedo, metallic, roughness, ); + if lights_uniform.lights[i].light_type == 0u { + contribution = contribution * shadow_factor; + } + Lo += contribution; } // Ambient term diff --git a/crates/voltex_renderer/src/shadow.rs b/crates/voltex_renderer/src/shadow.rs index a31777d..5e57507 100644 --- a/crates/voltex_renderer/src/shadow.rs +++ b/crates/voltex_renderer/src/shadow.rs @@ -68,7 +68,7 @@ impl ShadowMap { // binding 2: ShadowUniform buffer wgpu::BindGroupLayoutEntry { binding: 2, - visibility: wgpu::ShaderStages::FRAGMENT, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false,