fix: shadow caster index, torus winding, per-frame device poll ; close stage 15
Bug fixes found while validating stage 15: - demo: set_shadow_caster(Some(0)) selected the default +Z light (packed index 0 from Lights::new()); the demo's warm directional light is packed at index 1. The shadow camera then looked down -Z, so misaligned objects occluded each other (cone/torus rendered black). Use index 1. - primitives::torus: index winding was flipped ([a,c,b]); the outer surface (outward normals, CCW from outside) was culled and only the dark interior stayed visible. Reversed to [a,b,c]/[b,d,c] so it is CCW from outside. - app: call device.poll() each frame in about_to_wait; without it wgpu async callbacks (on_submitted_work_done, map_async) never fire in the windowed loop. Docs: - conf: clarify the embedded-shader fallback is expected/harmless and that SHADOW_SHADER_PATH is kept for API compatibility only. - README item 15: runtime-verified headless. - DRAFT.md: emptied to a completion summary per convention (full stage-15 plan preserved in git history).
This commit is contained in:
@@ -161,8 +161,10 @@ impl AppHandler for Demo {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The first directional light (packed index 0) casts shadows.
|
||||
app.scene.set_shadow_caster(Some(0));
|
||||
// The warm directional light above casts shadows. It is packed at index 1: index 0 is
|
||||
// the default +Z directional light pre-loaded by `Lights::new()` (kept here for the
|
||||
// base lighting), so the demo's own light is the SECOND one in the packed array.
|
||||
app.scene.set_shadow_caster(Some(1));
|
||||
app.scene.set_ambient([0.14, 0.14, 0.16]);
|
||||
|
||||
// 6. Active camera, driven by the orbital controller (position, distance, preset target).
|
||||
|
||||
@@ -290,6 +290,18 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
let Some(app) = self.app.as_mut() else {
|
||||
return;
|
||||
};
|
||||
// Poll the device each frame: wgpu only fires async callbacks (queue.on_submitted_work_done,
|
||||
// buffer/texture map_async) when the device is polled, and the event loop never does it on
|
||||
// our behalf. `Wait` with no timeout = block until the most recent submission completes
|
||||
// (i.e. once per frame on a live GPU, which is what we want for the windowed loop).
|
||||
// A failed poll (e.g. a device-lost error) is logged, not fatal: the next frame's poll
|
||||
// will retry, and wgpu surfaces the loss through the device's error handler anyway.
|
||||
if let Err(e) = app.context().device.poll(wgpu::PollType::Wait {
|
||||
submission_index: None,
|
||||
timeout: None,
|
||||
}) {
|
||||
eprintln!("WSG : device.poll() a échoué ({e:?})");
|
||||
}
|
||||
// Étape 15 (input) : débute la frame d'input (rotation pressed/released + reset deltas),
|
||||
// exécute la logique utilisateur, puis clôt (nettoie les états transitoires).
|
||||
app.input.begin_frame();
|
||||
|
||||
@@ -422,8 +422,15 @@ pub fn torus(major: f32, minor: f32, major_segments: u32, minor_segments: u32) -
|
||||
let b = a + 1;
|
||||
let c = a + mn + 1;
|
||||
let d = c + 1;
|
||||
// Triangles [a, b, c] / [b, d, c] : en face, l'angle u (majeur) croît avec +u et
|
||||
// l'angle v (mineur) croît avec +v ; cross(tang_u, tang_v) pointe vers l'EXTÉRIEUR
|
||||
// du tube (= la normale stockée), donc le winding est CCW vu de l'extérieur —
|
||||
// cohérent avec `front_face: Face::Ccw` (culling des faces arrière).
|
||||
// L'ordre [a, c, b] d'origine était inversé : la face externe (CCW vu de l'extérieur,
|
||||
// normale extérieure) était Cullée et seul l'intérieur du tube, dont les normales
|
||||
// pointent vers l'extérieur, restait visible — le tore apparaissait noir (N·L ≤ 0).
|
||||
indices
|
||||
.extend_from_slice(&[a as u16, c as u16, b as u16, b as u16, c as u16, d as u16]);
|
||||
.extend_from_slice(&[a as u16, b as u16, c as u16, b as u16, d as u16, c as u16]);
|
||||
}
|
||||
}
|
||||
Geometry::new(positions)
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
/// for file-based loading. This is the unified pipeline shader (Étape 3 : un seul layout pour tous) :
|
||||
/// it carries the full uniform contract (frame + object bind groups) and supports an unlit mode so flat
|
||||
/// 2D rendering is a special case of the 3D lit path. The `basic` family was removed (Étape 5).
|
||||
///
|
||||
/// NOTE: the shipped `assets/shaders/*.wgsl` files are OPTIONAL — when they are absent (library consumed
|
||||
/// from a checkout without the assets directory, or from a published crate), `PipelineCache::load_shader`
|
||||
/// falls back to the embedded `STANDARD_SHADER` source, which is byte-identical. The fallback is therefore
|
||||
/// expected and harmless, not an error condition.
|
||||
pub const STANDARD_SHADER_PATH: &str = "assets/shaders/standard_shader.wgsl";
|
||||
|
||||
/// The standard (Phong) WGSL shader source code, embedded at compile time via `include_str!`.
|
||||
@@ -21,8 +26,9 @@ pub const STANDARD_SHADER_PATH: &str = "assets/shaders/standard_shader.wgsl";
|
||||
/// pipeline uses the unified layout (frame @0 + object @1), this is the only shader the library ships.
|
||||
pub const STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl");
|
||||
|
||||
/// Path to the depth-only **shadow** WGSL shader on disk (Étape 14, D4). Used by the Renderer's
|
||||
/// shadow-map pass: a minimal vertex shader that transforms vertices into light-clip space.
|
||||
/// Path to the depth-only **shadow** WGSL shader on disk (Étape 14, D4). Kept only for API
|
||||
/// compatibility — the shadow pass always compiles the embedded `SHADOW_SHADER` directly
|
||||
/// (it is internal to the library, no external file is ever read).
|
||||
pub const SHADOW_SHADER_PATH: &str = "assets/shaders/shadow_shader.wgsl";
|
||||
|
||||
/// The depth-only shadow WGSL shader source, embedded at compile time via `include_str!`
|
||||
|
||||
Reference in New Issue
Block a user