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
2 changes: 1 addition & 1 deletion assets/shaders/vulkan/g_pass.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 5 additions & 6 deletions assets/shaders/vulkan/terrain.frag
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down
Binary file modified assets/shaders/vulkan/terrain.frag.spv
Binary file not shown.
8 changes: 4 additions & 4 deletions modules/world-lod/src/lod_chunk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 },

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
}
145 changes: 140 additions & 5 deletions modules/world-lod/src/lod_mesh.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading