From b45d5a188403af66b16cfa07f82375c851da0138 Mon Sep 17 00:00:00 2001 From: Samuel Bouchet Date: Fri, 27 Mar 2026 14:21:35 +0100 Subject: [PATCH] Phase 5.1: smooth PS blending uses same logic as blocky PS + debug scene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrote voxelSmoothPS.hlsl to derive a dominant face axis from the smooth normal, then use the exact same neighbor verification as voxelPS.hlsl: faceU/faceV tangent tables, stair-priority getNeighborMat(), face-aligned fractional coords, blendZone 0.25, corner attenuation, bleedMask checks. Added generateDebugSmooth() with 11 isolated test configurations (smooth↔blocky transitions, staircases, surrounded patches, reference blocky pairs). Launch with: BVLEVoxels.exe debugsmooth --- CLAUDE.md | 16 ++- shaders/voxelSmoothPS.hlsl | 210 +++++++++++++++++++++++++++++++----- shaders/voxelSmoothVS.hlsl | 14 +-- src/app/main.cpp | 4 + src/voxel/VoxelMesher.cpp | 29 +++-- src/voxel/VoxelRenderer.cpp | 31 ++++-- src/voxel/VoxelRenderer.h | 1 + src/voxel/VoxelWorld.cpp | 100 +++++++++++++++-- src/voxel/VoxelWorld.h | 3 + 9 files changed, 350 insertions(+), 58 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5e23e2a..cdfa49d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -473,10 +473,18 @@ Système de biseaux décoratifs (« topings ») sur les faces +Y exposées pour - **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 +**Material blending (per-pixel, same as blocky PS)** : +- **Dominant axis detection** : le PS smooth dérive un « face virtuelle » depuis la normale lisse. L'axe avec la plus grande composante `|N|` détermine la face dominante (0-5). Cela donne accès aux mêmes tables `faceNormals`, `faceUDirs`, `faceVDirs` que le PS blocky +- **Même voxelCoord** : `floor(worldPos - normalDir * 0.001)` — tiny offset le long de la normale dominante (PAS `N * 0.5` qui est trop large et tombe dans le mauvais voxel) +- **Même `getNeighborMat()` avec stair priority** : vérifie `pos + edgeDir + normalDir` en premier (le bloc qui masque visuellement l'arête), puis fallback `pos + edgeDir` +- **Face-aligned U/V** : `frac(dot(worldPos, uDir))` / `frac(dot(worldPos, vDir))` — position fractionnaire dans le voxel selon les tangentes de la face dominante +- **Même blend zone (0.25)**, corner attenuation subtractive, winner-takes-all heightmap avec sharpness=16 +- **Même bleedMask/resistBleedMask** checks via CB +- **PIÈGE** : NE PAS utiliser les 3 axes world-space avec un filtre `dirDotN > 0.6` — ça ne filtre pas correctement les voisins souterrains et donne des blends incorrects. La dérivation d'un face dominant + U/V alignés est la seule approche correcte + +**Debug scene smooth** : +- Lancé avec `BVLEVoxels.exe debugsmooth` +- 11 configurations isolées dans un seul chunk : SmoothStone↔Grass, SmoothStone↔Dirt, SmoothStone↔Sand, SmoothStone↔Stone, Snow↔Grass, Snow↔Sand, références blocky (Sand↔Dirt, Grass↔Dirt), escalier SmoothStone, patch smooth entouré de grass, bloc smooth isolé #### Phase 5.2 - Optimisations et polish [A FAIRE] diff --git a/shaders/voxelSmoothPS.hlsl b/shaders/voxelSmoothPS.hlsl index 56c8400..538246f 100644 --- a/shaders/voxelSmoothPS.hlsl +++ b/shaders/voxelSmoothPS.hlsl @@ -1,57 +1,213 @@ // BVLE Voxels - Smooth Surface Nets Pixel Shader (Phase 5.1) -// Triplanar texture sampling + material blending + same lighting as voxel PS. +// Per-pixel heightmap blending using the SAME neighbor verification as voxelPS.hlsl. +// Derives a dominant face axis from the smooth normal, then uses identical +// faceU/faceV/stair-priority logic as the blocky pixel shader. #include "voxelCommon.hlsli" Texture2DArray materialTextures : register(t1); +StructuredBuffer chunkInfoBuffer : register(t2); +StructuredBuffer voxelData : register(t3); SamplerState texSampler : register(s0); struct PSInput { float4 position : SV_POSITION; float3 worldPos : WORLDPOS; float3 normal : NORMAL; - nointerpolation uint matPacked : MATERIALID; + nointerpolation uint primaryMat : PRIMARYMAT; + nointerpolation uint chunkIndex : CHUNKINDEX; }; -// 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; +static const uint CSIZE = 32; +static const uint CVOL = CSIZE * CSIZE * CSIZE; + +// ── Face direction tables (SAME as voxelPS.hlsl) ──────────────── +// Face normals: +X, -X, +Y, -Y, +Z, -Z +static const int3 faceNormals[6] = { + int3( 1, 0, 0), int3(-1, 0, 0), + int3( 0, 1, 0), int3( 0,-1, 0), + int3( 0, 0, 1), int3( 0, 0,-1) +}; + +// Face tangent axes (U, V) — must match voxelPS.hlsl +static const int3 faceUDirs[6] = { + int3(0, 1, 0), int3(0, 1, 0), + int3(1, 0, 0), int3(1, 0, 0), + int3(1, 0, 0), int3(1, 0, 0) +}; +static const int3 faceVDirs[6] = { + int3(0, 0, 1), int3(0, 0, 1), + int3(0, 0, 1), int3(0, 0, 1), + int3(0, 1, 0), int3(0, 1, 0) +}; + +// ── Voxel data read (same as voxelPS.hlsl) ─────────────────────── +uint readVoxelMat(int3 coord, uint chunkIdx) { + GPUChunkInfo info = chunkInfoBuffer[chunkIdx]; + int3 local = coord - (int3)info.worldPos.xyz; + if (any(local < 0) || any(local >= (int3)CSIZE)) + return 0; + uint flatIdx = (uint)local.x + (uint)local.y * CSIZE + (uint)local.z * CSIZE * CSIZE; + uint pairIndex = flatIdx >> 1; + uint shift = (flatIdx & 1) * 16; + uint voxel = (voxelData[chunkIdx * (CVOL / 2) + pairIndex] >> shift) & 0xFFFF; + return voxel >> 8; } +// ── Stair-priority neighbor lookup (SAME as voxelPS.hlsl) ──────── +uint getNeighborMat(int3 voxelCoord, int3 edgeDir, int3 normalDir, uint chunkIdx) { + // Stair neighbor (priority): block at edge AND offset by normal + int3 stairPos = voxelCoord + edgeDir + normalDir; + uint stairMat = readVoxelMat(stairPos, chunkIdx); + if (stairMat > 0) + return stairMat; + + // Planar neighbor (fallback): adjacent block in face plane + int3 planarPos = voxelCoord + edgeDir; + return readVoxelMat(planarPos, chunkIdx); +} + +// ── Triplanar helpers ──────────────────────────────────────────── +float3 triplanarWeights(float3 n, float sharpness) { + float3 w = abs(n); + w = pow(w, (float3)sharpness); + return w / (w.x + w.y + w.z + 0.0001); +} + +float3 sampleTriplanar(float3 wp, float3 n, uint texIdx, float tiling) { + float3 w = triplanarWeights(n, 4.0); + float3 cx = materialTextures.Sample(texSampler, float3(wp.yz * tiling, (float)texIdx)).rgb; + float3 cy = materialTextures.Sample(texSampler, float3(wp.xz * tiling, (float)texIdx)).rgb; + float3 cz = materialTextures.Sample(texSampler, float3(wp.xy * tiling, (float)texIdx)).rgb; + return cx * w.x + cy * w.y + cz * w.z; +} + +float4 sampleTriplanarRGBA(float3 wp, float3 n, uint texIdx, float tiling) { + float3 w = triplanarWeights(n, 4.0); + float4 cx = materialTextures.Sample(texSampler, float3(wp.yz * tiling, (float)texIdx)); + float4 cy = materialTextures.Sample(texSampler, float3(wp.xz * tiling, (float)texIdx)); + float4 cz = materialTextures.Sample(texSampler, float3(wp.xy * tiling, (float)texIdx)); + return cx * w.x + cy * w.y + cz * w.z; +} + +// ── Main PS ────────────────────────────────────────────────────── [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; + // ── Derive dominant face from smooth normal (same tables as blocky PS) ── + // Find the axis with the largest absolute normal component + float3 absN = abs(N); + uint dominantAxis; + if (absN.x >= absN.y && absN.x >= absN.z) + dominantAxis = 0; // X + else if (absN.y >= absN.z) + dominantAxis = 1; // Y + else + dominantAxis = 2; // Z - // Triplanar blend weights - float3 blend = abs(N); - blend = blend / (blend.x + blend.y + blend.z + 0.001); + // Map to face index: axis*2 + (negative ? 1 : 0) + uint face = dominantAxis * 2; + if (N[dominantAxis] < 0.0) face += 1; - // 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); + int3 normalDir = faceNormals[face]; + int3 uDir = faceUDirs[face]; + int3 vDir = faceVDirs[face]; + + // ── Compute voxel coordinate (SAME as blocky PS) ── + // Tiny offset inward along dominant normal to handle integer boundaries + float3 samplePos = input.worldPos - (float3)normalDir * 0.001; + int3 voxelCoord = (int3)floor(samplePos); + + // Read actual material at this voxel position + uint selfMat = readVoxelMat(voxelCoord, input.chunkIndex); + if (selfMat == 0u) selfMat = input.primaryMat; // air fallback + + // ── Face-aligned fractional position (SAME as blocky PS) ── + float faceFracU = frac(dot(input.worldPos, (float3)uDir)); + float faceFracV = frac(dot(input.worldPos, (float3)vDir)); + + // Distance from nearest edge (0 = at edge, 0.5 = at center) + float uDist = 0.5 - abs(faceFracU - 0.5); + float vDist = 0.5 - abs(faceFracV - 0.5); + + // Nearest edge direction + int uSign = (faceFracU >= 0.5) ? 1 : -1; + int vSign = (faceFracV >= 0.5) ? 1 : -1; + int3 uEdgeDir = uDir * uSign; + int3 vEdgeDir = vDir * vSign; + + // ── Stair-priority neighbor lookup (SAME as blocky PS) ── + uint uNeighborMat = getNeighborMat(voxelCoord, uEdgeDir, normalDir, input.chunkIndex); + uint vNeighborMat = getNeighborMat(voxelCoord, vEdgeDir, normalDir, input.chunkIndex); + + // ── Blend weights (SAME params as blocky PS) ── + float blendZone = 0.25; + float uEdge = abs(faceFracU - 0.5) * 2.0; + float vEdge = abs(faceFracV - 0.5) * 2.0; + + // Corner attenuation — subtractive (same as blocky PS) + float blendStart = 1.0 - blendZone * 2.0; + float uAdj = uEdge - saturate(vEdge - 0.80); + float vAdj = vEdge - saturate(uEdge - 0.80); + float uWeight = saturate((uAdj - blendStart) / (1.0 - blendStart)) * 0.5; + float vWeight = saturate((vAdj - blendStart) / (1.0 - blendStart)) * 0.5; + + // Blend conditions (same as blocky PS, with bleed mask checks) + bool mainResists = (resistBleedMask >> selfMat) & 1u; + bool uNeighCanBleed = (bleedMask >> uNeighborMat) & 1u; + bool vNeighCanBleed = (bleedMask >> vNeighborMat) & 1u; + bool uBlend = (uNeighborMat > 0u && uNeighborMat != selfMat && uWeight > 0.001 + && !mainResists && uNeighCanBleed); + bool vBlend = (vNeighborMat > 0u && vNeighborMat != selfMat && vWeight > 0.001 + && !mainResists && vNeighCanBleed); + + // ── Texturing ── + uint selfTexIdx = clamp(selfMat - 1u, 0u, 5u); + float3 albedo; + + if (uBlend || vBlend) { + float4 mainTex = sampleTriplanarRGBA(input.worldPos, N, selfTexIdx, tiling); + float3 result = mainTex.rgb; + float sharpness = 16.0; + + if (uBlend) { + uint uTexIdx = clamp(uNeighborMat - 1u, 0u, 5u); + float4 uTex = sampleTriplanarRGBA(input.worldPos, N, uTexIdx, tiling); + float bias = 0.5 - uWeight; + float mainScore = mainTex.a + bias; + float neighScore = uTex.a - bias; + float blend = saturate((neighScore - mainScore) * sharpness + 0.5); + result = lerp(result, uTex.rgb, blend); + } + + if (vBlend) { + uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 5u); + float4 vTex = sampleTriplanarRGBA(input.worldPos, N, vTexIdx, tiling); + float bias = 0.5 - vWeight; + float mainScore = mainTex.a + bias; + float neighScore = vTex.a - bias; + float blend = saturate((neighScore - mainScore) * sharpness + 0.5); + result = lerp(result, vTex.rgb, blend); + } + + albedo = result; } else { - texColor = primaryColor; + albedo = sampleTriplanar(input.worldPos, N, selfTexIdx, tiling); } - // Lighting (same model as voxel PS) + // Lighting 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); + float3 color = albedo * (sunColor.rgb * NdotL + ambient); - return float4(lit, 1.0); + // Distance fog + float dist = length(input.worldPos - cameraPosition.xyz); + float fog = 1.0 - exp(-dist * 0.003); + float3 fogColor = float3(0.55, 0.70, 0.90); + color = lerp(color, fogColor, saturate(fog)); + + return float4(color, 1.0); } diff --git a/shaders/voxelSmoothVS.hlsl b/shaders/voxelSmoothVS.hlsl index c0a4859..1f8dc43 100644 --- a/shaders/voxelSmoothVS.hlsl +++ b/shaders/voxelSmoothVS.hlsl @@ -1,6 +1,6 @@ // 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. +// Passes primaryMat + chunkIndex for per-pixel blending in PS. #include "voxelCommon.hlsli" @@ -17,7 +17,8 @@ struct VSOutput { float4 position : SV_POSITION; float3 worldPos : WORLDPOS; float3 normal : NORMAL; - nointerpolation uint matPacked : MATERIALID; + nointerpolation uint primaryMat : PRIMARYMAT; + nointerpolation uint chunkIndex : CHUNKINDEX; }; [RootSignature(VOXEL_ROOTSIG)] @@ -25,9 +26,10 @@ 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 + output.position = mul(viewProjection, float4(vtx.position, 1.0)); + output.worldPos = vtx.position; + output.normal = vtx.normal; + output.primaryMat = vtx.matPacked & 0xFF; + output.chunkIndex = vtx.chunkIndex & 0xFFFF; return output; } diff --git a/src/app/main.cpp b/src/app/main.cpp index e217545..ffb3fc7 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -165,6 +165,10 @@ int APIENTRY wWinMain( if (wi::arguments::HasArgument("debug")) { renderPath.debugMode = true; } + // Check for "debugsmooth" argument to enable smooth blending debug scene + if (wi::arguments::HasArgument("debugsmooth")) { + renderPath.debugSmooth = true; + } // Activate our custom voxel render path application.ActivatePath(&renderPath); diff --git a/src/voxel/VoxelMesher.cpp b/src/voxel/VoxelMesher.cpp index 7e4dcb3..b5ec146 100644 --- a/src/voxel/VoxelMesher.cpp +++ b/src/voxel/VoxelMesher.cpp @@ -489,17 +489,34 @@ uint32_t SmoothMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) { bestMat = (uint8_t)m; bestCount = primaryCounts[m]; } } - // Secondary material: search ALL materials (including blocky) for blending + // Secondary material: only count SURFACE-EXPOSED voxels (at least one + // empty neighbor). This prevents underground materials (dirt under stone) + // from bleeding through — same principle as blocky face blending. + static const int dirs6[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}; + uint8_t surfaceMatCounts[256] = {}; + for (int c = 0; c < 8; c++) { + if (corner[c] >= 0.0f) continue; + int cx = x + cornerOff[c][0], cy = y + cornerOff[c][1], cz = z + cornerOff[c][2]; + VoxelData v = readVoxel(chunk, world, cx, cy, cz); + if (v.isEmpty()) continue; + // Check if this voxel is on the surface + bool onSurface = false; + for (int d = 0; d < 6 && !onSurface; d++) { + if (sdf[gridIdx(cx + dirs6[d][0], cy + dirs6[d][1], cz + dirs6[d][2])] > 0.0f) + onSurface = true; + } + if (onSurface) surfaceMatCounts[v.getMaterialID()]++; + } 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]; + if (surfaceMatCounts[m] > secCount) { + secMat = (uint8_t)m; secCount = surfaceMatCounts[m]; } } - uint8_t blendW = 0; - if (secCount > 0 && bestCount + secCount > 0) - blendW = (uint8_t)(255u * secCount / (bestCount + secCount)); + // blendWeight: binary flag — 255 at material boundary, 0 at interior. + // GPU interpolation creates the smooth edge-to-interior falloff. + uint8_t blendW = (secCount > 0 && secMat != bestMat) ? 255 : 0; // Normal from SDF gradient (used later for face normal orientation check) float gnx, gny, gnz; diff --git a/src/voxel/VoxelRenderer.cpp b/src/voxel/VoxelRenderer.cpp index f4f767f..1f95047 100644 --- a/src/voxel/VoxelRenderer.cpp +++ b/src/voxel/VoxelRenderer.cpp @@ -1344,16 +1344,22 @@ void VoxelRenderer::renderTopings( void VoxelRenderer::uploadSmoothData(VoxelWorld& world) { if (!device_ || !smoothPso_.IsValid()) return; - // Collect all smooth vertices from all chunks + // Collect all smooth vertices from all chunks, stamping each with its chunkIndex. + // The chunkIndex must match the order in chunkInfoBuffer_ (assigned by forEachChunk). std::vector allVerts; - allVerts.reserve(64 * 1024); // rough estimate + allVerts.reserve(64 * 1024); + uint32_t chunkIdx = 0; world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) { - if (!chunk.hasSmooth || chunk.smoothVertexCount == 0) return; - - allVerts.insert(allVerts.end(), - chunk.smoothVertices.begin(), - chunk.smoothVertices.end()); + if (chunk.hasSmooth && chunk.smoothVertexCount > 0) { + for (auto& sv : chunk.smoothVertices) { + sv.chunkIndex = (uint16_t)chunkIdx; + } + allVerts.insert(allVerts.end(), + chunk.smoothVertices.begin(), + chunk.smoothVertices.end()); + } + chunkIdx++; }); smoothVertexCount_ = (uint32_t)std::min(allVerts.size(), (size_t)MAX_SMOOTH_VERTICES); @@ -1425,7 +1431,9 @@ void VoxelRenderer::renderSmooth( dev->BindPipelineState(&smoothPso_, cmd); dev->BindConstantBuffer(&constantBuffer_, 0, cmd); dev->BindResource(&textureArray_, 1, cmd); - dev->BindResource(&smoothVertexBuffer_, 6, cmd); // t6 + dev->BindResource(&chunkInfoBuffer_, 2, cmd); // t2: chunk info for PS voxel lookups + dev->BindResource(&voxelDataBuffer_, 3, cmd); // t3: voxel data for PS neighbor blending + dev->BindResource(&smoothVertexBuffer_, 6, cmd); // t6: smooth vertices dev->BindSampler(&sampler_, 0, cmd); // Push constants (unused by smooth VS, but must be valid 48 bytes) @@ -1452,7 +1460,12 @@ void VoxelRenderPath::Start() { renderer.debugFaceColors_ = debugMode; // Generate world - if (debugMode) { + if (debugSmooth) { + world.generateDebugSmooth(); + cameraPos = { 15.0f, 12.0f, -5.0f }; + cameraPitch = -0.5f; + cameraYaw = 0.8f; + } else if (debugMode) { world.generateDebug(); cameraPos = { 10.0f, 10.0f, 0.0f }; cameraPitch = -0.4f; diff --git a/src/voxel/VoxelRenderer.h b/src/voxel/VoxelRenderer.h index 5795330..c12e833 100644 --- a/src/voxel/VoxelRenderer.h +++ b/src/voxel/VoxelRenderer.h @@ -236,6 +236,7 @@ public: TopingSystem topingSystem; bool debugMode = false; + bool debugSmooth = false; float cameraSpeed = 50.0f; float cameraSensitivity = 0.003f; diff --git a/src/voxel/VoxelWorld.cpp b/src/voxel/VoxelWorld.cpp index 0b0493a..fe45d9a 100644 --- a/src/voxel/VoxelWorld.cpp +++ b/src/voxel/VoxelWorld.cpp @@ -140,17 +140,19 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) { uint8_t surfaceMat; bool surfaceSmooth = false; - if (matVal < -0.25f) { + if (matVal < -0.30f) { surfaceMat = 4; // Sand - } else if (matVal < -0.10f) { + } else if (matVal < -0.15f) { + surfaceMat = 2; // Dirt (adjacent to sand for sand↔dirt testing) + } else if (matVal < -0.05f) { surfaceMat = 3; // Stone (blocky, with topings) - } else if (matVal < 0.0f) { + } else if (matVal < 0.05f) { surfaceMat = 6; // SmoothStone (smooth surface) surfaceSmooth = true; - } else if (matVal < 0.15f) { - surfaceMat = 2; // Dirt (blocky) - } else if (matVal < 0.30f) { + } else if (matVal < 0.20f) { surfaceMat = 1; // Grass + } else if (matVal < 0.30f) { + surfaceMat = 4; // Sand (adjacent to grass for sand↔grass testing) } else if (matNoise3 > 0.1f) { surfaceMat = 5; // Snow (smooth) surfaceSmooth = true; @@ -346,4 +348,90 @@ void VoxelWorld::generateDebug() { chunks_[cp] = std::move(chunk); } +void VoxelWorld::generateDebugSmooth() { + chunks_.clear(); + + // Create two chunks at Y=0 to have enough space + // Chunk (0,0,0) and (1,0,0) for 64 blocks along X + auto makeChunk = [&](int cx, int cy, int cz) -> Chunk& { + ChunkPos cp = {cx, cy, cz}; + auto chunk = std::make_unique(); + chunk->pos = cp; + std::memset(chunk->voxels, 0, sizeof(chunk->voxels)); + chunks_[cp] = std::move(chunk); + return *chunks_[cp]; + }; + + Chunk& c00 = makeChunk(0, 0, 0); + + // Helper: place a filled platform of a given material + auto fillBlock = [](Chunk& c, int x0, int y0, int z0, int x1, int y1, int z1, + uint8_t mat, uint8_t flags = 0) { + for (int z = z0; z <= z1; z++) + for (int y = y0; y <= y1; y++) + for (int x = x0; x <= x1; x++) + if (c.isInBounds(x, y, z)) + c.at(x, y, z) = VoxelData(mat, flags); + }; + + // ── Config 1 (X=2..6): SmoothStone (6) next to Grass (1) ── + // SmoothStone 3x3x3 block + fillBlock(c00, 2, 2, 2, 4, 4, 4, 6, VoxelData::FLAG_SMOOTH); + // Grass 3x3x3 block touching on +X side + fillBlock(c00, 5, 2, 2, 7, 4, 4, 1); + + // ── Config 2 (X=2..6, Z=8): SmoothStone (6) next to Dirt (2) ── + fillBlock(c00, 2, 2, 8, 4, 4, 10, 6, VoxelData::FLAG_SMOOTH); + fillBlock(c00, 5, 2, 8, 7, 4, 10, 2); + + // ── Config 3 (X=2..6, Z=14): SmoothStone (6) next to Sand (4) ── + fillBlock(c00, 2, 2, 14, 4, 4, 16, 6, VoxelData::FLAG_SMOOTH); + fillBlock(c00, 5, 2, 14, 7, 4, 16, 4); + + // ── Config 4 (X=2..6, Z=20): SmoothStone (6) next to Stone (3, blocky) ── + fillBlock(c00, 2, 2, 20, 4, 4, 22, 6, VoxelData::FLAG_SMOOTH); + fillBlock(c00, 5, 2, 20, 7, 4, 22, 3); + + // ── Config 5 (X=2..6, Z=26): Snow (5, smooth) next to Grass (1) ── + fillBlock(c00, 2, 2, 26, 4, 4, 28, 5, VoxelData::FLAG_SMOOTH); + fillBlock(c00, 5, 2, 26, 7, 4, 28, 1); + + // ── Config 6 (X=12..16): Sand (4) next to Dirt (2) — blocky reference ── + fillBlock(c00, 12, 2, 2, 14, 4, 4, 4); + fillBlock(c00, 15, 2, 2, 17, 4, 4, 2); + + // ── Config 7 (X=12..16, Z=8): Grass (1) next to Dirt (2) — blocky reference ── + fillBlock(c00, 12, 2, 8, 14, 4, 10, 1); + fillBlock(c00, 15, 2, 8, 17, 4, 10, 2); + + // ── Config 8 (X=12..16, Z=14): SmoothStone staircase ── + // Step 1 (low) + fillBlock(c00, 12, 2, 14, 14, 2, 16, 6, VoxelData::FLAG_SMOOTH); + // Step 2 (mid) + fillBlock(c00, 15, 2, 14, 17, 3, 16, 6, VoxelData::FLAG_SMOOTH); + // Step 3 (high) + fillBlock(c00, 18, 2, 14, 20, 4, 16, 6, VoxelData::FLAG_SMOOTH); + + // ── Config 9 (X=12..20, Z=20): Large smooth terrain patch ── + // SmoothStone ground with grass neighbors on all sides + fillBlock(c00, 14, 2, 20, 18, 3, 24, 6, VoxelData::FLAG_SMOOTH); + // Grass borders + fillBlock(c00, 12, 2, 20, 13, 3, 24, 1); + fillBlock(c00, 19, 2, 20, 20, 3, 24, 1); + fillBlock(c00, 14, 2, 18, 18, 3, 19, 1); + fillBlock(c00, 14, 2, 25, 18, 3, 26, 1); + + // ── Config 10 (X=24..28, Z=2): Snow smooth next to Sand ── + fillBlock(c00, 24, 2, 2, 26, 4, 4, 5, VoxelData::FLAG_SMOOTH); + fillBlock(c00, 27, 2, 2, 29, 4, 4, 4); + + // ── Config 11 (X=24..28, Z=8): Isolated smooth block (no blending) ── + fillBlock(c00, 25, 2, 9, 27, 4, 11, 6, VoxelData::FLAG_SMOOTH); + + // Mark all chunks dirty + for (auto& [pos, chunk] : chunks_) { + chunk->dirty = true; + } +} + } // namespace voxel diff --git a/src/voxel/VoxelWorld.h b/src/voxel/VoxelWorld.h index 24955fa..d0dc0b6 100644 --- a/src/voxel/VoxelWorld.h +++ b/src/voxel/VoxelWorld.h @@ -56,6 +56,9 @@ public: // Generate debug world: isolated blocks for face visibility testing void generateDebug(); + // Generate debug world for smooth blending: isolated material transitions + void generateDebugSmooth(); + // Get a chunk (nullptr if not loaded) Chunk* getChunk(const ChunkPos& pos); const Chunk* getChunk(const ChunkPos& pos) const;