From 58dddf3873d1417d7c9af8759e404c6840cdbd02 Mon Sep 17 00:00:00 2001 From: IntegratedQuantum Date: Thu, 16 Jul 2026 16:46:44 +0200 Subject: [PATCH] Smarter pruning during the recursive subdivision during climate generation --- .../terrain/climategen/NoiseBasedVoronoi.zig | 88 +++++++++++++------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/src/server/terrain/climategen/NoiseBasedVoronoi.zig b/src/server/terrain/climategen/NoiseBasedVoronoi.zig index 7a5a4f364f..90086e0334 100644 --- a/src/server/terrain/climategen/NoiseBasedVoronoi.zig +++ b/src/server/terrain/climategen/NoiseBasedVoronoi.zig @@ -209,7 +209,20 @@ const GenerationStructure = struct { return x*x*(3 - 2*x); } - fn findClosestBiomeTo(prefilteredCandidates: []*BiomePoint, wx: i32, wy: i32, relX: i32, relY: i32, worldSeed: u64) BiomeSample { + fn interpolationValueBetween(biome1: *const BiomePoint, biome2: *const BiomePoint, pos: Vec2i) f32 { + const dist = biome1.voronoiDistanceFunction(pos); + const dist2 = biome2.voronoiDistanceFunction(pos); + const totalDist = dist + dist2; + const interpolationStrength = 0.5; + const centerPos = totalDist*0.5; + const interpolationStart = centerPos - interpolationStrength*0.5; + const interpolationEnd = centerPos + interpolationStrength*0.5; + if (dist < interpolationStart) return 0; + if (dist > interpolationEnd) return 1; + return smoothInterpolation((dist - interpolationStart)/(interpolationEnd - interpolationStart)); + } + + fn findClosestBiomeTo(prefilteredCandidates: []const *const BiomePoint, wx: i32, wy: i32, relX: i32, relY: i32, worldSeed: u64) BiomeSample { const x = wx +% relX*terrain.SurfaceMap.MapFragment.biomeSize; var y = wy +% relY*terrain.SurfaceMap.MapFragment.biomeSize; if (@mod(relX, 2) == 1) y += terrain.SurfaceMap.MapFragment.biomeSize/2; @@ -221,8 +234,8 @@ const GenerationStructure = struct { var hills: f32 = 0; var mountains: f32 = 0; var totalWeight: f32 = 0; - // all BiomePoints are within ±2 chunks of the current one. - var candidateList: main.List(struct { point: *BiomePoint, weight: f32 }) = .initCapacity(main.stackAllocator, prefilteredCandidates.len); + + var candidateList: main.List(struct { point: *const BiomePoint, weight: f32 }) = .initCapacity(main.stackAllocator, prefilteredCandidates.len); defer candidateList.deinit(main.stackAllocator); for (prefilteredCandidates) |candidate| { candidateList.appendAssumeCapacity(.{.point = candidate, .weight = 1}); @@ -230,24 +243,17 @@ const GenerationStructure = struct { // Interpolate between all pairs of biomes. var i: usize = 0; outer: while (i < candidateList.items.len) { - const dist = candidateList.items[i].point.voronoiDistanceFunction(.{x, y}); var j = i + 1; while (j < candidateList.items.len) { - const dist2 = candidateList.items[j].point.voronoiDistanceFunction(.{x, y}); - const totalDist = dist + dist2; - const interpolationStrength = 0.5; - const centerPos = totalDist*0.5; - const interpolationStart = centerPos - interpolationStrength*0.5; - const interpolationEnd = centerPos + interpolationStrength*0.5; - if (dist < interpolationStart) { + const interp = interpolationValueBetween(candidateList.items[i].point, candidateList.items[j].point, .{x, y}); + if (interp == 0) { _ = candidateList.swapRemove(j); continue; } - if (dist > interpolationEnd) { + if (interp == 1) { _ = candidateList.swapRemove(i); continue :outer; } - const interp = smoothInterpolation((dist - interpolationStart)/(interpolationEnd - interpolationStart)); candidateList.items[i].weight *= 1 - interp; candidateList.items[j].weight *= interp; j += 1; @@ -435,10 +441,45 @@ const GenerationStructure = struct { } } + fn pruneInterpolationCandidates(allocator: NeverFailingAllocator, wx: i32, wy: i32, wxMax: i32, wyMax: i32, candidates: []const *const BiomePoint) []const *const BiomePoint { + var result: main.List(*const BiomePoint) = .empty; + + // Check interpolation between all pairs of biomes. + outer: for (candidates) |candidate| { + if (candidate.pos[0] >= wx and candidate.pos[0] < wxMax and candidate.pos[1] >= wy and candidate.pos[1] < wyMax) { + result.append(allocator, candidate); + continue; + } + var i: usize = 0; + while (i < result.items.len) { + blk: { + const interp1 = interpolationValueBetween(candidate, result.items[i], .{wx, wy}); + if (interp1 != 0 and interp1 != 1) break :blk; + const interp2 = interpolationValueBetween(candidate, result.items[i], .{wx, wyMax}); + if (interp2 != interp1) break :blk; + const interp3 = interpolationValueBetween(candidate, result.items[i], .{wxMax, wy}); + if (interp3 != interp1) break :blk; + const interp4 = interpolationValueBetween(candidate, result.items[i], .{wxMax, wyMax}); + if (interp4 != interp1) break :blk; + if (interp1 == 0) { + _ = result.swapRemove(i); + continue; + } else { + continue :outer; + } + } + i += 1; + } + result.append(allocator, candidate); + } + + return result.toOwnedSlice(allocator); + } + const margin = chunkSize >> terrain.SurfaceMap.MapFragment.biomeShift; const preMapSize = ClimateMapFragment.mapEntrysSize + 2*margin; - pub fn fillRecursively(wx: i32, wy: i32, preMap: *[preMapSize][preMapSize]BiomeSample, biomeCandidates: []*BiomePoint, worldSeed: u64, relX: i32, relY: i32, width: u31, height: u31) void { - if (width <= 1 or height <= 1) { + pub fn fillRecursively(wx: i32, wy: i32, preMap: *[preMapSize][preMapSize]BiomeSample, biomeCandidates: []const *const BiomePoint, worldSeed: u64, relX: i32, relY: i32, width: u31, height: u31) void { + if (width < 8 or height < 8) { for (0..width) |dx| { const indexX = @as(usize, @intCast(relX + margin)) + dx; for (0..height) |dy| { @@ -451,8 +492,6 @@ const GenerationStructure = struct { // Subdivide const halfWidth = width/2; const halfHeight = height/2; - var newCandidates: main.List(*BiomePoint) = .initCapacity(main.stackAllocator, biomeCandidates.len); - defer newCandidates.deinit(main.stackAllocator); for (0..2) |dx| { for (0..2) |dy| { const newRelX = relX + if (dx == 0) 0 else halfWidth; @@ -465,18 +504,9 @@ const GenerationStructure = struct { const wyMin = wy +% newRelY*terrain.SurfaceMap.MapFragment.biomeSize; const wyMax = wyMin +% newHeight*terrain.SurfaceMap.MapFragment.biomeSize +% terrain.SurfaceMap.MapFragment.biomeSize; - newCandidates.clearRetainingCapacity(); - for (biomeCandidates) |candidate| { - const influenceRadius = 3*@as(i32, @ceil(candidate.radius)); - const candidateMinX = wxMin -% influenceRadius; - const candidateMaxX = wxMax +% influenceRadius; - const candidateMinY = wyMin -% influenceRadius; - const candidateMaxY = wyMax +% influenceRadius; - if (candidate.pos[0] -% candidateMinX < 0 or candidate.pos[0] -% candidateMaxX > 0) continue; - if (candidate.pos[1] -% candidateMinY < 0 or candidate.pos[1] -% candidateMaxY > 0) continue; - newCandidates.appendAssumeCapacity(candidate); - } - fillRecursively(wx, wy, preMap, newCandidates.items, worldSeed, newRelX, newRelY, newWidth, newHeight); + const pruned = pruneInterpolationCandidates(main.stackAllocator, wxMin, wyMin, wxMax, wyMax, biomeCandidates); + defer main.stackAllocator.free(pruned); + fillRecursively(wx, wy, preMap, pruned, worldSeed, newRelX, newRelY, newWidth, newHeight); } } }