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)
This commit is contained in:
Jérôme Bousquié
2026-09-18 18:26:38 +02:00
parent 49ecdea249
commit d518948deb
12 changed files with 394 additions and 70 deletions
+22 -11
View File
@@ -23,7 +23,7 @@ use crate::core::Frame;
use crate::math::Transform;
use crate::pipeline::{DEPTH_FORMAT, create_uniform_bind_group_layouts};
use crate::resources::uniform::{FRAME_UNIFORMS_SIZE, OBJECT_UNIFORM_SIZE};
use crate::resources::{Camera, FrameUniforms, Material, Mesh, ObjectUniform};
use crate::resources::{Camera, FrameUniforms, Lights, Material, Mesh, ObjectUniform};
use crate::scene::Scene;
use glam::Vec4;
use std::cell::RefCell;
@@ -184,20 +184,31 @@ impl Renderer {
self.format = format;
}
/// Rewrites the shared per-frame uniform buffer from the scene's active camera and the current
/// viewport aspect, then returns the frame bind group wired to that buffer. Called at the start of
/// every `render_scene` so the GPU sees the latest camera matrices and camera position (Étape 4.3).
/// Rewrites the shared per-frame uniform buffer from the scene's active camera, its global
/// light list, its ambient color, and the current viewport aspect, then returns the frame bind
/// group wired to that buffer. Called at the start of every `render_scene` so the GPU sees the
/// latest camera matrices, camera position, and lighting (Étape 4.3, Étape 12).
///
/// The directional light stays at the `FrameUniforms::default()` values (white, along +Z) — scene
/// lighting configuration is a later step; only the camera-driven fields are derived from `camera`.
/// Inputs: camera (the scene's active camera), aspect (viewport width / height).
fn write_frame_uniforms(&self, camera: &Camera, aspect: f32) {
/// The light array is packed via `Lights::into_frame_array` (directionals first, then point
/// lights). Inputs: camera (the scene's active camera), lights (the scene's global light list),
/// ambient (the scene's ambient hemisphere color, rgb), aspect (viewport width / height).
fn write_frame_uniforms(
&self,
camera: &Camera,
lights: &Lights,
ambient: [f32; 3],
aspect: f32,
) {
let (light_array, num_directional, num_point) = lights.into_frame_array();
let frame = FrameUniforms {
view: camera.view_matrix(),
proj: camera.projection_matrix(aspect),
cam_pos: camera.position.extend(1.0),
light_dir: Vec4::new(0.0, 0.0, 1.0, 0.0),
light_color: Vec4::ONE,
ambient: Vec4::new(ambient[0], ambient[1], ambient[2], 1.0),
lights: light_array,
num_directional,
num_point,
_pad: [0, 0],
options: [if self.unlit { 1 } else { 0 }, 0, 0, 0],
};
self.queue
@@ -268,7 +279,7 @@ impl Renderer {
/// Before drawing, the shared frame uniform buffer is rewritten from `scene.camera()` so the GPU
/// receives the active camera's view/projection matrices and position for this frame (Étape 4.3).
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene, aspect: f32) {
self.write_frame_uniforms(scene.camera(), aspect);
self.write_frame_uniforms(scene.camera(), scene.lights(), scene.ambient(), aspect);
let mut encoder = self
.device