réorg doc
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# Meshes, Materials & Import
|
||||
|
||||
Examples covering **geometry and materials**: the minimal workflow, the 3D MVP,
|
||||
PBR shading, file import, and the low-level (non-`App`) workflow.
|
||||
|
||||
| Example | Run command | What it shows |
|
||||
|---------|-------------|---------------|
|
||||
| `simple` | `cargo run -p wsg-lib --example simple` | The minimal declarative workflow: a flat two-tone quad, **unlit**, rendered automatically |
|
||||
| `cube` | `cargo run -p wsg-lib --example cube` | The 3D MVP: a textured (checkerboard) cube, lit (directional + point + spot), spinning |
|
||||
| `pbr` | `cargo run -p wsg-lib --example pbr` | PBR metallic/roughness + normal mapping |
|
||||
| `import` | `cargo run -p wsg-lib --example import --features import-obj` | OBJ file import (non-graphical, prints stats to stdout) |
|
||||
| `manual` | `cargo run -p wsg-lib --example manual` | The **advanced** workflow: `Context`/`Renderer`/`PipelineCache` driven by hand, without the `App` facade |
|
||||
|
||||
> All commands run from the repo root. The `import` example additionally
|
||||
> requires the `import-obj` Cargo feature.
|
||||
|
||||
---
|
||||
|
||||
## `simple` — Minimal Declarative Workflow
|
||||
|
||||
The "15 lines, no wgpu" model: `AppBuilder` creates the window + GPU, and
|
||||
`App::run` drives the update → render → present loop. The scene renders
|
||||
**automatically** — the default `AppHandler::render` calls
|
||||
`app.render_scene(frame.view())`.
|
||||
|
||||
The mesh is a flat two-tone quad declared from a `Geometry` (per-vertex
|
||||
positions + colors) and drawn with the `standard` shader in **unlit** mode
|
||||
(`renderer.set_unlit(true)`): flat 2D is a special case of 3D, one pipeline for all.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example simple
|
||||
```
|
||||
|
||||
No keys — static render.
|
||||
|
||||
---
|
||||
|
||||
## `cube` — The 3D MVP
|
||||
|
||||
A lit unit cube that rotates, **textured** with a procedural 8×8 checkerboard
|
||||
via the diffuse path (bind group `@group(2)`). Follows the declarative workflow
|
||||
(like `simple`): `AppBuilder` + automatic scene, **no wgpu import**. The texture
|
||||
is generated procedurally (RGBA bytes → `Texture::from_rgba8`) to stay
|
||||
self-contained; the default camera at (0, 0, 3) frames the cube, and
|
||||
`update()` rotates the entity via `set_entity_transform` each frame.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example cube
|
||||
```
|
||||
|
||||
No keys — the cube spins on its own.
|
||||
|
||||
---
|
||||
|
||||
## `pbr` — PBR Metallic/Roughness + Normal Mapping
|
||||
|
||||
Demonstrates the Cook-Torrance PBR workflow: GGX distribution + Smith geometry +
|
||||
Schlick Fresnel + hemispheric IBL + normal mapping.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example pbr
|
||||
```
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| Drag (LMB) | Orbit camera |
|
||||
| Wheel | Zoom |
|
||||
| `R` | Reset camera |
|
||||
|
||||
Scene: 6 PBR materials (mirror metal, smooth plastic, rusty metal, ceramic,
|
||||
bump map, matte floor). The bump-map cube shows procedural sin-wave surface
|
||||
detail.
|
||||
|
||||
---
|
||||
|
||||
## `import` — OBJ File Import
|
||||
|
||||
**Non-graphical** example: parses a `.obj` file and prints statistics
|
||||
(vertex count, normals, UVs, indices, bounding box) to stdout.
|
||||
|
||||
```sh
|
||||
# With a file:
|
||||
cargo run -p wsg-lib --example import --features import-obj -- /path/to/model.obj
|
||||
|
||||
# Without argument (demo triangle):
|
||||
cargo run -p wsg-lib --example import --features import-obj
|
||||
```
|
||||
|
||||
No keys — runs and exits.
|
||||
|
||||
---
|
||||
|
||||
## `manual` — Low-level Workflow (no `App` facade)
|
||||
|
||||
Demonstrates the API **without** the `App` facade: direct use of `Context`,
|
||||
`Renderer`, `PipelineCache`, `Mesh`, `Material`. Renders a colored quad (unlit).
|
||||
|
||||
Useful for understanding what the `App` facade encapsulates:
|
||||
|
||||
- `Context` (*Manager* layer): GPU lifecycle — `Instance`/`Surface`/`Adapter`/
|
||||
`Device`/`Queue`, `configure()` for the swapchain, per-frame surface texture.
|
||||
- `Renderer` (*Executor* layer): `render(view, mesh, material)` = one object per
|
||||
submission; `present(frame)`.
|
||||
- `PipelineCache`: `register_shader(id, path)` then `Material::new(format, id, &mut cache)`.
|
||||
|
||||
The window and GPU are created in winit 0.30's `resumed()` callback
|
||||
(`run_app` + `ApplicationHandler`). The two-layer architecture is detailed in
|
||||
[`docs/tech/ARCHI_APP.md`](../../docs/tech/ARCHI_APP.md) and
|
||||
[`FRAME_LOOP.md`](../../docs/tech/FRAME_LOOP.md).
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example manual
|
||||
```
|
||||
|
||||
No keys — static render (unlit quad, 4 colors).
|
||||
|
||||
> **Tip**: start with the declarative workflow. The manual workflow doesn't
|
||||
> render more pixels — it gives more control over command encoding.
|
||||
@@ -0,0 +1,111 @@
|
||||
//! A lit unit cube that rotates, **textured** with a procedural checkerboard via the diffuse path
|
||||
//! (bind group `@group(2)`).
|
||||
//!
|
||||
//! A 3D mesh with Phong lighting on screen — the library's 3D showcase.
|
||||
//! Follows the declarative workflow (like `simple`): `AppBuilder` + automatic scene, **no wgpu import**.
|
||||
//! The scene owns its `PipelineCache`: go through `register_shader` +
|
||||
//! `add_material_shader`/`add_material_texture` + `create_mesh` + `add_entity`. The mesh
|
||||
//! is declared from a **`Geometry`** (positions, normals, indices). A texture is
|
||||
//! registered by id (`add_texture`) and a textured material bound to it (`add_material_texture`);
|
||||
//! the texture is generated *procedurally* (RGBA 8×8 checkerboard) to stay self-contained, no on-disk asset.
|
||||
//! The default active camera (`Scene::default`, position (0,0,3), fov 45°) frames the cube, and
|
||||
//! `AppHandler::update` rotates the entity via `set_entity_transform` each frame.
|
||||
use glam::{Quat, Vec3};
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::mesh::cube;
|
||||
use wsg_lib::resources::Texture;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
/// Demo handler: rotates the textured cube in `update`.
|
||||
struct Cube {
|
||||
/// Cumulative rotation angle (radians), incremented each frame.
|
||||
angle: f32,
|
||||
}
|
||||
|
||||
/// Generates a *procedural* RGBA 8×8 checkerboard (white/brick), no on-disk asset, to texture the
|
||||
/// cube. Returned as a raw RGBA8 `Vec<u8>`, loadable 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) {
|
||||
// Phong shader `standard` (carries the frame + object + texture bind groups).
|
||||
app.scene
|
||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
// Builds the checkerboard texture with the Context's device/queue (via `app.context()`), then
|
||||
// registers it in the scene by id; a textured material is then bound to that 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(1.0), Some("cube_material"))
|
||||
.unwrap();
|
||||
app.scene.add_entity("cube", "cube_mesh").unwrap();
|
||||
|
||||
// In addition to the default directional light (+Z), a warm **point** light
|
||||
// is added in front of the cube. Its halo (linear attenuation over the
|
||||
// radius) is visible on the near face of the cube, on top of the directional lighting.
|
||||
app.scene
|
||||
.add_point_light(
|
||||
Vec3::new(1.0, 0.5, 1.5), // world position, in front/right of the cube
|
||||
[1.0, 0.7, 0.3], // warm tint
|
||||
1.0, // intensity
|
||||
3.0, // attenuation radius
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// A green **spot** light aimed at the cube from the left.
|
||||
// The cone (half-angle ~20°) projects a directed beam onto the cube's faces, with a
|
||||
// smoothed penumbra at the edge and linear attenuation over the radius.
|
||||
app.scene
|
||||
.add_spot_light(
|
||||
Vec3::new(-2.0, 1.0, 1.5), // world position, left/above/behind the camera
|
||||
Vec3::new(2.0, -1.0, -1.5).normalize(), // cone axis, toward the cube (origin)
|
||||
[0.3, 1.0, 0.4], // green tint
|
||||
1.2, // intensity
|
||||
4.0, // attenuation radius
|
||||
0.35, // half-angle (~20°) in radians
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
// Cumulative cube rotation (double axis for a more readable motion).
|
||||
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 })
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! # Example: File Import (OBJ)
|
||||
//!
|
||||
//! Demonstrates loading a Wavefront OBJ file with `wsg_lib::mesh::load_obj`.
|
||||
//! Parses the file and prints geometry statistics.
|
||||
//!
|
||||
//! ## Build & Run
|
||||
//! ```sh
|
||||
//! cargo run -p wsg-lib --example import --features import-obj -- /path/to/model.obj
|
||||
//! ```
|
||||
//!
|
||||
//! Without a file argument, parses a built-in sample triangle.
|
||||
|
||||
use wsg_lib::mesh::import::parse_obj;
|
||||
use wsg_lib::mesh::load_obj;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
let content = if args.len() > 1 {
|
||||
let path = &args[1];
|
||||
eprintln!("Loading: {path}");
|
||||
match load_obj(path) {
|
||||
Ok(geom) => {
|
||||
print_stats(&geom);
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!("No file argument — parsing a built-in sample.");
|
||||
eprintln!("Usage: import <model.obj>");
|
||||
// Built-in sample: a simple triangle with UVs and normals
|
||||
"v 0.0 0.0 0.0\nv 1.0 0.0 0.0\nv 0.5 1.0 0.0\nvn 0 0 1\nvt 0.0 0.0\nvt 1.0 0.0\nvt 0.5 1.0\nf 1/1/1 2/2/1 3/3/1\n"
|
||||
};
|
||||
|
||||
let geom = parse_obj(content).expect("sample should parse");
|
||||
print_stats(&geom);
|
||||
}
|
||||
|
||||
fn print_stats(geom: &wsg_lib::Geometry) {
|
||||
println!("\n=== Geometry Statistics ===");
|
||||
println!(" Vertices: {}", geom.positions.len());
|
||||
if let Some(n) = &geom.normals {
|
||||
println!(" Normals: {}", n.len());
|
||||
}
|
||||
if let Some(uv) = &geom.uvs {
|
||||
println!(" UVs: {}", uv.len());
|
||||
}
|
||||
if let Some(idx) = &geom.indices {
|
||||
println!(" Indices: {} ({} triangles)", idx.len(), idx.len() / 3);
|
||||
}
|
||||
if let Err(e) = geom.validate() {
|
||||
println!(" Validation FAILED: {e}");
|
||||
} else {
|
||||
println!(" Validation: OK");
|
||||
}
|
||||
// Bounding box
|
||||
if let Some(bbox) = geom.bbox() {
|
||||
println!(" BBox min: {:?}", bbox.min);
|
||||
println!(" BBox max: {:?}", bbox.max);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Low-level workflow: direct use of `Context`, `Renderer`, `PipelineCache`, `Mesh` and
|
||||
//! `Material`, bypassing the `App` facade. Renders a flat quad (shader `standard` **unlit**) via the
|
||||
//! winit 0.30 loop (`EventLoop::run_app` + `ApplicationHandler`). The window and the GPU are created
|
||||
//! in `resumed()` (winit 0.30 only exposes the display after resume). The mesh is built via
|
||||
//! `Mesh::from_geometry(device, Arc<Geometry>, None)` from a `Geometry` (positions + colors per vertex).
|
||||
use std::sync::Arc;
|
||||
use winit::application::ApplicationHandler;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
|
||||
use winit::window::{Window, WindowAttributes};
|
||||
use wsg_lib::core::Context;
|
||||
use wsg_lib::core::Frame;
|
||||
use wsg_lib::core::{Renderer, ShadowConfig};
|
||||
use wsg_lib::pipeline::PipelineCache;
|
||||
use wsg_lib::resources::{Geometry, Material, Mesh};
|
||||
use wsg_lib::utils;
|
||||
|
||||
/// Low-level application: holds the GPU objects + window, all created in `resumed`.
|
||||
struct App {
|
||||
/// System window, shared via Arc (as in app.rs).
|
||||
window: Option<Arc<Window>>,
|
||||
/// GPU context (Instance, Surface, Adapter, Device, Queue).
|
||||
context: Option<Context>,
|
||||
/// Execution layer that submits draw calls.
|
||||
renderer: Option<Renderer>,
|
||||
/// Shader/pipeline cache.
|
||||
cache: Option<PipelineCache>,
|
||||
/// Quad material (pipeline).
|
||||
material: Option<Material>,
|
||||
/// Quad mesh (vertices + indices).
|
||||
mesh: Option<Mesh>,
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
/// Creates the window then the GPU, and builds the mesh/material. Runs once at startup.
|
||||
/// Redundant `resumed` creating again? Double protection via `self.context.is_some()`.
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.context.is_some() {
|
||||
return;
|
||||
}
|
||||
event_loop.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
let attrs = WindowAttributes::default()
|
||||
.with_title("WSG Manual")
|
||||
.with_inner_size(LogicalSize::new(800.0, 600.0));
|
||||
let window = Arc::new(event_loop.create_window(attrs).unwrap());
|
||||
|
||||
// 1. Initialisation
|
||||
let context = pollster::block_on(Context::new(window.clone())).expect("GPU init failed");
|
||||
|
||||
// Surface configuration and format retrieval
|
||||
let format = context
|
||||
.configure(&context.adapter, 800, 600)
|
||||
.expect("configuration failed");
|
||||
|
||||
// 2. Renderer initialization (it retrieves everything it needs)
|
||||
let device = Arc::new(context.device.clone());
|
||||
let mut cache = PipelineCache::new(device, context.queue.clone(), 1);
|
||||
cache
|
||||
.register_shader("standard", utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
// Flat 2D rendering: `standard` in unlit mode (the frame+object bind groups are set by
|
||||
// draw_entity, the default frame matrix is the identity → NDC positions unchanged).
|
||||
let mut renderer = Renderer::new(&context, format, 800, 600, &ShadowConfig::default(), None, None, None, None, None);
|
||||
renderer.set_unlit(true);
|
||||
|
||||
// 3. Material: uses renderer.device() and renderer.format()
|
||||
let material = Material::new(renderer.format(), "standard", &mut cache);
|
||||
|
||||
// Mesh: uses the renderer's device. The mesh is built from a `Geometry`
|
||||
// (positions + colors per vertex) via `Mesh::from_geometry` — the mesh also keeps the
|
||||
// `Arc<Geometry>` on the CPU side.
|
||||
let geometry = Geometry::new(vec![
|
||||
// Position (x,y,z) | Color (r,g,b,a) — normals/UVs default via to_vertices
|
||||
[-0.5, 0.5, 0.0],
|
||||
[0.5, 0.5, 0.0],
|
||||
[0.5, -0.5, 0.0],
|
||||
[-0.5, -0.5, 0.0],
|
||||
])
|
||||
.with_colors(vec![
|
||||
[1.0, 0.0, 0.0, 1.0], // top-left (red)
|
||||
[0.0, 1.0, 0.0, 1.0], // top-right (green)
|
||||
[0.0, 0.0, 1.0, 1.0], // bottom-right (blue)
|
||||
[1.0, 1.0, 0.0, 1.0], // bottom-left (yellow)
|
||||
])
|
||||
.with_indices(vec![0, 1, 2, 0, 2, 3]);
|
||||
let mesh = Mesh::from_geometry(renderer.device(), Arc::new(geometry), None);
|
||||
|
||||
self.window = Some(window);
|
||||
self.context = Some(context);
|
||||
self.renderer = Some(renderer);
|
||||
self.cache = Some(cache);
|
||||
self.material = Some(material);
|
||||
self.mesh = Some(mesh);
|
||||
}
|
||||
|
||||
/// Each frame, requests a redraw for continuous rendering (animation).
|
||||
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
/// Window event dispatch: RedrawRequested renders then presents, CloseRequested exits.
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: winit::window::WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
match event {
|
||||
winit::event::WindowEvent::RedrawRequested => {
|
||||
if let (Some(context), Some(renderer), Some(mesh), Some(material)) =
|
||||
(&self.context, &self.renderer, &self.mesh, &self.material)
|
||||
{
|
||||
if let Some(frame) = Frame::try_new(&context.surface) {
|
||||
// 1. Render (no more useless device/queue arguments)
|
||||
renderer.render(frame.view(), mesh, material);
|
||||
|
||||
// 2. Present
|
||||
renderer.present(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
winit::event::WindowEvent::CloseRequested => {
|
||||
event_loop.exit(); // this is where you ask the loop to stop
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("Current directory: {:?}", std::env::current_dir().unwrap());
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
let mut app = App {
|
||||
window: None,
|
||||
context: None,
|
||||
renderer: None,
|
||||
cache: None,
|
||||
material: None,
|
||||
mesh: None,
|
||||
};
|
||||
event_loop.run_app(&mut app).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! # Exemple PBR — Metallic/Roughness + Normal Mapping (Étape 27)
|
||||
//!
|
||||
//! Démonstration du workflow PBR Cook-Torrance (GGX + Smith + Schlick) avec IBL hémisphérique.
|
||||
//!
|
||||
//! ## Scène
|
||||
//! - Sol : plan 20×20, PBR matte (metallic=0, roughness=0.8)
|
||||
//! - Cube métal : metallic=1.0, roughness=0.1 → reflet spéculaire net (miroir)
|
||||
//! - Cube plastique : metallic=0.0, roughness=0.4 → spéculaire large et doux
|
||||
//! - Cube rouillé : metallic=0.8, roughness=0.7 → métal rugueux
|
||||
//! - Sphere céramique : metallic=0.3, roughness=0.3
|
||||
//! - Cube normal map : bump procédural (sin wave)
|
||||
//!
|
||||
//! ## Contrôles
|
||||
//! | Touche | Action |
|
||||
//! |--------|--------|
|
||||
//! | Drag (LMB) | Orbite caméra |
|
||||
//! | Molette | Zoom |
|
||||
//! | `R` | Reset caméra |
|
||||
//!
|
||||
//! ## Lancement
|
||||
//! ```bash
|
||||
//! cargo run -p wsg-lib --example pbr
|
||||
//! ```
|
||||
|
||||
use glam::Vec3;
|
||||
use winit::event::MouseButton;
|
||||
use winit::keyboard::KeyCode;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::camera::CameraController;
|
||||
use wsg_lib::core::{ToneMapper, Transform};
|
||||
use wsg_lib::mesh::{cube, icosphere, plane};
|
||||
use wsg_lib::resources::Texture;
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
struct PbrDemo {
|
||||
camera: CameraController,
|
||||
}
|
||||
|
||||
impl Default for PbrDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
camera: CameraController::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppHandler for PbrDemo {
|
||||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||||
app.scene
|
||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
// Normal map procédurale 256×256 : bump sin(x)*sin(y).
|
||||
let bump_map = make_bump_normal_map(&app.context().device, &app.context().queue);
|
||||
app.scene.add_texture("bump_nm", bump_map).unwrap();
|
||||
|
||||
// Matériaux PBR.
|
||||
app.scene.add_material_pbr("floor", "standard", 0.0, 0.8).unwrap();
|
||||
app.scene.add_material_pbr("metal", "standard", 1.0, 0.1).unwrap();
|
||||
app.scene.add_material_pbr("plastic", "standard", 0.0, 0.4).unwrap();
|
||||
app.scene.add_material_pbr("rust", "standard", 0.8, 0.7).unwrap();
|
||||
app.scene.add_material_pbr("ceramic", "standard", 0.3, 0.3).unwrap();
|
||||
app.scene
|
||||
.add_material_pbr_textured("bump", "standard", 0.0, 0.5, None, Some("bump_nm"))
|
||||
.unwrap();
|
||||
|
||||
// Sol (plan 20×20).
|
||||
app.scene
|
||||
.create_mesh("floor_mesh", plane(1.0, 1.0, 1, 1), Some("floor"))
|
||||
.unwrap();
|
||||
{
|
||||
let mut tf = Transform::identity();
|
||||
tf.translation = Vec3::new(0.0, 0.0, 0.0);
|
||||
tf.scale = Vec3::new(20.0, 1.0, 20.0);
|
||||
app.scene.add_entity_with_transform("floor", "floor_mesh", tf).unwrap();
|
||||
}
|
||||
|
||||
// Cubes.
|
||||
app.scene.create_mesh("cube_mesh", cube(1.0), None).unwrap();
|
||||
let cubes: [(&str, &str, Vec3); 4] = [
|
||||
("c_metal", "metal", Vec3::new(-3.0, 0.5, 0.0)),
|
||||
("c_plastic", "plastic", Vec3::new(-1.0, 0.5, 0.0)),
|
||||
("c_rust", "rust", Vec3::new(1.0, 0.5, 0.0)),
|
||||
("c_bump", "bump", Vec3::new(3.0, 0.5, 0.0)),
|
||||
];
|
||||
for (id, mat, pos) in &cubes {
|
||||
app.scene
|
||||
.create_mesh(&format!("{id}_mesh"), cube(1.0), Some(mat))
|
||||
.unwrap();
|
||||
let mut tf = Transform::identity();
|
||||
tf.translation = *pos;
|
||||
app.scene
|
||||
.add_entity_with_transform(id, &format!("{id}_mesh"), tf)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Sphere céramique.
|
||||
app.scene
|
||||
.create_mesh("sphere_mesh", icosphere(0.5, 4), Some("ceramic"))
|
||||
.unwrap();
|
||||
{
|
||||
let mut tf = Transform::identity();
|
||||
tf.translation = Vec3::new(0.0, 0.5, -3.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("s_ceramic", "sphere_mesh", tf)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Lumières.
|
||||
app.scene
|
||||
.add_directional_light(Vec3::new(-1.0, 2.0, 1.0).normalize(), [1.0, 0.95, 0.9], 2.0)
|
||||
.unwrap();
|
||||
app.scene
|
||||
.add_point_light(Vec3::new(0.0, 3.0, 2.0), [0.3, 0.5, 1.0], 8.0, 5.0)
|
||||
.unwrap();
|
||||
|
||||
|
||||
// Ambiance (IBL hémisphérique).
|
||||
app.scene.set_ambient([0.3, 0.35, 0.4]);
|
||||
|
||||
// Caméra.
|
||||
self.camera.yaw = 0.0;
|
||||
self.camera.pitch = 0.3;
|
||||
self.camera.distance = 8.0;
|
||||
self.camera.target = Vec3::new(0.0, 0.5, 0.0);
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
eprintln!("[PBR] Scene: 6 PBR materials (metal/plastic/rust/ceramic/bump/floor)");
|
||||
eprintln!("[PBR] Drag=orbit, Wheel=zoom, R=reset");
|
||||
}
|
||||
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
// Orbite caméra.
|
||||
let (dx, dy) = app.input.mouse_delta();
|
||||
if app.input.mouse_button_held(MouseButton::Left) {
|
||||
self.camera.orbit(dx, dy);
|
||||
}
|
||||
let (_, sy) = app.input.scroll_delta();
|
||||
self.camera.zoom(sy);
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// R = reset.
|
||||
if app.input.key_pressed(KeyCode::KeyR) {
|
||||
self.camera = CameraController::default();
|
||||
self.camera.target = Vec3::new(0.0, 0.5, 0.0);
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Génère une normal map procédurale 256×256 : pattern sin(x*freq)*sin(y*freq) → bump.
|
||||
/// Chaque pixel : normale perturbée encodée en RGB (nx*0.5+0.5, ny*0.5+0.5, nz*0.5+0.5) * 255.
|
||||
fn make_bump_normal_map(device: &wgpu::Device, queue: &wgpu::Queue) -> Texture {
|
||||
let size = 256u32;
|
||||
let freq = 8.0;
|
||||
let mut pixels: Vec<u8> = vec![0u8; (size * size * 4) as usize];
|
||||
|
||||
for y in 0..size {
|
||||
for x in 0..size {
|
||||
let u = x as f32 / size as f32;
|
||||
let v = y as f32 / size as f32;
|
||||
let h = (u * freq * std::f32::consts::PI).sin()
|
||||
* (v * freq * std::f32::consts::PI).sin();
|
||||
let eps = 1.0 / size as f32;
|
||||
let hx = ((u + eps) * freq * std::f32::consts::PI).sin()
|
||||
* (v * freq * std::f32::consts::PI).sin();
|
||||
let hy = (u * freq * std::f32::consts::PI).sin()
|
||||
* ((v + eps) * freq * std::f32::consts::PI).sin();
|
||||
let dhdx = (hx - h) / eps;
|
||||
let dhdy = (hy - h) / eps;
|
||||
let n = Vec3::new(-dhdx, -dhdy, 1.0).normalize();
|
||||
let idx = ((y * size + x) * 4) as usize;
|
||||
pixels[idx] = ((n.x * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8;
|
||||
pixels[idx + 1] = ((n.y * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8;
|
||||
pixels[idx + 2] = ((n.z * 0.5 + 0.5) * 255.0).clamp(0.0, 255.0) as u8;
|
||||
pixels[idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
Texture::from_rgba8(device, queue, size, size, &pixels, "bump_normal_map")
|
||||
.expect("bump normal map creation failed")
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> Result<(), WsgError> {
|
||||
let app = AppBuilder::new()
|
||||
.title("WSG — PBR Metallic/Roughness")
|
||||
.size(1280, 720)
|
||||
.with_hdr(ToneMapper::Aces)
|
||||
.build()
|
||||
.await?;
|
||||
app.run(PbrDemo::default())
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Minimal declarative workflow, no explicit WGPU handling in this file.
|
||||
//! `AppBuilder` creates the event loop, then `App::run` opens the window, builds the `Context`/`Renderer`
|
||||
//! and drives the update → render → present loop. In winit 0.30 the GPU only exists
|
||||
//! after `resumed`: that is why shader registration + mesh/material/entity creation live in
|
||||
//! the `AppHandler::setup` hook, called once the context is ready. The PipelineCache
|
||||
//! lives in the scene (`Scene::init_gpu`, called in `resumed`): go through `register_shader` +
|
||||
//! `add_material_shader` + `create_mesh` + `add_entity`, the material being bound to the mesh. The mesh
|
||||
//! is declared from a **`Geometry`**: per-vertex positions + colors
|
||||
//! for the unlit quad. The scene renders automatically: the default `render()` method calls
|
||||
//! `app.render_scene(frame.view())`.
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::resources::Geometry;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
struct MonQuad;
|
||||
|
||||
impl AppHandler for MonQuad {
|
||||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||||
// Flat 2D example: the `standard` shader in **unlit** mode (options.x = 1) returns the vertex
|
||||
// color as-is. Flat 2D is thus a special case of 3D — a single pipeline for all.
|
||||
app.renderer_mut().set_unlit(true);
|
||||
app.scene
|
||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
let geometry = Geometry::new(vec![
|
||||
[-0.5, 0.5, 0.0], // top-left
|
||||
[0.5, 0.5, 0.0], // top-right
|
||||
[0.5, -0.5, 0.0], // bottom-right
|
||||
[-0.5, -0.5, 0.0], // bottom-left
|
||||
])
|
||||
.with_normals(vec![[0.0, 0.0, 1.0]; 4])
|
||||
.with_colors(vec![
|
||||
[1.0, 0.0, 0.0, 1.0], // top-left (red)
|
||||
[0.0, 1.0, 0.0, 1.0], // top-right (green)
|
||||
[0.0, 0.0, 1.0, 1.0], // bottom-right (blue)
|
||||
[1.0, 1.0, 0.0, 1.0], // bottom-left (yellow)
|
||||
])
|
||||
.with_indices(vec![0, 1, 2, 0, 2, 3]);
|
||||
|
||||
// Default material: `None` lets the Scene inject its `standard` at render time
|
||||
// (`Scene::default_material`) — this exercises the default path.
|
||||
app.scene.create_mesh("quad_mesh", geometry, None).unwrap();
|
||||
app.scene.add_entity("quad", "quad_mesh").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> Result<(), WsgError> {
|
||||
let app = AppBuilder::new().title("WSG Simple").build().await?;
|
||||
app.run(MonQuad)
|
||||
}
|
||||
Reference in New Issue
Block a user