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:
Samuel Bouchet 2026-03-27 13:03:55 +01:00
parent 72af8af979
commit aab38bb9b9
13 changed files with 810 additions and 27 deletions

View file

@ -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

View file

@ -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)"
)

View file

@ -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

View file

@ -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;

View 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);
}

View 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;
}

View file

@ -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

View file

@ -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

View file

@ -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") + "]";

View file

@ -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 ───────────

View file

@ -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;

View file

@ -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 {

View file

@ -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];
}