diff --git a/assets/shaders/vulkan/g_pass.frag b/assets/shaders/vulkan/g_pass.frag index cc4f0b0..59838d3 100644 --- a/assets/shaders/vulkan/g_pass.frag +++ b/assets/shaders/vulkan/g_pass.frag @@ -35,25 +35,22 @@ layout(set = 0, binding = 0) uniform GlobalUniforms { vec4 lpv_origin; } global; -// 4x4 Bayer matrix for dithered LOD transitions -float bayerDither4x4(vec2 position) { - const float bayerMatrix[16] = float[]( - 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0, - 12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0, - 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0, - 15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 - ); - int x = int(mod(position.x, 4.0)); - int y = int(mod(position.y, 4.0)); - return bayerMatrix[x + y * 4]; +// World-space hash for LOD transition masking. +// Using world-space noise avoids a fixed screen-space dot pattern. +float lodTransitionNoise(vec2 worldXZ) { + vec2 p = floor(worldXZ * 0.25); + p = fract(p * vec2(0.1031, 0.1030)); + p += dot(p, p.yx + 33.33); + return fract((p.x + p.y) * p.x); } void main() { - const float LOD_TRANSITION_WIDTH = 24.0; + const float LOD_TRANSITION_WIDTH = 16.0; if (vTileID < 0 && vMaskRadius > 0.0) { float distFromMask = length(vFragPosWorld.xz) - vMaskRadius; float fade = clamp(distFromMask / LOD_TRANSITION_WIDTH, 0.0, 1.0); - float ditherThreshold = bayerDither4x4(gl_FragCoord.xy); + vec2 worldXZ = vFragPosWorld.xz + global.cam_pos.xz; + float ditherThreshold = lodTransitionNoise(worldXZ); if (fade < ditherThreshold) discard; } diff --git a/src/world/lod_manager.zig b/src/world/lod_manager.zig index cfb7c10..b3d461a 100644 --- a/src/world/lod_manager.zig +++ b/src/world/lod_manager.zig @@ -55,6 +55,7 @@ const MeshMap = lod_gpu.MeshMap; const RegionMap = lod_gpu.RegionMap; const MAX_LOD_REGIONS = 2048; +const CHUNK_COVERAGE_PADDING: i32 = 1; comptime { if (LODLevel.count < 2) { @@ -780,14 +781,15 @@ pub const LODManager = struct { } } - /// Check if all chunks within the given world bounds are loaded and renderable + /// Check if all chunks within the given world bounds are loaded and renderable. + /// Includes a small chunk halo around bounds to avoid exposing border cut-faces. pub fn areAllChunksLoaded(self: *Self, bounds: LODChunk.WorldBounds, checker: ChunkChecker, ctx: *anyopaque) bool { _ = self; // Convert world bounds to chunk coordinates - const min_cx = @divFloor(bounds.min_x, CHUNK_SIZE_X); - const min_cz = @divFloor(bounds.min_z, CHUNK_SIZE_X); - const max_cx = @divFloor(bounds.max_x - 1, CHUNK_SIZE_X); // -1 because max is exclusive - const max_cz = @divFloor(bounds.max_z - 1, CHUNK_SIZE_X); + const min_cx = @divFloor(bounds.min_x, CHUNK_SIZE_X) - CHUNK_COVERAGE_PADDING; + const min_cz = @divFloor(bounds.min_z, CHUNK_SIZE_X) - CHUNK_COVERAGE_PADDING; + const max_cx = @divFloor(bounds.max_x - 1, CHUNK_SIZE_X) + CHUNK_COVERAGE_PADDING; // -1 because max is exclusive + const max_cz = @divFloor(bounds.max_z - 1, CHUNK_SIZE_X) + CHUNK_COVERAGE_PADDING; // Check every chunk in the region var cz = min_cz; diff --git a/src/world/lod_renderer.zig b/src/world/lod_renderer.zig index 29db8f2..6c9e5db 100644 --- a/src/world/lod_renderer.zig +++ b/src/world/lod_renderer.zig @@ -25,6 +25,8 @@ const AABB = @import("../engine/math/aabb.zig").AABB; const rhi_types = @import("../engine/graphics/rhi_types.zig"); const log = @import("../engine/core/log.zig"); +const CHUNK_COVERAGE_PADDING: i32 = 1; + /// Expected RHI interface for LODRenderer: /// - createBuffer(size: usize, usage: BufferUsage) !BufferHandle /// - destroyBuffer(handle: BufferHandle) void @@ -142,17 +144,18 @@ pub fn LODRenderer(comptime RHI: type) type { const bounds = chunk.worldBounds(); // Check if all underlying block chunks are loaded. - // If they are, we skip rendering the LOD chunk to let blocks show through. + // We require a 1-chunk halo around the LOD region to avoid exposing + // block chunk cut-faces when neighbors are still missing. if (chunk_checker) |checker| { const side: i32 = @intCast(chunk.lod_level.chunksPerSide()); const start_cx = chunk.region_x * side; const start_cz = chunk.region_z * side; var all_loaded = true; - var lcz: i32 = 0; - while (lcz < side) : (lcz += 1) { - var lcx: i32 = 0; - while (lcx < side) : (lcx += 1) { + var lcz: i32 = -CHUNK_COVERAGE_PADDING; + while (lcz < side + CHUNK_COVERAGE_PADDING) : (lcz += 1) { + var lcx: i32 = -CHUNK_COVERAGE_PADDING; + while (lcx < side + CHUNK_COVERAGE_PADDING) : (lcx += 1) { if (!checker(start_cx + lcx, start_cz + lcz, checker_ctx.?)) { all_loaded = false; break;