Files
wsg/lib/src/resources/uniform.rs
T
Jérôme Bousquié d518948deb feat: multi-lumières directionnelles + ponctuelles (Étape 12, Phase 4.2)
- Light (48 o) + MAX_LIGHTS=8 ; FrameUniforms étendu (ambient, lights[8], compteurs, _pad)
- resources/lights.rs : Lights (1 dir +Z par défaut, non-régression) + into_frame_array
- Scene : API déclarative add_directional_light / add_point_light / set_ambient / clear_lights
- Renderer::write_frame_uniforms upload les lumières/ambiant de la scène
- standard_shader.wgsl : struct Light + boucles d'accumulation (dir + ponctuelles, atténuation linéaire)
- exemple cube : 1 lumière ponctuelle chaude
- Docs : shaders/resources README, README roadmap, ROADMAP (item coché + Spot notée futur)
- Build/test/fmt OK ; non-régression par défaut (1 dir +Z + ambiant blanc)
2026-09-18 18:26:38 +02:00

147 lines
6.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # Uniform Module — GPU Buffer Data Types
//!
//! Defines the CPU-side `Pod` (plain old data) structs that are uploaded to GPU uniform buffers.
//! Their memory layout must match **exactly** the WGSL uniforms declared in `standard_shader.wgsl`
//! (see the "Uniform Contract" section of that file) — 16-byte alignment (std140), no padding.
//!
//! Two bind groups are shared by every pipeline (single-layout decision, Étape 3) :
//! - `@group(0) @binding(0)` : `FrameUniforms` (per-frame : camera + lights) → 192 bytes
//! - `@group(1) @binding(0)` : `ObjectUniform` (per-entity model matrix) → 64 bytes
//!
//! ## Interaction with Other Modules
//! - `pipeline_cache::build_pipeline()` creates the two bind group layouts matching these types.
//! - `Renderer` allocates the buffers and `BindGroup`s from these types and writes them each frame.
//! - `standard_shader.wgsl` consumes them (layout identical to these structs).
use bytemuck::{Pod, Zeroable};
use glam::{Mat4, Vec4};
/// Byte size of the per-frame uniform buffer (`FrameUniforms`).
pub const FRAME_UNIFORMS_SIZE: u64 = std::mem::size_of::<FrameUniforms>() as u64;
/// Byte size of the per-object uniform buffer (`ObjectUniform`).
pub const OBJECT_UNIFORM_SIZE: u64 = std::mem::size_of::<ObjectUniform>() as u64;
/// Maximum number of lights stored in the per-frame uniform buffer.
/// Bounded capacity: adding more than this returns `WsgError` (no dynamic UBO allocation).
pub const MAX_LIGHTS: usize = 8;
/// A single light, stored in the per-frame uniform array. One struct serves both types; the
/// *position in the array* disambiguates: indices `0..num_directional` are directional
/// (`position_dir.xyz` = direction **from the surface toward the light**), indices
/// `num_directional..` are point (`position_dir.xyz` = world position). No type flag in the struct.
///
/// 3 × Vec4 = 48 bytes, 16-byte aligned (std140-compatible with the WGSL `struct Light`).
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, PartialEq)]
pub struct Light {
/// xyz = direction from surface toward the light (directional) or world position (point); w = 0.
pub position_dir: Vec4,
/// rgb = color; a = intensity (multiplier).
pub color: Vec4,
/// x = attenuation radius (point lights); 0 for directional.
pub radius: Vec4,
}
/// Per-frame GPU uniforms : camera matrices + ambient + global light list + options.
///
/// Mirrors the WGSL `FrameUniforms` struct in `standard_shader.wgsl` (offset table there).
/// 192 + 48·MAX_LIGHTS bytes (16-byte aligned) — `Pod` for direct `bytes_of` upload. The bind-group
/// layout uses `min_binding_size: None`, so extending this struct is transparent (no relayout).
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct FrameUniforms {
/// Camera view matrix (world → view space). Offset 0.
pub view: Mat4,
/// Camera projection matrix (view → clip space). Offset 64.
pub proj: Mat4,
/// Camera world position (`.xyz` used). Offset 128.
pub cam_pos: Vec4,
/// Ambient hemisphere color (`.rgb` used). Offset 144.
pub ambient: Vec4,
/// Global light list: `0..num_directional` directional, then `num_point` point. Offset 160.
pub lights: [Light; MAX_LIGHTS],
/// Number of active directional lights (indices `0..num_directional`). Offset 160 + 48·MAX_LIGHTS.
pub num_directional: u32,
/// Number of active point lights (indices after the directionals).
pub num_point: u32,
/// Padding so `options` lands on a 16-byte boundary — matching the WGSL `vec4<u32>`
/// (alignment 16), which Rust's `repr(C)` would otherwise place at offset 552.
pub _pad: [u32; 2],
/// Options. `options[0]` = unlit flag (1 → flat color, no lighting).
pub options: [u32; 4],
}
impl Default for FrameUniforms {
/// Sensible defaults : identity camera, white ambient, a single white directional light along
/// +Z (from surface toward light), *lit* mode — reproduces the pre-multi-light look exactly.
fn default() -> Self {
Self {
view: Mat4::IDENTITY,
proj: Mat4::IDENTITY,
cam_pos: Vec4::ZERO,
ambient: Vec4::ONE,
lights: [Light {
position_dir: Vec4::new(0.0, 0.0, 1.0, 0.0), // from surface toward light = +Z
color: Vec4::ONE,
radius: Vec4::ZERO,
}; MAX_LIGHTS],
num_directional: 1,
num_point: 0,
_pad: [0, 0],
options: [0, 0, 0, 0],
}
}
}
/// Per-object GPU uniforms : the entity's world-space model matrix.
///
/// Mirrors the WGSL `ObjectUniform` struct. 64 bytes, `Pod`.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, Default)]
pub struct ObjectUniform {
/// Model matrix (object → world space). Offset 0.
pub model: Mat4,
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::offset_of;
use std::mem::{align_of, size_of};
#[test]
fn frame_uniforms_layout_matches_wgsl() {
// The offsets below must match the offset table in standard_shader.wgsl.
// Header (view..ambient) = 160, lights = 48·MAX_LIGHTS, then counters(8) + pad(8) +
// options(16) = 32. Total = 160 + 48·MAX_LIGHTS + 32 = 576 bytes.
assert_eq!(size_of::<FrameUniforms>(), 160 + 48 * MAX_LIGHTS + 32);
assert_eq!(align_of::<FrameUniforms>(), 16);
let f = FrameUniforms::default();
assert_eq!(offset_of!(FrameUniforms, view), 0);
assert_eq!(offset_of!(FrameUniforms, proj), 64);
assert_eq!(offset_of!(FrameUniforms, cam_pos), 128);
assert_eq!(offset_of!(FrameUniforms, ambient), 144);
assert_eq!(offset_of!(FrameUniforms, lights), 160);
assert_eq!(
offset_of!(FrameUniforms, num_directional),
160 + 48 * MAX_LIGHTS
);
assert_eq!(
offset_of!(FrameUniforms, options),
160 + 48 * MAX_LIGHTS + 16
);
// Default is lit mode (unlit flag cleared), one directional light, no point lights.
assert_eq!(f.options[0], 0);
assert_eq!(f.num_directional, 1);
assert_eq!(f.num_point, 0);
}
#[test]
fn object_uniform_layout_matches_wgsl() {
assert_eq!(size_of::<ObjectUniform>(), 64);
assert_eq!(align_of::<ObjectUniform>(), 16);
assert_eq!(offset_of!(ObjectUniform, model), 0);
}
}