From 07006cd1ae4dac4fc6afd8cbb2a3b4971573c473 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sun, 3 May 2026 19:49:54 +0100 Subject: [PATCH] fix: polish LOD seam transitions --- assets/shaders/vulkan/g_pass.frag | 2 +- assets/shaders/vulkan/terrain.frag | 11 +- assets/shaders/vulkan/terrain.frag.spv | Bin 82940 -> 82848 bytes modules/world-lod/src/lod_chunk.zig | 8 +- modules/world-lod/src/lod_mesh.zig | 145 ++++++++++++++++++++++++- 5 files changed, 150 insertions(+), 16 deletions(-) diff --git a/assets/shaders/vulkan/g_pass.frag b/assets/shaders/vulkan/g_pass.frag index 24895573..1d412c29 100644 --- a/assets/shaders/vulkan/g_pass.frag +++ b/assets/shaders/vulkan/g_pass.frag @@ -46,7 +46,7 @@ float lodTransitionNoise(vec2 worldXZ) { } void main() { - const float LOD_TRANSITION_WIDTH = 24.0; + const float LOD_TRANSITION_WIDTH = 32.0; bool isLOD = vTileID < 0 || vMaskRadius > 0.0; if (vMaskRadius >= 1.0) { float distFromMask = length(vFragPosWorld.xz) - vMaskRadius; diff --git a/assets/shaders/vulkan/terrain.frag b/assets/shaders/vulkan/terrain.frag index 4f0520f3..a8319ed2 100644 --- a/assets/shaders/vulkan/terrain.frag +++ b/assets/shaders/vulkan/terrain.frag @@ -672,7 +672,7 @@ void main() { float debugSkyFill = 0.0; float debugBlockLight = clamp(max(vBlockLight.r, max(vBlockLight.g, vBlockLight.b)), 0.0, 1.0); float debugOutdoor = baselineOutdoorFactor(vSkyLight); - const float LOD_TRANSITION_WIDTH = 24.0; + const float LOD_TRANSITION_WIDTH = 32.0; const float AO_FADE_DISTANCE = 128.0; const float TEXTURE_FADE_START = 32.0; const float TEXTURE_FADE_END = 128.0; @@ -686,12 +686,11 @@ void main() { } if (vMaskRadius >= 1.0) { - const float CHUNK_SIZE = 16.0; vec2 worldXZ = vFragPosWorld.xz + global.cam_pos.xz; - vec2 fragChunk = floor(worldXZ / CHUNK_SIZE); - vec2 cameraChunk = floor(global.cam_pos.xz / CHUNK_SIZE); - float maskChunks = floor(vMaskRadius / CHUNK_SIZE + 0.5); - if (length(fragChunk - cameraChunk) <= maskChunks) discard; + float distFromMask = length(vFragPosWorld.xz) - vMaskRadius; + float fade = clamp(distFromMask / LOD_TRANSITION_WIDTH, 0.0, 1.0); + float ditherThreshold = lodTransitionNoise(worldXZ); + if (fade < ditherThreshold) discard; } vec2 tileBase = vec2(mod(float(vTileID), 16.0), floor(float(vTileID) / 16.0)) * (1.0 / 16.0); diff --git a/assets/shaders/vulkan/terrain.frag.spv b/assets/shaders/vulkan/terrain.frag.spv index d9e35bcb1cba10fc0deaa02678d40d8acf311378..ced24a4c9f75680c4edd1f74b3b491a4abd026ef 100644 GIT binary patch delta 21674 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 h3enro2{(o;1`v?F4 delta 21521 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 diff --git a/modules/world-lod/src/lod_chunk.zig b/modules/world-lod/src/lod_chunk.zig index a1650751..9dfe8f7b 100644 --- a/modules/world-lod/src/lod_chunk.zig +++ b/modules/world-lod/src/lod_chunk.zig @@ -285,7 +285,7 @@ pub const LODConfig = struct { /// Fog start position as percentage of LOD radius (0.0-1.0) where fog begins. /// Values closer to 0.0 start fog near the player; 1.0 disables fog for that level. - fog_start_percent: [LODLevel.count]f32 = .{ 0.5, 0.5, 0.4, 0.3 }, + fog_start_percent: [LODLevel.count]f32 = .{ 0.55, 0.48, 0.38, 0.28 }, qem_triangle_targets: [LODLevel.count]u32 = .{ 0, 2000, 800, 200 }, @@ -393,7 +393,7 @@ pub const LODConfig = struct { const self: *LODConfig = @ptrCast(@alignCast(ptr)); // Keep a small overlap so the chunk ring and LOD ring blend instead of // leaving a camera-centered dead zone between them. - const overlap_chunks = @max(self.radii[0] - 1, 0); + const overlap_chunks = @max(self.radii[0] - 2, 0); return @as(f32, @floatFromInt(overlap_chunks)) * @as(f32, @floatFromInt(CHUNK_SIZE_X)); } fn getQEMTargetWrapper(ptr: *anyopaque, lod: LODLevel) u32 { @@ -498,8 +498,8 @@ test "ILODConfig.calculateMaskRadius" { .radii = .{ 16, 40, 80, 160 }, }; const interface = config.interface(); - try std.testing.expectEqual(@as(f32, 240.0), interface.calculateMaskRadius()); + try std.testing.expectEqual(@as(f32, 224.0), interface.calculateMaskRadius()); config.radii[0] = 32; - try std.testing.expectEqual(@as(f32, 496.0), interface.calculateMaskRadius()); + try std.testing.expectEqual(@as(f32, 480.0), interface.calculateMaskRadius()); } diff --git a/modules/world-lod/src/lod_mesh.zig b/modules/world-lod/src/lod_mesh.zig index dfca8624..cc8e2ecd 100644 --- a/modules/world-lod/src/lod_mesh.zig +++ b/modules/world-lod/src/lod_mesh.zig @@ -32,6 +32,7 @@ const encodeNormal = rhi_types.encodeNormal; const encodeMeta = rhi_types.encodeMeta; const encodeBlocklight = rhi_types.encodeBlocklight; const QuadricSimplifier = @import("world-meshing").meshing.quadric_simplifier.QuadricSimplifier; +const engine_core = @import("engine-core"); const log = @import("engine-core").log; const lod_seam = @import("lod_seam.zig"); @@ -197,15 +198,16 @@ pub const LODMesh = struct { var vertices = std.ArrayListUnmanaged(Vertex).empty; defer vertices.deinit(self.allocator); + const diag_enabled = engine_core.envFlag("ZIGCRAFT_LOD_DIAG", false); var gz: u32 = 0; while (gz + 1 < data.width) : (gz += 1) { var gx: u32 = 0; while (gx + 1 < data.width) : (gx += 1) { - const h00 = data.heightmap[gx + gz * data.width]; - const h10 = data.heightmap[(gx + 1) + gz * data.width]; - const h01 = data.heightmap[gx + (gz + 1) * data.width]; - const h11 = data.heightmap[(gx + 1) + (gz + 1) * data.width]; + const h00 = stitchedHeight(data, gx, gz); + const h10 = stitchedHeight(data, gx + 1, gz); + const h01 = stitchedHeight(data, gx, gz + 1); + const h11 = stitchedHeight(data, gx + 1, gz + 1); const c00 = data.colors[gx + gz * data.width]; const c10 = data.colors[(gx + 1) + gz * data.width]; @@ -247,6 +249,13 @@ pub const LODMesh = struct { } } + if (diag_enabled) { + const max_adjust = maxStitchedHeightAdjustment(data); + if (max_adjust > 0.25) { + log.log.info("LOD_SEAM_DIAG lod={} origin=({}, {}) max_edge_adjust={d:.2}", .{ @intFromEnum(self.lod_level), world_x, world_z, max_adjust }); + } + } + // Store pending vertices self.mutex.lock(); defer self.mutex.unlock(); @@ -705,10 +714,69 @@ fn blockForLODCell(data: *const LODSimplifiedData, gx: u32, gz: u32) BlockType { } fn blockForLODQuad(data: *const LODSimplifiedData, gx: u32, gz: u32) BlockType { - if (averageWaterCoverage(data, gx, gz) >= 0.25) return .water; + const water_coverage = averageWaterCoverage(data, gx, gz); + if (water_coverage >= 0.35) return .water; + if (water_coverage > 0.0 and representativeWaterDepth(data, gx, gz) >= 1.5) return .water; + return representativeSurfaceBlock(data, gx, gz); +} + +fn representativeSurfaceBlock(data: *const LODSimplifiedData, gx: u32, gz: u32) BlockType { + const x0 = @min(gx, data.width - 1); + const z0 = @min(gz, data.width - 1); + const x1 = @min(gx + 1, data.width - 1); + const z1 = @min(gz + 1, data.width - 1); + const indices = [_]u32{ + x0 + z0 * data.width, + x1 + z0 * data.width, + x0 + z1 * data.width, + x1 + z1 * data.width, + }; + + var best_block: BlockType = .air; + var best_count: u32 = 0; + for (indices) |idx| { + const block = if (data.material_layers[idx].surface != .air) data.material_layers[idx].surface else if (data.top_blocks[idx] != .air) data.top_blocks[idx] else data.biomes[idx].getSurfaceBlock(); + if (block == .air or block == .water) continue; + + var count: u32 = 0; + for (indices) |other_idx| { + const other = if (data.material_layers[other_idx].surface != .air) data.material_layers[other_idx].surface else if (data.top_blocks[other_idx] != .air) data.top_blocks[other_idx] else data.biomes[other_idx].getSurfaceBlock(); + if (other == block) count += 1; + } + if (count > best_count) { + best_block = block; + best_count = count; + } + } + + if (best_block != .air) return best_block; return blockForLODCell(data, gx, gz); } +fn representativeWaterDepth(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { + const x0 = @min(gx, data.width - 1); + const z0 = @min(gz, data.width - 1); + const x1 = @min(gx + 1, data.width - 1); + const z1 = @min(gz + 1, data.width - 1); + const indices = [_]u32{ + x0 + z0 * data.width, + x1 + z0 * data.width, + x0 + z1 * data.width, + x1 + z1 * data.width, + }; + + var weighted_depth: f32 = 0.0; + var coverage: f32 = 0.0; + for (indices) |idx| { + const water = data.water[idx]; + if (!water.is_surface) continue; + weighted_depth += water.depth * water.coverage; + coverage += water.coverage; + } + if (coverage <= 0.001) return 0.0; + return weighted_depth / coverage; +} + fn selectCellMaterial(data: *const LODSimplifiedData, atlas: *const TextureAtlas, gx: u32, gz: u32) TextureAtlas.BlockTiles { const top_block = blockForLODQuad(data, gx, gz); const side_block = sideBlockForLODQuad(data, gx, gz, top_block); @@ -793,6 +861,37 @@ fn averageWaterCoverage(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { return (c00 + c10 + c01 + c11) * 0.25; } +fn stitchedHeight(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { + const height = data.getHeight(gx, gz); + if (data.width < 5) return height; + + const blend_cells: u32 = 2; + const max_idx = data.width - 1; + const edge_dist = @min(@min(gx, gz), @min(max_idx - gx, max_idx - gz)); + if (edge_dist >= blend_cells) return height; + + const coarse_x = @min(((gx + 1) / 2) * 2, max_idx); + const coarse_z = @min(((gz + 1) / 2) * 2, max_idx); + const coarse_height = data.getHeight(coarse_x, coarse_z); + const edge_weight = 1.0 - (@as(f32, @floatFromInt(edge_dist)) / @as(f32, @floatFromInt(blend_cells))); + const blend = edge_weight * 0.35; + return height * (1.0 - blend) + coarse_height * blend; +} + +fn maxStitchedHeightAdjustment(data: *const LODSimplifiedData) f32 { + if (data.width < 5) return 0.0; + + var max_adjust: f32 = 0.0; + var i: u32 = 0; + while (i < data.width) : (i += 1) { + max_adjust = @max(max_adjust, @abs(data.getHeight(i, 0) - stitchedHeight(data, i, 0))); + max_adjust = @max(max_adjust, @abs(data.getHeight(i, data.width - 1) - stitchedHeight(data, i, data.width - 1))); + max_adjust = @max(max_adjust, @abs(data.getHeight(0, i) - stitchedHeight(data, 0, i))); + max_adjust = @max(max_adjust, @abs(data.getHeight(data.width - 1, i) - stitchedHeight(data, data.width - 1, i))); + } + return max_adjust; +} + // Helper functions for unpacking colors fn unpackR(color: u32) f32 { return @as(f32, @floatFromInt((color >> 16) & 0xFF)) / 255.0; @@ -1545,6 +1644,42 @@ test "buildFromSimplifiedData promotes mixed water cells to water material" { try std.testing.expect(found_floor_side); } +test "blockForLODQuad uses representative non-water surface" { + const allocator = std.testing.allocator; + var data = try LODSimplifiedData.init(allocator, .lod1); + defer data.deinit(); + + for (0..data.width * data.width) |i| { + data.biomes[i] = .plains; + data.top_blocks[i] = .grass; + data.material_layers[i] = .{ + .surface = .grass, + .subsurface = .dirt, + .foundation = .stone, + }; + } + + data.material_layers[1].surface = .stone; + data.material_layers[data.width].surface = .stone; + data.material_layers[data.width + 1].surface = .stone; + + try std.testing.expectEqual(BlockType.stone, blockForLODQuad(&data, 0, 0)); +} + +test "stitchedHeight blends boundary points toward coarse grid" { + const allocator = std.testing.allocator; + var data = try LODSimplifiedData.init(allocator, .lod1); + defer data.deinit(); + + for (0..data.width * data.width) |i| { + data.heightmap[i] = 10.0; + } + data.setHeight(0, 1, 100.0); + + try std.testing.expect(stitchedHeight(&data, 0, 1) < 100.0); + try std.testing.expectEqual(@as(f32, 10.0), stitchedHeight(&data, 4, 4)); +} + test "buildFromSimplifiedData uses averaged color tile for far LOD tops" { const allocator = std.testing.allocator; const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES;