9.5 KiB
Étape 24 — MSAA 4× (Anti-aliasing multi-échantillons)
Statut : ⬜ En cours Roadmap : 6.4 Prérequis : Pipeline HDR (étape 20) + Bloom (étape 23)
Problème
Sans anti-aliasing, les bords des géométries présentent du staircasing (aliasing) : les silhouettes ont des escaliers visibles, surtout sur les contours fins et les lointains. C'est le défaut visuel le plus flagrant d'un moteur sans post-process.
Solution
Rendre la scène avec N échantillons par pixel (4× par défaut), puis résoudre (en moyenne) vers une texture single-sample. Le reste du pipeline (bloom, TM) opère sur la texture résolue — aucun changement.
Pipeline actuel vs avec MSAA
Sans HDR, sans MSAA (actuel)
Scene → swapchain (Rgba8UnormSrgb) → present
Sans HDR, avec MSAA
Scene → MSAA texture (4×, format surface) ──resolve──→ swapchain → present
+ MSAA depth (4×)
Le resolve est fait automatiquement par wgpu dans le render pass
(resolve_target sur la color attachment).
Avec HDR, sans MSAA (actuel)
Scene → HDR texture (Rgba16Float) → [Bloom] → TM → swapchain → present
Avec HDR, avec MSAA (nouveau)
Scene → MSAA HDR (4×, Rgba16Float) ──resolve──→ HDR texture (Rgba16Float)
+ MSAA depth (4×) → [Bloom] → TM → swapchain → present
Principe : MSAA s'insère uniquement entre le rasterizer et le premier consommateur de la texture de scène. Les post-processes (bloom, TM) voient toujours une texture single-sample.
Décisions de design
| # | Décision | Rationale |
|---|---|---|
| D1 | AppBuilder::with_msaa(count) — opt-in, zero cost désactivé |
Principe WSG : chaque effet est optionnel |
| D2 | sample_count configurable (2, 4, 8) — default 4 |
4× est le bon rapport qualité/coût ; 8× pour du "max" |
| D3 | Texture MSAA offscreen (jamais le swapchain en MSAA direct) | Uniformité : même code path que HDR, resize plus simple |
| D4 | Depth buffer recréé en MSAA quand actif | Le depth doit avoir le même sample_count que le color |
| D5 | Resolve via RenderPassColorAttachment::resolve_target |
Natif wgpu, pas de shader supplémentaire |
| D6 | Aucun nouveau shader | MSAA est une feature rasterizer, pas un post-process |
| D7 | Format MSAA = format de la cible (Rgba16Float si HDR, surface format sinon) | Le resolve produit la même texture qu'avant |
| D8 | Resize recrée les textures MSAA + depth | Même pattern que HDR resize |
| D9 | Bloom/TM inchangés — ils lisent la texture résolue (single-sample) | Zéro impact sur les passes post |
| D10 | MsaaConfig { sample_count: u32 } — public, re-exporté |
API minimale, extensible |
API utilisateur
use wsg_lib::prelude::*;
let app = AppBuilder::new()
.title("MSAA Demo")
.with_hdr(ToneMapper::Aces)
.with_msaa(4) // ← 4 échantillons (2, 4, ou 8)
.with_bloom(BloomConfig::default())
.build()
.await?;
Sans .with_msaa(...) → comportement identique à aujourd'hui (0 échantillon overhead).
Implémentation
24.1 — MsaaConfig (public)
Fichier : lib/src/core/msaa.rs
/// Configuration MSAA. `sample_count` doit être 2, 4, ou 8
/// (valeur supportée par le GPU — vérifiée à l'init).
#[derive(Clone, Copy, Debug)]
pub struct MsaaConfig {
pub sample_count: u32,
}
impl Default for MsaaCapable {
fn default() -> Self {
Self { sample_count: 4 }
}
}
24.2 — Champs Renderer
Ajouter à Renderer :
msaa_config: MsaaConfig, // toujours présent (sample_count=1 si désactivé)
msaa_color_texture: Option<wgpu::Texture>, // Some si msaa active
msaa_color_view: Option<wgpu::TextureView>,
msaa_depth_texture: Option<wgpu::Texture>,
msaa_depth_view: Option<wgpu::TextureView>,
Quand msaa_config.sample_count > 1 :
msaa_color_texture= texturesample_count=N, format = HDR ou surfacemsaa_depth_texture= texture depthsample_count=N- Le render pass scène utilise ces views +
resolve_target
Quand sample_count == 1 :
- Tous les
OptionsontNone - Le render pass utilise la texture/view existante (comportement actuel)
24.3 — Allocation à l'init (Renderer::new)
let sample_count = msaa_config.map(|c| c.sample_count).unwrap_or(1);
// Vérifier que le format supporte ce sample_count
let formats = device.limits(); // ou surface.capabilities()
// Pour le surface : surface.capabilities().formats
// Pour l'offscreen : device.limits().max_color_attachments, etc.
// En pratique : Rgba16Float et Rgba8Unorm supportent 4× partout.
if sample_count > 1 {
let msaa_tex = device.create_texture(&TextureDescriptor {
size: Extent3d { width, height, depth_or_array_layers: 1 },
sample_count,
dimension: Dimension::D2,
format: target_format, // Rgba16Float ou surface format
usage: TextureUsages::RENDER_ATTACHMENT,
..
});
// + depth MSAA
}
24.4 — Render pass scène (modification render_scene)
Le render pass principal doit utiliser les views MSAA quand actif :
// Déterminer la color attachment
let (color_view, resolve_target) = if let Some(msaa_view) = &self.msaa_color_view {
// MSAA actif : render dans MSAA, resolve vers la texture single
let resolve = if self.hdr.is_some() {
Some(self.hdr.as_ref().unwrap().view.clone()) // resolve → HDR tex
} else {
Some(view.clone()) // resolve → swapchain
};
(msaa_view.clone(), resolve)
} else {
// Pas de MSAA : comportement actuel
let color_view = if let Some(hdr) = &self.hdr {
hdr.view.clone()
} else {
view.clone()
};
(color_view, None)
};
// Depth : MSAA ou single
let depth_view = if let Some(msaa_depth) = &self.msaa_depth_view {
msaa_depth.clone()
} else {
self.depth_view.clone()
};
encoder.render_pass(RenderPassDescriptor {
color_attachments: &[Some(RenderPassColorAttachment {
view: &color_view,
resolve_target: resolve_target.as_ref(),
load_op: LoadOp::Clear,
store_op: StoreOp::Store, // Store même en MSAA (wgpu gère le resolve)
})],
depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
view: &depth_view,
..
}),
..
});
24.5 — Resize
fn resize(&mut self, device, width, height) {
// ... resize depth existant ...
if self.msaa_config.sample_count > 1 {
// Recréer MSAA color + depth
self.msaa_color_texture = Some(create_msaa_texture(...));
self.msaa_color_view = Some(...);
self.msaa_depth_texture = Some(create_msaa_depth(...));
self.msaa_depth_view = Some(...);
}
// HDR resize existant
// Bloom resize existant
}
24.6 — Plomberie App/AppBuilder
// AppBuilder
pub fn with_msaa(mut self, sample_count: u32) -> Self {
assert!((2..=8).contains(&sample_count) && sample_count.is_power_of_two(),
"sample_count must be 2, 4, or 8");
self.msaa_config = Some(MsaaConfig { sample_count });
self
}
// App
pub fn msaa_enabled(&self) -> bool { ... }
pub fn set_msaa(&mut self, sample_count: u32) { ... } // nécessite resize
Plomberie : AppBuilder → App → AppRunner → Renderer::new(msaa_config)
24.7 — Re-exports
lib.rs + prelude.rs : pub use crate::core::MsaaConfig;
24.8 — Exemple msaa.rs
Scène simple (cube + sphere + ground) avec/without MSAA commutable à la runtime
(clavier M). Camera orbitale pour voir les bords de près.
Contrôles :
M— toggle MSAA (nécessite un resize/recréation des textures)R/1/2/3— presets caméra- Drag/wheel — orbit/zoom
24.9 — Documentation
docs/user/msaa.md: activation, config, coûts, limitationsdocs/user/README.md: section "Anti-aliasing"lib/examples/README.md: entréemsaadocs/ROADMAP.md: 6.4 → ✅
Coût GPU
| Config | Coût relatif |
|---|---|
| Sans MSAA | 1× |
| MSAA 4× | ~1.3–1.5× (le rasterizer sur-échantillonne, le fill rate est partagé) |
| MSAA 8× | ~1.5–2× |
MSAA est beaucoup moins coûteux qu'un post-process AA (FXAA, TAA) car le sur-coût est dans le rasterizer (edges only) et pas dans un blur fullscreen. Les post-processes (bloom, TM) ne sont pas affectés — ils tournent sur la texture résolue (1 échantillon/pixel).
Limitations / non-goals
- Pas de TAA (temporal AA) — nécessiterait un history buffer + motion vectors, bien plus complexe. MSAA 4× couvre 90% du besoin pour un lib "simple".
- Pas de MSAA sur les post-processes — le bloom/TM lissent déjà l'image.
- Pas de coverage sampling (DX12-only) — WebGPU expose seulement MSAA.
- Resize = recréation des textures MSAA (pas de resize in-place).
Tests
MsaaConfigDefault = 4- Validation
sample_count(rejette 3, 5, 16) - Build avec
with_msaa(4)+ HDR + Bloom → compile - Exemple
msaa.rscompile - WGSL validation inchangé (pas de nouveau shader)
- Test unit :
Renderer::newavecmsaa_config=Some(4)ne panique pas (nécessite un device mock — peut être un test intégration ignoré)
Critères d'acceptation
cargo check0 errors, 0 warningscargo testtous vertscargo run -p wsg-lib --example msaa→ bords lisses avec M, escaliers sans Mcargo run -p wsg-lib --example demo→ inchangé (pas dewith_msaa)- Combinaison HDR + Bloom + MSAA fonctionne (demo avec
.with_msaa(4))