diff --git a/src/server/terrain/SurfaceMap.zig b/src/server/terrain/SurfaceMap.zig index 19030e59e..48996f8d3 100644 --- a/src/server/terrain/SurfaceMap.zig +++ b/src/server/terrain/SurfaceMap.zig @@ -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; -} \ No newline at end of file +} diff --git a/src/server/terrain/noise/FractalNoise1D.zig b/src/server/terrain/noise/FractalNoise1D.zig new file mode 100644 index 000000000..31fc6f2ff --- /dev/null +++ b/src/server/terrain/noise/FractalNoise1D.zig @@ -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); + } +} diff --git a/src/server/terrain/noise/noise.zig b/src/server/terrain/noise/noise.zig index 848270901..ccdbebf2e 100644 --- a/src/server/terrain/noise/noise.zig +++ b/src/server/terrain/noise/noise.zig @@ -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"); \ No newline at end of file +pub const ValueNoise = @import("ValueNoise.zig");