refactor(resources): activate diffuse textures on all render paths (Étape 10)

Implémente le plan Étape 10 (Phase 4.1 Textures), décisions D1-D4 actées :
- 10.1 : nouveau type resources::Texture (device+view+sampler), format
  Rgba8UnormSrgb, sampler linear/repeat, dep 'image' (png/jpeg). Constructeurs
  from_rgba8 / from_bytes / from_file / white_placeholder.
- 10.2 : create_texture_bind_group_layout (groupe 2 : sampler+texture, fragment).
  build_pipeline pose désormais 3 layouts [frame, object, texture] — « un seul
  layout pour tous » (D1). PipelineCache détient le layout + le placeholder blanc.
- 10.3 : shader standard — UV transmis au fragment (location 2), groupe @2
  texture_sampler + diffuse_texture, échantillonnage inconditionnel
  base = texel * couleur(vertex) (D2) : sans texture (placeholder blanc) pas
  de régression en lit comme en unlit.
- 10.4 : Material gagne texture: Option<Arc<Texture>> + texture_bind_group,
  construit dans le constructeur via le cache (layout partagé + placeholder).
- 10.5 : draw_entity bind @group(2) ; Scene : add_texture / get_texture /
  add_material_texture ; init_gpu accepte la Queue pour bâtir le placeholder.
- exemple cube : géométrie avec UV [0,1]² par face + texture damier procédurale.

Validation : fmt, check 0 warning, tests verts (3 + doc), doc sans missing_docs,
cube/simple/manual lancés sans erreur backend.
This commit is contained in:
Jérôme Bousquié
2026-09-18 14:18:02 +02:00
parent 3de84aa4dc
commit 440f2dffa3
13 changed files with 588 additions and 55 deletions
+29 -7
View File
@@ -1,13 +1,17 @@
//! # Standard Shader Module (Phong)
//! # Standard Shader Module (Phong + diffuse texture)
//!
//! 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 »).
//! Since Étape 10 (DRAFT D2) the fragment can also sample a diffuse texture whose texel
//! modulates the vertex color (`texel.rgb * in.color.rgb`).
//!
//! ## Uniform Contract
//! Two bind groups, shared by every material (one single pipeline layout — voir Étape 3) :
//! Three 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]
//! - `@group(2) @binding(0)` : `texture_sampler` (sampler) — diffuse (Étape 10)
//! - `@group(2) @binding(1)` : `diffuse_texture` (texture_2d<f32>) (Étape 10)
//!
//! `FrameUniforms` layout (std140 — each element 16-byte aligned, no padding) :
//! | Offset | Field | Type | Meaning |
@@ -23,6 +27,12 @@
//! `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.
//!
//! ## Texturing (Étape 10, D2)
//! The fragment samples `diffuse_texture` **unconditionally**. A texture-less `Material` binds the
//! white 1×1 placeholder (texel = `[1,1,1]`), which is the multiplicative identity: `texel * color`
//! leaves the vertex color unchanged, exactly reproducing the pre-Étape-10 look in both lit and
//! unlit modes. A real texture tints/multiplies the vertex color.
//!
//! ## Vertex Input Layout (matches the full `resources::Vertex` struct, 56-byte stride)
//! | Location | Attribute | Type | Offset (bytes) |
//! |----------|-----------|----------|----------------|
@@ -33,7 +43,7 @@
//!
//! ## Entry Points
//! - `@vertex vs_main` : world = model * position ; clip = proj * view * world.
//! - `@fragment fs_main` : ambient (hemispheric) + directional diffuse, or flat color when unlit.
//! - `@fragment fs_main` : base = texel * vertex color; × (ambient + diffuse) when lit, or base when unlit.
struct VertexInput {
@location(0) position: vec3<f32>,
@@ -57,12 +67,17 @@ struct ObjectUniform {
@group(0) @binding(0) var<uniform> frame: FrameUniforms;
@group(1) @binding(0) var<uniform> object: ObjectUniform;
// Étape 10 (DRAFT D1) : groupe texture — sampler (0) + texture diffuse (1). Un matériau sans
// texture lie le placeholder blanc 1×1 (D2), d'où l'échantillonnage inconditionnel.
@group(2) @binding(0) var texture_sampler: sampler;
@group(2) @binding(1) var diffuse_texture: texture_2d<f32>;
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) world_pos: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) color: vec4<f32>,
@location(2) uv: vec2<f32>,
@location(3) color: vec4<f32>,
};
@vertex
@@ -81,15 +96,22 @@ fn vs_main(input: VertexInput) -> VertexOutput {
object.model[2].xyz,
);
out.normal = normal_matrix * input.normal;
out.uv = input.uv;
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.
// Étape 10 (D2) : échantillonnage inconditionnel. Le texel module la couleur du vertex
// (base = texel * color). Avec le placeholder blanc (texel = 1), base == vertex color :
// aucune régression pour les matériaux sans texture, en lit comme en unlit.
let texel = textureSample(diffuse_texture, texture_sampler, in.uv);
let base = texel.rgb * in.color.rgb;
// Flat (unlit) mode : pas d'éclairage, texel * couleur du vertex telle quelle.
if (frame.options.x != 0u) {
return in.color;
return vec4<f32>(base, in.color.a);
}
let n = normalize(in.normal);
@@ -104,6 +126,6 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// Diffuse directionnel classique.
let diffuse = frame.light_color.rgb * ndotl;
let lit = in.color.rgb * (ambient + diffuse);
let lit = base * (ambient + diffuse);
return vec4<f32>(lit, in.color.a);
}