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:
Generated
+133
@@ -18,6 +18,12 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
@@ -181,6 +187,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
@@ -307,6 +319,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -387,12 +408,32 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide 0.9.1",
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
@@ -565,6 +606,21 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -753,6 +809,36 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "30.0.0"
|
||||
@@ -1237,6 +1323,19 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide 0.8.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
@@ -1316,6 +1415,12 @@ version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.39.4"
|
||||
@@ -1520,6 +1625,12 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
@@ -2432,6 +2543,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"glam",
|
||||
"image",
|
||||
"pollster",
|
||||
"thiserror 2.0.18",
|
||||
"wgpu",
|
||||
@@ -2520,3 +2632,24 @@ dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zlib-rs"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
@@ -13,3 +13,6 @@ thiserror = "2"
|
||||
bytemuck = { version = "1.25.0", features = ["derive"] }
|
||||
glam = { version = "0.33", features = ["bytemuck"] } # feature requis pour Pod/Zeroable sur Mat4/Vec4 (uniform.rs)
|
||||
pollster = { version="1.0.1", features = ["macro"] }
|
||||
# Étape 10 (Textures, DRAFT D3) : décodage d'images (PNG/JPEG) pour charger des textures diffuses.
|
||||
# default-features = false pour n'emporter que les codecs utiles (plus petit arbre de compilation).
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||
|
||||
+47
-15
@@ -1,28 +1,31 @@
|
||||
//! Étape 5 — MVP 3D : un cube unitaire éclairé qui tourne, rendu automatiquement par la boucle `App`.
|
||||
//! Étape 5 — MVP 3D : un cube unitaire éclairé qui tourne ; **Étape 10** — le cube est **texturé**
|
||||
//! (damier procédural) via le nouveau chemin diffues (bind group `@group(2)`).
|
||||
//!
|
||||
//! 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**. Depuis l'Étape 7 la scène possède son `PipelineCache` : on passe par `register_shader` +
|
||||
//! `add_material_shader` + `create_mesh` + `add_entity` (le matériau est lié au mesh, plus de material_id).
|
||||
//! Depuis l'Étape 8 (DRAFT 8.4/8.5) le mesh est déclaré à partir d'une **`Geometry`** (positions, normales,
|
||||
//! indices) plutôt que d'un `&[Vertex]` brut : `Mesh` dérive ses buffers internes via `to_vertices()`.
|
||||
//! `add_material_shader`/`add_material_texture` + `create_mesh` + `add_entity`. Depuis l'Étape 8 le mesh
|
||||
//! est déclaré à partir d'une **`Geometry`** (positions, normales, indices). Depuis l'Étape 10 (D4) on
|
||||
//! enregistre une texture par id (`add_texture`) puis on lie un matériau texturé (`add_material_texture`) ;
|
||||
//! la texture est générée *procéduralement* (damier RGBA 8×8) pour rester autonome, sans asset sur disque.
|
||||
//! 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 wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::resources::Geometry;
|
||||
use wsg_lib::resources::{Geometry, Texture};
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
/// Handler de démonstration : fait tourner le cube dans `update`.
|
||||
/// Handler de démonstration : fait tourner le cube texturé dans `update`.
|
||||
struct Cube {
|
||||
/// Angle de rotation cumulé (radians), incrémenté à chaque frame.
|
||||
angle: f32,
|
||||
}
|
||||
|
||||
/// Construit la `Geometry` 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 absente (défaut blanc opaque via
|
||||
/// `Geometry::to_vertices`), l'UV est laissé à zéro (inutilisé par `standard` pour un matériau sans texture).
|
||||
/// Construit la `Geometry` d'un cube unitaire centré à l'origine (arête de 1), une normale et des
|
||||
/// coordonnées UV par face. 24 sommets (4 par face) + 36 indices ; la couleur est absente (défaut
|
||||
/// blanc opaque via `Geometry::to_vertices`). Depuis l'Étape 10, chaque face reçoit des UV [0,1]² pour
|
||||
/// que la texture diffuse soit proprement projetée sur le cube.
|
||||
fn cube_geometry() -> Geometry {
|
||||
let s = 0.5; // demi-arête
|
||||
// Chaque face : (normale sortante, 4 coins). Le culling est désactivé par défaut (PrimitiveState
|
||||
@@ -57,10 +60,14 @@ fn cube_geometry() -> Geometry {
|
||||
|
||||
let mut positions = Vec::with_capacity(24);
|
||||
let mut normals = Vec::with_capacity(24);
|
||||
let mut uvs = Vec::with_capacity(24);
|
||||
// Mapping UV canonique d'un carré : BL(0,0) BR(1,0) TR(1,1) TL(0,1).
|
||||
let quad_uvs = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
|
||||
for (normal, corners) in faces {
|
||||
for corner in corners {
|
||||
positions.push(corner);
|
||||
for (i, corner) in corners.iter().enumerate() {
|
||||
positions.push(*corner);
|
||||
normals.push(normal);
|
||||
uvs.push(quad_uvs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,20 +80,45 @@ fn cube_geometry() -> Geometry {
|
||||
|
||||
Geometry::new(positions)
|
||||
.with_normals(normals)
|
||||
.with_uvs(uvs)
|
||||
.with_indices(indices)
|
||||
}
|
||||
|
||||
/// Génère un damier RGBA 8×8 (blanc/brique) *procédural*, sans asset sur disque, pour texturer le
|
||||
/// cube (Étape 10, D3/D4). Renvoyé en `Vec<u8>` brut RGBA8, chargeable via `Texture::from_rgba8`.
|
||||
fn checkerboard_rgba() -> Vec<u8> {
|
||||
const SIZE: u32 = 8;
|
||||
let mut rgba = Vec::with_capacity((SIZE * SIZE * 4) as usize);
|
||||
for y in 0..SIZE {
|
||||
for x in 0..SIZE {
|
||||
let even = (x + y) % 2 == 0;
|
||||
let (r, g, b) = if even { (255, 255, 255) } else { (190, 40, 40) };
|
||||
rgba.extend_from_slice(&[r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
rgba
|
||||
}
|
||||
|
||||
impl AppHandler for Cube {
|
||||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||||
// Shader Phong `standard` (porteur des bind groups frame + object). Depuis l'Étape 7 le
|
||||
// PipelineCache vit dans la scène : `register_shader` / `add_material_shader` / `create_mesh`
|
||||
// en sont la façade déclarative (le matériau est lié au mesh au moment du create_mesh).
|
||||
// Shader Phong `standard` (porteur des bind groups frame + object + texture, Étape 10).
|
||||
app.scene
|
||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
// Construit la texture damier avec le device/queue du Context (via `app.context()`), puis
|
||||
// l'enregistre dans la scène par id ; on lie ensuite un matériau texturé à cette id.
|
||||
let (device, queue) = {
|
||||
let ctx = app.context();
|
||||
(ctx.device.clone(), ctx.queue.clone())
|
||||
};
|
||||
let texture =
|
||||
Texture::from_rgba8(&device, &queue, 8, 8, &checkerboard_rgba(), "checker").unwrap();
|
||||
app.scene.add_texture("checker_texture", texture).unwrap();
|
||||
app.scene
|
||||
.add_material_shader("cube_material", "standard")
|
||||
.add_material_texture("cube_material", "standard", "checker_texture")
|
||||
.unwrap();
|
||||
|
||||
app.scene
|
||||
.create_mesh("cube_mesh", cube_geometry(), Some("cube_material"))
|
||||
.unwrap();
|
||||
|
||||
@@ -57,7 +57,7 @@ impl ApplicationHandler for App {
|
||||
|
||||
// 2. Initialisation du Renderer (Il récupère tout ce dont il a besoin)
|
||||
let device = Arc::new(context.device.clone());
|
||||
let mut cache = PipelineCache::new(device);
|
||||
let mut cache = PipelineCache::new(device, context.queue.clone());
|
||||
cache
|
||||
.register_shader("standard", utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
+2
-2
@@ -236,9 +236,9 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
let renderer = Renderer::new(&context, format, self.width, self.height);
|
||||
|
||||
// Étape 7 (DRAFT 7.1) : the PipelineCache now lives in the Scene. We wire the GPU context
|
||||
// (device + format + cache) into the Scene before setup so it can build materials/meshes.
|
||||
// (device + queue + format + cache) into the Scene before setup so it can build materials/meshes.
|
||||
let mut scene = Scene::new();
|
||||
scene.init_gpu(device, format);
|
||||
scene.init_gpu(device, context.queue.clone(), format);
|
||||
|
||||
let mut app = App {
|
||||
scene,
|
||||
|
||||
@@ -389,13 +389,15 @@ fn create_depth_texture(
|
||||
(depth_texture, depth_view)
|
||||
}
|
||||
|
||||
/// Binds a Material pipeline, the two uniform bind groups, and Mesh buffers into an active render
|
||||
/// Binds a Material pipeline, the three shared bind groups, and Mesh buffers into an active render
|
||||
/// pass and issues the draw call. Shared by `Renderer::render` and `Renderer::render_scene`.
|
||||
/// The frame (group 0) and object (group 1) bind groups are **required** by every pipeline layout
|
||||
/// (Étape 3 : un seul layout pour tous) — they must be bound even if the shader does not read them.
|
||||
/// Draws indexed geometry when an index buffer exists, otherwise falls back to a non-indexed draw.
|
||||
/// Inputs: pass (active render pass), mesh (geometry to draw), material (pipeline to bind),
|
||||
/// frame_bind_group (shared per-frame uniforms), object_bind_group (per-entity/identity model).
|
||||
/// The frame (@0), object (@1) and texture (@2) bind groups are **required** by every pipeline layout
|
||||
/// (Étape 3 : un seul layout pour tous — Étape 10 : groupe texture) — they must be bound even if the
|
||||
/// shader does not read them. Draws indexed geometry when an index buffer exists, otherwise falls
|
||||
/// back to a non-indexed draw.
|
||||
/// Inputs: pass (active render pass), mesh (geometry to draw), material (pipeline + texture bind
|
||||
/// group to bind), frame_bind_group (shared per-frame uniforms), object_bind_group (per-entity/identity
|
||||
/// model).
|
||||
fn draw_entity(
|
||||
pass: &mut wgpu::RenderPass<'_>,
|
||||
mesh: &Mesh,
|
||||
@@ -410,6 +412,9 @@ fn draw_entity(
|
||||
pass.set_pipeline(&material.pipeline);
|
||||
pass.set_bind_group(0, frame_bind_group, &[]);
|
||||
pass.set_bind_group(1, object_bind_group, &[]);
|
||||
// Étape 10 (DRAFT 10.4) : groupe texture — le Material possède son bind group (placeholder
|
||||
// blanc s'il n'a pas de texture, D1/D2). Toujours liable car posé sur toutes les pipelines.
|
||||
pass.set_bind_group(2, &material.texture_bind_group, &[]);
|
||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||
if let Some(index_buffer) = &mesh.index_buffer {
|
||||
pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
|
||||
|
||||
@@ -11,4 +11,7 @@
|
||||
|
||||
pub mod pipeline_cache;
|
||||
// Re-exports
|
||||
pub use pipeline_cache::{DEPTH_FORMAT, PipelineCache, create_uniform_bind_group_layouts};
|
||||
pub use pipeline_cache::{
|
||||
DEPTH_FORMAT, PipelineCache, create_texture_bind_group_layout,
|
||||
create_uniform_bind_group_layouts,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
//! - wgpu 30 requires `compilation_options` in VertexState/FragmentState and `depth_slice` in color attachments.
|
||||
//! - **Batching**: Multiple Materials with the same shader_id share one pipeline, enabling material-level batching in Renderer.
|
||||
|
||||
use crate::resources::Vertex;
|
||||
use crate::resources::{Texture, Vertex};
|
||||
use crate::utils::STANDARD_SHADER;
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -59,6 +59,37 @@ pub fn create_uniform_bind_group_layouts(device: &wgpu::Device) -> [wgpu::BindGr
|
||||
]
|
||||
}
|
||||
|
||||
/// Creates the texture bind group layout (group 2) shared by every pipeline (Étape 10, DRAFT D1).
|
||||
/// Binds the diffuse texture + its sampler in the **fragment** stage only. Added to every pipeline
|
||||
/// layout alongside the frame (@0) + object (@1) uniform groups, so « un seul layout pour tous »
|
||||
/// (Étape 3) is preserved: a texture-less `Material` binds the white 1×1 placeholder instead.
|
||||
///
|
||||
/// - `binding 0` : sampler (filtering, linear/repeat — D3).
|
||||
/// - `binding 1` : `texture_2d<f32>` diffuse.
|
||||
pub fn create_texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
|
||||
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("texture_bind_group_layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Depth texture format shared by the whole library (Étape 9, décision D1 du 2026-09-18).
|
||||
///
|
||||
/// Single z-buffer format used for **both** the depth attachment textures (`Renderer`) and the
|
||||
@@ -76,22 +107,68 @@ pub struct PipelineCache {
|
||||
pipelines: HashMap<String, Arc<wgpu::RenderPipeline>>,
|
||||
/// Maps shader IDs to file paths on disk for WGSL loading in `load_shader()`.
|
||||
shader_paths: HashMap<String, String>,
|
||||
/// Shared bind group layout for the texture group (`@group(2)`), used by every pipeline and by
|
||||
/// every Material's texture bind group (Étape 10, DRAFT D1 : « un seul layout pour tous »).
|
||||
texture_bind_group_layout: wgpu::BindGroupLayout,
|
||||
/// White 1×1 placeholder texture bound by materials that have no diffuse texture (DRAFT D1/D2).
|
||||
/// A white texel is the multiplicative identity, so sampling it reproduces the pre-Étape-10 look.
|
||||
placeholder: Arc<Texture>,
|
||||
}
|
||||
|
||||
impl PipelineCache {
|
||||
/// Creates an empty pipeline cache with no pre-loaded shaders or pipelines.
|
||||
/// Inputs: device (owned Arc reference to wgpu Device, required for creating ShaderModules and RenderPipelines).
|
||||
/// Returns a new PipelineCache ready for shader registration via register_shader().
|
||||
/// Creates an empty pipeline cache with no pre-loaded shaders or pipelines, plus the shared
|
||||
/// texture bind group layout (group 2) and the white placeholder texture (Étape 10).
|
||||
/// Inputs: device (owned Arc reference to wgpu Device), queue (used once to upload the white
|
||||
/// placeholder). Returns a new PipelineCache ready for shader registration via register_shader().
|
||||
/// Called at application startup before any Material creation. Shader paths must be registered via register_shader() first.
|
||||
pub fn new(device: Arc<wgpu::Device>) -> Self {
|
||||
pub fn new(device: Arc<wgpu::Device>, queue: wgpu::Queue) -> Self {
|
||||
let placeholder = Texture::white_placeholder(&device, &queue).arc();
|
||||
let texture_bind_group_layout = create_texture_bind_group_layout(&device);
|
||||
Self {
|
||||
device,
|
||||
pipelines: HashMap::new(),
|
||||
// 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 STANDARD_SHADER constant.
|
||||
shader_paths: HashMap::new(),
|
||||
texture_bind_group_layout,
|
||||
placeholder,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the shared white placeholder texture, bound by `Material`s without a diffuse texture.
|
||||
/// Called by `Material` construction (through [`PipelineCache::texture_bind_group`]) and by
|
||||
/// `Scene::get_texture` fallbacks. Étape 10 (DRAFT D1/D2).
|
||||
pub fn placeholder(&self) -> &Arc<Texture> {
|
||||
&self.placeholder
|
||||
}
|
||||
|
||||
/// Returns a reference to the shared group-2 bind group layout (sampler + texture), used by
|
||||
/// every Material to build its texture bind group. Étape 10 (DRAFT D1).
|
||||
pub fn texture_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
|
||||
&self.texture_bind_group_layout
|
||||
}
|
||||
|
||||
/// Builds a group-2 bind group for a Material from its diffuse texture (or the white placeholder
|
||||
/// when `texture` is `None`). Centralizes the sampler+texture binding so `Material` never touches
|
||||
/// wgpu directly (Étape 10, DRAFT D4). Inputs: texture — the material's diffuse texture, `None`
|
||||
/// for a texture-less material (binds the placeholder). Returns the group-2 bind group.
|
||||
pub fn texture_bind_group(&self, texture: Option<Arc<Texture>>) -> wgpu::BindGroup {
|
||||
let tex = texture.unwrap_or_else(|| self.placeholder.clone());
|
||||
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("texture bind group"),
|
||||
layout: &self.texture_bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::Sampler(&tex.sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&tex.view),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
/// Registers an external WGSL shader file path associated with a given ID.
|
||||
/// Inputs: id (unique key for this shader), path (filesystem path to .wgsl file).
|
||||
/// Returns Ok(id) on success or Err(String) if the ID is already registered. Called during scene setup to register custom shaders.
|
||||
@@ -203,12 +280,17 @@ impl PipelineCache {
|
||||
],
|
||||
};
|
||||
|
||||
// Pipeline layout — the two uniform bind groups (frame @0 + object @1) are attached
|
||||
// to EVERY pipeline (Étape 3, décision actée « un seul layout pour tous »), even if a
|
||||
// given shader does not read them. `immediate_size` stays 0 (no var<immediate> used).
|
||||
let bind_group_layouts = create_uniform_bind_group_layouts(device);
|
||||
let layout_refs: Vec<Option<&wgpu::BindGroupLayout>> =
|
||||
bind_group_layouts.iter().map(Some).collect();
|
||||
// Pipeline layout — the two uniform bind groups (frame @0 + object @1) AND the texture
|
||||
// bind group (@2, Étape 10 DRAFT D1) are attached to EVERY pipeline (Étape 3, décision
|
||||
// actée « un seul layout pour tous »), even if a given shader does not read them.
|
||||
// `immediate_size` stays 0 (no var<immediate> used).
|
||||
let uniform_layouts = create_uniform_bind_group_layouts(device);
|
||||
let texture_layout = create_texture_bind_group_layout(device);
|
||||
let layout_refs: Vec<Option<&wgpu::BindGroupLayout>> = vec![
|
||||
Some(&uniform_layouts[0]), // frame @0
|
||||
Some(&uniform_layouts[1]), // object @1
|
||||
Some(&texture_layout), // texture @2
|
||||
];
|
||||
let render_pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("render_pipeline_layout"),
|
||||
|
||||
@@ -1,37 +1,74 @@
|
||||
//! # Material Module — Appearance Descriptor (shader_id → RenderPipeline)
|
||||
//! # Material Module — Appearance Descriptor (shader_id → RenderPipeline + diffuse texture)
|
||||
//!
|
||||
//! Defines `Material`, a lightweight appearance descriptor that pairs a shader identifier with
|
||||
//! a shared RenderPipeline. Materials are created via PipelineCache to ensure pipeline reuse—
|
||||
//! a shared RenderPipeline and, since Étape 10 (DRAFT D4), an optional diffuse `Texture` plus the
|
||||
//! matching group-2 bind group. Materials are created via PipelineCache to ensure pipeline reuse—
|
||||
//! multiple materials referencing the same shader_id point to the identical compiled GPU pipeline.
|
||||
//!
|
||||
//! ## Architecture Notes (per ARCHI_APP.md)
|
||||
//! - **Identifiants**: Each Material is registered in Scene by string identifier, enabling dynamic access
|
||||
//! during the render loop without borrow checker issues. The shader_id serves as the `Handle<T>` key.
|
||||
//! - **Phase de Déclaration**: Materials are instantiated once in the declarative phase before the render loop begins.
|
||||
//! - **Texture (Étape 10, D4)**: the appearance lives on the Material. A texture-less Material binds
|
||||
//! the shared white placeholder (DRAFT D1/D2), so every pipeline layout (`@group(2)`) is satisfied.
|
||||
|
||||
use crate::pipeline::PipelineCache;
|
||||
use crate::resources::Texture;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Lightweight appearance descriptor: links a shader ID to a shared RenderPipeline.
|
||||
/// Does not own the pipeline; holds an Arc for zero-copy sharing across objects using the same shader.
|
||||
/// Lightweight appearance descriptor: links a shader ID to a shared RenderPipeline and an optional
|
||||
/// diffuse texture. Does not own the pipeline; holds an Arc for zero-copy sharing across objects
|
||||
/// using the same shader. Owns its texture bind group (group 2), built at construction.
|
||||
pub struct Material {
|
||||
/// Unique shader identifier used to look up or create a compiled RenderPipeline in PipelineCache.
|
||||
pub shader_id: String,
|
||||
/// Shared reference to the compiled GPU render pipeline. Multiple Materials can share one through Arc cloning.
|
||||
pub pipeline: Arc<wgpu::RenderPipeline>,
|
||||
/// Diffuse texture sampled by this material. `None` → the white placeholder is bound (DRAFT D1/D2).
|
||||
pub texture: Option<Arc<Texture>>,
|
||||
/// Group-2 bind group linking the diffuse texture (or the placeholder) and its sampler. Built in
|
||||
/// the constructor from the shared layout (DRAFT D4) → bound by `draw_entity` at `@group(2)`.
|
||||
pub texture_bind_group: wgpu::BindGroup,
|
||||
}
|
||||
|
||||
impl Material {
|
||||
/// Creates a new Material by requesting the cache to provide (or create) its RenderPipeline.
|
||||
/// Creates a new Material by requesting the cache to provide (or create) its RenderPipeline, and
|
||||
/// building a group-2 texture bind group that binds the **white placeholder** (no diffuse texture).
|
||||
/// Inputs: format (surface texture format required for fragment output), shader_id (unique key into PipelineCache),
|
||||
/// cache (mutable reference for potential insertion of new pipelines).
|
||||
/// Returns a Material holding the Arc-wrapped pipeline. Called at scene initialization time only.
|
||||
pub fn new(format: wgpu::TextureFormat, shader_id: &str, cache: &mut PipelineCache) -> Self {
|
||||
// Request pipeline from cache — returns cached instance if already exists, creates new otherwise
|
||||
Self::build(format, shader_id, None, cache)
|
||||
}
|
||||
|
||||
/// Creates a Material with a diffuse texture: compiles/retrieves the pipeline and builds a
|
||||
/// group-2 texture bind group that samples `texture` (DRAFT D4). Inputs: format, shader_id,
|
||||
/// texture (the diffuse texture to sample), cache. Returns the texturized Material.
|
||||
pub fn new_with_texture(
|
||||
format: wgpu::TextureFormat,
|
||||
shader_id: &str,
|
||||
texture: Arc<Texture>,
|
||||
cache: &mut PipelineCache,
|
||||
) -> Self {
|
||||
Self::build(format, shader_id, Some(texture), cache)
|
||||
}
|
||||
|
||||
/// Shared construction: requests the pipeline from the cache, then builds the group-2 texture
|
||||
/// bind group from `texture` (or the cache placeholder when `None`). The bind group is created
|
||||
/// right here so it exactly matches the shared layout, keeping « un seul layout pour tous » (D1).
|
||||
fn build(
|
||||
format: wgpu::TextureFormat,
|
||||
shader_id: &str,
|
||||
texture: Option<Arc<Texture>>,
|
||||
cache: &mut PipelineCache,
|
||||
) -> Self {
|
||||
let pipeline = cache.get_or_create(format, shader_id);
|
||||
let texture_bind_group = cache.texture_bind_group(texture.clone());
|
||||
Self {
|
||||
shader_id: shader_id.to_string(),
|
||||
pipeline,
|
||||
texture,
|
||||
texture_bind_group,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
pub mod camera;
|
||||
pub mod material;
|
||||
pub mod mesh;
|
||||
pub mod texture;
|
||||
pub mod uniform;
|
||||
pub mod vertex;
|
||||
|
||||
@@ -22,6 +23,7 @@ pub mod vertex;
|
||||
pub use camera::Camera;
|
||||
pub use material::Material;
|
||||
pub use mesh::Mesh;
|
||||
pub use texture::{Texture, TextureError};
|
||||
pub use uniform::{FrameUniforms, ObjectUniform};
|
||||
pub use vertex::Vertex;
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
//! # Texture Module — GPU Diffuse Texture (device + view + sampler)
|
||||
//!
|
||||
//! Defines `Texture`, the GPU representation of a diffuse image: the backing `wgpu::Texture`,
|
||||
//! its `TextureView` (for sampling in the shader) and its `Sampler` (filtering/address mode).
|
||||
//! Added in Étape 10 (DRAFT D3) to texturize the standard shader via bind group `@group(2)`.
|
||||
//!
|
||||
//! ## Architecture Notes (per DRAFT Étape 10, D3/D4)
|
||||
//! - **Format** : `Rgba8UnormSrgb` (espace sRGB, correct pour une couleur diffuse).
|
||||
//! - **Usage** : `TEXTURE_BINDING | COPY_DST` (échantillonnée en fragment, remplie par upload CPU).
|
||||
//! - **Mipmaps** : objet unique (`mip_level_count: 1` — YAGNI, pas de génération de mipmaps à cette étape).
|
||||
//! - **Sampler** : `Linear` + `Repeat` (filtrage doux, coordonnées UV classiques).
|
||||
//! - **Placeholder** : une texture blanche 1×1 (texel identité multiplicative) sert au `Material`
|
||||
//! sans texture — voir `white_placeholder`.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
/// GPU diffuse texture format (DRAFT D3): sRGB 8-bit RGBA. Matches the color space expected for
|
||||
/// sampled diffuse albedo and the fragment bind group layout (`texture_2d<f32>`).
|
||||
pub const TEXTURE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
|
||||
|
||||
/// Errors produced while decoding an image into a `Texture`.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TextureError {
|
||||
/// The image could not be decoded by the `image` crate (corrupt/unsupported file).
|
||||
#[error("failed to decode image: {0}")]
|
||||
Decode(#[from] image::ImageError),
|
||||
/// The pixel buffer chunk (DRW) is empty — nothing to upload.
|
||||
#[error("no pixel data provided to build the texture")]
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// GPU diffuse texture: backing texture, sampling view and sampler. Immutable after creation,
|
||||
/// shared (behind `Arc`) by `Material`s via the Scene resource depot (Étape 10, D4).
|
||||
pub struct Texture {
|
||||
/// Backing GPU image, kept alive for the whole lifetime of the texture.
|
||||
_texture: wgpu::Texture,
|
||||
/// Sampling view of the backing image, bound into the group-2 bind group.
|
||||
pub view: wgpu::TextureView,
|
||||
/// Sampler (filtering + address mode) used to sample the texture in the shader.
|
||||
pub sampler: wgpu::Sampler,
|
||||
}
|
||||
|
||||
impl Texture {
|
||||
/// The core constructor: uploads raw RGBA8 pixels into a `Rgba8UnormSrgb` 2D texture.
|
||||
/// This is the primitive used by `from_bytes`/`from_file` (after decoding) and by
|
||||
/// `white_placeholder`. Inputs: device (GPU), queue (for `write_texture`), width (px),
|
||||
/// height (px), rgba (raw 4-bytes-per-pixel data, `width * height * 4` long), label (debug).
|
||||
/// Returns the texture, or `Err(TextureError::Empty)` if `rgba` is empty.
|
||||
pub fn from_rgba8(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
width: u32,
|
||||
height: u32,
|
||||
rgba: &[u8],
|
||||
label: &str,
|
||||
) -> Result<Self, TextureError> {
|
||||
if rgba.is_empty() {
|
||||
return Err(TextureError::Empty);
|
||||
}
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(label),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1, // YAGNI : pas de mipmaps à cette étape (DRAFT D3)
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: TEXTURE_FORMAT,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
rgba,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(4 * width),
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some(label),
|
||||
address_mode_u: wgpu::AddressMode::Repeat,
|
||||
address_mode_v: wgpu::AddressMode::Repeat,
|
||||
address_mode_w: wgpu::AddressMode::Repeat,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Linear,
|
||||
..Default::default()
|
||||
});
|
||||
Ok(Self {
|
||||
_texture: texture,
|
||||
view,
|
||||
sampler,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes an encoded image (PNG/JPEG via the `image` crate) from a byte slice and uploads it.
|
||||
/// Inputs: device (GPU), queue (write target), label (debug), bytes (encoded image data).
|
||||
/// Returns the decoded+uploaded texture, or `Err(TextureError)` on decode/empty failure.
|
||||
pub fn from_bytes(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
label: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<Self, TextureError> {
|
||||
let img = image::load_from_memory(bytes)?;
|
||||
// Normalise en RGBA8 (sous-échantillonne Luma8/Rgb8 en RGBA8, comme le veut Rgba8UnormSrgb).
|
||||
let rgba = img.to_rgba8();
|
||||
Self::from_rgba8(device, queue, rgba.width(), rgba.height(), &rgba, label)
|
||||
}
|
||||
|
||||
/// Reads a file from `path` and uploads its pixels via [`Texture::from_bytes`].
|
||||
/// Inputs: device (GPU), queue (write target), label (debug), path (image file on disk).
|
||||
/// Returns the texture, or `Err(TextureError)` if the file cannot be read/decoded.
|
||||
pub fn from_file(
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
label: &str,
|
||||
path: &str,
|
||||
) -> Result<Self, TextureError> {
|
||||
let bytes =
|
||||
std::fs::read(path).map_err(|e| TextureError::Decode(image::ImageError::IoError(e)))?;
|
||||
Self::from_bytes(device, queue, label, &bytes)
|
||||
}
|
||||
|
||||
/// Builds the white 1×1 placeholder used by `Material`s without a texture (DRAFT D1/D2).
|
||||
/// A white texel is the multiplicative identity: sampling it leaves the vertex color
|
||||
/// unchanged, so a texture-less material renders exactly as before (no breakage in
|
||||
/// unlit vertex-colored geometry). Inputs: device, queue. Returns the 1×1 white texture.
|
||||
pub fn white_placeholder(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
|
||||
Self::from_rgba8(
|
||||
device,
|
||||
queue,
|
||||
1,
|
||||
1,
|
||||
&[255, 255, 255, 255],
|
||||
"default white texture",
|
||||
)
|
||||
.expect("1×1 white placeholder must not be empty")
|
||||
}
|
||||
|
||||
/// Shared convenience wrapper so `Arc<Texture>` can be created ergonomically by callers.
|
||||
pub(crate) fn arc(self) -> Arc<Texture> {
|
||||
Arc::new(self)
|
||||
}
|
||||
}
|
||||
+60
-6
@@ -17,7 +17,7 @@
|
||||
|
||||
use crate::math::{Geometry, Transform};
|
||||
use crate::pipeline::PipelineCache;
|
||||
use crate::resources::{Camera, Material, Mesh};
|
||||
use crate::resources::{Camera, Material, Mesh, Texture};
|
||||
use crate::scene::Entity;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -45,6 +45,9 @@ pub struct Scene {
|
||||
meshes: HashMap<String, Arc<Mesh>>,
|
||||
/// Map of material identifiers to owned `Arc<Material>` instances. Populated via `add_material()`.
|
||||
materials: HashMap<String, Arc<Material>>,
|
||||
/// Map of diffuse texture identifiers to owned `Arc<Texture>` instances (Étape 10, D4).
|
||||
/// Populated via `add_texture()`; materials reference them via `add_material_texture()` by id.
|
||||
textures: HashMap<String, Arc<Texture>>,
|
||||
/// Map of entity labels to `Entity` associations. Populated via `add_entity()` / `add_entity_with_transform()`.
|
||||
entities: HashMap<String, Entity>,
|
||||
/// Active camera used for rendering. Read each frame by `Renderer::render_scene` to compute the
|
||||
@@ -66,6 +69,7 @@ impl Scene {
|
||||
Self {
|
||||
meshes: HashMap::new(),
|
||||
materials: HashMap::new(),
|
||||
textures: HashMap::new(),
|
||||
entities: HashMap::new(),
|
||||
camera: Camera::default(),
|
||||
gpu: None,
|
||||
@@ -73,17 +77,19 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the GPU-facing pipeline context (device + format + `PipelineCache`) to this Scene,
|
||||
/// enabling it to build materials and meshes itself. Called once during `AppRunner::resumed`,
|
||||
/// Attaches the GPU-facing pipeline context (device + queue + format + `PipelineCache`) to this
|
||||
/// Scene, enabling it to build materials and meshes itself. Called once during `AppRunner::resumed`,
|
||||
/// just after the `Context`/`Renderer` are created and **before** `AppHandler::setup`, so setup
|
||||
/// can register shaders/materials/meshes/entities using `self`. Returns `&mut self` for chaining.
|
||||
/// Inputs: device — shared GPU device (Arc clone); format — surface texture output format.
|
||||
/// can register shaders/materials/textures/meshes/entities using `self`. Returns `&mut self` for chaining.
|
||||
/// Inputs: device — shared GPU device (Arc clone); queue — GPU command queue (used to build the
|
||||
/// geometry); format — surface texture output format.
|
||||
pub fn init_gpu(
|
||||
&mut self,
|
||||
device: Arc<wgpu::Device>,
|
||||
queue: wgpu::Queue,
|
||||
format: wgpu::TextureFormat,
|
||||
) -> &mut Self {
|
||||
let cache = PipelineCache::new(device.clone());
|
||||
let cache = PipelineCache::new(device.clone(), queue.clone());
|
||||
self.gpu = Some(SceneGpu {
|
||||
device,
|
||||
format,
|
||||
@@ -133,6 +139,54 @@ impl Scene {
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
/// Registers a diffuse texture in the Scene's resource depot under a unique identifier, so
|
||||
/// materials can reference it declaratively (Étape 10, D4). The texture is wrapped in `Arc` for
|
||||
/// zero-copy sharing across materials. Returns Ok(id) or Err(String) if the id already exists.
|
||||
/// Inputs: id (unique identifier), texture (GPU diffuse texture to register).
|
||||
pub fn add_texture(&mut self, id: &str, texture: Texture) -> Result<String, String> {
|
||||
if self.textures.contains_key(id) {
|
||||
return Err(format!("Texture ID '{}' already exists.", id));
|
||||
}
|
||||
self.textures.insert(id.to_string(), Arc::new(texture));
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
/// Retrieves a registered diffuse texture by its identifier, if present. Called by the user to
|
||||
/// read back a texture (or by internals when resolving material↔texture links). Étape 10 (D4).
|
||||
pub fn get_texture(&self, id: &str) -> Option<&Arc<Texture>> {
|
||||
self.textures.get(id)
|
||||
}
|
||||
|
||||
/// Builds and registers a Material from a shader id **and** a diffuse texture registered via
|
||||
/// [`Scene::add_texture`]. The material samples `texture_id` (Étape 10, D4). Returns Ok(id) or
|
||||
/// Err(String) if the material id exists or the texture id does not. Inputs: id (material id to
|
||||
/// register), shader_id (pipeline key), texture_id (existing texture id in this Scene).
|
||||
pub fn add_material_texture(
|
||||
&mut self,
|
||||
id: &str,
|
||||
shader_id: &str,
|
||||
texture_id: &str,
|
||||
) -> Result<String, String> {
|
||||
if self.materials.contains_key(id) {
|
||||
return Err(format!("Material ID '{}' already exists.", id));
|
||||
}
|
||||
let texture = self
|
||||
.textures
|
||||
.get(texture_id)
|
||||
.ok_or_else(|| format!("Texture '{}' does not exist.", texture_id))?
|
||||
.clone();
|
||||
let mut cache = self.gpu().cache.borrow_mut();
|
||||
let material = Arc::new(Material::new_with_texture(
|
||||
self.gpu().format,
|
||||
shader_id,
|
||||
texture,
|
||||
&mut cache,
|
||||
));
|
||||
drop(cache);
|
||||
self.materials.insert(id.to_string(), material);
|
||||
Ok(id.to_string())
|
||||
}
|
||||
|
||||
/// Builds, (optionally) links to a Material, and registers a Mesh in one declarative call.
|
||||
/// Since Étape 8 the mesh is declared from a CPU `Geometry` (DRAFT Étape 8, D4) instead of raw
|
||||
/// `&[Vertex]`. This builds the shared `Arc<Geometry>` and creates the GPU buffers via
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user