295 lines
15 KiB
Markdown
295 lines
15 KiB
Markdown
# 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.docx` à 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
|
||
│ │ └── VoxelRenderer.h/.cpp# Renderer + VoxelRenderPath (sous-classe RenderPath3D)
|
||
│ └── 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 VS et PS)
|
||
│ ├── voxelVS.hlsl # Vertex shader (vertex pulling)
|
||
│ └── voxelPS.hlsl # Pixel shader (triplanar + lighting)
|
||
└── 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
|
||
```
|
||
|
||
### 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] AO (4x2 bits par coin)
|
||
[63:49] flags (réservés)
|
||
```
|
||
|
||
### 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
|
||
- Caves : `|fbm(x,y,z)| < threshold` en 3D
|
||
- 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)
|
||
|
||
### Renderer (`VoxelRenderer.cpp`)
|
||
|
||
- **Vertex pulling** : pas de vertex buffer classique, le VS lit un `StructuredBuffer<PackedQuad>` via `SV_VertexID`
|
||
- **Pipeline** : PSO avec `RSTYPE_FRONT` (backface cull), `DSSTYPE_DEFAULT` (depth test), `BSTYPE_OPAQUE`
|
||
- **Per-chunk** : push constants (b999, 48 bytes) pour la position monde du chunk, bind du quad buffer en `t0`
|
||
- **Textures** : texture array 2D (256x256, 5 layers) générée procéduralement, triplanar mapping dans le PS
|
||
- **Culling** : distance-based simple (512 blocs), pas de frustum culling GPU
|
||
- **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`
|
||
|
||
## 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 [A FAIRE]
|
||
|
||
- Porter le mesher en compute shader
|
||
- MultiDrawIndirect (un seul draw call pour tous les chunks)
|
||
- Frustum culling GPU + indirect args
|
||
- Backface culling par orientation (6 groupes de faces)
|
||
- Benchmark CPU vs GPU mesher
|
||
|
||
### Phase 3 - Texture blending [A FAIRE]
|
||
|
||
- Triplanar mapping (déjà en place, à affiner)
|
||
- Height-based blending aux frontières de matériaux
|
||
- Heightmaps dans le canal alpha ou texture séparée
|
||
- Neighbor material ID dans le vertex format (8 bits dans les flags réservés)
|
||
|
||
### Phase 4 - Toping [A FAIRE]
|
||
|
||
- TopingSystem avec bitmask d'adjacence 4 bits (16 variantes)
|
||
- Instance buffer GPU par chunk
|
||
- Instanced draw dans le G-buffer
|
||
- 2-3 types de test (rebord de pierre, bordure d'herbe)
|
||
|
||
### Phase 5 - Rendu smooth [A FAIRE]
|
||
|
||
- Surface Nets (ou Marching Cubes) en compute shader
|
||
- Flag `smooth` dans VoxelData
|
||
- Coexistence blocky/smooth dans le même chunk
|
||
- Buffer séparé pour les triangles smooth
|
||
|
||
### Phase 6 - Ray tracing hybride [A FAIRE]
|
||
|
||
- BLAS par chunk (depuis le mesh greedy), TLAS par frame
|
||
- RT Shadows via ray queries (compute shader)
|
||
- RT AO (4-8 rayons, courte portée)
|
||
- Fallback shadow maps / SSAO si RT non disponible
|
||
|
||
## Métriques cibles
|
||
|
||
| Métrique | Cible |
|
||
|----------|-------|
|
||
| FPS 1440p | > 60 fps, monde 512x512x128 |
|
||
| Meshing GPU | < 200 us par chunk 32^3 |
|
||
| Re-mesh | < 1 frame (16ms) pour 1 chunk |
|
||
| Mémoire GPU | < 500 Mo pour 512x512x128 |
|
||
| RT shadows + AO | < 4ms en 1440p |
|
||
| Draw calls | < 100 (hors post-process) |
|
||
|
||
## 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)
|
||
- Faces : 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z
|