Replace per-chunk DrawInstanced loop with a single DrawInstancedIndirectCount. CPU fills indirect args buffer with same frustum+backface cull logic as Phase 2.1. Key discoveries: - Wicked Engine command signature includes push constant (20-byte stride, not 16) - SV_VertexID does not reliably include startVertexLocation with ExecuteIndirect - Solution: pack chunkIndex|(faceIndex<<16) in push constant, VS reconstructs quad offset from GPUChunkInfo lookup - No explicit DX12 barriers needed (implicit promotion from COMMON suffices) Also adds voxel_engine_spec.md and updates references from .docx to .md.
17 KiB
SPECIFICATION TECHNIQUE
Prototype de Moteur Voxel Hybride
Plateforme cible : Wicked Engine (C++17, Vulkan/DX12)
GPU de référence : AMD RX 9070 XT (RDNA 4) + compatibilité Nvidia RTX 3060+
Date : Mars 2026
1. Contexte et objectifs
Ce prototype vise à valider la faisabilité et la performance d'un moteur de rendu voxel enrichi, inspiré de titres comme Enshrouded, sur du matériel GPU moderne (RDNA 2+ / RTX 3060+). Le moteur combine rasterisation haute performance pour le rendu primaire et ray tracing optionnel pour l'éclairage.
1.1 Objectifs du prototype
- Valider les performances de meshing et de rendu sur GPU moderne (cible : 60+ fps à 1440p, monde de 512×512×256 voxels visibles)
- Implémenter un pipeline de rendu hybride rasterisation + RT (ray queries pour ombres/AO)
- Démontrer les 4 fonctionnalités clés : meshing blocky, toping, texture blending, rendu smooth alternatif
- Mesurer les coûts relatifs de chaque technique pour guider les choix d'architecture du moteur final
1.2 Non-objectifs (hors scope prototype)
- Gameplay, UI in-game, sauvegarde/chargement de monde
- Multijoueur, streaming réseau
- Système de LOD distant (clipmaps, impostors) — uniquement LOD par taille de chunk
- Physique avancée (collisions basiques suffisantes pour la navigation caméra)
- Optimisation mobile / consoles
1.3 Plateforme technique
| Composant | Choix |
|---|---|
| Moteur de base | Wicked Engine (fork, branche dédiée) |
| Langage | C++17, HLSL pour les shaders |
| API graphique | Vulkan (primaire) + DX12 (secondaire). Cross-vendor AMD/Nvidia. |
| Shader model | SM 6.6 (bindless descriptor heap) |
| GPU référence | AMD RX 9070 XT (RDNA 4), testé aussi sur Nvidia RTX 3060+ |
| Build | CMake, Visual Studio 2022 / GCC 13+ |
| Profiling | AMD RGP, Nvidia Nsight, RenderDoc, Tracy (CPU) |
**Justification Wicked Engine : **Le moteur fournit un pipeline de rendu bindless GPU-driven moderne (vertex pulling, indirect draw), un ECS, la gestion d'input/audio/assets, le scripting Lua, et un éditeur. Le renderer est contrôlable et remplaçable pièce par pièce. Licence MIT.
2. Architecture générale
2.1 Vue d'ensemble du pipeline
Le pipeline de rendu se décompose en passes séquentielles. Les passes de rasterisation produisent le G-buffer. Les passes de ray tracing enrichissent l'éclairage. Le compositing final assemble le tout.
Passes de rendu (ordre d'exécution)
| # | Passe | Description | Type |
|---|---|---|---|
| 0 | GPU Compute Meshing | Binary greedy mesh des chunks modifiés (compute shader) | Compute |
| 1 | Frustum + Occlusion Cull | Culling GPU-driven des chunks visibles, écriture des indirect draw args | Compute |
| 2 | G-Buffer Voxels | Rasterisation des chunks meshés via MultiDrawIndirect + vertex pulling | Raster |
| 3 | G-Buffer Toping | Rasterisation des meshes décoratifs (instanced draw) | Raster |
| 4 | G-Buffer Smooth | Rasterisation des chunks marching cubes / surface nets (si activé) | Raster |
| 5 | RT Shadows + AO | Ray queries dans compute shader sur TLAS (voxels + toping) | Compute + RT |
| 6 | Deferred Lighting | PBR lighting avec résultats RT | Compute |
| 7 | Post-process | Bloom, tonemapping, TAA (existant Wicked) | Compute + Raster |
2.2 Structures de données voxel
Chunk
-
Taille : 32×32×32 voxels (configurable, max 64³ pour le binary greedy mesh)
-
Stockage : tableau dense de VoxelData, 2 octets par voxel
-
**VoxelData layout (16 bits) : **8 bits material ID (256 matériaux), 4 bits flags (smooth, transparent, emissive, custom), 4 bits metadata (orientation, variant)
-
Chunks stockés dans une hashmap<ivec3, Chunk*> pour un monde infini sparse
-
Upload GPU : le tableau dense est copié dans un storage buffer quand le chunk est modifié Palette de matériaux
-
Tableau global de 256 MaterialDesc, uploadé une fois en GPU storage buffer
-
**MaterialDesc : **albedo texture index (uint16), normal texture index, heightmap texture index, roughness/metallic packé, flags (triplanar, blend mode, smooth override)
-
Toutes les textures dans un texture array 2D bindless (512×512, max 256 layers) Toping registry
-
Tableau de TopingDef : mesh asset ID, conditions de placement (face, adjacence bitmask), LOD meshes
-
Chaque TopingDef référence 1-6 mesh variants selon le bitmask d'adjacence des voisins
-
Les mesh assets sont stockés dans le système d'assets standard de Wicked Engine
3. Fonctionnalité 1 : Meshing des voxels
3.1 Algorithme : Binary Greedy Meshing
Implémentation basée sur le binary greedy mesher de cgerikj (github.com/cgerikj/binary-greedy-meshing), adaptée pour fonctionner en compute shader GPU.
Pipeline de meshing (compute shader)
**Étape 1 — Masque binaire : **Pour chaque axe (X, Y, Z), générer un tableau 32×32 de uint32 où chaque bit représente la présence d'un voxel opaque sur cet axe. Une opération bitwise (col & !(col << 1)) identifie les faces visibles.
**Étape 2 — Face culling : **Pour chaque direction (6 faces), un tableau 30×30 de uint32 encode les faces exposées à l'air. 64 faces sont cullées simultanément par opération bitwise.
**Étape 3 — Greedy merge : **Les faces visibles de même type de matériau sont fusionnées en quads maximaux via des opérations bitwise. Le type de voxel source est vérifié pour éviter de fusionner des matériaux différents.
**Étape 4 — Vertex output : **Chaque quad produit 8 octets : 6 bits X, 6 bits Y, 6 bits Z, 6 bits largeur, 6 bits hauteur, 8 bits material ID, reste pour AO et flags. Écriture dans un append buffer GPU.
Vertex format packé (8 octets par quad)
| Champ | Bits | Description |
|---|---|---|
| position X | 6 | Position locale dans le chunk (0-63) |
| position Y | 6 | Position locale dans le chunk (0-63) |
| position Z | 6 | Position locale dans le chunk (0-63) |
| width | 6 | Largeur du quad fusionné (1-32) |
| height | 6 | Hauteur du quad fusionné (1-32) |
| face | 3 | Direction de la face (0-5 : +X,-X,+Y,-Y,+Z,-Z) |
| material ID | 8 | Index dans la palette de matériaux |
| AO | 8 | 4×2 bits : AO précalculé aux 4 coins du quad |
| flags | 15 | Réservé (blend, smooth edge, ...) |
Rendu : Vertex Pulling + MultiDrawIndirect
- Tous les quads de tous les chunks dans un seul grand storage buffer GPU
- Un IndirectDrawArgs buffer contenant un entry par chunk visible (rempli par le compute de culling)
- Le vertex shader reconstruit les 4 coins du quad à partir des 8 octets packés, en utilisant SV_VertexID
- Un seul appel DrawIndirect rend tous les chunks visibles d'un coup
- Backface culling par orientation de chunk : 6 groupes de faces, masqués selon la direction caméra
3.2 Critères de validation
- Meshing d'un chunk 32³ en < 200µs sur GPU (compute shader)
- Rendu de 10 000 chunks (> 1M voxels visibles) à 60+ fps en 1440p
- Mémoire GPU < 500 Mo pour un monde de 512×512×128
- Re-mesh d'un chunk modifié en < 1 frame (pas de stutter visible)
4. Fonctionnalité 2 : Toping (meshes décoratifs)
4.1 Concept
Le toping permet de remplacer ou décorer certaines faces de voxels par des meshes 3D arbitraires. Le mesh utilisé s'adapte automatiquement selon les blocs adjacents (connected models).
4.2 Système d'adjacence (bitmask)
Pour chaque face d'un voxel marqué « toping », le moteur lit les 4 voisins cardinaux de cette face et encode leur état dans un bitmask 4 bits (haut, droite, bas, gauche). Ce bitmask sert d'index dans une table de 16 variantes de mesh.
- Exemple : un rebord de fenêtre utilise un mesh droit si les voisins gauche et droite sont du même type, un coin si un seul est présent, un embout si aucun n'est présent
- Les variantes sont définies dans le TopingDef (asset data), pas en code
- Le calcul du bitmask se fait au moment du meshing du chunk (CPU ou compute)
4.3 Pipeline de rendu
- Les toping instances sont collectées lors du meshing : position monde + mesh variant ID + rotation
- Stockées dans un instance buffer GPU par chunk
- Rendues par instanced draw (un draw call par type de mesh, batched)
- Les meshes de toping écrivent dans le même G-buffer que les voxels
- Le fragment shader des toping utilise des UVs classiques (pas de triplanar), PBR standard
4.4 Critères de validation
- Support de 16 variantes par type de toping (bitmask 4 bits)
- Rendu de 50 000 instances de toping à < 2ms GPU
- Ajout/suppression d'un bloc met à jour les toping adjacents sans stutter
5. Fonctionnalité 3 : Texture blending
5.1 Technique : Height-based triplanar blending
Le blending entre matériaux adjacents combine deux techniques complémentaires.
Triplanar mapping (base)
-
Chaque matériau est projeté selon les 3 axes (X, Y, Z) avec blend par la normale de surface
-
Les poids de blend sont élevés à une puissance (sharpness) pour contrôler la netteté de la transition
-
Les UVs sont dérivées de la position monde (tiling automatique, pas d'UV authoring) Height-based blending (transition entre matériaux)
-
Chaque matériau a une heightmap (stockée dans le canal alpha de l'albedo, ou texture séparée)
-
Aux frontières entre deux matériaux, le blend est pondéré par la hauteur : le matériau « plus haut » localement domine
-
Résultat : le sable va dans les fissures de la pierre, l'herbe pousse sur les arêtes du terrain Implémentation dans le fragment shader
-
Le vertex shader passe : position monde, normale, material ID du voxel courant + material IDs des voisins sur les axes de blend
-
Le fragment shader échantillonne 2 matériaux × 3 axes = 6 texture fetches pour l'albedo (+ 6 pour normal maps si activé)
-
Le blend final combine triplanar weights × height weights × distance-to-edge falloff
-
Optimisation : si les voisins sont du même matériau, early-out vers un single material path (1×3 fetches)
5.2 Données supplémentaires par vertex
- Le vertex format du mesher est étendu avec : 8 bits neighbor material ID (le voisin le plus influent)
- Le blend weight est calculé dans le vertex shader à partir de la position du vertex relative au centre du quad
- Alternative : encoder le neighbor material dans les 15 bits de flags réservés du vertex format
5.3 Critères de validation
- Transitions visuellement lisses entre 2 matériaux adjacents (pas de ligne dure visible)
- Coût GPU du blending < 0.5ms additionnel par rapport au rendu single-material
- La heightmap influence visiblement la forme de la transition (test : sable/pierre)
6. Fonctionnalité 4 : Rendu smooth alternatif
6.1 Approche : Surface Nets (ou Marching Cubes)
Certains voxels marqués avec le flag « smooth » dans leurs 4 bits de flags sont rendus avec un mesher alternatif qui produit une surface lisse au lieu de cubes.
Algorithme recommandé : Naive Surface Nets
-
Plus simple que Dual Contouring, moins de cas edge que Marching Cubes
-
Un vertex par cellule, positionné au centroïde des edge crossings
-
Le champ de distance signé (SDF) est dérivé des voxels : voxel plein = -1, voxel vide = +1, lissé par les voisins
-
Le mesh résultant est stocké dans un buffer séparé (pas mélangé avec les quads du greedy mesh) Alternative : Transvoxel
-
Si le LOD multi-résolution est nécessaire, Transvoxel gère le stitching entre niveaux de détail
-
Plus complexe à implémenter, réserver pour une phase ultérieure
6.2 Coexistence blocky / smooth
- Un chunk peut contenir les deux types de voxels
- Le mesher produit deux outputs : quads (blocky) et triangles (smooth), dans des buffers séparés
- Les deux sont rendus dans le même G-buffer, dans deux draw calls distincts (passe 2 et passe 4)
- Le fragment shader smooth utilise le triplanar mapping (pas d'UVs générées)
- La frontière blocky/smooth est gérée par un falloff dans le SDF : les voxels « smooth » adjacents à des voxels « blocky » ont un SDF écrasé vers 0/1 pour éviter les gaps
6.3 Critères de validation
- Un terrain organique (collines, grottes) rendu sans arêtes visibles entre les voxels
- Coexistence blocky/smooth dans le même chunk sans cracks ni gaps visuels
- Performance du mesher smooth : < 500µs par chunk 32³ sur GPU
7. Ray tracing hybride (qualité d'éclairage)
7.1 Scope pour le prototype
Le ray tracing n'est pas utilisé pour le rendu primaire (la rasterisation est plus performante). Il est utilisé pour améliorer la qualité de l'éclairage via des ray queries dans un compute shader post-G-buffer.
Effets RT implémentés
- **RT Shadows : **1 rayon par pixel vers la lumière directionnelle. Hard shadows, avec option soft shadows (jittered area light, 4-8 samples).
- **RT Ambient Occlusion : **4-8 rayons par pixel en hémisphère cosine-weighted autour de la normale. Courte portée (2-4 mètres).
- Les deux effets utilisent VK_KHR_ray_query (ray queries inline dans le compute shader), pas un full RT pipeline
7.2 Acceleration structures
- BLAS : un BLAS de triangles par chunk meshé (le mesh issu du greedy mesher, reconstruit quand le chunk change)
- BLAS toping : les meshes de toping partagés, instanciés dans le TLAS
- TLAS : reconstruit chaque frame (les instances bougent peu, le rebuild est rapide)
- Les chunks hors frustum ne sont pas inclus dans le TLAS
7.3 Fallback sans RT
- Si le GPU ne supporte pas VK_KHR_ray_query : fallback vers shadow maps classiques + SSAO
- Le fallback est le pipeline d'éclairage standard de Wicked Engine (déjà implémenté)
- Détection au startup via vkGetPhysicalDeviceFeatures2 (rayQueryFeaturesKHR)
7.4 Critères de validation
- RT Shadows fonctionnels sur AMD RDNA 2+ et Nvidia RTX 3060+
- Coût GPU du RT pass (shadows + AO) < 4ms en 1440p
- Fallback automatique vers shadow maps si RT non disponible
8. Intégration dans Wicked Engine
8.1 Modules à créer
| Module | Responsabilité | Dépendance Wicked |
|---|---|---|
| VoxelWorld | Hashmap de chunks, génération procédurale, modification, streaming | wi::ecs, wi::jobsystem |
| VoxelMesher | Binary greedy mesh (compute), surface nets (compute), output buffers | wi::graphics (compute) |
| VoxelRenderer | Render passes custom, vertex pulling, indirect draw, texture blending shader | wi::renderer (hooks) |
| TopingSystem | Registry des toping, calcul des instances, instance buffer management | wi::scene, wi::ecs |
| VoxelRT | BLAS/TLAS management, ray query compute pass | wi::graphics (RT ext) |
8.2 Modules Wicked utilisés tels quels
- wi::input : clavier, souris, gamepad
- wi::audio : son ambiant, effets
- wi::lua : scripting pour le prototypage rapide de scènes de test
- wi::renderer : post-processing (bloom, tonemapping, TAA), sky rendering
- wi::scene : entités non-voxel (lumières, caméra, props)
- wi::physics : collisions caméra basiques (Jolt)
- Editeur Wicked : placement de lumières, débug visuel, profiling intégré
8.3 Points d'intégration dans le renderer
Le VoxelRenderer s'insère dans le render path de Wicked via des hooks dans le RenderPath3D. Les passes voxel custom sont exécutées entre le depth prepass et le deferred lighting, au même moment où Wicked rend normalement ses objets opaques.
9. Plan de développement
Phase 1 — Setup et meshing de base
- Forker Wicked Engine, créer la structure de modules
- Implémenter VoxelWorld (hashmap, génération procédurale bruit Perlin)
- Implémenter le binary greedy mesher en CPU (port du code cgerikj en C++)
- Rendu basique : un draw call par chunk, vertex pulling, texture array
- **Livrable : **monde procédural navigable avec caméra libre, rendu blocky texturé
Phase 2 — Performance GPU
- Porter le mesher en compute shader
- Implémenter MultiDrawIndirect (un seul draw call pour tous les chunks)
- Frustum culling GPU + indirect args
- Backface culling par orientation (6 groupes de faces)
- **Livrable : **benchmark de performance meshing + rendu, comparaison CPU vs GPU mesher
Phase 3 — Texture blending
- Implémenter le triplanar mapping dans le fragment shader voxel
- Ajouter le height-based blending aux frontières de matériaux
- Créer 4-5 matériaux de test (herbe, terre, pierre, sable, neige) avec heightmaps
- **Livrable : **transitions visuellement convaincantes entre matériaux
Phase 4 — Toping
- Implémenter le TopingSystem avec bitmask d'adjacence
- Créer 2-3 types de toping de test (rebord de pierre, bordure d'herbe, décoration murale)
- Rendu instancié des toping dans le G-buffer
- **Livrable : **meshes décoratifs placés automatiquement, adaptatifs aux voisins
Phase 5 — Rendu smooth
- Implémenter Surface Nets (ou Marching Cubes) en compute shader
- Gérer la coexistence blocky/smooth dans le même chunk
- Triplanar mapping sur les surfaces smooth
- **Livrable : **terrain organique smooth côtoyant des structures blocky sans artefacts
Phase 6 — Ray tracing hybride
- Construire les BLAS/TLAS à partir des meshes existants
- Implémenter le compute shader de RT shadows (ray queries)
- Implémenter le RT AO
- Fallback automatique shadow maps/SSAO
- **Livrable : **éclairage RT fonctionnel sur AMD et Nvidia, mesures de performance
10. Métriques de succès
| Métrique | Cible | Mesure |
|---|---|---|
| FPS en 1440p | > 60 fps, monde 512×512×128 | RGP / Nsight frame time |
| Temps de meshing GPU | < 200µs par chunk 32³ | GPU timestamp queries |
| Temps de re-mesh | < 1 frame (16ms) pour 1 chunk | Tracy CPU profiler |
| Mémoire GPU monde | < 500 Mo pour 512×512×128 | RGP memory view |
| RT shadows + AO | < 4ms en 1440p | GPU timestamp queries |
| Draw calls par frame | < 100 (hors post-process) | RenderDoc statistics |
| Compatibilité cross-vendor | Fonctionne sur RDNA 2+ et RTX 3060+ | Tests manuels |
Fin du document de spécification