feat(shaders): add standard Phong shader with uniform contract (data only)
Étape 2 du plan 3D+Phong : nouveau shader unifié, non encore branché à un pipeline (Étape 3 : infra uniforms). - shaders/standard_shader.wgsl : contrat vertex complet (position/normal/uv/color, 56 octets, corrige la défaut latente du basic) ; 2 bind groups formative frame (view/proj/cam_pos/light_dir/light_color/options) + object (model) ; éclairage hemisphérique ambient + diffuse directionnel; mode unlit (options.x) pour que la 2D plate soit un cas partticulier de la 3D. - utils/conf.rs : constantes STANDARD_SHADER_PATH + STANDARD_SHADER (include_str!). - tests/wgsl_validate.rs : validation hors-ligne naga (via wgpu::naga, aucune nouvelle dépendance) tant que le shader n'est pas compilé par un pipeline. - shaders/README.md : documente le contrat standard et le statut du basic. - Erreur WGSL corrigée en validation: cast mat4x4->mat3x3 non supporte, remplacé par construction explicite de la sous-matrice 3x3. - Validation: cargo test (naga OK), check workspace+examples 0 warning, doc OK, fmt OK.
This commit is contained in:
+12
-7
@@ -45,21 +45,26 @@ toucher au rendu (pure façade de données, validable par compilation).
|
|||||||
|
|
||||||
**But** : produire un rendu 3D éclairé via un nouveau shader, sans encore le brancher.
|
**But** : produire un rendu 3D éclairé via un nouveau shader, sans encore le brancher.
|
||||||
|
|
||||||
- [ ] 2.1 **Créer `lib/src/shaders/standard_shader.wgsl`** avec le **contrat vertex correct** :
|
- [X] 2.1 **Créer `lib/src/shaders/standard_shader.wgsl`** avec le **contrat vertex correct** :
|
||||||
`@location(0) position : vec3`, `(1) normal : vec3`, `(2) uv : vec2`, `(3) color : vec4`.
|
`@location(0) position : vec3`, `(1) normal : vec3`, `(2) uv : vec2`, `(3) color : vec4`.
|
||||||
- `@group(0) @binding(0)` : `FrameUniforms { view: mat4, proj: mat4, cam_pos: vec4, light_dir: vec4, light_color: vec4 }`
|
- `@group(0) @binding(0)` : `FrameUniforms { view: mat4, proj: mat4, cam_pos: vec4, light_dir: vec4, light_color: vec4, options: vec4<u32> }` (options.x = unlit flag)
|
||||||
- `@group(1) @binding(0)` : `ObjectUniform { model: mat4 }`
|
- `@group(1) @binding(0)` : `ObjectUniform { model: mat4 }`
|
||||||
- `vs_main` : `clip_position = proj * view * model * vec4(position,1)` ; passe `normal`/`color` en espace monde.
|
- `vs_main` : `clip_position = proj * view * model * vec4(position,1)` ; passe `normal`/`color` en espace monde.
|
||||||
- `fs_main` : éclairage hémisphérique (ambient) + diffuse directionnel (max(dot(N,L),0)), sortie `vec4(color*light, 1)`.
|
- `fs_main` : éclairage hémisphérique (ambient) + diffuse directionnel (max(dot(N,L),0)), sortie `vec4(color*light, 1)`.
|
||||||
- **Mode unlit** : un flag dans `FrameUniforms` (ou `light_color` nul) **neutralise la directionnelle** → couleur
|
- **Mode unlit** : `options.x != 0` neutralise la directionnelle → couleur plate. Ainsi « 2D » = `standard`
|
||||||
plate. Ainsi « 2D » = `standard` non-éclairé, **cas particulier de la 3D** (décision actée).
|
non-éclairé, **cas particulier de la 3D** (décision actée). *(fait — 2026-09-16)*
|
||||||
- [ ] 2.2 **Constantes** : ajouter `STANDARD_SHADER_PATH = "assets/shaders/standard_shader.wgsl"` et
|
- [X] 2.2 **Constantes** : ajouter `STANDARD_SHADER_PATH = "assets/shaders/standard_shader.wgsl"` et
|
||||||
`STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl")` dans `lib/src/utils/conf.rs`.
|
`STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl")` dans `lib/src/utils/conf.rs`.
|
||||||
|
*(fait — 2026-09-16)*
|
||||||
- [ ] 2.3 **Migrer `basic` vers le mode unlit de `standard`** (défaut latente réglée) : plus de pipeline au
|
- [ ] 2.3 **Migrer `basic` vers le mode unlit de `standard`** (défaut latente réglée) : plus de pipeline au
|
||||||
**layout vide séparé**. Le rendu plat = `standard` non-éclairé (identité/ortho + ambiance) sous le **même
|
**layout vide séparé**. Le rendu plat = `standard` non-éclairé (identité/ortho + ambiance) sous le **même
|
||||||
layout uniformisé**. Le fallback embarqué (`BASIC_SHADER`) devient la variante unlit de `standard`.
|
layout uniformisé**. Le fallback embarqué (`BASIC_SHADER`) devient la variante unlit de `standard`.
|
||||||
- [ ] **Validation** : nouveau `shaders/mod.rs` si include_str le requiert ; `cargo check` OK (le shader n'est
|
*(bloqué : dépend des bind groups du `PipelineCache`, infra de l'Étape 3)*
|
||||||
pas encore compilé par un pipeline tant que l'Étape 3 ne le charge pas).
|
- [X] **Validation** : shader validé hors-ligne via un **nouveau test permanent** `lib/tests/wgsl_validate.rs`
|
||||||
|
(naga via `wgpu::naga`, aucune nouvelle dépendance) ; corrigé au passage le cast `mat4x4 -> mat3x3` non
|
||||||
|
supporté (construction de la sous-matrice 3×3 explicite). `cargo test` + `cargo check --workspace --examples`
|
||||||
|
0 warning ; `cargo doc --no-deps` OK ; `cargo fmt` propre. Le shader n'est pas encore compilé par un pipeline
|
||||||
|
(Étape 3). *(fait — 2026-09-16)*
|
||||||
|
|
||||||
## Étape 3 — Infrastructure uniforms dans le `PipelineCache`
|
## Étape 3 — Infrastructure uniforms dans le `PipelineCache`
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ Contains WGSL shader source files used by the PipelineCache module. These are lo
|
|||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| **basic_shader.wgsl** | Default vertex/fragment shader pair (vs_main / fs_main entry points) with position, uv, and color attributes. |
|
| **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)). |
|
||||||
|
|
||||||
## Shader Contract (basic_shader.wgsl)
|
## Shader Contract (basic_shader.wgsl)
|
||||||
|
|
||||||
@@ -26,3 +27,34 @@ The WGSL shader defines:
|
|||||||
| 2 | color | vec3<f32> | 24 |
|
| 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.
|
**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)
|
||||||
|
|
||||||
|
`standard_shader.wgsl` est le shader unifié (Phong) de WSG. Il corrige le défaut latente de `basic`
|
||||||
|
(contrat vertex incomplet) et expose les deux bind groups partagés par tout matériau.
|
||||||
|
|
||||||
|
### Vertex Input Layout (56-byte stride — correspond au `resources::Vertex`)
|
||||||
|
|
||||||
|
| Location | Attribute | Type | Offset (bytes) |
|
||||||
|
|----------|-----------|------|----------------|
|
||||||
|
| 0 | position | vec3<f32> | 0 |
|
||||||
|
| 1 | normal | vec3<f32> | 12 |
|
||||||
|
| 2 | uv | vec2<f32> | 24 |
|
||||||
|
| 3 | color | vec4<f32> | 32 |
|
||||||
|
|
||||||
|
### Uniforms (bind groups)
|
||||||
|
|
||||||
|
| Group / Binding | Struct | Contenu |
|
||||||
|
|-----------------|--------|---------|
|
||||||
|
| `@group(0) @binding(0)` | `FrameUniforms` (192 B) | `view`, `proj`, `cam_pos`, `light_dir`, `light_color`, `options` (.x = unlit flag) |
|
||||||
|
| `@group(1) @binding(0)` | `ObjectUniform` (64 B) | `model` (matrice modèle de l'entité) |
|
||||||
|
|
||||||
|
`light_dir` pointe de la surface vers la lumière ; le fragment shader l'inverse pour le terme N·L.
|
||||||
|
|
||||||
|
### Mode unlit
|
||||||
|
|
||||||
|
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.
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
//! # Standard Shader Module (Phong)
|
||||||
|
//!
|
||||||
|
//! Default lit shading pipeline for WSG. Implements an ambient + directional-diffuse
|
||||||
|
//! (Phong-style) lighting model with an explicit "unlit" mode so that flat 2D rendering
|
||||||
|
//! is a special case of the 3D path (see DRAFT décision actée : « 2D ⊂ 3D »).
|
||||||
|
//!
|
||||||
|
//! ## Uniform Contract
|
||||||
|
//! Two bind groups, shared by every material (one single pipeline layout — voir Étape 3) :
|
||||||
|
//! - `@group(0) @binding(0)` : `FrameUniforms` (per-frame, camera + lights) [192 bytes]
|
||||||
|
//! - `@group(1) @binding(0)` : `ObjectUniform` (per-entity model matrix) [64 bytes]
|
||||||
|
//!
|
||||||
|
//! `FrameUniforms` layout (std140 — each element 16-byte aligned, no padding) :
|
||||||
|
//! | Offset | Field | Type | Meaning |
|
||||||
|
//! |--------|--------------|-----------|-----------------------------------|
|
||||||
|
//! | 0 | view | mat4x4<f32> | Camera view matrix |
|
||||||
|
//! | 64 | proj | mat4x4<f32> | Camera projection matrix |
|
||||||
|
//! | 128 | cam_pos | vec4<f32> | Camera world position (.xyz) |
|
||||||
|
//! | 144 | light_dir | vec4<f32> | Light direction (see below) |
|
||||||
|
//! | 160 | light_color | vec4<f32> | Light color (.rgb) |
|
||||||
|
//! | 176 | options | vec4<u32> | x = unlit flag (1 => flat color) |
|
||||||
|
//! | 192 | total | | |
|
||||||
|
//!
|
||||||
|
//! `light_dir` convention : vector pointing **from the surface toward the light**.
|
||||||
|
//! The fragment shader negates it to obtain the light direction for the N·L term.
|
||||||
|
//!
|
||||||
|
//! ## Vertex Input Layout (matches the full `resources::Vertex` struct, 56-byte stride)
|
||||||
|
//! | Location | Attribute | Type | Offset (bytes) |
|
||||||
|
//! |----------|-----------|----------|----------------|
|
||||||
|
//! | 0 | position | vec3<f32>| 0 |
|
||||||
|
//! | 1 | normal | vec3<f32>| 12 |
|
||||||
|
//! | 2 | uv | vec2<f32>| 24 |
|
||||||
|
//! | 3 | color | vec4<f32>| 32 |
|
||||||
|
//!
|
||||||
|
//! ## Entry Points
|
||||||
|
//! - `@vertex vs_main` : world = model * position ; clip = proj * view * world.
|
||||||
|
//! - `@fragment fs_main` : ambient (hemispheric) + directional diffuse, or flat color when unlit.
|
||||||
|
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position: vec3<f32>,
|
||||||
|
@location(1) normal: vec3<f32>,
|
||||||
|
@location(2) uv: vec2<f32>,
|
||||||
|
@location(3) color: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FrameUniforms {
|
||||||
|
view: mat4x4<f32>,
|
||||||
|
proj: mat4x4<f32>,
|
||||||
|
cam_pos: vec4<f32>,
|
||||||
|
light_dir: vec4<f32>,
|
||||||
|
light_color: vec4<f32>,
|
||||||
|
options: vec4<u32>, // .x : unlit flag (1 = flat color, no directional lighting)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ObjectUniform {
|
||||||
|
model: mat4x4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<uniform> frame: FrameUniforms;
|
||||||
|
@group(1) @binding(0) var<uniform> object: ObjectUniform;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) world_pos: vec3<f32>,
|
||||||
|
@location(1) normal: vec3<f32>,
|
||||||
|
@location(2) color: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
let world = object.model * vec4<f32>(input.position, 1.0);
|
||||||
|
out.clip_position = frame.proj * frame.view * world;
|
||||||
|
out.world_pos = world.xyz;
|
||||||
|
// Model matrix is assumed to contain no non-uniform scale, so the normal is
|
||||||
|
// transformed by the upper-left 3x3 without needing an inverse-transpose.
|
||||||
|
// WGSL n'autorise pas un cast mat4x4 -> mat3x3 ; on construit la sous-matrice
|
||||||
|
// à partir des trois premières colonnes.
|
||||||
|
let normal_matrix = mat3x3<f32>(
|
||||||
|
object.model[0].xyz,
|
||||||
|
object.model[1].xyz,
|
||||||
|
object.model[2].xyz,
|
||||||
|
);
|
||||||
|
out.normal = normal_matrix * input.normal;
|
||||||
|
out.color = input.color;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
// Flat (unlit) mode : pas d'éclairage, couleur du vertex telle quelle.
|
||||||
|
if (frame.options.x != 0u) {
|
||||||
|
return in.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = normalize(in.normal);
|
||||||
|
// light_dir pointe de la surface vers la lumière ; on inverse pour le terme N·L.
|
||||||
|
let l = normalize(-frame.light_dir.xyz);
|
||||||
|
let ndotl = max(dot(n, l), 0.0);
|
||||||
|
|
||||||
|
// Ambient hémisphérique : dépend de la composante verticale de la normale.
|
||||||
|
let sky = max(n.y, 0.0);
|
||||||
|
let ambient = frame.light_color.rgb * (0.3 + 0.4 * sky);
|
||||||
|
|
||||||
|
// Diffuse directionnel classique.
|
||||||
|
let diffuse = frame.light_color.rgb * ndotl;
|
||||||
|
|
||||||
|
let lit = in.color.rgb * (ambient + diffuse);
|
||||||
|
return vec4<f32>(lit, in.color.a);
|
||||||
|
}
|
||||||
@@ -17,6 +17,17 @@ pub const BASIC_SHADER_PATH: &str = "assets/shaders/basic_shader.wgsl";
|
|||||||
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
|
/// Serves as a fallback when `BASIC_SHADER_PATH` cannot be read at runtime.
|
||||||
pub const BASIC_SHADER: &str = include_str!("../shaders/basic_shader.wgsl");
|
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()
|
||||||
|
/// once standardized (Étape 3) : this shader 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.
|
||||||
|
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!`.
|
||||||
|
/// Not yet compiled by any pipeline (Étape 2 : shader seul, non branché). Becomes the unified
|
||||||
|
/// pipeline shader once the uniform infrastructure exists (Étape 3). The unlit variant is the
|
||||||
|
/// replacement for the flat `basic` family.
|
||||||
|
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.
|
||||||
pub const APP_DEFAULT_TITLE: &str = "WSG App";
|
pub const APP_DEFAULT_TITLE: &str = "WSG App";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! # Validation WGSL (naga)
|
||||||
|
//!
|
||||||
|
//! Le shader `standard_shader.wgsl` n'est pas encore chargé par un `RenderPipeline` (voir Étapes 3–5) :
|
||||||
|
//! cette validation hors-ligne via `wgpu::naga` est donc la **seule** garantie de sa validité tant qu'il
|
||||||
|
//! n'est pas branché. Elle protège contre les régressions futures (ré-édition du shader, changement de
|
||||||
|
//! layout) sans nécessiter de contexte GPU.
|
||||||
|
//!
|
||||||
|
//! Aucune nouvelle dépendance n'est introduite : `wgpu` ré-exporte `naga`, déjà dépendance de `wsg-lib`.
|
||||||
|
|
||||||
|
use wgpu::naga;
|
||||||
|
|
||||||
|
/// Parse et valide complètement le shader embarqué `standard_shader.wgsl` via naga.
|
||||||
|
/// Un échec ici signifie que le shader serait rejeté par `Device::create_shader_module` à l'Étape 3.
|
||||||
|
#[test]
|
||||||
|
fn standard_shader_is_valid_wgsl() {
|
||||||
|
let src = include_str!("../src/shaders/standard_shader.wgsl");
|
||||||
|
let module = naga::front::wgsl::parse_str(src)
|
||||||
|
.unwrap_or_else(|e| panic!("standard_shader.wgsl : erreur de parsing : {e:?}"));
|
||||||
|
|
||||||
|
let mut validator = naga::valid::Validator::new(
|
||||||
|
naga::valid::ValidationFlags::all(),
|
||||||
|
naga::valid::Capabilities::all(),
|
||||||
|
);
|
||||||
|
validator
|
||||||
|
.validate(&module)
|
||||||
|
.unwrap_or_else(|e| panic!("standard_shader.wgsl : échec de validation : {e:?}"));
|
||||||
|
|
||||||
|
// Contrat : exactement les deux entrées vs_main / fs_main attendues.
|
||||||
|
assert!(module.entry_points.len() >= 2, "vs_main + fs_main attendus");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user