440f2dffa3
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.
147 lines
6.0 KiB
Rust
147 lines
6.0 KiB
Rust
//! É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`/`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, Texture};
|
||
use wsg_lib::utils::WsgError;
|
||
|
||
/// 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 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
|
||
// 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 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 (i, corner) in corners.iter().enumerate() {
|
||
positions.push(*corner);
|
||
normals.push(normal);
|
||
uvs.push(quad_uvs[i]);
|
||
}
|
||
}
|
||
|
||
// 2 triangles par face, 36 indices.
|
||
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]);
|
||
}
|
||
|
||
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 + 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_texture("cube_material", "standard", "checker_texture")
|
||
.unwrap();
|
||
|
||
app.scene
|
||
.create_mesh("cube_mesh", cube_geometry(), Some("cube_material"))
|
||
.unwrap();
|
||
app.scene.add_entity("cube", "cube_mesh").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 })
|
||
}
|