bvle-voxels/CLAUDE.md
Samuel Bouchet dac63e3be5 Phase 6.2: toping BLAS shadows + adaptive TMin + perf optimization
- Re-enable toping BLAS in TLAS (3 instances: blocky + smooth + topings)
  with PREFER_FAST_TRACE for optimized BVH traversal (23M tris)
- Separate shadow/AO ray origins: shadow uses worldPos directly (zero bias),
  AO keeps normal bias (0.15) for hemisphere self-avoidance
- Adaptive TMin solves self-hit vs gap dilemma:
  ground (N.y≈1) → TMin=0.002 for tight blade shadows,
  blade surfaces (N.y≈0) → TMin=0.10 to skip own geometry
- Shadow rays 4→3 with tight cone (0.012 rad), AO rays 8→4
  (7 total rays/pixel, temporal accumulation compensates)
- Remove screen-space contact shadows (doesn't work for thin geometry)
2026-03-30 13:58:57 +02:00

663 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# BVLE Voxels - Prototype de Moteur Voxel Hybride
## Vue d'ensemble
Prototype de moteur voxel basé sur **Wicked Engine** (MIT, C++17, DX12/Vulkan) pour valider les performances de rendu sur GPU moderne (AMD RDNA 2+ / Nvidia RTX 3060+). Le document de spécification complet est dans `voxel_engine_spec.md` à la racine du projet.
Cible : 60+ fps en 1440p, monde de 512x512x256 voxels visibles.
## Architecture
```
bvle-voxels/
├── CMakeLists.txt # Build CMake racine
├── engine/ # Wicked Engine (clone --depth 1, branche main)
│ └── WickedEngine/shaders/voxel/ # Nos shaders copiés ici pour compilation DXC
├── src/
│ ├── voxel/ # Bibliothèque VoxelEngine (static lib)
│ │ ├── VoxelTypes.h # Types fondamentaux (VoxelData, PackedQuad, MaterialDesc, ChunkPos)
│ │ ├── VoxelWorld.h/.cpp # Monde voxel (hashmap de chunks, génération procédurale)
│ │ ├── VoxelMesher.h/.cpp # Binary Greedy Mesher CPU + SmoothMesher (Naive Surface Nets)
│ │ ├── VoxelRenderer.h/.cpp# Renderer + VoxelRenderPath (sous-classe RenderPath3D)
│ │ └── TopingSystem.h/.cpp # Système de topings (biseaux décoratifs sur faces +Y)
│ └── app/
│ └── main.cpp # Point d'entrée Win32 + crash handler SEH
├── shaders/ # Sources HLSL des shaders voxel (copiés dans engine/ au build)
│ ├── voxelCommon.hlsli # Root signature et CB partagés (inclus par tous les shaders)
│ ├── voxelVS.hlsl # Vertex shader (vertex pulling, triple-mode: CPU/MDI/GPU mesh)
│ ├── voxelPS.hlsl # Pixel shader (triplanar + lighting)
│ ├── voxelCullCS.hlsl # Compute shader frustum+backface cull (Phase 2.3)
│ ├── voxelMeshCS.hlsl # Compute shader GPU mesher 1×1 (Phase 2.4-2.5)
│ ├── voxelTopingVS.hlsl # Vertex shader topings (instanced vertex pulling, t4/t5)
│ ├── voxelTopingPS.hlsl # Pixel shader topings (triplanar + directional lighting)
│ ├── voxelSmoothVS.hlsl # Vertex shader smooth Surface Nets (vertex pulling, t6)
│ ├── voxelSmoothPS.hlsl # Pixel shader smooth (triplanar + material blending)
│ ├── voxelBLASExtractCS.hlsl # Compute shader BLAS position extraction (Phase 6.1)
│ ├── voxelShadowCS.hlsl # Compute shader RT shadows + raw AO (inline ray queries, Phase 6.2+6.3)
│ ├── voxelAOBlurCS.hlsl # Compute shader bilateral AO blur (separable H/V, Phase 6.3)
│ └── voxelAOApplyCS.hlsl # Compute shader AO apply + tone mapping + saturation (Phase 6.3 + 7)
└── CLAUDE.md
```
## Build
### Prérequis
- CMake 3.19+ (`winget install Kitware.CMake`)
- Visual Studio 2022 Build Tools (`winget install Microsoft.VisualStudio.2022.BuildTools`)
- Windows SDK 10.0.26100+ (`winget install Microsoft.WindowsSDK.10.0.26100`)
### Commandes
```bash
# Configurer (depuis la racine du projet)
cmake -B build -G "Visual Studio 17 2022" -A x64 -DCMAKE_SYSTEM_VERSION=10.0.26100.0
# Compiler
cmake --build build --config Release --target BVLEVoxels --parallel
# Exécutable produit dans build/Release/BVLEVoxels.exe
```
Le SDK 10.0.26100 est requis car les headers DX12 (`d3dx12_check_feature_support.h`) fournis par Wicked Engine ne sont pas compatibles avec le SDK 22621.
### Post-build automatique (CMakeLists.txt)
Le build copie automatiquement :
1. `dxcompiler.dll` → à côté de l'exe (requis pour la compilation runtime des shaders)
2. `shaders/*.hlsl``engine/WickedEngine/shaders/voxel/` (pour que `LoadShader` les trouve via `SHADERSOURCEPATH`)
3. `engine/Content/` → à côté de l'exe (assets Wicked Engine)
## Intégration Wicked Engine
### Backend graphique
Wicked Engine utilise **DX12 par défaut sur Windows**, Vulkan sur Linux. Les shaders sont écrits en **HLSL** et compilés via DXC vers :
- `shaders/hlsl6/*.cso` pour DX12
- `shaders/spirv/*.spv` pour Vulkan
Pour forcer Vulkan sur Windows, passer `"vulkan"` en argument de ligne de commande.
### Point d'entrée et architecture de rendu
`VoxelRenderPath` hérite de `wi::RenderPath3D`. **IMPORTANT** : le rendu voxel utilise ses propres render targets (`voxelRT_`, `voxelDepth_`) et est exécuté dans `Render()` sur un **command list dédié** (`device->BeginCommandList()`). Le résultat est ensuite composité dans `Compose()` via `wi::image::Draw()`.
**NE JAMAIS créer un render pass dans `Compose()`** : cette méthode est appelée à l'intérieur du render pass du swapchain. Imbriquer des render passes est interdit en D3D12 (cause `DXGI_ERROR_INVALID_CALL → device removed`).
Architecture correcte :
```
Render() → RenderPath3D::Render() // Wicked rend sa scène
→ device->BeginCommandList() // Nouveau cmd list
→ renderer.render(cmd, ...) // Notre render pass (clear + draw voxels → voxelRT_)
Compose() → RenderPath3D::Compose() // Wicked affiche son résultat
→ wi::image::Draw(voxelRT_) // On overlay nos voxels par-dessus
```
La caméra est gérée manuellement dans `Update()` en écrivant directement `camera->Eye`, `camera->At` (direction LookTo), `camera->Up`.
### APIs Wicked utilisées
| Besoin | API Wicked |
|--------|-----------|
| Clavier WASD | `wi::input::Down(CHARACTER_RANGE_START + offset)` (pas de `KEYBOARD_BUTTON_W`) |
| Souris delta | `wi::input::GetMouseState().delta_position` |
| Cacher curseur | `wi::input::HidePointer(bool)` |
| Shader loading | `wi::renderer::LoadShader()` - compile auto les .hlsl en .cso si absent |
| PSO states | `wi::renderer::GetRasterizerState()` etc. retournent des pointeurs (pas besoin de `&`) |
| Render pass | `RenderPassImage::RenderTarget(texture, loadOp, storeOp, layoutBefore, layoutAfter, subresource=-1)` |
| Font overlay | `wi::font::Params` est un struct - setter les membres un par un |
| Camera | `CameraComponent::At` est une **direction** (utilisé avec `XMMatrixLookToLH`), pas un point cible |
| Buffer create | `device->CreateBuffer(desc, raw_data_ptr, buffer)` — PAS de `SubresourceData` pour les buffers ! |
| Texture create | `device->CreateTexture(desc, subresourceData_ptr, texture)` — utilise `SubresourceData*` (différent de CreateBuffer) |
| Buffer update | `device->UpdateBuffer(buffer, data, cmd, size, offset)` |
| Push constants | `device->PushConstants(data, size, cmd)` — mappés à `register(b999)`, taille fixe 48 bytes (12 × uint32) |
| Command list | `device->BeginCommandList()` — nouveau cmd list pour render passes séparés |
| Render pass | NE JAMAIS imbriquer ! Un seul render pass actif par command list |
| Debug DX12 | Passer `"debugdevice"` en argument pour activer la couche de debug D3D12 |
| Logging | `wi::backlog::post(message, logLevel)` — préférer au logging fichier |
### Shaders custom — PIÈGES IMPORTANTS
Les shaders custom doivent respecter le **binding model de Wicked Engine** :
1. **Root signature obligatoire** : chaque shader DOIT avoir une root signature DX12 intégrée, soit via `#include "globals.hlsli"` (auto), soit via `[RootSignature(MACRO)]` sur le entry point.
2. **Root signature Wicked** (HLSL 6.6+) :
- `b999` → push constants (12 × uint32 = 48 bytes max)
- `b0, b1, b2` → CBV root descriptors
- `t0-t15, u0-u15` → dans une descriptor table partagée
- `s0-s7` → samplers dynamiques
- `s100-s109` → static samplers (linear, point, aniso, etc.)
3. **Chemins des shaders** :
- `SHADERPATH` = `<exe_dir>/shaders/hlsl6/` — où les `.cso` compilés sont stockés
- `SHADERSOURCEPATH` = `../../engine/WickedEngine/shaders/` — où les `.hlsl` sources sont cherchés
- Les shaders custom doivent être copiés dans `SHADERSOURCEPATH` (sous-dossier `voxel/`)
- `LoadShader(stage, shader, "voxel/voxelVS.cso")` → compile `SHADERSOURCEPATH/voxel/voxelVS.hlsl` si `.cso` absent
4. **`dxcompiler.dll` doit être à côté de l'exe** sinon la compilation runtime échoue silencieusement.
5. **CreateBuffer prend `void*`**, pas `SubresourceData*`. L'API texture (`CreateTexture`) prend bien `SubresourceData*`.
6. **Winding des triangles — PIÈGE MAJEUR** :
Wicked Engine utilise `front_counter_clockwise = true` + `CullMode::BACK` (state `RSTYPE_FRONT`). Malgré cela, les quads voxel doivent utiliser un winding **CW** (clockwise) comme défaut, pas CCW. Confirmé empiriquement via `SV_IsFrontFace` : avec des corners CCW standard, DX12 voit tous les triangles comme **back-facing**.
La règle pour nos tangent axes U/V :
- `cross(U,V) = N` (faces +X, -Y, +Z) → corners **CW** pour être front-facing
- `cross(U,V) ≠ N` (faces -X, +Y, -Z) → corners **CCW** pour être front-facing
```
CW corners: (0,0)(0,1)(1,0), (1,0)(0,1)(1,1) ← défaut
CCW corners: (0,0)(1,0)(0,1), (0,1)(1,0)(1,1) ← faces 1,2,5
```
7. **DrawInstancedIndirectCount — PIÈGE MAJEUR** :
Les command signatures de Wicked Engine pour `*IndirectCount` incluent un **push constant** (1 × uint32, écrit dans `b999[0]`) AVANT chaque `D3D12_DRAW_ARGUMENTS`. Le stride par draw entry est donc **20 bytes**, pas 16.
Layout mémoire du buffer d'args indirect :
```
[uint32 pushConstant][uint32 vertexCount][uint32 instanceCount][uint32 startVertex][uint32 startInstance]
4 bytes 16 bytes (D3D12_DRAW_ARGUMENTS)
= 20 bytes par draw entry
```
Le push constant est écrit automatiquement par `ExecuteIndirect` dans `b999[0]` (premier champ de la struct push constants, soit `chunkIndex` dans notre cas). Les autres champs de b999 (quadOffset, flags...) restent tels que définis par le `PushConstants()` appelé avant `DrawInstancedIndirectCount`.
**En mode MDI, le push constant est utilisé pour packer `chunkIndex | (faceIndex << 16)`**. Le VS décode ces deux valeurs et reconstruit le quadOffset depuis le `GPUChunkInfo` :
```hlsl
chunkIndex = push.chunkIndex & 0xFFFF;
faceIdx = push.chunkIndex >> 16;
quadIndex = chunkInfo[chunkIndex].quadOffset + faceOffset[faceIdx] + (vertexID / 6);
```
**Source** : `wiGraphicsDevice_DX12.cpp` lignes 3930-3939 — la command signature est créée par PSO avec `D3D12_INDIRECT_ARGUMENT_TYPE_CONSTANT` + `D3D12_INDIRECT_ARGUMENT_TYPE_DRAW`.
8. **SV_VertexID et startVertexLocation — PIÈGE MAJEUR** :
Avec `ExecuteIndirect` (DrawInstancedIndirectCount), `SV_VertexID` **n'inclut PAS de manière fiable** `startVertexLocation` de `D3D12_DRAW_ARGUMENTS`. Observé sur AMD RDNA 4 (RX 9070 XT) : SV_VertexID commence toujours à 0 pour chaque draw, ignorant startVertexLocation.
**Solution** : toujours mettre `startVertexLocation = 0` dans les indirect args, et passer l'offset des quads par un autre canal (push constant + GPUChunkInfo lookup). Ne JAMAIS compter sur `startVertexLocation` pour encoder un offset dans le mega-buffer.
9. **Barriers sur buffers indirect — NON NÉCESSAIRES en pratique** :
Les buffers `Usage::DEFAULT` démarrent en COMMON et décayent vers COMMON après chaque exécution de command list. La promotion implicite COMMON → COPY_DST (via UpdateBuffer) et COMMON → INDIRECT_ARGUMENT (via DrawInstancedIndirectCount) fonctionne sans barriers explicites. C'est le même pattern que les SRV buffers (megaQuadBuffer_, chunkInfoBuffer_) qui passent de COPY_DST à SRV usage sans barrier en Phase 2.1.
**⚠️ Pour la Phase 2.3 (compute cull)**, des barriers explicites SONT nécessaires :
- `drawCountBuffer_` : COPY_DST → UAV (après UpdateBuffer zero) puis UAV → INDIRECT_ARGUMENT (après dispatch)
- `indirectArgsBuffer_` : UNDEFINED → UAV (COMMON après decay, `ResourceState::UNDEFINED = 0` = COMMON en Wicked) puis UAV → INDIRECT_ARGUMENT
- Wicked Engine appelle `DiscardResource()` quand `state_before == UNDEFINED`, ce qui est OK (le compute écrase les données)
10. **PushConstants après BindComputeShader — PIÈGE MAJEUR** :
`PushConstants()` dispatche vers `SetGraphicsRoot32BitConstants` ou `SetComputeRoot32BitConstants` selon l'état actif :
- Si `active_pso != nullptr` → **GRAPHICS** push constants
- Sinon si `active_cs != nullptr` → **COMPUTE** push constants
Après `BindComputeShader` + `Dispatch`, `active_cs` reste actif. Appeler `PushConstants` à ce moment écrit dans les push constants **compute**, pas **graphics**. Le vertex shader ne voit jamais la valeur !
**Règle** : toujours appeler `PushConstants` **APRÈS** `BindPipelineState` (qui set `active_pso`) pour cibler les push constants graphics. L'ordre correct :
```cpp
BindPipelineState(&pso_); // ← active_pso = &pso_
PushConstants(&data, ...); // ← SetGraphicsRoot32BitConstants ✓
Draw*(...);
```
### Diagnostics et debugging
**Crash handler SEH** (`main.cpp`) : `SetUnhandledExceptionFilter` écrit :
- `bvle_crash.log` : stack trace avec symboles + adresses
- `bvle_crash.dmp` : minidump analysable avec Visual Studio
- Nécessite `dbghelp.lib` et build avec symbols (`RelWithDebInfo` ou `Debug`)
**D3D12 Debug Layer** : lancer avec `BVLEVoxels.exe debugdevice` pour activer. Active aussi DRED (Device Removed Extended Data) pour diagnostiquer les GPU hangs.
**Erreurs GPU courantes** :
- `DXGI_ERROR_INVALID_CALL` → render pass imbriqué ou resource state invalide
- `DXGI_ERROR_DEVICE_HUNG` → shader en boucle infinie ou accès mémoire hors limites
- Dialog bloquant avec `messageBox` → vient de `wi::helper::messageBox()`, ne pas confondre avec un crash
**⚠️ Détection de crash GPU depuis CLI (Claude Code)** : les crashs GPU (`DXGI_ERROR_INVALID_CALL`, device removed) affichent une **modale Windows bloquante** via `wi::helper::messageBox()`. `timeout` tue le process sans détecter le crash. Pour détecter correctement :
1. **NE PAS utiliser `timeout`** pour tester — demander à l'utilisateur de lancer manuellement
2. Vérifier `bvle_backlog.txt` après exécution (contient les erreurs DX12)
3. Vérifier `bvle_crash.log` et `bvle_crash.dmp` pour les crashs SEH
4. Lancer avec `debugdevice` pour obtenir les messages de validation D3D12 détaillés dans le backlog
5. Un exit code non-zéro n'est PAS fiable : `timeout` renvoie 124, la modale attend indéfiniment
**Backlog Wicked** : `wi::backlog::SetLogFile("bvle_backlog.txt")` redirige les logs vers un fichier. Touche `~` (tilde) pour toggler la console à l'écran.
### Gestion des resource states DX12 (buffers)
**Wicked Engine ne fait AUCUN tracking automatique d'état pour les buffers.** Les `GPUBarrier::Buffer(buf, before, after)` sont passées directement à D3D12 sans validation. **Le `state_before` DOIT correspondre à l'état DX12 réel, sinon → DXGI_ERROR_INVALID_CALL.**
**Pièges critiques :**
- `UpdateBuffer()` → appelle `CopyBufferRegion` sans aucune barrier. Le buffer **DOIT** être en COPY_DST (ou COMMON pour promotion implicite sur frame 1).
- Après `DrawInstancedIndirectCount`, les buffers indirect restent en **INDIRECT_ARGUMENT**. Appeler `UpdateBuffer` dessus au frame suivant → crash car pas de transition INDIRECT_ARGUMENT → COPY_DST.
- Les buffers créés avec `Usage::DEFAULT` démarrent en état **COMMON** (D3D12). COMMON supporte la promotion implicite vers COPY_DST, SRV, etc. mais **PAS vers UAV**.
- Solution recommandée : **tracker l'état manuellement** avec un `mutable ResourceState` et faire des barriers explicites entre chaque usage.
**Mode debug face-color** : lancer avec `BVLEVoxels.exe debug` pour activer. Génère un monde de test (blocs isolés) et colore chaque face selon sa direction :
- Bright Red / Dark Red = +X / -X
- Bright Green / Dark Green = +Y / -Y
- Bright Blue / Dark Blue = +Z / -Z
## Détails d'implémentation
### VoxelData (16 bits)
```
[15:8] material ID (256 matériaux)
[7:4] flags (smooth, transparent, emissive, custom)
[3:0] metadata (orientation, variant)
```
### PackedQuad (64 bits = 8 octets par quad)
```
[5:0] position X (0-63)
[11:6] position Y (0-63)
[17:12] position Z (0-63)
[23:18] width (1-32)
[29:24] height (1-32)
[32:30] face (0-5 : +X,-X,+Y,-Y,+Z,-Z)
[40:33] material ID
[48:41] blendMatID (8 bits, matériau voisin pour height-based blending)
[59:49] chunkIndex (11 bits, utilisé par GPU mesh path pour lookup GPUChunkInfo)
[63:60] blendEdges (4 bits : +U(0), -U(1), +V(2), -V(3) — bords avec matériau différent)
```
### Binary Greedy Mesher (CPU, `VoxelMesher.cpp`)
1. **Masques binaires** : pour chaque axe (X,Y,Z), `solid[u][v]` = bitmask 32 bits de voxels solides
2. **Face culling** : `visible = solid & ~(solid >> 1)` pour faces positives (shift adapté par direction), avec lookup cross-chunk aux frontières
3. **Greedy merge** : par tranche de profondeur, grille 2D de material IDs, expansion rectangulaire maximale (largeur puis hauteur)
### Génération procédurale (`VoxelWorld.cpp`)
- Perlin noise 3D (permutation-based, seed configurable)
- fBm 5 octaves pour le heightmap (génération initiale), 2 octaves en animation (perf)
- Caves : `|fbm(x,y,z)| < threshold` en 3D (désactivées en mode animation)
- Matériaux par altitude : sable < 25, herbe 25-70, pierre 70-90, neige > 90
- Chunks générés en Y = 0..7 (hauteur max 256 blocs)
- Animation 60 Hz : `regenerateAnimated()` parallélise génération + pack GPU fusionnés via `wi::jobsystem`
### Renderer (`VoxelRenderer.cpp`)
- **Triple-mode VS** : CPU path (`flags=0`), MDI path (`flags & 1`), GPU mesh path (`flags & 2`)
- **GPU mesh path (actif par défaut)** : compute shader `voxelMeshCS` génère les quads 1×1, `DrawInstanced` avec readback 1-frame-delay du compteur atomique
- **Mega-buffer** : tous les quads de tous les chunks dans un seul `StructuredBuffer<PackedQuad>` (2M quads, 16 MB) — utilisé en mode CPU/MDI
- **Vertex pulling** : le VS lit le quad buffer via `SV_VertexID`, pas de vertex buffer classique
- **Pipeline** : PSO avec `RSTYPE_FRONT` (backface cull), `DSSTYPE_DEFAULT` (depth test), `BSTYPE_OPAQUE`
- **Per-chunk info** : `StructuredBuffer<GPUChunkInfo>` (80 bytes/chunk) avec worldPos, quadOffset, faceOffsets[6], faceCounts[6]
- **Push constants** (b999, 48 bytes) : chunkIndex + quadOffset + flags (bit 0 = MDI mode, bit 1 = GPU mesh mode)
- **CPU culling** : frustum AABB (`wi::primitive::Frustum`) + backface par face group (camera vs AABB) — mode MDI uniquement
- **MDI rendering** (Phase 2.2) : un seul `DrawInstancedIndirectCount` remplace la boucle per-chunk. Push constant = `chunkIndex | (faceIndex << 16)`, le VS reconstruit quadOffset depuis GPUChunkInfo
- **Per-face-group draws** (Phase 2.1 fallback) : jusqu'à 6 `DrawInstanced` par chunk visible
- **Textures** : texture array 2D (256x256, 5 layers) générée procéduralement, triplanar mapping dans le PS. Alpha = heightmap procédural pour blending
- **Height-based blending** (Phase 3) : le PS lit directement `voxelDataBuffer` (SRV t3) pour lookup des matériaux voisins per-pixel. Winner-takes-all : le matériau avec la heightmap la plus haute gagne 100%. Transitions nettes mais forme organique dessinée par les heightmaps. Corner attenuation subtractive (param=0.80). Mode debug blend (F4)
- **Render targets propres** : `voxelRT_` (R8G8B8A8) + `voxelDepth_` (D32_FLOAT), rendu dans `Render()` sur cmd list dédié
- **Composition** : overlay sur le swapchain via `wi::image::Draw()` dans `Compose()`
- **Stats overlay** : affichage HUD des chunks/quads/draw calls via `wi::font::Draw`
- **Frustum planes** : extraction Gribb-Hartmann dans le CB pour le compute shader de cull
- **GPU timestamp queries** : 6 slots (cull begin/end, draw begin/end, mesh begin/end)
- **CPU profiling** : `ProfileAccum` avec moyennes toutes les 5s dans le backlog (Regenerate, UpdateMeshes, VoxelPack, GPU Upload, GPU Dispatch, Render, Frame)
## Phases de développement (spec)
### Phase 1 - Setup et meshing de base [FAIT]
- Fork Wicked Engine, structure de modules
- VoxelWorld avec génération procédurale Perlin (rayon 4 chunks = ~150 chunks)
- Binary Greedy Mesher CPU (~300K quads pour le monde initial)
- Rendu basique avec vertex pulling et texture array
- Caméra libre de navigation (WASD + souris)
- Crash handler SEH avec stack trace symbolique
### Phase 2 - Performance GPU [FAIT]
Découpée en sous-phases pour isoler les sources de bugs potentiels :
#### Phase 2.1 - Mega-buffer + CPU cull + per-face DrawInstanced [FAIT]
- Mega-buffer : tous les quads dans un seul SRV, packés par chunk
- Tri par face group dans le mesher (`faceOffsets[6]`, `faceCounts[6]`)
- CPU frustum culling (AABB vs `wi::primitive::Frustum`)
- CPU backface culling par face group (camera.Eye vs chunk AABB)
- Per-face-group `DrawInstanced` (max 6 draws par chunk visible)
- `GPUChunkInfo` StructuredBuffer pour lookup VS
#### Phase 2.2 - CPU-filled indirect args + DrawInstancedIndirectCount [FAIT]
- Le CPU remplit `IndirectDrawArgs[]` avec la même logique que 2.1 (frustum + backface)
- Le CPU écrit le draw count
- Upload des deux buffers vers le GPU (sans barriers explicites — promotion implicite)
- Un seul `DrawInstancedIndirectCount` remplace la boucle per-chunk
- Le VS décode `chunkIndex | (faceIndex << 16)` depuis le push constant et reconstruit le quadOffset
- **Intérêt** : teste le MDI rendering SANS compute shader (isole les problèmes de barriers)
- **Pièges résolus** :
- `IndirectDrawArgs` fait 20 bytes (pas 16) — voir point 7 dans "Shaders custom — PIÈGES IMPORTANTS"
- `SV_VertexID` n'inclut pas `startVertexLocation` avec ExecuteIndirect — voir point 8
- Pas de barriers explicites nécessaires — voir point 9
#### Phase 2.3 - GPU compute culling [FAIT]
- Le compute shader `voxelCullCS.hlsl` remplace le CPU pour remplir les indirect args
- Barriers DX12 : UNDEFINED → UAV (pre-compute) → INDIRECT_ARGUMENT (post-compute)
- GPU timestamp queries actifs (GPU Cull ~0.006 ms pour 168 chunks)
- **Pièges résolus** :
- `PushConstants` DOIT être appelé APRÈS `BindPipelineState` — voir point 10
- Compute shader corrigé : push constant packing + startVertexLocation=0 — voir points 7-8
- `ResourceState::UNDEFINED` = COMMON en Wicked (valeur 0), déclenche `DiscardResource()` — OK pour les buffers réécrits
#### Phase 2.4 - GPU compute mesher (benchmark) [FAIT]
- Le compute shader `voxelMeshCS.hlsl` fait le meshing 1×1 sur GPU (1 thread par voxel, 8×8×8 thread groups)
- Benchmark automatique au premier frame après génération du monde (mode CPU fallback)
- Résultats (168 chunks, Ryzen 7 9800X3D + RX 9070 XT) :
- CPU greedy: 277 ms, 358K quads → greedy merge réduit les quads de 6.8×
- GPU baseline (1×1): 5.3 ms, 2.43M quads → 52× plus rapide que CPU
- GPU greedy merge non implémenté (pourrait combiner vitesse GPU + réduction de quads)
- Le benchmark est one-shot : state machine IDLE → DISPATCH → READBACK → DONE
#### Phase 2.5 - GPU meshing production + optimisations perf [FAIT]
- **GPU meshing en production** : remplace le CPU greedy mesher comme pipeline par défaut
- `voxelMeshCS.hlsl` : chunkIndex encodé dans les bits [63:49] de chaque quad (11 bits)
- `voxelVS.hlsl` : mode `flags & 2` extrait le chunkIndex depuis le quad, lookup `GPUChunkInfo`
- `VoxelRenderer` : dispatch compute shader → barrier UAV→SRV → `DrawInstanced`
- Readback 1-frame-delay du compteur atomique pour le vertex count
- Le `gpuQuadBuffer_` a les bind flags `UNORDERED_ACCESS | SHADER_RESOURCE`
- **Optimisations perf CPU** (profilées et mesurées) :
- **VoxelPack par memcpy** : `sizeof(VoxelData) == 2`, donc `voxels[]` est directement compatible avec le format GPU (uint16 pairs). Remplace la boucle bit-shift (28ms → <1ms)
- **Cache dirty** : `packedVoxelCache_` ne se repack que quand les chunks changent, pas chaque frame
- **Fused regenerate+pack** : `regenerateAnimated()` accepte un pointeur de destination, chaque job parallèle fait generate + memcpy dans le même thread. Élimine la double itération du hashmap et le pack séquentiel (6ms → 0ms)
- **Skip GPU dispatch** : `gpuMeshDirty_` flag empêche le re-dispatch/upload quand rien n'a changé
- **Upload conditionnel** : `chunkInfoBuffer_` ne se re-upload que quand `chunkInfoDirty_`
- **Animation allégée** : 2 octaves fBm (au lieu de 5) + pas de caves en mode animation (54ms → 8ms)
- **Résultats finaux** (171 chunks, Ryzen 7 9800X3D + RX 9070 XT, animation 60 Hz) :
- Regenerate: 8.7ms (parallèle, 2 octaves)
- VoxelPack: 0ms (fusionné dans regenerate)
- GPU Upload: 4.5ms (~11 MB voxel data)
- GPU Dispatch: 0.1ms (171 × 64 thread groups)
- Frame total: ~9ms → **80-110 FPS** avec animation terrain 60 Hz
- Sans animation: **700+ FPS**
### Phase 3 - Texture blending [FAIT]
Approche **PS-based** : le pixel shader lit directement les données voxel (pas de pré-encodage dans les quads). Voir `blending_experiments.md` pour le détail des itérations.
- **Heightmaps procéduraux** dans le canal alpha de chaque texture de matériau (5 matériaux, paramètres freq/contrast différents)
- **PS neighbor lookup** (`voxelPS.hlsl`) : bind `voxelDataBuffer` à `t3`, `chunkInfoBuffer` à `t2`. Lit les matériaux voisins per-pixel via `readVoxelMat(coord, chunkIdx)`
- **Stair priority** : pour chaque bord, vérifie `pos + edgeDir + normalDir` en premier (le bloc qui masque visuellement le coin), puis fallback `pos + edgeDir`
- **2 axes indépendants** : U et V sont traités séparément avec nearest-edge detection via `sign(faceFrac - 0.5)`
- **Winner-takes-all heightmap** : `mainScore = h_main + bias`, `neighScore = h_neigh - bias`, `bias = 0.5 - weight`. Le matériau avec le score le plus haut gagne à 100%. Sharpness=16 pour anti-aliasing
- **Corner attenuation subtractive** : `xAdj = xEdge - saturate(yEdge - 0.80)` — réduit le blend aux coins où les deux axes se croisent
- **Zone de blend** : 0.25 voxels depuis chaque bord (50% de la face)
- **CB** : `blendEnabled` (float, 1.0 en GPU mesh path, 0.0 sinon) + `debugBlend` (float, toggle F4)
- **VS** (`voxelVS.hlsl`) : passe `chunkIndex` (nointerpolation uint) au PS pour les lookups voxel
- **GPU mesher** (`voxelMeshCS.hlsl`) : simplifié (pas de blend computation), encode seulement `chunkIndex` dans les bits [27:17] du quad
- **Mode debug** (F4) : visualise les zones de blend (rouge=U, bleu=V, vert=pas de blend, rouge vif=data mismatch)
- **Fonctionne uniquement en GPU mesh path** (1×1 quads) ; CPU/MDI paths ont `blendEnabled=0`
### Phase 4 - Toping [EN COURS]
Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour adoucir les transitions entre blocs.
#### Phase 4.1 - Infrastructure TopingSystem [FAIT]
- **TopingSystem** (`TopingSystem.h/.cpp`) : data structures + mesh generation + instance collection
- **4-bit adjacency bitmask** : pour chaque face +Y exposée, vérifie 4 voisins cardinaux (±X, ±Z) pour même matériau avec +Y exposée → 16 variantes
- **Priority-based adjacency** : `TopingDef.priority` détermine quel toping cède aux frontières de matériaux. Grass (priority=1) génère des biseaux par-dessus stone (priority=0)
- **Mesh par matériau** :
- **Stone** : wedge cross-section (outer wall + slope) + corner fills + caps aux terminaisons
- **Grass** : brins d'herbe individuels groupés en touffes, 2 segments courbés, double-sided
- ~191K instances pour ~170 chunks
#### Phase 4.2 - Rendu GPU + shading végétal [FAIT]
- **Shaders dédiés** : `voxelTopingVS.hlsl` (vertex pulling instancié) + `voxelTopingPS.hlsl` (shading par matériau)
- **Vertex pulling** : `StructuredBuffer<TopingVertex>` (t4) + `StructuredBuffer<float3>` (t5 instances)
- **Push constants** : `vertexOffset`, `instanceOffset`, `materialID` réutilisent les 3 premiers champs de b999
- **Per-group DrawInstanced** : instances triées par (type, variant), un `DrawInstanced` par groupe contigu
- **Render pass séparé** avec `LoadOp::LOAD` : topings rendus après voxels, préservent RT et depth
- **PSO** : même rasterizer/depth/blend que les voxels (`RSTYPE_FRONT`, `DSSTYPE_DEFAULT`, `BSTYPE_OPAQUE`)
- **Shading végétal stylisé** (inspiré Airborn Trees, `voxelTopingPS.hlsl`) :
- **Half-Lambert wrap lighting** : `(N·L * 0.5 + 0.5)²` — enveloppe la lumière, pas de terminator dur
- **Translucency** : `dot(V, L) * (1 - NdotL) * 0.4` — lumière traversant les brins fins à contre-jour
- **Ambient chaud** : `(0.22, 0.28, 0.20)` — plus lumineux et verdâtre que l'ambient stone
- **Stone** : Lambert classique identique aux voxels (branchement sur `materialID == 3`)
- **Génération de touffes d'herbe** (`TopingSystem.cpp`) :
- **Tufts** : clusters de 39 brins partageant un centre commun (scatter ±0.03)
- **Position des touffes** : hash-driven le long du bord + inset quadratique 0.00.30 du bord
- **Par-tuft personality** : heightScale (0.201.0), leanScale (0.31.8), blade count (39)
- **Par-brin variety** : hauteur, largeur, angle (±55° fan + jitter), courbure (midLeanRatio 0.080.43)
- **Hash déterministe** : `hashF(a,b,c)` golden-ratio based pour reproductibilité
- **Stone corner fills** : triangle de pente diagonal aux coins où deux bords ouverts se rejoignent
- **Stone caps** : triangle fermant la section du biseau aux terminaisons de strip
- **Pièges résolus** :
- **Winding CW** : `emitTri()` auto-corrige le winding via `dot(geom, desired) > 0` → swap B↔C
- **Slope normal = inward + up** : utiliser `(e.ix, e.iz)`, PAS `(e.nx, e.nz)`
- **sunDirection** : `L = normalize(-sunDirection.xyz)` (direction de voyage → direction vers le soleil)
#### Phase 4.3 - Polish et extensions [A FAIRE]
- Plus de types de topings (neige, mousse, etc.)
- LOD : supprimer les topings à distance
- Animation subtile (vent sur l'herbe via vertex shader)
- Optimisation : compute shader pour le instance collection
### Phase 5 - Rendu smooth [EN COURS]
#### Phase 5.1 - Naive Surface Nets CPU [FAIT]
- **Algorithme** : Naive Surface Nets (dual contouring simplifié) dans `SmoothMesher::meshChunk()`
- **SDF binaire** : solid = -1, empty = +1 (pas de distance field continu)
- **Vertex placement** : centroïde des edge crossings pour chaque cellule à la surface
- **Matériaux smooth** : SmoothStone (mat 6, `FLAG_SMOOTH`) et Snow (mat 5, `FLAG_SMOOTH`)
- **Matériaux blocky** : Stone (mat 3), Grass (mat 1), Dirt (mat 2), Sand (mat 4)
- **SmoothVertex** (32 bytes) : position, face normal, materialID, secondaryMat, blendWeight, chunkIndex
- **Shaders dédiés** : `voxelSmoothVS.hlsl` (vertex pulling t6) + `voxelSmoothPS.hlsl` (triplanar + blending)
- **Render pass séparé** avec `LoadOp::LOAD` : smooth rendu après voxels+topings, préserve RT et depth
**Cross-chunk connectivity** :
- **PAD=2** dans la grille SDF pour accéder aux cellules [-1..CHUNK_SIZE]
- **Vertex range étendu** : `[-1, CHUNK_SIZE)` au lieu de `[0, CHUNK_SIZE)` — les cellules au bord du chunk voisin génèrent des vertices
- **Canonical ownership** : chaque edge est émise par un seul chunk (celui contenant le grid point inférieur), pas de duplication
**Smooth↔blocky boundary** :
- **`hasSmooth` filter étendu** : génère des vertices si la cellule OU une cellule 6-connectée adjacente contient un coin smooth. Sans cet élargissement, les cellules à la frontière smooth↔blocky (100% blocky mais adjacentes à du smooth) n'ont pas de vertex → les quads de connexion ne peuvent pas être émis → trou de génération
- **Per-axis boundary clamping** : les vertices aux frontières smooth↔blocky sont clampés vers la grille entière (empêche le mesh smooth de dépasser sur les faces blocky)
- **GPU mesher** : les voxels smooth sont traités comme solides dans `isNeighborAir()` — les faces blocky ne sont pas émises vers les voxels smooth (le mesh smooth couvre la frontière)
**Face normals — PIÈGES MAJEURS** :
- **Face normals, pas SDF gradient** : le SDF binaire donne des gradients à 45° aux marches, causant du stretching triplanar. Les face normals (cross product des edges du triangle) sont géométriquement correctes.
- **Orientation par axe de l'edge** : chaque quad vient d'une edge X, Y ou Z. La direction `solid→empty` est connue. On vérifie que la composante de la face normal sur cet axe a le bon signe, sinon flip.
- **Y-axis winding inversé** : les sharing cells Y sont arrangées différemment de X et Z. Le winding naturel du quad Y est opposé → `if (axis == 1) useWindingA = !useWindingA;`
- **SDF gradient dot product** : NE PAS utiliser pour orienter les normals (échoue quand le gradient est nul ou ambigu avec SDF binaire)
- **Centroid SDF sampling** : NE PAS utiliser non plus (les deux côtés arrondissent souvent au même voxel)
**Material blending (per-pixel, same as blocky PS)** :
- **Dominant axis detection** : le PS smooth dérive un « face virtuelle » depuis la normale lisse. L'axe avec la plus grande composante `|N|` détermine la face dominante (0-5). Cela donne accès aux mêmes tables `faceNormals`, `faceUDirs`, `faceVDirs` que le PS blocky
- **Même voxelCoord** : `floor(worldPos - normalDir * 0.001)` — tiny offset le long de la normale dominante (PAS `N * 0.5` qui est trop large et tombe dans le mauvais voxel)
- **Même `getNeighborMat()` avec stair priority** : vérifie `pos + edgeDir + normalDir` en premier (le bloc qui masque visuellement l'arête), puis fallback `pos + edgeDir`
- **Face-aligned U/V** : `frac(dot(worldPos, uDir))` / `frac(dot(worldPos, vDir))` — position fractionnaire dans le voxel selon les tangentes de la face dominante
- **Même blend zone (0.25)**, corner attenuation subtractive, winner-takes-all heightmap avec sharpness=16
- **Même bleedMask/resistBleedMask** checks via CB
- **PIÈGE** : NE PAS utiliser les 3 axes world-space avec un filtre `dirDotN > 0.6` — ça ne filtre pas correctement les voisins souterrains et donne des blends incorrects. La dérivation d'un face dominant + U/V alignés est la seule approche correcte
**Debug scene smooth** :
- Lancé avec `BVLEVoxels.exe debugsmooth`
- 11 configurations isolées dans un seul chunk : SmoothStone↔Grass, SmoothStone↔Dirt, SmoothStone↔Sand, SmoothStone↔Stone, Snow↔Grass, Snow↔Sand, références blocky (Sand↔Dirt, Grass↔Dirt), escalier SmoothStone, patch smooth entouré de grass, bloc smooth isolé
#### Phase 5.2 - Smooth normals + optimisations perf [FAIT]
- **Smooth vertex normals** : accumulation area-weighted des face normals dans chaque vertex indexé, puis normalisation. Donne un éclairage Gouraud lisse sans géométrie additionnelle
- **Geometric normals pour triplanar** : le PS utilise `ddx`/`ddy` du worldPos pour reconstruire la normal géométrique (face) pour les poids triplanar, la smooth normal pour le lighting uniquement. Empêche le stretching de textures causé par les normals lissées
- **Depth bias smooth PSO** : rasterizer avec `depth_bias = 1` pour résoudre le z-fighting smooth↔blocky aux frontières
- **Surface-only vertex extension** : le filtre `hasSmooth` étendu vérifie aussi que la cellule est sur la surface (`hasPos && hasNeg`) ET non entièrement souterraine. Empêche le smooth mesh de plonger dans le sous-sol
**Optimisations CPU (560ms → 17ms = 33× plus rapide)** :
- **Cache VoxelData dans la grille** : `voxelGrid[]` stocke VoxelData aux côtés du SDF, élimine tous les `readVoxel` redondants dans le boundary clamping, material counting, surface check
- **Pré-cache 27 chunks voisins** : `neighborChunks[3][3][3]` rempli avant le grid fill. `readVoxelFast()` utilise un accès direct au tableau du chunk voisin au lieu de `world.getVoxel()` (hashmap lookup). Élimine ~14K hashmap lookups par chunk smooth
- **Suppression `computeNormal` mort** : la fonction SDF gradient (6 readVoxel/vertex) était écrasée par les smooth normals. Code mort supprimé
- **Early exit `containsSmooth`** : flag posé pendant `generateChunk()`. `meshChunk()` vérifie ce chunk + 26 voisins (27 hashmap lookups) avant le grid fill coûteux (46K voxel reads). Skip ~70% des chunks
- **Dilation smoothNear** : grille pré-dilatée (smooth + face-neighbors) remplace le check hasSmooth étendu. 8 lookups/cell au lieu de 56 (7× moins dans la boucle la plus chaude)
- **Thread-local scratch buffers** : `SmoothScratch` (~600 KB) alloué une fois par thread, réutilisé entre les appels. Élimine malloc/free par chunk
- **Parallélisation `wi::jobsystem`** : tous les chunks meshés en parallèle sur tous les cœurs CPU
- **Staging vectors persistants** : `smoothStagingVerts_` réutilisé entre frames, évite les allocations de vecteurs
**Optimisations TopingSystem** :
- **`collectInstancesParallel()`** : chaque chunk écrit dans un vecteur local, merge séquentiel. Élimine la contention
- **Staging vectors persistants** : `topingSorted_`, `topingGpuInsts_` réutilisés entre frames
**Résultats animation (648 chunks, Ryzen 7 9800X3D + RX 9070 XT)** :
- SmoothMesh: 560ms → 17ms (parallèle, dilation, cache)
- SmoothUpload: 13ms → 4ms (staging persistant)
- TopingCollect: 58ms → 6.5ms (parallèle)
- TopingUpload: 7.5ms → 1.2ms (bug fix timing + staging persistant)
- Frame total: 662ms → 58ms (1.5 → 17 FPS avec animation terrain)
#### Phase 5.3 - GPU compute Surface Nets [A FAIRE]
- Compute shader pour SDF grid fill + vertex generation + quad emission
- Élimine le CPU bottleneck restant (17ms → <1ms estimé)
- Pattern similaire au GPU mesher blocky (Phase 2.4-2.5)
- Readback 1-frame-delay du compteur atomique pour le vertex count
#### Phase 5.4 - Polish [A FAIRE]
- SDF lissé (distance field approximatif au lieu de binaire ±1)
- LOD : réduction de triangles à distance
- Pipeline asynchrone : double-buffer GPU resources, CPU frame N prépare pendant que GPU rend frame N-1
### Phase 6 - Ray tracing hybride [EN COURS]
#### Phase 6.1 - Infrastructure RT (Normal RT + BLAS/TLAS) [FAIT]
- **Normal render target (MRT)** : `voxelNormalRT_` (R16G16B16A16_SNORM) added as SV_TARGET1
- All 3 pixel shaders (voxelPS, voxelTopingPS, voxelSmoothPS) output `PSOutput` struct with `SV_TARGET0` (color) + `SV_TARGET1` (world-space normal)
- All 3 render passes (`render`, `renderTopings`, `renderSmooth`) use 3 `RenderPassImage` entries (color + normal + depth)
- **BLAS extraction compute shader** (`voxelBLASExtractCS.hlsl`) :
- Reads `gpuQuadBuffer_` (StructuredBuffer<PackedQuad>), extracts world-space float3 positions
- 1 thread per quad → 6 vertices (2 triangles), same unpack + winding logic as voxelVS.hlsl
- Output: `blasPositionBuffer_` (RWByteAddressBuffer, raw buffer), non-indexed triangles
- Dispatched after GPU mesh pass, only when quad count changes
- **Blocky BLAS** : single BLAS from `blasPositionBuffer_` (all blocky quads as non-indexed triangles)
- `PREFER_FAST_BUILD` flag for quick rebuilds during animation
- Vertex format: R32G32B32_FLOAT, stride 12 bytes
- **Smooth BLAS** : single BLAS from `gpuSmoothVertexBuffer_` directly (no extraction needed)
- Position at offset 0, stride 32 bytes (SmoothVtx struct)
- Same `PREFER_FAST_BUILD` flag
- **Toping BLAS** : single BLAS from expanded toping vertices (mesh × instances → world-space float3)
- CPU-side expansion in `uploadTopingData()`: iterates sorted instances, transforms local vertices to world positions
- Dedicated `topingBLASPositionBuffer_` + `topingBLASIndexBuffer_` (separate from blocky)
- `PREFER_FAST_TRACE` flag (optimizes BVH traversal, important for 23M tris)
- ~23M triangles for ~153K instances (varies with blade count/segments)
- **TLAS** : 3 instances (blocky + smooth + topings), identity transforms (all positions are world-space)
- Instance buffer created via `CreateBuffer2` with pre-filled instance data (callback)
- `instance_mask = 0xFF` for both instances
- Recreated each rebuild (avoids `UpdateBuffer` on RAY_TRACING flagged buffers)
- **Lifecycle** : BLAS/TLAS rebuilt when geometry changes (quad count differs from previous frame)
- `rtDirty_` flag triggers rebuild on first frame
- Smooth BLAS auto-recreated when vertex count changes
- **HUD** : RT status line showing TLAS state + triangle counts for blocky/smooth
- **Pièges résolus** :
- **Index buffer obligatoire dans BLAS** : `CreateRaytracingAccelerationStructure` dans Wicked accède TOUJOURS `index_buffer` via `to_internal()` (ligne 4356 de `wiGraphicsDevice_DX12.cpp`), même pour de la géométrie non-indexée. Un `GPUBuffer` par défaut (invalide) cause un null deref à offset 0xd8. De plus, `index_count = 0` avec `IndexBuffer != 0` fait que DX12 interprète "0 triangles indexés" → BLAS vide. Solution : fournir un vrai sequential index buffer `[0,1,2,...]` avec `index_count = vertex_count` et `index_format = UINT32`
- **`CreateBuffer2` pour TLAS instance buffer** : les buffers avec `ResourceMiscFlag::RAY_TRACING` ne supportent pas `UpdateBuffer` (state mismatch). Utiliser `CreateBuffer2` avec callback pour pré-remplir les instances à la création
- **Memory barriers BLAS→TLAS→RT — PIÈGE MAJEUR** : `BuildRaytracingAccelerationStructure` est asynchrone GPU. Sans barriers :
- Le TLAS build peut s'exécuter avant que les BLAS ne soient terminés
- Les ray queries peuvent s'exécuter avant que le TLAS ne soit prêt
- Résultat : BLAS apparaît vide (zéro hits) sans aucun crash ni erreur
- Solution (pattern de `wiRenderer.cpp` lignes 5788, 5808) :
1. `GPUBarrier::Memory()` après tous les BLAS builds, avant le TLAS build
2. `GPUBarrier::Memory(&tlas_)` après le TLAS build, avant les ray queries
#### Phase 6.2 - RT Shadows [FAIT]
- **Compute shader** (`voxelShadowCS.hlsl`) avec inline ray queries (`RayQuery<>`, SM 6.5)
- Lit `voxelDepth_` (t0, D32→R32_FLOAT) + `voxelNormalRT_` (t1) + TLAS (t2)
- Reconstruit worldPos depuis depth via `inverseViewProjection` (ajouté au VoxelCB)
- 3 shadow rays jittered vers le soleil (cone 0.012 rad ≈ 0.7°) pour soft shadows
- `RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH` + `RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES` (shadow binaire)
- Surfaces back-facing (NdotL ≤ 0) : assombries sans ray trace (shadowFactor=0.45)
- **In-place modulation** : `RWTexture2D<float4>` sur `voxelRT_` (u0), chaque thread lit/modifie son pixel (pas de race)
- Colored shadows : `lerp(color, color * shadowTint, shadowAmount)` au lieu de simple darkening
- `voxelRT_` créé avec `UNORDERED_ACCESS` additionnel pour permettre l'écriture compute
- **Shadow origin bias — PIÈGE MAJEUR** :
- L'origine du shadow ray utilise `shadowOrigin = worldPos` (zéro bias position) au lieu du normal bias de l'AO
- Le normal bias (`worldPos + N * 0.15`) pousse l'origine AU-DESSUS des bases de brins d'herbe → gap entre le brin et son ombre
- Le light bias (`worldPos + L * offset`) crée aussi un gap (proportionnel au bias)
- **TMin adaptatif** résout le dilemme self-hit vs gap :
- `TMin = lerp(0.002, 0.10, 1.0 - abs(N.y))`
- Sol (N.y ≈ 1) → TMin=0.002 : ombres collées aux bases des brins
- Brins d'herbe (N.y ≈ 0) → TMin=0.10 : skip la propre géométrie du brin
- **Screen-space contact shadows ne fonctionnent PAS** pour l'herbe : les brins sont trop fins pour que le delta de profondeur NDC soit distinguable du bruit. Testé en 4 itérations (NDC comparison, world-space reconstruction, height filter) — tous échouent
- **Dispatch** : 8×8 thread groups, `ceil(w/8) × ceil(h/8)`, après les 3 render passes (blocky+topings+smooth)
- **Barriers** :
- Pre : `voxelDepth_` DEPTHSTENCIL→SHADER_RESOURCE + `voxelRT_` SHADER_RESOURCE→UAV
- Post : `voxelDepth_` SHADER_RESOURCE→DEPTHSTENCIL + `voxelRT_` UAV→SHADER_RESOURCE
- **Mode debug** (F5 × 2 = DBG) : rouge=shadow hit, vert=miss, bleu=back-facing, gris foncé=ciel
- **Toggle** : F5 cycle OFF→ON→DBG_SHADOW→DBG_AO→OFF
- **CB** : `inverseViewProjection` (float4x4) ajouté après `viewProjection` dans VoxelCB (HLSL + C++)
- **Push constants** : width, height, normalBias, maxDistance, debugMode
#### Phase 6.3 - RT AO [FAIT]
- **Intégré dans `voxelShadowCS.hlsl`** : 4 rayons AO hémisphère cosine-weighted + 3 rayons shadow jittered par pixel (7 total)
- **Distance-weighted AO** : `(1 - hitT/aoRadius)²` — falloff quadratique, valeurs continues au lieu de binaire hit/miss
- **Interleaved Gradient Noise (IGN)** : remplace le hash world-space pour le sampling. Bruit structuré screen-space avec excellentes propriétés spectrales (Jorge Jimenez, 2014)
- **Cranley-Patterson rotation** : `frac(IGN + frameIndex * φ)` — chaque frame explore de nouvelles directions de rayons. Golden ratio (φ ≈ 0.618) assure une couverture maximale de l'hémisphère au fil des frames
- **Accumulation temporelle** : `lerp(history, current, 0.05)` ≈ accumulation de ~20 frames
- `aoHistoryTexture_` (R8_UNORM) persiste entre frames
- `prevViewProjection` dans VoxelCB pour reprojection worldPos → UV du frame précédent
- Rejet si UV hors écran (disocclusion basique)
- `frameIndex` + `historyValid` dans les push constants
- Copy `aoRaw → aoHistory` entre le shadow CS et le blur (capture le signal pré-blur)
- **Bilateral blur séparable** (`voxelAOBlurCS.hlsl`) : 2 passes H+V, rayon 6 (kernel 13×13), edge-stopping sur depth + normals
- **Pipeline 5 passes** :
1. Shadow CS : shadow in-place sur `voxelRT_` + AO temporellement accumulé → `aoRawTexture_` (u1)
2. Copy : `aoRawTexture_` → `aoHistoryTexture_` (pour le frame suivant, pré-blur)
3. Blur H : `aoRawTexture_` → `aoBlurredTexture_` (bilateral, depth/normal edge-stopping)
4. Blur V : `aoBlurredTexture_` → `aoRawTexture_` (idem, direction verticale)
5. Apply : `aoRawTexture_` × `voxelRT_` → modulation finale (ou debug AO grayscale si debugMode=2)
- **Push constants** : width, height, normalBias, shadowMaxDist, debugMode, aoRadius, aoRayCount, aoStrength, frameIndex, historyValid
#### Phase 6.4 - Fallback [A FAIRE]
- Shadow maps + SSAO when RT not available
- `CheckCapability(RAYTRACING)` gating
### Phase 7 - Stylized Lighting (Wonderbox-inspired) [EN COURS]
#### Phase 7.1 - Hemisphere Ambient + Colored Shadows + Rim Light + Tone Mapping [FAIT]
- **Hemisphere ambient** : `lerp(groundAmbient, skyAmbient, N.y * 0.5 + 0.5)` — warm brown below, cool blue above. Applied in all 3 pixel shaders (voxelPS, voxelSmoothPS, voxelTopingPS). Vegetation gets 1.5× ambient for inter-reflection
- **Colored shadows** : RT shadows lerp toward `shadowTint` (blue-violet) instead of just darkening. `shadowFactor=0.55` (softer than 0.3)
- **Rim light** : `pow(1 - NdotV, exponent) * intensity * rimColor`. Warm golden rim on silhouettes. Reduced to 30% on vegetation (thin geometry causes halos)
- **Tone mapping + saturation** : soft exponential tone mapping (`1 - exp(-c)`) + saturation boost in `voxelAOApplyCS.hlsl` (final post-process pass)
- **CB paramètres** : `skyAmbient`, `groundAmbient`, `shadowTint`, `fogColor`, `fogParams`, `rimColor`, `rimParams`, `toneMapParams` added to VoxelCB
- **Fog centralisé** : fog density + color from CB instead of hardcoded per-shader
- **Screenshot mode** : CLI argument `screenshot` → fixed camera, 60 frames AO convergence, save `bvle_screenshot.png`, quit. Small non-intrusive window (`SW_SHOWNOACTIVATE`). No HUD.
## Métriques cibles et résultats
| Métrique | Cible | Résultat (Ryzen 7 9800X3D + RX 9070 XT) |
|----------|-------|---------------------------------------|
| FPS 1440p | > 60 fps | ✅ 80-110 FPS (anim blocky), 700+ FPS (statique) |
| FPS anim smooth+topings | > 15 fps | ✅ 17 FPS (smooth+topings+blocky anim 60Hz) |
| Meshing GPU (blocky) | < 200 µs/chunk | ✅ ~0.6 µs/chunk (0.1ms / 171 chunks) |
| Meshing CPU (smooth) | < 30ms | ✅ 17ms (parallèle, 648 chunks) |
| Re-mesh complet | < 16ms | ✅ ~13ms blocky (regen 8.7ms + upload 4.5ms) |
| Mémoire GPU | < 500 Mo | ✅ ~30 Mo (11 MB voxels + 16 MB quads + buffers) |
| RT shadows + AO | < 4ms en 1440p | ⏳ Phase 6 |
| Draw calls | < 100 | ✅ 1 (GPU mesh) ou 1 (MDI) |
## Conventions
- Namespaces : tout le code voxel est dans `namespace voxel`
- Chunks : 32x32x32, configurable via `CHUNK_SIZE`
- Coordonnées : Y = haut, monde infini en X/Z, hashmap sparse
- Matériaux : palette de 256, index 0 = air (vide), 1=grass, 2=dirt, 3=stone (blocky), 4=sand, 5=snow (smooth), 6=smoothstone (smooth)
- Faces : 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z
- Smooth flag : `FLAG_SMOOTH = 0x1` dans VoxelData flags — active Surface Nets au lieu du rendu blocky