diff --git a/CLAUDE.md b/CLAUDE.md index 54b0672..5e23e2a 100644 --- a/CLAUDE.md +++ b/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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4567247..2cbf5f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,10 @@ add_custom_command(TARGET BVLEVoxels POST_BUILD $/shaders/hlsl6/voxel/voxelPS.cso $/shaders/hlsl6/voxel/voxelCullCS.cso $/shaders/hlsl6/voxel/voxelMeshCS.cso + $/shaders/hlsl6/voxel/voxelTopingVS.cso + $/shaders/hlsl6/voxel/voxelTopingPS.cso + $/shaders/hlsl6/voxel/voxelSmoothVS.cso + $/shaders/hlsl6/voxel/voxelSmoothPS.cso $/shaders/hlsl6/voxel/voxelCommon.hlsli.cso COMMENT "Clearing stale voxel shader cache (forces recompilation from current .hlsl sources)" ) diff --git a/shaders/voxelMeshCS.hlsl b/shaders/voxelMeshCS.hlsl index 2fcd02f..2aa033f 100644 --- a/shaders/voxelMeshCS.hlsl +++ b/shaders/voxelMeshCS.hlsl @@ -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 diff --git a/shaders/voxelPS.hlsl b/shaders/voxelPS.hlsl index da3bb8e..521c5c3 100644 --- a/shaders/voxelPS.hlsl +++ b/shaders/voxelPS.hlsl @@ -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; diff --git a/shaders/voxelSmoothPS.hlsl b/shaders/voxelSmoothPS.hlsl new file mode 100644 index 0000000..56c8400 --- /dev/null +++ b/shaders/voxelSmoothPS.hlsl @@ -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 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); +} diff --git a/shaders/voxelSmoothVS.hlsl b/shaders/voxelSmoothVS.hlsl new file mode 100644 index 0000000..c0a4859 --- /dev/null +++ b/shaders/voxelSmoothVS.hlsl @@ -0,0 +1,33 @@ +// BVLE Voxels - Smooth Surface Nets Vertex Shader (Phase 5.1) +// Vertex pulling from StructuredBuffer. +// 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 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; +} diff --git a/src/voxel/VoxelMesher.cpp b/src/voxel/VoxelMesher.cpp index b082ee3..7e4dcb3 100644 --- a/src/voxel/VoxelMesher.cpp +++ b/src/voxel/VoxelMesher.cpp @@ -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 sdf(GRID * GRID * GRID, 1.0f); + // smoothGrid: true if the voxel at that position is smooth + std::vector 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 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 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 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 diff --git a/src/voxel/VoxelMesher.h b/src/voxel/VoxelMesher.h index 49f329f..b0322a9 100644 --- a/src/voxel/VoxelMesher.h +++ b/src/voxel/VoxelMesher.h @@ -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 diff --git a/src/voxel/VoxelRenderer.cpp b/src/voxel/VoxelRenderer.cpp index 6d1b82b..f4f767f 100644 --- a/src/voxel/VoxelRenderer.cpp +++ b/src/voxel/VoxelRenderer.cpp @@ -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 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 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(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") + "]"; diff --git a/src/voxel/VoxelRenderer.h b/src/voxel/VoxelRenderer.h index 3b93daa..5795330 100644 --- a/src/voxel/VoxelRenderer.h +++ b/src/voxel/VoxelRenderer.h @@ -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, 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 ─────────── diff --git a/src/voxel/VoxelTypes.h b/src/voxel/VoxelTypes.h index 686a981..a0c3d02 100644 --- a/src/voxel/VoxelTypes.h +++ b/src/voxel/VoxelTypes.h @@ -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; diff --git a/src/voxel/VoxelWorld.cpp b/src/voxel/VoxelWorld.cpp index 6b40a38..0b0493a 100644 --- a/src/voxel/VoxelWorld.cpp +++ b/src/voxel/VoxelWorld.cpp @@ -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 { diff --git a/src/voxel/VoxelWorld.h b/src/voxel/VoxelWorld.h index c3e1b97..24955fa 100644 --- a/src/voxel/VoxelWorld.h +++ b/src/voxel/VoxelWorld.h @@ -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 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]; }