MSAA
This commit is contained in:
+32
-1
@@ -16,7 +16,8 @@ cargo run -p wsg-lib --example <nom>
|
||||
| `hdr` | HDR + Tone Mapping (ACES) + contrôle d'exposition |
|
||||
| `emissive` | Matériaux émissifs (intensités croissantes 0 → 4.0) |
|
||||
| `shadow` | Shadow mapping (ombre portée directionnelle) |
|
||||
| `culling` | Culling GPU-driven (grille 20×20, objets hors frustum ignorés) |
|
||||
| `culling` | Culling GPU-driven (grille 15×15, objets hors frustum ignorés) |
|
||||
| `msaa` | MSAA 4× (anti-aliasing multi-échantillons, arêtes lisses) |
|
||||
| `manual` | Workflow bas niveau (Context + Renderer + PipelineCache) |
|
||||
| `import` | Import de fichier OBJ (non graphique, stdout) |
|
||||
|
||||
@@ -220,6 +221,36 @@ cargo run -p wsg-lib --example culling
|
||||
|
||||
---
|
||||
|
||||
## `msaa` — MSAA 4× (Anti-aliasing)
|
||||
|
||||
Démontre l'anti-aliasing multi-échantillons : les arêtes des objets (cube, sphère)
|
||||
sont lisses au lieu d'être "en escalier". La scène contient un cube (arêtes nettes),
|
||||
une sphère (silhouette courbe) et un petit cube près de la caméra (aliasing maximal).
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example msaa
|
||||
```
|
||||
|
||||
### Touches
|
||||
|
||||
| Touche | Action |
|
||||
|--------|--------|
|
||||
| Glisser (LMB) | Orbiter la caméra |
|
||||
| Molette | Zoom |
|
||||
| `R` | Reset caméra |
|
||||
| `M` | Afficher le nombre d'échantillons |
|
||||
|
||||
### Pour comparer avec/sans MSAA
|
||||
|
||||
Supprimer la ligne `.with_msaa(4)` dans le source et recompiler : la scène est
|
||||
identique, seules les arêtes diffèrent (escaler vs lisse).
|
||||
|
||||
> **Note** : MSAA est un réglage de build-time (allocation de textures multi-échantillons).
|
||||
> Il fonctionne indépendamment de HDR : avec HDR, la texture MSAA est `Rgba16Float`
|
||||
> et résout dans la texture HDR avant bloom/TM.
|
||||
|
||||
---
|
||||
|
||||
## `manual` — Workflow bas niveau
|
||||
|
||||
Démontre l'API **sans** la façade `App` : utilisation directe de `Context`,
|
||||
|
||||
@@ -56,14 +56,14 @@ impl ApplicationHandler for App {
|
||||
|
||||
// 2. Renderer initialization (it retrieves everything it needs)
|
||||
let device = Arc::new(context.device.clone());
|
||||
let mut cache = PipelineCache::new(device, context.queue.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);
|
||||
let mut renderer = Renderer::new(&context, format, 800, 600, &ShadowConfig::default(), None, None, None);
|
||||
renderer.set_unlit(true);
|
||||
|
||||
// 3. Material: uses renderer.device() and renderer.format()
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! **MSAA (Multi-Sample Anti-Aliasing)** — demonstrates 4× MSAA edge smoothing.
|
||||
//!
|
||||
//! Shows how MSAA eliminates the jagged "staircase" artifacts (aliasing) along
|
||||
//! sharp edges. The scene contains a cube (sharp edges), a sphere (curved surface),
|
||||
//! and a ground plane — all with high-contrast edges where aliasing is most visible.
|
||||
//!
|
||||
//! To compare with/without MSAA: remove the `.with_msaa(4)` line from the builder
|
||||
//! below and rebuild. The scene and lighting are identical — only the edge
|
||||
//! smoothness differs.
|
||||
//!
|
||||
//! ## Pipeline (MSAA + HDR)
|
||||
//! ```text
|
||||
//! Main pass → MSAA texture (4 samples, Rgba16Float)
|
||||
//! ↓ resolve (average 4 samples → 1)
|
||||
//! HDR texture (single sample)
|
||||
//! ↓
|
||||
//! Tone Mapping → surface
|
||||
//! ```
|
||||
//!
|
||||
//! ## Controls
|
||||
//! | Key | Action |
|
||||
//! |-----|--------|
|
||||
//! | Drag (LMB) | Orbit camera |
|
||||
//! | Wheel | Zoom |
|
||||
//! | `R` | Reset camera |
|
||||
//! | `M` | Toggle MSAA info (shows sample count) |
|
||||
//!
|
||||
//! ## Build & Run
|
||||
//! ```sh
|
||||
//! cargo run -p wsg-lib --example msaa
|
||||
//! ```
|
||||
|
||||
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::{Transform, ToneMapper};
|
||||
use wsg_lib::mesh::{cube, icosphere, plane};
|
||||
use wsg_lib::AppHandler;
|
||||
use wsg_lib::utils::WsgError;
|
||||
|
||||
struct MsaaDemo {
|
||||
camera: CameraController,
|
||||
show_info: bool,
|
||||
}
|
||||
|
||||
impl AppHandler for MsaaDemo {
|
||||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
||||
app.scene
|
||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
|
||||
// Ground plane.
|
||||
app.scene
|
||||
.create_mesh("ground_mesh", plane(10.0, 10.0, 1, 1), None)
|
||||
.unwrap();
|
||||
app.scene.add_entity("ground", "ground_mesh").unwrap();
|
||||
|
||||
// Cube — sharp edges make aliasing very visible.
|
||||
app.scene
|
||||
.create_mesh("cube_mesh", cube(1.0), None)
|
||||
.unwrap();
|
||||
let mut cube_tf = Transform::identity();
|
||||
cube_tf.translation = Vec3::new(1.5, 0.5, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("cube", "cube_mesh", cube_tf)
|
||||
.unwrap();
|
||||
|
||||
// Sphere — curved surface, aliasing visible on the silhouette.
|
||||
app.scene
|
||||
.create_mesh("sphere_mesh", icosphere(0.6, 3), None)
|
||||
.unwrap();
|
||||
let mut sphere_tf = Transform::identity();
|
||||
sphere_tf.translation = Vec3::new(-1.5, 0.6, 0.0);
|
||||
app.scene
|
||||
.add_entity_with_transform("sphere", "sphere_mesh", sphere_tf)
|
||||
.unwrap();
|
||||
|
||||
// Small cube near the camera — very close edges, maximum aliasing.
|
||||
app.scene
|
||||
.create_mesh("small_cube_mesh", cube(0.3), None)
|
||||
.unwrap();
|
||||
let mut small_tf = Transform::identity();
|
||||
small_tf.translation = Vec3::new(0.0, 0.15, 1.5);
|
||||
app.scene
|
||||
.add_entity_with_transform("small_cube", "small_cube_mesh", small_tf)
|
||||
.unwrap();
|
||||
|
||||
// Directional light (strong, creates high-contrast edges).
|
||||
let light_dir = Vec3::new(0.5, 1.0, 0.3).normalize();
|
||||
app.scene
|
||||
.add_directional_light(light_dir, [1.0, 0.95, 0.85], 1.5)
|
||||
.unwrap();
|
||||
app.scene.set_ambient([0.08, 0.08, 0.1]);
|
||||
|
||||
// Camera.
|
||||
self.camera.yaw = 0.4;
|
||||
self.camera.pitch = 0.2;
|
||||
self.camera.distance = 4.0;
|
||||
self.camera.target = Vec3::new(0.0, 0.4, 0.0);
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// Print MSAA status.
|
||||
let sc = app.renderer().msaa_sample_count();
|
||||
eprintln!("[MSAA] sample_count = {} ({})", sc, if sc > 1 { "active" } else { "disabled" });
|
||||
}
|
||||
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
// Orbit camera.
|
||||
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);
|
||||
|
||||
if app.input.key_pressed(KeyCode::KeyR) {
|
||||
self.camera.yaw = 0.4;
|
||||
self.camera.pitch = 0.2;
|
||||
self.camera.distance = 4.0;
|
||||
}
|
||||
self.camera.apply_to(app.scene.camera_mut());
|
||||
|
||||
// Toggle info display.
|
||||
if app.input.key_pressed(KeyCode::KeyM) {
|
||||
self.show_info = !self.show_info;
|
||||
let sc = app.renderer().msaa_sample_count();
|
||||
eprintln!("[MSAA] {}× {}", sc, if sc > 1 { "enabled" } else { "disabled (single sample)" });
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, app: &mut wsg_lib::App, frame: &wsg_lib::core::Frame) {
|
||||
app.render_scene(frame.view());
|
||||
}
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> Result<(), WsgError> {
|
||||
let app = AppBuilder::new()
|
||||
.title("WSG MSAA 4×")
|
||||
.size(960, 640)
|
||||
.with_msaa(4) // ← Enable 4× MSAA (remove for comparison)
|
||||
.with_hdr(ToneMapper::Aces) // MSAA works with or without HDR
|
||||
.build()
|
||||
.await?;
|
||||
app.run(MsaaDemo {
|
||||
camera: CameraController::default(),
|
||||
show_info: false,
|
||||
})
|
||||
}
|
||||
+30
-4
@@ -23,7 +23,7 @@
|
||||
//! once right after GPU initialization so users can register shaders/meshes/materials/entities.
|
||||
|
||||
use crate::AppHandler;
|
||||
use crate::core::{BloomConfig, Context, Renderer, ShadowConfig, ToneMapper};
|
||||
use crate::core::{BloomConfig, Context, MsaaConfig, Renderer, ShadowConfig, ToneMapper};
|
||||
use crate::input::InputState;
|
||||
use crate::scene::Scene;
|
||||
use crate::utils::WsgError;
|
||||
@@ -70,6 +70,9 @@ pub struct App {
|
||||
/// Exposure multiplier (Étape 22, 6.1). Applied in the tone mapping pass before the curve.
|
||||
/// Default 1.0. Adjustable at runtime via `set_exposure` or keyboard (+/-).
|
||||
pub exposure: f32,
|
||||
/// MSAA configuration (Étape 24). `None` = no MSAA (default, zero overhead);
|
||||
/// `Some(config)` activates multi-sample anti-aliasing.
|
||||
msaa: Option<MsaaConfig>,
|
||||
/// Winit event loop for window management. Set to None after run() consumes it.
|
||||
event_loop: Option<EventLoop<()>>, // On met en Option pour pouvoir faire .take() facilement
|
||||
/// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle.
|
||||
@@ -137,6 +140,7 @@ impl App {
|
||||
hdr: self.hdr,
|
||||
bloom_config: self.bloom_config.clone(),
|
||||
exposure: self.exposure,
|
||||
msaa: self.msaa.clone(),
|
||||
handler,
|
||||
app: None,
|
||||
};
|
||||
@@ -211,8 +215,9 @@ impl App {
|
||||
// Étape 20: when HDR is active, the Scene uses Rgba16Float regardless of the
|
||||
// surface format, so no re-init is needed on surface format change.
|
||||
let device = std::sync::Arc::new(self.renderer_mut().device().clone());
|
||||
let sc = self.renderer_mut().msaa_sample_count();
|
||||
self.scene
|
||||
.init_gpu(device, self.context().queue.clone(), new_format);
|
||||
.init_gpu(device, self.context().queue.clone(), new_format, sc);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -239,6 +244,8 @@ pub struct AppBuilder {
|
||||
bloom_config: Option<BloomConfig>,
|
||||
/// Initial exposure multiplier (Étape 22, 6.1). Default 1.0.
|
||||
exposure: f32,
|
||||
/// MSAA configuration (Étape 24). `None` = no MSAA (default).
|
||||
msaa: Option<MsaaConfig>,
|
||||
}
|
||||
|
||||
impl AppBuilder {
|
||||
@@ -254,6 +261,7 @@ impl AppBuilder {
|
||||
hdr: None,
|
||||
bloom_config: None,
|
||||
exposure: 1.0,
|
||||
msaa: None,
|
||||
}
|
||||
}
|
||||
/// Sets the window title to display in the OS taskbar/window decorations.
|
||||
@@ -306,6 +314,19 @@ impl AppBuilder {
|
||||
self.exposure = exposure;
|
||||
self
|
||||
}
|
||||
/// Enables MSAA (Multi-Sample Anti-Aliasing) with the given sample count (Étape 24).
|
||||
/// The count must be 2, 4, or 8 (validated at build time; invalid values fall back to no MSAA
|
||||
/// with a warning). When disabled (not set), the renderer uses single-sample (zero overhead).
|
||||
/// Works independently of HDR: with HDR, the MSAA texture is `Rgba16Float` and resolves
|
||||
/// into the HDR texture before bloom/TM; without HDR, it resolves directly to the swapchain.
|
||||
pub fn with_msaa(mut self, sample_count: u32) -> Self {
|
||||
if let Some(reason) = MsaaConfig::validate(sample_count) {
|
||||
eprintln!("[wsg] Warning: with_msaa({}) — {} — MSAA disabled.", sample_count, reason);
|
||||
} else {
|
||||
self.msaa = Some(MsaaConfig { sample_count });
|
||||
}
|
||||
self
|
||||
}
|
||||
/// Builds the configured `App` instance: creates the event loop and stores the window
|
||||
/// configuration. The GPU context, window and renderer are created later, when the event loop
|
||||
/// is resumed (inside `App::run`), because winit 0.30 only allows window creation in that phase.
|
||||
@@ -324,6 +345,7 @@ impl AppBuilder {
|
||||
hdr: self.hdr,
|
||||
bloom_config: self.bloom_config,
|
||||
exposure: self.exposure,
|
||||
msaa: self.msaa,
|
||||
event_loop: Some(event_loop),
|
||||
context: None,
|
||||
renderer: None,
|
||||
@@ -352,6 +374,8 @@ struct AppRunner<H: AppHandler> {
|
||||
bloom_config: Option<BloomConfig>,
|
||||
/// Initial exposure (Étape 22, 6.1); stored in the App for per-frame use.
|
||||
exposure: f32,
|
||||
/// MSAA config (Étape 24); passed to `Renderer::new` in `resumed`.
|
||||
msaa: Option<MsaaConfig>,
|
||||
/// The user-provided game logic.
|
||||
handler: H,
|
||||
/// The fully-built App facade, populated on the first `resumed` event.
|
||||
@@ -385,7 +409,7 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
.expect("surface configuration failed");
|
||||
let device = Arc::new(context.device.clone());
|
||||
let renderer =
|
||||
Renderer::new(&context, format, self.width, self.height, &self.shadow_config, self.hdr, self.bloom_config.clone());
|
||||
Renderer::new(&context, format, self.width, self.height, &self.shadow_config, self.hdr, self.bloom_config.clone(), self.msaa.clone());
|
||||
// Step 15, D8: apply the culling flag (off by default — non-regression).
|
||||
renderer.set_culling(self.culling);
|
||||
|
||||
@@ -399,7 +423,8 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
format
|
||||
};
|
||||
let mut scene = Scene::new();
|
||||
scene.init_gpu(device, context.queue.clone(), main_format);
|
||||
let msaa_sc = self.msaa.as_ref().map(|c| c.sample_count).unwrap_or(1);
|
||||
scene.init_gpu(device, context.queue.clone(), main_format, msaa_sc);
|
||||
|
||||
let mut app = App {
|
||||
scene,
|
||||
@@ -412,6 +437,7 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
hdr: self.hdr,
|
||||
bloom_config: self.bloom_config.clone(),
|
||||
exposure: self.exposure,
|
||||
msaa: self.msaa.clone(),
|
||||
event_loop: None,
|
||||
context: Some(context),
|
||||
renderer: Some(renderer),
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod frustum;
|
||||
pub mod geometry;
|
||||
pub mod hdr;
|
||||
pub mod lod;
|
||||
pub mod msaa;
|
||||
pub mod renderer;
|
||||
pub mod shadow;
|
||||
pub mod transform;
|
||||
@@ -28,6 +29,7 @@ pub use frustum::Frustum;
|
||||
pub use geometry::{BBox, Geometry, GeometryError};
|
||||
pub use hdr::ToneMapper;
|
||||
pub use lod::{lod_level, projected_radius_px};
|
||||
pub use msaa::MsaaConfig;
|
||||
pub use renderer::Renderer;
|
||||
pub use shadow::ShadowConfig;
|
||||
pub use transform::Transform;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! MSAA (Multi-Sample Anti-Aliasing) configuration (Étape 24, 6.4).
|
||||
//!
|
||||
//! When enabled, the main scene pass renders into a multi-sampled texture
|
||||
//! (N samples per pixel) and wgpu resolves it (averages) into the single-sample
|
||||
//! target (HDR texture or swapchain). Post-processes (bloom, TM) operate on
|
||||
//! the resolved single-sample texture — they are unaffected.
|
||||
//!
|
||||
//! MSAA is a rasterizer feature: **no new shader** is needed. The cost is
|
||||
//! in the rasterizer/fill-rate (edges are over-sampled), typically 1.3–1.5×
|
||||
//! for 4× MSAA.
|
||||
|
||||
/// MSAA configuration.
|
||||
///
|
||||
/// `sample_count` must be a power of two (2, 4, or 8) and must be supported
|
||||
/// by the GPU for the target texture format. The default is 4.
|
||||
///
|
||||
/// When disabled (not set in the builder), the renderer uses `sample_count = 1`
|
||||
/// (single sample, no MSAA) and the behavior is identical to pre-MSAA.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MsaaConfig {
|
||||
/// Number of samples per pixel. Must be 2, 4, or 8.
|
||||
pub sample_count: u32,
|
||||
}
|
||||
|
||||
impl Default for MsaaConfig {
|
||||
fn default() -> Self {
|
||||
Self { sample_count: 4 }
|
||||
}
|
||||
}
|
||||
|
||||
impl MsaaConfig {
|
||||
/// Validates that `sample_count` is a supported value (2, 4, or 8).
|
||||
/// Returns `None` if valid, `Some(reason)` if not.
|
||||
pub fn validate(sample_count: u32) -> Option<&'static str> {
|
||||
match sample_count {
|
||||
2 | 4 | 8 => None,
|
||||
_ => Some("sample_count must be 2, 4, or 8"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_4() {
|
||||
assert_eq!(MsaaConfig::default().sample_count, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_powers_of_two() {
|
||||
assert_eq!(MsaaConfig::validate(2), None);
|
||||
assert_eq!(MsaaConfig::validate(4), None);
|
||||
assert_eq!(MsaaConfig::validate(8), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_invalid() {
|
||||
assert!(MsaaConfig::validate(1).is_some());
|
||||
assert!(MsaaConfig::validate(3).is_some());
|
||||
assert!(MsaaConfig::validate(16).is_some());
|
||||
assert!(MsaaConfig::validate(0).is_some());
|
||||
}
|
||||
}
|
||||
+122
-8
@@ -40,6 +40,7 @@ use crate::resources::{
|
||||
use crate::scene::Scene;
|
||||
use crate::core::bloom::{BloomConfig, BloomPipeline};
|
||||
use crate::core::hdr::ToneMapper;
|
||||
use crate::core::msaa::MsaaConfig;
|
||||
use crate::utils::conf::{
|
||||
GPU_DRIVEN_SHADER, GPU_WORKGROUP_SIZE, LOD_THRESHOLDS, MAX_ENTITIES, MAX_LOD_LEVELS, TONEMAP_SHADER,
|
||||
};
|
||||
@@ -159,6 +160,16 @@ pub struct Renderer {
|
||||
bloom: Option<BloomPipeline>,
|
||||
/// Bloom configuration (used per-frame for uniform writes). Only meaningful when bloom is active.
|
||||
bloom_config: BloomConfig,
|
||||
/// MSAA configuration (Étape 24). `sample_count = 1` means MSAA is disabled (zero overhead).
|
||||
msaa_config: MsaaConfig,
|
||||
/// MSAA color texture (N samples). `None` when MSAA is disabled.
|
||||
msaa_color_texture: Option<wgpu::Texture>,
|
||||
/// MSAA color view used as the main pass color attachment when MSAA is active.
|
||||
msaa_color_view: Option<wgpu::TextureView>,
|
||||
/// MSAA depth texture (N samples). `None` when MSAA is disabled.
|
||||
msaa_depth_texture: Option<wgpu::Texture>,
|
||||
/// MSAA depth view used as the main pass depth attachment when MSAA is active.
|
||||
msaa_depth_view: Option<wgpu::TextureView>,
|
||||
}
|
||||
|
||||
/// Internal HDR pipeline state: offscreen `Rgba16Float` texture + tone mapping render pipeline.
|
||||
@@ -201,6 +212,7 @@ impl Renderer {
|
||||
shadow_config: &super::shadow::ShadowConfig,
|
||||
hdr: Option<ToneMapper>,
|
||||
bloom_config: Option<BloomConfig>,
|
||||
msaa_config: Option<MsaaConfig>,
|
||||
) -> Self {
|
||||
let queue: wgpu::Queue = context.queue.clone();
|
||||
let device: wgpu::Device = context.device.clone();
|
||||
@@ -594,6 +606,11 @@ impl Renderer {
|
||||
hdr: None,
|
||||
bloom: None,
|
||||
bloom_config: bloom_config.clone().unwrap_or_default(),
|
||||
msaa_config: msaa_config.clone().unwrap_or_default(),
|
||||
msaa_color_texture: None,
|
||||
msaa_color_view: None,
|
||||
msaa_depth_texture: None,
|
||||
msaa_depth_view: None,
|
||||
};
|
||||
// Seed the shared frame buffer with an identity camera + current unlit flag so the low-level
|
||||
// `render` path (which has no window/camera) sees coherent values before `render_scene` runs.
|
||||
@@ -613,6 +630,33 @@ impl Renderer {
|
||||
renderer.bloom_config = bloom_config.clone().unwrap();
|
||||
}
|
||||
}
|
||||
// Étape 24: allocate MSAA textures when sample_count > 1.
|
||||
// The MSAA color texture uses the same format as the main target (HDR or surface).
|
||||
if renderer.msaa_config.sample_count > 1 {
|
||||
let sc = renderer.msaa_config.sample_count;
|
||||
let color_format = if renderer.hdr.is_some() {
|
||||
wgpu::TextureFormat::Rgba16Float
|
||||
} else {
|
||||
format
|
||||
};
|
||||
let msaa_tex = renderer.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("MSAA color texture"),
|
||||
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: sc,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: color_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let (msaa_depth_tex, msaa_depth_view) =
|
||||
create_msaa_depth_texture(&renderer.device, width, height, sc);
|
||||
renderer.msaa_color_texture = Some(msaa_tex);
|
||||
renderer.msaa_color_view = Some(msaa_view);
|
||||
renderer.msaa_depth_texture = Some(msaa_depth_tex);
|
||||
renderer.msaa_depth_view = Some(msaa_depth_view);
|
||||
}
|
||||
renderer
|
||||
}
|
||||
|
||||
@@ -674,6 +718,32 @@ impl Renderer {
|
||||
hdr.bind_group = bg;
|
||||
}
|
||||
}
|
||||
// Étape 24: recreate MSAA textures at the new size.
|
||||
if self.msaa_config.sample_count > 1 {
|
||||
let sc = self.msaa_config.sample_count;
|
||||
let color_format = if self.hdr.is_some() {
|
||||
wgpu::TextureFormat::Rgba16Float
|
||||
} else {
|
||||
self.format
|
||||
};
|
||||
let msaa_tex = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("MSAA color texture"),
|
||||
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count: sc,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: color_format,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let msaa_view = msaa_tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let (msaa_depth_tex, msaa_depth_view) =
|
||||
create_msaa_depth_texture(&self.device, width, height, sc);
|
||||
self.msaa_color_texture = Some(msaa_tex);
|
||||
self.msaa_color_view = Some(msaa_view);
|
||||
self.msaa_depth_texture = Some(msaa_depth_tex);
|
||||
self.msaa_depth_view = Some(msaa_depth_view);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the stored surface texture format after a surface reconfigure (ROADMAP Phase 4.4).
|
||||
@@ -963,26 +1033,38 @@ impl Renderer {
|
||||
// are hoisted out of the slot loop: one per DISTINCT material, not one per entity.
|
||||
// Étape 20: when HDR is active, the color attachment targets the offscreen HDR texture
|
||||
// instead of the surface; the TM pass (step 8) then copies it to the surface.
|
||||
let main_target = match &self.hdr {
|
||||
Some(h) => &h.view,
|
||||
None => view,
|
||||
// Étape 24: when MSAA is active, the color attachment targets the MSAA texture and
|
||||
// resolves into the single-sample target (HDR or swapchain). The depth is also MSAA.
|
||||
let (color_view, resolve_target, depth_attach) = if let Some(msaa_view) = &self.msaa_color_view {
|
||||
// MSAA active: render into MSAA, resolve to single-sample target.
|
||||
let resolve = match &self.hdr {
|
||||
Some(h) => Some(h.view.clone()),
|
||||
None => Some(view.clone()),
|
||||
};
|
||||
let depth = self.msaa_depth_view.as_ref().unwrap();
|
||||
(msaa_view.clone(), resolve, depth)
|
||||
} else {
|
||||
// No MSAA: current behavior.
|
||||
let color = match &self.hdr {
|
||||
Some(h) => &h.view,
|
||||
None => view,
|
||||
};
|
||||
(color.clone(), None, &self.depth_view)
|
||||
};
|
||||
{
|
||||
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("scene render pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: main_target,
|
||||
resolve_target: None,
|
||||
view: &color_view,
|
||||
resolve_target: resolve_target.as_ref(),
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
// Step 9 (DRAFT 9.2): same depth attachment as the low-level path, for a
|
||||
// coherent z-test (D2 — both render passes share the depth_view).
|
||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||
view: &self.depth_view,
|
||||
view: depth_attach,
|
||||
depth_ops: Some(wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(1.0),
|
||||
store: wgpu::StoreOp::Store,
|
||||
@@ -1406,6 +1488,16 @@ impl Renderer {
|
||||
self.bloom_config = config.clone();
|
||||
}
|
||||
|
||||
/// Returns `true` if MSAA is active (sample_count > 1). (Étape 24)
|
||||
pub fn msaa_enabled(&self) -> bool {
|
||||
self.msaa_config.sample_count > 1
|
||||
}
|
||||
|
||||
/// Returns the current MSAA sample count (1 = disabled). (Étape 24)
|
||||
pub fn msaa_sample_count(&self) -> u32 {
|
||||
self.msaa_config.sample_count
|
||||
}
|
||||
|
||||
/// Computes the per-slot LOD levels for this frame (Step 19, D8): for each ACTIVE slot, the
|
||||
/// entity's bounding sphere — the **same sphere** the GPU frustum culling uses (D8: bbox
|
||||
/// center + max half-extent × max scale component, rotated by the entity's quaternion) — is
|
||||
@@ -1504,6 +1596,28 @@ fn create_depth_texture(
|
||||
(depth_texture, depth_view)
|
||||
}
|
||||
|
||||
/// Creates an MSAA depth texture (N samples) with a view (Étape 24). Used when MSAA is active:
|
||||
/// the main pass needs a multi-sampled depth buffer matching the MSAA color attachment.
|
||||
fn create_msaa_depth_texture(
|
||||
device: &wgpu::Device,
|
||||
width: u32,
|
||||
height: u32,
|
||||
sample_count: u32,
|
||||
) -> (wgpu::Texture, wgpu::TextureView) {
|
||||
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("MSAA depth texture"),
|
||||
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
sample_count,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: DEPTH_FORMAT,
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
});
|
||||
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
(tex, view)
|
||||
}
|
||||
|
||||
/// Allocates the shadow-map texture + view backing the depth-only shadow pass's
|
||||
/// `depth_stencil_attachment` (Step 14, D2/D8). Square (`size` x `size`), `DEPTH_FORMAT`, single
|
||||
/// mip, no MSAA. Unlike the screen depth texture this one is flagged **both** `RENDER_ATTACHMENT`
|
||||
|
||||
@@ -58,6 +58,10 @@ pub use crate::core::ShadowConfig;
|
||||
/// Users enable HDR via `AppBuilder::with_hdr(ToneMapper::Aces)`.
|
||||
pub use crate::core::ToneMapper;
|
||||
|
||||
/// Re-export of the MSAA configuration for convenient top-level access.
|
||||
/// Users enable MSAA via `AppBuilder::with_msaa(4)`.
|
||||
pub use crate::core::MsaaConfig;
|
||||
|
||||
/// Re-export of the geometry data type (positions, normals, UVs, indices).
|
||||
pub use crate::core::Geometry;
|
||||
|
||||
|
||||
@@ -202,6 +202,9 @@ pub struct PipelineCache {
|
||||
/// 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-Step-10 look.
|
||||
placeholder: Arc<Texture>,
|
||||
/// MSAA sample count for pipeline compilation (Étape 24). Must match the render pass's
|
||||
/// attachment sample count. 1 = no MSAA (default).
|
||||
sample_count: u32,
|
||||
}
|
||||
|
||||
impl PipelineCache {
|
||||
@@ -210,7 +213,7 @@ impl PipelineCache {
|
||||
/// 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>, queue: wgpu::Queue) -> Self {
|
||||
pub fn new(device: Arc<wgpu::Device>, queue: wgpu::Queue, sample_count: u32) -> Self {
|
||||
let placeholder = Texture::white_placeholder(&device, &queue).arc();
|
||||
let texture_bind_group_layout = create_texture_bind_group_layout(&device);
|
||||
Self {
|
||||
@@ -221,6 +224,7 @@ impl PipelineCache {
|
||||
shader_paths: HashMap::new(),
|
||||
texture_bind_group_layout,
|
||||
placeholder,
|
||||
sample_count,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,7 +309,7 @@ impl PipelineCache {
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(shader_id);
|
||||
let shader = self.load_shader(&self.device, path);
|
||||
let pipeline = Self::build_pipeline(&self.device, format, &shader);
|
||||
let pipeline = self.build_pipeline(format, &shader);
|
||||
|
||||
// Step 3: Cache the new pipeline behind Arc and return it
|
||||
let pipeline_arc = Arc::new(pipeline);
|
||||
@@ -330,13 +334,11 @@ impl PipelineCache {
|
||||
}
|
||||
|
||||
/// Builds a RenderPipeline from a shader module, device, and surface texture format.
|
||||
/// Inputs: device (GPU command source), format (output texture format), shader (compiled WGSL module).
|
||||
/// Inputs: format (output texture format), shader (compiled WGSL module).
|
||||
/// Uses `self.device` and `self.sample_count` (Étape 24: MSAA-aware compilation).
|
||||
/// Returns a fully configured RenderPipeline ready for draw calls. Called internally by `get_or_create()`.
|
||||
/// Internal steps: 1) define VertexBufferLayout from Vertex struct offsets →
|
||||
/// 2) create PipelineLayout with bind_group_layouts + immediate_size →
|
||||
/// 3) create RenderPipeline with vertex/fragment states, primitive config, multisample state.
|
||||
fn build_pipeline(
|
||||
device: &wgpu::Device,
|
||||
&self,
|
||||
format: wgpu::TextureFormat,
|
||||
shader: &wgpu::ShaderModule,
|
||||
) -> wgpu::RenderPipeline {
|
||||
@@ -349,9 +351,9 @@ impl PipelineCache {
|
||||
// attached to EVERY pipeline (Step 3, decision ratified "a single layout for all"), 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 shadow_layout = create_shadow_map_bind_group_layout(device);
|
||||
let uniform_layouts = create_uniform_bind_group_layouts(&self.device);
|
||||
let texture_layout = create_texture_bind_group_layout(&self.device);
|
||||
let shadow_layout = create_shadow_map_bind_group_layout(&self.device);
|
||||
let layout_refs: Vec<Option<&wgpu::BindGroupLayout>> = vec![
|
||||
Some(&uniform_layouts[0]), // frame @0
|
||||
Some(&uniform_layouts[1]), // object @1
|
||||
@@ -359,14 +361,14 @@ impl PipelineCache {
|
||||
Some(&shadow_layout), // shadow map @3
|
||||
];
|
||||
let render_pipeline_layout =
|
||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("render_pipeline_layout"),
|
||||
bind_group_layouts: &layout_refs,
|
||||
immediate_size: 0, // no var<immediate> used
|
||||
});
|
||||
|
||||
// Create the full RenderPipeline — vertex state + fragment state + primitive configuration.
|
||||
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("Render Pipeline"),
|
||||
layout: Some(&render_pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
@@ -399,7 +401,11 @@ impl PipelineCache {
|
||||
stencil: wgpu::StencilState::default(),
|
||||
bias: wgpu::DepthBiasState::default(),
|
||||
}),
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
// Étape 24: MSAA-aware — the sample count must match the render pass's attachments.
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: self.sample_count,
|
||||
..Default::default()
|
||||
},
|
||||
// multiview → replaced by multiview_mask (NonZeroU32) and cache fields in wgpu 30.
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
// Core types
|
||||
pub use crate::core::geometry::{BBox, Geometry};
|
||||
pub use crate::core::transform::Transform;
|
||||
pub use crate::core::{BloomConfig, ShadowConfig, ToneMapper};
|
||||
pub use crate::core::{BloomConfig, MsaaConfig, ShadowConfig, ToneMapper};
|
||||
pub use crate::resources::Material;
|
||||
|
||||
// Camera
|
||||
|
||||
@@ -148,8 +148,9 @@ impl Scene {
|
||||
device: Arc<wgpu::Device>,
|
||||
queue: wgpu::Queue,
|
||||
format: wgpu::TextureFormat,
|
||||
sample_count: u32,
|
||||
) -> &mut Self {
|
||||
let cache = PipelineCache::new(device.clone(), queue.clone());
|
||||
let cache = PipelineCache::new(device.clone(), queue.clone(), sample_count);
|
||||
self.gpu = Some(SceneGpu {
|
||||
device,
|
||||
format,
|
||||
|
||||
Reference in New Issue
Block a user