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) |
**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.
- 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))identifielesfacesvisibles.
**É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 |
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 à <2msGPU
- Ajout/suppression d'un bloc met à jour les toping adjacents sans stutter
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µsparchunk32³surGPU
# 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) <4msen1440p
- Fallback automatique vers shadow maps si RT non disponible
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
J'aimerais tester quelque chose, c'est un nouveau type de block qui ne contient que des modèles 3D customs et qui aurait des comportements de jointure dynamique selon les blocs voisins identiques. Spécifiquement, j'aimerais créer des tuyaux qui se connectent les uns aux autres ou créent des nouvelles connexions pour toujours toucher les blocks tuyaux voisin.
Le ciel te plaît — parfait ! On continue vers Wonderbox. Qu'est-ce que tu voudrais améliorer ensuite ? En comparant avec la ref, je vois plusieurs pistes :
Couleurs plus saturées/profondes — le vert de l'herbe Wonderbox est plus riche et profond
Fog atmosphérique — la brume chaude au loin qui fond le terrain dans le ciel
Ombres plus marquées — le contraste ombre/lumière est plus prononcé dans Wonderbox
## 1. Gestion mémoire : état des lieux et clarification
**Pattern actuel (répété 5× dans le code) :**
```
uploadXxxData() {
staging.resize(count); // CPU staging vector
/* fill staging */
if (capacity <count){
capacity = count * 1.25;
CreateBuffer(nullptr); // GPU buffer, no data
}
dirty = true; // flag for Render()
}
Render() {
if (dirty) {
UpdateBuffer(staging); // actual GPU upload
dirty = false;
}
}
```
**Problèmes :**
- Pattern dupliqué pour : `topingInstanceBuffer_`, `topingBLASPositionBuffer_`, `smoothVertexBuffer_`, et les 3 BLAS + TLAS
- Chaque duplication a engendré des bugs (le crash `memmove`, la fuite VRAM BLAS, les ombres figées)
- Les dirty flags sont dispersés (`topingInstanceDirty_`, `smoothVertexDirty_`, `topingBLASDirty_`, `rtDirty_`) avec des dépendances d'ordre non-évidentes (le BLAS upload doit précéder le BLAS build)
- 15 membres `mutable` juste pour les flags + capacités
**Proposition :** Extraire un `DeferredGPUBuffer` encapsulant ce pattern :
Ça élimine ~50 lignes de boilerplate par buffer et centralise les invariants (capacity > count, create avec nullptr, upload avec taille réelle).
---
## 2. Performance : propositions sans régression fonctionnelle
### 2.2 — Paralléliser le tri + packing d'instances toping (~5ms → ~1ms)
Le `std::sort` sur 30K éléments et la copie dans `topingGpuInsts_` sont single-thread. Utiliser `wi::jobsystem` pour partitionner par type (2 types = 2 jobs), ou un counting sort (16 buckets par variant × 2 types = 32 buckets) qui est O(N) au lieu de O(N log N).
### 2.3 — Skip le BLAS rebuild quand seul le blocky change
Actuellement buildAccelerationStructures() rebuild les 3 BLAS + TLAS à chaque frame d'animation. Si seul le terrain blocky change (pas de vent/toping), le toping BLAS rebuild est inutile. Ajouter des dirty flags granulaires :
mutable bool blockyBLASDirty_ = false;
mutable bool smoothBLASDirty_ = false;
// topingBLASDirty_ existe déjà
## 3. Refactoring : axes de simplification
### 3.2 — Extraire le RT dans une classe dédiée
`VoxelRenderer` fait 2900+ lignes et mélange rendering, meshing, et ray tracing. Extraire un `VoxelRTManager` :
// All RT-related state (rtAvailable_, rtDirty_, aoTextures_, etc.)
};
```
Ça isole les ~500 lignes de RT et ses 20+ membres, rendant le debugging plus ciblé.
### 3.3 — Unifier le pattern deferred upload
Comme décrit en §1, le `DeferredGPUBuffer` centralisé évite la duplication error-prone. Chaque bug rencontré (crash memmove, VRAM leak, ombres figées) vient d'une variation mal implémentée de ce même pattern.
### 3.4 — Simplifier `VoxelRenderPath`
`VoxelRenderPath` fait office de "god object" : caméra, input, animation, profiling, render targets, wind. Extraire :
- Input/caméra → struct `CameraController`
- Profiling → struct `VoxelProfiler` (déjà un bon candidat, les `ProfileAccum` sont isolables)