Phase 2: GPU-driven voxel rendering pipeline
Mega-buffer architecture replacing per-chunk GPU buffers: - Single StructuredBuffer<PackedQuad> for all chunks (2M quads, 16 MB) - StructuredBuffer<GPUChunkInfo> with per-chunk metadata (position, quad offsets, face groups) - VS reads chunk info via push constants (b999) for driver-safe chunk indexing - CPU frustum culling with wi::primitive::Frustum + AABB per chunk - Quads sorted by face direction in greedy mesher (faceOffsets/faceCounts) - GPU frustum + backface cull compute shader (voxelCullCS.hlsl) - GPU binary mesher compute shader baseline (voxelMeshCS.hlsl) - Indirect draw buffers and timestamp query infrastructure - README with build instructions and project architecture
This commit is contained in:
commit
5f346bb14a
18 changed files with 2697 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Build output
|
||||||
|
build/
|
||||||
|
out/
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Compiled shaders (regenerated at runtime by DXC)
|
||||||
|
shaders/hlsl6/
|
||||||
|
shaders/spirv/
|
||||||
|
|
||||||
|
# Wicked Engine submodule (cloned separately)
|
||||||
|
engine/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vs/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# Crash dumps & logs
|
||||||
|
*.dmp
|
||||||
|
bvle_crash.log
|
||||||
|
log.txt
|
||||||
|
|
||||||
|
# OS
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
278
CLAUDE.md
Normal file
278
CLAUDE.md
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
**Backlog Wicked** : `wi::backlog::SetLogFile("bvle_backlog.txt")` redirige les logs vers un fichier. Touche `~` (tilde) pour toggler la console à l'écran.
|
||||||
|
|
||||||
|
**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
|
||||||
51
CMakeLists.txt
Normal file
51
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
cmake_minimum_required(VERSION 3.19)
|
||||||
|
project(BVLEVoxels LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
# Wicked Engine options - disable what we don't need
|
||||||
|
set(WICKED_EDITOR OFF CACHE BOOL "" FORCE)
|
||||||
|
set(WICKED_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(WICKED_IMGUI_EXAMPLE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(WICKED_WINDOWS_TEMPLATE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(WICKED_LINUX_TEMPLATE OFF CACHE BOOL "" FORCE)
|
||||||
|
set(WICKED_ENABLE_SYMLINKS OFF CACHE BOOL "" FORCE)
|
||||||
|
|
||||||
|
add_subdirectory(engine)
|
||||||
|
|
||||||
|
# ── Voxel Engine Library ──────────────────────────────────────────
|
||||||
|
file(GLOB_RECURSE VOXEL_SOURCES src/voxel/*.cpp src/voxel/*.h)
|
||||||
|
add_library(VoxelEngine STATIC ${VOXEL_SOURCES})
|
||||||
|
target_include_directories(VoxelEngine PUBLIC src)
|
||||||
|
target_link_libraries(VoxelEngine PUBLIC WickedEngine)
|
||||||
|
|
||||||
|
# ── Main Application ─────────────────────────────────────────────
|
||||||
|
file(GLOB APP_SOURCES src/app/*.cpp src/app/*.h)
|
||||||
|
add_executable(BVLEVoxels WIN32 ${APP_SOURCES})
|
||||||
|
target_link_libraries(BVLEVoxels PRIVATE VoxelEngine WickedEngine)
|
||||||
|
|
||||||
|
# Copy Content directory (shaders, etc.) to build output
|
||||||
|
add_custom_command(TARGET BVLEVoxels POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/Content
|
||||||
|
$<TARGET_FILE_DIR:BVLEVoxels>/Content
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy DXC shader compiler DLL next to the exe (required for runtime shader compilation)
|
||||||
|
add_custom_command(TARGET BVLEVoxels POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/WickedEngine/dxcompiler.dll
|
||||||
|
$<TARGET_FILE_DIR:BVLEVoxels>/dxcompiler.dll
|
||||||
|
COMMENT "Copying DXC shader compiler DLL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Copy our custom shader sources into Wicked's shader source tree
|
||||||
|
# so LoadShader can find and compile them as "voxel/voxelVS.cso"
|
||||||
|
add_custom_command(TARGET BVLEVoxels POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
${CMAKE_SOURCE_DIR}/shaders
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/WickedEngine/shaders/voxel
|
||||||
|
COMMENT "Copying voxel shaders to Wicked Engine shader source directory"
|
||||||
|
)
|
||||||
120
README.md
Normal file
120
README.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
# BVLE Voxels
|
||||||
|
|
||||||
|
Prototype de moteur voxel hybride basé sur [Wicked Engine](https://github.com/turanszkij/WickedEngine) (MIT, C++17, DX12/Vulkan).
|
||||||
|
|
||||||
|
Cible : **60+ fps en 1440p**, monde de 512×512×256 voxels visibles.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
|
||||||
|
| Outil | Version | Installation |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| **Windows** | 10/11 (x64) | — |
|
||||||
|
| **CMake** | 3.19+ | `winget install Kitware.CMake` |
|
||||||
|
| **Visual Studio 2022 Build Tools** | 17.x | `winget install Microsoft.VisualStudio.2022.BuildTools` |
|
||||||
|
| **Windows SDK** | 10.0.26100+ | `winget install Microsoft.WindowsSDK.10.0.26100` |
|
||||||
|
| **GPU** | DX12 feature level 12.0+ | AMD RDNA 2+ / Nvidia RTX 3060+ recommandé |
|
||||||
|
|
||||||
|
> Le SDK **10.0.26100** est requis car les headers DX12 fournis par Wicked Engine ne sont pas compatibles avec le SDK 22621.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Cloner le dépôt
|
||||||
|
git clone <url> bvle-voxels
|
||||||
|
cd bvle-voxels
|
||||||
|
|
||||||
|
# 2. Cloner Wicked Engine dans engine/
|
||||||
|
git clone --depth 1 https://github.com/turanszkij/WickedEngine.git engine
|
||||||
|
|
||||||
|
# 3. Configurer CMake
|
||||||
|
cmake -B build -G "Visual Studio 17 2022" -A x64 -DCMAKE_SYSTEM_VERSION=10.0.26100.0
|
||||||
|
|
||||||
|
# 4. Compiler
|
||||||
|
cmake --build build --config Release --target BVLEVoxels --parallel
|
||||||
|
|
||||||
|
# 5. Lancer
|
||||||
|
./build/Release/BVLEVoxels.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commandes de lancement
|
||||||
|
|
||||||
|
| Commande | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `BVLEVoxels.exe` | Mode normal (monde procédural, rayon 4 chunks) |
|
||||||
|
| `BVLEVoxels.exe debug` | Mode debug face-color (+X=Rouge, -X=Rouge sombre, etc.) |
|
||||||
|
| `BVLEVoxels.exe debugdevice` | Active la couche de debug D3D12 |
|
||||||
|
| `BVLEVoxels.exe vulkan` | Force le backend Vulkan |
|
||||||
|
|
||||||
|
## Contrôles
|
||||||
|
|
||||||
|
| Touche | Action |
|
||||||
|
|--------|--------|
|
||||||
|
| **WASD** | Déplacement caméra |
|
||||||
|
| **Espace / Ctrl** | Monter / Descendre |
|
||||||
|
| **Shift** | Vitesse ×3 |
|
||||||
|
| **Clic droit** | Capturer/libérer la souris |
|
||||||
|
| **~** (tilde) | Console Wicked Engine |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
bvle-voxels/
|
||||||
|
├── CMakeLists.txt # Build CMake
|
||||||
|
├── engine/ # Wicked Engine (git clone --depth 1)
|
||||||
|
├── src/
|
||||||
|
│ ├── voxel/ # Bibliothèque VoxelEngine (static lib)
|
||||||
|
│ │ ├── VoxelTypes.h # Types (VoxelData, PackedQuad, ChunkPos)
|
||||||
|
│ │ ├── VoxelWorld.h/.cpp # Monde voxel (hashmap, génération Perlin)
|
||||||
|
│ │ ├── VoxelMesher.h/.cpp # Binary Greedy Mesher CPU
|
||||||
|
│ │ └── VoxelRenderer.h/.cpp# Renderer GPU-driven + VoxelRenderPath
|
||||||
|
│ └── app/
|
||||||
|
│ └── main.cpp # Point d'entrée Win32
|
||||||
|
├── shaders/ # Sources HLSL
|
||||||
|
│ ├── voxelCommon.hlsli # Root signature, CB, structs partagés
|
||||||
|
│ ├── voxelVS.hlsl # Vertex shader (vertex pulling)
|
||||||
|
│ ├── voxelPS.hlsl # Pixel shader (triplanar + lighting)
|
||||||
|
│ ├── voxelCullCS.hlsl # Compute: frustum + backface culling
|
||||||
|
│ └── voxelMeshCS.hlsl # Compute: GPU mesher (binary, baseline)
|
||||||
|
└── voxel_engine_spec.docx # Document de spécification complet
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pipeline de rendu (Phase 2 — GPU-driven)
|
||||||
|
|
||||||
|
```
|
||||||
|
CPU: mesh dirty chunks (greedy merge) → pack quads + chunkInfo dans mega-buffers → upload GPU
|
||||||
|
GPU: frustum cull compute → indirect args → DrawInstancedIndirectCount (1 appel = N draws)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Buffers GPU :**
|
||||||
|
|
||||||
|
| Buffer | Type | Slot | Rôle |
|
||||||
|
|--------|------|------|------|
|
||||||
|
| `megaQuadBuffer_` | StructuredBuffer\<PackedQuad\> | SRV t0 | 2M quads max (16 MB) |
|
||||||
|
| `chunkInfoBuffer_` | StructuredBuffer\<GPUChunkInfo\> | SRV t2 | 2048 chunks max |
|
||||||
|
| `indirectArgsBuffer_` | RWStructuredBuffer | UAV u0 | Indirect draw args |
|
||||||
|
| `drawCountBuffer_` | RWByteAddressBuffer | UAV u1 | Compteur atomique de draws |
|
||||||
|
|
||||||
|
**Caractéristiques Phase 2 :**
|
||||||
|
- Mega-buffer unique pour tous les quads de tous les chunks
|
||||||
|
- Vertex pulling via `SV_VertexID` + push constants (`b999`)
|
||||||
|
- Frustum culling CPU (wi::primitive::Frustum)
|
||||||
|
- Backface culling par face group (6 directions × chunk)
|
||||||
|
- GPU frustum cull compute shader (prêt, activation via flag)
|
||||||
|
- GPU mesher compute shader baseline (binary, sans greedy merge)
|
||||||
|
- Tri des quads par direction de face dans le mesher CPU
|
||||||
|
- GPU timestamp queries pour benchmark
|
||||||
|
|
||||||
|
## Phases de développement
|
||||||
|
|
||||||
|
- [x] **Phase 1** — Setup, meshing CPU, rendu basique
|
||||||
|
- [x] **Phase 2** — GPU-driven pipeline, mega-buffer, culling, compute shaders
|
||||||
|
- [ ] **Phase 3** — Texture blending (triplanar, height-based)
|
||||||
|
- [ ] **Phase 4** — Toping (rebords, bordures procédurales)
|
||||||
|
- [ ] **Phase 5** — Rendu smooth (Surface Nets / Marching Cubes)
|
||||||
|
- [ ] **Phase 6** — Ray tracing hybride (RT shadows + AO)
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Wicked Engine est sous licence MIT. Le code spécifique BVLE est propriétaire.
|
||||||
99
shaders/voxelCommon.hlsli
Normal file
99
shaders/voxelCommon.hlsli
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
// BVLE Voxels - Shared shader definitions
|
||||||
|
// Root signature, common structures, and constant buffers for voxel shaders.
|
||||||
|
|
||||||
|
#ifndef VOXEL_COMMON_HLSLI
|
||||||
|
#define VOXEL_COMMON_HLSLI
|
||||||
|
|
||||||
|
// Wicked Engine DX12 root signature (HLSL 6.6+ bindless model)
|
||||||
|
// b999: push constants (12 x uint32 = 48 bytes)
|
||||||
|
// b0-b2: root CBV descriptors
|
||||||
|
// t0-t15, u0-u15: SRV/UAV descriptor table
|
||||||
|
// s0-s7: dynamic samplers
|
||||||
|
// s100+: static samplers
|
||||||
|
#define VOXEL_ROOTSIG \
|
||||||
|
"RootFlags(CBV_SRV_UAV_HEAP_DIRECTLY_INDEXED | SAMPLER_HEAP_DIRECTLY_INDEXED), " \
|
||||||
|
"RootConstants(num32BitConstants=12, b999), " \
|
||||||
|
"CBV(b0), " \
|
||||||
|
"CBV(b1), " \
|
||||||
|
"CBV(b2), " \
|
||||||
|
"DescriptorTable( " \
|
||||||
|
"CBV(b3, numDescriptors = 11, flags = DATA_STATIC_WHILE_SET_AT_EXECUTE)," \
|
||||||
|
"SRV(t0, numDescriptors = 16, flags = DESCRIPTORS_VOLATILE | DATA_STATIC_WHILE_SET_AT_EXECUTE)," \
|
||||||
|
"UAV(u0, numDescriptors = 16, flags = DESCRIPTORS_VOLATILE | DATA_STATIC_WHILE_SET_AT_EXECUTE)" \
|
||||||
|
")," \
|
||||||
|
"DescriptorTable( " \
|
||||||
|
"Sampler(s0, offset = 0, numDescriptors = 8, flags = DESCRIPTORS_VOLATILE)" \
|
||||||
|
")," \
|
||||||
|
"StaticSampler(s100, addressU = TEXTURE_ADDRESS_CLAMP, addressV = TEXTURE_ADDRESS_CLAMP, addressW = TEXTURE_ADDRESS_CLAMP, filter = FILTER_MIN_MAG_MIP_LINEAR)," \
|
||||||
|
"StaticSampler(s101, addressU = TEXTURE_ADDRESS_WRAP, addressV = TEXTURE_ADDRESS_WRAP, addressW = TEXTURE_ADDRESS_WRAP, filter = FILTER_MIN_MAG_MIP_LINEAR)," \
|
||||||
|
"StaticSampler(s102, addressU = TEXTURE_ADDRESS_MIRROR, addressV = TEXTURE_ADDRESS_MIRROR, addressW = TEXTURE_ADDRESS_MIRROR, filter = FILTER_MIN_MAG_MIP_LINEAR)," \
|
||||||
|
"StaticSampler(s103, addressU = TEXTURE_ADDRESS_CLAMP, addressV = TEXTURE_ADDRESS_CLAMP, addressW = TEXTURE_ADDRESS_CLAMP, filter = FILTER_MIN_MAG_MIP_POINT)," \
|
||||||
|
"StaticSampler(s104, addressU = TEXTURE_ADDRESS_WRAP, addressV = TEXTURE_ADDRESS_WRAP, addressW = TEXTURE_ADDRESS_WRAP, filter = FILTER_MIN_MAG_MIP_POINT)," \
|
||||||
|
"StaticSampler(s105, addressU = TEXTURE_ADDRESS_MIRROR, addressV = TEXTURE_ADDRESS_MIRROR, addressW = TEXTURE_ADDRESS_MIRROR, filter = FILTER_MIN_MAG_MIP_POINT)," \
|
||||||
|
"StaticSampler(s106, addressU = TEXTURE_ADDRESS_CLAMP, addressV = TEXTURE_ADDRESS_CLAMP, addressW = TEXTURE_ADDRESS_CLAMP, filter = FILTER_ANISOTROPIC, maxAnisotropy = 16)," \
|
||||||
|
"StaticSampler(s107, addressU = TEXTURE_ADDRESS_WRAP, addressV = TEXTURE_ADDRESS_WRAP, addressW = TEXTURE_ADDRESS_WRAP, filter = FILTER_ANISOTROPIC, maxAnisotropy = 16)," \
|
||||||
|
"StaticSampler(s108, addressU = TEXTURE_ADDRESS_MIRROR, addressV = TEXTURE_ADDRESS_MIRROR, addressW = TEXTURE_ADDRESS_MIRROR, filter = FILTER_ANISOTROPIC, maxAnisotropy = 16)," \
|
||||||
|
"StaticSampler(s109, addressU = TEXTURE_ADDRESS_CLAMP, addressV = TEXTURE_ADDRESS_CLAMP, addressW = TEXTURE_ADDRESS_CLAMP, filter = FILTER_COMPARISON_MIN_MAG_LINEAR_MIP_POINT, comparisonFunc = COMPARISON_GREATER_EQUAL),"
|
||||||
|
|
||||||
|
// ── Per-frame constant buffer (b0) ──────────────────────────────
|
||||||
|
cbuffer VoxelCB : register(b0) {
|
||||||
|
float4x4 viewProjection;
|
||||||
|
float4 cameraPosition;
|
||||||
|
float4 sunDirection;
|
||||||
|
float4 sunColor;
|
||||||
|
float chunkSize;
|
||||||
|
float textureTiling;
|
||||||
|
float2 _pad;
|
||||||
|
// Frustum culling data (used by cull compute shader)
|
||||||
|
float4 frustumPlanes[6]; // ax+by+cz+d=0, xyz=normal, w=distance
|
||||||
|
uint chunkCount;
|
||||||
|
uint _cullPad0;
|
||||||
|
uint _cullPad1;
|
||||||
|
uint _cullPad2;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Indirect draw args (must match C++ IndirectDrawArgs / DX12 DrawInstanced) ──
|
||||||
|
struct IndirectDrawArgsInstanced {
|
||||||
|
uint vertexCountPerInstance;
|
||||||
|
uint instanceCount;
|
||||||
|
uint startVertexLocation;
|
||||||
|
uint startInstanceLocation;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── GPU chunk info (must match C++ GPUChunkInfo, 80 bytes) ──────
|
||||||
|
// NOTE: No arrays — scalar-only to guarantee C-style packing in StructuredBuffer.
|
||||||
|
struct GPUChunkInfo {
|
||||||
|
float4 worldPos; // xyz = chunk origin in world space, w = debug flag
|
||||||
|
uint quadOffset; // offset into mega quad buffer (in quads)
|
||||||
|
uint quadCount; // number of quads for this chunk
|
||||||
|
uint _pad0;
|
||||||
|
uint _pad1;
|
||||||
|
// Per-face data (6 faces: +X -X +Y -Y +Z -Z)
|
||||||
|
uint faceOff0, faceOff1, faceOff2, faceOff3, faceOff4, faceOff5;
|
||||||
|
uint faceCnt0, faceCnt1, faceCnt2, faceCnt3, faceCnt4, faceCnt5;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper functions to access scalar face fields by index
|
||||||
|
uint getFaceOffset(GPUChunkInfo info, uint f) {
|
||||||
|
switch (f) {
|
||||||
|
case 0: return info.faceOff0;
|
||||||
|
case 1: return info.faceOff1;
|
||||||
|
case 2: return info.faceOff2;
|
||||||
|
case 3: return info.faceOff3;
|
||||||
|
case 4: return info.faceOff4;
|
||||||
|
default: return info.faceOff5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint getFaceCount(GPUChunkInfo info, uint f) {
|
||||||
|
switch (f) {
|
||||||
|
case 0: return info.faceCnt0;
|
||||||
|
case 1: return info.faceCnt1;
|
||||||
|
case 2: return info.faceCnt2;
|
||||||
|
case 3: return info.faceCnt3;
|
||||||
|
case 4: return info.faceCnt4;
|
||||||
|
default: return info.faceCnt5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // VOXEL_COMMON_HLSLI
|
||||||
93
shaders/voxelCullCS.hlsl
Normal file
93
shaders/voxelCullCS.hlsl
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
// BVLE Voxels - Frustum + Backface Culling Compute Shader
|
||||||
|
// 1 thread per chunk: tests AABB vs 6 frustum planes, then emits up to 6 draws
|
||||||
|
// (one per visible face group, back-facing groups are culled).
|
||||||
|
|
||||||
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
|
StructuredBuffer<GPUChunkInfo> chunkInfoBuffer : register(t2);
|
||||||
|
RWStructuredBuffer<IndirectDrawArgsInstanced> indirectArgs : register(u0);
|
||||||
|
RWByteAddressBuffer drawCount : register(u1);
|
||||||
|
|
||||||
|
// Test AABB against 6 frustum planes (returns true if visible)
|
||||||
|
bool frustumTestAABB(float3 aabbMin, float3 aabbMax)
|
||||||
|
{
|
||||||
|
[unroll]
|
||||||
|
for (uint i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
float4 plane = frustumPlanes[i];
|
||||||
|
float3 pVertex;
|
||||||
|
pVertex.x = (plane.x >= 0.0) ? aabbMax.x : aabbMin.x;
|
||||||
|
pVertex.y = (plane.y >= 0.0) ? aabbMax.y : aabbMin.y;
|
||||||
|
pVertex.z = (plane.z >= 0.0) ? aabbMax.z : aabbMin.z;
|
||||||
|
|
||||||
|
if (dot(plane.xyz, pVertex) + plane.w < 0.0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Face normals: +X, -X, +Y, -Y, +Z, -Z
|
||||||
|
static const float3 faceNormals[6] = {
|
||||||
|
float3( 1, 0, 0), float3(-1, 0, 0),
|
||||||
|
float3( 0, 1, 0), float3( 0,-1, 0),
|
||||||
|
float3( 0, 0, 1), float3( 0, 0,-1)
|
||||||
|
};
|
||||||
|
|
||||||
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
|
[numthreads(64, 1, 1)]
|
||||||
|
void main(uint3 DTid : SV_DispatchThreadID)
|
||||||
|
{
|
||||||
|
uint chunkIdx = DTid.x;
|
||||||
|
if (chunkIdx >= chunkCount) return;
|
||||||
|
|
||||||
|
GPUChunkInfo info = chunkInfoBuffer[chunkIdx];
|
||||||
|
if (info.quadCount == 0) return;
|
||||||
|
|
||||||
|
float3 aabbMin = info.worldPos.xyz;
|
||||||
|
float3 aabbMax = aabbMin + (float3)chunkSize;
|
||||||
|
|
||||||
|
if (!frustumTestAABB(aabbMin, aabbMax)) return;
|
||||||
|
|
||||||
|
// Camera-to-chunk vector for backface test
|
||||||
|
float3 chunkCenter = (aabbMin + aabbMax) * 0.5;
|
||||||
|
float3 viewDir = chunkCenter - cameraPosition.xyz;
|
||||||
|
|
||||||
|
// Emit one draw per visible face group
|
||||||
|
[unroll]
|
||||||
|
for (uint f = 0; f < 6; f++)
|
||||||
|
{
|
||||||
|
uint fCnt = getFaceCount(info, f);
|
||||||
|
if (fCnt == 0) continue;
|
||||||
|
|
||||||
|
// Backface cull: if camera sees the back of this face group, skip it.
|
||||||
|
// A face group with normal N is back-facing if dot(viewDir, N) > 0.
|
||||||
|
// But we need a per-face test relative to the chunk AABB, not just center:
|
||||||
|
// face +X: back-facing if camera.x < aabbMin.x (camera is on -X side)
|
||||||
|
// face -X: back-facing if camera.x > aabbMax.x (camera is on +X side)
|
||||||
|
// This is more conservative and correct than dot product with center.
|
||||||
|
bool backFacing = false;
|
||||||
|
switch (f)
|
||||||
|
{
|
||||||
|
case 0: backFacing = (cameraPosition.x < aabbMin.x); break; // +X
|
||||||
|
case 1: backFacing = (cameraPosition.x > aabbMax.x); break; // -X
|
||||||
|
case 2: backFacing = (cameraPosition.y < aabbMin.y); break; // +Y
|
||||||
|
case 3: backFacing = (cameraPosition.y > aabbMax.y); break; // -Y
|
||||||
|
case 4: backFacing = (cameraPosition.z < aabbMin.z); break; // +Z
|
||||||
|
case 5: backFacing = (cameraPosition.z > aabbMax.z); break; // -Z
|
||||||
|
}
|
||||||
|
if (backFacing) continue;
|
||||||
|
|
||||||
|
uint drawIdx;
|
||||||
|
drawCount.InterlockedAdd(0, 1, drawIdx);
|
||||||
|
|
||||||
|
// The face group's quads start at (chunk's mega-buffer offset + face offset within chunk)
|
||||||
|
uint faceQuadOffset = info.quadOffset + getFaceOffset(info, f);
|
||||||
|
|
||||||
|
IndirectDrawArgsInstanced args;
|
||||||
|
args.vertexCountPerInstance = fCnt * 6;
|
||||||
|
args.instanceCount = 1;
|
||||||
|
args.startVertexLocation = faceQuadOffset * 6;
|
||||||
|
args.startInstanceLocation = chunkIdx;
|
||||||
|
indirectArgs[drawIdx] = args;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
shaders/voxelMeshCS.hlsl
Normal file
86
shaders/voxelMeshCS.hlsl
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
// BVLE Voxels - GPU Compute Mesher (Binary Face Culling only)
|
||||||
|
// 1 thread per voxel: checks 6 neighbors, emits 1x1 PackedQuad per visible face.
|
||||||
|
// No greedy merge — this is the simple GPU baseline for benchmark comparison.
|
||||||
|
|
||||||
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
|
// Push constants: chunk index + output offset
|
||||||
|
struct MeshPush {
|
||||||
|
uint chunkIndex; // which chunk to mesh
|
||||||
|
uint voxelBufferOffset; // offset into the voxel data buffer (in uint16 pairs)
|
||||||
|
uint quadBufferOffset; // offset into the output quad buffer (in quads)
|
||||||
|
uint maxOutputQuads; // safety cap on output
|
||||||
|
uint pad[8]; // pad to 48 bytes (12 x uint32)
|
||||||
|
};
|
||||||
|
[[vk::push_constant]] ConstantBuffer<MeshPush> push : register(b999);
|
||||||
|
|
||||||
|
// Input: voxel data for one chunk (32^3 = 32768 voxels, packed as uint16 pairs in uint)
|
||||||
|
// Each uint holds 2 voxels: low 16 bits = voxel A, high 16 bits = voxel B
|
||||||
|
StructuredBuffer<uint> voxelData : register(t0);
|
||||||
|
|
||||||
|
// Output: packed quads (append buffer with atomic counter)
|
||||||
|
RWStructuredBuffer<uint2> outputQuads : register(u0); // uint2 = 8 bytes = PackedQuad
|
||||||
|
RWByteAddressBuffer quadCounter : register(u1); // atomic counter
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
static const uint CSIZE = 32;
|
||||||
|
static const uint CVOL = CSIZE * CSIZE * CSIZE; // 32768
|
||||||
|
|
||||||
|
// Read a single voxel (16-bit) from the packed buffer
|
||||||
|
uint readVoxel(uint flatIndex) {
|
||||||
|
uint pairIndex = flatIndex >> 1; // which uint (2 voxels per uint)
|
||||||
|
uint shift = (flatIndex & 1) * 16; // 0 or 16
|
||||||
|
return (voxelData[push.voxelBufferOffset + pairIndex] >> shift) & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if neighbor is air (handles out-of-bounds as air for chunk boundaries)
|
||||||
|
bool isNeighborAir(int3 pos, int3 dir) {
|
||||||
|
int3 n = pos + dir;
|
||||||
|
// Out-of-chunk = treat as air (boundary faces always visible)
|
||||||
|
if (any(n < 0) || any(n >= (int3)CSIZE))
|
||||||
|
return true;
|
||||||
|
uint flatN = (uint)n.x + (uint)n.y * CSIZE + (uint)n.z * CSIZE * CSIZE;
|
||||||
|
return readVoxel(flatN) == 0; // materialID 0 = air
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack a quad into uint2 (matches CPU PackedQuad format)
|
||||||
|
uint2 packQuad(uint x, uint y, uint z, uint w, uint h, uint face, uint matID) {
|
||||||
|
uint lo = x | (y << 6) | (z << 12) | (w << 18) | (h << 24) | (face << 30);
|
||||||
|
uint hi = (face >> 2) | (matID << 1) | (0 << 9) | (0 << 17); // AO=0, flags=0
|
||||||
|
return uint2(lo, hi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Face directions
|
||||||
|
static const int3 faceDirs[6] = {
|
||||||
|
int3( 1, 0, 0), int3(-1, 0, 0),
|
||||||
|
int3( 0, 1, 0), int3( 0,-1, 0),
|
||||||
|
int3( 0, 0, 1), int3( 0, 0,-1)
|
||||||
|
};
|
||||||
|
|
||||||
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
|
[numthreads(8, 8, 8)] // 512 threads = covers 32^3 with 64 groups of 512
|
||||||
|
void main(uint3 DTid : SV_DispatchThreadID)
|
||||||
|
{
|
||||||
|
if (any(DTid >= CSIZE)) return;
|
||||||
|
|
||||||
|
uint flatIdx = DTid.x + DTid.y * CSIZE + DTid.z * CSIZE * CSIZE;
|
||||||
|
uint voxel = readVoxel(flatIdx);
|
||||||
|
if (voxel == 0) return; // air voxel, nothing to emit
|
||||||
|
|
||||||
|
uint matID = voxel >> 8; // high 8 bits = material ID
|
||||||
|
|
||||||
|
// Check each face direction
|
||||||
|
[unroll]
|
||||||
|
for (uint f = 0; f < 6; f++) {
|
||||||
|
if (!isNeighborAir((int3)DTid, faceDirs[f])) continue;
|
||||||
|
|
||||||
|
// Emit a 1x1 quad
|
||||||
|
uint slot;
|
||||||
|
quadCounter.InterlockedAdd(0, 1, slot);
|
||||||
|
if (slot >= push.maxOutputQuads) return; // overflow guard
|
||||||
|
|
||||||
|
outputQuads[push.quadBufferOffset + slot] = packQuad(
|
||||||
|
DTid.x, DTid.y, DTid.z, 1, 1, f, matID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
shaders/voxelPS.hlsl
Normal file
83
shaders/voxelPS.hlsl
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// BVLE Voxels - Pixel Shader (Triplanar textured with simple lighting)
|
||||||
|
|
||||||
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
|
Texture2DArray materialTextures : register(t1);
|
||||||
|
SamplerState materialSampler : register(s0);
|
||||||
|
|
||||||
|
struct PSInput {
|
||||||
|
float4 position : SV_POSITION;
|
||||||
|
float3 worldPos : WORLDPOS;
|
||||||
|
float3 normal : NORMAL;
|
||||||
|
float2 uv : TEXCOORD0;
|
||||||
|
nointerpolation uint materialID : MATERIALID;
|
||||||
|
nointerpolation uint faceID : FACEID;
|
||||||
|
nointerpolation float debugFlag : DEBUGFLAG;
|
||||||
|
float ao : AO;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triplanar blend weights
|
||||||
|
float3 triplanarWeights(float3 normal, float sharpness) {
|
||||||
|
float3 w = abs(normal);
|
||||||
|
w = pow(w, (float3)sharpness);
|
||||||
|
return w / (w.x + w.y + w.z + 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 sampleTriplanar(float3 worldPos, float3 normal, uint texIndex, float tiling) {
|
||||||
|
float3 w = triplanarWeights(normal, 4.0);
|
||||||
|
|
||||||
|
float3 colX = materialTextures.Sample(materialSampler, float3(worldPos.yz * tiling, (float)texIndex)).rgb;
|
||||||
|
float3 colY = materialTextures.Sample(materialSampler, float3(worldPos.xz * tiling, (float)texIndex)).rgb;
|
||||||
|
float3 colZ = materialTextures.Sample(materialSampler, float3(worldPos.xy * tiling, (float)texIndex)).rgb;
|
||||||
|
|
||||||
|
return colX * w.x + colY * w.y + colZ * w.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug face colors
|
||||||
|
static const float3 faceDebugColors[6] = {
|
||||||
|
float3(1.0, 0.2, 0.2), // 0: +X = RED
|
||||||
|
float3(0.5, 0.0, 0.0), // 1: -X = DARK RED
|
||||||
|
float3(0.2, 1.0, 0.2), // 2: +Y = GREEN
|
||||||
|
float3(0.0, 0.5, 0.0), // 3: -Y = DARK GREEN
|
||||||
|
float3(0.2, 0.2, 1.0), // 4: +Z = BLUE
|
||||||
|
float3(0.0, 0.0, 0.5), // 5: -Z = DARK BLUE
|
||||||
|
};
|
||||||
|
|
||||||
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
|
float4 main(PSInput input) : SV_TARGET0
|
||||||
|
{
|
||||||
|
// ── DEBUG MODE: face direction colors ──
|
||||||
|
if (input.debugFlag > 0.5)
|
||||||
|
{
|
||||||
|
uint fid = min(input.faceID, 5u);
|
||||||
|
float3 faceColor = faceDebugColors[fid];
|
||||||
|
float2 checker = floor(input.worldPos.xz * 0.5);
|
||||||
|
float check = frac((checker.x + checker.y) * 0.5) * 2.0;
|
||||||
|
faceColor *= (0.85 + 0.15 * check);
|
||||||
|
return float4(faceColor, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── NORMAL MODE: triplanar textured ──
|
||||||
|
float3 N = normalize(input.normal);
|
||||||
|
float3 L = normalize(-sunDirection.xyz);
|
||||||
|
float NdotL = max(dot(N, L), 0.0);
|
||||||
|
|
||||||
|
float3 baseColor = N * 0.5 + 0.5;
|
||||||
|
|
||||||
|
uint texIndex = clamp(input.materialID - 1u, 0u, 4u);
|
||||||
|
float tiling = textureTiling;
|
||||||
|
float3 texColor = sampleTriplanar(input.worldPos, N, texIndex, tiling);
|
||||||
|
|
||||||
|
float3 albedo = (input.materialID > 0u) ? texColor : baseColor;
|
||||||
|
|
||||||
|
float3 ambient = float3(0.15, 0.18, 0.25);
|
||||||
|
float3 diffuse = sunColor.rgb * NdotL;
|
||||||
|
float3 color = albedo * (ambient + diffuse) * input.ao;
|
||||||
|
|
||||||
|
float dist = length(input.worldPos - cameraPosition.xyz);
|
||||||
|
float fog = 1.0 - exp(-dist * 0.003);
|
||||||
|
float3 fogColor = float3(0.55, 0.70, 0.90);
|
||||||
|
color = lerp(color, fogColor, saturate(fog));
|
||||||
|
|
||||||
|
return float4(color, 1.0);
|
||||||
|
}
|
||||||
127
shaders/voxelVS.hlsl
Normal file
127
shaders/voxelVS.hlsl
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
// BVLE Voxels - Vertex Shader (Vertex Pulling from mega-buffer)
|
||||||
|
// Phase 2: uses SV_InstanceID to look up chunk info instead of push constants.
|
||||||
|
|
||||||
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
|
struct PackedQuad {
|
||||||
|
uint2 data; // 8 bytes = 2 x uint32
|
||||||
|
};
|
||||||
|
|
||||||
|
StructuredBuffer<PackedQuad> quadBuffer : register(t0);
|
||||||
|
StructuredBuffer<GPUChunkInfo> chunkInfoBuffer : register(t2);
|
||||||
|
|
||||||
|
// Push constants: chunk index + quad offset for current draw call
|
||||||
|
struct VoxelPush {
|
||||||
|
uint chunkIndex;
|
||||||
|
uint quadOffset; // offset into mega quad buffer (in quads)
|
||||||
|
uint pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7, pad8, pad9;
|
||||||
|
};
|
||||||
|
[[vk::push_constant]] ConstantBuffer<VoxelPush> push : register(b999);
|
||||||
|
|
||||||
|
struct VSOutput {
|
||||||
|
float4 position : SV_POSITION;
|
||||||
|
float3 worldPos : WORLDPOS;
|
||||||
|
float3 normal : NORMAL;
|
||||||
|
float2 uv : TEXCOORD0;
|
||||||
|
nointerpolation uint materialID : MATERIALID;
|
||||||
|
nointerpolation uint faceID : FACEID;
|
||||||
|
nointerpolation float debugFlag : DEBUGFLAG;
|
||||||
|
float ao : AO;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Unpack 64 bits from 2 x uint32
|
||||||
|
void unpackQuad(uint2 raw, out uint px, out uint py, out uint pz,
|
||||||
|
out uint w, out uint h, out uint face,
|
||||||
|
out uint matID, out uint ao)
|
||||||
|
{
|
||||||
|
uint lo = raw.x;
|
||||||
|
uint hi = raw.y;
|
||||||
|
px = lo & 0x3F;
|
||||||
|
py = (lo >> 6) & 0x3F;
|
||||||
|
pz = (lo >> 12) & 0x3F;
|
||||||
|
w = (lo >> 18) & 0x3F;
|
||||||
|
h = (lo >> 24) & 0x3F;
|
||||||
|
face = ((lo >> 30) & 0x3) | ((hi & 0x1) << 2);
|
||||||
|
matID = (hi >> 1) & 0xFF;
|
||||||
|
ao = (hi >> 9) & 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Face normals: +X, -X, +Y, -Y, +Z, -Z
|
||||||
|
static const float3 faceNormals[6] = {
|
||||||
|
float3( 1, 0, 0), float3(-1, 0, 0),
|
||||||
|
float3( 0, 1, 0), float3( 0,-1, 0),
|
||||||
|
float3( 0, 0, 1), float3( 0, 0,-1)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Face U/V tangent axes for quad expansion
|
||||||
|
static const float3 faceU[6] = {
|
||||||
|
float3(0, 1, 0), float3(0, 1, 0),
|
||||||
|
float3(1, 0, 0), float3(1, 0, 0),
|
||||||
|
float3(1, 0, 0), float3(1, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
static const float3 faceV[6] = {
|
||||||
|
float3(0, 0, 1), float3(0, 0, 1),
|
||||||
|
float3(0, 0, 1), float3(0, 0, 1),
|
||||||
|
float3(0, 1, 0), float3(0, 1, 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
|
VSOutput main(uint vertexID : SV_VertexID)
|
||||||
|
{
|
||||||
|
VSOutput output;
|
||||||
|
|
||||||
|
// Look up chunk info via push constant (SV_InstanceID doesn't include StartInstanceLocation in D3D12)
|
||||||
|
GPUChunkInfo info = chunkInfoBuffer[push.chunkIndex];
|
||||||
|
|
||||||
|
// 6 vertices per quad (2 triangles)
|
||||||
|
// Use push.quadOffset instead of relying on StartVertexLocation in SV_VertexID
|
||||||
|
uint localVertex = vertexID;
|
||||||
|
uint quadIndex = push.quadOffset + (localVertex / 6);
|
||||||
|
uint cornerIndex = localVertex % 6;
|
||||||
|
|
||||||
|
PackedQuad packed = quadBuffer[quadIndex];
|
||||||
|
uint px, py, pz, w, h, face, matID, ao;
|
||||||
|
unpackQuad(packed.data, px, py, pz, w, h, face, matID, ao);
|
||||||
|
|
||||||
|
// Corner offsets for 2 triangles (6 vertices per quad)
|
||||||
|
// cross(U,V) matches N for faces: +X(0), -Y(3), +Z(4) -> CW corners
|
||||||
|
// cross(U,V) opposes N for faces: -X(1), +Y(2), -Z(5) -> CCW corners
|
||||||
|
static const float2 cornersCW[6] = {
|
||||||
|
float2(0, 0), float2(0, 1), float2(1, 0),
|
||||||
|
float2(1, 0), float2(0, 1), float2(1, 1)
|
||||||
|
};
|
||||||
|
static const float2 cornersCCW[6] = {
|
||||||
|
float2(0, 0), float2(1, 0), float2(0, 1),
|
||||||
|
float2(0, 1), float2(1, 0), float2(1, 1)
|
||||||
|
};
|
||||||
|
bool useCCW = (face == 1 || face == 2 || face == 5);
|
||||||
|
float2 corner = useCCW ? cornersCCW[cornerIndex] : cornersCW[cornerIndex];
|
||||||
|
|
||||||
|
float3 basePos = float3((float)px, (float)py, (float)pz);
|
||||||
|
float3 normal = faceNormals[face];
|
||||||
|
float3 uAxis = faceU[face];
|
||||||
|
float3 vAxis = faceV[face];
|
||||||
|
|
||||||
|
// Positive faces: offset by 1 in normal direction
|
||||||
|
float3 faceOffset = (face % 2 == 0) ? normal : float3(0, 0, 0);
|
||||||
|
|
||||||
|
// Expand quad
|
||||||
|
float3 localPos = basePos + faceOffset + uAxis * corner.x * (float)w + vAxis * corner.y * (float)h;
|
||||||
|
float3 worldPos = localPos + info.worldPos.xyz;
|
||||||
|
|
||||||
|
output.position = mul(viewProjection, float4(worldPos, 1.0));
|
||||||
|
output.worldPos = worldPos;
|
||||||
|
output.normal = normal;
|
||||||
|
output.uv = corner * float2((float)w, (float)h) * textureTiling;
|
||||||
|
output.materialID = matID;
|
||||||
|
output.faceID = face;
|
||||||
|
output.debugFlag = info.worldPos.w;
|
||||||
|
|
||||||
|
// AO: 4 corners x 2 bits
|
||||||
|
uint aoCorner = min(cornerIndex, 3u);
|
||||||
|
float aoValue = (float)((ao >> (aoCorner * 2u)) & 3u) / 3.0;
|
||||||
|
output.ao = 1.0 - aoValue * 0.4;
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
187
src/app/main.cpp
Normal file
187
src/app/main.cpp
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
#include "WickedEngine.h"
|
||||||
|
#include "voxel/VoxelRenderer.h"
|
||||||
|
#include <fstream>
|
||||||
|
#include <DbgHelp.h>
|
||||||
|
#pragma comment(lib, "dbghelp.lib")
|
||||||
|
|
||||||
|
// ── BVLE Voxels - Prototype Application ─────────────────────────
|
||||||
|
// Wicked Engine based voxel engine prototype for performance validation.
|
||||||
|
|
||||||
|
// ── Crash handler: writes stack trace + minidump on unhandled exception ──
|
||||||
|
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep) {
|
||||||
|
std::ofstream crash("bvle_crash.log", std::ios::trunc);
|
||||||
|
crash << "=== BVLE CRASH REPORT ===" << std::endl;
|
||||||
|
|
||||||
|
DWORD code = ep->ExceptionRecord->ExceptionCode;
|
||||||
|
PVOID addr = ep->ExceptionRecord->ExceptionAddress;
|
||||||
|
crash << "Exception code: 0x" << std::hex << code << std::endl;
|
||||||
|
crash << "Crash address: 0x" << addr << std::endl;
|
||||||
|
|
||||||
|
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) {
|
||||||
|
ULONG_PTR type = ep->ExceptionRecord->ExceptionInformation[0];
|
||||||
|
ULONG_PTR target = ep->ExceptionRecord->ExceptionInformation[1];
|
||||||
|
crash << (type == 0 ? "Reading" : "Writing") << " address: 0x" << std::hex << target << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE process = GetCurrentProcess();
|
||||||
|
HANDLE thread = GetCurrentThread();
|
||||||
|
SymInitialize(process, NULL, TRUE);
|
||||||
|
|
||||||
|
CONTEXT* ctx = ep->ContextRecord;
|
||||||
|
STACKFRAME64 frame = {};
|
||||||
|
frame.AddrPC.Offset = ctx->Rip; frame.AddrPC.Mode = AddrModeFlat;
|
||||||
|
frame.AddrFrame.Offset = ctx->Rbp; frame.AddrFrame.Mode = AddrModeFlat;
|
||||||
|
frame.AddrStack.Offset = ctx->Rsp; frame.AddrStack.Mode = AddrModeFlat;
|
||||||
|
|
||||||
|
crash << "\nStack trace:" << std::endl;
|
||||||
|
for (int i = 0; i < 32; i++) {
|
||||||
|
if (!StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, thread, &frame,
|
||||||
|
ctx, NULL, SymFunctionTableAccess64, SymGetModuleBase64, NULL))
|
||||||
|
break;
|
||||||
|
char symbolBuf[sizeof(SYMBOL_INFO) + 256];
|
||||||
|
SYMBOL_INFO* symbol = (SYMBOL_INFO*)symbolBuf;
|
||||||
|
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||||
|
symbol->MaxNameLen = 255;
|
||||||
|
DWORD64 disp64 = 0;
|
||||||
|
crash << " [" << i << "] 0x" << std::hex << frame.AddrPC.Offset;
|
||||||
|
if (SymFromAddr(process, frame.AddrPC.Offset, &disp64, symbol))
|
||||||
|
crash << " " << symbol->Name << " +0x" << disp64;
|
||||||
|
IMAGEHLP_LINE64 line = {}; line.SizeOfStruct = sizeof(IMAGEHLP_LINE64);
|
||||||
|
DWORD disp32 = 0;
|
||||||
|
if (SymGetLineFromAddr64(process, frame.AddrPC.Offset, &disp32, &line))
|
||||||
|
crash << " (" << line.FileName << ":" << std::dec << line.LineNumber << ")";
|
||||||
|
crash << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
HANDLE dumpFile = CreateFileA("bvle_crash.dmp", GENERIC_WRITE, 0, NULL,
|
||||||
|
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
|
||||||
|
if (dumpFile != INVALID_HANDLE_VALUE) {
|
||||||
|
MINIDUMP_EXCEPTION_INFORMATION mei;
|
||||||
|
mei.ThreadId = GetCurrentThreadId();
|
||||||
|
mei.ExceptionPointers = ep;
|
||||||
|
mei.ClientPointers = FALSE;
|
||||||
|
MiniDumpWriteDump(process, GetCurrentProcessId(), dumpFile,
|
||||||
|
MiniDumpWithDataSegs, &mei, NULL, NULL);
|
||||||
|
CloseHandle(dumpFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
crash.close();
|
||||||
|
SymCleanup(process);
|
||||||
|
return EXCEPTION_EXECUTE_HANDLER;
|
||||||
|
}
|
||||||
|
|
||||||
|
static wi::Application application;
|
||||||
|
static voxel::VoxelRenderPath renderPath;
|
||||||
|
|
||||||
|
int APIENTRY wWinMain(
|
||||||
|
_In_ HINSTANCE hInstance,
|
||||||
|
_In_opt_ HINSTANCE hPrevInstance,
|
||||||
|
_In_ LPWSTR lpCmdLine,
|
||||||
|
_In_ int nCmdShow)
|
||||||
|
{
|
||||||
|
SetUnhandledExceptionFilter(CrashHandler);
|
||||||
|
|
||||||
|
// Win32 window setup
|
||||||
|
static auto WndProc = [](HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -> LRESULT
|
||||||
|
{
|
||||||
|
switch (message)
|
||||||
|
{
|
||||||
|
case WM_SIZE:
|
||||||
|
case WM_DPICHANGED:
|
||||||
|
if (application.is_window_active)
|
||||||
|
application.SetWindow(hWnd);
|
||||||
|
break;
|
||||||
|
case WM_CHAR:
|
||||||
|
switch (wParam)
|
||||||
|
{
|
||||||
|
case VK_BACK:
|
||||||
|
wi::gui::TextInputField::DeleteFromInput();
|
||||||
|
break;
|
||||||
|
case VK_RETURN:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
wi::gui::TextInputField::AddInput((const wchar_t)wParam);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WM_INPUT:
|
||||||
|
wi::input::rawinput::ParseMessage((void*)lParam);
|
||||||
|
break;
|
||||||
|
case WM_KILLFOCUS:
|
||||||
|
application.is_window_active = false;
|
||||||
|
break;
|
||||||
|
case WM_SETFOCUS:
|
||||||
|
application.is_window_active = true;
|
||||||
|
break;
|
||||||
|
case WM_DESTROY:
|
||||||
|
PostQuitMessage(0);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||||
|
|
||||||
|
WNDCLASSEXW wcex = {};
|
||||||
|
wcex.cbSize = sizeof(WNDCLASSEX);
|
||||||
|
wcex.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
wcex.lpfnWndProc = WndProc;
|
||||||
|
wcex.hInstance = hInstance;
|
||||||
|
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
||||||
|
wcex.lpszClassName = L"BVLEVoxels";
|
||||||
|
RegisterClassExW(&wcex);
|
||||||
|
|
||||||
|
HWND hWnd = CreateWindowW(
|
||||||
|
wcex.lpszClassName,
|
||||||
|
L"BVLE Voxels - Prototype",
|
||||||
|
WS_OVERLAPPEDWINDOW,
|
||||||
|
CW_USEDEFAULT, 0,
|
||||||
|
1920, 1080,
|
||||||
|
nullptr, nullptr, hInstance, nullptr
|
||||||
|
);
|
||||||
|
ShowWindow(hWnd, SW_SHOWMAXIMIZED);
|
||||||
|
|
||||||
|
// Initialize Wicked Engine (selects DX12 by default on Windows, Vulkan on Linux)
|
||||||
|
// Pass "vulkan" as command line argument to force Vulkan backend
|
||||||
|
// Pass "debugdevice" for D3D debug layer, "gpuvalidation" for GPU-based validation
|
||||||
|
application.SetWindow(hWnd);
|
||||||
|
wi::arguments::Parse(lpCmdLine);
|
||||||
|
|
||||||
|
|
||||||
|
// Redirect Wicked Engine log to file
|
||||||
|
wi::backlog::SetLogFile("bvle_backlog.txt");
|
||||||
|
|
||||||
|
// Info display
|
||||||
|
application.infoDisplay.active = true;
|
||||||
|
application.infoDisplay.watermark = false;
|
||||||
|
application.infoDisplay.resolution = true;
|
||||||
|
application.infoDisplay.fpsinfo = true;
|
||||||
|
application.infoDisplay.heap_allocation_counter = true;
|
||||||
|
|
||||||
|
// Check for "debug" argument to enable face-color debug mode
|
||||||
|
if (wi::arguments::HasArgument("debug")) {
|
||||||
|
renderPath.debugMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate our custom voxel render path
|
||||||
|
application.ActivatePath(&renderPath);
|
||||||
|
|
||||||
|
// Main loop
|
||||||
|
MSG msg = { 0 };
|
||||||
|
while (msg.message != WM_QUIT)
|
||||||
|
{
|
||||||
|
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
|
||||||
|
TranslateMessage(&msg);
|
||||||
|
DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
application.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wi::jobsystem::ShutDown();
|
||||||
|
return (int)msg.wParam;
|
||||||
|
}
|
||||||
238
src/voxel/VoxelMesher.cpp
Normal file
238
src/voxel/VoxelMesher.cpp
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
#include "VoxelMesher.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
// ── Build binary masks per axis ─────────────────────────────────
|
||||||
|
// For each axis, solid[u][v] is a 32-bit mask where bit i=1 means
|
||||||
|
// voxel at position i along that axis is solid.
|
||||||
|
void VoxelMesher::buildAxisMasks(const Chunk& chunk, AxisMasks masks[3]) {
|
||||||
|
std::memset(masks, 0, sizeof(AxisMasks) * 3);
|
||||||
|
|
||||||
|
for (int z = 0; z < CHUNK_SIZE; z++) {
|
||||||
|
for (int y = 0; y < CHUNK_SIZE; y++) {
|
||||||
|
for (int x = 0; x < CHUNK_SIZE; x++) {
|
||||||
|
if (!chunk.at(x, y, z).isEmpty()) {
|
||||||
|
// X-axis: march along X, indexed by [Y][Z]
|
||||||
|
masks[0].solid[y][z] |= (1u << x);
|
||||||
|
// Y-axis: march along Y, indexed by [X][Z]
|
||||||
|
masks[1].solid[x][z] |= (1u << y);
|
||||||
|
// Z-axis: march along Z, indexed by [X][Y]
|
||||||
|
masks[2].solid[x][y] |= (1u << z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: get voxel, considering neighbor chunks for boundary faces
|
||||||
|
static VoxelData getVoxelSafe(const Chunk& chunk, const VoxelWorld& world, int x, int y, int z) {
|
||||||
|
if (chunk.isInBounds(x, y, z)) {
|
||||||
|
return chunk.at(x, y, z);
|
||||||
|
}
|
||||||
|
// Cross-chunk lookup
|
||||||
|
int wx = chunk.pos.x * CHUNK_SIZE + x;
|
||||||
|
int wy = chunk.pos.y * CHUNK_SIZE + y;
|
||||||
|
int wz = chunk.pos.z * CHUNK_SIZE + z;
|
||||||
|
return world.getVoxel(wx, wy, wz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Greedy Merge ────────────────────────────────────────────────
|
||||||
|
// For a given face direction, merge visible faces of the same material
|
||||||
|
// into maximal rectangular quads.
|
||||||
|
void VoxelMesher::greedyMerge(
|
||||||
|
const Chunk& chunk,
|
||||||
|
const VoxelWorld& world,
|
||||||
|
uint8_t face,
|
||||||
|
const uint32_t faceMasks[CHUNK_SIZE][CHUNK_SIZE],
|
||||||
|
std::vector<PackedQuad>& outQuads
|
||||||
|
) {
|
||||||
|
// Determine axis mapping based on face
|
||||||
|
// face 0,1 = X axis -> iterate over Y,Z slices
|
||||||
|
// face 2,3 = Y axis -> iterate over X,Z slices
|
||||||
|
// face 4,5 = Z axis -> iterate over X,Y slices
|
||||||
|
|
||||||
|
// For each slice along the face normal axis
|
||||||
|
for (int depth = 0; depth < CHUNK_SIZE; depth++) {
|
||||||
|
// Build a 2D grid of material IDs for this slice
|
||||||
|
uint8_t matGrid[CHUNK_SIZE][CHUNK_SIZE];
|
||||||
|
bool visited[CHUNK_SIZE][CHUNK_SIZE];
|
||||||
|
std::memset(matGrid, 0, sizeof(matGrid));
|
||||||
|
std::memset(visited, 0, sizeof(visited));
|
||||||
|
|
||||||
|
int faceCount = 0;
|
||||||
|
for (int v = 0; v < CHUNK_SIZE; v++) {
|
||||||
|
for (int u = 0; u < CHUNK_SIZE; u++) {
|
||||||
|
// Check if this face is visible at this depth
|
||||||
|
bool faceVisible = false;
|
||||||
|
|
||||||
|
int x, y, z;
|
||||||
|
switch (face) {
|
||||||
|
case FACE_POS_X: x = depth; y = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
|
||||||
|
case FACE_NEG_X: x = depth; y = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
|
||||||
|
case FACE_POS_Y: y = depth; x = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
|
||||||
|
case FACE_NEG_Y: y = depth; x = u; z = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
|
||||||
|
case FACE_POS_Z: z = depth; x = u; y = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
|
||||||
|
case FACE_NEG_Z: z = depth; x = u; y = v; faceVisible = (faceMasks[u][v] >> depth) & 1; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceVisible) {
|
||||||
|
matGrid[v][u] = chunk.at(x, y, z).getMaterialID();
|
||||||
|
faceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (faceCount == 0) continue;
|
||||||
|
|
||||||
|
// Greedy merge: scan row by row, merge same-material quads
|
||||||
|
for (int v = 0; v < CHUNK_SIZE; v++) {
|
||||||
|
for (int u = 0; u < CHUNK_SIZE; u++) {
|
||||||
|
if (visited[v][u] || matGrid[v][u] == 0) continue;
|
||||||
|
|
||||||
|
uint8_t mat = matGrid[v][u];
|
||||||
|
|
||||||
|
// Expand width (along u)
|
||||||
|
int w = 1;
|
||||||
|
while (u + w < CHUNK_SIZE && !visited[v][u + w] && matGrid[v][u + w] == mat) {
|
||||||
|
w++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand height (along v)
|
||||||
|
int h = 1;
|
||||||
|
bool canExpand = true;
|
||||||
|
while (v + h < CHUNK_SIZE && canExpand) {
|
||||||
|
for (int du = 0; du < w; du++) {
|
||||||
|
if (visited[v + h][u + du] || matGrid[v + h][u + du] != mat) {
|
||||||
|
canExpand = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (canExpand) h++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as visited
|
||||||
|
for (int dv = 0; dv < h; dv++) {
|
||||||
|
for (int du = 0; du < w; du++) {
|
||||||
|
visited[v + dv][u + du] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the actual position in chunk-local coords
|
||||||
|
uint8_t px, py, pz;
|
||||||
|
switch (face) {
|
||||||
|
case FACE_POS_X: px = (uint8_t)depth; py = (uint8_t)u; pz = (uint8_t)v; break;
|
||||||
|
case FACE_NEG_X: px = (uint8_t)depth; py = (uint8_t)u; pz = (uint8_t)v; break;
|
||||||
|
case FACE_POS_Y: px = (uint8_t)u; py = (uint8_t)depth; pz = (uint8_t)v; break;
|
||||||
|
case FACE_NEG_Y: px = (uint8_t)u; py = (uint8_t)depth; pz = (uint8_t)v; break;
|
||||||
|
case FACE_POS_Z: px = (uint8_t)u; py = (uint8_t)v; pz = (uint8_t)depth; break;
|
||||||
|
case FACE_NEG_Z: px = (uint8_t)u; py = (uint8_t)v; pz = (uint8_t)depth; break;
|
||||||
|
default: px = py = pz = 0; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width/height in the quad's local UV space
|
||||||
|
uint8_t qw, qh;
|
||||||
|
switch (face) {
|
||||||
|
case FACE_POS_X: case FACE_NEG_X: qw = (uint8_t)w; qh = (uint8_t)h; break;
|
||||||
|
case FACE_POS_Y: case FACE_NEG_Y: qw = (uint8_t)w; qh = (uint8_t)h; break;
|
||||||
|
case FACE_POS_Z: case FACE_NEG_Z: qw = (uint8_t)w; qh = (uint8_t)h; break;
|
||||||
|
default: qw = qh = 1; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
outQuads.push_back(PackedQuad::create(
|
||||||
|
px, py, pz, qw, qh, face, mat, 0, 0
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t VoxelMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) {
|
||||||
|
chunk.quads.clear();
|
||||||
|
|
||||||
|
// Step 1: Build binary solid masks per axis
|
||||||
|
AxisMasks axisMasks[3];
|
||||||
|
buildAxisMasks(chunk, axisMasks);
|
||||||
|
|
||||||
|
// Step 2: For each face direction, compute visible face masks
|
||||||
|
// then do greedy merge
|
||||||
|
for (uint8_t face = 0; face < FACE_COUNT; face++) {
|
||||||
|
int axis = face / 2; // 0=X, 1=Y, 2=Z
|
||||||
|
bool positive = (face % 2 == 0);
|
||||||
|
|
||||||
|
// Compute visible face masks
|
||||||
|
// A face is visible if the voxel is solid and the neighbor in the face direction is air
|
||||||
|
uint32_t faceMasks[CHUNK_SIZE][CHUNK_SIZE];
|
||||||
|
std::memset(faceMasks, 0, sizeof(faceMasks));
|
||||||
|
|
||||||
|
for (int v = 0; v < CHUNK_SIZE; v++) {
|
||||||
|
for (int u = 0; u < CHUNK_SIZE; u++) {
|
||||||
|
uint32_t solid = axisMasks[axis].solid[u][v];
|
||||||
|
if (solid == 0) continue;
|
||||||
|
|
||||||
|
uint32_t visible;
|
||||||
|
if (positive) {
|
||||||
|
// +dir: face visible if solid here and NOT solid at pos+1
|
||||||
|
// Shift right: neighbor is at bit+1
|
||||||
|
uint32_t neighbor = (solid >> 1);
|
||||||
|
// The highest bit has no neighbor in this chunk - check boundary
|
||||||
|
visible = solid & ~neighbor;
|
||||||
|
// Bit 31 (chunk boundary): need to check neighbor chunk
|
||||||
|
// For now, always show boundary faces
|
||||||
|
if (solid & (1u << (CHUNK_SIZE - 1))) {
|
||||||
|
// Check if neighbor chunk's voxel is empty
|
||||||
|
int nx, ny, nz;
|
||||||
|
switch (axis) {
|
||||||
|
case 0: nx = CHUNK_SIZE; ny = u; nz = v; break; // X+
|
||||||
|
case 1: nx = u; ny = CHUNK_SIZE; nz = v; break; // Y+
|
||||||
|
case 2: nx = u; ny = v; nz = CHUNK_SIZE; break; // Z+
|
||||||
|
default: nx = ny = nz = 0;
|
||||||
|
}
|
||||||
|
if (!getVoxelSafe(chunk, world, nx, ny, nz).isEmpty()) {
|
||||||
|
visible &= ~(1u << (CHUNK_SIZE - 1)); // hide boundary face
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// -dir: face visible if solid here and NOT solid at pos-1
|
||||||
|
uint32_t neighbor = (solid << 1);
|
||||||
|
visible = solid & ~neighbor;
|
||||||
|
// Bit 0 (chunk boundary)
|
||||||
|
if (solid & 1u) {
|
||||||
|
int nx, ny, nz;
|
||||||
|
switch (axis) {
|
||||||
|
case 0: nx = -1; ny = u; nz = v; break;
|
||||||
|
case 1: nx = u; ny = -1; nz = v; break;
|
||||||
|
case 2: nx = u; ny = v; nz = -1; break;
|
||||||
|
default: nx = ny = nz = 0;
|
||||||
|
}
|
||||||
|
if (!getVoxelSafe(chunk, world, nx, ny, nz).isEmpty()) {
|
||||||
|
visible &= ~1u; // hide boundary face
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
faceMasks[u][v] = visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t beforeCount = (uint32_t)chunk.quads.size();
|
||||||
|
greedyMerge(chunk, world, face, faceMasks, chunk.quads);
|
||||||
|
chunk.faceOffsets[face] = beforeCount;
|
||||||
|
chunk.faceCounts[face] = (uint32_t)chunk.quads.size() - beforeCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.quadCount = (uint32_t)chunk.quads.size();
|
||||||
|
chunk.dirty = false;
|
||||||
|
return chunk.quadCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t VoxelMesher::calcAO(const VoxelWorld& world, const ChunkPos& cpos,
|
||||||
|
int x, int y, int z, uint8_t face) {
|
||||||
|
// Simplified AO: count occluding neighbors around the vertex
|
||||||
|
// Returns packed 4x2-bit AO for the 4 corners
|
||||||
|
// TODO: implement proper per-corner AO
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
40
src/voxel/VoxelMesher.h
Normal file
40
src/voxel/VoxelMesher.h
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
#pragma once
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include "VoxelWorld.h"
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
// ── Binary Greedy Mesher (CPU implementation, port of cgerikj) ──
|
||||||
|
// Generates PackedQuad list for a chunk using binary greedy meshing.
|
||||||
|
// For each axis, uses bitmask operations to find visible faces,
|
||||||
|
// then greedily merges same-material quads.
|
||||||
|
class VoxelMesher {
|
||||||
|
public:
|
||||||
|
// Mesh a single chunk, populating chunk.quads
|
||||||
|
// Returns number of quads generated
|
||||||
|
static uint32_t meshChunk(Chunk& chunk, const VoxelWorld& world);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Per-axis binary face culling
|
||||||
|
// col_masks[axis][u][v] = 32-bit mask of solid voxels along axis
|
||||||
|
struct AxisMasks {
|
||||||
|
uint32_t solid[CHUNK_SIZE][CHUNK_SIZE]; // solid[u][v] = bitmask along axis
|
||||||
|
};
|
||||||
|
|
||||||
|
static void buildAxisMasks(const Chunk& chunk, AxisMasks masks[3]);
|
||||||
|
|
||||||
|
// Greedy merge faces of same material into larger quads
|
||||||
|
static void greedyMerge(
|
||||||
|
const Chunk& chunk,
|
||||||
|
const VoxelWorld& world,
|
||||||
|
uint8_t face,
|
||||||
|
const uint32_t faceMasks[CHUNK_SIZE][CHUNK_SIZE],
|
||||||
|
std::vector<PackedQuad>& outQuads
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate ambient occlusion for quad corners
|
||||||
|
static uint8_t calcAO(const VoxelWorld& world, const ChunkPos& cpos,
|
||||||
|
int x, int y, int z, uint8_t face);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
597
src/voxel/VoxelRenderer.cpp
Normal file
597
src/voxel/VoxelRenderer.cpp
Normal file
|
|
@ -0,0 +1,597 @@
|
||||||
|
#include "VoxelRenderer.h"
|
||||||
|
#include "wiPrimitive.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
using namespace wi::graphics;
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
// ── VoxelRenderer Implementation ────────────────────────────────
|
||||||
|
|
||||||
|
VoxelRenderer::VoxelRenderer() = default;
|
||||||
|
VoxelRenderer::~VoxelRenderer() { shutdown(); }
|
||||||
|
|
||||||
|
void VoxelRenderer::initialize(GraphicsDevice* dev) {
|
||||||
|
device_ = dev;
|
||||||
|
if (!device_) return;
|
||||||
|
|
||||||
|
createPipeline();
|
||||||
|
if (!pso_.IsValid()) {
|
||||||
|
wi::backlog::post("VoxelRenderer: pipeline creation failed", wi::backlog::LogLevel::Error);
|
||||||
|
initialized_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
generateTextures();
|
||||||
|
|
||||||
|
// Create mega quad buffer (SRV for vertex pulling)
|
||||||
|
GPUBufferDesc megaDesc;
|
||||||
|
megaDesc.size = MEGA_BUFFER_CAPACITY * sizeof(PackedQuad);
|
||||||
|
megaDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
|
megaDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
|
megaDesc.stride = sizeof(PackedQuad);
|
||||||
|
megaDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&megaDesc, nullptr, &megaQuadBuffer_);
|
||||||
|
|
||||||
|
// Create chunk info buffer (SRV for VS chunk lookup)
|
||||||
|
GPUBufferDesc infoDesc;
|
||||||
|
infoDesc.size = MAX_CHUNKS * sizeof(GPUChunkInfo);
|
||||||
|
infoDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
|
infoDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
|
infoDesc.stride = sizeof(GPUChunkInfo);
|
||||||
|
infoDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&infoDesc, nullptr, &chunkInfoBuffer_);
|
||||||
|
|
||||||
|
// Create indirect args buffer (for DrawInstancedIndirectCount, up to 6 draws per chunk)
|
||||||
|
// UAV bind flag needed for GPU cull compute shader to write args
|
||||||
|
GPUBufferDesc argsDesc;
|
||||||
|
argsDesc.size = MAX_DRAWS * sizeof(IndirectDrawArgs);
|
||||||
|
argsDesc.bind_flags = BindFlag::UNORDERED_ACCESS;
|
||||||
|
argsDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED | ResourceMiscFlag::INDIRECT_ARGS;
|
||||||
|
argsDesc.stride = sizeof(IndirectDrawArgs);
|
||||||
|
argsDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&argsDesc, nullptr, &indirectArgsBuffer_);
|
||||||
|
|
||||||
|
// Create draw count buffer (single uint32, raw for RWByteAddressBuffer)
|
||||||
|
// UAV bind flag needed for GPU cull compute shader atomic counter
|
||||||
|
GPUBufferDesc countDesc;
|
||||||
|
countDesc.size = sizeof(uint32_t);
|
||||||
|
countDesc.bind_flags = BindFlag::UNORDERED_ACCESS;
|
||||||
|
countDesc.misc_flags = ResourceMiscFlag::BUFFER_RAW | ResourceMiscFlag::INDIRECT_ARGS;
|
||||||
|
countDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&countDesc, nullptr, &drawCountBuffer_);
|
||||||
|
|
||||||
|
// ── GPU Timestamp Queries ──────────────────────────────────────
|
||||||
|
GPUQueryHeapDesc queryDesc;
|
||||||
|
queryDesc.type = GpuQueryType::TIMESTAMP;
|
||||||
|
queryDesc.query_count = TS_COUNT;
|
||||||
|
device_->CreateQueryHeap(&queryDesc, ×tampHeap_);
|
||||||
|
|
||||||
|
GPUBufferDesc readbackDesc;
|
||||||
|
readbackDesc.size = TS_COUNT * sizeof(uint64_t);
|
||||||
|
readbackDesc.usage = Usage::READBACK;
|
||||||
|
device_->CreateBuffer(&readbackDesc, nullptr, ×tampReadback_);
|
||||||
|
|
||||||
|
// ── GPU Compute Mesher resources ─────────────────────────────
|
||||||
|
wi::renderer::LoadShader(ShaderStage::CS, meshShader_, "voxel/voxelMeshCS.cso");
|
||||||
|
gpuMesherAvailable_ = meshShader_.IsValid();
|
||||||
|
if (gpuMesherAvailable_) {
|
||||||
|
// Voxel data buffer: 1 chunk's worth (32^3 voxels / 2 per uint = 16384 uint)
|
||||||
|
GPUBufferDesc voxDesc;
|
||||||
|
voxDesc.size = (CHUNK_VOLUME / 2) * sizeof(uint32_t);
|
||||||
|
voxDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
|
voxDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
|
voxDesc.stride = sizeof(uint32_t);
|
||||||
|
voxDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&voxDesc, nullptr, &voxelDataBuffer_);
|
||||||
|
|
||||||
|
// GPU quad output: same capacity as mega-buffer
|
||||||
|
GPUBufferDesc gpuQDesc;
|
||||||
|
gpuQDesc.size = MEGA_BUFFER_CAPACITY * sizeof(uint64_t); // PackedQuad = 8 bytes
|
||||||
|
gpuQDesc.bind_flags = BindFlag::UNORDERED_ACCESS;
|
||||||
|
gpuQDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||||
|
gpuQDesc.stride = sizeof(uint64_t); // uint2 = 8 bytes
|
||||||
|
gpuQDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&gpuQDesc, nullptr, &gpuQuadBuffer_);
|
||||||
|
|
||||||
|
// Quad counter
|
||||||
|
GPUBufferDesc cntDesc;
|
||||||
|
cntDesc.size = sizeof(uint32_t);
|
||||||
|
cntDesc.bind_flags = BindFlag::UNORDERED_ACCESS;
|
||||||
|
cntDesc.misc_flags = ResourceMiscFlag::BUFFER_RAW;
|
||||||
|
cntDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&cntDesc, nullptr, &gpuQuadCounter_);
|
||||||
|
|
||||||
|
wi::backlog::post("VoxelRenderer: GPU compute mesher available");
|
||||||
|
} else {
|
||||||
|
wi::backlog::post("VoxelRenderer: GPU compute mesher not available", wi::backlog::LogLevel::Warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuMegaQuads_.reserve(MEGA_BUFFER_CAPACITY);
|
||||||
|
cpuChunkInfo_.reserve(MAX_CHUNKS);
|
||||||
|
chunkSlots_.reserve(MAX_CHUNKS);
|
||||||
|
cpuIndirectArgs_.reserve(MAX_CHUNKS);
|
||||||
|
|
||||||
|
initialized_ = true;
|
||||||
|
wi::backlog::post("VoxelRenderer: initialized (mega-buffer: "
|
||||||
|
+ std::to_string(MEGA_BUFFER_CAPACITY) + " quads capacity)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderer::shutdown() {
|
||||||
|
chunkSlots_.clear();
|
||||||
|
cpuChunkInfo_.clear();
|
||||||
|
cpuMegaQuads_.clear();
|
||||||
|
initialized_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderer::createPipeline() {
|
||||||
|
// Constant buffer for per-frame data
|
||||||
|
GPUBufferDesc cbDesc;
|
||||||
|
cbDesc.size = sizeof(VoxelConstants);
|
||||||
|
cbDesc.bind_flags = BindFlag::CONSTANT_BUFFER;
|
||||||
|
cbDesc.usage = Usage::DEFAULT;
|
||||||
|
device_->CreateBuffer(&cbDesc, nullptr, &constantBuffer_);
|
||||||
|
|
||||||
|
// Anisotropic wrap sampler
|
||||||
|
SamplerDesc samplerDesc;
|
||||||
|
samplerDesc.filter = Filter::ANISOTROPIC;
|
||||||
|
samplerDesc.address_u = TextureAddressMode::WRAP;
|
||||||
|
samplerDesc.address_v = TextureAddressMode::WRAP;
|
||||||
|
samplerDesc.address_w = TextureAddressMode::WRAP;
|
||||||
|
samplerDesc.max_anisotropy = 16;
|
||||||
|
device_->CreateSampler(&samplerDesc, &sampler_);
|
||||||
|
|
||||||
|
// Load shaders
|
||||||
|
wi::renderer::LoadShader(ShaderStage::VS, vertexShader_, "voxel/voxelVS.cso");
|
||||||
|
wi::renderer::LoadShader(ShaderStage::PS, pixelShader_, "voxel/voxelPS.cso");
|
||||||
|
wi::renderer::LoadShader(ShaderStage::CS, cullShader_, "voxel/voxelCullCS.cso");
|
||||||
|
|
||||||
|
if (!vertexShader_.IsValid() || !pixelShader_.IsValid()) {
|
||||||
|
wi::backlog::post("VoxelRenderer: shader loading failed", wi::backlog::LogLevel::Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gpuCullingEnabled_ = cullShader_.IsValid();
|
||||||
|
if (!gpuCullingEnabled_) {
|
||||||
|
wi::backlog::post("VoxelRenderer: cull compute shader not available, using CPU culling", wi::backlog::LogLevel::Warning);
|
||||||
|
} else {
|
||||||
|
wi::backlog::post("VoxelRenderer: GPU frustum+backface culling enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipeline: backface cull, depth test, opaque blend, triangle list
|
||||||
|
PipelineStateDesc psoDesc;
|
||||||
|
psoDesc.vs = &vertexShader_;
|
||||||
|
psoDesc.ps = &pixelShader_;
|
||||||
|
psoDesc.rs = wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT);
|
||||||
|
psoDesc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEFAULT);
|
||||||
|
psoDesc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_OPAQUE);
|
||||||
|
psoDesc.pt = PrimitiveTopology::TRIANGLELIST;
|
||||||
|
|
||||||
|
device_->CreatePipelineState(&psoDesc, &pso_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Procedural texture generation ───────────────────────────────
|
||||||
|
|
||||||
|
static void generateNoiseTexture(uint8_t* pixels, int w, int h,
|
||||||
|
uint8_t r0, uint8_t g0, uint8_t b0,
|
||||||
|
uint8_t r1, uint8_t g1, uint8_t b1,
|
||||||
|
uint32_t seed)
|
||||||
|
{
|
||||||
|
uint32_t s = seed;
|
||||||
|
for (int y = 0; y < h; y++) {
|
||||||
|
for (int x = 0; x < w; x++) {
|
||||||
|
s = s * 1664525u + 1013904223u;
|
||||||
|
float noise = (float)(s & 0xFFFF) / 65535.0f;
|
||||||
|
float fx = (float)x / w;
|
||||||
|
float fy = (float)y / h;
|
||||||
|
float pattern = 0.5f + 0.5f * std::sin(fx * 20.0f + noise * 3.0f) *
|
||||||
|
std::cos(fy * 20.0f + noise * 3.0f);
|
||||||
|
float t = noise * 0.6f + pattern * 0.4f;
|
||||||
|
|
||||||
|
int idx = (y * w + x) * 4;
|
||||||
|
pixels[idx + 0] = (uint8_t)(r0 + (r1 - r0) * t);
|
||||||
|
pixels[idx + 1] = (uint8_t)(g0 + (g1 - g0) * t);
|
||||||
|
pixels[idx + 2] = (uint8_t)(b0 + (b1 - b0) * t);
|
||||||
|
pixels[idx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderer::generateTextures() {
|
||||||
|
const int TEX_SIZE = 256;
|
||||||
|
const int NUM_MATERIALS = 5;
|
||||||
|
|
||||||
|
std::vector<uint8_t> allPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS);
|
||||||
|
|
||||||
|
struct MatColor { uint8_t r0,g0,b0, r1,g1,b1; uint32_t seed; };
|
||||||
|
MatColor colors[NUM_MATERIALS] = {
|
||||||
|
{ 60, 140, 40, 80, 180, 60, 101 }, // Grass
|
||||||
|
{ 100, 70, 40, 140, 100, 60, 202 }, // Dirt
|
||||||
|
{ 110, 110, 105, 140, 140, 130, 303 }, // Stone
|
||||||
|
{ 200, 190, 140, 230, 220, 170, 404 }, // Sand
|
||||||
|
{ 220, 225, 230, 245, 248, 252, 505 }, // Snow
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_MATERIALS; i++) {
|
||||||
|
auto& c = colors[i];
|
||||||
|
generateNoiseTexture(
|
||||||
|
allPixels.data() + i * TEX_SIZE * TEX_SIZE * 4,
|
||||||
|
TEX_SIZE, TEX_SIZE,
|
||||||
|
c.r0, c.g0, c.b0, c.r1, c.g1, c.b1, c.seed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextureDesc texDesc;
|
||||||
|
texDesc.type = TextureDesc::Type::TEXTURE_2D;
|
||||||
|
texDesc.width = TEX_SIZE;
|
||||||
|
texDesc.height = TEX_SIZE;
|
||||||
|
texDesc.array_size = NUM_MATERIALS;
|
||||||
|
texDesc.mip_levels = 1;
|
||||||
|
texDesc.format = Format::R8G8B8A8_UNORM;
|
||||||
|
texDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||||
|
texDesc.usage = Usage::DEFAULT;
|
||||||
|
|
||||||
|
std::vector<SubresourceData> subData(NUM_MATERIALS);
|
||||||
|
for (int i = 0; i < NUM_MATERIALS; i++) {
|
||||||
|
subData[i].data_ptr = allPixels.data() + i * TEX_SIZE * TEX_SIZE * 4;
|
||||||
|
subData[i].row_pitch = TEX_SIZE * 4;
|
||||||
|
subData[i].slice_pitch = TEX_SIZE * TEX_SIZE * 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
device_->CreateTexture(&texDesc, subData.data(), &textureArray_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mega-buffer rebuild ─────────────────────────────────────────
|
||||||
|
// Packs all chunk quads contiguously into a single buffer.
|
||||||
|
// Simple strategy: full rebuild whenever any chunk is dirty.
|
||||||
|
|
||||||
|
void VoxelRenderer::rebuildMegaBuffer(VoxelWorld& world) {
|
||||||
|
cpuMegaQuads_.clear();
|
||||||
|
chunkSlots_.clear();
|
||||||
|
cpuChunkInfo_.clear();
|
||||||
|
|
||||||
|
uint32_t offset = 0;
|
||||||
|
float debugFlag = debugFaceColors_ ? 1.0f : 0.0f;
|
||||||
|
|
||||||
|
world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
|
||||||
|
if (chunk.quadCount == 0) return;
|
||||||
|
if (offset + chunk.quadCount > MEGA_BUFFER_CAPACITY) return; // overflow guard
|
||||||
|
|
||||||
|
ChunkSlot slot;
|
||||||
|
slot.pos = pos;
|
||||||
|
slot.quadOffset = offset;
|
||||||
|
slot.quadCount = chunk.quadCount;
|
||||||
|
chunkSlots_.push_back(slot);
|
||||||
|
|
||||||
|
GPUChunkInfo info = {};
|
||||||
|
info.worldPos = XMFLOAT4(
|
||||||
|
(float)(pos.x * CHUNK_SIZE),
|
||||||
|
(float)(pos.y * CHUNK_SIZE),
|
||||||
|
(float)(pos.z * CHUNK_SIZE),
|
||||||
|
debugFlag
|
||||||
|
);
|
||||||
|
info.quadOffset = offset;
|
||||||
|
info.quadCount = chunk.quadCount;
|
||||||
|
for (int f = 0; f < 6; f++) {
|
||||||
|
info.faceOffsets[f] = chunk.faceOffsets[f];
|
||||||
|
info.faceCounts[f] = chunk.faceCounts[f];
|
||||||
|
}
|
||||||
|
cpuChunkInfo_.push_back(info);
|
||||||
|
|
||||||
|
cpuMegaQuads_.insert(cpuMegaQuads_.end(), chunk.quads.begin(), chunk.quads.end());
|
||||||
|
offset += chunk.quadCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
chunkCount_ = (uint32_t)chunkSlots_.size();
|
||||||
|
totalQuads_ = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderer::updateMeshes(VoxelWorld& world) {
|
||||||
|
if (!device_) return;
|
||||||
|
|
||||||
|
// Re-mesh dirty chunks
|
||||||
|
bool anyDirty = false;
|
||||||
|
world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
|
||||||
|
if (chunk.dirty) {
|
||||||
|
VoxelMesher::meshChunk(chunk, world);
|
||||||
|
anyDirty = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (anyDirty || megaBufferDirty_) {
|
||||||
|
rebuildMegaBuffer(world);
|
||||||
|
megaBufferDirty_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Render pass ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void VoxelRenderer::render(
|
||||||
|
CommandList cmd,
|
||||||
|
const wi::scene::CameraComponent& camera,
|
||||||
|
const Texture& depthBuffer,
|
||||||
|
const Texture& renderTarget
|
||||||
|
) const {
|
||||||
|
if (!initialized_ || chunkCount_ == 0 || !pso_.IsValid()) return;
|
||||||
|
|
||||||
|
auto* dev = device_;
|
||||||
|
|
||||||
|
// Upload mega-buffer and chunk info to GPU
|
||||||
|
if (!cpuMegaQuads_.empty()) {
|
||||||
|
dev->UpdateBuffer(&megaQuadBuffer_, cpuMegaQuads_.data(), cmd,
|
||||||
|
cpuMegaQuads_.size() * sizeof(PackedQuad));
|
||||||
|
}
|
||||||
|
if (!cpuChunkInfo_.empty()) {
|
||||||
|
dev->UpdateBuffer(&chunkInfoBuffer_, cpuChunkInfo_.data(), cmd,
|
||||||
|
cpuChunkInfo_.size() * sizeof(GPUChunkInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-frame constants
|
||||||
|
VoxelConstants cb = {};
|
||||||
|
XMStoreFloat4x4(&cb.viewProjection, camera.GetViewProjection());
|
||||||
|
cb.cameraPosition = XMFLOAT4(camera.Eye.x, camera.Eye.y, camera.Eye.z, 1.0f);
|
||||||
|
cb.sunDirection = XMFLOAT4(-0.5f, -0.8f, -0.3f, 0.0f);
|
||||||
|
cb.sunColor = XMFLOAT4(1.2f, 1.1f, 0.9f, 1.0f);
|
||||||
|
cb.chunkSize = (float)CHUNK_SIZE;
|
||||||
|
cb.textureTiling = 0.25f;
|
||||||
|
cb.chunkCount = chunkCount_;
|
||||||
|
dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb));
|
||||||
|
|
||||||
|
// CPU frustum culling
|
||||||
|
wi::primitive::Frustum frustum;
|
||||||
|
frustum.Create(camera.GetViewProjection());
|
||||||
|
|
||||||
|
// ── Render pass: color + depth ────────────────────────────────
|
||||||
|
RenderPassImage rp[] = {
|
||||||
|
RenderPassImage::RenderTarget(
|
||||||
|
&renderTarget,
|
||||||
|
RenderPassImage::LoadOp::CLEAR,
|
||||||
|
RenderPassImage::StoreOp::STORE,
|
||||||
|
ResourceState::SHADER_RESOURCE,
|
||||||
|
ResourceState::SHADER_RESOURCE
|
||||||
|
),
|
||||||
|
RenderPassImage::DepthStencil(
|
||||||
|
&depthBuffer,
|
||||||
|
RenderPassImage::LoadOp::CLEAR,
|
||||||
|
RenderPassImage::StoreOp::STORE,
|
||||||
|
ResourceState::DEPTHSTENCIL,
|
||||||
|
ResourceState::DEPTHSTENCIL,
|
||||||
|
ResourceState::DEPTHSTENCIL
|
||||||
|
),
|
||||||
|
};
|
||||||
|
dev->RenderPassBegin(rp, 2, cmd);
|
||||||
|
|
||||||
|
Viewport vp;
|
||||||
|
vp.width = (float)renderTarget.GetDesc().width;
|
||||||
|
vp.height = (float)renderTarget.GetDesc().height;
|
||||||
|
vp.min_depth = 0.0f;
|
||||||
|
vp.max_depth = 1.0f;
|
||||||
|
dev->BindViewports(1, &vp, cmd);
|
||||||
|
|
||||||
|
Rect scissor = { 0, 0, (int)vp.width, (int)vp.height };
|
||||||
|
dev->BindScissorRects(1, &scissor, cmd);
|
||||||
|
|
||||||
|
dev->BindPipelineState(&pso_, cmd);
|
||||||
|
dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
|
||||||
|
dev->BindResource(&megaQuadBuffer_, 0, cmd); // t0: mega quad buffer
|
||||||
|
dev->BindResource(&textureArray_, 1, cmd); // t1: material textures
|
||||||
|
dev->BindResource(&chunkInfoBuffer_, 2, cmd); // t2: chunk info
|
||||||
|
dev->BindSampler(&sampler_, 0, cmd);
|
||||||
|
|
||||||
|
visibleChunks_ = 0;
|
||||||
|
drawCalls_ = 0;
|
||||||
|
|
||||||
|
// Push constant structure (must be 48 bytes = 12 x uint32, matches b999)
|
||||||
|
struct VoxelPush {
|
||||||
|
uint32_t chunkIndex;
|
||||||
|
uint32_t quadOffset; // offset into mega quad buffer (in quads)
|
||||||
|
uint32_t pad[10];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simple DrawInstanced loop with frustum culling + push constants
|
||||||
|
for (uint32_t i = 0; i < chunkCount_; i++) {
|
||||||
|
const auto& slot = chunkSlots_[i];
|
||||||
|
if (slot.quadCount == 0) continue;
|
||||||
|
|
||||||
|
XMFLOAT3 aabbMin(
|
||||||
|
(float)(slot.pos.x * CHUNK_SIZE),
|
||||||
|
(float)(slot.pos.y * CHUNK_SIZE),
|
||||||
|
(float)(slot.pos.z * CHUNK_SIZE)
|
||||||
|
);
|
||||||
|
XMFLOAT3 aabbMax(
|
||||||
|
aabbMin.x + CHUNK_SIZE,
|
||||||
|
aabbMin.y + CHUNK_SIZE,
|
||||||
|
aabbMin.z + CHUNK_SIZE
|
||||||
|
);
|
||||||
|
wi::primitive::AABB aabb(aabbMin, aabbMax);
|
||||||
|
if (!frustum.CheckBoxFast(aabb)) continue;
|
||||||
|
|
||||||
|
visibleChunks_++;
|
||||||
|
|
||||||
|
// Pass chunk index AND quad offset via push constants
|
||||||
|
// (SV_VertexID/SV_InstanceID offsets unreliable across drivers)
|
||||||
|
VoxelPush pushData = {};
|
||||||
|
pushData.chunkIndex = i;
|
||||||
|
pushData.quadOffset = slot.quadOffset;
|
||||||
|
dev->PushConstants(&pushData, sizeof(pushData), cmd);
|
||||||
|
|
||||||
|
// startVertexLocation = 0: the VS computes quad address from push.quadOffset
|
||||||
|
dev->DrawInstanced(slot.quadCount * 6, 1, 0, 0, cmd);
|
||||||
|
drawCalls_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
dev->RenderPassEnd(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── VoxelRenderPath (custom RenderPath3D) ───────────────────────
|
||||||
|
|
||||||
|
void VoxelRenderPath::Start() {
|
||||||
|
RenderPath3D::Start();
|
||||||
|
|
||||||
|
auto* device = wi::graphics::GetDevice();
|
||||||
|
renderer.initialize(device);
|
||||||
|
renderer.debugFaceColors_ = debugMode;
|
||||||
|
|
||||||
|
// Generate world
|
||||||
|
if (debugMode) {
|
||||||
|
world.generateDebug();
|
||||||
|
cameraPos = { 10.0f, 10.0f, 0.0f };
|
||||||
|
cameraPitch = -0.4f;
|
||||||
|
cameraYaw = 0.5f;
|
||||||
|
} else {
|
||||||
|
world.generateAround(cameraPos.x, cameraPos.y, cameraPos.z, 4);
|
||||||
|
}
|
||||||
|
if (renderer.isInitialized()) {
|
||||||
|
renderer.updateMeshes(world);
|
||||||
|
}
|
||||||
|
worldGenerated_ = true;
|
||||||
|
|
||||||
|
setAO(AO_DISABLED);
|
||||||
|
setFXAAEnabled(true);
|
||||||
|
setBloomEnabled(false);
|
||||||
|
|
||||||
|
createRenderTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderPath::createRenderTargets() {
|
||||||
|
auto* device = wi::graphics::GetDevice();
|
||||||
|
if (!device) return;
|
||||||
|
|
||||||
|
uint32_t w = GetPhysicalWidth();
|
||||||
|
uint32_t h = GetPhysicalHeight();
|
||||||
|
if (w == 0 || h == 0) { w = 1920; h = 1080; }
|
||||||
|
|
||||||
|
wi::graphics::TextureDesc rtDesc;
|
||||||
|
rtDesc.type = wi::graphics::TextureDesc::Type::TEXTURE_2D;
|
||||||
|
rtDesc.width = w;
|
||||||
|
rtDesc.height = h;
|
||||||
|
rtDesc.format = wi::graphics::Format::R8G8B8A8_UNORM;
|
||||||
|
rtDesc.bind_flags = wi::graphics::BindFlag::RENDER_TARGET | wi::graphics::BindFlag::SHADER_RESOURCE;
|
||||||
|
rtDesc.mip_levels = 1;
|
||||||
|
rtDesc.sample_count = 1;
|
||||||
|
rtDesc.layout = wi::graphics::ResourceState::SHADER_RESOURCE;
|
||||||
|
device->CreateTexture(&rtDesc, nullptr, &voxelRT_);
|
||||||
|
|
||||||
|
wi::graphics::TextureDesc depthDesc;
|
||||||
|
depthDesc.type = wi::graphics::TextureDesc::Type::TEXTURE_2D;
|
||||||
|
depthDesc.width = w;
|
||||||
|
depthDesc.height = h;
|
||||||
|
depthDesc.format = wi::graphics::Format::D32_FLOAT;
|
||||||
|
depthDesc.bind_flags = wi::graphics::BindFlag::DEPTH_STENCIL | wi::graphics::BindFlag::SHADER_RESOURCE;
|
||||||
|
depthDesc.mip_levels = 1;
|
||||||
|
depthDesc.sample_count = 1;
|
||||||
|
depthDesc.layout = wi::graphics::ResourceState::DEPTHSTENCIL;
|
||||||
|
device->CreateTexture(&depthDesc, nullptr, &voxelDepth_);
|
||||||
|
|
||||||
|
rtCreated_ = voxelRT_.IsValid() && voxelDepth_.IsValid();
|
||||||
|
wi::backlog::post("VoxelRenderPath: render targets " + std::string(rtCreated_ ? "OK" : "FAILED")
|
||||||
|
+ " (" + std::to_string(w) + "x" + std::to_string(h) + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── WASD camera input ───────────────────────────────────────────
|
||||||
|
|
||||||
|
static constexpr wi::input::BUTTON KEY_W = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('W' - 'A'));
|
||||||
|
static constexpr wi::input::BUTTON KEY_A = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('A' - 'A'));
|
||||||
|
static constexpr wi::input::BUTTON KEY_S = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('S' - 'A'));
|
||||||
|
static constexpr wi::input::BUTTON KEY_D = (wi::input::BUTTON)(wi::input::CHARACTER_RANGE_START + ('D' - 'A'));
|
||||||
|
|
||||||
|
void VoxelRenderPath::handleInput(float dt) {
|
||||||
|
if (wi::input::Press(wi::input::MOUSE_BUTTON_RIGHT)) {
|
||||||
|
mouseCaptured = !mouseCaptured;
|
||||||
|
wi::input::HidePointer(mouseCaptured);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouseCaptured) {
|
||||||
|
auto mouseState = wi::input::GetMouseState();
|
||||||
|
cameraYaw += mouseState.delta_position.x * cameraSensitivity;
|
||||||
|
cameraPitch += mouseState.delta_position.y * cameraSensitivity;
|
||||||
|
cameraPitch = std::clamp(cameraPitch, -1.5f, 1.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
float cosPitch = std::cos(cameraPitch);
|
||||||
|
XMFLOAT3 forward(
|
||||||
|
std::sin(cameraYaw) * cosPitch,
|
||||||
|
-std::sin(cameraPitch),
|
||||||
|
std::cos(cameraYaw) * cosPitch
|
||||||
|
);
|
||||||
|
XMFLOAT3 right(std::cos(cameraYaw), 0.0f, -std::sin(cameraYaw));
|
||||||
|
|
||||||
|
float speed = cameraSpeed * dt;
|
||||||
|
if (wi::input::Down(wi::input::KEYBOARD_BUTTON_LSHIFT)) speed *= 3.0f;
|
||||||
|
|
||||||
|
if (wi::input::Down(KEY_W)) { cameraPos.x += forward.x * speed; cameraPos.y += forward.y * speed; cameraPos.z += forward.z * speed; }
|
||||||
|
if (wi::input::Down(KEY_S)) { cameraPos.x -= forward.x * speed; cameraPos.y -= forward.y * speed; cameraPos.z -= forward.z * speed; }
|
||||||
|
if (wi::input::Down(KEY_A)) { cameraPos.x -= right.x * speed; cameraPos.z -= right.z * speed; }
|
||||||
|
if (wi::input::Down(KEY_D)) { cameraPos.x += right.x * speed; cameraPos.z += right.z * speed; }
|
||||||
|
if (wi::input::Down(wi::input::KEYBOARD_BUTTON_SPACE)) cameraPos.y += speed;
|
||||||
|
if (wi::input::Down(wi::input::KEYBOARD_BUTTON_LCONTROL)) cameraPos.y -= speed;
|
||||||
|
|
||||||
|
camera->Eye = cameraPos;
|
||||||
|
camera->At = forward;
|
||||||
|
camera->Up = XMFLOAT3(0, 1, 0);
|
||||||
|
camera->UpdateCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderPath::Update(float dt) {
|
||||||
|
lastDt_ = dt;
|
||||||
|
float instantFps = (dt > 0.0f) ? (1.0f / dt) : 0.0f;
|
||||||
|
smoothFps_ = smoothFps_ * 0.95f + instantFps * 0.05f;
|
||||||
|
if (camera) handleInput(dt);
|
||||||
|
if (renderer.isInitialized()) renderer.updateMeshes(world);
|
||||||
|
RenderPath3D::Update(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderPath::Render() const {
|
||||||
|
RenderPath3D::Render();
|
||||||
|
|
||||||
|
if (renderer.isInitialized() && camera && rtCreated_) {
|
||||||
|
auto* device = wi::graphics::GetDevice();
|
||||||
|
CommandList cmd = device->BeginCommandList();
|
||||||
|
renderer.render(cmd, *camera, voxelDepth_, voxelRT_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelRenderPath::Compose(CommandList cmd) const {
|
||||||
|
frameCount_++;
|
||||||
|
|
||||||
|
RenderPath3D::Compose(cmd);
|
||||||
|
|
||||||
|
if (rtCreated_ && voxelRT_.IsValid()) {
|
||||||
|
wi::image::Params fx;
|
||||||
|
fx.enableFullScreen();
|
||||||
|
fx.blendFlag = wi::enums::BLENDMODE_OPAQUE;
|
||||||
|
wi::image::Draw(&voxelRT_, fx, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HUD overlay
|
||||||
|
wi::font::Params fp;
|
||||||
|
fp.posX = 10; fp.posY = 10; fp.size = 20;
|
||||||
|
fp.color = wi::Color(255, 255, 255, 230);
|
||||||
|
fp.shadowColor = wi::Color(0, 0, 0, 180);
|
||||||
|
|
||||||
|
char fpsStr[16];
|
||||||
|
snprintf(fpsStr, sizeof(fpsStr), "%.1f", smoothFps_);
|
||||||
|
char dtStr[16];
|
||||||
|
snprintf(dtStr, sizeof(dtStr), "%.2f", lastDt_ * 1000.0f);
|
||||||
|
|
||||||
|
std::string stats = "BVLE Voxel Engine (Phase 2 — GPU-driven)\n";
|
||||||
|
stats += "FPS: " + std::string(fpsStr) + " (" + std::string(dtStr) + " ms)\n";
|
||||||
|
if (debugMode) {
|
||||||
|
stats += "=== DEBUG FACE MODE ===\n";
|
||||||
|
stats += "+X=Red -X=DkRed +Y=Green -Y=DkGreen +Z=Blue -Z=DkBlue\n";
|
||||||
|
}
|
||||||
|
stats += "Chunks: " + std::to_string(renderer.getVisibleChunks())
|
||||||
|
+ "/" + std::to_string(renderer.getChunkCount()) + "\n";
|
||||||
|
stats += "Quads: " + std::to_string(renderer.getTotalQuads()) + "\n";
|
||||||
|
stats += "Draw Calls: " + std::to_string(renderer.getDrawCalls())
|
||||||
|
+ " (DrawInstanced + CPU cull + backface)\n";
|
||||||
|
|
||||||
|
char cullStr[16], drawStr[16];
|
||||||
|
snprintf(cullStr, sizeof(cullStr), "%.3f", renderer.getGpuCullTimeMs());
|
||||||
|
snprintf(drawStr, sizeof(drawStr), "%.3f", renderer.getGpuDrawTimeMs());
|
||||||
|
stats += "GPU Cull: " + std::string(cullStr) + " ms | Draw: " + std::string(drawStr) + " ms\n";
|
||||||
|
stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse";
|
||||||
|
|
||||||
|
wi::font::Draw(stats, fp, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
180
src/voxel/VoxelRenderer.h
Normal file
180
src/voxel/VoxelRenderer.h
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
#pragma once
|
||||||
|
#include "VoxelWorld.h"
|
||||||
|
#include "VoxelMesher.h"
|
||||||
|
#include "WickedEngine.h"
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
// ── GPU-visible chunk info (must match HLSL GPUChunkInfo) ────────
|
||||||
|
struct GPUChunkInfo {
|
||||||
|
XMFLOAT4 worldPos; // xyz = chunk origin, w = debug flag
|
||||||
|
uint32_t quadOffset; // offset into mega quad buffer
|
||||||
|
uint32_t quadCount; // number of quads for this chunk
|
||||||
|
uint32_t pad[2]; // align to 32 bytes
|
||||||
|
uint32_t faceOffsets[6]; // per-face quad offset within this chunk's quads
|
||||||
|
uint32_t faceCounts[6]; // per-face quad count
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Voxel Renderer (Phase 2: mega-buffer + MDI pipeline) ────────
|
||||||
|
class VoxelRenderer {
|
||||||
|
public:
|
||||||
|
VoxelRenderer();
|
||||||
|
~VoxelRenderer();
|
||||||
|
|
||||||
|
void initialize(wi::graphics::GraphicsDevice* device);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
// Mesh dirty chunks and repack the mega-buffer
|
||||||
|
void updateMeshes(VoxelWorld& world);
|
||||||
|
|
||||||
|
// Render all visible chunks
|
||||||
|
void render(
|
||||||
|
wi::graphics::CommandList cmd,
|
||||||
|
const wi::scene::CameraComponent& camera,
|
||||||
|
const wi::graphics::Texture& depthBuffer,
|
||||||
|
const wi::graphics::Texture& renderTarget
|
||||||
|
) const;
|
||||||
|
|
||||||
|
// Generate procedural textures for materials
|
||||||
|
void generateTextures();
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
uint32_t getTotalQuads() const { return totalQuads_; }
|
||||||
|
uint32_t getVisibleChunks() const { return visibleChunks_; }
|
||||||
|
uint32_t getDrawCalls() const { return drawCalls_; }
|
||||||
|
uint32_t getChunkCount() const { return chunkCount_; }
|
||||||
|
bool isInitialized() const { return initialized_; }
|
||||||
|
bool isGpuCulling() const { return gpuCullingEnabled_; }
|
||||||
|
|
||||||
|
bool debugFaceColors_ = false;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void createPipeline();
|
||||||
|
void rebuildMegaBuffer(VoxelWorld& world);
|
||||||
|
|
||||||
|
wi::graphics::GraphicsDevice* device_ = nullptr;
|
||||||
|
|
||||||
|
// Shaders & Pipeline
|
||||||
|
wi::graphics::Shader vertexShader_;
|
||||||
|
wi::graphics::Shader pixelShader_;
|
||||||
|
wi::graphics::PipelineState pso_;
|
||||||
|
wi::graphics::Shader cullShader_; // Frustum cull compute shader
|
||||||
|
|
||||||
|
// Texture array for materials (256x256, 5 layers for prototype)
|
||||||
|
wi::graphics::Texture textureArray_;
|
||||||
|
wi::graphics::Sampler sampler_;
|
||||||
|
|
||||||
|
// ── Mega-buffer architecture (Phase 2) ──────────────────────
|
||||||
|
static constexpr uint32_t MEGA_BUFFER_CAPACITY = 2 * 1024 * 1024; // 2M quads max (16 MB)
|
||||||
|
static constexpr uint32_t MAX_CHUNKS = 2048;
|
||||||
|
static constexpr uint32_t MAX_DRAWS = MAX_CHUNKS * 6; // up to 6 face groups per chunk
|
||||||
|
|
||||||
|
wi::graphics::GPUBuffer megaQuadBuffer_; // StructuredBuffer<PackedQuad>, SRV t0
|
||||||
|
wi::graphics::GPUBuffer chunkInfoBuffer_; // StructuredBuffer<GPUChunkInfo>, SRV t2
|
||||||
|
|
||||||
|
// CPU-side tracking
|
||||||
|
struct ChunkSlot {
|
||||||
|
ChunkPos pos;
|
||||||
|
uint32_t quadOffset; // offset into mega-buffer (in quads)
|
||||||
|
uint32_t quadCount;
|
||||||
|
};
|
||||||
|
std::vector<ChunkSlot> chunkSlots_;
|
||||||
|
std::vector<GPUChunkInfo> cpuChunkInfo_;
|
||||||
|
std::vector<PackedQuad> cpuMegaQuads_; // CPU staging for mega-buffer
|
||||||
|
uint32_t chunkCount_ = 0;
|
||||||
|
bool megaBufferDirty_ = true;
|
||||||
|
|
||||||
|
// ── Indirect draw (Phase 2 MDI) ─────────────────────────────
|
||||||
|
// IndirectDrawArgsInstanced: { vertexCount, instanceCount, startVertex, startInstance }
|
||||||
|
struct IndirectDrawArgs {
|
||||||
|
uint32_t vertexCountPerInstance;
|
||||||
|
uint32_t instanceCount;
|
||||||
|
uint32_t startVertexLocation;
|
||||||
|
uint32_t startInstanceLocation;
|
||||||
|
};
|
||||||
|
wi::graphics::GPUBuffer indirectArgsBuffer_; // IndirectDrawArgs[MAX_DRAWS]
|
||||||
|
wi::graphics::GPUBuffer drawCountBuffer_; // uint32_t[1]
|
||||||
|
mutable std::vector<IndirectDrawArgs> cpuIndirectArgs_;
|
||||||
|
bool gpuCullingEnabled_ = false; // GPU compute cull vs CPU fallback
|
||||||
|
|
||||||
|
// Constants buffer (must match HLSL VoxelCB)
|
||||||
|
struct VoxelConstants {
|
||||||
|
XMFLOAT4X4 viewProjection;
|
||||||
|
XMFLOAT4 cameraPosition;
|
||||||
|
XMFLOAT4 sunDirection;
|
||||||
|
XMFLOAT4 sunColor;
|
||||||
|
float chunkSize;
|
||||||
|
float textureTiling;
|
||||||
|
float _pad[2];
|
||||||
|
XMFLOAT4 frustumPlanes[6]; // ax+by+cz+d=0
|
||||||
|
uint32_t chunkCount;
|
||||||
|
uint32_t _cullPad0;
|
||||||
|
uint32_t _cullPad1;
|
||||||
|
uint32_t _cullPad2;
|
||||||
|
};
|
||||||
|
wi::graphics::GPUBuffer constantBuffer_;
|
||||||
|
|
||||||
|
// ── GPU Compute Mesher (Phase 2 benchmark) ─────────────────────
|
||||||
|
wi::graphics::Shader meshShader_; // voxelMeshCS compute shader
|
||||||
|
wi::graphics::GPUBuffer voxelDataBuffer_; // chunk voxel data (StructuredBuffer<uint>)
|
||||||
|
wi::graphics::GPUBuffer gpuQuadBuffer_; // GPU mesh output (RWStructuredBuffer<uint2>)
|
||||||
|
wi::graphics::GPUBuffer gpuQuadCounter_; // atomic counter for GPU mesh output
|
||||||
|
bool gpuMesherAvailable_ = false;
|
||||||
|
|
||||||
|
// ── GPU Timestamp Queries (Phase 2 benchmark) ────────────────
|
||||||
|
wi::graphics::GPUQueryHeap timestampHeap_;
|
||||||
|
wi::graphics::GPUBuffer timestampReadback_;
|
||||||
|
static constexpr uint32_t TS_CULL_BEGIN = 0;
|
||||||
|
static constexpr uint32_t TS_CULL_END = 1;
|
||||||
|
static constexpr uint32_t TS_DRAW_BEGIN = 2;
|
||||||
|
static constexpr uint32_t TS_DRAW_END = 3;
|
||||||
|
static constexpr uint32_t TS_COUNT = 4;
|
||||||
|
mutable float gpuCullTimeMs_ = 0.0f;
|
||||||
|
mutable float gpuDrawTimeMs_ = 0.0f;
|
||||||
|
|
||||||
|
// Stats (mutable: updated during const Render() call)
|
||||||
|
mutable uint32_t totalQuads_ = 0;
|
||||||
|
mutable uint32_t visibleChunks_ = 0;
|
||||||
|
mutable uint32_t drawCalls_ = 0;
|
||||||
|
|
||||||
|
bool initialized_ = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
float getGpuCullTimeMs() const { return gpuCullTimeMs_; }
|
||||||
|
float getGpuDrawTimeMs() const { return gpuDrawTimeMs_; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Custom RenderPath that integrates voxel rendering ───────────
|
||||||
|
class VoxelRenderPath : public wi::RenderPath3D {
|
||||||
|
public:
|
||||||
|
VoxelWorld world;
|
||||||
|
VoxelRenderer renderer;
|
||||||
|
|
||||||
|
bool debugMode = false;
|
||||||
|
|
||||||
|
float cameraSpeed = 50.0f;
|
||||||
|
float cameraSensitivity = 0.003f;
|
||||||
|
XMFLOAT3 cameraPos = { 256.0f, 100.0f, 256.0f };
|
||||||
|
float cameraPitch = -0.3f;
|
||||||
|
float cameraYaw = 0.0f;
|
||||||
|
bool mouseCaptured = false;
|
||||||
|
|
||||||
|
void Start() override;
|
||||||
|
void Update(float dt) override;
|
||||||
|
void Render() const override;
|
||||||
|
void Compose(wi::graphics::CommandList cmd) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void handleInput(float dt);
|
||||||
|
void createRenderTargets();
|
||||||
|
mutable bool worldGenerated_ = false;
|
||||||
|
mutable int frameCount_ = 0;
|
||||||
|
mutable float lastDt_ = 0.016f;
|
||||||
|
mutable float smoothFps_ = 60.0f;
|
||||||
|
|
||||||
|
wi::graphics::Texture voxelRT_;
|
||||||
|
wi::graphics::Texture voxelDepth_;
|
||||||
|
mutable bool rtCreated_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
118
src/voxel/VoxelTypes.h
Normal file
118
src/voxel/VoxelTypes.h
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
// ── Voxel Data (16 bits per voxel, as per spec) ─────────────────
|
||||||
|
// Layout: 8 bits material ID | 4 bits flags | 4 bits metadata
|
||||||
|
struct VoxelData {
|
||||||
|
uint16_t packed = 0;
|
||||||
|
|
||||||
|
VoxelData() = default;
|
||||||
|
explicit VoxelData(uint8_t materialID, uint8_t flags = 0, uint8_t meta = 0) {
|
||||||
|
packed = (uint16_t(materialID) << 8) | (uint16_t(flags & 0xF) << 4) | (meta & 0xF);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getMaterialID() const { return uint8_t(packed >> 8); }
|
||||||
|
uint8_t getFlags() const { return uint8_t((packed >> 4) & 0xF); }
|
||||||
|
uint8_t getMetadata() const { return uint8_t(packed & 0xF); }
|
||||||
|
|
||||||
|
bool isEmpty() const { return packed == 0; }
|
||||||
|
bool isSmooth() const { return (getFlags() & FLAG_SMOOTH) != 0; }
|
||||||
|
bool isTransparent() const { return (getFlags() & FLAG_TRANSPARENT) != 0; }
|
||||||
|
bool isEmissive() const { return (getFlags() & FLAG_EMISSIVE) != 0; }
|
||||||
|
|
||||||
|
static constexpr uint8_t FLAG_SMOOTH = 0x1;
|
||||||
|
static constexpr uint8_t FLAG_TRANSPARENT = 0x2;
|
||||||
|
static constexpr uint8_t FLAG_EMISSIVE = 0x4;
|
||||||
|
static constexpr uint8_t FLAG_CUSTOM = 0x8;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Chunk Constants ─────────────────────────────────────────────
|
||||||
|
static constexpr int CHUNK_SIZE = 32;
|
||||||
|
static constexpr int CHUNK_VOLUME = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE;
|
||||||
|
|
||||||
|
// ── Packed Vertex for Greedy Mesh Quads (8 bytes per quad) ──────
|
||||||
|
// Layout per spec:
|
||||||
|
// 6 bits posX | 6 bits posY | 6 bits posZ |
|
||||||
|
// 6 bits width | 6 bits height | 3 bits face |
|
||||||
|
// 8 bits materialID | 8 bits AO | 15 bits flags
|
||||||
|
struct PackedQuad {
|
||||||
|
uint64_t data;
|
||||||
|
|
||||||
|
static PackedQuad create(
|
||||||
|
uint8_t x, uint8_t y, uint8_t z,
|
||||||
|
uint8_t w, uint8_t h, uint8_t face,
|
||||||
|
uint8_t materialID, uint8_t ao = 0, uint16_t flags = 0
|
||||||
|
) {
|
||||||
|
PackedQuad q;
|
||||||
|
q.data =
|
||||||
|
(uint64_t(x & 0x3F)) |
|
||||||
|
(uint64_t(y & 0x3F) << 6) |
|
||||||
|
(uint64_t(z & 0x3F) << 12) |
|
||||||
|
(uint64_t(w & 0x3F) << 18) |
|
||||||
|
(uint64_t(h & 0x3F) << 24) |
|
||||||
|
(uint64_t(face & 0x7) << 30) |
|
||||||
|
(uint64_t(materialID) << 33) |
|
||||||
|
(uint64_t(ao) << 41) |
|
||||||
|
(uint64_t(flags & 0x7FFF) << 49);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getX() const { return uint8_t(data & 0x3F); }
|
||||||
|
uint8_t getY() const { return uint8_t((data >> 6) & 0x3F); }
|
||||||
|
uint8_t getZ() const { return uint8_t((data >> 12) & 0x3F); }
|
||||||
|
uint8_t getWidth() const { return uint8_t((data >> 18) & 0x3F); }
|
||||||
|
uint8_t getHeight() const { return uint8_t((data >> 24) & 0x3F); }
|
||||||
|
uint8_t getFace() const { return uint8_t((data >> 30) & 0x7); }
|
||||||
|
uint8_t getMaterialID() const { return uint8_t((data >> 33) & 0xFF); }
|
||||||
|
uint8_t getAO() const { return uint8_t((data >> 41) & 0xFF); }
|
||||||
|
uint16_t getFlags() const { return uint16_t((data >> 49) & 0x7FFF); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Face directions: +X, -X, +Y, -Y, +Z, -Z
|
||||||
|
enum Face : uint8_t {
|
||||||
|
FACE_POS_X = 0,
|
||||||
|
FACE_NEG_X = 1,
|
||||||
|
FACE_POS_Y = 2,
|
||||||
|
FACE_NEG_Y = 3,
|
||||||
|
FACE_POS_Z = 4,
|
||||||
|
FACE_NEG_Z = 5,
|
||||||
|
FACE_COUNT = 6
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Material Descriptor ─────────────────────────────────────────
|
||||||
|
struct MaterialDesc {
|
||||||
|
uint16_t albedoTextureIndex = 0;
|
||||||
|
uint16_t normalTextureIndex = 0;
|
||||||
|
uint16_t heightmapTextureIndex = 0;
|
||||||
|
uint8_t roughness = 128; // 0-255 mapped to 0.0-1.0
|
||||||
|
uint8_t metallic = 0;
|
||||||
|
uint8_t flags = 0; // triplanar, blend mode, etc.
|
||||||
|
uint8_t _pad = 0;
|
||||||
|
|
||||||
|
static constexpr uint8_t FLAG_TRIPLANAR = 0x1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Chunk Position Hash ─────────────────────────────────────────
|
||||||
|
struct ChunkPos {
|
||||||
|
int32_t x, y, z;
|
||||||
|
|
||||||
|
bool operator==(const ChunkPos& other) const {
|
||||||
|
return x == other.x && y == other.y && z == other.z;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ChunkPosHash {
|
||||||
|
size_t operator()(const ChunkPos& p) const {
|
||||||
|
// FNV-1a inspired hash
|
||||||
|
size_t h = 0x811c9dc5;
|
||||||
|
h ^= size_t(p.x); h *= 0x01000193;
|
||||||
|
h ^= size_t(p.y); h *= 0x01000193;
|
||||||
|
h ^= size_t(p.z); h *= 0x01000193;
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
282
src/voxel/VoxelWorld.cpp
Normal file
282
src/voxel/VoxelWorld.cpp
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
#include "VoxelWorld.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
VoxelWorld::VoxelWorld() {
|
||||||
|
setupDefaultMaterials();
|
||||||
|
}
|
||||||
|
|
||||||
|
VoxelWorld::~VoxelWorld() = default;
|
||||||
|
|
||||||
|
void VoxelWorld::setupDefaultMaterials() {
|
||||||
|
// Material 0: Air (empty, never rendered)
|
||||||
|
// Material 1: Grass
|
||||||
|
materials[1].albedoTextureIndex = 0;
|
||||||
|
materials[1].roughness = 200;
|
||||||
|
materials[1].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||||||
|
// Material 2: Dirt
|
||||||
|
materials[2].albedoTextureIndex = 1;
|
||||||
|
materials[2].roughness = 220;
|
||||||
|
materials[2].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||||||
|
// Material 3: Stone
|
||||||
|
materials[3].albedoTextureIndex = 2;
|
||||||
|
materials[3].roughness = 180;
|
||||||
|
materials[3].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||||||
|
// Material 4: Sand
|
||||||
|
materials[4].albedoTextureIndex = 3;
|
||||||
|
materials[4].roughness = 230;
|
||||||
|
materials[4].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||||||
|
// Material 5: Snow
|
||||||
|
materials[5].albedoTextureIndex = 4;
|
||||||
|
materials[5].roughness = 150;
|
||||||
|
materials[5].flags = MaterialDesc::FLAG_TRIPLANAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Permutation-based noise (no external dependency) ────────────
|
||||||
|
|
||||||
|
static constexpr int PERM_SIZE = 256;
|
||||||
|
static uint8_t perm[512];
|
||||||
|
static uint32_t permSeed = 0;
|
||||||
|
static bool permInitialized = false;
|
||||||
|
|
||||||
|
static void initPerm(uint32_t seed) {
|
||||||
|
if (permInitialized && permSeed == seed) return;
|
||||||
|
for (int i = 0; i < PERM_SIZE; i++) perm[i] = (uint8_t)i;
|
||||||
|
// Fisher-Yates shuffle with seed
|
||||||
|
uint32_t s = seed;
|
||||||
|
for (int i = PERM_SIZE - 1; i > 0; i--) {
|
||||||
|
s = s * 1664525u + 1013904223u; // LCG
|
||||||
|
int j = s % (i + 1);
|
||||||
|
uint8_t tmp = perm[i];
|
||||||
|
perm[i] = perm[j];
|
||||||
|
perm[j] = tmp;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 256; i++) perm[i + 256] = perm[i];
|
||||||
|
permSeed = seed;
|
||||||
|
permInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float fade(float t) { return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); }
|
||||||
|
static float lerp(float a, float b, float t) { return a + t * (b - a); }
|
||||||
|
|
||||||
|
static float grad(int hash, float x, float y, float z) {
|
||||||
|
int h = hash & 15;
|
||||||
|
float u = h < 8 ? x : y;
|
||||||
|
float v = h < 4 ? y : (h == 12 || h == 14 ? x : z);
|
||||||
|
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
|
||||||
|
}
|
||||||
|
|
||||||
|
float VoxelWorld::noise3D(float x, float y, float z) const {
|
||||||
|
initPerm(seed_);
|
||||||
|
int X = (int)std::floor(x) & 255;
|
||||||
|
int Y = (int)std::floor(y) & 255;
|
||||||
|
int Z = (int)std::floor(z) & 255;
|
||||||
|
x -= std::floor(x);
|
||||||
|
y -= std::floor(y);
|
||||||
|
z -= std::floor(z);
|
||||||
|
float u = fade(x), v = fade(y), w = fade(z);
|
||||||
|
|
||||||
|
int A = perm[X] + Y;
|
||||||
|
int AA = perm[A] + Z;
|
||||||
|
int AB = perm[A + 1] + Z;
|
||||||
|
int B = perm[X + 1] + Y;
|
||||||
|
int BA = perm[B] + Z;
|
||||||
|
int BB = perm[B + 1] + Z;
|
||||||
|
|
||||||
|
return lerp(
|
||||||
|
lerp(lerp(grad(perm[AA], x, y, z), grad(perm[BA], x-1, y, z), u),
|
||||||
|
lerp(grad(perm[AB], x, y-1, z), grad(perm[BB], x-1, y-1, z), u), v),
|
||||||
|
lerp(lerp(grad(perm[AA+1], x, y, z-1), grad(perm[BA+1], x-1, y, z-1), u),
|
||||||
|
lerp(grad(perm[AB+1], x, y-1, z-1), grad(perm[BB+1], x-1, y-1, z-1), u), v),
|
||||||
|
w);
|
||||||
|
}
|
||||||
|
|
||||||
|
float VoxelWorld::fbm(float x, float y, float z, int octaves) const {
|
||||||
|
float value = 0.0f;
|
||||||
|
float amplitude = 1.0f;
|
||||||
|
float frequency = 1.0f;
|
||||||
|
float maxVal = 0.0f;
|
||||||
|
for (int i = 0; i < octaves; i++) {
|
||||||
|
value += amplitude * noise3D(x * frequency, y * frequency, z * frequency);
|
||||||
|
maxVal += amplitude;
|
||||||
|
amplitude *= 0.5f;
|
||||||
|
frequency *= 2.0f;
|
||||||
|
}
|
||||||
|
return value / maxVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelWorld::generateChunk(Chunk& chunk) {
|
||||||
|
const float scale = 0.02f; // terrain horizontal scale
|
||||||
|
const float heightScale = 64.0f;
|
||||||
|
const float baseHeight = 40.0f;
|
||||||
|
const float caveScale = 0.05f;
|
||||||
|
const float caveThreshold = 0.3f;
|
||||||
|
|
||||||
|
for (int z = 0; z < CHUNK_SIZE; z++) {
|
||||||
|
for (int x = 0; x < CHUNK_SIZE; x++) {
|
||||||
|
// World-space coordinates
|
||||||
|
float wx = (float)(chunk.pos.x * CHUNK_SIZE + x);
|
||||||
|
float wz = (float)(chunk.pos.z * CHUNK_SIZE + z);
|
||||||
|
|
||||||
|
// Heightmap using fBm
|
||||||
|
float height = baseHeight + heightScale * fbm(wx * scale, 0.0f, wz * scale, 5);
|
||||||
|
|
||||||
|
for (int y = 0; y < CHUNK_SIZE; y++) {
|
||||||
|
float wy = (float)(chunk.pos.y * CHUNK_SIZE + y);
|
||||||
|
VoxelData v;
|
||||||
|
|
||||||
|
if (wy > height) {
|
||||||
|
// Air above terrain
|
||||||
|
v = VoxelData();
|
||||||
|
} else {
|
||||||
|
// Cave generation
|
||||||
|
float cave = fbm(wx * caveScale, wy * caveScale, wz * caveScale, 3);
|
||||||
|
if (std::abs(cave) < caveThreshold && wy > 10.0f && wy < height - 3.0f) {
|
||||||
|
v = VoxelData(); // Cave
|
||||||
|
} else if (wy > height - 1.0f) {
|
||||||
|
// Surface layer: material depends on height
|
||||||
|
if (wy > 90.0f) {
|
||||||
|
v = VoxelData(5); // Snow
|
||||||
|
} else if (wy > 70.0f) {
|
||||||
|
v = VoxelData(3); // Stone
|
||||||
|
} else if (wy < 25.0f) {
|
||||||
|
v = VoxelData(4); // Sand
|
||||||
|
} else {
|
||||||
|
v = VoxelData(1); // Grass
|
||||||
|
}
|
||||||
|
} else if (wy > height - 4.0f) {
|
||||||
|
v = VoxelData(2); // Dirt
|
||||||
|
} else {
|
||||||
|
v = VoxelData(3); // Stone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.at(x, y, z) = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelWorld::generateAround(float cx, float cy, float cz, int radiusChunks) {
|
||||||
|
int ccx = (int)std::floor(cx / CHUNK_SIZE);
|
||||||
|
int ccy = (int)std::floor(cy / CHUNK_SIZE);
|
||||||
|
int ccz = (int)std::floor(cz / CHUNK_SIZE);
|
||||||
|
|
||||||
|
for (int dz = -radiusChunks; dz <= radiusChunks; dz++) {
|
||||||
|
for (int dx = -radiusChunks; dx <= radiusChunks; dx++) {
|
||||||
|
// Y range: only generate chunks that could contain terrain (0 to ~4 chunks high)
|
||||||
|
for (int dy = 0; dy < 8; dy++) {
|
||||||
|
ChunkPos pos = { ccx + dx, dy, ccz + dz };
|
||||||
|
if (chunks_.find(pos) == chunks_.end()) {
|
||||||
|
auto chunk = std::make_unique<Chunk>();
|
||||||
|
chunk->pos = pos;
|
||||||
|
generateChunk(*chunk);
|
||||||
|
chunks_[pos] = std::move(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Chunk* VoxelWorld::getChunk(const ChunkPos& pos) {
|
||||||
|
auto it = chunks_.find(pos);
|
||||||
|
return it != chunks_.end() ? it->second.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Chunk* VoxelWorld::getChunk(const ChunkPos& pos) const {
|
||||||
|
auto it = chunks_.find(pos);
|
||||||
|
return it != chunks_.end() ? it->second.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
VoxelData VoxelWorld::getVoxel(int wx, int wy, int wz) const {
|
||||||
|
// Integer floor division that works for negatives
|
||||||
|
auto floorDiv = [](int a, int b) -> int {
|
||||||
|
return (a >= 0) ? (a / b) : ((a - b + 1) / b);
|
||||||
|
};
|
||||||
|
auto floorMod = [](int a, int b) -> int {
|
||||||
|
int r = a % b;
|
||||||
|
return (r < 0) ? r + b : r;
|
||||||
|
};
|
||||||
|
|
||||||
|
ChunkPos cp = {
|
||||||
|
floorDiv(wx, CHUNK_SIZE),
|
||||||
|
floorDiv(wy, CHUNK_SIZE),
|
||||||
|
floorDiv(wz, CHUNK_SIZE)
|
||||||
|
};
|
||||||
|
const Chunk* chunk = getChunk(cp);
|
||||||
|
if (!chunk) return VoxelData();
|
||||||
|
|
||||||
|
return chunk->at(
|
||||||
|
floorMod(wx, CHUNK_SIZE),
|
||||||
|
floorMod(wy, CHUNK_SIZE),
|
||||||
|
floorMod(wz, CHUNK_SIZE)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelWorld::setVoxel(int wx, int wy, int wz, VoxelData v) {
|
||||||
|
auto floorDiv = [](int a, int b) -> int {
|
||||||
|
return (a >= 0) ? (a / b) : ((a - b + 1) / b);
|
||||||
|
};
|
||||||
|
auto floorMod = [](int a, int b) -> int {
|
||||||
|
int r = a % b;
|
||||||
|
return (r < 0) ? r + b : r;
|
||||||
|
};
|
||||||
|
|
||||||
|
ChunkPos cp = {
|
||||||
|
floorDiv(wx, CHUNK_SIZE),
|
||||||
|
floorDiv(wy, CHUNK_SIZE),
|
||||||
|
floorDiv(wz, CHUNK_SIZE)
|
||||||
|
};
|
||||||
|
Chunk* chunk = getChunk(cp);
|
||||||
|
if (!chunk) return;
|
||||||
|
|
||||||
|
chunk->at(
|
||||||
|
floorMod(wx, CHUNK_SIZE),
|
||||||
|
floorMod(wy, CHUNK_SIZE),
|
||||||
|
floorMod(wz, CHUNK_SIZE)
|
||||||
|
) = v;
|
||||||
|
chunk->dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoxelWorld::generateDebug() {
|
||||||
|
chunks_.clear();
|
||||||
|
|
||||||
|
// Create a single chunk at origin
|
||||||
|
ChunkPos cp = {0, 0, 0};
|
||||||
|
auto chunk = std::make_unique<Chunk>();
|
||||||
|
chunk->pos = cp;
|
||||||
|
std::memset(chunk->voxels, 0, sizeof(chunk->voxels));
|
||||||
|
|
||||||
|
VoxelData stone(3); // material 3 = stone
|
||||||
|
|
||||||
|
// Block 1: single isolated block at (5, 5, 5)
|
||||||
|
// → should show all 6 faces
|
||||||
|
chunk->at(5, 5, 5) = stone;
|
||||||
|
|
||||||
|
// Block 2: 2x1x1 bar at (12, 5, 5) along X
|
||||||
|
// → internal faces should be culled
|
||||||
|
chunk->at(12, 5, 5) = stone;
|
||||||
|
chunk->at(13, 5, 5) = stone;
|
||||||
|
|
||||||
|
// Block 3: L-shape at (5, 5, 12)
|
||||||
|
chunk->at(5, 5, 12) = stone;
|
||||||
|
chunk->at(6, 5, 12) = stone;
|
||||||
|
chunk->at(5, 5, 13) = stone;
|
||||||
|
|
||||||
|
// Block 4: 3-high column at (12, 5, 12)
|
||||||
|
chunk->at(12, 5, 12) = stone;
|
||||||
|
chunk->at(12, 6, 12) = stone;
|
||||||
|
chunk->at(12, 7, 12) = stone;
|
||||||
|
|
||||||
|
// Block 5: single block at (20, 5, 5) with material 1 (grass)
|
||||||
|
chunk->at(20, 5, 5) = VoxelData(1);
|
||||||
|
|
||||||
|
chunk->dirty = true;
|
||||||
|
chunks_[cp] = std::move(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
87
src/voxel/VoxelWorld.h
Normal file
87
src/voxel/VoxelWorld.h
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#pragma once
|
||||||
|
#include "VoxelTypes.h"
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace voxel {
|
||||||
|
|
||||||
|
// ── Chunk ───────────────────────────────────────────────────────
|
||||||
|
struct Chunk {
|
||||||
|
VoxelData voxels[CHUNK_VOLUME];
|
||||||
|
ChunkPos pos;
|
||||||
|
bool dirty = true;
|
||||||
|
|
||||||
|
// Mesh data (output of greedy mesher, sorted by face ID 0-5)
|
||||||
|
std::vector<PackedQuad> quads;
|
||||||
|
uint32_t quadCount = 0;
|
||||||
|
uint32_t faceOffsets[6] = {}; // offset (in quads) for each face group within quads[]
|
||||||
|
uint32_t faceCounts[6] = {}; // number of quads per face group
|
||||||
|
|
||||||
|
VoxelData& at(int x, int y, int z) {
|
||||||
|
return voxels[x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE];
|
||||||
|
}
|
||||||
|
|
||||||
|
const VoxelData& at(int x, int y, int z) const {
|
||||||
|
return voxels[x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInBounds(int x, int y, int z) const {
|
||||||
|
return x >= 0 && x < CHUNK_SIZE &&
|
||||||
|
y >= 0 && y < CHUNK_SIZE &&
|
||||||
|
z >= 0 && z < CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── World ───────────────────────────────────────────────────────
|
||||||
|
class VoxelWorld {
|
||||||
|
public:
|
||||||
|
VoxelWorld();
|
||||||
|
~VoxelWorld();
|
||||||
|
|
||||||
|
// Generate a procedural world around a center position
|
||||||
|
void generateAround(float cx, float cy, float cz, int radiusChunks);
|
||||||
|
|
||||||
|
// Generate debug world: isolated blocks for face visibility testing
|
||||||
|
void generateDebug();
|
||||||
|
|
||||||
|
// Get a chunk (nullptr if not loaded)
|
||||||
|
Chunk* getChunk(const ChunkPos& pos);
|
||||||
|
const Chunk* getChunk(const ChunkPos& pos) const;
|
||||||
|
|
||||||
|
// Get voxel at world position (handles cross-chunk lookup)
|
||||||
|
VoxelData getVoxel(int wx, int wy, int wz) const;
|
||||||
|
void setVoxel(int wx, int wy, int wz, VoxelData v);
|
||||||
|
|
||||||
|
// Iterate all chunks
|
||||||
|
template<typename Fn>
|
||||||
|
void forEachChunk(Fn&& fn) {
|
||||||
|
for (auto& [pos, chunk] : chunks_) {
|
||||||
|
fn(pos, *chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename Fn>
|
||||||
|
void forEachChunk(Fn&& fn) const {
|
||||||
|
for (auto& [pos, chunk] : chunks_) {
|
||||||
|
fn(pos, *chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t chunkCount() const { return chunks_.size(); }
|
||||||
|
|
||||||
|
// Material palette
|
||||||
|
MaterialDesc materials[256];
|
||||||
|
void setupDefaultMaterials();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void generateChunk(Chunk& chunk);
|
||||||
|
float noise3D(float x, float y, float z) const;
|
||||||
|
float fbm(float x, float y, float z, int octaves) const;
|
||||||
|
|
||||||
|
std::unordered_map<ChunkPos, std::unique_ptr<Chunk>, ChunkPosHash> chunks_;
|
||||||
|
uint32_t seed_ = 42;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace voxel
|
||||||
BIN
voxel_engine_spec.docx
Normal file
BIN
voxel_engine_spec.docx
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue