feat(examples): 3D MVP cube via standard shader, drop basic (Étape 5)
This commit is contained in:
@@ -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 })
|
||||||
|
}
|
||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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%.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user