refactor examples

This commit is contained in:
Jérôme Bousquié
2026-09-25 10:19:24 +02:00
parent ab3f056dbb
commit 35aeb769a8
37 changed files with 3430 additions and 457 deletions
+109 -19
View File
@@ -19,7 +19,9 @@
//! texture state changes happen once per distinct material, not once per entity.
//! - **Low-Level Access**: Advanced users can bypass Scene and call Renderer directly for custom rendering paths.
use crate::camera::Camera;
use crate::core::Context;
use crate::lights::{Lights, MAX_LIGHTS};
use crate::core::Frame;
use crate::core::Frustum;
use crate::core::lod::{lod_level, projected_radius_px};
@@ -29,13 +31,14 @@ use crate::pipeline::{
};
use crate::resources::uniform::{
BBOX_SLOT_SIZE, BBoxSlot, CULL_UNIFORMS_SIZE, DRAW_SLOT_SIZE, DrawSlot, FRAME_UNIFORMS_SIZE,
LOD_TABLE_SIZE, LodTable, MAT_SLOT_SIZE, MAX_LIGHTS, MatSlot, OBJECT_UNIFORM_SIZE,
LOD_TABLE_SIZE, LodTable, MAT_SLOT_SIZE, MatSlot, OBJECT_UNIFORM_SIZE,
SHADOW_UNIFORM_SIZE, TRANSFORM_SLOT_SIZE, TransformSlot,
};
use crate::resources::{
Camera, CullUniforms, FrameUniforms, Lights, Material, Mesh, ObjectUniform, ShadowUniform,
CullUniforms, FrameUniforms, Material, Mesh, ObjectUniform, ShadowUniform,
};
use crate::scene::Scene;
use crate::core::bloom::{BloomConfig, BloomPipeline};
use crate::core::hdr::ToneMapper;
use crate::utils::conf::{
GPU_DRIVEN_SHADER, GPU_WORKGROUP_SIZE, LOD_THRESHOLDS, MAX_ENTITIES, MAX_LOD_LEVELS, TONEMAP_SHADER,
@@ -151,6 +154,11 @@ pub struct Renderer {
/// HDR pipeline (Étape 20). Present only when HDR is enabled via `AppBuilder::with_hdr`.
/// When `None`, the main pass renders directly to the surface (LDR, zero overhead).
hdr: Option<HdrPipeline>,
/// Bloom pipeline (Étape 23). Present only when both HDR and bloom are active.
/// When `None`, the TM pass reads the HDR texture directly (no bloom, zero overhead).
bloom: Option<BloomPipeline>,
/// Bloom configuration (used per-frame for uniform writes). Only meaningful when bloom is active.
bloom_config: BloomConfig,
}
/// Internal HDR pipeline state: offscreen `Rgba16Float` texture + tone mapping render pipeline.
@@ -163,12 +171,17 @@ struct HdrPipeline {
/// Tone mapping render pipeline (fullscreen triangle + ACES/Reinhard curve).
pipeline: wgpu::RenderPipeline,
/// Bind group for the TM pass (HDR texture + sampler + uniform with exposure & viewport).
/// The uniform buffer is owned by the bind group (freed when the bind group is replaced).
bind_group: wgpu::BindGroup,
/// TM uniform buffer (32 bytes: exposure + viewport). Re-written each frame for live exposure.
uniform_buffer: wgpu::Buffer,
/// Bind group layout for the TM pass (reused on resize to recreate the bind group).
layout: wgpu::BindGroupLayout,
/// Sampler for the HDR texture (linear, clamp).
sampler: wgpu::Sampler,
/// Viewport width in pixels (for the TM uniform's pad.xy).
width: u32,
/// Viewport height in pixels.
height: u32,
}
impl Renderer {
@@ -187,6 +200,7 @@ impl Renderer {
height: u32,
shadow_config: &super::shadow::ShadowConfig,
hdr: Option<ToneMapper>,
bloom_config: Option<BloomConfig>,
) -> Self {
let queue: wgpu::Queue = context.queue.clone();
let device: wgpu::Device = context.device.clone();
@@ -223,6 +237,7 @@ impl Renderer {
});
let identity_object = ObjectUniform {
model: glam::Mat4::IDENTITY,
emissive: glam::Vec4::ZERO,
};
queue.write_buffer(&object_buffer, 0, bytemuck::bytes_of(&identity_object));
let shared_object_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
@@ -425,9 +440,11 @@ impl Renderer {
label: Some("GPU world matrices"),
size: MAX_ENTITIES as u64 * MAT_SLOT_SIZE,
// COPY_SRC: lets `debug_dump` read the GPU-written slots back via copy + map.
// COPY_DST: lets the CPU write emissive values into the slot padding (Étape 22).
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::UNIFORM
| wgpu::BufferUsages::COPY_SRC,
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bbox_buffer = device.create_buffer(&wgpu::BufferDescriptor {
@@ -575,12 +592,27 @@ impl Renderer {
viewport_height: height,
shadow_config: shadow_config.clone(),
hdr: None,
bloom: None,
bloom_config: bloom_config.clone().unwrap_or_default(),
};
// Seed the shared frame buffer with an identity camera + current unlit flag so the low-level
// `render` path (which has no window/camera) sees coherent values before `render_scene` runs.
renderer.write_default_frame_uniforms();
// Étape 20: allocate the HDR pipeline (offscreen texture + TM pipeline) when enabled.
renderer.hdr = hdr.map(|tm| create_hdr_pipeline(&renderer.device, &renderer.queue, width, height, tm, format));
// Étape 23: allocate the bloom pipeline when both HDR and bloom are active.
if bloom_config.is_some() {
if let Some(hdr) = &mut renderer.hdr {
let bloom = BloomPipeline::new(&renderer.device, width, height, &hdr.view);
// Recreate the TM bind group to read from the bloom composite texture.
let (bg, _buf) = create_hdr_bind_group(
&renderer.device, &hdr.layout, &hdr.sampler, bloom.composite_texture(), width, height,
);
hdr.bind_group = bg;
renderer.bloom = Some(bloom);
renderer.bloom_config = bloom_config.clone().unwrap();
}
}
renderer
}
@@ -623,10 +655,24 @@ impl Renderer {
// Étape 20: recreate the HDR texture + bind group at the new size (D10).
if let Some(hdr) = &mut self.hdr {
let (tex, view) = create_hdr_texture(&self.device, width, height);
let bg = create_hdr_bind_group(&self.device, &hdr.layout, &hdr.sampler, &tex, width, height);
let (bg, buf) = create_hdr_bind_group(&self.device, &hdr.layout, &hdr.sampler, &tex, width, height);
hdr.texture = tex;
hdr.view = view;
hdr.bind_group = bg;
hdr.uniform_buffer = buf;
hdr.width = width;
hdr.height = height;
}
// Étape 23: resize bloom textures + re-point TM bind group at the composite.
if self.bloom.is_some() {
if let Some(hdr) = &mut self.hdr {
let bloom = self.bloom.as_mut().unwrap();
bloom.resize(&self.device, width, height, &hdr.view);
let (bg, _buf) = create_hdr_bind_group(
&self.device, &hdr.layout, &hdr.sampler, bloom.composite_texture(), width, height,
);
hdr.bind_group = bg;
}
}
}
@@ -717,15 +763,15 @@ impl Renderer {
// camera must look along the light's **travel direction** (light → scene), i.e. the negation
// of the surface→light vector for directional lights.
let dir = match light.light_type() {
crate::resources::LightType::Directional => Vec3::new(
crate::lights::LightType::Directional => Vec3::new(
-light.position_dir.x,
-light.position_dir.y,
-light.position_dir.z,
),
crate::resources::LightType::Spot => {
crate::lights::LightType::Spot => {
Vec3::new(light.dir_angle.x, light.dir_angle.y, light.dir_angle.z)
}
crate::resources::LightType::Point => return None,
crate::lights::LightType::Point => return None,
};
let r = self.shadow_config.scene_radius;
let target = Vec3::from(self.shadow_config.scene_center);
@@ -807,7 +853,7 @@ impl Renderer {
/// draw). This removes the CPU-side per-entity loop from the render hot path.
/// Inputs: view — the frame's texture view color attachment; scene — the scene whose entities are
/// drawn; aspect — the viewport aspect ratio (width/height) for the camera's perspective projection.
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene, aspect: f32) {
pub fn render_scene(&self, view: &wgpu::TextureView, scene: &Scene, aspect: f32, exposure: f32) {
// 1. Rewrite the shared frame uniform buffer (camera view/proj, position, lights, shadow flags).
self.write_frame_uniforms(
scene.camera(),
@@ -1007,9 +1053,42 @@ impl Renderer {
}
}
// 8. Étape 20: tone mapping pass — renders a fullscreen triangle that reads the HDR
// texture, applies exposure + tone mapping curve, and writes to the surface.
// Only runs when HDR is active; the surface is the color target (no depth needed).
// 8. Étape 22 (6.1): write the current exposure into the TM uniform buffer (per-frame,
// so live adjustments via keyboard take effect immediately).
// 8b. Étape 22 (6.2): write each active slot's emissive into the matrix buffer padding
// (bytes 64-79). The compute pass only overwrites bytes 0-63 (the matrix), so the
// emissive persists. This must happen before the encoder submit (CPU→GPU copy).
if let Some(hdr) = &self.hdr {
let uniform_data = [
exposure, 0.0, 0.0, 0.0,
hdr.width as f32, hdr.height as f32, 0.0, 0.0,
];
self.queue.write_buffer(&hdr.uniform_buffer, 0, bytemuck::cast_slice(&uniform_data));
}
// Emissive (6.2): write per-slot into the matrix buffer padding (bytes 64-79).
// The compute pass only overwrites bytes 0-63 (the matrix), so the emissive persists.
for slot in scene.iter_slot_draws().filter(|s| s.active) {
let mat = slot
.mesh
.material()
.cloned()
.unwrap_or_else(|| scene.default_material());
if mat.emissive != [0.0; 4] {
let offset = (slot.slot_index as u64 * MAT_SLOT_SIZE + 64) as u64;
self.queue.write_buffer(&self.matrix_buffer, offset, bytemuck::cast_slice(&mat.emissive));
}
}
// 8c. Étape 23: bloom passes (threshold → blur H → blur V → composite).
// Only runs when both HDR and bloom are active. The composite texture becomes
// the input to the TM pass (the TM bind group was re-pointed at construction).
if let Some(bloom) = &self.bloom {
bloom.record_passes(&mut encoder, &self.queue, &self.bloom_config);
}
// 9. Étape 20: tone mapping pass — renders a fullscreen triangle that reads the HDR
// texture (or the bloom composite when bloom is active), applies exposure + tone
// mapping curve, and writes to the surface.
if let Some(hdr) = &self.hdr {
let mut tm_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("tone mapping pass"),
@@ -1321,6 +1400,12 @@ impl Renderer {
self.lod_enabled.set(enabled);
}
/// Updates the bloom configuration at runtime (Étape 23).
/// Takes effect on the next frame (uniforms are re-written each frame in `record_passes`).
pub fn set_bloom_config(&mut self, config: &BloomConfig) {
self.bloom_config = config.clone();
}
/// Computes the per-slot LOD levels for this frame (Step 19, D8): for each ACTIVE slot, the
/// entity's bounding sphere — the **same sphere** the GPU frustum culling uses (D8: bbox
/// center + max half-extent × max scale component, rotated by the entity's quaternion) — is
@@ -1524,8 +1609,9 @@ fn create_hdr_texture(device: &wgpu::Device, width: u32, height: u32) -> (wgpu::
(texture, view)
}
/// Creates the tone mapping bind group: HDR texture (binding 0) + sampler (binding 1) + uniform (binding 2).
/// The uniform contains exposure (1.0) and viewport size (pad.xy).
/// Creates the tone mapping bind group + uniform buffer: HDR texture (binding 0) + sampler (binding 1)
/// + uniform (binding 2). The uniform contains exposure (1.0) and viewport size (pad.xy).
/// Returns both the bind group and the uniform buffer (so the exposure can be re-written per frame).
fn create_hdr_bind_group(
device: &wgpu::Device,
layout: &wgpu::BindGroupLayout,
@@ -1533,7 +1619,7 @@ fn create_hdr_bind_group(
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> wgpu::BindGroup {
) -> (wgpu::BindGroup, wgpu::Buffer) {
// Write the uniform: exposure = 1.0, pad.xy = viewport size.
// WGSL uniform layout: f32 at offset 0 (4B), vec3<f32> at offset 16 (16B, aligned to 16).
// Total = 32 bytes. We pack as 8 f32s: [exposure, 0, 0, 0, w, h, 0, 0].
@@ -1546,7 +1632,7 @@ fn create_hdr_bind_group(
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("tm uniform"),
size: 32,
usage: wgpu::BufferUsages::UNIFORM,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: true,
});
{
@@ -1555,7 +1641,7 @@ fn create_hdr_bind_group(
drop(w);
uniform_buffer.unmap();
}
device.create_bind_group(&wgpu::BindGroupDescriptor {
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("tm bind group"),
layout,
entries: &[
@@ -1576,7 +1662,8 @@ fn create_hdr_bind_group(
}),
},
],
})
});
(bind_group, uniform_buffer)
}
/// Creates the full HDR pipeline (Étape 20): offscreen texture + TM pipeline + bind group.
@@ -1669,15 +1756,18 @@ fn create_hdr_pipeline(
});
// 5. Bind group with the initial texture + viewport size.
let bind_group = create_hdr_bind_group(device, &layout, &sampler, &texture, width, height);
let (bind_group, uniform_buffer) = create_hdr_bind_group(device, &layout, &sampler, &texture, width, height);
HdrPipeline {
texture,
view,
pipeline,
bind_group,
uniform_buffer,
layout,
sampler,
width,
height,
}
}