diff --git a/shaders/voxelSmoothPS.hlsl b/shaders/voxelSmoothPS.hlsl index 538246f..eddafcc 100644 --- a/shaders/voxelSmoothPS.hlsl +++ b/shaders/voxelSmoothPS.hlsl @@ -93,7 +93,17 @@ float4 sampleTriplanarRGBA(float3 wp, float3 n, uint texIdx, float tiling) { // ── Main PS ────────────────────────────────────────────────────── [RootSignature(VOXEL_ROOTSIG)] float4 main(PSInput input) : SV_TARGET0 { - float3 N = normalize(input.normal); + float3 N = normalize(input.normal); // smooth normal (for lighting) + + // Geometric normal from screen-space derivatives of worldPos. + // This is the true triangle face normal — use it for triplanar weights + // to avoid texture stretching caused by smooth normal interpolation. + float3 dpx = ddx(input.worldPos); + float3 dpy = ddy(input.worldPos); + float3 geoN = normalize(cross(dpx, dpy)); + // Ensure geometric normal faces same hemisphere as smooth normal + if (dot(geoN, N) < 0.0) geoN = -geoN; + float tiling = textureTiling; // ── Derive dominant face from smooth normal (same tables as blocky PS) ── @@ -168,13 +178,13 @@ float4 main(PSInput input) : SV_TARGET0 { float3 albedo; if (uBlend || vBlend) { - float4 mainTex = sampleTriplanarRGBA(input.worldPos, N, selfTexIdx, tiling); + float4 mainTex = sampleTriplanarRGBA(input.worldPos, geoN, 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); + float4 uTex = sampleTriplanarRGBA(input.worldPos, geoN, uTexIdx, tiling); float bias = 0.5 - uWeight; float mainScore = mainTex.a + bias; float neighScore = uTex.a - bias; @@ -184,7 +194,7 @@ float4 main(PSInput input) : SV_TARGET0 { if (vBlend) { uint vTexIdx = clamp(vNeighborMat - 1u, 0u, 5u); - float4 vTex = sampleTriplanarRGBA(input.worldPos, N, vTexIdx, tiling); + float4 vTex = sampleTriplanarRGBA(input.worldPos, geoN, vTexIdx, tiling); float bias = 0.5 - vWeight; float mainScore = mainTex.a + bias; float neighScore = vTex.a - bias; @@ -194,7 +204,7 @@ float4 main(PSInput input) : SV_TARGET0 { albedo = result; } else { - albedo = sampleTriplanar(input.worldPos, N, selfTexIdx, tiling); + albedo = sampleTriplanar(input.worldPos, geoN, selfTexIdx, tiling); } // Lighting diff --git a/src/voxel/VoxelMesher.cpp b/src/voxel/VoxelMesher.cpp index 8c983f1..fc88547 100644 --- a/src/voxel/VoxelMesher.cpp +++ b/src/voxel/VoxelMesher.cpp @@ -377,28 +377,24 @@ uint32_t SmoothMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) { 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++) { - // hasSmooth check: at least one corner of THIS cell or an ADJACENT - // cell must be a smooth voxel. This ensures that cells at the - // smooth↔blocky boundary (all blocky corners but neighbor cell has - // smooth) still generate vertices — otherwise the quad connecting - // them can't be emitted, leaving a gap. + // hasSmooth check: at least one corner of the cell must be a smooth + // voxel OR be face-adjacent (6-connected) to a smooth voxel. + // The 1-voxel extension ensures cells at the smooth↔blocky boundary + // generate vertices for quad connectivity (closing the gap). + // Checking direct face-adjacency (not neighbor cells' corners) prevents + // the smooth mesh from cascading into underground 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)]) + int gx = x + dx, gy = y + dy, gz = z + dz; + if (smoothGrid[gridIdx(gx, gy, gz)]) { hasSmooth = true; - } - if (!hasSmooth) { - // Check 6-connected neighbor cells for smooth corners - // (extend reach by 1 cell in each direction) - static const int nbr[6][3] = {{-1,0,0},{1,0,0},{0,-1,0},{0,1,0},{0,0,-1},{0,0,1}}; - for (int n = 0; n < 6 && !hasSmooth; n++) { - int nx = x + nbr[n][0], ny = y + nbr[n][1], nz = z + nbr[n][2]; - 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(nx + dx, ny + dy, nz + dz)]) + } else { + // Check 6 face-neighbors of this corner for smooth voxels + static const int d6[6][3] = {{1,0,0},{-1,0,0},{0,1,0},{0,-1,0},{0,0,1},{0,0,-1}}; + for (int d = 0; d < 6 && !hasSmooth; d++) { + if (smoothGrid[gridIdx(gx+d6[d][0], gy+d6[d][1], gz+d6[d][2])]) hasSmooth = true; } } @@ -647,44 +643,67 @@ uint32_t SmoothMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) { } } - // ── 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]; + // ── Step 4: Compute smooth vertex normals ────────────────────── + // Accumulate area-weighted face normals into each indexed vertex, + // then normalize. This gives Gouraud-style smooth shading across + // the Surface Nets mesh without adding geometry. + + const int vertCount = (int)chunk.smoothVertices.size(); + + // Zero out vertex normals (will accumulate face normals) + for (auto& sv : chunk.smoothVertices) { + sv.nx = 0; sv.ny = 0; sv.nz = 0; + } + + // For each triangle: compute oriented face normal, accumulate into vertices. + // The cross product magnitude is proportional to triangle area, so larger + // triangles contribute more — this is the standard area-weighted approach. + for (const auto& tri : triangles) { + const SmoothVertex& va = chunk.smoothVertices[tri.a]; + const SmoothVertex& vb = chunk.smoothVertices[tri.b]; + const 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. + // Orient using the known edge axis (same logic as before) 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; + // Accumulate (area-weighted — cross product magnitude IS the area×2) + chunk.smoothVertices[tri.a].nx += fnx; + chunk.smoothVertices[tri.a].ny += fny; + chunk.smoothVertices[tri.a].nz += fnz; + chunk.smoothVertices[tri.b].nx += fnx; + chunk.smoothVertices[tri.b].ny += fny; + chunk.smoothVertices[tri.b].nz += fnz; + chunk.smoothVertices[tri.c].nx += fnx; + chunk.smoothVertices[tri.c].ny += fny; + chunk.smoothVertices[tri.c].nz += fnz; + } - expanded.push_back(va); - expanded.push_back(vb); - expanded.push_back(vc); + // Normalize accumulated vertex normals + for (auto& sv : chunk.smoothVertices) { + float len = std::sqrt(sv.nx*sv.nx + sv.ny*sv.ny + sv.nz*sv.nz); + if (len > 0.0001f) { + sv.nx /= len; sv.ny /= len; sv.nz /= len; + } else { + sv.nx = 0; sv.ny = 1; sv.nz = 0; + } + } + + // ── Step 5: Expand indexed triangles to triangle list ───────── + std::vector expanded; + expanded.reserve(triangles.size() * 3); + for (const auto& tri : triangles) { + expanded.push_back(chunk.smoothVertices[tri.a]); + expanded.push_back(chunk.smoothVertices[tri.b]); + expanded.push_back(chunk.smoothVertices[tri.c]); } chunk.smoothVertices = std::move(expanded); diff --git a/src/voxel/VoxelRenderer.cpp b/src/voxel/VoxelRenderer.cpp index 1f95047..1a13470 100644 --- a/src/voxel/VoxelRenderer.cpp +++ b/src/voxel/VoxelRenderer.cpp @@ -201,10 +201,15 @@ void VoxelRenderer::createPipeline() { wi::renderer::LoadShader(ShaderStage::PS, smoothPS_, "voxel/voxelSmoothPS.cso"); if (smoothVS_.IsValid() && smoothPS_.IsValid()) { + // Custom rasterizer with depth bias to resolve z-fighting at smooth↔blocky boundaries + smoothRasterizer_ = *wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT); + smoothRasterizer_.depth_bias = 2; // small integer bias + smoothRasterizer_.slope_scaled_depth_bias = 1.0f; // scale with surface slope + PipelineStateDesc smoothPsoDesc; smoothPsoDesc.vs = &smoothVS_; smoothPsoDesc.ps = &smoothPS_; - smoothPsoDesc.rs = wi::renderer::GetRasterizerState(wi::enums::RSTYPE_FRONT); + smoothPsoDesc.rs = &smoothRasterizer_; smoothPsoDesc.dss = wi::renderer::GetDepthStencilState(wi::enums::DSSTYPE_DEFAULT); smoothPsoDesc.bs = wi::renderer::GetBlendState(wi::enums::BSTYPE_OPAQUE); smoothPsoDesc.pt = PrimitiveTopology::TRIANGLELIST; diff --git a/src/voxel/VoxelRenderer.h b/src/voxel/VoxelRenderer.h index c12e833..5ff9239 100644 --- a/src/voxel/VoxelRenderer.h +++ b/src/voxel/VoxelRenderer.h @@ -86,6 +86,7 @@ private: // Shaders & Pipeline (smooth surfaces, Phase 5) wi::graphics::Shader smoothVS_; wi::graphics::Shader smoothPS_; + wi::graphics::RasterizerState smoothRasterizer_; wi::graphics::PipelineState smoothPso_; wi::graphics::GPUBuffer smoothVertexBuffer_; // StructuredBuffer, SRV t6 static constexpr uint32_t MAX_SMOOTH_VERTICES = 4 * 1024 * 1024; // 4M vertices max