Étape 14: add shadow mapping (directional light, Phase 4.2)
Implement shadow mapping for directional lights: - Scene::set_shadow_caster(Option<usize>) selects the shadow-casting light by packed frame-array index (None disables; point lights rejected at render). - Lights::get(index) resolves a packed index across the directional/point/spot lists. - Renderer allocates a shadow depth map, comparison sampler, group-3 bind groups, shadow uniform buffer and shadow pipeline; render_scene does a depth-only shadow pass before the main pass; compute_shadow_light_view_proj builds an orthographic light-space frustum from the scene radius. - standard_shader: shadow_light_index/light_view_proj/shadow_params uniforms, @group(3) depth map + comparison sampler, 3x3 PCF compute_shadow(). - shadow_shader: path/vertex shader with attribute layout matching the shared vertex buffer (only position consumed). - shadow_test example: directional shadow caster casts a PCF-softened shadow onto a ground slab; documented in examples README.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
//! # Shadow Shader (Étape 14, Phase 4.2 — depth-only pass)
|
||||
//!
|
||||
//! Minimal vertex shader used for the **shadow map pass** (DRAFT Étape 14, D4). It transforms each
|
||||
//! vertex into the light's clip space and lets the depth write happen — no fragment stage, no color
|
||||
//! output, no lighting : the rasterizer only records the depth (D2).
|
||||
//!
|
||||
//! Only the `position` attribute (location 0) is consumed, so this pipeline needs no normal/uv/color
|
||||
//! buffers and is as cheap as possible.
|
||||
//!
|
||||
//! ## Uniform Contract (this pipeline's own layout — independent of the main pipeline)
|
||||
//! - `@group(0) @binding(0)` : `ShadowUniform` — the light's `view_proj` matrix (world → light clip).
|
||||
//! - `@group(1) @binding(0)` : `ObjectUniform` — the entity's per-entity model matrix (shared with
|
||||
//! the main pipeline, so the Renderer reuses its per-entity object bind groups).
|
||||
//!
|
||||
//! The light VP is passed as a group-0 uniform rather than reusing the camera `FrameUniforms`
|
||||
//! because the shadow pass is rendered from the light's point of view, not the camera's.
|
||||
|
||||
struct ShadowUniform {
|
||||
view_proj: mat4x4<f32>,
|
||||
};
|
||||
|
||||
struct ObjectUniform {
|
||||
model: mat4x4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> shadow: ShadowUniform;
|
||||
@group(1) @binding(0) var<uniform> object: ObjectUniform;
|
||||
|
||||
struct VertexInput {
|
||||
@location(0) position: vec3<f32>,
|
||||
@location(1) normal: vec3<f32>,
|
||||
@location(2) uv: vec2<f32>,
|
||||
@location(3) color: vec4<f32>,
|
||||
};
|
||||
|
||||
// Output carries only the clip position; any attribute interpolated without a fragment stage is
|
||||
// still fine (it is simply discarded). Keeping just the position minimizes the vertex output size.
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||
var out: VertexOutput;
|
||||
let world = object.model * vec4<f32>(input.position, 1.0);
|
||||
out.clip_position = shadow.view_proj * world;
|
||||
return out;
|
||||
}
|
||||
@@ -7,24 +7,29 @@
|
||||
//! modulates the vertex color (`texel.rgb * in.color.rgb`).
|
||||
//!
|
||||
//! ## Uniform Contract
|
||||
//! Three bind groups, shared by every material (one single pipeline layout — voir Étape 3) :
|
||||
//! - `@group(0) @binding(0)` : `FrameUniforms` (per-frame, camera + lights) [704 bytes]
|
||||
//! Four bind groups, shared by every material (one single pipeline layout — voir Étape 3) :
|
||||
//! - `@group(0) @binding(0)` : `FrameUniforms` (per-frame, camera + lights + shadow) [784 bytes]
|
||||
//! - `@group(1) @binding(0)` : `ObjectUniform` (per-entity model matrix) [64 bytes]
|
||||
//! - `@group(2) @binding(0)` : `texture_sampler` (sampler) — diffuse (Étape 10)
|
||||
//! - `@group(2) @binding(1)` : `diffuse_texture` (texture_2d<f32>) (Étape 10)
|
||||
//! - `@group(3) @binding(0)` : `shadow_sampler` (sampler_comparison) (Étape 14)
|
||||
//! - `@group(3) @binding(1)` : `shadow_map` (texture_depth_2d) (Étape 14)
|
||||
//!
|
||||
//! `FrameUniforms` layout (std140 — each element 16-byte aligned) :
|
||||
//! | Offset | Field | Type | Meaning |
|
||||
//! |-----------------------|----------------|---------------|----------------------------------|
|
||||
//! | 0 | view | mat4x4<f32> | Camera view matrix |
|
||||
//! | 64 | proj | mat4x4<f32> | Camera projection matrix |
|
||||
//! | 128 | cam_pos | vec4<f32> | Camera world position (.xyz) |
|
||||
//! | 144 | ambient | vec4<f32> | Ambient hemisphere color (.rgb) |
|
||||
//! | 160 | lights[0..MAX] | array<Light> | Global light list |
|
||||
//! | 160 + 64·MAX_LIGHTS | num_directional| u32 | # directional (indices 0..n) |
|
||||
//! | | num_point | u32 | # point (indices n..) |
|
||||
//! | | num_spot | u32 | # spot (indices after point) |
|
||||
//! | | options | vec4<u32> | x = unlit flag (1 => flat color) |
|
||||
//! | Offset | Field | Type | Meaning |
|
||||
//! |-----------------------|-------------------|---------------|----------------------------------|
|
||||
//! | 0 | view | mat4x4<f32> | Camera view matrix |
|
||||
//! | 64 | proj | mat4x4<f32> | Camera projection matrix |
|
||||
//! | 128 | cam_pos | vec4<f32> | Camera world position (.xyz) |
|
||||
//! | 144 | ambient | vec4<f32> | Ambient hemisphere color (.rgb) |
|
||||
//! | 160 | lights[0..MAX] | array<Light> | Global light list |
|
||||
//! | 160 + 64·MAX_LIGHTS | num_directional | u32 | # directional (indices 0..n) |
|
||||
//! | | num_point | u32 | # point (indices n..) |
|
||||
//! | | num_spot | u32 | # spot (indices after point) |
|
||||
//! | | shadow_light_index| u32 | packed index of shadow light |
|
||||
//! | 160 + 64·MAX_LIGHTS+16| light_view_proj | mat4x4<f32> | world → light clip space (D3) |
|
||||
//! | | shadow_params | vec4<f32> | .x = map size, .y = depth bias |
|
||||
//! | | options | vec4<u32> | .x = unlit ; .y = shadows on |
|
||||
//!
|
||||
//! `MAX_LIGHTS = 8`. `struct Light` is 64 bytes (4 × vec4). Directional lights occupy
|
||||
//! `lights[0..num_directional]` (`position_dir.xyz` = direction **from the surface toward the
|
||||
@@ -79,12 +84,15 @@ struct FrameUniforms {
|
||||
view: mat4x4<f32>,
|
||||
proj: mat4x4<f32>,
|
||||
cam_pos: vec4<f32>,
|
||||
ambient: vec4<f32>, // .rgb = ambient hemisphere color
|
||||
lights: array<Light, MAX_LIGHTS>, // directional, then point, then spot
|
||||
ambient: vec4<f32>, // .rgb = ambient hemisphere color
|
||||
lights: array<Light, MAX_LIGHTS>, // directional, then point, then spot
|
||||
num_directional: u32,
|
||||
num_point: u32,
|
||||
num_spot: u32,
|
||||
options: vec4<u32>, // .x : unlit flag (1 = flat color, no lighting)
|
||||
shadow_light_index: u32, // packed index of the shadow light ; MAX_LIGHTS = off
|
||||
light_view_proj: mat4x4<f32>, // world → shadow light clip space (Étape 14, D3)
|
||||
shadow_params: vec4<f32>, // .x = shadow map size, .y = depth bias
|
||||
options: vec4<u32>, // .x = unlit flag ; .y = shadows on
|
||||
};
|
||||
|
||||
struct ObjectUniform {
|
||||
@@ -97,6 +105,10 @@ struct ObjectUniform {
|
||||
// texture lie le placeholder blanc 1×1 (D2), d'où l'échantillonnage inconditionnel.
|
||||
@group(2) @binding(0) var texture_sampler: sampler;
|
||||
@group(2) @binding(1) var diffuse_texture: texture_2d<f32>;
|
||||
// Étape 14 (DRAFT D1/D5) : groupe ombre — comparaison sampler (0) + carte de profondeur (1).
|
||||
// Toujours lié (layout unifié) ; inutilisé tant que `options.y == 0` (ombres désactivées).
|
||||
@group(3) @binding(0) var shadow_sampler: sampler_comparison;
|
||||
@group(3) @binding(1) var shadow_map: texture_depth_2d;
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) clip_position: vec4<f32>,
|
||||
@@ -189,6 +201,37 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
diffuse += frame.lights[i].color.rgb * frame.lights[i].color.a * ndotl * falloff * spot_factor;
|
||||
}
|
||||
|
||||
let lit = base * (ambient + diffuse);
|
||||
let lit = base * (ambient + diffuse) * compute_shadow(in.world_pos);
|
||||
return vec4<f32>(lit, in.color.a);
|
||||
}
|
||||
|
||||
// Étape 14 (DRAFT 3.2, D5) : PCF shadow factor for this fragment. Reprojects the world position
|
||||
// into the shadow light's clip space, converts to depth-map UVs + normalized depth, then averages
|
||||
// a 3×3 `textureSampleCompare` neighborhood using the comparison sampler (GreaterEqual). Returns
|
||||
// 1.0 when fully lit (or shadows disabled), 0.0 when fully in shadow. The reference depth is
|
||||
// pulled toward the viewer by `frame.shadow_params.y` (bias) to suppress acne.
|
||||
fn compute_shadow(world_pos: vec3<f32>) -> f32 {
|
||||
// Shadows off (options.y == 0) or no valid caster (sentinel = MAX_LIGHTS) → fully lit.
|
||||
if (frame.options.y == 0u || frame.shadow_light_index == MAX_LIGHTS) {
|
||||
return 1.0;
|
||||
}
|
||||
let light_clip = frame.light_view_proj * vec4<f32>(world_pos, 1.0);
|
||||
// Perspective divide then map NDC [-1,1] → UV [0,1]. Orthographic depth is linear in the map.
|
||||
let shadow_ndc = light_clip.xyz / max(light_clip.w, 1e-6);
|
||||
var shadow_uv = shadow_ndc.xy * 0.5 + 0.5;
|
||||
shadow_uv = vec2<f32>(shadow_uv.x, 1.0 - shadow_uv.y); // flip V for texture coordinates
|
||||
let current_depth = shadow_ndc.z * 0.5 + 0.5;
|
||||
let bias = frame.shadow_params.y;
|
||||
let texel = 1.0 / max(frame.shadow_params.x, 1.0);
|
||||
|
||||
// 3×3 PCF : average of the comparison results around the fragment's texel.
|
||||
var lit_count = 0.0;
|
||||
for (var ox = -1i; ox <= 1; ox++) {
|
||||
for (var oy = -1i; oy <= 1; oy++) {
|
||||
let offset = vec2<f32>(f32(ox), f32(oy)) * texel;
|
||||
lit_count += textureSampleCompare(
|
||||
shadow_map, shadow_sampler, shadow_uv + offset, current_depth - bias);
|
||||
}
|
||||
}
|
||||
return lit_count / 9.0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user