Phase 5.1: smooth PS blending uses same logic as blocky PS + debug scene
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
This commit is contained in:
parent
aab38bb9b9
commit
b45d5a1884
9 changed files with 350 additions and 58 deletions
16
CLAUDE.md
16
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)
|
- **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)
|
- **Centroid SDF sampling** : NE PAS utiliser non plus (les deux côtés arrondissent souvent au même voxel)
|
||||||
|
|
||||||
**Material blending** :
|
**Material blending (per-pixel, same as blocky PS)** :
|
||||||
- **Deux matériaux par vertex** : primaryMat (smooth-only counts, évite subsurface bleed) + secondaryMat (all counts, inclut blocky pour le blending aux frontières)
|
- **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
|
||||||
- **blendWeight** : uint8 0-255, ratio du secondaire dans le vote des 8 corners
|
- **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)
|
||||||
- **PS** : `lerp(primaryColor, secondaryColor, blendWeight)` entre deux samplings triplanar
|
- **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]
|
#### Phase 5.2 - Optimisations et polish [A FAIRE]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,213 @@
|
||||||
// BVLE Voxels - Smooth Surface Nets Pixel Shader (Phase 5.1)
|
// 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"
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
Texture2DArray<float4> materialTextures : register(t1);
|
Texture2DArray<float4> materialTextures : register(t1);
|
||||||
|
StructuredBuffer<GPUChunkInfo> chunkInfoBuffer : register(t2);
|
||||||
|
StructuredBuffer<uint> voxelData : register(t3);
|
||||||
SamplerState texSampler : register(s0);
|
SamplerState texSampler : register(s0);
|
||||||
|
|
||||||
struct PSInput {
|
struct PSInput {
|
||||||
float4 position : SV_POSITION;
|
float4 position : SV_POSITION;
|
||||||
float3 worldPos : WORLDPOS;
|
float3 worldPos : WORLDPOS;
|
||||||
float3 normal : NORMAL;
|
float3 normal : NORMAL;
|
||||||
nointerpolation uint matPacked : MATERIALID;
|
nointerpolation uint primaryMat : PRIMARYMAT;
|
||||||
|
nointerpolation uint chunkIndex : CHUNKINDEX;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sample triplanar texture for a given material index
|
static const uint CSIZE = 32;
|
||||||
float3 sampleTriplanar(float3 worldPos, float3 blend, float tiling, uint matIdx) {
|
static const uint CVOL = CSIZE * CSIZE * CSIZE;
|
||||||
uint texIdx = clamp(matIdx - 1u, 0u, 5u);
|
|
||||||
float4 xS = materialTextures.Sample(texSampler, float3(worldPos.yz * tiling, (float)texIdx));
|
// ── Face direction tables (SAME as voxelPS.hlsl) ────────────────
|
||||||
float4 yS = materialTextures.Sample(texSampler, float3(worldPos.xz * tiling, (float)texIdx));
|
// Face normals: +X, -X, +Y, -Y, +Z, -Z
|
||||||
float4 zS = materialTextures.Sample(texSampler, float3(worldPos.xy * tiling, (float)texIdx));
|
static const int3 faceNormals[6] = {
|
||||||
return xS.rgb * blend.x + yS.rgb * blend.y + zS.rgb * blend.z;
|
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)]
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
float4 main(PSInput input) : SV_TARGET0 {
|
float4 main(PSInput input) : SV_TARGET0 {
|
||||||
float3 N = normalize(input.normal);
|
float3 N = normalize(input.normal);
|
||||||
float tiling = textureTiling;
|
float tiling = textureTiling;
|
||||||
|
|
||||||
// Unpack materials: materialID(8) | secondaryMat(8) | blendWeight(8) | pad(8)
|
// ── Derive dominant face from smooth normal (same tables as blocky PS) ──
|
||||||
uint primaryMat = input.matPacked & 0xFF;
|
// Find the axis with the largest absolute normal component
|
||||||
uint secondaryMat = (input.matPacked >> 8) & 0xFF;
|
float3 absN = abs(N);
|
||||||
float blendWeight = ((input.matPacked >> 16) & 0xFF) / 255.0;
|
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
|
// Map to face index: axis*2 + (negative ? 1 : 0)
|
||||||
float3 blend = abs(N);
|
uint face = dominantAxis * 2;
|
||||||
blend = blend / (blend.x + blend.y + blend.z + 0.001);
|
if (N[dominantAxis] < 0.0) face += 1;
|
||||||
|
|
||||||
// Sample primary and secondary materials
|
int3 normalDir = faceNormals[face];
|
||||||
float3 primaryColor = sampleTriplanar(input.worldPos, blend, tiling, primaryMat);
|
int3 uDir = faceUDirs[face];
|
||||||
float3 texColor;
|
int3 vDir = faceVDirs[face];
|
||||||
if (blendWeight > 0.01 && secondaryMat != primaryMat) {
|
|
||||||
float3 secondaryColor = sampleTriplanar(input.worldPos, blend, tiling, secondaryMat);
|
// ── Compute voxel coordinate (SAME as blocky PS) ──
|
||||||
texColor = lerp(primaryColor, secondaryColor, blendWeight);
|
// Tiny offset inward along dominant normal to handle integer boundaries
|
||||||
} else {
|
float3 samplePos = input.worldPos - (float3)normalDir * 0.001;
|
||||||
texColor = primaryColor;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lighting (same model as voxel PS)
|
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 {
|
||||||
|
albedo = sampleTriplanar(input.worldPos, N, selfTexIdx, tiling);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lighting
|
||||||
float3 L = normalize(-sunDirection.xyz);
|
float3 L = normalize(-sunDirection.xyz);
|
||||||
float NdotL = max(dot(N, L), 0.0);
|
float NdotL = max(dot(N, L), 0.0);
|
||||||
|
|
||||||
float3 ambient = float3(0.15, 0.18, 0.25);
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// BVLE Voxels - Smooth Surface Nets Vertex Shader (Phase 5.1)
|
// BVLE Voxels - Smooth Surface Nets Vertex Shader (Phase 5.1)
|
||||||
// Vertex pulling from StructuredBuffer<SmoothVertex>.
|
// Vertex pulling from StructuredBuffer<SmoothVertex>.
|
||||||
// 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"
|
#include "voxelCommon.hlsli"
|
||||||
|
|
||||||
|
|
@ -17,7 +17,8 @@ struct VSOutput {
|
||||||
float4 position : SV_POSITION;
|
float4 position : SV_POSITION;
|
||||||
float3 worldPos : WORLDPOS;
|
float3 worldPos : WORLDPOS;
|
||||||
float3 normal : NORMAL;
|
float3 normal : NORMAL;
|
||||||
nointerpolation uint matPacked : MATERIALID;
|
nointerpolation uint primaryMat : PRIMARYMAT;
|
||||||
|
nointerpolation uint chunkIndex : CHUNKINDEX;
|
||||||
};
|
};
|
||||||
|
|
||||||
[RootSignature(VOXEL_ROOTSIG)]
|
[RootSignature(VOXEL_ROOTSIG)]
|
||||||
|
|
@ -28,6 +29,7 @@ VSOutput main(uint vertexID : SV_VertexID) {
|
||||||
output.position = mul(viewProjection, float4(vtx.position, 1.0));
|
output.position = mul(viewProjection, float4(vtx.position, 1.0));
|
||||||
output.worldPos = vtx.position;
|
output.worldPos = vtx.position;
|
||||||
output.normal = vtx.normal;
|
output.normal = vtx.normal;
|
||||||
output.matPacked = vtx.matPacked; // pass all packed material data to PS
|
output.primaryMat = vtx.matPacked & 0xFF;
|
||||||
|
output.chunkIndex = vtx.chunkIndex & 0xFFFF;
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,10 @@ int APIENTRY wWinMain(
|
||||||
if (wi::arguments::HasArgument("debug")) {
|
if (wi::arguments::HasArgument("debug")) {
|
||||||
renderPath.debugMode = true;
|
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
|
// Activate our custom voxel render path
|
||||||
application.ActivatePath(&renderPath);
|
application.ActivatePath(&renderPath);
|
||||||
|
|
|
||||||
|
|
@ -489,17 +489,34 @@ uint32_t SmoothMesher::meshChunk(Chunk& chunk, const VoxelWorld& world) {
|
||||||
bestMat = (uint8_t)m; bestCount = primaryCounts[m];
|
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;
|
uint8_t secMat = bestMat, secCount = 0;
|
||||||
for (int m = 1; m < 256; m++) {
|
for (int m = 1; m < 256; m++) {
|
||||||
if (m == bestMat) continue;
|
if (m == bestMat) continue;
|
||||||
if (allMatCounts[m] > secCount) {
|
if (surfaceMatCounts[m] > secCount) {
|
||||||
secMat = (uint8_t)m; secCount = allMatCounts[m];
|
secMat = (uint8_t)m; secCount = surfaceMatCounts[m];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uint8_t blendW = 0;
|
// blendWeight: binary flag — 255 at material boundary, 0 at interior.
|
||||||
if (secCount > 0 && bestCount + secCount > 0)
|
// GPU interpolation creates the smooth edge-to-interior falloff.
|
||||||
blendW = (uint8_t)(255u * secCount / (bestCount + secCount));
|
uint8_t blendW = (secCount > 0 && secMat != bestMat) ? 255 : 0;
|
||||||
|
|
||||||
// Normal from SDF gradient (used later for face normal orientation check)
|
// Normal from SDF gradient (used later for face normal orientation check)
|
||||||
float gnx, gny, gnz;
|
float gnx, gny, gnz;
|
||||||
|
|
|
||||||
|
|
@ -1344,16 +1344,22 @@ void VoxelRenderer::renderTopings(
|
||||||
void VoxelRenderer::uploadSmoothData(VoxelWorld& world) {
|
void VoxelRenderer::uploadSmoothData(VoxelWorld& world) {
|
||||||
if (!device_ || !smoothPso_.IsValid()) return;
|
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<SmoothVertex> allVerts;
|
std::vector<SmoothVertex> allVerts;
|
||||||
allVerts.reserve(64 * 1024); // rough estimate
|
allVerts.reserve(64 * 1024);
|
||||||
|
|
||||||
|
uint32_t chunkIdx = 0;
|
||||||
world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
|
world.forEachChunk([&](const ChunkPos& pos, Chunk& chunk) {
|
||||||
if (!chunk.hasSmooth || chunk.smoothVertexCount == 0) return;
|
if (chunk.hasSmooth && chunk.smoothVertexCount > 0) {
|
||||||
|
for (auto& sv : chunk.smoothVertices) {
|
||||||
|
sv.chunkIndex = (uint16_t)chunkIdx;
|
||||||
|
}
|
||||||
allVerts.insert(allVerts.end(),
|
allVerts.insert(allVerts.end(),
|
||||||
chunk.smoothVertices.begin(),
|
chunk.smoothVertices.begin(),
|
||||||
chunk.smoothVertices.end());
|
chunk.smoothVertices.end());
|
||||||
|
}
|
||||||
|
chunkIdx++;
|
||||||
});
|
});
|
||||||
|
|
||||||
smoothVertexCount_ = (uint32_t)std::min(allVerts.size(), (size_t)MAX_SMOOTH_VERTICES);
|
smoothVertexCount_ = (uint32_t)std::min(allVerts.size(), (size_t)MAX_SMOOTH_VERTICES);
|
||||||
|
|
@ -1425,7 +1431,9 @@ void VoxelRenderer::renderSmooth(
|
||||||
dev->BindPipelineState(&smoothPso_, cmd);
|
dev->BindPipelineState(&smoothPso_, cmd);
|
||||||
dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
|
dev->BindConstantBuffer(&constantBuffer_, 0, cmd);
|
||||||
dev->BindResource(&textureArray_, 1, 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);
|
dev->BindSampler(&sampler_, 0, cmd);
|
||||||
|
|
||||||
// Push constants (unused by smooth VS, but must be valid 48 bytes)
|
// Push constants (unused by smooth VS, but must be valid 48 bytes)
|
||||||
|
|
@ -1452,7 +1460,12 @@ void VoxelRenderPath::Start() {
|
||||||
renderer.debugFaceColors_ = debugMode;
|
renderer.debugFaceColors_ = debugMode;
|
||||||
|
|
||||||
// Generate world
|
// 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();
|
world.generateDebug();
|
||||||
cameraPos = { 10.0f, 10.0f, 0.0f };
|
cameraPos = { 10.0f, 10.0f, 0.0f };
|
||||||
cameraPitch = -0.4f;
|
cameraPitch = -0.4f;
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,7 @@ public:
|
||||||
TopingSystem topingSystem;
|
TopingSystem topingSystem;
|
||||||
|
|
||||||
bool debugMode = false;
|
bool debugMode = false;
|
||||||
|
bool debugSmooth = false;
|
||||||
|
|
||||||
float cameraSpeed = 50.0f;
|
float cameraSpeed = 50.0f;
|
||||||
float cameraSensitivity = 0.003f;
|
float cameraSensitivity = 0.003f;
|
||||||
|
|
|
||||||
|
|
@ -140,17 +140,19 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
|
||||||
|
|
||||||
uint8_t surfaceMat;
|
uint8_t surfaceMat;
|
||||||
bool surfaceSmooth = false;
|
bool surfaceSmooth = false;
|
||||||
if (matVal < -0.25f) {
|
if (matVal < -0.30f) {
|
||||||
surfaceMat = 4; // Sand
|
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)
|
surfaceMat = 3; // Stone (blocky, with topings)
|
||||||
} else if (matVal < 0.0f) {
|
} else if (matVal < 0.05f) {
|
||||||
surfaceMat = 6; // SmoothStone (smooth surface)
|
surfaceMat = 6; // SmoothStone (smooth surface)
|
||||||
surfaceSmooth = true;
|
surfaceSmooth = true;
|
||||||
} else if (matVal < 0.15f) {
|
} else if (matVal < 0.20f) {
|
||||||
surfaceMat = 2; // Dirt (blocky)
|
|
||||||
} else if (matVal < 0.30f) {
|
|
||||||
surfaceMat = 1; // Grass
|
surfaceMat = 1; // Grass
|
||||||
|
} else if (matVal < 0.30f) {
|
||||||
|
surfaceMat = 4; // Sand (adjacent to grass for sand↔grass testing)
|
||||||
} else if (matNoise3 > 0.1f) {
|
} else if (matNoise3 > 0.1f) {
|
||||||
surfaceMat = 5; // Snow (smooth)
|
surfaceMat = 5; // Snow (smooth)
|
||||||
surfaceSmooth = true;
|
surfaceSmooth = true;
|
||||||
|
|
@ -346,4 +348,90 @@ void VoxelWorld::generateDebug() {
|
||||||
chunks_[cp] = std::move(chunk);
|
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>();
|
||||||
|
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
|
} // namespace voxel
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ public:
|
||||||
// Generate debug world: isolated blocks for face visibility testing
|
// Generate debug world: isolated blocks for face visibility testing
|
||||||
void generateDebug();
|
void generateDebug();
|
||||||
|
|
||||||
|
// Generate debug world for smooth blending: isolated material transitions
|
||||||
|
void generateDebugSmooth();
|
||||||
|
|
||||||
// Get a chunk (nullptr if not loaded)
|
// Get a chunk (nullptr if not loaded)
|
||||||
Chunk* getChunk(const ChunkPos& pos);
|
Chunk* getChunk(const ChunkPos& pos);
|
||||||
const Chunk* getChunk(const ChunkPos& pos) const;
|
const Chunk* getChunk(const ChunkPos& pos) const;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue