From f166394b60958a277ecb80356998e492b314462b Mon Sep 17 00:00:00 2001 From: Samuel Bouchet Date: Thu, 26 Mar 2026 12:47:10 +0100 Subject: [PATCH] Phase 3: per-material bleed flags + patch-based terrain for blend testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- README.md | 4 ++-- shaders/voxelCommon.hlsli | 4 ++-- shaders/voxelPS.hlsl | 13 ++++++++++--- src/voxel/VoxelRenderer.cpp | 16 ++++++++++------ src/voxel/VoxelRenderer.h | 4 ++-- src/voxel/VoxelWorld.cpp | 38 ++++++++++++++++++++++++++----------- 6 files changed, 53 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 430bfcf..0b54b25 100644 --- a/README.md +++ b/README.md @@ -107,8 +107,8 @@ GPU: frustum cull compute → indirect args → DrawInstancedIndirectCount (1 ap ## Phases de développement - [x] **Phase 1** — Setup, meshing CPU, rendu basique -- [ ] **Phase 2** — GPU-driven pipeline, mega-buffer, culling, compute shaders -- [ ] **Phase 3** — Texture blending (triplanar, height-based) +- [x] **Phase 2** — GPU-driven pipeline, mega-buffer, culling, compute shaders +- [x] **Phase 3** — Texture blending (triplanar, height-based) - [ ] **Phase 4** — Toping (rebords, bordures procédurales) - [ ] **Phase 5** — Rendu smooth (Surface Nets / Marching Cubes) - [ ] **Phase 6** — Ray tracing hybride (RT shadows + AO) diff --git a/shaders/voxelCommon.hlsli b/shaders/voxelCommon.hlsli index 311794c..0f26816 100644 --- a/shaders/voxelCommon.hlsli +++ b/shaders/voxelCommon.hlsli @@ -48,8 +48,8 @@ cbuffer VoxelCB : register(b0) { // Frustum culling data (used by cull compute shader) float4 frustumPlanes[6]; // ax+by+cz+d=0, xyz=normal, w=distance uint chunkCount; - uint _cullPad0; - uint _cullPad1; + uint bleedMask; // bit N set = material N can bleed onto neighbors + uint resistBleedMask; // bit N set = material N resists bleed from neighbors uint _cullPad2; }; diff --git a/shaders/voxelPS.hlsl b/shaders/voxelPS.hlsl index 82a143c..da3bb8e 100644 --- a/shaders/voxelPS.hlsl +++ b/shaders/voxelPS.hlsl @@ -203,9 +203,16 @@ float4 main(PSInput input) : SV_TARGET0 float uWeight = saturate((uAdj - blendStart) / (1.0 - blendStart)) * 0.5; float vWeight = saturate((vAdj - blendStart) / (1.0 - blendStart)) * 0.5; - // Only blend if neighbor has a different material - bool uBlend = (uNeighborMat > 0u && uNeighborMat != input.materialID && uWeight > 0.001); - bool vBlend = (vNeighborMat > 0u && vNeighborMat != input.materialID && vWeight > 0.001); + // Only blend if neighbor has a different material AND blend flags allow it: + // - Current material must NOT resist bleed (resistBleedMask) + // - 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 ── if (debugBlend > 0.5) { diff --git a/src/voxel/VoxelRenderer.cpp b/src/voxel/VoxelRenderer.cpp index 02fad78..96d6fdd 100644 --- a/src/voxel/VoxelRenderer.cpp +++ b/src/voxel/VoxelRenderer.cpp @@ -230,8 +230,8 @@ void VoxelRenderer::generateTextures() { MatColor colors[NUM_MATERIALS] = { { 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 - { 110, 110, 105, 140, 140, 130, 303, 2.5f, 1.2f }, // Stone: rough, high peaks - { 200, 190, 140, 230, 220, 170, 404, 3.0f, 0.4f }, // Sand: fine, uniform + { 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) + { 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 }; @@ -690,8 +690,12 @@ void VoxelRenderer::render( cb.blendEnabled = 1.0f; // Phase 3: PS-based blending enabled in GPU mesh path cb.debugBlend = debugBlend_ ? 1.0f : 0.0f; cb.chunkCount = chunkCount_; - cb._cullPad0 = 0; - cb._cullPad1 = 0; + // Per-material blend flags (bit N = material N): + // 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; dev->UpdateBuffer(&constantBuffer_, &cb, cmd, sizeof(cb)); @@ -777,8 +781,8 @@ void VoxelRenderer::render( cb.textureTiling = 0.25f; cb.blendEnabled = 0.0f; // Phase 3: blending disabled in CPU/MDI paths (no voxel data SRV) cb.debugBlend = 0.0f; - cb._cullPad0 = 0; - cb._cullPad1 = 0; + cb.bleedMask = 0; + cb.resistBleedMask = 0; cb._cullPad2 = 0; cb.chunkCount = chunkCount_; extractFrustumPlanes(vpMatrix, cb.frustumPlanes); diff --git a/src/voxel/VoxelRenderer.h b/src/voxel/VoxelRenderer.h index 82be5fb..a3368be 100644 --- a/src/voxel/VoxelRenderer.h +++ b/src/voxel/VoxelRenderer.h @@ -125,8 +125,8 @@ private: float debugBlend; XMFLOAT4 frustumPlanes[6]; // ax+by+cz+d=0 uint32_t chunkCount; - uint32_t _cullPad0; - uint32_t _cullPad1; + uint32_t bleedMask; // bit N set = material N can bleed onto neighbors + uint32_t resistBleedMask; // bit N set = material N resists bleed from neighbors uint32_t _cullPad2; }; wi::graphics::GPUBuffer constantBuffer_; diff --git a/src/voxel/VoxelWorld.cpp b/src/voxel/VoxelWorld.cpp index 0852d17..6b40a38 100644 --- a/src/voxel/VoxelWorld.cpp +++ b/src/voxel/VoxelWorld.cpp @@ -110,7 +110,7 @@ float VoxelWorld::fbm(float x, float y, float z, int octaves) const { void VoxelWorld::generateChunk(Chunk& chunk, float timeOffset) { 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 caveScale = 0.05f; 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 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++) { float wy = (float)(chunk.pos.y * CHUNK_SIZE + y); 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) { v = VoxelData(); // Cave } else if (wy > height - 1.0f) { - if (wy > 90.0f) v = VoxelData(5); - else if (wy > 70.0f) v = VoxelData(3); - else if (wy < 25.0f) v = VoxelData(4); - else v = VoxelData(1); + v = VoxelData(surfaceMat); } else if (wy > height - 4.0f) { - v = VoxelData(2); + v = VoxelData(2); // Dirt sub-surface } else { - v = VoxelData(3); + v = VoxelData(3); // Stone deep underground } } else { // Animation path: simplified material assignment (no caves) if (wy > height - 1.0f) { - if (wy > 90.0f) v = VoxelData(5); - else if (wy > 70.0f) v = VoxelData(3); - else if (wy < 25.0f) v = VoxelData(4); - else v = VoxelData(1); + v = VoxelData(surfaceMat); } else if (wy > height - 4.0f) { v = VoxelData(2); } else {