Improve biome interpolation on biome changes.

Now it uses a 1d noise, to offset the biome positions and the height interpolation.

This should also fix #633 and helps reduce the impact of #766
This commit is contained in:
IntegratedQuantum 2024-11-05 23:27:22 +01:00
parent 9e4f347268
commit 7b4461ad00
3 changed files with 146 additions and 24 deletions

View File

@ -328,6 +328,10 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
const mapFragment = main.globalAllocator.create(MapFragment);
defer main.stackAllocator.destroy(mapFragment);
mapFragment.init(pos.wx, pos.wy, pos.voxelSize);
var xNoise: [MapFragment.mapSize]f32 = undefined;
var yNoise: [MapFragment.mapSize]f32 = undefined;
terrain.noise.FractalNoise1D.generateSparseFractalTerrain(pos.wx, 32, main.server.world.?.seed, &xNoise);
terrain.noise.FractalNoise1D.generateSparseFractalTerrain(pos.wy, 32, main.server.world.?.seed ^ 0x785298638131, &yNoise);
var originalHeightMap: [MapFragment.mapSize][MapFragment.mapSize]i32 = undefined;
const oldNeighborInfo = mapFragment.load(main.server.world.?.biomePalette, &originalHeightMap) catch |err| {
std.log.err("Error loading map at position {}: {s}", .{pos, @errorName(err)});
@ -369,7 +373,7 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
for(0..interpolationDistance) |b| {
const weirdSquareInterpolation = struct{fn interp(x: f32, y: f32) f32 {
// Basically we want to interpolate the values such that two sides of the square have value zero, while the opposing two sides have value 1.
// Chenge coordinate system:
// Change coordinate system:
if(x == y) return 0.5;
const sqrt2 = @sqrt(0.5);
const k = sqrt2*x + sqrt2*y - sqrt2;
@ -390,7 +394,8 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
if(neighborInfo.@"+o" and neighborInfo.@"o+" and neighborInfo.@"++") factor = 1;
const x = MapFragment.mapSize - 1 - a;
const y = MapFragment.mapSize - 1 - b;
originalHeightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
originalHeightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
}
if(neighborInfo.@"+o" or neighborInfo.@"o-") {
var factor: f32 = 1;
@ -400,7 +405,8 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
if(neighborInfo.@"+o" and neighborInfo.@"o-" and neighborInfo.@"+-") factor = 1;
const x = MapFragment.mapSize - 1 - a;
const y = b;
originalHeightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
originalHeightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
}
if(neighborInfo.@"-o" or neighborInfo.@"o+") {
var factor: f32 = 1;
@ -410,7 +416,8 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
if(neighborInfo.@"-o" and neighborInfo.@"o+" and neighborInfo.@"-+") factor = 1;
const x = a;
const y = MapFragment.mapSize - 1 - b;
originalHeightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
originalHeightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
}
if(neighborInfo.@"-o" or neighborInfo.@"o-") {
var factor: f32 = 1;
@ -420,71 +427,137 @@ pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
if(neighborInfo.@"-o" and neighborInfo.@"o-" and neighborInfo.@"--") factor = 1;
const x = a;
const y = b;
originalHeightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
originalHeightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(originalHeightMap[x][y]))*(1 - factor));
}
}
}
}
}
{ // Interpolate the terraing height:
const InterpolationPolynomial = struct {
// Basically we want an interpolation function with the following properties:
// f(0) = 0
// f(1) = 1
// f'(0) = 0
// f'(1) = 0
// f(noise) = 0.5
// This must be a polynomial of degree 4 with a factor x²
// f(x) = ax + bx³ + cx²
// f'(x) = 4ax³ + 3x² + 2cx
// f(1) = a + b + c = 1 c = 1 - a - b
// f'(1) = 4a + 3b + 2c = 0 4a + 3b + 2 - 2a - 2b = 0 2a + b + 2 = 0 b = -2a - 2
// f(noise) = a noise + b noise³ + c noise² = 0.5 a noise + (-2a - 2) noise³ + (3 + a) noise² = 0.5 a (noise - 2noise³ + noise²) = 2noise³ - 3 noise² + 0.5
// a = (2noise³ - 3 noise² + 0.5)/(noise - 2noise³ + noise²)
// a = (2noise - 3 + 0.5/noise²)/(noise² - 2noise + 1)
// a = (2noise - 3 + 0.5/noise²)/(noise - 1)²
a: f32,
b: f32,
c: f32,
fn get(noise: f32) @This() {
const noise2 = noise*noise;
const noise3 = noise2*noise;
const noise4 = noise2*noise2;
const a = (2*noise3 - 3*noise2 + 0.5)/(noise4 - 2*noise3 + noise2);
const b = -2*a - 2;
const c = 1 - a - b;
return .{.a = a, .b = b, .c = c};
}
fn eval(self: @This(), x: f32) f32 {
return @max(0, @min(0.99999, ((self.a*x + self.b)*x + self.c)*x*x));
}
};
const generatedMap = main.globalAllocator.create(MapFragment);
defer main.stackAllocator.destroy(generatedMap);
generatedMap.init(pos.wx, pos.wy, pos.voxelSize);
profile.mapFragmentGenerator.generateMapFragment(generatedMap, profile.seed);
@memcpy(&mapFragment.heightMap, &originalHeightMap);
for(0..interpolationDistance) |a| { // edges
for(0..MapFragment.mapSize) |b| {
var factor = @as(f32, @floatFromInt(a))/interpolationDistance;
factor = (3 - 2*factor)*factor*factor;
for(0..MapFragment.mapSize) |b| {
const polynomialX = InterpolationPolynomial.get(yNoise[b]*0.5 + 0.25);
const polynomialY = InterpolationPolynomial.get(xNoise[b]*0.5 + 0.25);
for(0..interpolationDistance) |a| { // edges
const factorX = polynomialX.eval(@as(f32, @floatFromInt(a))/interpolationDistance);
const factorY = polynomialY.eval(@as(f32, @floatFromInt(a))/interpolationDistance);
if(!neighborInfo.@"+o") {
const x = MapFragment.mapSize - 1 - a;
const y = b;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factorX + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factorX));
if(factorX < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
if(!neighborInfo.@"-o") {
const x = a;
const y = b;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factorX + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factorX));
if(factorX < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
if(!neighborInfo.@"o+") {
const x = b;
const y = MapFragment.mapSize - 1 - a;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factorY + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factorY));
if(factorY < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
if(!neighborInfo.@"o-") {
const x = b;
const y = a;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factorY + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factorY));
if(factorY < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
}
}
for(0..interpolationDistance) |a| { // corners:
const polynomialY1 = InterpolationPolynomial.get(xNoise[a]*0.5 + 0.25);
const polynomialY2 = InterpolationPolynomial.get(xNoise[MapFragment.mapSize - 1 - a]*0.5 + 0.25);
for(0..interpolationDistance) |b| {
var factorA = @as(f32, @floatFromInt(a))/interpolationDistance;
factorA = (3 - 2*factorA)*factorA*factorA;
var factorB = @as(f32, @floatFromInt(b))/interpolationDistance;
factorB = (3 - 2*factorB)*factorB*factorB;
const factor = 1 - (1 - factorA)*(1 - factorB);
const polynomialX1 = InterpolationPolynomial.get(yNoise[b]*0.5 + 0.25);
const polynomialX2 = InterpolationPolynomial.get(yNoise[MapFragment.mapSize - 1 - b]*0.5 + 0.25);
const factorX1 = polynomialX1.eval(@as(f32, @floatFromInt(a))/interpolationDistance);
const factorX2 = polynomialX2.eval(@as(f32, @floatFromInt(a))/interpolationDistance);
const factorY1 = polynomialY1.eval(@as(f32, @floatFromInt(b))/interpolationDistance);
const factorY2 = polynomialY2.eval(@as(f32, @floatFromInt(b))/interpolationDistance);
if(!neighborInfo.@"++" and neighborInfo.@"+o" and neighborInfo.@"o+") {
const factor = 1 - (1 - factorX2)*(1 - factorY2);
const x = MapFragment.mapSize - 1 - a;
const y = MapFragment.mapSize - 1 - b;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
if(factor < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
if(!neighborInfo.@"+-" and neighborInfo.@"+o" and neighborInfo.@"o-") {
const factor = 1 - (1 - factorX1)*(1 - factorY2);
const x = MapFragment.mapSize - 1 - a;
const y = b;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
if(factor < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
if(!neighborInfo.@"-+" and neighborInfo.@"-o" and neighborInfo.@"o+") {
const factor = 1 - (1 - factorX2)*(1 - factorY1);
const x = a;
const y = MapFragment.mapSize - 1 - b;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
if(factor < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
if(!neighborInfo.@"--" and neighborInfo.@"-o" and neighborInfo.@"o-") {
const factor = 1 - (1 - factorX1)*(1 - factorY1);
const x = a;
const y = b;
mapFragment.heightMap[x][y] = @intFromFloat(@as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
mapFragment.heightMap[x][y] = @intFromFloat(0.5 + @as(f32, @floatFromInt(mapFragment.heightMap[x][y]))*factor + @as(f32, @floatFromInt(generatedMap.heightMap[x][y]))*(1 - factor));
if(factor < 0.25) {
mapFragment.biomeMap[x][y] = generatedMap.biomeMap[x][y];
}
}
}
}
@ -566,4 +639,4 @@ pub fn getOrGenerateFragmentAndIncreaseRefCount(wx: i32, wy: i32, voxelSize: u31
);
const result = cache.findOrCreate(compare, cacheInit, MapFragment.increaseRefCount);
return result;
}
}

View File

@ -0,0 +1,46 @@
const std = @import("std");
const main = @import("root");
fn setSeed(x: i32, offsetX: i32, seed: *u64, worldSeed: u64, scale: u31) void {
seed.* = main.random.initSeed2D(worldSeed, .{offsetX +% x, scale});
}
pub fn generateFractalTerrain(wx: i32, x0: u31, width: u32, scale: u31, worldSeed: u64, map: []f32) void {
const max = scale + 1;
const mask: i32 = scale - 1;
const bigMap = main.stackAllocator.alloc(f32, max);
defer main.stackAllocator.free(bigMap);
const offset = wx & ~mask;
var seed: u64 = undefined;
// Generate the 4 corner points of this map using a coordinate-depending seed:
setSeed(0, offset, &seed, worldSeed, scale);
bigMap[0] = main.random.nextFloat(&seed);
setSeed(scale, offset, &seed, worldSeed, scale);
bigMap[scale] = main.random.nextFloat(&seed);
generateInitializedFractalTerrain(offset, scale, scale, worldSeed, bigMap, 0, 0.9999);
@memcpy(map[x0..][0..width], bigMap[@intCast((wx & mask))..][0..width]);
}
pub fn generateInitializedFractalTerrain(offset: i32, scale: u31, startingScale: u31, worldSeed: u64, bigMap: []f32, lowerLimit: f32, upperLimit: f32) void {
const max = startingScale + 1;
var seed: u64 = undefined;
var res: u31 = startingScale/2;
while(res != 0) : (res /= 2) {
const randomnessScale = @as(f32, @floatFromInt(res))/@as(f32, @floatFromInt(scale))/2;
// No coordinate on the grid:
var x = res;
while(x+res < max) : (x += 2*res) {
setSeed(x, offset, &seed, worldSeed, res);
bigMap[x] = (bigMap[x-res] + bigMap[x+res])/2 + main.random.nextFloatSigned(&seed)*randomnessScale;
bigMap[x] = @min(upperLimit, @max(lowerLimit, bigMap[x]));
}
}
}
pub fn generateSparseFractalTerrain(wx: i32, scale: u31, worldSeed: u64, map: []f32) void {
var x0: u31 = 0;
while(x0 < map.len) : (x0 += scale) {
generateFractalTerrain(wx +% x0, x0, @min(map.len - x0, scale), scale, worldSeed, map);
}
}

View File

@ -2,6 +2,9 @@
/// Like FractalNoise, except in 3D and it generates values on demand and caches results, instead of generating everything at once.
pub const CachedFractalNoise3D = @import("CachedFractalNoise3D.zig");
/// Like FractalNoise, except in 1D.
pub const FractalNoise1D = @import("FractalNoise1D.zig");
/// Like FractalNoise, except in 3D.
pub const FractalNoise3D = @import("FractalNoise3D.zig");
@ -21,4 +24,4 @@ pub const BlueNoise = @import("BlueNoise.zig");
pub const PerlinNoise = @import("PerlinNoise.zig");
pub const ValueNoise = @import("ValueNoise.zig");
pub const ValueNoise = @import("ValueNoise.zig");