MSAA
This commit is contained in:
+257
-302
@@ -1,343 +1,298 @@
|
|||||||
# Étape 23 — Bloom (post-process HDR)
|
# Étape 24 — MSAA 4× (Anti-aliasing multi-échantillons)
|
||||||
|
|
||||||
**Statut** : ✅ Terminé
|
**Statut** : ⬜ En cours
|
||||||
**Prérequis** : HDR + Tone Mapping (Étape 20 ✅), Emissive (Étape 22 ✅)
|
**Roadmap** : 6.4
|
||||||
|
**Prérequis** : Pipeline HDR (étape 20) + Bloom (étape 23)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Objectif
|
## Problème
|
||||||
|
|
||||||
Ajouter un effet **bloom** : les zones très brillantes de la scène (emissive > 1.0, spéculaires,
|
Sans anti-aliasing, les bords des géométries présentent du **staircasing** (aliasing) :
|
||||||
overbright lighting) diffusent une lueur vers les zones voisines. C'est l'effet "glow" qui rend
|
les silhouettes ont des escaliers visibles, surtout sur les contours fins et les
|
||||||
les néons et les sources de lumière visuellement impactants.
|
lointains. C'est le défaut visuel le plus flagrant d'un moteur sans post-process.
|
||||||
|
|
||||||
Le bloom est un **post-process** qui opère sur la texture HDR, entre le rendu de la scène et le
|
## Solution
|
||||||
tone mapping. Il est **opt-in** (`AppBuilder::with_bloom(...)`) et n'a **zéro coût** quand
|
|
||||||
désactivé (aucune texture/pipeline allouée).
|
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
|
## Pipeline actuel vs avec MSAA
|
||||||
|
|
||||||
|
### Sans HDR, sans MSAA (actuel)
|
||||||
```
|
```
|
||||||
Scene render → HDR texture (Rgba16Float, full res)
|
Scene → swapchain (Rgba8UnormSrgb) → present
|
||||||
│
|
|
||||||
├─[bloom actif?]─→ 1. Threshold (half res) : extrait les pixels > threshold
|
|
||||||
│ 2. Blur H (half res) : Gaussian 9 taps
|
|
||||||
│ 3. Blur V (half res) : Gaussian 9 taps
|
|
||||||
│ 4. Composite (full res) : HDR += bloom × intensity
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
TM pass → surface
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Quand bloom est désactivé : `Scene → HDR → TM → surface` (comme aujourd'hui, zéro overhead).
|
### 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).
|
||||||
|
|
||||||
**4 passes fullscreen** supplémentaires (seulement si HDR + bloom actifs).
|
### 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Composants
|
## Décisions de design
|
||||||
|
|
||||||
### `BloomConfig` (pub, dans `core/bloom.rs`)
|
| # | Décision | Rationale |
|
||||||
|
|---|----------|-----------|
|
||||||
```rust
|
| D1 | `AppBuilder::with_msaa(count)` — opt-in, zero cost désactivé | Principe WSG : chaque effet est optionnel |
|
||||||
pub struct BloomConfig {
|
| D2 | `sample_count` configurable (2, 4, 8) — default 4 | 4× est le bon rapport qualité/coût ; 8× pour du "max" |
|
||||||
/// Seuil de luminance (en unités HDR linéaires). Au-dessus → contribue au bloom.
|
| D3 | Texture MSAA **offscreen** (jamais le swapchain en MSAA direct) | Uniformité : même code path que HDR, resize plus simple |
|
||||||
/// Défaut : 1.0 (seul ce qui dépasse 1.0 "bloom" — les emissives > 1.0, les spéculaires).
|
| D4 | Depth buffer recréé en MSAA quand actif | Le depth doit avoir le même `sample_count` que le color |
|
||||||
pub threshold: f32,
|
| D5 | Resolve via `RenderPassColorAttachment::resolve_target` | Natif wgpu, pas de shader supplémentaire |
|
||||||
/// Intensité du bloom (multiplicateur sur le résultat du blur). Défaut : 0.8.
|
| D6 | **Aucun nouveau shader** | MSAA est une feature rasterizer, pas un post-process |
|
||||||
pub intensity: f32,
|
| D7 | Format MSAA = format de la cible (Rgba16Float si HDR, surface format sinon) | Le resolve produit la même texture qu'avant |
|
||||||
/// Rayon du blur en pixels (à la résolution half-res). Défaut : 4.0.
|
| D8 | Resize recrée les textures MSAA + depth | Même pattern que HDR resize |
|
||||||
pub radius: f32,
|
| 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 |
|
||||||
impl Default for BloomConfig { /* threshold=1.0, intensity=0.8, radius=4.0 */ }
|
|
||||||
```
|
|
||||||
|
|
||||||
### `BloomPipeline` (interne, dans `core/bloom.rs`)
|
|
||||||
|
|
||||||
```rust
|
|
||||||
struct BloomPipeline {
|
|
||||||
/// Texture half-res pour le bloom (Rgba16Float).
|
|
||||||
bright_texture: wgpu::Texture,
|
|
||||||
bright_view: wgpu::TextureView,
|
|
||||||
/// Texture half-res pour le blur ping-pong (2nd buffer).
|
|
||||||
blur_texture: wgpu::Texture,
|
|
||||||
blur_view: wgpu::TextureView,
|
|
||||||
/// Sampler linear pour le blur.
|
|
||||||
sampler: wgpu::Sampler,
|
|
||||||
/// Pipeline threshold (fullscreen → half-res).
|
|
||||||
threshold_pipeline: wgpu::RenderPipeline,
|
|
||||||
/// Pipeline blur (fullscreen half-res, direction via uniform).
|
|
||||||
blur_pipeline: wgpu::RenderPipeline,
|
|
||||||
/// Pipeline composite (full-res: HDR += bloom).
|
|
||||||
composite_pipeline: wgpu::RenderPipeline,
|
|
||||||
/// Bind groups pré-alloués.
|
|
||||||
threshold_bg: wgpu::BindGroup,
|
|
||||||
blur_bg_a: wgpu::BindGroup, // reads bright, writes blur
|
|
||||||
blur_bg_b: wgpu::BindGroup, // reads blur, writes bright (ping-pong)
|
|
||||||
composite_bg: wgpu::BindGroup, // reads HDR + bright
|
|
||||||
/// Uniform buffer pour le blur (direction + radius).
|
|
||||||
blur_uniform: wgpu::Buffer,
|
|
||||||
/// Uniform buffer pour le threshold (threshold value).
|
|
||||||
threshold_uniform: wgpu::Buffer,
|
|
||||||
/// Half-res dimensions.
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Shaders (3 fichiers WGSL)
|
|
||||||
|
|
||||||
#### `bloom_threshold.wgsl`
|
|
||||||
- Vertex : fullscreen triangle
|
|
||||||
- Fragment : lit la texture HDR (full res), calcule la luminance, sort `color × smoothstep(threshold, threshold+knee, lum)` ou `max(color - threshold, 0)` si `lum > threshold`, sinon `0`
|
|
||||||
- Écrit dans la texture half-res
|
|
||||||
|
|
||||||
#### `bloom_blur.wgsl`
|
|
||||||
- Vertex : fullscreen triangle (à la résolution half-res)
|
|
||||||
- Fragment : 9-tap Gaussian séparable. L'offset est `texel_size × radius × i` dans la direction donnée par l'uniform.
|
|
||||||
- Uniform : `vec2<f32> direction` (dx, dy), `f32 radius`
|
|
||||||
- Weights Gaussian : `[0.227027, 0.194595, 0.121622, 0.054054, 0.016216]` (symétrique)
|
|
||||||
|
|
||||||
#### `bloom_composite.wgsl`
|
|
||||||
- Vertex : fullscreen triangle (full res)
|
|
||||||
- Fragment : `result = hdr_color + bloom_color × intensity`
|
|
||||||
- Uniform : `f32 intensity`
|
|
||||||
- Lit les 2 textures (HDR full-res + bloom half-res, upscalé par le sampler linear)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Shaders
|
|
||||||
|
|
||||||
### `bloom_threshold.wgsl`
|
|
||||||
|
|
||||||
```wgsl
|
|
||||||
// Fullscreen triangle vertex (même pattern que tonemap)
|
|
||||||
struct VsOut {
|
|
||||||
@builtin(position) pos: vec4<f32>,
|
|
||||||
@location(0) uv: vec2<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
@vertex
|
|
||||||
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
|
|
||||||
var pos: vec2<f32>;
|
|
||||||
pos.x = f32((vi << 1) & 2) * 2.0 - 1.0;
|
|
||||||
pos.y = f32(vi & 2) * 2.0 - 1.0;
|
|
||||||
var out: VsOut;
|
|
||||||
out.pos = vec4<f32>(pos.x, -pos.y, 0.0, 1.0);
|
|
||||||
out.uv = vec2<f32>(pos.x * 0.5 + 0.5, 0.5 - pos.y * 0.5);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThresholdUniforms {
|
|
||||||
threshold: f32,
|
|
||||||
knee: f32,
|
|
||||||
pad: vec2<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> tmu: ThresholdUniforms;
|
|
||||||
@group(0) @binding(1) var src_tex: texture_2d<f32>;
|
|
||||||
@group(0) @binding(2) var src_sampler: sampler;
|
|
||||||
@group(0) @binding(3) var<atomic u32> pad; // placeholder — not needed, use texture_storage
|
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|
||||||
let color = textureSample(src_tex, src_sampler, in.uv).rgb;
|
|
||||||
let lum = dot(color, vec3<f32>(0.2126, 0.7152, 0.0722));
|
|
||||||
// Soft knee: smooth transition above threshold
|
|
||||||
let soft = max(lum - tmu.threshold, 0.0);
|
|
||||||
let contrib = soft / (soft + tmu.knee); // 0..1 smooth
|
|
||||||
return vec4<f32>(color * contrib, 1.0);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `bloom_blur.wgsl`
|
|
||||||
|
|
||||||
```wgsl
|
|
||||||
// Même VsOut / vs_main que threshold (fullscreen triangle)
|
|
||||||
|
|
||||||
struct BlurUniforms {
|
|
||||||
direction: vec2<f32>, // texel offset: (1/w, 0) or (0, 1/h)
|
|
||||||
radius: f32,
|
|
||||||
pad: vec2<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> bu: BlurUniforms;
|
|
||||||
@group(0) @binding(1) var src_tex: texture_2d<f32>;
|
|
||||||
@group(0) @binding(2) var src_sampler: sampler;
|
|
||||||
|
|
||||||
const W: array<f32, 5> = array<f32, 5>(
|
|
||||||
0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162
|
|
||||||
);
|
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|
||||||
let center = textureSample(src_tex, src_sampler, in.uv).rgb;
|
|
||||||
var sum = center * W[0];
|
|
||||||
for (var i: u32 = 1u; i < 5u; i = i + 1u) {
|
|
||||||
let off = bu.direction * (f32(i) * bu.radius);
|
|
||||||
let s = textureSample(src_tex, src_sampler, in.uv + off).rgb
|
|
||||||
+ textureSample(src_tex, src_sampler, in.uv - off).rgb;
|
|
||||||
sum = sum + s * W[i];
|
|
||||||
}
|
|
||||||
return vec4<f32>(sum, 1.0);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### `bloom_composite.wgsl`
|
|
||||||
|
|
||||||
```wgsl
|
|
||||||
// Même VsOut / vs_main
|
|
||||||
|
|
||||||
struct CompositeUniforms {
|
|
||||||
intensity: f32,
|
|
||||||
pad: vec3<f32>,
|
|
||||||
};
|
|
||||||
|
|
||||||
@group(0) @binding(0) var<uniform> cu: CompositeUniforms;
|
|
||||||
@group(0) @binding(1) var hdr_tex: texture_2d<f32>;
|
|
||||||
@group(0) @binding(2) var hdr_sampler: sampler;
|
|
||||||
@group(0) @binding(3) var bloom_tex: texture_2d<f32>;
|
|
||||||
@group(0) @binding(4) var bloom_sampler: sampler;
|
|
||||||
|
|
||||||
@fragment
|
|
||||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
|
||||||
let hdr = textureSample(hdr_tex, hdr_sampler, in.uv).rgb;
|
|
||||||
let bloom = textureSample(bloom_tex, bloom_sampler, in.uv).rgb;
|
|
||||||
return vec4<f32>(hdr + bloom * cu.intensity, 1.0);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Intégration dans `Renderer::render_scene`
|
|
||||||
|
|
||||||
```
|
|
||||||
Step 7: Main render pass → HDR texture (ou surface si pas HDR)
|
|
||||||
Step 8: [Bloom] Si HDR + bloom actifs :
|
|
||||||
8a. Threshold pass (HDR full → bright half)
|
|
||||||
8b. Blur H (bright half → blur half)
|
|
||||||
8c. Blur V (blur half → bright half) [ping-pong]
|
|
||||||
8d. Composite (HDR full + bright half → HDR full)
|
|
||||||
8e. write_buffer(exposure) — comme aujourd'hui
|
|
||||||
Step 9: TM pass (HDR full → surface)
|
|
||||||
```
|
|
||||||
|
|
||||||
Le composite **modifie la texture HDR in-place** (rend dans une 2ème texture puis swap, ou
|
|
||||||
rend directement dans la HDR texture si on utilise un ping-pong). En pratique : le composite
|
|
||||||
rend dans la `HDR texture` elle-même (le bind group lit la HDR comme input ET écrit dedans —
|
|
||||||
**NON**, c'est undefined behavior en wgpu).
|
|
||||||
|
|
||||||
**Solution** : le composite écrit dans un **3ème buffer full-res** (ou on swap les rôles :
|
|
||||||
le bloom écrit dans la HDR texture en lisant une copie). La solution la plus simple :
|
|
||||||
- Le threshold lit la HDR texture et écrit dans `bright` (half res)
|
|
||||||
- Le blur ping-ponge entre `bright` et `blur` (half res)
|
|
||||||
- Le composite lit la HDR texture + `bright` (half res) et écrit dans la **HDR texture**
|
|
||||||
(c'est OK car le composite est une pass séparée qui commence APRÈS que le threshold/blur
|
|
||||||
ont fini d'écrire — et le composite lit la HDR texture en input mais écrit aussi dedans)
|
|
||||||
|
|
||||||
Attendez — **non**, en wgpu/WebGPU, on ne peut PAS lire et écrire la même texture dans la même
|
|
||||||
render pass. Mais on peut le faire dans des **passes différentes** (le composite est une pass
|
|
||||||
séparée du threshold). Le problème est que le composite lit la HDR texture (qui n'a pas été
|
|
||||||
modifiée par threshold/blur — ils ont écrit dans bright/blur) et écrit dans la HDR texture.
|
|
||||||
C'est **valide** car c'est dans une render pass unique : le GPU ne permet pas de lire ET écrire
|
|
||||||
la même texture attachment dans la même pass.
|
|
||||||
|
|
||||||
**Solution propre** : utiliser un **ping-pong full-res** :
|
|
||||||
- `hdr_texture` (existante) : contient le rendu de la scène
|
|
||||||
- `bloom_composite_texture` (full-res, allouée avec le bloom) : reçoit le résultat du composite
|
|
||||||
- Le TM pass lit `bloom_composite_texture` au lieu de `hdr_texture`
|
|
||||||
|
|
||||||
Quand bloom est inactif : le TM lit `hdr_texture` directement (comme aujourd'hui).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API utilisateur
|
## API utilisateur
|
||||||
|
|
||||||
| Composant | Changement |
|
```rust
|
||||||
|-----------|-----------|
|
use wsg_lib::prelude::*;
|
||||||
| `AppBuilder` | `with_bloom(config: BloomConfig)` — active le bloom |
|
|
||||||
| `App` | `set_bloom_config(config)`, `bloom_enabled() -> bool` |
|
|
||||||
| `Renderer` | Champ `bloom: Option<BloomPipeline>`, `bloom_config: BloomConfig` |
|
|
||||||
| `core/mod.rs` | `pub mod bloom;` + re-export `BloomConfig` |
|
|
||||||
| `lib.rs` | Re-export `BloomConfig` |
|
|
||||||
| `prelude.rs` | Re-export `BloomConfig` |
|
|
||||||
|
|
||||||
**Règle** : le bloom n'a d'effet que si HDR est actif. `with_bloom()` sans `with_hdr()` est
|
let app = AppBuilder::new()
|
||||||
un no-op (log un warning).
|
.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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resize
|
## Implémentation
|
||||||
|
|
||||||
Au resize, si le bloom est actif :
|
### 24.1 — `MsaaConfig` (public)
|
||||||
- Recréer les textures half-res (bright, blur)
|
|
||||||
- Recréer le composite texture full-res
|
Fichier : `lib/src/core/msaa.rs`
|
||||||
- Recréer les bind groups
|
|
||||||
- Mettre à jour les uniforms (dimensions)
|
```rust
|
||||||
|
/// 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` :
|
||||||
|
```rust
|
||||||
|
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` = texture `sample_count=N`, format = HDR ou surface
|
||||||
|
- `msaa_depth_texture` = texture depth `sample_count=N`
|
||||||
|
- Le render pass scène utilise ces views + `resolve_target`
|
||||||
|
|
||||||
|
Quand `sample_count == 1` :
|
||||||
|
- Tous les `Option` sont `None`
|
||||||
|
- Le render pass utilise la texture/view existante (comportement actuel)
|
||||||
|
|
||||||
|
### 24.3 — Allocation à l'init (`Renderer::new`)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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 :
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
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
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 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, limitations
|
||||||
|
- `docs/user/README.md` : section "Anti-aliasing"
|
||||||
|
- `lib/examples/README.md` : entrée `msaa`
|
||||||
|
- `docs/ROADMAP.md` : 6.4 → ✅
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Décisions
|
## Coût GPU
|
||||||
|
|
||||||
| # | Décision | Justification |
|
| Config | Coût relatif |
|
||||||
|---|----------|---------------|
|
|--------|:---:|
|
||||||
| D1 | 4 passes (threshold + blur H + blur V + composite) | Bonne qualité/performances. Un seul niveau de mip suffit pour un bloom "soft" |
|
| Sans MSAA | 1× |
|
||||||
| D2 | Résolution half-res pour le bloom | Standard. Le blur à half-res est 4× moins coûteux et le résultat upscalé par le sampler linear est lisse |
|
| MSAA 4× | ~1.3–1.5× (le rasterizer sur-échantillonne, le fill rate est partagé) |
|
||||||
| D3 | Soft-knee threshold (pas un cutoff dur) | `soft/(soft+knee)` donne une transition douce, pas d'aliasing au seuil |
|
| MSAA 8× | ~1.5–2× |
|
||||||
| D4 | Composite via ping-pong full-res (3ème texture) | Évite le conflit read/write sur la même texture dans une même pass |
|
|
||||||
| D5 | Bloom seulement si HDR actif | Le bloom opère en espace linéaire HDR. Sans HDR, les valeurs sont déjà clampées [0,1] → pas de "bright" à extraire |
|
|
||||||
| D6 | `BloomConfig` avec 3 champs (threshold, intensity, radius) | Minimum utile. Pas de multi-mip, pas de directional bloom pour MVP |
|
|
||||||
| D7 | Sampler `Linear` + `ClampToEdge` pour le blur | Les bords ne doivent pas sampler hors-texture (artefacts noirs) |
|
|
||||||
| D8 | Le TM pass lit la texture composite (si bloom) ou la HDR (si pas bloom) | Le TM est agnostique de la source — il lit juste une texture full-res Rgba16Float |
|
|
||||||
| D9 | Uniform threshold : 16 bytes (threshold + knee + 2 pad) | Aligned 16, simple |
|
|
||||||
| D10 | Uniform blur : 16 bytes (direction vec2 + radius + pad) | Aligned 16 |
|
|
||||||
| D11 | Uniform composite : 16 bytes (intensity + 3 pad) | Aligned 16 |
|
|
||||||
|
|
||||||
---
|
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).
|
||||||
|
|
||||||
## Fichiers modifiés / créés
|
## Limitations / non-goals
|
||||||
|
|
||||||
| Fichier | Changement |
|
- **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".
|
||||||
| `lib/src/core/bloom.rs` | **Nouveau** : `BloomConfig`, `BloomPipeline`, allocation + bind groups |
|
- **Pas de MSAA sur les post-processes** — le bloom/TM lissent déjà l'image.
|
||||||
| `lib/src/core/renderer.rs` | + `bloom: Option<BloomPipeline>`, `bloom_config` ; passes 8a-8d ; TM lit composite ou HDR ; resize |
|
- **Pas de coverage sampling** (DX12-only) — WebGPU expose seulement MSAA.
|
||||||
| `lib/src/core/hdr.rs` | `create_hdr_bind_group` accepte une texture arbitraire (pas seulement `self.texture`) |
|
- **Resize = recréation** des textures MSAA (pas de resize in-place).
|
||||||
| `lib/src/core/mod.rs` | + `pub mod bloom;` + re-exports |
|
|
||||||
| `lib/src/shaders/bloom_threshold.wgsl` | **Nouveau** |
|
|
||||||
| `lib/src/shaders/bloom_blur.wgsl` | **Nouveau** |
|
|
||||||
| `lib/src/shaders/bloom_composite.wgsl` | **Nouveau** |
|
|
||||||
| `lib/src/shaders/conf.rs` | + `BLOOM_THRESHOLD_SHADER`, `BLOOM_BLUR_SHADER`, `BLOOM_COMPOSITE_SHADER` |
|
|
||||||
| `lib/src/app.rs` | + `bloom_config`, `bloom_enabled`, `set_bloom_config`, builder `with_bloom` |
|
|
||||||
| `lib/src/lib.rs` | Re-export `BloomConfig` |
|
|
||||||
| `lib/src/prelude.rs` | Re-export `BloomConfig` |
|
|
||||||
| `lib/tests/wgsl_validate.rs` | + 3 tests (threshold, blur, composite) |
|
|
||||||
| `lib/examples/demo.rs` | + `with_bloom(BloomConfig::default())` |
|
|
||||||
| `docs/user/bloom.md` | **Nouveau** : doc utilisateur |
|
|
||||||
| `docs/ROADMAP.md` | 6.3 → ✅ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
| Test | Vérifie |
|
- [ ] `MsaaConfig` Default = 4
|
||||||
|------|---------|
|
- [ ] Validation `sample_count` (rejette 3, 5, 16)
|
||||||
| `bloom_config_default` | threshold=1.0, intensity=0.8, radius=4.0 |
|
- [ ] Build avec `with_msaa(4)` + HDR + Bloom → compile
|
||||||
| `bloom_requires_hdr` | `with_bloom` sans `with_hdr` → warning, bloom inactif |
|
- [ ] Exemple `msaa.rs` compile
|
||||||
| `bloom_pipeline_allocates_half_res` | dimensions = (w/2, h/2) |
|
- [ ] WGSL validation inchangé (pas de nouveau shader)
|
||||||
| `bloom_zero_intensity_is_noop` | intensity=0 → composite = HDR (pas de changement) |
|
- [ ] Test unit : `Renderer::new` avec `msaa_config=Some(4)` ne panique pas
|
||||||
| WGSL threshold | compile avec naga |
|
(nécessite un device mock — peut être un test intégration ignoré)
|
||||||
| WGSL blur | compile avec naga |
|
|
||||||
| WGSL composite | compile avec naga |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Critères d'acceptation
|
## Critères d'acceptation
|
||||||
|
|
||||||
- [ ] `cargo test` passe (tous tests existants + nouveaux)
|
1. `cargo check` 0 errors, 0 warnings
|
||||||
- [ ] `cargo run --example demo` : le glow sphere produit un halo visible
|
2. `cargo test` tous verts
|
||||||
- [ ] Sans bloom : rendu identique à avant (zéro régression)
|
3. `cargo run -p wsg-lib --example msaa` → bords lisses avec M, escaliers sans M
|
||||||
- [ ] Sans HDR + avec bloom : pas de crash (bloom ignoré, warning)
|
4. `cargo run -p wsg-lib --example demo` → inchangé (pas de `with_msaa`)
|
||||||
- [ ] Resize : le bloom continue de fonctionner
|
5. Combinaison HDR + Bloom + MSAA fonctionne (demo avec `.with_msaa(4)`)
|
||||||
- [ ] 0 warnings
|
|
||||||
|
|||||||
+6
-1
@@ -66,10 +66,15 @@ Ce document est la **vue d'ensemble de progression**. Chaque étape a son DRAFT
|
|||||||
| 6.1 | **Exposure control** (clavier / API live) | ⭐⭐ | Trés faible | ✅ |
|
| 6.1 | **Exposure control** (clavier / API live) | ⭐⭐ | Trés faible | ✅ |
|
||||||
| 6.2 | **Emissive materials** (champ `emissive` → bénéficie du HDR) | ⭐⭐⭐ | Faible | ✅ |
|
| 6.2 | **Emissive materials** (champ `emissive` → bénéficie du HDR) | ⭐⭐⭐ | Faible | ✅ |
|
||||||
| 6.3 | **Bloom** (post-process : downsample → threshold → blur → composite) | ⭐⭐⭐ | Moyen | ✅ |
|
| 6.3 | **Bloom** (post-process : downsample → threshold → blur → composite) | ⭐⭐⭐ | Moyen | ✅ |
|
||||||
| 6.4 | **MSAA 4×** (anti-aliasing multi-échantillons + resolve) | ⭐⭐⭐ | Moyen | ⬜ |
|
| 6.4 | **MSAA 4×** (anti-aliasing multi-échantillons + resolve) | ⭐⭐⭐ | Moyen | ✅ |
|
||||||
| 6.5 | **Normal mapping / PBR** (nouveau shader, tangent space, metalness-roughness) | ⭐⭐⭐ | Élevé | ⬜ |
|
| 6.5 | **Normal mapping / PBR** (nouveau shader, tangent space, metalness-roughness) | ⭐⭐⭐ | Élevé | ⬜ |
|
||||||
| 6.6 | **Cascaded Shadow Maps** (2–3 cascades + blend, plus de précision près de la camera) | ⭐⭐ | Élevé | ⬜ |
|
| 6.6 | **Cascaded Shadow Maps** (2–3 cascades + blend, plus de précision près de la camera) | ⭐⭐ | Élevé | ⬜ |
|
||||||
| 6.7 | **SSAO** (ambient occlusion screen-space, depth + normal buffer) | ⭐⭐ | Élevé | ⬜ |
|
| 6.7 | **SSAO** (ambient occlusion screen-space, depth + normal buffer) | ⭐⭐ | Élevé | ⬜ |
|
||||||
|
| 6.13 | **Fog** (exponential / exponential² / height fog, paramètre par scène ou par matériau) | ⭐⭐⭐ | Faible | ⬜ |
|
||||||
|
| 6.14 | **Area lights** (rectangular area light, BRDF approx — specular + diffuse) | ⭐⭐⭐ | Élevé | ⬜ |
|
||||||
|
| 6.15 | **Textured area lights** (area light avec texture d’émission, e.g. panneaux LED, néons) | ⭐⭐⭐ | Moyen | ⬜ |
|
||||||
|
| 6.16 | **Volumetric lighting** (god rays / light scattering — radial blur ou ray-march 3D) | ⭐⭐⭐⭐ | Élevé | ⬜ |
|
||||||
|
| 6.17 | **Depth of field** (post-process CoC : circle-of-confusion + bokeh blur) | ⭐⭐⭐ | Moyen | ⬜ |
|
||||||
|
|
||||||
### Cibles techniques (refactoring)
|
### Cibles techniques (refactoring)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ GPU graphics background is required.
|
|||||||
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall |
|
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall |
|
||||||
| [Mesh & primitives](mesh.md) | Procedural generators + file import (OBJ), feature-gated |
|
| [Mesh & primitives](mesh.md) | Procedural generators + file import (OBJ), feature-gated |
|
||||||
| [HDR & tone mapping](hdr.md) | Offscreen float render + ACES/Reinhard, opt-in via `with_hdr` |
|
| [HDR & tone mapping](hdr.md) | Offscreen float render + ACES/Reinhard, opt-in via `with_hdr` |
|
||||||
|
| [MSAA (anti-aliasing)](msaa.md) | Multi-sample edge smoothing, opt-in via `with_msaa(4)` |
|
||||||
| [GPU-driven rendering](gpu-driven.md) | GPU world matrices + indirect draws, opt-in frustum culling |
|
| [GPU-driven rendering](gpu-driven.md) | GPU world matrices + indirect draws, opt-in frustum culling |
|
||||||
| [Camera & input](camera-input.md) | Active camera, orbital controller, unified keyboard/mouse state |
|
| [Camera & input](camera-input.md) | Active camera, orbital controller, unified keyboard/mouse state |
|
||||||
| [Examples](examples.md) | The 7 repo examples, the advanced `manual` workflow, adding your own example |
|
| [Examples](examples.md) | The 7 repo examples, the advanced `manual` workflow, adding your own example |
|
||||||
@@ -36,6 +37,7 @@ WSG follows a strict rule: **a feature you don't enable costs nothing at runtime
|
|||||||
|---------|--------------|----------------|
|
|---------|--------------|----------------|
|
||||||
| Shadows | `scene.set_shadow_caster(Some(idx))` | No shadow map allocated, no depth pass, no PCF sampling |
|
| Shadows | `scene.set_shadow_caster(Some(idx))` | No shadow map allocated, no depth pass, no PCF sampling |
|
||||||
| HDR + Tone mapping | `AppBuilder::with_hdr(ToneMapper::Aces)` | No offscreen texture, no TM pass, direct-to-surface render |
|
| HDR + Tone mapping | `AppBuilder::with_hdr(ToneMapper::Aces)` | No offscreen texture, no TM pass, direct-to-surface render |
|
||||||
|
| MSAA | `AppBuilder::with_msaa(4)` | Single-sample (1×), zero overhead |
|
||||||
| GPU-driven culling | `AppBuilder::with_gpu_driven(true)` | No compute pipeline, no indirect draw buffers |
|
| GPU-driven culling | `AppBuilder::with_gpu_driven(true)` | No compute pipeline, no indirect draw buffers |
|
||||||
| LOD | `scene.create_mesh_with_lod(…, levels)` | Single-level mesh, no decimation, no hysteresis |
|
| LOD | `scene.create_mesh_with_lod(…, levels)` | Single-level mesh, no decimation, no hysteresis |
|
||||||
| Primitives | Cargo feature `prim-*` (default: all) | Not compiled at all |
|
| Primitives | Cargo feature `prim-*` (default: all) | Not compiled at all |
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# MSAA (Anti-aliasing)
|
||||||
|
|
||||||
|
Multi-Sample Anti-Aliasing (MSAA) smooths jagged edges by rendering the scene
|
||||||
|
at a higher sample count (e.g. 4 samples per pixel), then averaging the samples
|
||||||
|
into the final image.
|
||||||
|
|
||||||
|
## Activation
|
||||||
|
|
||||||
|
MSAA is opt-in via the builder. When disabled (default), the renderer uses
|
||||||
|
single-sample rendering with zero overhead:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let app = AppBuilder::new()
|
||||||
|
.title("My App")
|
||||||
|
.with_msaa(4) // 4× MSAA (also: 2 or 8)
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
MSAA is a **rasterizer feature** — no new shader is needed. The pipeline:
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Without MSAA (default) │
|
||||||
|
│ │
|
||||||
|
│ Main pass ──→ HDR texture (1 sample) ──→ [Bloom] ──→ TM ──→ Surface │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ With MSAA 4× (and HDR) │
|
||||||
|
│ │
|
||||||
|
│ Main pass ──→ MSAA texture (4 samples, Rgba16Float) │
|
||||||
|
│ ↓ resolve (hardware average) │
|
||||||
|
│ HDR texture (1 sample) │
|
||||||
|
│ ↓ │
|
||||||
|
│ [Bloom] ──→ TM ──→ Surface │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ With MSAA 4× (no HDR) │
|
||||||
|
│ │
|
||||||
|
│ Main pass ──→ MSAA texture (4 samples, surface format) │
|
||||||
|
│ ↓ resolve │
|
||||||
|
│ Swapchain (surface) │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- The **main scene pass** renders into the MSAA texture (N samples/pixel).
|
||||||
|
- The **resolve** (hardware average) produces the single-sample output.
|
||||||
|
- **Post-processes** (bloom, tone mapping) operate on the **resolved**
|
||||||
|
single-sample texture — they are completely unaffected by MSAA.
|
||||||
|
- The **shadow map** is always single-sample (depth-only, not visible directly).
|
||||||
|
|
||||||
|
## Sample count
|
||||||
|
|
||||||
|
| Count | Quality | Cost (approx.) | Use case |
|
||||||
|
|-------|---------|----------------|----------|
|
||||||
|
| 2 | Basic | ~1.1× | Low-end / battery |
|
||||||
|
| **4** | **Good** | **~1.3–1.5×** | **Default, most games** |
|
||||||
|
| 8 | Excellent | ~1.6–2.0× | High-end / static scenes |
|
||||||
|
|
||||||
|
The cost is in the rasterizer/fill-rate (each pixel is shaded N times at
|
||||||
|
triangles' edges). Interior pixels (covered by a single triangle) are
|
||||||
|
shaded only once — MSAA only multiplies the **edge** cost.
|
||||||
|
|
||||||
|
## Runtime query
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// In AppHandler::setup or update:
|
||||||
|
let active = app.renderer().msaa_enabled(); // true if sample_count > 1
|
||||||
|
let count = app.renderer().msaa_sample_count(); // 1, 2, 4, or 8
|
||||||
|
```
|
||||||
|
|
||||||
|
MSAA is a **build-time** setting: the multi-sample textures are allocated
|
||||||
|
at startup. Changing the sample count requires recreating the textures
|
||||||
|
(window resize does this automatically).
|
||||||
|
|
||||||
|
## Configuration struct
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wsg_lib::MsaaConfig;
|
||||||
|
|
||||||
|
let config = MsaaConfig { sample_count: 4 };
|
||||||
|
// Or use the builder shortcut (validated):
|
||||||
|
let app = AppBuilder::new().with_msaa(4).build().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
| Feature | Compatible? | Notes |
|
||||||
|
|---------|-------------|-------|
|
||||||
|
| HDR + Tone Mapping | ✅ | MSAA texture is `Rgba16Float`, resolves into HDR |
|
||||||
|
| Bloom | ✅ | Bloom reads the resolved (single-sample) HDR texture |
|
||||||
|
| Shadows | ✅ | Shadow map is always single-sample |
|
||||||
|
| Frustum Culling | ✅ | Independent (compute pass) |
|
||||||
|
| LOD | ✅ | Independent (draw args) |
|
||||||
|
| Emissive | ✅ | Per-entity, in the main pass |
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- **Does not smooth UV-dependent aliasing** (texture shimmer). For that,
|
||||||
|
use mipmaps + anisotropic filtering (future: texture module).
|
||||||
|
- **Cost scales with overdraw**: fully transparent or heavily overlapping
|
||||||
|
geometry pays the full N× cost.
|
||||||
|
- **GPU support**: most modern GPUs support 4× for all formats. 8× may be
|
||||||
|
limited for float formats (check `Device::limits().max_color_attachment_samples`).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wsg_lib::app::AppBuilder;
|
||||||
|
use wsg_lib::core::ToneMapper;
|
||||||
|
|
||||||
|
let app = AppBuilder::new()
|
||||||
|
.title("MSAA Demo")
|
||||||
|
.size(1280, 720)
|
||||||
|
.with_msaa(4)
|
||||||
|
.with_hdr(ToneMapper::Aces)
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
See `examples/msaa.rs` for a full interactive demo with cube, sphere,
|
||||||
|
and ground plane where aliasing is clearly visible without MSAA.
|
||||||
+32
-1
@@ -16,7 +16,8 @@ cargo run -p wsg-lib --example <nom>
|
|||||||
| `hdr` | HDR + Tone Mapping (ACES) + contrôle d'exposition |
|
| `hdr` | HDR + Tone Mapping (ACES) + contrôle d'exposition |
|
||||||
| `emissive` | Matériaux émissifs (intensités croissantes 0 → 4.0) |
|
| `emissive` | Matériaux émissifs (intensités croissantes 0 → 4.0) |
|
||||||
| `shadow` | Shadow mapping (ombre portée directionnelle) |
|
| `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) |
|
| `manual` | Workflow bas niveau (Context + Renderer + PipelineCache) |
|
||||||
| `import` | Import de fichier OBJ (non graphique, stdout) |
|
| `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
|
## `manual` — Workflow bas niveau
|
||||||
|
|
||||||
Démontre l'API **sans** la façade `App` : utilisation directe de `Context`,
|
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)
|
// 2. Renderer initialization (it retrieves everything it needs)
|
||||||
let device = Arc::new(context.device.clone());
|
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
|
cache
|
||||||
.register_shader("standard", utils::STANDARD_SHADER_PATH)
|
.register_shader("standard", utils::STANDARD_SHADER_PATH)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Flat 2D rendering: `standard` in unlit mode (the frame+object bind groups are set by
|
// 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).
|
// 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);
|
renderer.set_unlit(true);
|
||||||
|
|
||||||
// 3. Material: uses renderer.device() and renderer.format()
|
// 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.
|
//! once right after GPU initialization so users can register shaders/meshes/materials/entities.
|
||||||
|
|
||||||
use crate::AppHandler;
|
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::input::InputState;
|
||||||
use crate::scene::Scene;
|
use crate::scene::Scene;
|
||||||
use crate::utils::WsgError;
|
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.
|
/// 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 (+/-).
|
/// Default 1.0. Adjustable at runtime via `set_exposure` or keyboard (+/-).
|
||||||
pub exposure: f32,
|
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.
|
/// 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
|
event_loop: Option<EventLoop<()>>, // On met en Option pour pouvoir faire .take() facilement
|
||||||
/// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle.
|
/// GPU hardware context — owns Instance, Surface, Adapter, Device, Queue lifecycle.
|
||||||
@@ -137,6 +140,7 @@ impl App {
|
|||||||
hdr: self.hdr,
|
hdr: self.hdr,
|
||||||
bloom_config: self.bloom_config.clone(),
|
bloom_config: self.bloom_config.clone(),
|
||||||
exposure: self.exposure,
|
exposure: self.exposure,
|
||||||
|
msaa: self.msaa.clone(),
|
||||||
handler,
|
handler,
|
||||||
app: None,
|
app: None,
|
||||||
};
|
};
|
||||||
@@ -211,8 +215,9 @@ impl App {
|
|||||||
// Étape 20: when HDR is active, the Scene uses Rgba16Float regardless of the
|
// É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.
|
// surface format, so no re-init is needed on surface format change.
|
||||||
let device = std::sync::Arc::new(self.renderer_mut().device().clone());
|
let device = std::sync::Arc::new(self.renderer_mut().device().clone());
|
||||||
|
let sc = self.renderer_mut().msaa_sample_count();
|
||||||
self.scene
|
self.scene
|
||||||
.init_gpu(device, self.context().queue.clone(), new_format);
|
.init_gpu(device, self.context().queue.clone(), new_format, sc);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -239,6 +244,8 @@ pub struct AppBuilder {
|
|||||||
bloom_config: Option<BloomConfig>,
|
bloom_config: Option<BloomConfig>,
|
||||||
/// Initial exposure multiplier (Étape 22, 6.1). Default 1.0.
|
/// Initial exposure multiplier (Étape 22, 6.1). Default 1.0.
|
||||||
exposure: f32,
|
exposure: f32,
|
||||||
|
/// MSAA configuration (Étape 24). `None` = no MSAA (default).
|
||||||
|
msaa: Option<MsaaConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppBuilder {
|
impl AppBuilder {
|
||||||
@@ -254,6 +261,7 @@ impl AppBuilder {
|
|||||||
hdr: None,
|
hdr: None,
|
||||||
bloom_config: None,
|
bloom_config: None,
|
||||||
exposure: 1.0,
|
exposure: 1.0,
|
||||||
|
msaa: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Sets the window title to display in the OS taskbar/window decorations.
|
/// Sets the window title to display in the OS taskbar/window decorations.
|
||||||
@@ -306,6 +314,19 @@ impl AppBuilder {
|
|||||||
self.exposure = exposure;
|
self.exposure = exposure;
|
||||||
self
|
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
|
/// 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
|
/// 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.
|
/// 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,
|
hdr: self.hdr,
|
||||||
bloom_config: self.bloom_config,
|
bloom_config: self.bloom_config,
|
||||||
exposure: self.exposure,
|
exposure: self.exposure,
|
||||||
|
msaa: self.msaa,
|
||||||
event_loop: Some(event_loop),
|
event_loop: Some(event_loop),
|
||||||
context: None,
|
context: None,
|
||||||
renderer: None,
|
renderer: None,
|
||||||
@@ -352,6 +374,8 @@ struct AppRunner<H: AppHandler> {
|
|||||||
bloom_config: Option<BloomConfig>,
|
bloom_config: Option<BloomConfig>,
|
||||||
/// Initial exposure (Étape 22, 6.1); stored in the App for per-frame use.
|
/// Initial exposure (Étape 22, 6.1); stored in the App for per-frame use.
|
||||||
exposure: f32,
|
exposure: f32,
|
||||||
|
/// MSAA config (Étape 24); passed to `Renderer::new` in `resumed`.
|
||||||
|
msaa: Option<MsaaConfig>,
|
||||||
/// The user-provided game logic.
|
/// The user-provided game logic.
|
||||||
handler: H,
|
handler: H,
|
||||||
/// The fully-built App facade, populated on the first `resumed` event.
|
/// 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");
|
.expect("surface configuration failed");
|
||||||
let device = Arc::new(context.device.clone());
|
let device = Arc::new(context.device.clone());
|
||||||
let renderer =
|
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).
|
// Step 15, D8: apply the culling flag (off by default — non-regression).
|
||||||
renderer.set_culling(self.culling);
|
renderer.set_culling(self.culling);
|
||||||
|
|
||||||
@@ -399,7 +423,8 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
|||||||
format
|
format
|
||||||
};
|
};
|
||||||
let mut scene = Scene::new();
|
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 {
|
let mut app = App {
|
||||||
scene,
|
scene,
|
||||||
@@ -412,6 +437,7 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
|||||||
hdr: self.hdr,
|
hdr: self.hdr,
|
||||||
bloom_config: self.bloom_config.clone(),
|
bloom_config: self.bloom_config.clone(),
|
||||||
exposure: self.exposure,
|
exposure: self.exposure,
|
||||||
|
msaa: self.msaa.clone(),
|
||||||
event_loop: None,
|
event_loop: None,
|
||||||
context: Some(context),
|
context: Some(context),
|
||||||
renderer: Some(renderer),
|
renderer: Some(renderer),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub mod frustum;
|
|||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
pub mod hdr;
|
pub mod hdr;
|
||||||
pub mod lod;
|
pub mod lod;
|
||||||
|
pub mod msaa;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod shadow;
|
pub mod shadow;
|
||||||
pub mod transform;
|
pub mod transform;
|
||||||
@@ -28,6 +29,7 @@ pub use frustum::Frustum;
|
|||||||
pub use geometry::{BBox, Geometry, GeometryError};
|
pub use geometry::{BBox, Geometry, GeometryError};
|
||||||
pub use hdr::ToneMapper;
|
pub use hdr::ToneMapper;
|
||||||
pub use lod::{lod_level, projected_radius_px};
|
pub use lod::{lod_level, projected_radius_px};
|
||||||
|
pub use msaa::MsaaConfig;
|
||||||
pub use renderer::Renderer;
|
pub use renderer::Renderer;
|
||||||
pub use shadow::ShadowConfig;
|
pub use shadow::ShadowConfig;
|
||||||
pub use transform::Transform;
|
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::scene::Scene;
|
||||||
use crate::core::bloom::{BloomConfig, BloomPipeline};
|
use crate::core::bloom::{BloomConfig, BloomPipeline};
|
||||||
use crate::core::hdr::ToneMapper;
|
use crate::core::hdr::ToneMapper;
|
||||||
|
use crate::core::msaa::MsaaConfig;
|
||||||
use crate::utils::conf::{
|
use crate::utils::conf::{
|
||||||
GPU_DRIVEN_SHADER, GPU_WORKGROUP_SIZE, LOD_THRESHOLDS, MAX_ENTITIES, MAX_LOD_LEVELS, TONEMAP_SHADER,
|
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: Option<BloomPipeline>,
|
||||||
/// Bloom configuration (used per-frame for uniform writes). Only meaningful when bloom is active.
|
/// Bloom configuration (used per-frame for uniform writes). Only meaningful when bloom is active.
|
||||||
bloom_config: BloomConfig,
|
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.
|
/// Internal HDR pipeline state: offscreen `Rgba16Float` texture + tone mapping render pipeline.
|
||||||
@@ -201,6 +212,7 @@ impl Renderer {
|
|||||||
shadow_config: &super::shadow::ShadowConfig,
|
shadow_config: &super::shadow::ShadowConfig,
|
||||||
hdr: Option<ToneMapper>,
|
hdr: Option<ToneMapper>,
|
||||||
bloom_config: Option<BloomConfig>,
|
bloom_config: Option<BloomConfig>,
|
||||||
|
msaa_config: Option<MsaaConfig>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let queue: wgpu::Queue = context.queue.clone();
|
let queue: wgpu::Queue = context.queue.clone();
|
||||||
let device: wgpu::Device = context.device.clone();
|
let device: wgpu::Device = context.device.clone();
|
||||||
@@ -594,6 +606,11 @@ impl Renderer {
|
|||||||
hdr: None,
|
hdr: None,
|
||||||
bloom: None,
|
bloom: None,
|
||||||
bloom_config: bloom_config.clone().unwrap_or_default(),
|
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
|
// 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.
|
// `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();
|
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
|
renderer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,6 +718,32 @@ impl Renderer {
|
|||||||
hdr.bind_group = bg;
|
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).
|
/// 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.
|
// 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
|
// É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.
|
// instead of the surface; the TM pass (step 8) then copies it to the surface.
|
||||||
let main_target = match &self.hdr {
|
// Étape 24: when MSAA is active, the color attachment targets the MSAA texture and
|
||||||
Some(h) => &h.view,
|
// resolves into the single-sample target (HDR or swapchain). The depth is also MSAA.
|
||||||
None => view,
|
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 {
|
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("scene render pass"),
|
label: Some("scene render pass"),
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
view: main_target,
|
view: &color_view,
|
||||||
resolve_target: None,
|
resolve_target: resolve_target.as_ref(),
|
||||||
depth_slice: None,
|
depth_slice: None,
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
store: wgpu::StoreOp::Store,
|
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 {
|
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||||
view: &self.depth_view,
|
view: depth_attach,
|
||||||
depth_ops: Some(wgpu::Operations {
|
depth_ops: Some(wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(1.0),
|
load: wgpu::LoadOp::Clear(1.0),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Store,
|
||||||
@@ -1406,6 +1488,16 @@ impl Renderer {
|
|||||||
self.bloom_config = config.clone();
|
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
|
/// 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
|
/// 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
|
/// 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)
|
(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
|
/// 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
|
/// `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`
|
/// 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)`.
|
/// Users enable HDR via `AppBuilder::with_hdr(ToneMapper::Aces)`.
|
||||||
pub use crate::core::ToneMapper;
|
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).
|
/// Re-export of the geometry data type (positions, normals, UVs, indices).
|
||||||
pub use crate::core::Geometry;
|
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).
|
/// 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.
|
/// A white texel is the multiplicative identity, so sampling it reproduces the pre-Step-10 look.
|
||||||
placeholder: Arc<Texture>,
|
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 {
|
impl PipelineCache {
|
||||||
@@ -210,7 +213,7 @@ impl PipelineCache {
|
|||||||
/// Inputs: device (owned Arc reference to wgpu Device), queue (used once to upload the white
|
/// 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().
|
/// 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.
|
/// 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 placeholder = Texture::white_placeholder(&device, &queue).arc();
|
||||||
let texture_bind_group_layout = create_texture_bind_group_layout(&device);
|
let texture_bind_group_layout = create_texture_bind_group_layout(&device);
|
||||||
Self {
|
Self {
|
||||||
@@ -221,6 +224,7 @@ impl PipelineCache {
|
|||||||
shader_paths: HashMap::new(),
|
shader_paths: HashMap::new(),
|
||||||
texture_bind_group_layout,
|
texture_bind_group_layout,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
sample_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,7 +309,7 @@ impl PipelineCache {
|
|||||||
.map(|s| s.as_str())
|
.map(|s| s.as_str())
|
||||||
.unwrap_or(shader_id);
|
.unwrap_or(shader_id);
|
||||||
let shader = self.load_shader(&self.device, path);
|
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
|
// Step 3: Cache the new pipeline behind Arc and return it
|
||||||
let pipeline_arc = Arc::new(pipeline);
|
let pipeline_arc = Arc::new(pipeline);
|
||||||
@@ -330,13 +334,11 @@ impl PipelineCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a RenderPipeline from a shader module, device, and surface texture format.
|
/// 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()`.
|
/// 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(
|
fn build_pipeline(
|
||||||
device: &wgpu::Device,
|
&self,
|
||||||
format: wgpu::TextureFormat,
|
format: wgpu::TextureFormat,
|
||||||
shader: &wgpu::ShaderModule,
|
shader: &wgpu::ShaderModule,
|
||||||
) -> wgpu::RenderPipeline {
|
) -> wgpu::RenderPipeline {
|
||||||
@@ -349,9 +351,9 @@ impl PipelineCache {
|
|||||||
// attached to EVERY pipeline (Step 3, decision ratified "a single layout for all"), even
|
// attached to EVERY pipeline (Step 3, decision ratified "a single layout for all"), even
|
||||||
// if a given shader does not read them.
|
// if a given shader does not read them.
|
||||||
// `immediate_size` stays 0 (no var<immediate> used).
|
// `immediate_size` stays 0 (no var<immediate> used).
|
||||||
let uniform_layouts = create_uniform_bind_group_layouts(device);
|
let uniform_layouts = create_uniform_bind_group_layouts(&self.device);
|
||||||
let texture_layout = create_texture_bind_group_layout(device);
|
let texture_layout = create_texture_bind_group_layout(&self.device);
|
||||||
let shadow_layout = create_shadow_map_bind_group_layout(device);
|
let shadow_layout = create_shadow_map_bind_group_layout(&self.device);
|
||||||
let layout_refs: Vec<Option<&wgpu::BindGroupLayout>> = vec![
|
let layout_refs: Vec<Option<&wgpu::BindGroupLayout>> = vec![
|
||||||
Some(&uniform_layouts[0]), // frame @0
|
Some(&uniform_layouts[0]), // frame @0
|
||||||
Some(&uniform_layouts[1]), // object @1
|
Some(&uniform_layouts[1]), // object @1
|
||||||
@@ -359,14 +361,14 @@ impl PipelineCache {
|
|||||||
Some(&shadow_layout), // shadow map @3
|
Some(&shadow_layout), // shadow map @3
|
||||||
];
|
];
|
||||||
let render_pipeline_layout =
|
let render_pipeline_layout =
|
||||||
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
label: Some("render_pipeline_layout"),
|
label: Some("render_pipeline_layout"),
|
||||||
bind_group_layouts: &layout_refs,
|
bind_group_layouts: &layout_refs,
|
||||||
immediate_size: 0, // no var<immediate> used
|
immediate_size: 0, // no var<immediate> used
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the full RenderPipeline — vertex state + fragment state + primitive configuration.
|
// 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"),
|
label: Some("Render Pipeline"),
|
||||||
layout: Some(&render_pipeline_layout),
|
layout: Some(&render_pipeline_layout),
|
||||||
vertex: wgpu::VertexState {
|
vertex: wgpu::VertexState {
|
||||||
@@ -399,7 +401,11 @@ impl PipelineCache {
|
|||||||
stencil: wgpu::StencilState::default(),
|
stencil: wgpu::StencilState::default(),
|
||||||
bias: wgpu::DepthBiasState::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 → replaced by multiview_mask (NonZeroU32) and cache fields in wgpu 30.
|
||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
cache: None,
|
cache: None,
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@
|
|||||||
// Core types
|
// Core types
|
||||||
pub use crate::core::geometry::{BBox, Geometry};
|
pub use crate::core::geometry::{BBox, Geometry};
|
||||||
pub use crate::core::transform::Transform;
|
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;
|
pub use crate::resources::Material;
|
||||||
|
|
||||||
// Camera
|
// Camera
|
||||||
|
|||||||
@@ -148,8 +148,9 @@ impl Scene {
|
|||||||
device: Arc<wgpu::Device>,
|
device: Arc<wgpu::Device>,
|
||||||
queue: wgpu::Queue,
|
queue: wgpu::Queue,
|
||||||
format: wgpu::TextureFormat,
|
format: wgpu::TextureFormat,
|
||||||
|
sample_count: u32,
|
||||||
) -> &mut Self {
|
) -> &mut Self {
|
||||||
let cache = PipelineCache::new(device.clone(), queue.clone());
|
let cache = PipelineCache::new(device.clone(), queue.clone(), sample_count);
|
||||||
self.gpu = Some(SceneGpu {
|
self.gpu = Some(SceneGpu {
|
||||||
device,
|
device,
|
||||||
format,
|
format,
|
||||||
|
|||||||
Reference in New Issue
Block a user