Phase 5.1: smooth normals, triplanar fix, depth bias, hasSmooth tighten
- Smooth vertex normals: area-weighted accumulation of face normals per indexed vertex before triangle expansion. Gives Gouraud-smooth shading without adding geometry. - Triplanar fix: PS uses geometric normal (ddx/ddy of worldPos) for texture projection weights, smooth normal for lighting only. Prevents texture stretching on smoothed surfaces. - Depth bias: custom rasterizer state (depth_bias=2, slope_scaled=1.0) on smooth PSO resolves z-fighting at smooth↔blocky overlap. - hasSmooth filter tightened: check face-adjacent voxels of each corner (1-voxel reach) instead of neighbor cells' corners (2-cell cascade). Prevents smooth mesh from extending into underground blocky territory.
This commit is contained in:
parent
c755f20325
commit
d075a8492c
4 changed files with 83 additions and 48 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<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];
|
||||
// ── 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<SmoothVertex> 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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<SmoothVertex>, SRV t6
|
||||
static constexpr uint32_t MAX_SMOOTH_VERTICES = 4 * 1024 * 1024; // 4M vertices max
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue