feat(examples): 3D MVP cube via standard shader, drop basic (Étape 5)

This commit is contained in:
Jérôme Bousquié
2026-09-17 10:40:43 +02:00
parent 43e8bfb40a
commit 0a85aff17b
15 changed files with 218 additions and 111 deletions
+122
View File
@@ -0,0 +1,122 @@
//! Étape 5 — MVP 3D : un cube unitaire éclairé qui tourne, rendu automatiquement par la boucle `App`.
//!
//! Démonstration de l'objectif MVP du ROADMAP 1.3 + 1.5 : un mesh 3D avec éclairage Phong à l'écran.
//! On suit le workflow déclaratif (comme `simple`) : `AppBuilder` + scène automatique, **sans importer
//! wgpu**. La seule nouveauté déclarative est l'enregistrement du shader `standard` (Phong) au lieu de
//! `basic`. La caméra active par défaut (`Scene::default`, position (0,0,3), fov 45°) cadre le cube, et
//! `AppHandler::update` fait tourner l'entité via `set_entity_transform` chaque frame.
use glam::Quat;
use std::sync::Arc;
use wsg_lib::AppHandler;
use wsg_lib::app::AppBuilder;
use wsg_lib::resources::{Material, Mesh, Vertex};
use wsg_lib::utils::WsgError;
/// Handler de démonstration : fait tourner le cube dans `update`.
struct Cube {
/// Angle de rotation cumulé (radians), incrémenté à chaque frame.
angle: f32,
}
/// Génère les sommets d'un cube unitaire centré à l'origine (arête de 1), une normale par face.
/// 24 sommets (4 par face) + 36 indices ; la couleur est blanche, l'UV est laissé à zéro (inutilisé
/// par `standard` pour un matériau sans texture).
fn cube_vertices() -> Vec<Vertex> {
let s = 0.5; // demi-arête
let color = [1.0, 1.0, 1.0, 1.0];
// Chaque face : (normale sortante, 4 coins). Le culling est désactivé par défaut (PrimitiveState
// par défaut), donc l'ordre d'enroulement n'affecte pas la visibilité ; seules les normales comptent
// pour l'éclairage.
let faces: [([f32; 3], [[f32; 3]; 4]); 6] = [
(
[0.0, 0.0, 1.0],
[[-s, -s, s], [s, -s, s], [s, s, s], [-s, s, s]],
), // +Z
(
[0.0, 0.0, -1.0],
[[s, -s, -s], [-s, -s, -s], [-s, s, -s], [s, s, -s]],
), // -Z
(
[1.0, 0.0, 0.0],
[[s, -s, -s], [s, s, -s], [s, s, s], [s, -s, s]],
), // +X
(
[-1.0, 0.0, 0.0],
[[-s, -s, s], [-s, s, s], [-s, s, -s], [-s, -s, -s]],
), // -X
(
[0.0, 1.0, 0.0],
[[-s, s, -s], [s, s, -s], [s, s, s], [-s, s, s]],
), // +Y
(
[0.0, -1.0, 0.0],
[[-s, -s, s], [s, -s, s], [s, -s, -s], [-s, -s, -s]],
), // -Y
];
let mut verts = Vec::with_capacity(24);
for (normal, corners) in faces {
for corner in corners {
verts.push(Vertex {
position: corner,
normal,
uv: [0.0, 0.0],
color,
});
}
}
verts
}
/// Génère les indices d'un cube à partir de ses 24 sommets (2 triangles par face, 36 indices).
fn cube_indices() -> Vec<u16> {
let mut indices = Vec::with_capacity(36);
for face in 0..6u16 {
let b = face * 4;
indices.extend_from_slice(&[b, b + 1, b + 2, b, b + 2, b + 3]);
}
indices
}
impl AppHandler for Cube {
fn setup(&mut self, app: &mut wsg_lib::App) {
let format = app.renderer().format();
// Shader Phong `standard` (porteur des bind groups frame + object) au lieu de `basic`.
app.cache()
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
let mesh = Arc::new(Mesh::new(
app.renderer().device(),
&cube_vertices(),
Some(&cube_indices()),
));
let material = Arc::new(Material::new(format, "standard", app.cache()));
app.scene.add_mesh("cube_mesh", mesh).unwrap();
app.scene.add_material("cube_material", material).unwrap();
app.scene
.add_entity("cube", "cube_mesh", "cube_material")
.unwrap();
}
fn update(&mut self, app: &mut wsg_lib::App) {
// Rotation cumulée du cube (double axe pour un mouvement plus lisible).
self.angle += 0.02;
let base = *app
.scene
.entity_transform("cube")
.expect("cube entity present");
let mut transform = base;
transform.rotation =
Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.3);
app.scene.set_entity_transform("cube", transform);
}
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
let app = AppBuilder::new().title("WSG Cube").build().await?;
app.run(Cube { angle: 0.0 })
}
+9 -6
View File
@@ -1,7 +1,7 @@
//! Workflow bas-niveau : utilisation directe du `Context`, `Renderer`, `PipelineCache`, `Mesh` et //! Workflow bas-niveau : utilisation directe du `Context`, `Renderer`, `PipelineCache`, `Mesh` et
//! `Material`, contournant la façade `App`. Rendu d'un quad plat éclairé via la boucle winit 0.30 //! `Material`, contournant la façade `App`. Rendu d'un quad plat (shader `standard` **unlit**) via la
//! (`EventLoop::run_app` + `ApplicationHandler`). La fenêtre et le GPU sont créés dans `resumed()`, //! boucle winit 0.30 (`EventLoop::run_app` + `ApplicationHandler`). La fenêtre et le GPU sont créés
//! comme l'exigent winit 0.30 et la migration faite dans `app.rs`. //! dans `resumed()`, comme l'exigent winit 0.30 et la migration faite dans `app.rs`.
use std::sync::Arc; use std::sync::Arc;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
@@ -59,13 +59,16 @@ impl ApplicationHandler for App {
let device = Arc::new(context.device.clone()); let device = Arc::new(context.device.clone());
let mut cache = PipelineCache::new(device); let mut cache = PipelineCache::new(device);
cache cache
.register_shader("basic", utils::BASIC_SHADER_PATH) .register_shader("standard", utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
let renderer = Renderer::new(&context, format); // Rendu 2D plat : `standard` en mode unlit (les bind groups frame+object sont posés par
// draw_entity, la matrice frame par défaut est l'identité → positions NDC inchangées).
let mut renderer = Renderer::new(&context, format);
renderer.set_unlit(true);
// 3. Material : On utilise renderer.device() et renderer.format() // 3. Material : On utilise renderer.device() et renderer.format()
let material = Material::new(renderer.format(), "basic", &mut cache); let material = Material::new(renderer.format(), "standard", &mut cache);
// Mesh : On utilise le device du renderer // Mesh : On utilise le device du renderer
let vertices = [ let vertices = [
+9 -5
View File
@@ -16,9 +16,11 @@ impl AppHandler for MonQuad {
fn setup(&mut self, app: &mut wsg_lib::App) { fn setup(&mut self, app: &mut wsg_lib::App) {
let format = app.renderer().format(); let format = app.renderer().format();
// Enregistrement du shader, création du matériau et du mesh du quad (sans importer wgpu). // Exemple 2D plat : le shader `standard` en mode **unlit** (options.x = 1) renvoie la couleur
// du vertex telle quelle. Ainsi le 2D est un cas particulier du 3D — un seul pipeline pour tous.
app.renderer_mut().set_unlit(true);
app.cache() app.cache()
.register_shader("basic", wsg_lib::utils::BASIC_SHADER_PATH) .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap(); .unwrap();
let vertices = [ let vertices = [
Vertex { Vertex {
@@ -53,12 +55,14 @@ impl AppHandler for MonQuad {
&vertices, &vertices,
Some(&indices), Some(&indices),
)); ));
let material = Arc::new(Material::new(format, "basic", app.cache())); let material = Arc::new(Material::new(format, "standard", app.cache()));
app.scene.add_mesh("quad_mesh", mesh).unwrap(); app.scene.add_mesh("quad_mesh", mesh).unwrap();
app.scene.add_material("basic_material", material).unwrap();
app.scene app.scene
.add_entity("quad", "quad_mesh", "basic_material") .add_material("standard_material", material)
.unwrap();
app.scene
.add_entity("quad", "quad_mesh", "standard_material")
.unwrap(); .unwrap();
} }
} }
+10
View File
@@ -72,6 +72,16 @@ impl App {
.expect("renderer not initialized yet — call app.run(handler) first") .expect("renderer not initialized yet — call app.run(handler) first")
} }
/// Returns a mutable reference to the GPU renderer.
/// Panics if called before `App::run` has created the renderer (i.e. before `resumed` fires).
/// Callers can configure the renderer here, e.g. `app.renderer_mut().set_unlit(true)` in `setup`
/// to select flat 2D rendering (DRAFT Étape 5).
pub fn renderer_mut(&mut self) -> &mut Renderer {
self.renderer
.as_mut()
.expect("renderer not initialized yet — call app.run(handler) first")
}
/// Returns a reference to the GPU hardware context. /// Returns a reference to the GPU hardware context.
/// Panics if called before `App::run` has created the context (i.e. before `resumed` fires). /// Panics if called before `App::run` has created the context (i.e. before `resumed` fires).
pub fn context(&self) -> &Context { pub fn context(&self) -> &Context {
+35 -5
View File
@@ -55,6 +55,10 @@ pub struct Renderer {
/// entity label. Needed because `render_scene(&self, &Scene)` is immutable; the model matrix is /// entity label. Needed because `render_scene(&self, &Scene)` is immutable; the model matrix is
/// rewritten each frame for every entity. /// rewritten each frame for every entity.
object_cache: RefCell<HashMap<String, (wgpu::Buffer, wgpu::BindGroup)>>, object_cache: RefCell<HashMap<String, (wgpu::Buffer, wgpu::BindGroup)>>,
/// Flat (unlit) rendering flag, exposed via [`Renderer::set_unlit`]. When true, `options.x` of the
/// `FrameUniforms` is set to 1 so the `standard` shader returns vertex colors as-is — flat 2D
/// rendering is thus a special case of the 3D lit path (DRAFT Étape 5). Defaults to `false` (lit).
unlit: bool,
} }
impl Renderer { impl Renderer {
@@ -72,14 +76,12 @@ impl Renderer {
// Shared frame uniforms: identity camera + white directional light, lit mode by default. // Shared frame uniforms: identity camera + white directional light, lit mode by default.
// Values become meaningful once an active camera is wired (Étape 4.3); for now the default // Values become meaningful once an active camera is wired (Étape 4.3); for now the default
// is a coherent scene when a shader actually reads them, and irrelevant to shaders that don't. // is a coherent scene when a shader actually reads them, and irrelevant to shaders that don't.
let default_frame = FrameUniforms::default();
let frame_buffer = device.create_buffer(&wgpu::BufferDescriptor { let frame_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frame uniform buffer"), label: Some("frame uniform buffer"),
size: FRAME_UNIFORMS_SIZE, size: FRAME_UNIFORMS_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false, mapped_at_creation: false,
}); });
queue.write_buffer(&frame_buffer, 0, bytemuck::bytes_of(&default_frame));
let frame_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let frame_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("frame bind group"), label: Some("frame bind group"),
layout: &frame_layout, layout: &frame_layout,
@@ -109,7 +111,7 @@ impl Renderer {
}], }],
}); });
Self { let renderer = Self {
queue, queue,
device, device,
format, format,
@@ -118,7 +120,35 @@ impl Renderer {
frame_bind_group, frame_bind_group,
shared_object_bind_group, shared_object_bind_group,
object_cache: RefCell::new(HashMap::new()), object_cache: RefCell::new(HashMap::new()),
} unlit: false,
};
// 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();
renderer
}
/// Writes the shared per-frame uniform buffer using an identity camera (view = proj = identity)
/// and the current [`Renderer::set_unlit`] flag. This is the initial state for the low-level
/// `render` path, which is independent of any window and therefore has no camera or aspect ratio.
/// Called at construction and whenever the renderer transitions between lit and unlit mode.
fn write_default_frame_uniforms(&self) {
let frame = FrameUniforms {
options: [if self.unlit { 1 } else { 0 }, 0, 0, 0],
..FrameUniforms::default()
};
self.queue
.write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame));
}
/// Toggles flat (unlit) rendering. When true, the `standard` shader returns vertex colors as-is
/// (`options.x = 1`), so flat 2D rendering is a special case of the 3D lit path (DRAFT Étape 5 :
/// « 2D ⊂ 3D »). Rewrites the shared frame buffer immediately so the low-level `render` path picks
/// up the change ; the `render_scene` path reads the flag each frame in `write_frame_uniforms`.
/// Inputs: unlit — true for flat rendering, false (default) for Phong-lit rendering.
pub fn set_unlit(&mut self, unlit: bool) {
self.unlit = unlit;
self.write_default_frame_uniforms();
} }
/// Rewrites the shared per-frame uniform buffer from the scene's active camera and the current /// Rewrites the shared per-frame uniform buffer from the scene's active camera and the current
@@ -135,7 +165,7 @@ impl Renderer {
cam_pos: camera.position.extend(1.0), cam_pos: camera.position.extend(1.0),
light_dir: Vec4::new(0.0, 0.0, 1.0, 0.0), light_dir: Vec4::new(0.0, 0.0, 1.0, 0.0),
light_color: Vec4::ONE, light_color: Vec4::ONE,
options: [0, 0, 0, 0], options: [if self.unlit { 1 } else { 0 }, 0, 0, 0],
}; };
self.queue self.queue
.write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame)); .write_buffer(&self.frame_buffer, 0, bytemuck::bytes_of(&frame));
+1 -1
View File
@@ -22,7 +22,7 @@
//! ```ignore //! ```ignore
//! use wsg_lib::core::{Context, Renderer}; //! use wsg_lib::core::{Context, Renderer};
//! use wsg_lib::resources::{Mesh, Material, Vertex}; //! use wsg_lib::resources::{Mesh, Material, Vertex};
//! use wsg_lib::utils::BASIC_SHADER; //! use wsg_lib::utils::STANDARD_SHADER;
//! ``` //! ```
// Warn if a public API item has no rustdoc comment, keeping API coverage at 100%. // Warn if a public API item has no rustdoc comment, keeping API coverage at 100%.
+2 -2
View File
@@ -6,11 +6,11 @@ The `pipeline` module contains the shader compilation cache that avoids duplicat
| File | Responsibility | | File | Responsibility |
|------|---------------| |------|---------------|
| **pipeline_cache** | PipelineCache — maps (shader_id, format) keys to compiled RenderPipelines. Loads WGSL from disk or falls back to embedded BASIC_SHADER constant. Creates pipelines on-demand via build_pipeline(). | | **pipeline_cache** | PipelineCache — maps (shader_id, format) keys to compiled RenderPipelines. Loads WGSL from disk or falls back to embedded STANDARD_SHADER constant. Creates pipelines on-demand via build_pipeline(). |
## Interaction with Other Modules ## Interaction with Other Modules
- **utils::conf**: Provides BASIC_SHADER_PATH (disk path) and BASIC_SHADER (embedded fallback). - **utils::conf**: Provides STANDARD_SHADER_PATH (disk path) and STANDARD_SHADER (embedded fallback).
- **resources::vertex**: Vertex struct field offsets define the CPU-side layout that build_pipeline() uses as the vertex buffer contract. - **resources::vertex**: Vertex struct field offsets define the CPU-side layout that build_pipeline() uses as the vertex buffer contract.
- **resources::material**: Material::new() calls get_or_create() during construction to obtain a shared RenderPipeline. - **resources::material**: Material::new() calls get_or_create() during construction to obtain a shared RenderPipeline.
+1 -1
View File
@@ -6,7 +6,7 @@
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - `material` calls `get_or_create()` during its own construction to obtain a shared RenderPipeline. //! - `material` calls `get_or_create()` during its own construction to obtain a shared RenderPipeline.
//! - `conf::BASIC_SHADER` provides fallback WGSL source when an external file is not found. //! - `conf::STANDARD_SHADER` provides fallback WGSL source when an external file is not found.
//! - `vertex::Vertex` defines the CPU-side layout that `build_pipeline()` uses as the vertex buffer contract. //! - `vertex::Vertex` defines the CPU-side layout that `build_pipeline()` uses as the vertex buffer contract.
pub mod pipeline_cache; pub mod pipeline_cache;
+5 -5
View File
@@ -7,7 +7,7 @@
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - **Material** calls `get_or_create()` during its own construction to obtain a shared RenderPipeline. //! - **Material** calls `get_or_create()` during its own construction to obtain a shared RenderPipeline.
//! - **conf::BASIC_SHADER** provides fallback WGSL source when an external file is not found. //! - **conf::STANDARD_SHADER** provides fallback WGSL source when an external file is not found.
//! - **vertex::Vertex** defines the CPU-side layout that `build_pipeline` uses as the vertex buffer contract. //! - **vertex::Vertex** defines the CPU-side layout that `build_pipeline` uses as the vertex buffer contract.
//! //!
//! ## Technical Points //! ## Technical Points
@@ -16,7 +16,7 @@
//! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer. //! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer.
use crate::resources::Vertex; use crate::resources::Vertex;
use crate::utils::BASIC_SHADER; use crate::utils::STANDARD_SHADER;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
@@ -79,7 +79,7 @@ impl PipelineCache {
device, device,
pipelines: HashMap::new(), pipelines: HashMap::new(),
// Maps shader IDs to file paths on disk for WGSL loading in load_shader(). // Maps shader IDs to file paths on disk for WGSL loading in load_shader().
// When a path exists, it reads from it; otherwise falls back to BASIC_SHADER constant. // When a path exists, it reads from it; otherwise falls back to STANDARD_SHADER constant.
shader_paths: HashMap::new(), shader_paths: HashMap::new(),
} }
} }
@@ -139,13 +139,13 @@ impl PipelineCache {
pipeline_arc pipeline_arc
} }
/// Loads a WGSL shader module: reads from disk first, falls back to the embedded BASIC_SHADER constant. /// Loads a WGSL shader module: reads from disk first, falls back to the embedded STANDARD_SHADER constant.
/// Inputs: device (GPU command source for shader compilation), path (file path or shader_id string). /// Inputs: device (GPU command source for shader compilation), path (file path or shader_id string).
/// Returns a compiled wgpu::ShaderModule. Called internally by `get_or_create()` when compiling a new pipeline. /// Returns a compiled wgpu::ShaderModule. Called internally by `get_or_create()` when compiling a new pipeline.
fn load_shader(&self, device: &wgpu::Device, path: &str) -> wgpu::ShaderModule { fn load_shader(&self, device: &wgpu::Device, path: &str) -> wgpu::ShaderModule {
let source = std::fs::read_to_string(path).unwrap_or_else(|_| { let source = std::fs::read_to_string(path).unwrap_or_else(|_| {
println!("Shader not found: {}, falling back to default", path); println!("Shader not found: {}, falling back to default", path);
BASIC_SHADER.to_string() STANDARD_SHADER.to_string()
}); });
device.create_shader_module(wgpu::ShaderModuleDescriptor { device.create_shader_module(wgpu::ShaderModuleDescriptor {
+1 -1
View File
@@ -14,4 +14,4 @@ The `resources` module defines three immutable data types that flow through the
- **pipeline**: build_pipeline() reads Vertex field offsets to construct VertexBufferLayout attributes array. - **pipeline**: build_pipeline() reads Vertex field offsets to construct VertexBufferLayout attributes array.
- **scene**: Scene stores Arc<Mesh> and Arc<Material> instances keyed by identifier strings. - **scene**: Scene stores Arc<Mesh> and Arc<Material> instances keyed by identifier strings.
- **utils**: Mesh creation uses BASIC_SHADER fallback when external shader files are missing. - **utils**: PipelineCache uses the embedded STANDARD_SHADER fallback when external shader files are missing.
+12 -25
View File
@@ -2,39 +2,24 @@
## Overview ## Overview
Contains WGSL shader source files used by the PipelineCache module. These are loaded at runtime from disk when referenced by their registered ID in PipelineCache.register_shader(). If a file is missing, PipelineCache falls back to the embedded BASIC_SHADER constant defined in utils::conf. Contains WGSL shader source files used by the PipelineCache module. These are loaded at runtime from
disk when referenced by their registered ID in PipelineCache.register_shader(). If a file is missing,
PipelineCache falls back to the embedded STANDARD_SHADER constant defined in utils::conf.
Depuis l'Étape 5, il n'existe plus qu'**un seul shader** : `standard_shader.wgsl` (Phong). L'ancien
`basic_shader.wgsl` a été supprimé comme pipeline séparé — le rendu 2D plat est désormais la **variante
unlit** de `standard` (décision actée dans le DRAFT : « 2D ⊂ 3D »).
## Files ## Files
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| **basic_shader.wgsl** | Legacy flat/unlit vertex/fragment shader pair (vs_main / fs_main) with position, uv, and color attributes. Scheduled to be replaced by the unlit variant of `standard_shader.wgsl` (DRAFT Étape 2.3 / 5). |
| **standard_shader.wgsl** | Standard (Phong) vertex/fragment shader — ambient + directional diffuse with an explicit unlit mode. Carries the full uniform contract (frame @group(0) + object @group(1)). | | **standard_shader.wgsl** | Standard (Phong) vertex/fragment shader — ambient + directional diffuse with an explicit unlit mode. Carries the full uniform contract (frame @group(0) + object @group(1)). |
## Shader Contract (basic_shader.wgsl)
The WGSL shader defines:
- `@vertex fn vs_main(model: VertexInput) -> VertexOutput` — vertex entry point
- `@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>` — fragment entry point writing RGBA output
### Vertex Input Layout
| Location | Attribute | Type | Offset (bytes) |
|----------|-----------|------|----------------|
| 0 | position | vec3<f32> | 0 |
| 1 | uv | vec2<f32> | 12 |
| 2 | color | vec3<f32> | 24 |
**Note**: This shader uses a 39-byte vertex stride (3+2+3 floats). It does NOT include normal data or alpha channel interpolation — it outputs fully opaque geometry with per-vertex color passthrough. This differs from the full `Vertex` struct layout (56 bytes with normal + alpha) defined in resources::Vertex; if a full shader matching the Vertex struct is needed, extend this shader accordingly.
> **Statut** : ce shader n'est plus un pipeline séparé ; il est destiné à disparaître au profit de la variante
> unlit de `standard_shader.wgsl` (DRAFT Étape 2.3 / Étape 5, « un seul layout pour tous »).
## Shader Contract (standard_shader.wgsl) ## Shader Contract (standard_shader.wgsl)
`standard_shader.wgsl` est le shader unifié (Phong) de WSG. Il corrige le défaut latente de `basic` `standard_shader.wgsl` est le shader unifié (Phong) de WSG. Il expose les deux bind groups partagés
(contrat vertex incomplet) et expose les deux bind groups partagés par tout matériau. par tout matériau (Étape 3 : un seul layout pour tous).
### Vertex Input Layout (56-byte stride — correspond au `resources::Vertex`) ### Vertex Input Layout (56-byte stride — correspond au `resources::Vertex`)
@@ -57,4 +42,6 @@ The WGSL shader defines:
### Mode unlit ### Mode unlit
Un flag `options.x != 0` neutralise la directionnelle et renvoie la couleur du vertex telle quelle Un flag `options.x != 0` neutralise la directionnelle et renvoie la couleur du vertex telle quelle
(couleur plate). Ainsi le rendu 2D plat est un **cas particulier** de la 3D éclairée. (couleur plate). Côté API, `Renderer::set_unlit(true)` (ou `app.renderer_mut().set_unlit(true)`)
positionne ce flag dans les frame uniforms. Ainsi le rendu 2D plat est un **cas particulier** de la 3D
éclairée.
-42
View File
@@ -1,42 +0,0 @@
//! # Basic Shader Module
//!
//! Default vertex/fragment shader pair used by PipelineCache when no external .wgsl file is found.
//! This shader implements a simple unlit rendering path: passes through position and color attributes
//! from VertexInput to fragment output, producing flat-colored geometry without lighting calculations.
//!
//! ## Shader Contract
//! Must define entry points matching PipelineCache::build_pipeline():
//! - @vertex fn vs_main(model: VertexInput) -> VertexOutput
//! - model.position → @location(0), vec3<f32>, offset 0 bytes in vertex buffer
//! - model.uv → @location(1), vec2<f32>, offset 12 bytes in vertex buffer
//! - model.color → @location(2), vec3<f32>, offset 24 bytes in vertex buffer
//! - @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32>
//! - Writes RGBA output where alpha is hardcoded to 1.0 (fully opaque).
//!
//! ## Technical Notes
//! - No normal or UV interpolation — this is an unlit shader that directly outputs the per-vertex color.
//! - The clip_position is computed as vec4<f32>(position, 1.0), assuming position is already in NDC space.
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
@location(2) color: vec3<f32>,
};
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec3<f32>,
};
@vertex
fn vs_main(model: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(model.position, 1.0);
out.color = model.color; // On transmet la couleur au fragment shader
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color, 1.0);
}
+2 -2
View File
@@ -6,11 +6,11 @@ The `utils` module defines two leaf concepts that other modules consume but have
| File | Responsibility | | File | Responsibility |
|------|---------------| |------|---------------|
| **conf** | Shared constants for shader paths (BASIC_SHADER_PATH) and embedded WGSL source code (BASIC_SHADER). Centralized here so all submodules import from one place instead of duplicating literal strings. Enables PipelineCache to fall back to an embedded default shader when the file-based one is missing. | | **conf** | Shared constants for shader paths (STANDARD_SHADER_PATH) and embedded WGSL source code (STANDARD_SHADER). Centralized here so all submodules import from one place instead of duplicating literal strings. Enables PipelineCache to fall back to an embedded default shader when the file-based one is missing. |
| **error** | WsgError enum — application-level error type mapping specific wgpu failure modes to user-friendly messages via thiserror. Every variant maps a GPU initialization or rendering failure to a recoverable or fatal outcome. | | **error** | WsgError enum — application-level error type mapping specific wgpu failure modes to user-friendly messages via thiserror. Every variant maps a GPU initialization or rendering failure to a recoverable or fatal outcome. |
## Interaction with Other Modules ## Interaction with Other Modules
- **pipeline::pipeline_cache**: load_shader() reads BASIC_SHADER_PATH from disk; falls back to BASIC_SHADER if unreadable. - **pipeline::pipeline_cache**: load_shader() reads STANDARD_SHADER_PATH from disk; falls back to STANDARD_SHADER if unreadable.
- **core::context**: Returns WsgError variants from all fallible methods (new, configure, begin_frame). - **core::context**: Returns WsgError variants from all fallible methods (new, configure, begin_frame).
- **core::renderer**: Does not use errors directly — panics on invalid state rather than returning Result. - **core::renderer**: Does not use errors directly — panics on invalid state rather than returning Result.
+6 -13
View File
@@ -6,26 +6,19 @@
//! Also provides application defaults for window title, width, and height used by AppBuilder. //! Also provides application defaults for window title, width, and height used by AppBuilder.
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - **pipeline_cache::load_shader()** reads `BASIC_SHADER_PATH` from disk; if unreadable, falls back to `BASIC_SHADER`. //! - **pipeline_cache::load_shader()** reads `STANDARD_SHADER_PATH` from disk; if unreadable, falls back to `STANDARD_SHADER`.
//! - Both constants are compile-time values (`include_str!`) ensuring the fallback shader is always available even without external files. //! - Both constants are compile-time values (`include_str!`) ensuring the fallback shader is always available even without external files.
//! - **app::AppBuilder** reads APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH, and APP_DEFAULT_HEIGHT for default window configuration. //! - **app::AppBuilder** reads APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH, and APP_DEFAULT_HEIGHT for default window configuration.
/// Path to the default WGSL shader file on disk (runtime). Used by PipelineCache::load_shader() for file-based loading.
pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
/// The basic WGSL shader source code, embedded at compile time via `include_str!`.
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
pub const BASIC_SHADER: &str = include_str!("../shaders/basic_shader.wgsl");
/// Path to the standard (Phong) WGSL shader file on disk (runtime). Used by PipelineCache::load_shader() /// Path to the standard (Phong) WGSL shader file on disk (runtime). Used by PipelineCache::load_shader()
/// once standardized (Étape 3) : this shader carries the full uniform contract (frame + object bind groups) /// for file-based loading. This is the unified pipeline shader (Étape 3 : un seul layout pour tous) :
/// and supports an unlit mode so flat 2D rendering is a special case of the 3D lit path. /// 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).
pub const STANDARD_SHADER_PATH: &str = "assets/shaders/standard_shader.wgsl"; 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!`. /// The standard (Phong) WGSL shader source code, embedded at compile time via `include_str!`.
/// Not yet compiled by any pipeline (Étape 2 : shader seul, non branché). Becomes the unified /// Serves as the fallback when `STANDARD_SHADER_PATH` cannot be read at runtime. Because every
/// pipeline shader once the uniform infrastructure exists (Étape 3). The unlit variant is the /// pipeline uses the unified layout (frame @0 + object @1), this is the only shader the library ships.
/// replacement for the flat `basic` family.
pub const STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl"); pub const STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl");
/// Default application title displayed in the OS taskbar/window decorations. /// Default application title displayed in the OS taskbar/window decorations.
+3 -3
View File
@@ -5,7 +5,7 @@
//! Both are consumed by other modules but have no internal dependencies on them. //! Both are consumed by other modules but have no internal dependencies on them.
//! //!
//! ## Interaction with Other Modules //! ## Interaction with Other Modules
//! - `pipeline_cache` loads shaders from disk using conf::BASIC_SHADER_PATH; falls back to BASIC_SHADER. //! - `pipeline_cache` loads shaders from disk using conf::STANDARD_SHADER_PATH; falls back to STANDARD_SHADER.
//! - `context` returns WsgError variants from all fallible methods (new, configure, begin_frame). //! - `context` returns WsgError variants from all fallible methods (new, configure, begin_frame).
//! - `renderer` does not use errors directly (panics on invalid state rather than returning Result). //! - `renderer` does not use errors directly (panics on invalid state rather than returning Result).
@@ -13,6 +13,6 @@ pub mod conf;
pub mod error; pub mod error;
// Re-exports // Re-exports
pub use conf::BASIC_SHADER; pub use conf::STANDARD_SHADER;
pub use conf::BASIC_SHADER_PATH; pub use conf::STANDARD_SHADER_PATH;
pub use error::WsgError; pub use error::WsgError;