Phase 4.1: TopingSystem infrastructure + procedural mesh generation
- TopingSystem with TopingDef registry, procedural mesh gen, instance collection - 2 toping types: stone bevel (h=0.06, smooth) + grass edge (h=0.12, bumpy) - 16 mesh variants per type indexed by 4-bit adjacency bitmask (~6 unique with symmetry) - Wedge cross-section: outer wall + sloped top, grass has sinusoidal height profile - Instance collection scans exposed +Y faces, checks same-material neighbors - Cross-chunk adjacency via VoxelWorld::getVoxel() - Integrated into VoxelRenderPath: init at Start(), stats in HUD - ~191K instances, 1920 mesh vertices for 170 chunks (validated) - Research doc (research_connected_meshes.md) + plan (plan_phase4.md)
This commit is contained in:
parent
f166394b60
commit
9e777d653b
6 changed files with 696 additions and 1 deletions
58
plan_phase4.md
Normal file
58
plan_phase4.md
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Plan Phase 4 — Toping (meshes décoratifs adaptatifs)
|
||||||
|
|
||||||
|
Le toping consiste à placer automatiquement des **meshes 3D procéduraux** sur les faces exposées de certains voxels, qui s'adaptent à leur voisinage via un bitmask 4 bits (16 variantes). C'est fondamentalement différent des phases précédentes : on passe du rendu de quads au rendu de **triangles instanciés** avec géométrie 3D.
|
||||||
|
|
||||||
|
## Phase 4.1 — Infrastructure TopingSystem + génération de meshes procéduraux
|
||||||
|
|
||||||
|
**Objectif** : Créer le système de données (TopingDef, TopingInstance) et générer des meshes de test procéduralement (pas d'import d'assets).
|
||||||
|
|
||||||
|
- **`TopingSystem` class** (`src/voxel/TopingSystem.h/.cpp`) :
|
||||||
|
- `TopingDef` : définit un type de toping (ID matériau source, face(s) applicables, 16 mesh variants indexées par bitmask d'adjacence)
|
||||||
|
- `TopingInstance` : position monde (float3) + rotation/face + variant index + toping type ID
|
||||||
|
- Registry de `TopingDef` (tableau fixe, 2-3 types de test)
|
||||||
|
- Collecte des instances : itérer sur les voxels exposés d'un chunk, vérifier le matériau, calculer le bitmask 4 bits des voisins cardinaux dans le plan de la face, émettre un `TopingInstance`
|
||||||
|
|
||||||
|
- **Meshes procéduraux de test** (pas d'assets externes) :
|
||||||
|
- **Type 1 — Rebord de pierre** : sur les faces +Y de la stone, petit biseau/corniche qui longe le bord supérieur
|
||||||
|
- **Type 2 — Bordure d'herbe** : sur les faces +Y du grass, petites touffes/relief organique sur les bords
|
||||||
|
- Chaque type a 16 variants (straight, corner, T-junction, cross, isolé, etc.) générées par code
|
||||||
|
|
||||||
|
- **Livrables** : TopingSystem peut scanner un chunk et produire une liste de TopingInstance. Pas de rendu encore.
|
||||||
|
|
||||||
|
## Phase 4.2 — GPU buffers + pipeline de rendu instancié
|
||||||
|
|
||||||
|
**Objectif** : Rendre les toping instances sur le même render target que les voxels.
|
||||||
|
|
||||||
|
- **Vertex buffer partagé** pour les mesh variants :
|
||||||
|
- Toutes les variantes de tous les TopingDef dans un seul vertex buffer (position + normal + UV)
|
||||||
|
- Index table : `meshOffset[topingType][variant]` → offset + vertex count dans le VB
|
||||||
|
|
||||||
|
- **Instance buffer** (`StructuredBuffer<TopingInstance>`) :
|
||||||
|
- Rempli par le CPU après collecte (comme le mega quad buffer)
|
||||||
|
- Contient : world position, face rotation matrix (ou quaternion), mesh variant offset
|
||||||
|
|
||||||
|
- **PSO toping** :
|
||||||
|
- Nouveau VS (`voxelTopingVS.hlsl`) : lit instance data + vertex data, transforme en world space
|
||||||
|
- Réutilise le PS existant (ou PS simplifié avec UVs classiques au lieu de triplanar)
|
||||||
|
- Même root signature, même depth buffer, même render target
|
||||||
|
- `DrawInstanced` ou `DrawInstancedIndirect` groupé par type de mesh
|
||||||
|
|
||||||
|
- **Intégration dans `VoxelRenderer::render()`** : après le draw des voxels, draw des topings dans le même render pass
|
||||||
|
|
||||||
|
## Phase 4.3 — Adjacence dynamique + optimisation
|
||||||
|
|
||||||
|
**Objectif** : Le bitmask s'adapte quand le monde change, et le pipeline est performant.
|
||||||
|
|
||||||
|
- **Recalcul incrémental** : quand un chunk est dirty, recalculer uniquement les toping instances de ce chunk + les chunks voisins (bords)
|
||||||
|
- **Frustum culling** : skip les instances hors frustum (par chunk, comme les voxels)
|
||||||
|
- **Benchmark** : mesurer le coût GPU pour 50K instances (cible < 2ms)
|
||||||
|
- **Mode debug** (F5 ?) : visualiser les bitmasks d'adjacence en couleur
|
||||||
|
- **Cross-chunk adjacency** : voisins aux frontières de chunks (comme le mesher fait déjà)
|
||||||
|
|
||||||
|
## Complexité estimée
|
||||||
|
|
||||||
|
Phase 4.1 est la plus lourde (design des structures + génération procédurale des 16 variants × 2 types). Phase 4.2 est mécanique (pipeline GPU classique). Phase 4.3 est de l'optimisation.
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
Avant d'implémenter, une étude des systèmes de meshes connectés contextuels est nécessaire (WFC, dual grid, marching squares variants, etc.) — voir `research_connected_meshes.md`.
|
||||||
252
research_connected_meshes.md
Normal file
252
research_connected_meshes.md
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
# Recherche : Systèmes de meshes connectés/contextuels pour moteurs voxel
|
||||||
|
|
||||||
|
**Objectif** : Trouver le meilleur système (bitmask, WFC, dual grid, hybride) pour placer automatiquement des meshes décoratifs adaptatifs sur un terrain voxel temps réel.
|
||||||
|
|
||||||
|
**Mots-clés** : auto-tiling 3D, connected meshes, Wave Function Collapse, dual grid, marching squares mesh variants, modular mesh kit, contextual mesh placement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Taxonomie des approches
|
||||||
|
|
||||||
|
### 1.1 Bitmask auto-tiling (lookup table)
|
||||||
|
|
||||||
|
Le plus simple : examiner les N voisins d'une cellule, construire un bitmask, indexer une table de mesh variants.
|
||||||
|
|
||||||
|
| Variante | Voisins | Cas bruts | Cas uniques (symétrie) | Meshes à créer |
|
||||||
|
|----------|---------|-----------|----------------------|----------------|
|
||||||
|
| 4-bit cardinal (2D) | 4 | 16 | ~6 | 6 + rotations |
|
||||||
|
| 8-bit blob (2D) | 8 | 256 | 47 | 47 |
|
||||||
|
| 4-bit per-face (3D) | 4 par face | 16 par face | ~6 par face | ~30 (6 faces × 5) |
|
||||||
|
| 6-bit face (3D) | 6 | 64 | ~18 | 18 |
|
||||||
|
|
||||||
|
**Avantages** : O(1), trivial à implémenter, déterministe, mise à jour instantanée.
|
||||||
|
**Inconvénients** : pas de cohérence globale, nombre de meshes croît vite avec la complexité.
|
||||||
|
|
||||||
|
**Implémentations notables** : Godot TileMap, RPG Maker, Vintage Story (JSON block models avec variantes par adjacence).
|
||||||
|
|
||||||
|
### 1.2 Wave Function Collapse (WFC)
|
||||||
|
|
||||||
|
Solveur de contraintes itératif : maintenir un ensemble de tiles possibles par cellule, collapser la cellule de moindre entropie, propager les contraintes aux voisins. Backtracking si contradiction.
|
||||||
|
|
||||||
|
**Avantages** : diversité visuelle, cohérence globale, flexible (n'importe quel tileset).
|
||||||
|
**Inconvénients** : coût CPU non-trivial, backtracking imprévisible, design de tilesets complexe, mal adapté au temps réel 60Hz.
|
||||||
|
|
||||||
|
**Implémentations notables** :
|
||||||
|
- [mxgmn/WaveFunctionCollapse](https://github.com/mxgmn/WaveFunctionCollapse) — référence originale (C#, ~12K stars)
|
||||||
|
- [BorisTheBrave/DeBroglie](https://github.com/BorisTheBrave/DeBroglie) — la lib WFC la plus complète (C#, 3D, symétries, painting)
|
||||||
|
- [Tessera](https://www.boristhebrave.com/2019/11/28/tessera-3d-tile-level-generation/) — outil Unity basé sur DeBroglie
|
||||||
|
- [marian42/wavefunctioncollapse](https://github.com/marian42/wavefunctioncollapse) — WFC 3D Unity pour bâtiments
|
||||||
|
|
||||||
|
### 1.3 Dual grid (marching squares/cubes avec meshes artisanaux)
|
||||||
|
|
||||||
|
La grille logique (données voxel) est **décalée d'une demi-cellule** par rapport à la grille visuelle. Chaque cellule visuelle examine 4 (2D) ou 8 (3D) coins de la grille logique → bitmask → lookup dans une table de mesh pré-modélisés.
|
||||||
|
|
||||||
|
C'est mathématiquement équivalent à **Marching Cubes**, mais au lieu de générer des triangles procéduralement, chaque cas est un **mesh artisanal**.
|
||||||
|
|
||||||
|
| Dimension | Coins | Cas bruts | Cas uniques (symétrie) |
|
||||||
|
|-----------|-------|-----------|----------------------|
|
||||||
|
| 2D | 4 | 16 | 6 |
|
||||||
|
| 3D | 8 | 256 | 15 (MC classique) ou ~23 (ambiguïtés) |
|
||||||
|
|
||||||
|
**Avantages** : très peu de meshes à créer (~10-15), transitions automatiquement seamless (bords partagés par construction), O(1) par cellule.
|
||||||
|
**Inconvénients** : la grille visuelle ne correspond pas à la grille logique (décalage ½ cellule → complique physique/gameplay), courbes limitées à un rayon de ½ cellule, multi-matériaux nécessite un traitement spécial.
|
||||||
|
|
||||||
|
**Sources clés** :
|
||||||
|
- [Oskar Stålberg tweet dual grid](https://x.com/OskSta/status/1448248658865049605)
|
||||||
|
- [Boris the Brave: Quarter-Tile Autotiling](https://www.boristhebrave.com/2023/05/31/quarter-tile-autotiling/) — variante sans décalage de grille
|
||||||
|
- [Wildtile](https://dualgridvoxels.docs.carbidefunction.co.uk/) — implémentation Unity 3D dual grid
|
||||||
|
- [TileMapDual](https://github.com/pablogila/TileMapDual) — implémentation Godot 2D
|
||||||
|
|
||||||
|
### 1.4 Quarter-tile / Eighth-tile (Boris the Brave)
|
||||||
|
|
||||||
|
**Alternative au dual grid sans décalage de grille**. Chaque cellule est subdivisée en 4 quadrants (2D) ou 8 octants (3D). Chaque quadrant sélectionne un sous-mesh basé sur 3 voisins (la cellule + 2 adjacents).
|
||||||
|
|
||||||
|
| Dimension | Sous-meshes uniques | Qualité visuelle |
|
||||||
|
|-----------|-------------------|-----------------|
|
||||||
|
| 2D (quarter-tile) | 5-6 | Équivalent blob (47 tiles) |
|
||||||
|
| 3D (eighth-tile) | ~10 | Équivalent MC (15 cas) |
|
||||||
|
|
||||||
|
**Avantages** : données restent sur les cellules (pas de décalage), peu de meshes à créer, qualité blob avec effort minimal.
|
||||||
|
**Inconvénients** : 4-8× plus de sous-meshes à instancier par cellule (mais GPU instancing amortit).
|
||||||
|
|
||||||
|
**Source** : https://www.boristhebrave.com/2023/05/31/quarter-tile-autotiling/
|
||||||
|
|
||||||
|
### 1.5 Marching cubes multi-matériaux (Boris the Brave)
|
||||||
|
|
||||||
|
Extension de marching squares/cubes pour N matériaux au lieu de 2 (solide/vide). **Seulement 15 cas topologiques** quel que soit le nombre de matériaux — seul le pattern de relations entre coins compte, pas les valeurs absolues.
|
||||||
|
|
||||||
|
**Insight clé** : une frontière herbe-pierre et une frontière sable-neige utilisent le **même template de mesh**, juste avec des textures différentes.
|
||||||
|
|
||||||
|
**Recommandation de l'auteur pour la 3D** : *"In 3D, I'd recommend that you just do regular marching cubes to get a single surface for all colors, then use a pixel shader to colorize that surface."* — c'est exactement notre approche Phase 3.
|
||||||
|
|
||||||
|
**Source** : https://www.boristhebrave.com/2021/12/29/2d-marching-cubes-with-multiple-colors/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Études de cas — Jeux et moteurs
|
||||||
|
|
||||||
|
### 2.1 Townscaper (Oskar Stålberg, 2020)
|
||||||
|
|
||||||
|
**Le cas de référence pour les meshes connectés procéduraux.**
|
||||||
|
|
||||||
|
**Technique** : pas du WFC standard, mais un système de contraintes déterministe sur dual grid :
|
||||||
|
- L'utilisateur peint des blocs solides/vides sur une grille irrégulière (Voronoi relaxé)
|
||||||
|
- Chaque point du dual grid examine les 8 cellules environnantes → bitmask → sélection de module
|
||||||
|
- ~80 modules pré-modélisés couvrent toutes les configurations
|
||||||
|
- **Pas de backtracking** : chaque configuration est valide par design ("every configuration must be valid")
|
||||||
|
- **Pas de randomness** : sélection déterministe (1 seul module valide par état)
|
||||||
|
|
||||||
|
**Différences clés vs WFC standard** :
|
||||||
|
|
||||||
|
| Aspect | WFC standard | Townscaper |
|
||||||
|
|--------|-------------|------------|
|
||||||
|
| Contradictions | Possibles → backtracking | Impossibles par design |
|
||||||
|
| Propagation | Globale | Locale uniquement |
|
||||||
|
| Sélection | Aléatoire parmi les valides | Déterministe (1 module/état) |
|
||||||
|
| Modules | 100+ | ~80 (~15-20 canoniques) |
|
||||||
|
| Performance | Variable (backtracking) | O(1) par cellule |
|
||||||
|
| Grille | Régulière | Irrégulière (Voronoi) |
|
||||||
|
|
||||||
|
**Principes réutilisables** :
|
||||||
|
1. Bitmask → lookup déterministe (pas besoin de solveur)
|
||||||
|
2. Exploiter les symétries (rotation/reflection) : 256 → ~15-20 cas uniques
|
||||||
|
3. Mise à jour locale uniquement (le voxel modifié + ses voisins immédiats)
|
||||||
|
4. Couleurs/matériaux = variations de texture sur les mêmes modules, pas des modules séparés
|
||||||
|
|
||||||
|
**Sources** :
|
||||||
|
- [How Townscaper Works (gamedeveloper.com)](https://www.gamedeveloper.com/game-platforms/how-townscaper-works-a-story-four-games-in-the-making)
|
||||||
|
- [Oskar Stålberg talk at Konsoll](https://konsoll.org/speaker/oskar-stalberg/)
|
||||||
|
- Threads Twitter/X avec diagrammes animés du dual grid
|
||||||
|
|
||||||
|
### 2.2 Wonderbox (Aquiris, Amilton Diesel)
|
||||||
|
|
||||||
|
**Statut** : pas de documentation technique publique trouvée. Amilton Diesel (Technical Art Director chez Aquiris, maintenant chez Epic Games) n'a pas publié de breakdown technique indexable. Le style visuel (blocs modulaires colorés en diorama) suggère un système de modules connectés, mais les détails techniques restent propriétaires.
|
||||||
|
|
||||||
|
**Sources** :
|
||||||
|
- [Aquiris press page](https://press.aquiris.com.br/games/wonderbox)
|
||||||
|
- [Wonderbox Behance gallery](https://www.behance.net/gallery/116125497/AQUIRIS-WONDERBOX)
|
||||||
|
|
||||||
|
### 2.3 Enshrouded (Keen Games, Holistic Engine)
|
||||||
|
|
||||||
|
**Technique** : moteur voxel propriétaire avec terrain SDF (signed distance field) + isosurface extraction lisse. Fondamentalement différent d'un moteur blocky — les voxels stockent des distances, pas des matériaux binaires.
|
||||||
|
|
||||||
|
**Décoration** : meshes placés sur la surface du terrain (vegetation, rochers) séparément du mesh voxel. Le système est hand-crafted par les artistes avec outils internes + Substance Painter.
|
||||||
|
|
||||||
|
**Pertinence** : limitée pour notre cas (terrain blocky, pas SDF). Le pattern architectural "terrain voxel + meshes décoratifs instanciés séparément" est par contre le même que notre Phase 4.
|
||||||
|
|
||||||
|
### 2.4 Portal Knights (Keen Games)
|
||||||
|
|
||||||
|
**Technique** : rendu blocky standard (cubes). Pas de système de décoration par adjacence documenté. Pertinence limitée.
|
||||||
|
|
||||||
|
### 2.5 Vintage Story (2024)
|
||||||
|
|
||||||
|
**Technique** : block models définis en JSON avec variantes sélectionnées par règles d'adjacence. Chaque bloc peut avoir plusieurs mesh variants, la sélection dépend du contexte (voisins, face exposée). Système data-driven (pas de code par type de bloc).
|
||||||
|
|
||||||
|
**Pertinence** : bon modèle pour un système de toping data-driven, mais le code source n'est pas ouvert pour la partie rendering.
|
||||||
|
|
||||||
|
### 2.6 Veloren (open source, Rust)
|
||||||
|
|
||||||
|
**Technique** : voxel RPG open source avec système de décoration combinant structures pré-créées + scatter-based placement. Codebase bien documentée sur [gitlab.com/veloren/veloren](https://gitlab.com/veloren/veloren).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. "Driven WFC" — L'approche hybride (Boris the Brave)
|
||||||
|
|
||||||
|
L'article le plus pertinent pour notre cas : [Driven WFC](https://www.boristhebrave.com/2021/06/06/driven-wavefunctioncollapse/).
|
||||||
|
|
||||||
|
**Concept** : utiliser WFC comme "remplisseur de détails", pas comme générateur de structure. Un système externe (utilisateur, heightmap, algorithme) détermine la macro-structure ; WFC sélectionne les variantes compatibles dans les contraintes.
|
||||||
|
|
||||||
|
**Application au toping** :
|
||||||
|
1. Le monde voxel détermine la macro-structure (quels blocs existent, quels matériaux)
|
||||||
|
2. Le bitmask d'adjacence **contraint** quelles variantes de toping sont valides
|
||||||
|
3. Optionnellement, WFC **sélectionne** parmi les variantes valides pour de la variété visuelle
|
||||||
|
|
||||||
|
C'est exactement le workflow de Townscaper : l'utilisateur peint la structure → le système contraint les modules possibles → sélection déterministe (ou aléatoire pour plus de variété).
|
||||||
|
|
||||||
|
**Pour notre prototype** : commencer par la sélection déterministe (bitmask → lookup), ajouter la variété WFC plus tard si nécessaire.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Articles techniques clés (Boris the Brave)
|
||||||
|
|
||||||
|
| Article | URL | Pertinence |
|
||||||
|
|---------|-----|-----------|
|
||||||
|
| **Quarter-Tile Autotiling** | https://www.boristhebrave.com/2023/05/31/quarter-tile-autotiling/ | ⭐⭐⭐ Alternative au dual grid, ~5-6 meshes pour qualité blob |
|
||||||
|
| **Beyond Basic Autotiling** | https://www.boristhebrave.com/2021/09/12/beyond-basic-autotiling/ | ⭐⭐⭐ Multi-matériaux, composition par couches |
|
||||||
|
| **Multi-Color Marching Squares** | https://www.boristhebrave.com/2021/12/29/2d-marching-cubes-with-multiple-colors/ | ⭐⭐⭐ 15 cas quel que soit le nombre de matériaux |
|
||||||
|
| **Driven WFC** | https://www.boristhebrave.com/2021/06/06/driven-wavefunctioncollapse/ | ⭐⭐⭐ WFC comme détail-filler, pas générateur |
|
||||||
|
| **Classification of Tilesets** | https://www.boristhebrave.com/2021/11/14/classification-of-tilesets/ | ⭐⭐ Taxonomie formelle, aide au design |
|
||||||
|
| **WFC Explained** | https://www.boristhebrave.com/2020/04/13/wave-function-collapse-explained/ | ⭐⭐ Comprendre AC-4, support counting |
|
||||||
|
| **Editable WFC** | https://www.boristhebrave.com/2022/04/25/editable-wfc/ | ⭐⭐ Re-solving local pour édition temps réel |
|
||||||
|
| **Constraint-Based Tile Generators** | https://www.boristhebrave.com/2021/10/31/constraint-based-tile-generators/ | ⭐⭐ Comparaison WFC vs Model Synthesis |
|
||||||
|
| **Marching Cubes Tutorials (2D/3D)** | https://www.boristhebrave.com/2018/04/15/marching-cubes-tutorial/ | ⭐ Fondamentaux dual grid |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Comparaison pour notre cas d'usage
|
||||||
|
|
||||||
|
Notre besoin : placer des meshes décoratifs 3D sur les faces exposées de voxels dans un moteur temps réel (60+ FPS, monde dynamique animé).
|
||||||
|
|
||||||
|
| Critère | Bitmask 4-bit | Quarter-tile | Dual grid 3D | WFC | Driven WFC |
|
||||||
|
|---------|--------------|-------------|-------------|-----|-----------|
|
||||||
|
| **Complexité code** | Très faible | Faible | Moyen | Élevé | Moyen |
|
||||||
|
| **Meshes à créer** | 16 (~6 uniques) | 5-6 | 10-15 | Variable | Variable |
|
||||||
|
| **Qualité visuelle** | Basique | Blob-quality | Smooth transitions | Excellent | Très bon |
|
||||||
|
| **Performance** | O(1) lookup | O(1) lookup × 4 | O(1) lookup | O(N) solver | O(1) + O(N) init |
|
||||||
|
| **Multi-matériaux** | N² explosion | Couches | 15 cas pour N mat | Naturel | Naturel |
|
||||||
|
| **Mise à jour dynamique** | Instantanée | Instantanée | Instantanée | Re-solve local | Re-solve local |
|
||||||
|
| **Cohérence globale** | Non | Non | Non | Oui | Partielle |
|
||||||
|
| **Adapté à 60Hz anim** | ✅ | ✅ | ✅ | ❌ | ⚠️ (init seul) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Recommandation
|
||||||
|
|
||||||
|
### Approche recommandée : Bitmask 4-bit + Quarter-tile optionnel
|
||||||
|
|
||||||
|
**Phase 4.1** : Bitmask 4-bit simple (16 cas, ~6 uniques avec symétrie)
|
||||||
|
- Implémentation triviale, validé rapidement
|
||||||
|
- Suffisant pour le prototype (rebords, bordures)
|
||||||
|
- Le plan existant dans `plan_phase4.md` est adapté
|
||||||
|
|
||||||
|
**Phase 4.2** (optionnel) : Upgrade vers Quarter-tile
|
||||||
|
- Même données voxel, meilleure qualité visuelle
|
||||||
|
- Chaque face subdivisée en 4 quadrants, ~5-6 sous-meshes
|
||||||
|
- Qualité visuelle blob (47 tiles) avec seulement 5-6 meshes par type de toping
|
||||||
|
- Référence : Boris the Brave quarter-tile article
|
||||||
|
|
||||||
|
**Non recommandé pour le prototype** :
|
||||||
|
- WFC complet : trop complexe, mal adapté à l'animation 60Hz
|
||||||
|
- Dual grid 3D : nécessiterait de refactorer le mesher (décalage de ½ cellule)
|
||||||
|
- Socket-based modules : overhead d'authoring trop important
|
||||||
|
|
||||||
|
### Insight clé de Townscaper applicable directement
|
||||||
|
|
||||||
|
**"Every configuration must be valid"** — concevoir les meshes de toping pour que TOUTE combinaison de bitmask produise un résultat visuellement acceptable. Pas de fallback, pas de cas d'erreur. Chaque bitmask (0-15) a un mesh dédié, même si certains sont dégénérés (mesh vide pour bitmask 0 = pas de voisins du bon type).
|
||||||
|
|
||||||
|
### Multi-matériaux via composition par couches
|
||||||
|
|
||||||
|
Au lieu de créer des meshes séparés pour chaque paire de matériaux (explosion N²), utiliser des **meshes de bordure composés par couche** :
|
||||||
|
- Un set de meshes "bordure" par matériau qui se compose par-dessus le quad voxel de base
|
||||||
|
- La texture/couleur vient du matériau, pas du mesh
|
||||||
|
- C'est l'approche "Beyond Basic Autotiling" de Boris the Brave
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sources complètes
|
||||||
|
|
||||||
|
### Articles techniques
|
||||||
|
- Boris the Brave — https://www.boristhebrave.com/ (voir tableau section 4)
|
||||||
|
- How Townscaper Works — https://www.gamedeveloper.com/game-platforms/how-townscaper-works-a-story-four-games-in-the-making
|
||||||
|
- Wildtile docs — https://dualgridvoxels.docs.carbidefunction.co.uk/
|
||||||
|
- Transvoxel — https://transvoxel.org/
|
||||||
|
|
||||||
|
### Repos open source
|
||||||
|
- [mxgmn/WaveFunctionCollapse](https://github.com/mxgmn/WaveFunctionCollapse) — WFC référence (~12K stars)
|
||||||
|
- [BorisTheBrave/DeBroglie](https://github.com/BorisTheBrave/DeBroglie) — WFC lib C# la plus complète
|
||||||
|
- [TileMapDual](https://github.com/pablogila/TileMapDual) — dual grid 2D Godot
|
||||||
|
- [Veloren](https://gitlab.com/veloren/veloren) — voxel RPG open source Rust
|
||||||
|
|
||||||
|
### Talks et vidéos
|
||||||
|
- Oskar Stålberg talks (YouTube : "Oskar Stålberg Townscaper", "Oskar Stålberg procedural")
|
||||||
|
- Sebastian Lague voxel terrain (YouTube, 2023-2024)
|
||||||
285
src/voxel/TopingSystem.cpp
Normal file
285
src/voxel/TopingSystem.cpp
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
#include "TopingSystem.h"
|
||||||
|
#include "VoxelWorld.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
// ── Edge definitions for +Y face ────────────────────────────────
|
||||||
|
// Each edge sits on one side of the unit square [0,1]² (the XZ plane at y=1).
|
||||||
|
// The bevel strip runs along the edge, with a wedge cross-section
|
||||||
|
// rising from the voxel face (y=1) to a peak (y=1+h) and sloping
|
||||||
|
// inward by width w.
|
||||||
|
//
|
||||||
|
// Cross-section (looking along the strip):
|
||||||
|
//
|
||||||
|
// peak (edge, 1+h)
|
||||||
|
// /|
|
||||||
|
// / |
|
||||||
|
// / | slope face (visible from above)
|
||||||
|
// / |
|
||||||
|
// / | outer wall (visible from the side)
|
||||||
|
// / |
|
||||||
|
// inner outer
|
||||||
|
// (1-w,1) (edge,1)
|
||||||
|
//
|
||||||
|
struct EdgeDef {
|
||||||
|
float sx, sz; // strip start point (on the voxel face)
|
||||||
|
float ex, ez; // strip end point
|
||||||
|
float ix, iz; // inward direction (unit, perpendicular to strip)
|
||||||
|
float nx, nz; // outer wall normal (points outward)
|
||||||
|
};
|
||||||
|
|
||||||
|
static const EdgeDef kEdges[4] = {
|
||||||
|
// sx sz ex ez ix iz nx nz
|
||||||
|
{ 1.0f, 0.0f, 1.0f, 1.0f,-1.0f, 0.0f, 1.0f, 0.0f }, // bit 0: +X edge
|
||||||
|
{ 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,-1.0f, 0.0f }, // bit 1: -X edge
|
||||||
|
{ 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,-1.0f, 0.0f, 1.0f }, // bit 2: +Z edge
|
||||||
|
{ 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,-1.0f }, // bit 3: -Z edge
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Helper: emit one triangle (3 vertices, shared normal) ───────
|
||||||
|
static void emitTri(std::vector<TopingVertex>& v,
|
||||||
|
float ax, float ay, float az,
|
||||||
|
float bx, float by, float bz,
|
||||||
|
float cx, float cy, float cz,
|
||||||
|
float nx, float ny, float nz)
|
||||||
|
{
|
||||||
|
v.push_back({ ax, ay, az, nx, ny, nz });
|
||||||
|
v.push_back({ bx, by, bz, nx, ny, nz });
|
||||||
|
v.push_back({ cx, cy, cz, nx, ny, nz });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
// TopingSystem implementation
|
||||||
|
// ═════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void TopingSystem::initialize() {
|
||||||
|
registerDefs();
|
||||||
|
generateMeshes();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Register toping types ───────────────────────────────────────
|
||||||
|
void TopingSystem::registerDefs() {
|
||||||
|
defs_.clear();
|
||||||
|
|
||||||
|
// Type 0: Stone bevel — clean angular ridge along open edges
|
||||||
|
// Applied to stone (materialID=3), face +Y
|
||||||
|
{
|
||||||
|
TopingDef def{};
|
||||||
|
def.materialID = 3;
|
||||||
|
def.face = FACE_POS_Y;
|
||||||
|
def.height = 0.06f; // subtle bevel
|
||||||
|
def.width = 0.12f;
|
||||||
|
def.segments = 1; // single smooth segment per edge
|
||||||
|
defs_.push_back(def);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type 1: Grass edge — organic bumpy tufts along open edges
|
||||||
|
// Applied to grass (materialID=1), face +Y
|
||||||
|
{
|
||||||
|
TopingDef def{};
|
||||||
|
def.materialID = 1;
|
||||||
|
def.face = FACE_POS_Y;
|
||||||
|
def.height = 0.12f; // taller, more visible
|
||||||
|
def.width = 0.18f;
|
||||||
|
def.segments = 4; // subdivided for bumpy profile
|
||||||
|
defs_.push_back(def);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generate all 16 mesh variants per def ───────────────────────
|
||||||
|
void TopingSystem::generateMeshes() {
|
||||||
|
vertices_.clear();
|
||||||
|
for (auto& def : defs_) {
|
||||||
|
for (int bitmask = 0; bitmask < 16; bitmask++) {
|
||||||
|
generateVariant(def, (uint8_t)bitmask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Generate mesh for one (def, bitmask) pair ───────────────────
|
||||||
|
// An UNSET bit means the edge is open → add bevel strip.
|
||||||
|
// A SET bit means a neighbor is present → no bevel (toping connects).
|
||||||
|
void TopingSystem::generateVariant(TopingDef& def, uint8_t bitmask) {
|
||||||
|
const uint32_t startOffset = (uint32_t)vertices_.size();
|
||||||
|
|
||||||
|
for (int edge = 0; edge < 4; edge++) {
|
||||||
|
if (bitmask & (1 << edge)) continue; // neighbor present → skip
|
||||||
|
|
||||||
|
const EdgeDef& e = kEdges[edge];
|
||||||
|
const float w = def.width;
|
||||||
|
const int segs = def.segments;
|
||||||
|
|
||||||
|
// Build height profile along the strip
|
||||||
|
std::vector<float> heights(segs + 1);
|
||||||
|
if (segs <= 1) {
|
||||||
|
// Stone: constant height (smooth bevel)
|
||||||
|
heights[0] = def.height;
|
||||||
|
heights[1] = def.height;
|
||||||
|
} else {
|
||||||
|
// Grass: sinusoidal bumps, phase offset per edge for variety
|
||||||
|
for (int j = 0; j <= segs; j++) {
|
||||||
|
float t = (float)j / segs;
|
||||||
|
float bump = sinf((t * 2.5f + edge * 0.31f) * 3.14159f);
|
||||||
|
heights[j] = def.height * (0.5f + 0.5f * std::abs(bump));
|
||||||
|
if (heights[j] < 0.02f) heights[j] = 0.02f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip direction
|
||||||
|
const float dx = e.ex - e.sx;
|
||||||
|
const float dz = e.ez - e.sz;
|
||||||
|
|
||||||
|
for (int i = 0; i < segs; i++) {
|
||||||
|
const float t0 = (float)i / segs;
|
||||||
|
const float t1 = (float)(i + 1) / segs;
|
||||||
|
const float h0 = heights[i];
|
||||||
|
const float h1 = heights[i + 1];
|
||||||
|
|
||||||
|
// Points at t0 along the strip (all at y=1, the voxel face)
|
||||||
|
const float x0 = e.sx + t0 * dx;
|
||||||
|
const float z0 = e.sz + t0 * dz;
|
||||||
|
// Points at t1
|
||||||
|
const float x1 = e.sx + t1 * dx;
|
||||||
|
const float z1 = e.sz + t1 * dz;
|
||||||
|
|
||||||
|
// Cross-section at t0:
|
||||||
|
// outerBot = (x0, 1, z0) — on the voxel face, at the edge
|
||||||
|
// peak = (x0, 1+h0, z0) — raised at the edge
|
||||||
|
// inner = (x0+w*ix, 1, z0+w*iz) — on the face, inward
|
||||||
|
const float pk0y = 1.0f + h0;
|
||||||
|
const float in0x = x0 + w * e.ix;
|
||||||
|
const float in0z = z0 + w * e.iz;
|
||||||
|
|
||||||
|
// Cross-section at t1:
|
||||||
|
const float pk1y = 1.0f + h1;
|
||||||
|
const float in1x = x1 + w * e.ix;
|
||||||
|
const float in1z = z1 + w * e.iz;
|
||||||
|
|
||||||
|
// ── Outer wall face (vertical, facing outward) ──────
|
||||||
|
// Normal points outward: (nx, 0, nz)
|
||||||
|
// CW winding from outside: peak0→outerBot0→peak1, peak1→outerBot0→outerBot1
|
||||||
|
emitTri(vertices_,
|
||||||
|
x0, pk0y, z0, x0, 1.0f, z0, x1, pk1y, z1,
|
||||||
|
e.nx, 0.0f, e.nz);
|
||||||
|
emitTri(vertices_,
|
||||||
|
x1, pk1y, z1, x0, 1.0f, z0, x1, 1.0f, z1,
|
||||||
|
e.nx, 0.0f, e.nz);
|
||||||
|
|
||||||
|
// ── Slope face (from peak down to inner edge) ───────
|
||||||
|
// Compute normal via cross product of two edge vectors.
|
||||||
|
// v_along_slope = inner - peak = (w*ix, -h, w*iz) at midpoint
|
||||||
|
// v_along_strip = strip direction = (dx/segs, 0, dz/segs)
|
||||||
|
const float avgH = (h0 + h1) * 0.5f;
|
||||||
|
const float asx = w * e.ix;
|
||||||
|
const float asy = -avgH;
|
||||||
|
const float asz = w * e.iz;
|
||||||
|
const float adx = dx / segs;
|
||||||
|
const float adz = dz / segs;
|
||||||
|
|
||||||
|
// normal = cross(v_along_strip, v_along_slope)
|
||||||
|
float cnx = /* 0*asz - adz*asy = */ adz * avgH;
|
||||||
|
float cny = /* adz*asx - adx*asz = */ adz * asx - adx * asz;
|
||||||
|
float cnz = /* adx*asy - 0*asx = */ -adx * avgH;
|
||||||
|
|
||||||
|
float clen = sqrtf(cnx * cnx + cny * cny + cnz * cnz);
|
||||||
|
if (clen > 0.0001f) {
|
||||||
|
cnx /= clen; cny /= clen; cnz /= clen;
|
||||||
|
} else {
|
||||||
|
cnx = 0.0f; cny = 1.0f; cnz = 0.0f;
|
||||||
|
}
|
||||||
|
// Ensure normal points upward (slope is visible from above)
|
||||||
|
if (cny < 0.0f) { cnx = -cnx; cny = -cny; cnz = -cnz; }
|
||||||
|
|
||||||
|
// CW winding from above: peak0→peak1→inner0, inner0→peak1→inner1
|
||||||
|
emitTri(vertices_,
|
||||||
|
x0, pk0y, z0, x1, pk1y, z1, in0x, 1.0f, in0z,
|
||||||
|
cnx, cny, cnz);
|
||||||
|
emitTri(vertices_,
|
||||||
|
in0x, 1.0f, in0z, x1, pk1y, z1, in1x, 1.0f, in1z,
|
||||||
|
cnx, cny, cnz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t count = (uint32_t)vertices_.size() - startOffset;
|
||||||
|
def.variants[bitmask] = { startOffset, count };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Collect toping instances from the world ─────────────────────
|
||||||
|
// Scans every exposed voxel face that matches a registered TopingDef,
|
||||||
|
// computes the 4-bit adjacency bitmask, and emits a TopingInstance.
|
||||||
|
//
|
||||||
|
// Currently only supports FACE_POS_Y (top face). For other faces,
|
||||||
|
// the adjacency directions would need to be adapted to the face plane.
|
||||||
|
void TopingSystem::collectInstances(const VoxelWorld& world) {
|
||||||
|
instances_.clear();
|
||||||
|
|
||||||
|
// Quick lookup: material → toping def index (-1 if none)
|
||||||
|
int8_t matToDef[256];
|
||||||
|
memset(matToDef, -1, sizeof(matToDef));
|
||||||
|
for (size_t i = 0; i < defs_.size(); i++) {
|
||||||
|
matToDef[defs_[i].materialID] = (int8_t)i;
|
||||||
|
}
|
||||||
|
|
||||||
|
world.forEachChunk([&](const ChunkPos& cpos, const Chunk& chunk) {
|
||||||
|
for (int z = 0; z < CHUNK_SIZE; z++) {
|
||||||
|
for (int y = 0; y < CHUNK_SIZE; y++) {
|
||||||
|
for (int x = 0; x < CHUNK_SIZE; x++) {
|
||||||
|
const VoxelData& v = chunk.at(x, y, z);
|
||||||
|
if (v.isEmpty()) continue;
|
||||||
|
|
||||||
|
const uint8_t mat = v.getMaterialID();
|
||||||
|
const int8_t defIdx = matToDef[mat];
|
||||||
|
if (defIdx < 0) continue;
|
||||||
|
|
||||||
|
const TopingDef& def = defs_[defIdx];
|
||||||
|
|
||||||
|
// World coordinates
|
||||||
|
const int wx = cpos.x * CHUNK_SIZE + x;
|
||||||
|
const int wy = cpos.y * CHUNK_SIZE + y;
|
||||||
|
const int wz = cpos.z * CHUNK_SIZE + z;
|
||||||
|
|
||||||
|
// Check if the target face is exposed (neighbor in face direction is empty)
|
||||||
|
// Currently only FACE_POS_Y is supported
|
||||||
|
if (def.face == FACE_POS_Y) {
|
||||||
|
if (!world.getVoxel(wx, wy + 1, wz).isEmpty()) continue;
|
||||||
|
|
||||||
|
// Face exposed. Compute 4-bit adjacency bitmask.
|
||||||
|
// A neighbor contributes if: same material AND its +Y face is also exposed.
|
||||||
|
uint8_t adj = 0;
|
||||||
|
|
||||||
|
// bit 0: +X
|
||||||
|
if (world.getVoxel(wx + 1, wy, wz).getMaterialID() == mat &&
|
||||||
|
world.getVoxel(wx + 1, wy + 1, wz).isEmpty())
|
||||||
|
adj |= 1;
|
||||||
|
|
||||||
|
// bit 1: -X
|
||||||
|
if (world.getVoxel(wx - 1, wy, wz).getMaterialID() == mat &&
|
||||||
|
world.getVoxel(wx - 1, wy + 1, wz).isEmpty())
|
||||||
|
adj |= 2;
|
||||||
|
|
||||||
|
// bit 2: +Z
|
||||||
|
if (world.getVoxel(wx, wy, wz + 1).getMaterialID() == mat &&
|
||||||
|
world.getVoxel(wx, wy + 1, wz + 1).isEmpty())
|
||||||
|
adj |= 4;
|
||||||
|
|
||||||
|
// bit 3: -Z
|
||||||
|
if (world.getVoxel(wx, wy, wz - 1).getMaterialID() == mat &&
|
||||||
|
world.getVoxel(wx, wy + 1, wz - 1).isEmpty())
|
||||||
|
adj |= 8;
|
||||||
|
|
||||||
|
instances_.push_back({
|
||||||
|
(float)wx, (float)wy, (float)wz,
|
||||||
|
(uint16_t)defIdx, adj
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO: support other face directions (FACE_NEG_Y, FACE_POS_X, etc.)
|
||||||
|
// Each face direction needs different adjacency directions in its plane.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
81
src/voxel/TopingSystem.h
Normal file
81
src/voxel/TopingSystem.h
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
#pragma once
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
class VoxelWorld;
|
||||||
|
|
||||||
|
// ── Toping mesh vertex (position + normal) ──────────────────────
|
||||||
|
struct TopingVertex {
|
||||||
|
float px, py, pz; // position in voxel-local space [0,1]³
|
||||||
|
float nx, ny, nz; // normal
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Reference to a slice of vertices in the shared pool ─────────
|
||||||
|
struct MeshSlice {
|
||||||
|
uint32_t offset; // first vertex index in TopingSystem::vertices_
|
||||||
|
uint32_t count; // number of vertices (multiple of 3)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Toping definition (one per decorative type) ─────────────────
|
||||||
|
// Each def targets a specific material + face, and provides 16
|
||||||
|
// mesh variants indexed by a 4-bit adjacency bitmask.
|
||||||
|
//
|
||||||
|
// Adjacency bitmask (for FACE_POS_Y):
|
||||||
|
// bit 0 = +X neighbor has same material AND exposed +Y face
|
||||||
|
// bit 1 = -X neighbor has same material AND exposed +Y face
|
||||||
|
// bit 2 = +Z neighbor has same material AND exposed +Y face
|
||||||
|
// bit 3 = -Z neighbor has same material AND exposed +Y face
|
||||||
|
//
|
||||||
|
// A SET bit means the neighbor IS present → no bevel on that edge.
|
||||||
|
// An UNSET bit means the edge is "open" → bevel strip generated.
|
||||||
|
struct TopingDef {
|
||||||
|
uint8_t materialID; // voxel material that triggers this toping
|
||||||
|
uint8_t face; // Face enum (FACE_POS_Y, etc.)
|
||||||
|
float height; // bevel peak height (voxel units)
|
||||||
|
float width; // bevel inward extent (voxel units)
|
||||||
|
int segments; // subdivisions per edge strip (1=smooth, 3+=bumpy)
|
||||||
|
MeshSlice variants[16]; // indexed by adjacency bitmask
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Placed toping instance ──────────────────────────────────────
|
||||||
|
struct TopingInstance {
|
||||||
|
float wx, wy, wz; // world position of the source voxel (integer coords)
|
||||||
|
uint16_t topingType; // index into TopingSystem::defs_
|
||||||
|
uint16_t variant; // adjacency bitmask (0-15)
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Toping System ───────────────────────────────────────────────
|
||||||
|
// Manages toping definitions, procedural mesh generation, and
|
||||||
|
// instance collection from the voxel world.
|
||||||
|
//
|
||||||
|
// Phase 4.1: data structures + mesh gen + collection (no rendering).
|
||||||
|
// Phase 4.2 will upload vertices_ to a GPU vertex buffer and
|
||||||
|
// instances_ to an instance buffer for instanced drawing.
|
||||||
|
class TopingSystem {
|
||||||
|
public:
|
||||||
|
void initialize();
|
||||||
|
void collectInstances(const VoxelWorld& world);
|
||||||
|
|
||||||
|
// Accessors for Phase 4.2 GPU upload
|
||||||
|
const std::vector<TopingVertex>& getVertices() const { return vertices_; }
|
||||||
|
const std::vector<TopingInstance>& getInstances() const { return instances_; }
|
||||||
|
const std::vector<TopingDef>& getDefs() const { return defs_; }
|
||||||
|
|
||||||
|
size_t getDefCount() const { return defs_.size(); }
|
||||||
|
size_t getInstanceCount() const { return instances_.size(); }
|
||||||
|
size_t getVertexCount() const { return vertices_.size(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void registerDefs();
|
||||||
|
void generateMeshes();
|
||||||
|
void generateVariant(TopingDef& def, uint8_t bitmask);
|
||||||
|
|
||||||
|
std::vector<TopingDef> defs_;
|
||||||
|
std::vector<TopingVertex> vertices_; // shared vertex pool for all types/variants
|
||||||
|
std::vector<TopingInstance> instances_; // collected per frame/update
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
|
|
@ -1151,6 +1151,20 @@ void VoxelRenderPath::Start() {
|
||||||
if (renderer.isInitialized()) {
|
if (renderer.isInitialized()) {
|
||||||
renderer.updateMeshes(world);
|
renderer.updateMeshes(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 4: Initialize toping system and collect instances
|
||||||
|
topingSystem.initialize();
|
||||||
|
topingSystem.collectInstances(world);
|
||||||
|
{
|
||||||
|
char msg[256];
|
||||||
|
snprintf(msg, sizeof(msg),
|
||||||
|
"TopingSystem: %zu defs, %zu vertices, %zu instances",
|
||||||
|
topingSystem.getDefCount(),
|
||||||
|
topingSystem.getVertexCount(),
|
||||||
|
topingSystem.getInstanceCount());
|
||||||
|
wi::backlog::post(msg);
|
||||||
|
}
|
||||||
|
|
||||||
worldGenerated_ = true;
|
worldGenerated_ = true;
|
||||||
|
|
||||||
setAO(AO_DISABLED);
|
setAO(AO_DISABLED);
|
||||||
|
|
@ -1396,7 +1410,7 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
|
||||||
char dtStr[16];
|
char dtStr[16];
|
||||||
snprintf(dtStr, sizeof(dtStr), "%.2f", lastDt_ * 1000.0f);
|
snprintf(dtStr, sizeof(dtStr), "%.2f", lastDt_ * 1000.0f);
|
||||||
|
|
||||||
std::string stats = "BVLE Voxel Engine (Phase 3 — Texture Blending)\n";
|
std::string stats = "BVLE Voxel Engine (Phase 4 — Toping)\n";
|
||||||
stats += "FPS: " + std::string(fpsStr) + " (" + std::string(dtStr) + " ms)\n";
|
stats += "FPS: " + std::string(fpsStr) + " (" + std::string(dtStr) + " ms)\n";
|
||||||
if (debugMode) {
|
if (debugMode) {
|
||||||
stats += "=== DEBUG FACE MODE ===\n";
|
stats += "=== DEBUG FACE MODE ===\n";
|
||||||
|
|
@ -1425,6 +1439,9 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
|
||||||
snprintf(drawStr, sizeof(drawStr), "%.3f", renderer.getGpuDrawTimeMs());
|
snprintf(drawStr, sizeof(drawStr), "%.3f", renderer.getGpuDrawTimeMs());
|
||||||
stats += "GPU Cull: " + std::string(cullStr) + " ms | Draw: " + std::string(drawStr) + " ms\n";
|
stats += "GPU Cull: " + std::string(cullStr) + " ms | Draw: " + std::string(drawStr) + " ms\n";
|
||||||
}
|
}
|
||||||
|
stats += "Topings: " + std::to_string(topingSystem.getInstanceCount())
|
||||||
|
+ " instances (" + std::to_string(topingSystem.getDefCount()) + " types, "
|
||||||
|
+ std::to_string(topingSystem.getVertexCount()) + " verts)\n";
|
||||||
stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n";
|
stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n";
|
||||||
stats += "F2: console | F3: anim [" + std::string(animatedTerrain_ ? "ON" : "OFF")
|
stats += "F2: console | F3: anim [" + std::string(animatedTerrain_ ? "ON" : "OFF")
|
||||||
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF") + "]";
|
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF") + "]";
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "VoxelWorld.h"
|
#include "VoxelWorld.h"
|
||||||
#include "VoxelMesher.h"
|
#include "VoxelMesher.h"
|
||||||
|
#include "TopingSystem.h"
|
||||||
#include "WickedEngine.h"
|
#include "WickedEngine.h"
|
||||||
|
|
||||||
namespace voxel {
|
namespace voxel {
|
||||||
|
|
@ -192,6 +193,7 @@ class VoxelRenderPath : public wi::RenderPath3D {
|
||||||
public:
|
public:
|
||||||
VoxelWorld world;
|
VoxelWorld world;
|
||||||
VoxelRenderer renderer;
|
VoxelRenderer renderer;
|
||||||
|
TopingSystem topingSystem;
|
||||||
|
|
||||||
bool debugMode = false;
|
bool debugMode = false;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue