Phase 5.1: Naive Surface Nets smooth rendering
Implement CPU-side Naive Surface Nets for smooth voxel surfaces (SmoothStone, Snow) coexisting with blocky voxels (Grass, Dirt, Stone, Sand). Key features: - SmoothMesher with binary SDF, centroid vertex placement, per-axis boundary clamping to align with blocky grid at smooth↔blocky transitions - Cross-chunk connectivity: PAD=2 SDF grid, vertex range [-1, CHUNK_SIZE), canonical edge ownership (no duplicate triangles, no z-fighting) - Face normals oriented by edge axis+sign (robust with binary SDF, unlike SDF gradient dot or centroid sampling approaches) - Y-axis winding fix: sharing cells have different spatial arrangement, requiring opposite winding from X and Z axes - GPU mesher treats smooth neighbors as solid (no blocky faces toward smooth) - Material blending: primary (smooth-only) + secondary (all counts) per vertex - Dedicated shaders: voxelSmoothVS (vertex pulling t6) + voxelSmoothPS (triplanar + lerp blending between two materials) - Separate render pass with LoadOp::LOAD after voxels+topings - New materials: SmoothStone (mat 6), blocky Stone (mat 3) and Dirt patches added to world generation for boundary testing
This commit is contained in:
parent
72af8af979
commit
aab38bb9b9
13 changed files with 810 additions and 27 deletions
54
CLAUDE.md
54
CLAUDE.md
|
|
@ -17,7 +17,7 @@ bvle-voxels/
|
|||
│ ├── 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
|
||||
│ │ ├── VoxelMesher.h/.cpp # Binary Greedy Mesher CPU + SmoothMesher (Naive Surface Nets)
|
||||
│ │ ├── VoxelRenderer.h/.cpp# Renderer + VoxelRenderPath (sous-classe RenderPath3D)
|
||||
│ │ └── TopingSystem.h/.cpp # Système de topings (biseaux décoratifs sur faces +Y)
|
||||
│ └── app/
|
||||
|
|
@ -29,7 +29,9 @@ bvle-voxels/
|
|||
│ ├── voxelCullCS.hlsl # Compute shader frustum+backface cull (Phase 2.3)
|
||||
│ ├── voxelMeshCS.hlsl # Compute shader GPU mesher 1×1 (Phase 2.4-2.5)
|
||||
│ ├── voxelTopingVS.hlsl # Vertex shader topings (instanced vertex pulling, t4/t5)
|
||||
│ └── voxelTopingPS.hlsl # Pixel shader topings (triplanar + directional lighting)
|
||||
│ ├── voxelTopingPS.hlsl # Pixel shader topings (triplanar + directional lighting)
|
||||
│ ├── voxelSmoothVS.hlsl # Vertex shader smooth Surface Nets (vertex pulling, t6)
|
||||
│ └── voxelSmoothPS.hlsl # Pixel shader smooth (triplanar + material blending)
|
||||
└── CLAUDE.md
|
||||
```
|
||||
|
||||
|
|
@ -441,12 +443,47 @@ Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour
|
|||
- Animation subtile (vent sur l'herbe via vertex shader)
|
||||
- Optimisation : compute shader pour le instance collection
|
||||
|
||||
### Phase 5 - Rendu smooth [A FAIRE]
|
||||
### Phase 5 - Rendu smooth [EN COURS]
|
||||
|
||||
- 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 5.1 - Naive Surface Nets CPU [FAIT]
|
||||
|
||||
- **Algorithme** : Naive Surface Nets (dual contouring simplifié) dans `SmoothMesher::meshChunk()`
|
||||
- **SDF binaire** : solid = -1, empty = +1 (pas de distance field continu)
|
||||
- **Vertex placement** : centroïde des edge crossings pour chaque cellule à la surface
|
||||
- **Matériaux smooth** : SmoothStone (mat 6, `FLAG_SMOOTH`) et Snow (mat 5, `FLAG_SMOOTH`)
|
||||
- **Matériaux blocky** : Stone (mat 3), Grass (mat 1), Dirt (mat 2), Sand (mat 4)
|
||||
- **SmoothVertex** (32 bytes) : position, face normal, materialID, secondaryMat, blendWeight, chunkIndex
|
||||
- **Shaders dédiés** : `voxelSmoothVS.hlsl` (vertex pulling t6) + `voxelSmoothPS.hlsl` (triplanar + blending)
|
||||
- **Render pass séparé** avec `LoadOp::LOAD` : smooth rendu après voxels+topings, préserve RT et depth
|
||||
|
||||
**Cross-chunk connectivity** :
|
||||
- **PAD=2** dans la grille SDF pour accéder aux cellules [-1..CHUNK_SIZE]
|
||||
- **Vertex range étendu** : `[-1, CHUNK_SIZE)` au lieu de `[0, CHUNK_SIZE)` — les cellules au bord du chunk voisin génèrent des vertices
|
||||
- **Canonical ownership** : chaque edge est émise par un seul chunk (celui contenant le grid point inférieur), pas de duplication
|
||||
|
||||
**Smooth↔blocky boundary** :
|
||||
- **`hasSmooth` filter** : ne génère des vertices que si au moins un coin de la cellule est un voxel smooth (évite le débordement sur territoire blocky)
|
||||
- **Per-axis boundary clamping** : les vertices aux frontières smooth↔blocky sont clampés vers la grille entière (empêche le mesh smooth de dépasser sur les faces blocky)
|
||||
- **GPU mesher** : les voxels smooth sont traités comme solides dans `isNeighborAir()` — les faces blocky ne sont pas émises vers les voxels smooth (le mesh smooth couvre la frontière)
|
||||
|
||||
**Face normals — PIÈGES MAJEURS** :
|
||||
- **Face normals, pas SDF gradient** : le SDF binaire donne des gradients à 45° aux marches, causant du stretching triplanar. Les face normals (cross product des edges du triangle) sont géométriquement correctes.
|
||||
- **Orientation par axe de l'edge** : chaque quad vient d'une edge X, Y ou Z. La direction `solid→empty` est connue. On vérifie que la composante de la face normal sur cet axe a le bon signe, sinon flip.
|
||||
- **Y-axis winding inversé** : les sharing cells Y sont arrangées différemment de X et Z. Le winding naturel du quad Y est opposé → `if (axis == 1) useWindingA = !useWindingA;`
|
||||
- **SDF gradient dot product** : NE PAS utiliser pour orienter les normals (échoue quand le gradient est nul ou ambigu avec SDF binaire)
|
||||
- **Centroid SDF sampling** : NE PAS utiliser non plus (les deux côtés arrondissent souvent au même voxel)
|
||||
|
||||
**Material blending** :
|
||||
- **Deux matériaux par vertex** : primaryMat (smooth-only counts, évite subsurface bleed) + secondaryMat (all counts, inclut blocky pour le blending aux frontières)
|
||||
- **blendWeight** : uint8 0-255, ratio du secondaire dans le vote des 8 corners
|
||||
- **PS** : `lerp(primaryColor, secondaryColor, blendWeight)` entre deux samplings triplanar
|
||||
|
||||
#### Phase 5.2 - Optimisations et polish [A FAIRE]
|
||||
|
||||
- SDF lissé (distance field approximatif au lieu de binaire ±1)
|
||||
- Smooth normals (vertex normals au lieu de face normals pour surfaces lisses)
|
||||
- GPU compute Surface Nets (compute shader au lieu de CPU)
|
||||
- LOD : réduction de triangles à distance
|
||||
|
||||
### Phase 6 - Ray tracing hybride [A FAIRE]
|
||||
|
||||
|
|
@ -471,5 +508,6 @@ Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour
|
|||
- 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)
|
||||
- Matériaux : palette de 256, index 0 = air (vide), 1=grass, 2=dirt, 3=stone (blocky), 4=sand, 5=snow (smooth), 6=smoothstone (smooth)
|
||||
- Faces : 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z
|
||||
- Smooth flag : `FLAG_SMOOTH = 0x1` dans VoxelData flags — active Surface Nets au lieu du rendu blocky
|
||||
|
|
|
|||
|
|
@ -59,6 +59,10 @@ add_custom_command(TARGET BVLEVoxels POST_BUILD
|
|||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelPS.cso
|
||||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelCullCS.cso
|
||||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelMeshCS.cso
|
||||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelTopingVS.cso
|
||||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelTopingPS.cso
|
||||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelSmoothVS.cso
|
||||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelSmoothPS.cso
|
||||
$<TARGET_FILE_DIR:BVLEVoxels>/shaders/hlsl6/voxel/voxelCommon.hlsli.cso
|
||||
COMMENT "Clearing stale voxel shader cache (forces recompilation from current .hlsl sources)"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -35,13 +35,16 @@ uint readVoxel(uint flatIndex) {
|
|||
}
|
||||
|
||||
// Check if neighbor is air (handles out-of-bounds as air for chunk boundaries)
|
||||
// Smooth voxels are treated as solid — the smooth Surface Nets mesh covers the boundary.
|
||||
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
|
||||
uint nv = readVoxel(flatN);
|
||||
if (nv == 0) return true; // air
|
||||
return false; // any solid (blocky or smooth) → hidden face
|
||||
}
|
||||
|
||||
// Pack a quad into uint2 (matches CPU PackedQuad format)
|
||||
|
|
@ -69,6 +72,12 @@ void main(uint3 DTid : SV_DispatchThreadID)
|
|||
uint voxel = readVoxel(flatIdx);
|
||||
if (voxel == 0) return; // air voxel, nothing to emit
|
||||
|
||||
// Phase 5: skip smooth voxels (they have their own Surface Nets mesh)
|
||||
// VoxelData layout: [15:8] matID, [7:4] flags, [3:0] metadata
|
||||
// FLAG_SMOOTH = 0x1 → bit 4 of the packed value
|
||||
uint flags = (voxel >> 4) & 0xF;
|
||||
if (flags & 0x1) return; // smooth voxel, skip blocky mesh
|
||||
|
||||
uint matID = voxel >> 8; // high 8 bits = material ID
|
||||
|
||||
// Check each face direction
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ float4 main(PSInput input) : SV_TARGET0
|
|||
float3 L = normalize(-sunDirection.xyz);
|
||||
float NdotL = max(dot(N, L), 0.0);
|
||||
|
||||
uint texIndex = clamp(input.materialID - 1u, 0u, 4u);
|
||||
uint texIndex = clamp(input.materialID - 1u, 0u, 5u);
|
||||
float tiling = textureTiling;
|
||||
float3 albedo;
|
||||
|
||||
|
|
@ -241,7 +241,7 @@ float4 main(PSInput input) : SV_TARGET0
|
|||
float sharpness = 16.0; // higher = sharper transition (∞ = binary)
|
||||
|
||||
if (uBlend) {
|
||||
uint uTexIdx = clamp(uNeighborMat - 1u, 0u, 4u);
|
||||
uint uTexIdx = clamp(uNeighborMat - 1u, 0u, 5u);
|
||||
float4 uTex = sampleTriplanarRGBA(input.worldPos, N, uTexIdx, tiling);
|
||||
|
||||
// Symmetric proximity bias: at edge (weight=0.5) bias=0 → pure heightmap.
|
||||
|
|
@ -255,7 +255,7 @@ float4 main(PSInput input) : SV_TARGET0
|
|||
}
|
||||
|
||||
if (vBlend) {
|
||||
uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 4u);
|
||||
uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 5u);
|
||||
float4 vTex = sampleTriplanarRGBA(input.worldPos, N, vTexIdx, tiling);
|
||||
|
||||
float bias = 0.5 - vWeight;
|
||||
|
|
|
|||
57
shaders/voxelSmoothPS.hlsl
Normal file
57
shaders/voxelSmoothPS.hlsl
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// BVLE Voxels - Smooth Surface Nets Pixel Shader (Phase 5.1)
|
||||
// Triplanar texture sampling + material blending + same lighting as voxel PS.
|
||||
|
||||
#include "voxelCommon.hlsli"
|
||||
|
||||
Texture2DArray<float4> materialTextures : register(t1);
|
||||
SamplerState texSampler : register(s0);
|
||||
|
||||
struct PSInput {
|
||||
float4 position : SV_POSITION;
|
||||
float3 worldPos : WORLDPOS;
|
||||
float3 normal : NORMAL;
|
||||
nointerpolation uint matPacked : MATERIALID;
|
||||
};
|
||||
|
||||
// Sample triplanar texture for a given material index
|
||||
float3 sampleTriplanar(float3 worldPos, float3 blend, float tiling, uint matIdx) {
|
||||
uint texIdx = clamp(matIdx - 1u, 0u, 5u);
|
||||
float4 xS = materialTextures.Sample(texSampler, float3(worldPos.yz * tiling, (float)texIdx));
|
||||
float4 yS = materialTextures.Sample(texSampler, float3(worldPos.xz * tiling, (float)texIdx));
|
||||
float4 zS = materialTextures.Sample(texSampler, float3(worldPos.xy * tiling, (float)texIdx));
|
||||
return xS.rgb * blend.x + yS.rgb * blend.y + zS.rgb * blend.z;
|
||||
}
|
||||
|
||||
[RootSignature(VOXEL_ROOTSIG)]
|
||||
float4 main(PSInput input) : SV_TARGET0 {
|
||||
float3 N = normalize(input.normal);
|
||||
float tiling = textureTiling;
|
||||
|
||||
// Unpack materials: materialID(8) | secondaryMat(8) | blendWeight(8) | pad(8)
|
||||
uint primaryMat = input.matPacked & 0xFF;
|
||||
uint secondaryMat = (input.matPacked >> 8) & 0xFF;
|
||||
float blendWeight = ((input.matPacked >> 16) & 0xFF) / 255.0;
|
||||
|
||||
// Triplanar blend weights
|
||||
float3 blend = abs(N);
|
||||
blend = blend / (blend.x + blend.y + blend.z + 0.001);
|
||||
|
||||
// Sample primary and secondary materials
|
||||
float3 primaryColor = sampleTriplanar(input.worldPos, blend, tiling, primaryMat);
|
||||
float3 texColor;
|
||||
if (blendWeight > 0.01 && secondaryMat != primaryMat) {
|
||||
float3 secondaryColor = sampleTriplanar(input.worldPos, blend, tiling, secondaryMat);
|
||||
texColor = lerp(primaryColor, secondaryColor, blendWeight);
|
||||
} else {
|
||||
texColor = primaryColor;
|
||||
}
|
||||
|
||||
// Lighting (same model as voxel PS)
|
||||
float3 L = normalize(-sunDirection.xyz);
|
||||
float NdotL = max(dot(N, L), 0.0);
|
||||
|
||||
float3 ambient = float3(0.15, 0.18, 0.25);
|
||||
float3 lit = texColor * (sunColor.rgb * NdotL + ambient);
|
||||
|
||||
return float4(lit, 1.0);
|
||||
}
|
||||
33
shaders/voxelSmoothVS.hlsl
Normal file
33
shaders/voxelSmoothVS.hlsl
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// BVLE Voxels - Smooth Surface Nets Vertex Shader (Phase 5.1)
|
||||
// Vertex pulling from StructuredBuffer<SmoothVertex>.
|
||||
// Each vertex is 32 bytes: float3 pos, float3 normal, uint matPacked, uint16 chunkIndex.
|
||||
|
||||
#include "voxelCommon.hlsli"
|
||||
|
||||
struct SmoothVtx {
|
||||
float3 position; // world-space position (chunk origin already added)
|
||||
float3 normal; // face normal
|
||||
uint matPacked; // materialID(8) | secondaryMat(8) | blendWeight(8) | pad(8)
|
||||
uint chunkIndex; // packed: chunkIndex in low 16 bits
|
||||
};
|
||||
|
||||
StructuredBuffer<SmoothVtx> smoothVertices : register(t6);
|
||||
|
||||
struct VSOutput {
|
||||
float4 position : SV_POSITION;
|
||||
float3 worldPos : WORLDPOS;
|
||||
float3 normal : NORMAL;
|
||||
nointerpolation uint matPacked : MATERIALID;
|
||||
};
|
||||
|
||||
[RootSignature(VOXEL_ROOTSIG)]
|
||||
VSOutput main(uint vertexID : SV_VertexID) {
|
||||
SmoothVtx vtx = smoothVertices[vertexID];
|
||||
|
||||
VSOutput output;
|
||||
output.position = mul(viewProjection, float4(vtx.position, 1.0));
|
||||
output.worldPos = vtx.position;
|
||||
output.normal = vtx.normal;
|
||||
output.matPacked = vtx.matPacked; // pass all packed material data to PS
|
||||
return output;
|
||||
}
|
||||
|
|
@ -13,7 +13,11 @@ void VoxelMesher::buildAxisMasks(const Chunk& chunk, AxisMasks masks[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()) {
|
||||
const VoxelData& v = chunk.at(x, y, z);
|
||||
// Phase 5: smooth voxels are still "solid" for face culling
|
||||
// (they block neighbor faces) but don't emit their own quads.
|
||||
// So they must be in the solid mask.
|
||||
if (!v.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]
|
||||
|
|
@ -78,7 +82,10 @@ void VoxelMesher::greedyMerge(
|
|||
}
|
||||
|
||||
if (faceVisible) {
|
||||
matGrid[v][u] = chunk.at(x, y, z).getMaterialID();
|
||||
// Phase 5: skip smooth voxels (they have Surface Nets mesh)
|
||||
const VoxelData& vd = chunk.at(x, y, z);
|
||||
if (vd.isSmooth()) continue;
|
||||
matGrid[v][u] = vd.getMaterialID();
|
||||
faceCount++;
|
||||
}
|
||||
}
|
||||
|
|
@ -235,4 +242,421 @@ uint8_t VoxelMesher::calcAO(const VoxelWorld& world, const ChunkPos& cpos,
|
|||
return 0;
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
// ── Naive Surface Nets Mesher (Phase 5) ─────────────────────────
|
||||
// ══════════════════════════════════════════════════════════════════
|
||||
//
|
||||
// Algorithm:
|
||||
// 1. Compute SDF for each voxel: smooth solid = -1, empty = +1
|
||||
// Non-smooth solid voxels act as hard walls (SDF crushed to -1).
|
||||
// 2. For each cell on the surface (SDF sign differs from at least one neighbor),
|
||||
// place a vertex at the centroid of edge crossings.
|
||||
// 3. For each edge (pair of adjacent cells) with a sign change,
|
||||
// emit a quad connecting the 4 cells that share that edge, then split to 2 triangles.
|
||||
// 4. Normals derived from SDF gradient (central differences).
|
||||
|
||||
// Padded grid: +2 border for cross-chunk SDF lookups and neighbor smooth detection
|
||||
static constexpr int PAD = 2;
|
||||
static constexpr int GRID = CHUNK_SIZE + 2 * PAD; // 36
|
||||
|
||||
static inline int gridIdx(int x, int y, int z) {
|
||||
return (x + PAD) + (y + PAD) * GRID + (z + PAD) * GRID * GRID;
|
||||
}
|
||||
|
||||
// Helper: read voxel data at chunk-local coords (with cross-chunk fallback)
|
||||
static VoxelData readVoxel(const Chunk& chunk, const VoxelWorld& world, int x, int y, int z) {
|
||||
if (chunk.isInBounds(x, y, z))
|
||||
return chunk.at(x, y, z);
|
||||
return world.getVoxel(
|
||||
chunk.pos.x * CHUNK_SIZE + x,
|
||||
chunk.pos.y * CHUNK_SIZE + y,
|
||||
chunk.pos.z * CHUNK_SIZE + z);
|
||||
}
|
||||
|
||||
float SmoothMesher::computeSDF(const Chunk& chunk, const VoxelWorld& world,
|
||||
int x, int y, int z) {
|
||||
VoxelData v = readVoxel(chunk, world, x, y, z);
|
||||
if (v.isEmpty()) return 1.0f; // empty → positive SDF
|
||||
return -1.0f; // any solid → negative SDF
|
||||
}
|
||||
|
||||
void SmoothMesher::computeNormal(const Chunk& chunk, const VoxelWorld& world,
|
||||
int x, int y, int z,
|
||||
float& nx, float& ny, float& nz) {
|
||||
// Central differences of the SDF
|
||||
float dx = computeSDF(chunk, world, x+1, y, z) - computeSDF(chunk, world, x-1, y, z);
|
||||
float dy = computeSDF(chunk, world, x, y+1, z) - computeSDF(chunk, world, x, y-1, z);
|
||||
float dz = computeSDF(chunk, world, x, y, z+1) - computeSDF(chunk, world, x, y, z-1);
|
||||
|
||||
float len = std::sqrt(dx*dx + dy*dy + dz*dz);
|
||||
if (len > 0.0001f) {
|
||||
nx = dx / len;
|
||||
ny = dy / len;
|
||||
nz = dz / len;
|
||||
} else {
|
||||
nx = 0.0f; ny = 1.0f; nz = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t SmoothMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) {
|
||||
chunk.smoothVertices.clear();
|
||||
chunk.hasSmooth = false;
|
||||
|
||||
// ── Step 1: Build SDF grid + smooth flag grid ────────────────
|
||||
// PAD=2 so we have SDF data for cells at [-1..CHUNK_SIZE] (all 8 corners accessible)
|
||||
// Also build a "isSmooth" grid for the same range to detect proximity to smooth voxels.
|
||||
std::vector<float> sdf(GRID * GRID * GRID, 1.0f);
|
||||
// smoothGrid: true if the voxel at that position is smooth
|
||||
std::vector<uint8_t> smoothGrid(GRID * GRID * GRID, 0);
|
||||
bool anySmooth = false;
|
||||
|
||||
for (int z = -PAD; z < CHUNK_SIZE + PAD; z++) {
|
||||
for (int y = -PAD; y < CHUNK_SIZE + PAD; y++) {
|
||||
for (int x = -PAD; x < CHUNK_SIZE + PAD; x++) {
|
||||
int gi = gridIdx(x, y, z);
|
||||
VoxelData v = readVoxel(chunk, world, x, y, z);
|
||||
sdf[gi] = v.isEmpty() ? 1.0f : -1.0f;
|
||||
if (v.isSmooth()) {
|
||||
smoothGrid[gi] = 1;
|
||||
// Only need anySmooth for this chunk's own voxels
|
||||
if (chunk.isInBounds(x, y, z)) anySmooth = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check 1 beyond the chunk (neighbor chunks may have smooth voxels that
|
||||
// affect cells at the chunk boundary)
|
||||
if (!anySmooth) {
|
||||
// Check if any neighbor voxels just outside the chunk are smooth
|
||||
for (int z = -1; z <= CHUNK_SIZE && !anySmooth; z++)
|
||||
for (int y = -1; y <= CHUNK_SIZE && !anySmooth; y++)
|
||||
for (int x = -1; x <= CHUNK_SIZE && !anySmooth; x++) {
|
||||
if (chunk.isInBounds(x, y, z)) continue; // already checked
|
||||
if (smoothGrid[gridIdx(x, y, z)]) anySmooth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anySmooth) return 0;
|
||||
chunk.hasSmooth = true;
|
||||
|
||||
// ── Step 2: Generate vertices for surface cells ──────────────
|
||||
// Extended range: [-1, CHUNK_SIZE) for cross-chunk connectivity.
|
||||
// This chunk generates vertices for cells at [-1..CHUNK_SIZE-1].
|
||||
// The vertex map covers [-1..CHUNK_SIZE-1] → size = CHUNK_SIZE+1, offset by +1.
|
||||
static constexpr int VERT_MIN = -1;
|
||||
static constexpr int VERT_MAX = CHUNK_SIZE; // exclusive
|
||||
static constexpr int VERT_RANGE = VERT_MAX - VERT_MIN; // CHUNK_SIZE + 1 = 33
|
||||
std::vector<int32_t> vertexMap(VERT_RANGE * VERT_RANGE * VERT_RANGE, -1);
|
||||
|
||||
auto vertMapIdx = [](int x, int y, int z) -> int {
|
||||
// shift coordinates by -VERT_MIN = +1 so index range is [0, VERT_RANGE)
|
||||
return (x - VERT_MIN) + (y - VERT_MIN) * VERT_RANGE + (z - VERT_MIN) * VERT_RANGE * VERT_RANGE;
|
||||
};
|
||||
|
||||
// World offset for this chunk
|
||||
float ox = (float)(chunk.pos.x * CHUNK_SIZE);
|
||||
float oy = (float)(chunk.pos.y * CHUNK_SIZE);
|
||||
float oz = (float)(chunk.pos.z * CHUNK_SIZE);
|
||||
|
||||
// Corner offsets: (dx,dy,dz) for corner index 0-7 of a cell
|
||||
static const int cornerOff[8][3] = {
|
||||
{0,0,0}, {1,0,0}, {0,1,0}, {1,1,0},
|
||||
{0,0,1}, {1,0,1}, {0,1,1}, {1,1,1},
|
||||
};
|
||||
static const float cornerOffF[8][3] = {
|
||||
{0,0,0}, {1,0,0}, {0,1,0}, {1,1,0},
|
||||
{0,0,1}, {1,0,1}, {0,1,1}, {1,1,1},
|
||||
};
|
||||
static const int edges[12][2] = {
|
||||
{0,1}, {2,3}, {4,5}, {6,7}, // X-axis edges
|
||||
{0,2}, {1,3}, {4,6}, {5,7}, // Y-axis edges
|
||||
{0,4}, {1,5}, {2,6}, {3,7}, // Z-axis edges
|
||||
};
|
||||
|
||||
for (int z = VERT_MIN; z < VERT_MAX; z++) {
|
||||
for (int y = VERT_MIN; y < VERT_MAX; y++) {
|
||||
for (int x = VERT_MIN; x < VERT_MAX; x++) {
|
||||
// Strict hasSmooth: at least one corner of the cell must be a smooth voxel.
|
||||
// This prevents generating vertices in entirely-blocky territory.
|
||||
bool hasSmooth = false;
|
||||
for (int dz = 0; dz <= 1 && !hasSmooth; dz++)
|
||||
for (int dy = 0; dy <= 1 && !hasSmooth; dy++)
|
||||
for (int dx = 0; dx <= 1 && !hasSmooth; dx++) {
|
||||
if (smoothGrid[gridIdx(x + dx, y + dy, z + dz)])
|
||||
hasSmooth = true;
|
||||
}
|
||||
if (!hasSmooth) continue;
|
||||
|
||||
// Get SDF at 8 corners of cell (x,y,z)
|
||||
float corner[8];
|
||||
bool hasPos = false, hasNeg = false;
|
||||
for (int c = 0; c < 8; c++) {
|
||||
corner[c] = sdf[gridIdx(x + cornerOff[c][0], y + cornerOff[c][1], z + cornerOff[c][2])];
|
||||
if (corner[c] < 0.0f) hasNeg = true;
|
||||
else hasPos = true;
|
||||
}
|
||||
|
||||
if (!hasPos || !hasNeg) continue; // no sign change → not on surface
|
||||
|
||||
// Compute vertex position as centroid of edge crossings.
|
||||
// +0.5 offset: SDF is sampled at voxel centers, so the cell spans
|
||||
// from (x+0.5) to (x+1.5) in world space. This naturally aligns
|
||||
// the isosurface with the integer grid (voxel face positions).
|
||||
float sumX = 0, sumY = 0, sumZ = 0;
|
||||
int crossCount = 0;
|
||||
|
||||
for (int e = 0; e < 12; e++) {
|
||||
float s0 = corner[edges[e][0]];
|
||||
float s1 = corner[edges[e][1]];
|
||||
if ((s0 < 0.0f) == (s1 < 0.0f)) continue;
|
||||
|
||||
float t = s0 / (s0 - s1);
|
||||
t = std::clamp(t, 0.01f, 0.99f);
|
||||
|
||||
const float* c0 = cornerOffF[edges[e][0]];
|
||||
const float* c1 = cornerOffF[edges[e][1]];
|
||||
sumX += c0[0] + t * (c1[0] - c0[0]);
|
||||
sumY += c0[1] + t * (c1[1] - c0[1]);
|
||||
sumZ += c0[2] + t * (c1[2] - c0[2]);
|
||||
crossCount++;
|
||||
}
|
||||
|
||||
if (crossCount == 0) continue;
|
||||
|
||||
float invCross = 1.0f / (float)crossCount;
|
||||
// centroid in [0,1] within the cell
|
||||
float cx = sumX * invCross;
|
||||
float cy = sumY * invCross;
|
||||
float cz = sumZ * invCross;
|
||||
|
||||
// ── Per-axis clamping at blocky boundaries ───────────
|
||||
// With +0.5 offset, the cell spans [x+0.5, x+1.5] in world space.
|
||||
// The integer grid (blocky faces) is at x+1. In centroid coords,
|
||||
// that's centroid = 0.5 (the midpoint of the cell).
|
||||
// If the +side corners (dx=1) contain a blocky solid, clamp centroid ≤ 0.5
|
||||
// If the -side corners (dx=0) contain a blocky solid, clamp centroid ≥ 0.5
|
||||
// This prevents the smooth mesh from extending into blocky territory.
|
||||
bool blockyXlo = false, blockyXhi = false;
|
||||
bool blockyYlo = false, blockyYhi = false;
|
||||
bool blockyZlo = false, blockyZhi = false;
|
||||
for (int c = 0; c < 8; c++) {
|
||||
if (corner[c] >= 0.0f) continue; // empty corner
|
||||
VoxelData v = readVoxel(chunk, world,
|
||||
x + cornerOff[c][0], y + cornerOff[c][1], z + cornerOff[c][2]);
|
||||
if (!v.isEmpty() && !v.isSmooth()) {
|
||||
// This corner is a blocky solid
|
||||
if (cornerOff[c][0] == 0) blockyXlo = true; else blockyXhi = true;
|
||||
if (cornerOff[c][1] == 0) blockyYlo = true; else blockyYhi = true;
|
||||
if (cornerOff[c][2] == 0) blockyZlo = true; else blockyZhi = true;
|
||||
}
|
||||
}
|
||||
if (blockyXhi) cx = std::min(cx, 0.5f);
|
||||
if (blockyXlo) cx = std::max(cx, 0.5f);
|
||||
if (blockyYhi) cy = std::min(cy, 0.5f);
|
||||
if (blockyYlo) cy = std::max(cy, 0.5f);
|
||||
if (blockyZhi) cz = std::min(cz, 0.5f);
|
||||
if (blockyZlo) cz = std::max(cz, 0.5f);
|
||||
|
||||
// World position with +0.5 offset (SDF at voxel centers)
|
||||
float vx = (float)x + 0.5f + cx;
|
||||
float vy = (float)y + 0.5f + cy;
|
||||
float vz = (float)z + 0.5f + cz;
|
||||
|
||||
// Determine material: prefer smooth voxels' materials to avoid
|
||||
// picking up subsurface blocky materials (e.g., dirt under stone)
|
||||
uint8_t smoothMatCounts[256] = {};
|
||||
uint8_t allMatCounts[256] = {};
|
||||
int smoothCount = 0;
|
||||
for (int c = 0; c < 8; c++) {
|
||||
if (corner[c] < 0.0f) {
|
||||
VoxelData v = readVoxel(chunk, world,
|
||||
x + cornerOff[c][0], y + cornerOff[c][1], z + cornerOff[c][2]);
|
||||
if (!v.isEmpty()) {
|
||||
allMatCounts[v.getMaterialID()]++;
|
||||
if (v.isSmooth()) {
|
||||
smoothMatCounts[v.getMaterialID()]++;
|
||||
smoothCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Primary material: prefer smooth-only counts to avoid subsurface bleed
|
||||
uint8_t* primaryCounts = (smoothCount > 0) ? smoothMatCounts : allMatCounts;
|
||||
uint8_t bestMat = 6, bestCount = 0;
|
||||
for (int m = 1; m < 256; m++) {
|
||||
if (primaryCounts[m] > bestCount) {
|
||||
bestMat = (uint8_t)m; bestCount = primaryCounts[m];
|
||||
}
|
||||
}
|
||||
// Secondary material: search ALL materials (including blocky) for blending
|
||||
uint8_t secMat = bestMat, secCount = 0;
|
||||
for (int m = 1; m < 256; m++) {
|
||||
if (m == bestMat) continue;
|
||||
if (allMatCounts[m] > secCount) {
|
||||
secMat = (uint8_t)m; secCount = allMatCounts[m];
|
||||
}
|
||||
}
|
||||
uint8_t blendW = 0;
|
||||
if (secCount > 0 && bestCount + secCount > 0)
|
||||
blendW = (uint8_t)(255u * secCount / (bestCount + secCount));
|
||||
|
||||
// Normal from SDF gradient (used later for face normal orientation check)
|
||||
float gnx, gny, gnz;
|
||||
computeNormal(chunk, world, x, y, z, gnx, gny, gnz);
|
||||
|
||||
// Store vertex
|
||||
int32_t vertIdx = (int32_t)chunk.smoothVertices.size();
|
||||
vertexMap[vertMapIdx(x, y, z)] = vertIdx;
|
||||
|
||||
SmoothVertex sv;
|
||||
sv.px = ox + vx;
|
||||
sv.py = oy + vy;
|
||||
sv.pz = oz + vz;
|
||||
sv.nx = gnx;
|
||||
sv.ny = gny;
|
||||
sv.nz = gnz;
|
||||
sv.materialID = bestMat;
|
||||
sv.secondaryMat = secMat;
|
||||
sv.blendWeight = blendW;
|
||||
sv._pad1 = 0;
|
||||
sv.chunkIndex = 0;
|
||||
sv._pad2 = 0;
|
||||
chunk.smoothVertices.push_back(sv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chunk.smoothVertices.empty()) {
|
||||
chunk.hasSmooth = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Step 3: Emit quads for edges with sign change ────────────
|
||||
// Canonical ownership: this chunk owns edges whose lower endpoint
|
||||
// is in [0, CHUNK_SIZE). Extended to check edges at the chunk
|
||||
// boundary (lower endpoint at CHUNK_SIZE-1, upper at CHUNK_SIZE).
|
||||
// The sharing cells may be at [-1..CHUNK_SIZE-1], all covered by vertex map.
|
||||
|
||||
// Tri with edge axis info for correct normal orientation.
|
||||
// normalAxis: 0=X, 1=Y, 2=Z — the axis of the edge that generated this quad.
|
||||
// normalSign: +1 if the normal should point in +axis direction, -1 for -axis.
|
||||
struct Tri { int32_t a, b, c; int8_t normalAxis; int8_t normalSign; };
|
||||
std::vector<Tri> triangles;
|
||||
triangles.reserve(chunk.smoothVertices.size() * 2);
|
||||
|
||||
// Helper: safe vertex map lookup (returns -1 if out of range)
|
||||
auto safeVertMap = [&](int x, int y, int z) -> int32_t {
|
||||
if (x < VERT_MIN || x >= VERT_MAX ||
|
||||
y < VERT_MIN || y >= VERT_MAX ||
|
||||
z < VERT_MIN || z >= VERT_MAX) return -1;
|
||||
return vertexMap[vertMapIdx(x, y, z)];
|
||||
};
|
||||
|
||||
// Helper: emit 2 triangles for a quad (a,b,c,d) with known desired normal.
|
||||
// The Y-axis sharing cells have a different spatial arrangement from X and Z,
|
||||
// requiring opposite winding to produce correct front-facing triangles.
|
||||
auto emitQuad = [&](int a, int b, int c, int d, float s0, int8_t axis) {
|
||||
if (a < 0 || b < 0 || c < 0 || d < 0) return;
|
||||
int8_t sign = (s0 < 0.0f) ? +1 : -1;
|
||||
// Y-axis has natural winding swapped relative to X and Z
|
||||
bool useWindingA = (s0 > 0.0f);
|
||||
if (axis == 1) useWindingA = !useWindingA;
|
||||
if (useWindingA) {
|
||||
triangles.push_back({a, b, d, axis, sign});
|
||||
triangles.push_back({a, d, c, axis, sign});
|
||||
} else {
|
||||
triangles.push_back({a, d, b, axis, sign});
|
||||
triangles.push_back({a, c, d, axis, sign});
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over edges owned by this chunk: grid points [0, CHUNK_SIZE)
|
||||
for (int z = 0; z < CHUNK_SIZE; z++) {
|
||||
for (int y = 0; y < CHUNK_SIZE; y++) {
|
||||
for (int x = 0; x < CHUNK_SIZE; x++) {
|
||||
float s0 = sdf[gridIdx(x, y, z)];
|
||||
|
||||
// X-axis edge: (x,y,z) → (x+1,y,z)
|
||||
{
|
||||
float s1 = sdf[gridIdx(x+1, y, z)];
|
||||
if ((s0 < 0.0f) != (s1 < 0.0f)) {
|
||||
emitQuad(
|
||||
safeVertMap(x, y-1, z-1), safeVertMap(x, y, z-1),
|
||||
safeVertMap(x, y-1, z), safeVertMap(x, y, z),
|
||||
s0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Y-axis edge: (x,y,z) → (x,y+1,z)
|
||||
{
|
||||
float s1 = sdf[gridIdx(x, y+1, z)];
|
||||
if ((s0 < 0.0f) != (s1 < 0.0f)) {
|
||||
emitQuad(
|
||||
safeVertMap(x-1, y, z-1), safeVertMap(x, y, z-1),
|
||||
safeVertMap(x-1, y, z), safeVertMap(x, y, z),
|
||||
s0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Z-axis edge: (x,y,z) → (x,y,z+1)
|
||||
{
|
||||
float s1 = sdf[gridIdx(x, y, z+1)];
|
||||
if ((s0 < 0.0f) != (s1 < 0.0f)) {
|
||||
emitQuad(
|
||||
safeVertMap(x-1, y-1, z), safeVertMap(x, y-1, z),
|
||||
safeVertMap(x-1, y, z), safeVertMap(x, y, z),
|
||||
s0, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step 4: Expand indexed triangles + compute face normals ──
|
||||
// Face normals from cross product, oriented using the known edge axis.
|
||||
std::vector<SmoothVertex> expanded;
|
||||
expanded.reserve(triangles.size() * 3);
|
||||
for (const auto& tri : triangles) {
|
||||
SmoothVertex va = chunk.smoothVertices[tri.a];
|
||||
SmoothVertex vb = chunk.smoothVertices[tri.b];
|
||||
SmoothVertex vc = chunk.smoothVertices[tri.c];
|
||||
|
||||
// Compute face normal from triangle edges
|
||||
float e1x = vb.px - va.px, e1y = vb.py - va.py, e1z = vb.pz - va.pz;
|
||||
float e2x = vc.px - va.px, e2y = vc.py - va.py, e2z = vc.pz - va.pz;
|
||||
float fnx = e1y * e2z - e1z * e2y;
|
||||
float fny = e1z * e2x - e1x * e2z;
|
||||
float fnz = e1x * e2y - e1y * e2x;
|
||||
float len = std::sqrt(fnx*fnx + fny*fny + fnz*fnz);
|
||||
if (len > 0.0001f) {
|
||||
fnx /= len; fny /= len; fnz /= len;
|
||||
} else {
|
||||
fnx = 0; fny = 1; fnz = 0;
|
||||
}
|
||||
|
||||
// Orient face normal using the known edge axis direction.
|
||||
// The quad for an X-axis edge has its normal roughly along X, etc.
|
||||
// Check if the face normal's component along the desired axis matches the sign.
|
||||
float component = (tri.normalAxis == 0) ? fnx : (tri.normalAxis == 1) ? fny : fnz;
|
||||
if ((component > 0.0f) != (tri.normalSign > 0)) {
|
||||
fnx = -fnx; fny = -fny; fnz = -fnz;
|
||||
}
|
||||
|
||||
// Assign face normal to all 3 vertices (flat shading)
|
||||
va.nx = fnx; va.ny = fny; va.nz = fnz;
|
||||
vb.nx = fnx; vb.ny = fny; vb.nz = fnz;
|
||||
vc.nx = fnx; vc.ny = fny; vc.nz = fnz;
|
||||
|
||||
expanded.push_back(va);
|
||||
expanded.push_back(vb);
|
||||
expanded.push_back(vc);
|
||||
}
|
||||
|
||||
chunk.smoothVertices = std::move(expanded);
|
||||
chunk.smoothVertexCount = (uint32_t)chunk.smoothVertices.size();
|
||||
|
||||
return chunk.smoothVertexCount;
|
||||
}
|
||||
|
||||
} // namespace voxel
|
||||
|
|
|
|||
|
|
@ -37,4 +37,25 @@ private:
|
|||
int x, int y, int z, uint8_t face);
|
||||
};
|
||||
|
||||
// ── Naive Surface Nets Mesher (Phase 5) ─────────────────────────
|
||||
// Generates smooth triangle mesh for voxels marked FLAG_SMOOTH.
|
||||
// Algorithm: one vertex per surface cell, positioned at edge-crossing centroid.
|
||||
// Quads emitted for each edge with sign change, then split into 2 triangles.
|
||||
class SmoothMesher {
|
||||
public:
|
||||
// Mesh smooth voxels in a chunk, populating chunk.smoothVertices.
|
||||
// Returns number of smooth vertices generated (always multiple of 3, triangle list).
|
||||
static uint32_t meshChunk(Chunk& chunk, const VoxelWorld& world);
|
||||
|
||||
private:
|
||||
// SDF value at a voxel position (solid smooth = -1, empty = +1)
|
||||
// Non-smooth solid voxels are treated as walls (SDF = -1 at boundary)
|
||||
static float computeSDF(const Chunk& chunk, const VoxelWorld& world,
|
||||
int x, int y, int z);
|
||||
|
||||
// Compute SDF gradient (numerical central differences) for normal
|
||||
static void computeNormal(const Chunk& chunk, const VoxelWorld& world,
|
||||
int x, int y, int z, float& nx, float& ny, float& nz);
|
||||
};
|
||||
|
||||
} // namespace voxel
|
||||
|
|
|
|||
|
|
@ -195,6 +195,24 @@ void VoxelRenderer::createPipeline() {
|
|||
} else {
|
||||
wi::backlog::post("VoxelRenderer: toping shader loading failed", wi::backlog::LogLevel::Warning);
|
||||
}
|
||||
|
||||
// ── Smooth surface pipeline (Phase 5) ────────────────────────
|
||||
wi::renderer::LoadShader(ShaderStage::VS, smoothVS_, "voxel/voxelSmoothVS.cso");
|
||||
wi::renderer::LoadShader(ShaderStage::PS, smoothPS_, "voxel/voxelSmoothPS.cso");
|
||||
|
||||
if (smoothVS_.IsValid() && smoothPS_.IsValid()) {
|
||||
PipelineStateDesc smoothPsoDesc;
|
||||
smoothPsoDesc.vs = &smoothVS_;
|
||||
smoothPsoDesc.ps = &smoothPS_;
|
||||
smoothPsoDesc.rs = wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT);
|
||||
smoothPsoDesc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEFAULT);
|
||||
smoothPsoDesc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_OPAQUE);
|
||||
smoothPsoDesc.pt = PrimitiveTopology::TRIANGLELIST;
|
||||
device_->CreatePipelineState(&smoothPsoDesc, &smoothPso_);
|
||||
wi::backlog::post("VoxelRenderer: smooth surface pipeline created");
|
||||
} else {
|
||||
wi::backlog::post("VoxelRenderer: smooth shader loading failed", wi::backlog::LogLevel::Warning);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Procedural texture generation ───────────────────────────────
|
||||
|
|
@ -235,7 +253,7 @@ static void generateNoiseTexture(uint8_t* pixels, int w, int h,
|
|||
|
||||
void VoxelRenderer::generateTextures() {
|
||||
const int TEX_SIZE = 256;
|
||||
const int NUM_MATERIALS = 5;
|
||||
const int NUM_MATERIALS = 6;
|
||||
|
||||
std::vector<uint8_t> allPixels(TEX_SIZE * TEX_SIZE * 4 * NUM_MATERIALS);
|
||||
|
||||
|
|
@ -246,11 +264,12 @@ void VoxelRenderer::generateTextures() {
|
|||
float heightContrast; // heightmap contrast (higher = more defined peaks)
|
||||
};
|
||||
MatColor colors[NUM_MATERIALS] = {
|
||||
{ 60, 140, 40, 80, 180, 60, 101, 1.5f, 0.8f }, // Grass: medium bumps
|
||||
{ 100, 70, 40, 140, 100, 60, 202, 0.8f, 0.6f }, // Dirt: smooth mounds
|
||||
{ 80, 80, 90, 120, 120, 130, 303, 2.5f, 0.5f }, // Stone: darker blue-gray, moderate height (was 1.2, lowered so neighbors bleed onto it more)
|
||||
{ 220, 200, 130, 245, 230, 160, 404, 3.0f, 0.4f }, // Sand: warmer yellow, fine
|
||||
{ 220, 225, 230, 245, 248, 252, 505, 1.0f, 0.5f }, // Snow: smooth, soft
|
||||
{ 60, 140, 40, 80, 180, 60, 101, 1.5f, 0.8f }, // 1: Grass: medium bumps
|
||||
{ 100, 70, 40, 140, 100, 60, 202, 0.8f, 0.6f }, // 2: Dirt: smooth mounds
|
||||
{ 80, 80, 90, 120, 120, 130, 303, 2.5f, 0.5f }, // 3: Stone (blocky): darker blue-gray
|
||||
{ 220, 200, 130, 245, 230, 160, 404, 3.0f, 0.4f }, // 4: Sand: warmer yellow, fine
|
||||
{ 220, 225, 230, 245, 248, 252, 505, 1.0f, 0.5f }, // 5: Snow: smooth, soft
|
||||
{ 100, 100, 110, 145, 145, 155, 606, 2.0f, 0.6f }, // 6: SmoothStone: lighter blue-gray, distinct from blocky stone
|
||||
};
|
||||
|
||||
for (int i = 0; i < NUM_MATERIALS; i++) {
|
||||
|
|
@ -711,8 +730,8 @@ void VoxelRenderer::render(
|
|||
// Per-material blend flags (bit N = material N):
|
||||
// canBleed: material can overflow visually onto adjacent voxels
|
||||
// resistBleed: adjacent materials cannot overflow onto this material
|
||||
// Material IDs: 1=Grass, 2=Dirt, 3=Stone, 4=Sand, 5=Snow
|
||||
cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5); // Grass, Dirt, Sand, Snow can bleed (NOT Stone)
|
||||
// Material IDs: 1=Grass, 2=Dirt, 3=Stone, 4=Sand, 5=Snow, 6=SmoothStone
|
||||
cb.bleedMask = (1u << 1) | (1u << 2) | (1u << 4) | (1u << 5); // Grass, Dirt, Sand, Snow can bleed (NOT Stone/SmoothStone)
|
||||
cb.resistBleedMask = (1u << 1); // Grass resists bleed (she bleeds onto others, not the reverse)
|
||||
cb.windTime = windTime_;
|
||||
dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb));
|
||||
|
|
@ -1320,6 +1339,109 @@ void VoxelRenderer::renderTopings(
|
|||
dev->RenderPassEnd(cmd);
|
||||
}
|
||||
|
||||
// ── Phase 5: Smooth Surface Nets upload + rendering ─────────────
|
||||
|
||||
void VoxelRenderer::uploadSmoothData(VoxelWorld& world) {
|
||||
if (!device_ || !smoothPso_.IsValid()) return;
|
||||
|
||||
// Collect all smooth vertices from all chunks
|
||||
std::vector<SmoothVertex> allVerts;
|
||||
allVerts.reserve(64 * 1024); // rough estimate
|
||||
|
||||
world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
|
||||
if (!chunk.hasSmooth || chunk.smoothVertexCount == 0) return;
|
||||
|
||||
allVerts.insert(allVerts.end(),
|
||||
chunk.smoothVertices.begin(),
|
||||
chunk.smoothVertices.end());
|
||||
});
|
||||
|
||||
smoothVertexCount_ = (uint32_t)std::min(allVerts.size(), (size_t)MAX_SMOOTH_VERTICES);
|
||||
|
||||
if (smoothVertexCount_ == 0) {
|
||||
smoothDirty_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or recreate vertex buffer
|
||||
GPUBufferDesc vbDesc;
|
||||
vbDesc.size = smoothVertexCount_ * sizeof(SmoothVertex);
|
||||
vbDesc.bind_flags = BindFlag::SHADER_RESOURCE;
|
||||
vbDesc.misc_flags = ResourceMiscFlag::BUFFER_STRUCTURED;
|
||||
vbDesc.stride = sizeof(SmoothVertex);
|
||||
vbDesc.usage = Usage::DEFAULT;
|
||||
device_->CreateBuffer(&vbDesc, allVerts.data(), &smoothVertexBuffer_);
|
||||
|
||||
smoothDirty_ = false;
|
||||
|
||||
char msg[128];
|
||||
snprintf(msg, sizeof(msg), "Smooth: uploaded %u vertices (%u triangles, %.1f KB)",
|
||||
smoothVertexCount_, smoothVertexCount_ / 3,
|
||||
smoothVertexCount_ * sizeof(SmoothVertex) / 1024.0f);
|
||||
wi::backlog::post(msg);
|
||||
}
|
||||
|
||||
void VoxelRenderer::renderSmooth(
|
||||
CommandList cmd,
|
||||
const Texture& depthBuffer,
|
||||
const Texture& renderTarget
|
||||
) const {
|
||||
if (!smoothPso_.IsValid() || !smoothVertexBuffer_.IsValid() ||
|
||||
smoothVertexCount_ == 0) return;
|
||||
|
||||
auto* dev = device_;
|
||||
|
||||
// Open render pass with LOAD (preserve voxel + toping render output)
|
||||
RenderPassImage rp[] = {
|
||||
RenderPassImage::RenderTarget(
|
||||
&renderTarget,
|
||||
RenderPassImage::LoadOp::LOAD,
|
||||
RenderPassImage::StoreOp::STORE,
|
||||
ResourceState::SHADER_RESOURCE,
|
||||
ResourceState::SHADER_RESOURCE
|
||||
),
|
||||
RenderPassImage::DepthStencil(
|
||||
&depthBuffer,
|
||||
RenderPassImage::LoadOp::LOAD,
|
||||
RenderPassImage::StoreOp::STORE,
|
||||
ResourceState::DEPTHSTENCIL,
|
||||
ResourceState::DEPTHSTENCIL,
|
||||
ResourceState::DEPTHSTENCIL
|
||||
),
|
||||
};
|
||||
dev->RenderPassBegin(rp, 2, cmd);
|
||||
|
||||
// Viewport & scissor
|
||||
Viewport vp;
|
||||
vp.top_left_x = 0; vp.top_left_y = 0;
|
||||
vp.width = (float)renderTarget.GetDesc().width;
|
||||
vp.height = (float)renderTarget.GetDesc().height;
|
||||
vp.min_depth = 0.0f; vp.max_depth = 1.0f;
|
||||
Rect scissor = { 0, 0, (int)renderTarget.GetDesc().width, (int)renderTarget.GetDesc().height };
|
||||
dev->BindViewports(1, &vp, cmd);
|
||||
dev->BindScissorRects(1, &scissor, cmd);
|
||||
|
||||
// Bind smooth pipeline (MUST be before PushConstants!)
|
||||
dev->BindPipelineState(&smoothPso_, cmd);
|
||||
dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
|
||||
dev->BindResource(&textureArray_, 1, cmd);
|
||||
dev->BindResource(&smoothVertexBuffer_, 6, cmd); // t6
|
||||
dev->BindSampler(&sampler_, 0, cmd);
|
||||
|
||||
// Push constants (unused by smooth VS, but must be valid 48 bytes)
|
||||
struct SmoothPush {
|
||||
uint32_t pad[12];
|
||||
};
|
||||
SmoothPush pushData = {};
|
||||
dev->PushConstants(&pushData, sizeof(pushData), cmd);
|
||||
|
||||
// Single draw call for all smooth vertices
|
||||
dev->DrawInstanced(smoothVertexCount_, 1, 0, 0, cmd);
|
||||
smoothDrawCalls_ = 1;
|
||||
|
||||
dev->RenderPassEnd(cmd);
|
||||
}
|
||||
|
||||
// ── VoxelRenderPath (custom RenderPath3D) ───────────────────────
|
||||
|
||||
void VoxelRenderPath::Start() {
|
||||
|
|
@ -1358,6 +1480,25 @@ void VoxelRenderPath::Start() {
|
|||
wi::backlog::post(msg);
|
||||
}
|
||||
|
||||
// Phase 5: CPU Surface Nets mesh for smooth voxels, upload to GPU
|
||||
if (renderer.isInitialized()) {
|
||||
uint32_t totalSmooth = 0;
|
||||
uint32_t smoothChunks = 0;
|
||||
world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
|
||||
uint32_t count = SmoothMesher::meshChunk(chunk, world);
|
||||
if (count > 0) {
|
||||
totalSmooth += count;
|
||||
smoothChunks++;
|
||||
}
|
||||
});
|
||||
renderer.uploadSmoothData(world);
|
||||
char msg[256];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"SmoothMesher: %u vertices (%u tris) in %u chunks",
|
||||
totalSmooth, totalSmooth / 3, smoothChunks);
|
||||
wi::backlog::post(msg);
|
||||
}
|
||||
|
||||
worldGenerated_ = true;
|
||||
|
||||
setAO(AO_DISABLED);
|
||||
|
|
@ -1549,6 +1690,9 @@ void VoxelRenderPath::Render() const {
|
|||
|
||||
// Phase 4: render topings (separate render pass, preserves voxel output)
|
||||
renderer.renderTopings(cmd, topingSystem, voxelDepth_, voxelRT_);
|
||||
|
||||
// Phase 5: render smooth surfaces (separate render pass, preserves all prior output)
|
||||
renderer.renderSmooth(cmd, voxelDepth_, voxelRT_);
|
||||
auto tRender1 = std::chrono::high_resolution_clock::now();
|
||||
profRender_.add(std::chrono::duration<float, std::milli>(tRender1 - tRender0).count());
|
||||
}
|
||||
|
|
@ -1608,7 +1752,7 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
|
|||
char dtStr[16];
|
||||
snprintf(dtStr, sizeof(dtStr), "%.2f", lastDt_ * 1000.0f);
|
||||
|
||||
std::string stats = "BVLE Voxel Engine (Phase 4 — Toping)\n";
|
||||
std::string stats = "BVLE Voxel Engine (Phase 5 — Smooth Surfaces)\n";
|
||||
stats += "FPS: " + std::string(fpsStr) + " (" + std::string(dtStr) + " ms)\n";
|
||||
if (debugMode) {
|
||||
stats += "=== DEBUG FACE MODE ===\n";
|
||||
|
|
@ -1640,6 +1784,11 @@ void VoxelRenderPath::Compose(CommandList cmd) const {
|
|||
stats += "Topings: " + std::to_string(topingSystem.getInstanceCount())
|
||||
+ " instances, " + std::to_string(renderer.getTopingDrawCalls())
|
||||
+ " draws (" + std::to_string(topingSystem.getDefCount()) + " types)\n";
|
||||
if (renderer.getSmoothVertexCount() > 0) {
|
||||
stats += "Smooth: " + std::to_string(renderer.getSmoothVertexCount())
|
||||
+ " verts (" + std::to_string(renderer.getSmoothVertexCount() / 3)
|
||||
+ " tris), " + std::to_string(renderer.getSmoothDrawCalls()) + " draws\n";
|
||||
}
|
||||
stats += "WASD+Space/Ctrl: move | Shift: fast | Right-click: capture mouse\n";
|
||||
stats += "F2: console | F3: anim [" + std::string(animatedTerrain_ ? "ON" : "OFF")
|
||||
+ "] | F4: dbg [" + std::string(renderer.debugBlend_ ? "ON" : "OFF") + "]";
|
||||
|
|
|
|||
|
|
@ -83,6 +83,16 @@ private:
|
|||
static constexpr uint32_t MAX_TOPING_INSTANCES = 256 * 1024; // 256K instances max
|
||||
mutable uint32_t topingDrawCalls_ = 0;
|
||||
|
||||
// Shaders & Pipeline (smooth surfaces, Phase 5)
|
||||
wi::graphics::Shader smoothVS_;
|
||||
wi::graphics::Shader smoothPS_;
|
||||
wi::graphics::PipelineState smoothPso_;
|
||||
wi::graphics::GPUBuffer smoothVertexBuffer_; // StructuredBuffer<SmoothVertex>, SRV t6
|
||||
static constexpr uint32_t MAX_SMOOTH_VERTICES = 4 * 1024 * 1024; // 4M vertices max
|
||||
mutable uint32_t smoothVertexCount_ = 0;
|
||||
mutable uint32_t smoothDrawCalls_ = 0;
|
||||
bool smoothDirty_ = true;
|
||||
|
||||
// Texture array for materials (256x256, 5 layers for prototype)
|
||||
wi::graphics::Texture textureArray_;
|
||||
wi::graphics::Sampler sampler_;
|
||||
|
|
@ -206,6 +216,16 @@ public:
|
|||
const wi::graphics::Texture& renderTarget
|
||||
) const;
|
||||
uint32_t getTopingDrawCalls() const { return topingDrawCalls_; }
|
||||
|
||||
// Phase 5: Smooth surface rendering
|
||||
void uploadSmoothData(VoxelWorld& world);
|
||||
void renderSmooth(
|
||||
wi::graphics::CommandList cmd,
|
||||
const wi::graphics::Texture& depthBuffer,
|
||||
const wi::graphics::Texture& renderTarget
|
||||
) const;
|
||||
uint32_t getSmoothVertexCount() const { return smoothVertexCount_; }
|
||||
uint32_t getSmoothDrawCalls() const { return smoothDrawCalls_; }
|
||||
};
|
||||
|
||||
// ── Custom RenderPath that integrates voxel rendering ───────────
|
||||
|
|
|
|||
|
|
@ -92,6 +92,21 @@ enum Face : uint8_t {
|
|||
FACE_COUNT = 6
|
||||
};
|
||||
|
||||
// ── Smooth Surface Nets vertex (Phase 5) ────────────────────────
|
||||
// Packed as 32 bytes for GPU StructuredBuffer.
|
||||
// Position is in world space (chunk origin already added).
|
||||
struct SmoothVertex {
|
||||
float px, py, pz; // 12 bytes — world position
|
||||
float nx, ny, nz; // 12 bytes — normal (face normal)
|
||||
uint8_t materialID; // 1 byte — primary material
|
||||
uint8_t secondaryMat; // 1 byte — secondary material for blending
|
||||
uint8_t blendWeight; // 1 byte — 0-255 → 0.0-1.0 blend toward secondary
|
||||
uint8_t _pad1; // 1 byte alignment
|
||||
uint16_t chunkIndex; // 2 bytes — which chunk owns this vertex
|
||||
uint16_t _pad2; // 2 bytes alignment
|
||||
}; // total = 32 bytes
|
||||
static_assert(sizeof(SmoothVertex) == 32, "SmoothVertex must be 32 bytes");
|
||||
|
||||
// ── Material Descriptor ─────────────────────────────────────────
|
||||
struct MaterialDesc {
|
||||
uint16_t albedoTextureIndex = 0;
|
||||
|
|
|
|||
|
|
@ -139,14 +139,21 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
|
|||
float matVal = matNoise1 * 0.6f + matNoise2 * 0.4f;
|
||||
|
||||
uint8_t surfaceMat;
|
||||
bool surfaceSmooth = false;
|
||||
if (matVal < -0.25f) {
|
||||
surfaceMat = 4; // Sand
|
||||
} else if (matVal < -0.10f) {
|
||||
surfaceMat = 3; // Stone (blocky, with topings)
|
||||
} else if (matVal < 0.0f) {
|
||||
surfaceMat = 3; // Stone
|
||||
surfaceMat = 6; // SmoothStone (smooth surface)
|
||||
surfaceSmooth = true;
|
||||
} else if (matVal < 0.15f) {
|
||||
surfaceMat = 2; // Dirt (blocky)
|
||||
} else if (matVal < 0.30f) {
|
||||
surfaceMat = 1; // Grass
|
||||
} else if (matNoise3 > 0.1f) {
|
||||
surfaceMat = 5; // Snow (patches via independent noise)
|
||||
surfaceMat = 5; // Snow (smooth)
|
||||
surfaceSmooth = true;
|
||||
} else {
|
||||
surfaceMat = 2; // Dirt
|
||||
}
|
||||
|
|
@ -155,6 +162,7 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
|
|||
float wy = (float)(chunk.pos.y * CHUNK_SIZE + y);
|
||||
VoxelData v;
|
||||
|
||||
uint8_t smoothFlag = surfaceSmooth ? VoxelData::FLAG_SMOOTH : 0;
|
||||
if (wy > height) {
|
||||
// Air above terrain
|
||||
v = VoxelData();
|
||||
|
|
@ -164,7 +172,7 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
|
|||
if (std::abs(cave) < caveThreshold && wy > 10.0f && wy < height - 3.0f) {
|
||||
v = VoxelData(); // Cave
|
||||
} else if (wy > height - 1.0f) {
|
||||
v = VoxelData(surfaceMat);
|
||||
v = VoxelData(surfaceMat, smoothFlag);
|
||||
} else if (wy > height - 4.0f) {
|
||||
v = VoxelData(2); // Dirt sub-surface
|
||||
} else {
|
||||
|
|
@ -173,7 +181,7 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
|
|||
} else {
|
||||
// Animation path: simplified material assignment (no caves)
|
||||
if (wy > height - 1.0f) {
|
||||
v = VoxelData(surfaceMat);
|
||||
v = VoxelData(surfaceMat, smoothFlag);
|
||||
} else if (wy > height - 4.0f) {
|
||||
v = VoxelData(2);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ struct Chunk {
|
|||
uint32_t faceOffsets[6] = {}; // offset (in quads) for each face group within quads[]
|
||||
uint32_t faceCounts[6] = {}; // number of quads per face group
|
||||
|
||||
// Smooth mesh data (output of Surface Nets mesher, Phase 5)
|
||||
std::vector<SmoothVertex> smoothVertices;
|
||||
uint32_t smoothVertexCount = 0;
|
||||
bool hasSmooth = false; // true if chunk contains any smooth voxels
|
||||
|
||||
VoxelData& at(int x, int y, int z) {
|
||||
return voxels[x + y * CHUNK_SIZE + z * CHUNK_SIZE * CHUNK_SIZE];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue