dof
This commit is contained in:
+353
-227
@@ -1,298 +1,424 @@
|
||||
# Étape 24 — MSAA 4× (Anti-aliasing multi-échantillons)
|
||||
# Étape 26 — Depth of Field (DoF)
|
||||
|
||||
**Statut** : ⬜ En cours
|
||||
**Roadmap** : 6.4
|
||||
**Prérequis** : Pipeline HDR (étape 20) + Bloom (étape 23)
|
||||
> **Objectif** : Flou de profondeur post-process — les objets hors de la distance
|
||||
> de focus sont flous, créant un effet cinématique. Opt-in via `with_dof()`,
|
||||
> zéro coût quand désactivé.
|
||||
|
||||
---
|
||||
|
||||
## Problème
|
||||
## Contexte & motivation
|
||||
|
||||
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.
|
||||
Le DoF (Depth of Field) simule le comportement d'un objectif photo : seuls les
|
||||
objets à la distance de focus sont nets, le reste est flou. Utilité :
|
||||
|
||||
## Solution
|
||||
- **Effet cinématique** — mettre en scène un objet/personnage
|
||||
- **Guidage du regard** — diriger l'attention du joueur
|
||||
- **Masquage subtil** — flou les zones non pertinentes (alternative douce au fog)
|
||||
|
||||
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 existant (avec HDR)
|
||||
|
||||
```text
|
||||
Main pass → HDR texture (Rgba16Float)
|
||||
↓
|
||||
Bloom (si actif) → composite
|
||||
↓
|
||||
Tone Mapping → surface
|
||||
```
|
||||
|
||||
### Pipeline avec DoF
|
||||
|
||||
```text
|
||||
Main pass → HDR texture + depth buffer
|
||||
↓
|
||||
Bloom (si actif) → bloom_composite
|
||||
↓
|
||||
DoF (si actif) :
|
||||
CoC pass: depth → coc_texture (R16F, radius en px)
|
||||
Blur pass: color + coc → dof_output (Rgba16F)
|
||||
↓
|
||||
Tone Mapping → surface
|
||||
```
|
||||
|
||||
Quand DoF est désactivé : TM lit directement la texture HDR/bloom (zéro coût).
|
||||
|
||||
---
|
||||
|
||||
## Pipeline actuel vs avec MSAA
|
||||
## Décisions
|
||||
|
||||
### Sans HDR, sans MSAA (actuel)
|
||||
```
|
||||
Scene → swapchain (Rgba8UnormSrgb) → present
|
||||
### D1 — 2 passes : CoC + Blur
|
||||
|
||||
| Pass | Entrées | Sortie | Format |
|
||||
|------|---------|--------|--------|
|
||||
| CoC | depth texture | coc_texture | `R16Float` (1 canal, radius en pixels) |
|
||||
| Blur | color + coc | dof_output | `Rgba16Float` (4 canaux, couleur floutée) |
|
||||
|
||||
Le CoC est calculé séparément pour éviter de recalculer la linearisation du
|
||||
depth dans chaque tap du blur.
|
||||
|
||||
### D2 — Formule du CoC
|
||||
|
||||
```wgsl
|
||||
// Linearize NDC depth [0,1] → world distance (perspective)
|
||||
fn linearize_depth(ndc_z: f32, near: f32, far: f32) -> f32 {
|
||||
return near * far / (far - ndc_z * (far - near));
|
||||
}
|
||||
|
||||
// CoC in pixels:
|
||||
let dist = linearize_depth(depth, near, far);
|
||||
let coc = max_blur * aperture * abs(dist - focus_distance) / max(focus_distance, 1e-4);
|
||||
coc = min(coc, max_blur);
|
||||
```
|
||||
|
||||
### 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).
|
||||
- `focus_distance` : distance (unités monde) où l'image est parfaitement nette
|
||||
- `aperture` : 0.0–1.0, contrôle l'intensité du flou (0 = pas de flou)
|
||||
- `max_blur` : radius maximum en pixels (clamp, évite le flou excessif)
|
||||
|
||||
### Avec HDR, sans MSAA (actuel)
|
||||
```
|
||||
Scene → HDR texture (Rgba16Float) → [Bloom] → TM → swapchain → present
|
||||
### D3 — Uniform struct (32 bytes)
|
||||
|
||||
```wgsl
|
||||
struct DoFUniform {
|
||||
focus_distance: f32, // world units
|
||||
aperture: f32, // 0.0-1.0
|
||||
max_blur: f32, // pixels
|
||||
near: f32, // camera near plane
|
||||
far: f32, // camera far plane
|
||||
inv_width: f32, // 1.0 / texture width
|
||||
inv_height: f32, // 1.0 / texture height
|
||||
_pad: f32,
|
||||
};
|
||||
```
|
||||
|
||||
### Avec HDR, avec MSAA (nouveau)
|
||||
```
|
||||
Scene → MSAA HDR (4×, Rgba16Float) ──resolve──→ HDR texture (Rgba16Float)
|
||||
+ MSAA depth (4×) → [Bloom] → TM → swapchain → present
|
||||
Un seul uniform partagé entre les 2 passes (CoC et Blur) — les valeurs sont
|
||||
identiques. Pas de ping-pong de buffers.
|
||||
|
||||
### D4 — Blur : disc 12-tap
|
||||
|
||||
Le blur utilise un pattern de 12 échantillons en disque (poisson-like),
|
||||
scallé par le CoC local :
|
||||
|
||||
```text
|
||||
· ·
|
||||
· ·
|
||||
· ·
|
||||
· · ·
|
||||
· ·
|
||||
· ·
|
||||
· ·
|
||||
```
|
||||
|
||||
**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.
|
||||
Chaque tap : `offset * coc_radius * texel_size`, pondéré uniformément (1/12).
|
||||
Le radius variable (par pixel) donne un bokeh naturel.
|
||||
|
||||
---
|
||||
> Pourquoi pas separable H+V comme bloom ? Le DoF produit un flou **circulaire**
|
||||
> (bokeh), pas un flou directionnel. Un disc blur single-pass est plus fidèle.
|
||||
> 12 taps × 1 texture = trivial GPU cost.
|
||||
|
||||
## Décisions de design
|
||||
### D5 — Textures
|
||||
|
||||
| # | 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 |
|
||||
| Texture | Format | Taille | Quand allouée |
|
||||
|---------|--------|--------|---------------|
|
||||
| `coc_texture` | `R16Float` | full-res (w×h) | DoF actif |
|
||||
| `dof_output` | `Rgba16Float` | full-res (w×h) | DoF actif |
|
||||
|
||||
---
|
||||
Quand DoF est désactivé : **aucune** texture DoF n'est allouée. Zéro coût.
|
||||
|
||||
## API utilisateur
|
||||
### D6 — API publique
|
||||
|
||||
```rust
|
||||
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`
|
||||
|
||||
```rust
|
||||
/// Configuration MSAA. `sample_count` doit être 2, 4, ou 8
|
||||
/// (valeur supportée par le GPU — vérifiée à l'init).
|
||||
/// Configuration du Depth of Field.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MsaaConfig {
|
||||
pub sample_count: u32,
|
||||
pub struct DoFConfig {
|
||||
/// Distance de focus (unités monde). L'image est nette à cette distance.
|
||||
pub focus_distance: f32,
|
||||
/// Intensité du flou (0.0 = aucun, 1.0 = max).
|
||||
pub aperture: f32,
|
||||
/// Radius maximum du flou en pixels.
|
||||
pub max_blur: f32,
|
||||
}
|
||||
|
||||
impl Default for MsaaCapable {
|
||||
fn default() -> Self {
|
||||
Self { sample_count: 4 }
|
||||
}
|
||||
impl DoFConfig {
|
||||
/// DoF standard : focus à `distance`, flou modéré.
|
||||
pub fn new(focus_distance: f32, aperture: f32, max_blur: f32) -> Self;
|
||||
|
||||
/// Preset cinématique : flou prononcé, max_blur=12px.
|
||||
pub fn cinematic(focus_distance: f32) -> Self;
|
||||
|
||||
/// Preset subtil : léger flou en arrière-plan, max_blur=6px.
|
||||
pub fn subtle(focus_distance: f32) -> Self;
|
||||
}
|
||||
```
|
||||
|
||||
### 24.2 — Champs `Renderer`
|
||||
|
||||
Ajouter à `Renderer` :
|
||||
**Builder** :
|
||||
```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>,
|
||||
AppBuilder::with_dof(DoFConfig::cinematic(5.0))
|
||||
```
|
||||
|
||||
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`
|
||||
**Runtime** :
|
||||
```rust
|
||||
app.renderer_mut().set_dof(Some(DoFConfig::new(3.0, 0.5, 8.0)));
|
||||
app.renderer_mut().set_dof(None); // désactiver
|
||||
```
|
||||
|
||||
Quand `sample_count == 1` :
|
||||
- Tous les `Option` sont `None`
|
||||
- Le render pass utilise la texture/view existante (comportement actuel)
|
||||
### D7 — Pipeline integration
|
||||
|
||||
### 24.3 — Allocation à l'init (`Renderer::new`)
|
||||
Dans `Renderer::render_scene` :
|
||||
|
||||
```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,
|
||||
// Après bloom (ou après main pass si pas de bloom) :
|
||||
if let Some(dof) = &self.dof_pipeline {
|
||||
// 1. CoC pass
|
||||
let mut coc_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &dof.coc_view,
|
||||
resolve_target: None,
|
||||
ops: ColorOps::ALL,
|
||||
format: TextureFormat::R16Float,
|
||||
..
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
..
|
||||
});
|
||||
// + depth MSAA
|
||||
}
|
||||
```
|
||||
coc_pass.set_pipeline(&dof.coc_pipeline);
|
||||
coc_pass.set_bind_group(0, &dof.coc_bind_group, &[]);
|
||||
coc_pass.draw(0, 3, 0, 1);
|
||||
drop(coc_pass);
|
||||
|
||||
### 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,
|
||||
// 2. Blur pass
|
||||
let mut blur_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &dof.output_view,
|
||||
resolve_target: None,
|
||||
ops: ColorOps::ALL,
|
||||
format: TextureFormat::Rgba16Float,
|
||||
..
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
..
|
||||
}),
|
||||
..
|
||||
});
|
||||
});
|
||||
blur_pass.set_pipeline(&dof.blur_pipeline);
|
||||
blur_pass.set_bind_group(0, &dof.blur_bind_group, &[]);
|
||||
blur_pass.draw(0, 3, 0, 1);
|
||||
drop(blur_pass);
|
||||
|
||||
// 3. TM lit dof_output au lieu de HDR
|
||||
// (re-pointer le bind group TM)
|
||||
}
|
||||
```
|
||||
|
||||
### 24.5 — Resize
|
||||
### D8 — Shaders
|
||||
|
||||
```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(...);
|
||||
#### `dof_coc.wgsl`
|
||||
|
||||
```wgsl
|
||||
// Vertex : fullscreen triangle (identique à TM/bloom)
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4<f32> {
|
||||
// même triangle que TM : (-1,-1), (3,-1), (-1,3)
|
||||
}
|
||||
|
||||
struct DoFUniform {
|
||||
focus_distance: f32,
|
||||
aperture: f32,
|
||||
max_blur: f32,
|
||||
near: f32,
|
||||
far: f32,
|
||||
inv_width: f32,
|
||||
inv_height: f32,
|
||||
_pad: f32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: DoFUniform;
|
||||
@group(0) @binding(1) var depth_tex: texture_depth_2d;
|
||||
@group(0) @binding(2) var sampler: sampler;
|
||||
|
||||
@fragment
|
||||
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) f32 {
|
||||
let uv = pos.xy * vec2(u.inv_width, u.inv_height);
|
||||
let ndc_z = textureSample(depth_tex, sampler, uv);
|
||||
|
||||
// Linearize: NDC [0,1] → world distance
|
||||
let dist = u.near * u.far / (u.far - ndc_z * (u.far - u.near));
|
||||
|
||||
// CoC in pixels
|
||||
var coc = u.max_blur * u.aperture * abs(dist - u.focus_distance)
|
||||
/ max(u.focus_distance, 1e-4);
|
||||
coc = min(coc, u.max_blur);
|
||||
|
||||
// Edge case: depth = 1.0 (far plane) → no blur
|
||||
if (ndc_z >= 0.9999) { coc = 0.0; }
|
||||
|
||||
return coc;
|
||||
}
|
||||
```
|
||||
|
||||
#### `dof_blur.wgsl`
|
||||
|
||||
```wgsl
|
||||
// Vertex : fullscreen triangle (id)
|
||||
|
||||
struct DoFUniform { /* idem */ };
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: DoFUniform;
|
||||
@group(0) @binding(1) var color_tex: texture_2d<f32>;
|
||||
@group(0) @binding(2) var coc_tex: texture_2d<f32>;
|
||||
@group(0) @binding(3) var sampler: sampler;
|
||||
|
||||
const TAPS: array<vec2<f32>, 12> = array<vec2<f32>, 12>(
|
||||
vec2(0.0, 0.0),
|
||||
vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(0.0, -1.0), vec2(-1.0, 0.0),
|
||||
vec2(0.707, 0.707), vec2(0.707, -0.707),
|
||||
vec2(-0.707, 0.707), vec2(-0.707, -0.707),
|
||||
vec2(0.383, 0.924), vec2(-0.383, 0.924), vec2(0.383, -0.924),
|
||||
);
|
||||
|
||||
@fragment
|
||||
fn fs_main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let uv = pos.xy * vec2(u.inv_width, u.inv_height);
|
||||
let coc = textureSample(coc_tex, sampler, uv).r;
|
||||
|
||||
if (coc < 0.5) {
|
||||
// Below 0.5px: no blur needed
|
||||
return textureSample(color_tex, sampler, uv);
|
||||
}
|
||||
|
||||
// HDR resize existant
|
||||
// Bloom resize existant
|
||||
|
||||
let radius = coc; // in pixels
|
||||
var sum = vec4<f32>(0.0);
|
||||
for (var i = 0u; i < 12u; i++) {
|
||||
let offset = TAPS[i] * radius * vec2(u.inv_width, u.inv_height);
|
||||
sum += textureSample(color_tex, sampler, uv + offset);
|
||||
}
|
||||
return sum / 12.0;
|
||||
}
|
||||
```
|
||||
|
||||
### 24.6 — Plomberie App/AppBuilder
|
||||
### D9 — Bind group layouts
|
||||
|
||||
**CoC pipeline** (3 bindings) :
|
||||
| Binding | Type | Description |
|
||||
|---------|------|-------------|
|
||||
| 0 | Uniform (32B) | DoF params |
|
||||
| 1 | Texture (depth) | Depth buffer de la scène |
|
||||
| 2 | Sampler | Linear, clamp |
|
||||
|
||||
**Blur pipeline** (4 bindings) :
|
||||
| Binding | Type | Description |
|
||||
|---------|------|-------------|
|
||||
| 0 | Uniform (32B) | DoF params |
|
||||
| 1 | Texture (color) | HDR/bloom color |
|
||||
| 2 | Texture (color) | CoC texture |
|
||||
| 3 | Sampler | Linear, clamp |
|
||||
|
||||
Chaque pipeline a **son propre** pipeline layout (règle wgpu 30).
|
||||
|
||||
### D10 — `DoFPipeline` struct
|
||||
|
||||
```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
|
||||
}
|
||||
pub(crate) struct DoFPipeline {
|
||||
// Textures
|
||||
coc_texture: Texture,
|
||||
coc_view: TextureView,
|
||||
output_texture: Texture,
|
||||
output_view: TextureView,
|
||||
|
||||
// App
|
||||
pub fn msaa_enabled(&self) -> bool { ... }
|
||||
pub fn set_msaa(&mut self, sample_count: u32) { ... } // nécessite resize
|
||||
// Sampler (shared between both passes)
|
||||
sampler: Sampler,
|
||||
|
||||
// Pipelines
|
||||
coc_pipeline: RenderPipeline,
|
||||
blur_pipeline: RenderPipeline,
|
||||
|
||||
// Uniform buffer (shared: same values for both passes)
|
||||
uniform_buffer: Buffer,
|
||||
|
||||
// Bind groups
|
||||
coc_bind_group: BindGroup,
|
||||
blur_bind_group: BindGroup,
|
||||
}
|
||||
```
|
||||
|
||||
Plomberie : `AppBuilder → App → AppRunner → Renderer::new(msaa_config)`
|
||||
Méthodes :
|
||||
- `DoFPipeline::new(device, width, height, depth_view, color_view)` → alloue tout
|
||||
- `DoFPipeline::update_uniform(&mut self, queue, config, near, far)` → écrit le buffer
|
||||
- `DoFPipeline::output_view(&self) -> &TextureView` → pour re-pointer le TM
|
||||
- `DoFPipeline::output_texture(&self) -> &Texture` → pour le bind group TM
|
||||
- `DoFPipeline::resize(...)` → recrée textures + bind groups
|
||||
|
||||
### 24.7 — Re-exports
|
||||
### D11 — Resize
|
||||
|
||||
`lib.rs` + `prelude.rs` : `pub use crate::core::MsaaConfig;`
|
||||
Dans `resize_depth` (ou équivalent) :
|
||||
```rust
|
||||
if let Some(dof) = &mut self.dof_pipeline {
|
||||
dof.resize(device, queue, new_w, new_h, &new_depth_view, &new_color_view);
|
||||
}
|
||||
```
|
||||
|
||||
### 24.8 — Exemple `msaa.rs`
|
||||
### D12 — Ordre des post-process
|
||||
|
||||
Scène simple (cube + sphere + ground) avec/without MSAA commutable à la runtime
|
||||
(clavier `M`). Camera orbitale pour voir les bords de près.
|
||||
```text
|
||||
Main pass → HDR
|
||||
→ Bloom (si actif) → bloom_composite
|
||||
→ DoF (si actif) → dof_output
|
||||
→ TM → surface
|
||||
```
|
||||
|
||||
Contrôles :
|
||||
- `M` — toggle MSAA (nécessite un resize/recréation des textures)
|
||||
- `R`/`1`/`2`/`3` — presets caméra
|
||||
- Drag/wheel — orbit/zoom
|
||||
DoF **après** bloom : le glow du bloom est aussi flouté par le DoF → plus naturel.
|
||||
|
||||
### 24.9 — Documentation
|
||||
### D13 — Compatibilité
|
||||
|
||||
- `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 → ✅
|
||||
| Avec | OK ? | Note |
|
||||
|------|------|------|
|
||||
| HDR | ✅ **requis** | DoF opère sur la texture HDR |
|
||||
| Bloom | ✅ | DoF après bloom (D12) |
|
||||
| MSAA | ✅ | Après resolve, DoF voit la texture single-sample |
|
||||
| Fog | ✅ | Fog est dans le main pass, DoF floute le résultat |
|
||||
| Culling | ✅ | Indépendant |
|
||||
|
||||
### D14 — `with_dof` sans `with_hdr` = no-op
|
||||
|
||||
Comme bloom, DoF nécessite HDR. `with_dof()` sans `with_hdr()` → warning + no-op.
|
||||
|
||||
---
|
||||
|
||||
## Coût GPU
|
||||
## Fichiers modifiés / créés
|
||||
|
||||
| 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× |
|
||||
| Fichier | Action |
|
||||
|---------|--------|
|
||||
| `lib/src/core/dof.rs` | **NEW** — `DoFConfig` + `DoFPipeline` |
|
||||
| `lib/src/core/mod.rs` | + `pub mod dof;` + re-exports |
|
||||
| `lib/src/lib.rs` | + `pub use DoFConfig` |
|
||||
| `lib/src/prelude.rs` | + `DoFConfig` |
|
||||
| `lib/src/core/renderer.rs` | + `dof` field, `set_dof()`, render pass, resize |
|
||||
| `lib/src/app.rs` | + `with_dof()`, plumbage App/Builder/Runner |
|
||||
| `lib/src/shaders/dof_coc.wgsl` | **NEW** |
|
||||
| `lib/src/shaders/dof_blur.wgsl` | **NEW** |
|
||||
| `lib/tests/wgsl_validate.rs` | + 2 shaders DoF |
|
||||
| `lib/examples/dof.rs` | **NEW** |
|
||||
| `lib/examples/README.md` | + section DoF |
|
||||
| `docs/user/dof.md` | **NEW** |
|
||||
| `docs/user/README.md` | + ligne DoF |
|
||||
| `docs/ROADMAP.md` | 6.17 → ✅ |
|
||||
|
||||
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
|
||||
## Plan d'implémentation
|
||||
|
||||
- **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).
|
||||
| # | Tâche | Dépend |
|
||||
|---|-------|--------|
|
||||
| 1 | `core/dof.rs` : `DoFConfig` + tests | — |
|
||||
| 2 | `core/mod.rs` + `lib.rs` + `prelude.rs` : exports | 1 |
|
||||
| 3 | `shaders/dof_coc.wgsl` + `shaders/dof_blur.wgsl` | — |
|
||||
| 4 | `tests/wgsl_validate.rs` : ajouter les 2 shaders | 3 |
|
||||
| 5 | `core/dof.rs` : `DoFPipeline` (textures, pipelines, BGL, bind groups) | 3 |
|
||||
| 6 | `core/renderer.rs` : fields + `new` + `set_dof` + `render_scene` + `resize` | 5 |
|
||||
| 7 | `app.rs` : `with_dof()` + plumbage | 6 |
|
||||
| 8 | `examples/dof.rs` | 6 |
|
||||
| 9 | Docs : examples README + user docs + ROADMAP | 8 |
|
||||
| 10 | Vérification : `cargo check` + tests + examples | all |
|
||||
|
||||
## Tests
|
||||
---
|
||||
|
||||
- [ ] `MsaaConfig` Default = 4
|
||||
- [ ] Validation `sample_count` (rejette 3, 5, 16)
|
||||
- [ ] Build avec `with_msaa(4)` + HDR + Bloom → compile
|
||||
- [ ] Exemple `msaa.rs` compile
|
||||
- [ ] WGSL validation inchangé (pas de nouveau shader)
|
||||
- [ ] Test unit : `Renderer::new` avec `msaa_config=Some(4)` ne panique pas
|
||||
(nécessite un device mock — peut être un test intégration ignoré)
|
||||
## Estimation
|
||||
|
||||
## Critères d'acceptation
|
||||
|
||||
1. `cargo check` 0 errors, 0 warnings
|
||||
2. `cargo test` tous verts
|
||||
3. `cargo run -p wsg-lib --example msaa` → bords lisses avec M, escaliers sans M
|
||||
4. `cargo run -p wsg-lib --example demo` → inchangé (pas de `with_msaa`)
|
||||
5. Combinaison HDR + Bloom + MSAA fonctionne (demo avec `.with_msaa(4)`)
|
||||
- **Effort** : Moyen (~200 lignes Rust + ~80 lignes WGSL)
|
||||
- **Risque** : Bas (pattern identique à bloom, 2 passes simples)
|
||||
- **Gain visuel** : ⭐⭐⭐ (effet cinématique immédiat)
|
||||
|
||||
+2
-2
@@ -70,11 +70,11 @@ Ce document est la **vue d'ensemble de progression**. Chaque étape a son DRAFT
|
||||
| 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.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.13 | **Fog** (exponential / exponential² / linear, paramètre par scène) | ⭐⭐⭐ | 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 | ⬜ |
|
||||
| 6.17 | **Depth of field** (post-process CoC : circle-of-confusion + bokeh blur) | ⭐⭐⭐ | Moyen | ✅ |
|
||||
|
||||
### Cibles techniques (refactoring)
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ GPU graphics background is required.
|
||||
| [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` |
|
||||
| [MSAA (anti-aliasing)](msaa.md) | Multi-sample edge smoothing, opt-in via `with_msaa(4)` |
|
||||
| [Fog (distance)](fog.md) | Distance fog (3 modes), masks world edges, opt-in via `with_fog()` |
|
||||
| [DoF (depth of field)](dof.md) | Cinematic bokeh blur, focus plane, opt-in via `with_dof()` |
|
||||
| [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 |
|
||||
| [Examples](examples.md) | The 7 repo examples, the advanced `manual` workflow, adding your own example |
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Brouillard de distance (Fog)
|
||||
|
||||
## Principe
|
||||
|
||||
Le brouillard de distance fond les objets vers une couleur prédéfinie en fonction
|
||||
de leur distance à la caméra. C'est l'outil standard pour :
|
||||
|
||||
- **Masquer le bord du monde rendu** — illusion d'un monde infini (Skyrim, GTA, Minecraft)
|
||||
- **Donner de la profondeur** — effet atmosphérique naturel
|
||||
- **Camoufler les transitions** — chargement de tuiles, LOD pops
|
||||
|
||||
## Activation
|
||||
|
||||
```rust
|
||||
use wsg_lib::prelude::*;
|
||||
|
||||
let app = AppBuilder::new()
|
||||
.with_fog(FogConfig::exponential2([0.7, 0.75, 0.85], 0.06))
|
||||
.build()
|
||||
.await?;
|
||||
```
|
||||
|
||||
Sans `.with_fog()`, le brouillard est désactivé — **zéro coût GPU** (la branche
|
||||
shader est jamais prise).
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Formule | Usage |
|
||||
|------|---------|-------|
|
||||
| `Linear` | `saturate((far - d) / (far - near))` | Cutoff net entre deux distances |
|
||||
| `Exponential` | `exp(-density × d)` | Brouillard naturel (forêt, lac) |
|
||||
| `Exponential2` | `exp(-density² × d²)` | Départ progressif, cutoff net — **idéal pour masquer** |
|
||||
|
||||
### Constructeurs
|
||||
|
||||
```rust
|
||||
// Linéaire : fondu entre near et far
|
||||
FogConfig::linear([0.7, 0.8, 0.9], 5.0, 50.0)
|
||||
|
||||
// Exponentiel : fondu naturel
|
||||
FogConfig::exponential([0.6, 0.7, 0.8], 0.03)
|
||||
|
||||
// Exponentiel² : masquage de bord de monde
|
||||
FogConfig::exponential2([0.7, 0.75, 0.85], 0.08)
|
||||
```
|
||||
|
||||
## Paramètres
|
||||
|
||||
| Champ | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `mode` | `FogMode` | Linéaire / Exponentiel / Exponential2 |
|
||||
| `color` | `[f32; 3]` | Couleur du brouillard (RGB, espace linéaire) |
|
||||
| `near` | `f32` | Distance début (mode linéaire uniquement) |
|
||||
| `far` | `f32` | Distance fin, brouillard complet (mode linéaire) |
|
||||
| `density` | `f32` | Densité (modes exp / exp²). Typique : 0.01–0.3 |
|
||||
|
||||
### Choisir la couleur
|
||||
|
||||
La couleur du brouillard **doit correspondre à la couleur du ciel/clear color**
|
||||
pour un effet "monde infini" seamless. Avec HDR + ACES, utiliser des valeurs
|
||||
linéaires cohérentes avec le tone mapping.
|
||||
|
||||
### Choisir la densité (exp²)
|
||||
|
||||
Pour masquer le bord du monde à une distance `D` :
|
||||
|
||||
```
|
||||
density ≈ 2.0 / D
|
||||
```
|
||||
|
||||
Exemples :
|
||||
- Monde visible jusqu'à 25 unités → `density = 0.08`
|
||||
- Monde visible jusqu'à 50 unités → `density = 0.04`
|
||||
- Monde visible jusqu'à 100 unités → `density = 0.02`
|
||||
|
||||
## Changement à l'exécution
|
||||
|
||||
```rust
|
||||
// Dans update() :
|
||||
if key_pressed(KeyCode::Digit1) {
|
||||
app.renderer_mut().set_fog(Some(FogConfig::linear([0.7, 0.8, 0.9], 5.0, 30.0)));
|
||||
}
|
||||
if key_pressed(KeyCode::Digit4) {
|
||||
app.renderer_mut().set_fog(None); // désactiver
|
||||
}
|
||||
```
|
||||
|
||||
Le changement prend effet au frame suivant.
|
||||
|
||||
## Pipeline
|
||||
|
||||
```text
|
||||
Main pass (shader fragment)
|
||||
↓
|
||||
Lighting → final_rgb
|
||||
↓
|
||||
FOG: mix(final_rgb, fog_color, 1 - fog_factor) ← ici
|
||||
↓
|
||||
→ HDR texture / swapchain
|
||||
↓
|
||||
(Bloom) → Tone Mapping → surface
|
||||
```
|
||||
|
||||
Le brouillard s'applique **avant** le tone mapping : les valeurs HDR restent
|
||||
non clampées, et le TM applique la courbe ACES/Reinhard au résultat déjà
|
||||
brouillé. Résultat : le brouillard est perceptuellement cohérent.
|
||||
|
||||
## Compatibilité
|
||||
|
||||
| Avec | OK ? | Note |
|
||||
|------|------|------|
|
||||
| HDR + TM | ✅ | Fog avant TM (recommandé) |
|
||||
| Bloom | ✅ | Le bloom extrait les zones brillantes du résultat post-fog |
|
||||
| MSAA | ✅ | Indépendant (rasterizer vs fragment shader) |
|
||||
| Culling GPU | ✅ | Indépendant (culling décide quoi dessiner, fog décide la couleur) |
|
||||
| Shadows | ✅ | L'ombre est calculée avant le fog |
|
||||
|
||||
## Limitations (v1)
|
||||
|
||||
- **Scene-level uniquement** : un seul brouillard pour toute la scène.
|
||||
Un brouillard par matériau nécessiterait un paramètre additionnel dans le
|
||||
bind group par objet.
|
||||
- **Distance euclidienne** : pas de brouillard volumétrique ni directionnel.
|
||||
- **Couleur fixe** : pas de gradient de couleur avec la distance.
|
||||
|
||||
## Exemple
|
||||
|
||||
Voir `examples/fog.rs` : 15 cubes en rangée + 5 sphères sur un plan 80×80,
|
||||
avec commutation runtime entre les 3 modes.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example fog --features "all-prims"
|
||||
```
|
||||
Reference in New Issue
Block a user