Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions assets/shaders/vulkan/g_pass.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
12 changes: 7 additions & 5 deletions src/world/lod_manager.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 8 additions & 5 deletions src/world/lod_renderer.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Loading