Phase 3: per-material bleed flags + patch-based terrain for blend testing

- Add bleedMask/resistBleedMask bitmasks to CB for per-material blend control
  - Grass: canBleed + resistsBleed (bleeds onto others, nothing bleeds onto it)
  - Stone: no bleed (doesn't overflow, but accepts bleed from others)
  - Other materials: normal bidirectional blending
- PS checks flags before blending: mainResists → skip, !neighCanBleed → skip
- Flatten terrain (heightScale 64→20) for better surface visibility
- Replace altitude-based material bands with noise-based 2D patches
  (3 noise channels create organic patches of all 5 materials on surface)
- Make stone/sand more visually distinct (stone=blue-gray, sand=warm yellow)
- Lower stone heightContrast (1.2→0.5) so neighbors bleed onto it more
This commit is contained in:
Samuel Bouchet 2026-03-26 12:47:10 +01:00
parent d7e69f97ca
commit f166394b60
6 changed files with 53 additions and 26 deletions

View file

@ -107,8 +107,8 @@ GPU: frustum cull compute → indirect args → DrawInstancedIndirectCount (1 ap
## Phases de développement ## Phases de développement
- [x] **Phase 1** — Setup, meshing CPU, rendu basique - [x] **Phase 1** — Setup, meshing CPU, rendu basique
- [ ] **Phase 2** — GPU-driven pipeline, mega-buffer, culling, compute shaders - [x] **Phase 2** — GPU-driven pipeline, mega-buffer, culling, compute shaders
- [ ] **Phase 3** — Texture blending (triplanar, height-based) - [x] **Phase 3** — Texture blending (triplanar, height-based)
- [ ] **Phase 4** — Toping (rebords, bordures procédurales) - [ ] **Phase 4** — Toping (rebords, bordures procédurales)
- [ ] **Phase 5** — Rendu smooth (Surface Nets / Marching Cubes) - [ ] **Phase 5** — Rendu smooth (Surface Nets / Marching Cubes)
- [ ] **Phase 6** — Ray tracing hybride (RT shadows + AO) - [ ] **Phase 6** — Ray tracing hybride (RT shadows + AO)

View file

@ -48,8 +48,8 @@ cbuffer VoxelCB : register(b0) {
// Frustum culling data (used by cull compute shader) // Frustum culling data (used by cull compute shader)
float4 frustumPlanes[6]; // ax+by+cz+d=0, xyz=normal, w=distance float4 frustumPlanes[6]; // ax+by+cz+d=0, xyz=normal, w=distance
uint chunkCount; uint chunkCount;
uint _cullPad0; uint bleedMask; // bit N set = material N can bleed onto neighbors
uint _cullPad1; uint resistBleedMask; // bit N set = material N resists bleed from neighbors
uint _cullPad2; uint _cullPad2;
}; };

View file

@ -203,9 +203,16 @@ float4 main(PSInput input) : SV_TARGET0
float uWeight = saturate((uAdj - blendStart) / (1.0 - blendStart)) * 0.5; float uWeight = saturate((uAdj - blendStart) / (1.0 - blendStart)) * 0.5;
float vWeight = saturate((vAdj - blendStart) / (1.0 - blendStart)) * 0.5; float vWeight = saturate((vAdj - blendStart) / (1.0 - blendStart)) * 0.5;
// Only blend if neighbor has a different material // Only blend if neighbor has a different material AND blend flags allow it:
bool uBlend = (uNeighborMat > 0u && uNeighborMat != input.materialID && uWeight > 0.001); // - Current material must NOT resist bleed (resistBleedMask)
bool vBlend = (vNeighborMat > 0u && vNeighborMat != input.materialID && vWeight > 0.001); // - Neighbor material must be allowed to bleed (bleedMask)
bool mainResists = (resistBleedMask >> input.materialID) & 1u;
bool uNeighCanBleed = (bleedMask >> uNeighborMat) & 1u;
bool vNeighCanBleed = (bleedMask >> vNeighborMat) & 1u;
bool uBlend = (uNeighborMat > 0u && uNeighborMat != input.materialID && uWeight > 0.001
&& !mainResists && uNeighCanBleed);
bool vBlend = (vNeighborMat > 0u && vNeighborMat != input.materialID && vWeight > 0.001
&& !mainResists && vNeighCanBleed);
// ── DEBUG BLEND MODE (F4): show blend zones as colors ── // ── DEBUG BLEND MODE (F4): show blend zones as colors ──
if (debugBlend > 0.5) { if (debugBlend > 0.5) {

View file

@ -230,8 +230,8 @@ void VoxelRenderer::generateTextures() {
MatColor colors[NUM_MATERIALS] = { MatColor colors[NUM_MATERIALS] = {
{ 60, 140, 40, 80, 180, 60, 101, 1.5f, 0.8f }, // Grass: medium bumps { 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 { 100, 70, 40, 140, 100, 60, 202, 0.8f, 0.6f }, // Dirt: smooth mounds
{ 110, 110, 105, 140, 140, 130, 303, 2.5f, 1.2f }, // Stone: rough, high peaks { 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)
{ 200, 190, 140, 230, 220, 170, 404, 3.0f, 0.4f }, // Sand: fine, uniform { 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 { 220, 225, 230, 245, 248, 252, 505, 1.0f, 0.5f }, // Snow: smooth, soft
}; };
@ -690,8 +690,12 @@ void VoxelRenderer::render(
cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path
cb.debugBlend = debugBlend_ ? 1.0f : 0.0f; cb.debugBlend = debugBlend_ ? 1.0f : 0.0f;
cb.chunkCount = chunkCount_; cb.chunkCount = chunkCount_;
cb._cullPad0 = 0; // Per-material blend flags (bit N = material N):
cb._cullPad1 = 0; // 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)
cb.resistBleedMask = (1u << 1); // Grass resists bleed (she bleeds onto others, not the reverse)
cb._cullPad2 = 0; cb._cullPad2 = 0;
dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb)); dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb));
@ -777,8 +781,8 @@ void VoxelRenderer::render(
cb.textureTiling = 0.25f; cb.textureTiling = 0.25f;
cb.blendEnabled = 0.0f; // Phase 3: blending disabled in CPU/MDI paths (no voxel data SRV) cb.blendEnabled = 0.0f; // Phase 3: blending disabled in CPU/MDI paths (no voxel data SRV)
cb.debugBlend = 0.0f; cb.debugBlend = 0.0f;
cb._cullPad0 = 0; cb.bleedMask = 0;
cb._cullPad1 = 0; cb.resistBleedMask = 0;
cb._cullPad2 = 0; cb._cullPad2 = 0;
cb.chunkCount = chunkCount_; cb.chunkCount = chunkCount_;
extractFrustumPlanes(vpMatrix, cb.frustumPlanes); extractFrustumPlanes(vpMatrix, cb.frustumPlanes);

View file

@ -125,8 +125,8 @@ private:
float debugBlend; float debugBlend;
XMFLOAT4 frustumPlanes[6]; // ax+by+cz+d=0 XMFLOAT4 frustumPlanes[6]; // ax+by+cz+d=0
uint32_t chunkCount; uint32_t chunkCount;
uint32_t _cullPad0; uint32_t bleedMask; // bit N set = material N can bleed onto neighbors
uint32_t _cullPad1; uint32_t resistBleedMask; // bit N set = material N resists bleed from neighbors
uint32_t _cullPad2; uint32_t _cullPad2;
}; };
wi::graphics::GPUBuffer constantBuffer_; wi::graphics::GPUBuffer constantBuffer_;

View file

@ -110,7 +110,7 @@ float VoxelWorld::fbm(float x, float y, float z, int octaves) const {
void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) { void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
const float scale = 0.02f; // terrain horizontal scale const float scale = 0.02f; // terrain horizontal scale
const float heightScale = 64.0f; const float heightScale = 20.0f; // flatter terrain (was 64)
const float baseHeight = 40.0f; const float baseHeight = 40.0f;
const float caveScale = 0.05f; const float caveScale = 0.05f;
const float caveThreshold = 0.3f; const float caveThreshold = 0.3f;
@ -129,6 +129,28 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
// to create a rolling wave effect across the terrain // to create a rolling wave effect across the terrain
float height = baseHeight + heightScale * fbm(wx * scale, timeOffset, wz * scale, heightOctaves); float height = baseHeight + heightScale * fbm(wx * scale, timeOffset, wz * scale, heightOctaves);
// ── Surface material via noise-based patches ──
// Use 2D noise at different frequencies/seeds to create organic patches
// of each material on the surface, instead of altitude bands.
float matNoise1 = fbm(wx * 0.03f + 500.0f, 0.0f, wz * 0.03f + 500.0f, 3); // large patches
float matNoise2 = fbm(wx * 0.08f + 1000.0f, 0.0f, wz * 0.08f + 1000.0f, 2); // medium detail
float matNoise3 = fbm(wx * 0.05f + 2000.0f, 0.0f, wz * 0.05f + 2000.0f, 3); // third channel
// Combined noise for material selection (range roughly -1..1)
float matVal = matNoise1 * 0.6f + matNoise2 * 0.4f;
uint8_t surfaceMat;
if (matVal < -0.25f) {
surfaceMat = 4; // Sand
} else if (matVal < 0.0f) {
surfaceMat = 3; // Stone
} else if (matVal < 0.30f) {
surfaceMat = 1; // Grass
} else if (matNoise3 > 0.1f) {
surfaceMat = 5; // Snow (patches via independent noise)
} else {
surfaceMat = 2; // Dirt
}
for (int y = 0; y < CHUNK_SIZE; y++) { for (int y = 0; y < CHUNK_SIZE; y++) {
float wy = (float)(chunk.pos.y * CHUNK_SIZE + y); float wy = (float)(chunk.pos.y * CHUNK_SIZE + y);
VoxelData v; VoxelData v;
@ -142,22 +164,16 @@ void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) {
if (std::abs(cave) < caveThreshold && wy > 10.0f && wy < height - 3.0f) { if (std::abs(cave) < caveThreshold && wy > 10.0f && wy < height - 3.0f) {
v = VoxelData(); // Cave v = VoxelData(); // Cave
} else if (wy > height - 1.0f) { } else if (wy > height - 1.0f) {
if (wy > 90.0f) v = VoxelData(5); v = VoxelData(surfaceMat);
else if (wy > 70.0f) v = VoxelData(3);
else if (wy < 25.0f) v = VoxelData(4);
else v = VoxelData(1);
} else if (wy > height - 4.0f) { } else if (wy > height - 4.0f) {
v = VoxelData(2); v = VoxelData(2); // Dirt sub-surface
} else { } else {
v = VoxelData(3); v = VoxelData(3); // Stone deep underground
} }
} else { } else {
// Animation path: simplified material assignment (no caves) // Animation path: simplified material assignment (no caves)
if (wy > height - 1.0f) { if (wy > height - 1.0f) {
if (wy > 90.0f) v = VoxelData(5); v = VoxelData(surfaceMat);
else if (wy > 70.0f) v = VoxelData(3);
else if (wy < 25.0f) v = VoxelData(4);
else v = VoxelData(1);
} else if (wy > height - 4.0f) { } else if (wy > height - 4.0f) {
v = VoxelData(2); v = VoxelData(2);
} else { } else {