mirror of
https://github.com/PixelGuys/Cubyz.git
synced 2025-08-03 03:06:55 -04:00
655 lines
29 KiB
Zig
655 lines
29 KiB
Zig
const std = @import("std");
|
|
const Atomic = std.atomic.Value;
|
|
|
|
const main = @import("main");
|
|
const Chunk = main.chunk.Chunk;
|
|
const ChunkPosition = main.chunk.ChunkPosition;
|
|
const Cache = main.utils.Cache;
|
|
const ZonElement = main.ZonElement;
|
|
const Vec3d = main.vec.Vec3d;
|
|
const BinaryWriter = main.utils.BinaryWriter;
|
|
const BinaryReader = main.utils.BinaryReader;
|
|
|
|
const terrain = @import("terrain.zig");
|
|
const TerrainGenerationProfile = terrain.TerrainGenerationProfile;
|
|
const Biome = terrain.biomes.Biome;
|
|
|
|
pub const MapFragmentPosition = struct {
|
|
wx: i32,
|
|
wy: i32,
|
|
voxelSize: u31,
|
|
voxelSizeShift: u5,
|
|
|
|
pub fn init(wx: i32, wy: i32, voxelSize: u31) MapFragmentPosition {
|
|
std.debug.assert(voxelSize - 1 & voxelSize == 0); // voxelSize must be a power of 2.
|
|
std.debug.assert(wx & voxelSize - 1 == 0 and wy & voxelSize - 1 == 0); // The coordinates are misaligned. They need to be aligned to the voxelSize grid.
|
|
return MapFragmentPosition{
|
|
.wx = wx,
|
|
.wy = wy,
|
|
.voxelSize = voxelSize,
|
|
.voxelSizeShift = @ctz(voxelSize),
|
|
};
|
|
}
|
|
|
|
pub fn equals(self: MapFragmentPosition, other: anytype) bool {
|
|
if(other) |ch| {
|
|
return self.wx == ch.pos.wx and self.wy == ch.pos.wy and self.voxelSize == ch.pos.voxelSize;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
pub fn hashCode(self: MapFragmentPosition) u32 {
|
|
return @bitCast((self.wx >> (MapFragment.mapShift + self.voxelSizeShift))*%33 +% (self.wy >> (MapFragment.mapShift + self.voxelSizeShift)) ^ self.voxelSize);
|
|
}
|
|
|
|
pub fn getMinDistanceSquared(self: MapFragmentPosition, playerPosition: Vec3d, comptime width: comptime_int) f64 {
|
|
const adjustedPosition = @mod(playerPosition + @as(Vec3d, @splat(1 << 31)), @as(Vec3d, @splat(1 << 32))) - @as(Vec3d, @splat(1 << 31));
|
|
const halfWidth: f64 = @floatFromInt(self.voxelSize*@divExact(width, 2));
|
|
var dx = @abs(@as(f64, @floatFromInt(self.wx)) + halfWidth - adjustedPosition[0]);
|
|
var dy = @abs(@as(f64, @floatFromInt(self.wy)) + halfWidth - adjustedPosition[1]);
|
|
dx = @max(0, dx - halfWidth);
|
|
dy = @max(0, dy - halfWidth);
|
|
return dx*dx + dy*dy;
|
|
}
|
|
|
|
pub fn getPriority(self: MapFragmentPosition, playerPos: Vec3d, comptime width: comptime_int) f32 {
|
|
return -@as(f32, @floatCast(self.getMinDistanceSquared(playerPos, width)))/@as(f32, @floatFromInt(self.voxelSize*self.voxelSize)) + 2*@as(f32, @floatFromInt(std.math.log2_int(u31, self.voxelSize)))*width*width;
|
|
}
|
|
};
|
|
|
|
/// Generates and stores the height and Biome maps of the planet.
|
|
pub const MapFragment = struct { // MARK: MapFragment
|
|
pub const biomeShift = 5;
|
|
/// The average diameter of a biome.
|
|
pub const biomeSize = 1 << biomeShift;
|
|
pub const mapShift = 8;
|
|
pub const mapSize = 1 << mapShift;
|
|
pub const mapMask = mapSize - 1;
|
|
|
|
heightMap: [mapSize][mapSize]i32 = undefined,
|
|
biomeMap: [mapSize][mapSize]*const Biome = undefined,
|
|
minHeight: i32 = std.math.maxInt(i32),
|
|
maxHeight: i32 = 0,
|
|
pos: MapFragmentPosition,
|
|
|
|
wasStored: Atomic(bool) = .init(false),
|
|
|
|
refCount: Atomic(u16) = .init(0),
|
|
|
|
pub fn init(self: *MapFragment, wx: i32, wy: i32, voxelSize: u31) void {
|
|
self.* = .{
|
|
.pos = MapFragmentPosition.init(wx, wy, voxelSize),
|
|
};
|
|
}
|
|
|
|
pub fn increaseRefCount(self: *MapFragment) void {
|
|
const prevVal = self.refCount.fetchAdd(1, .monotonic);
|
|
std.debug.assert(prevVal != 0);
|
|
}
|
|
|
|
pub fn decreaseRefCount(self: *MapFragment) void {
|
|
const prevVal = self.refCount.fetchSub(1, .monotonic);
|
|
std.debug.assert(prevVal != 0);
|
|
if(prevVal == 1) {
|
|
memoryPool.destroy(self);
|
|
}
|
|
}
|
|
|
|
pub fn getBiome(self: *MapFragment, wx: i32, wy: i32) *const Biome {
|
|
const xIndex = wx >> self.pos.voxelSizeShift & mapMask;
|
|
const yIndex = wy >> self.pos.voxelSizeShift & mapMask;
|
|
return self.biomeMap[@intCast(xIndex)][@intCast(yIndex)];
|
|
}
|
|
|
|
pub fn getHeight(self: *MapFragment, wx: i32, wy: i32) i32 {
|
|
const xIndex = wx >> self.pos.voxelSizeShift & mapMask;
|
|
const yIndex = wy >> self.pos.voxelSizeShift & mapMask;
|
|
return self.heightMap[@intCast(xIndex)][@intCast(yIndex)];
|
|
}
|
|
|
|
const StorageHeader = struct {
|
|
const minSupportedVersion: u8 = 0;
|
|
const activeVersion: u8 = 1;
|
|
version: u8 = activeVersion,
|
|
neighborInfo: NeighborInfo,
|
|
};
|
|
const NeighborInfo = packed struct(u8) {
|
|
@"-o": bool = false,
|
|
@"+o": bool = false,
|
|
@"o-": bool = false,
|
|
@"o+": bool = false,
|
|
@"--": bool = false,
|
|
@"-+": bool = false,
|
|
@"+-": bool = false,
|
|
@"++": bool = false,
|
|
};
|
|
|
|
pub fn load(self: *MapFragment, biomePalette: *main.assets.Palette, originalHeightMap: ?*[mapSize][mapSize]i32) !NeighborInfo {
|
|
const saveFolder: []const u8 = std.fmt.allocPrint(main.stackAllocator.allocator, "saves/{s}/maps", .{main.server.world.?.path}) catch unreachable;
|
|
defer main.stackAllocator.free(saveFolder);
|
|
|
|
const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{}/{}/{}.surface", .{saveFolder, self.pos.voxelSize, self.pos.wx, self.pos.wy}) catch unreachable;
|
|
defer main.stackAllocator.free(path);
|
|
|
|
const fullData = try main.files.read(main.stackAllocator, path);
|
|
defer main.stackAllocator.free(fullData);
|
|
|
|
var fullReader = BinaryReader.init(fullData);
|
|
|
|
const header: StorageHeader = .{
|
|
.version = try fullReader.readInt(u8),
|
|
.neighborInfo = @bitCast(try fullReader.readInt(u8)),
|
|
};
|
|
switch(header.version) {
|
|
0 => { // TODO: Remove after next breaking change
|
|
const rawData: []u8 = main.stackAllocator.alloc(u8, mapSize*mapSize*(@sizeOf(u32) + 2*@sizeOf(f32)));
|
|
defer main.stackAllocator.free(rawData);
|
|
if(try main.utils.Compression.inflateTo(rawData, fullReader.remaining) != rawData.len) return error.CorruptedFile;
|
|
const biomeData = rawData[0 .. mapSize*mapSize*@sizeOf(u32)];
|
|
const heightData = rawData[mapSize*mapSize*@sizeOf(u32) ..][0 .. mapSize*mapSize*@sizeOf(f32)];
|
|
const originalHeightData = rawData[mapSize*mapSize*(@sizeOf(u32) + @sizeOf(f32)) ..][0 .. mapSize*mapSize*@sizeOf(f32)];
|
|
for(0..mapSize) |x| {
|
|
for(0..mapSize) |y| {
|
|
self.biomeMap[x][y] = main.server.terrain.biomes.getById(biomePalette.palette.items[std.mem.readInt(u32, biomeData[4*(x*mapSize + y) ..][0..4], .big)]);
|
|
self.heightMap[x][y] = @intFromFloat(@as(f32, @bitCast(std.mem.readInt(u32, heightData[4*(x*mapSize + y) ..][0..4], .big))));
|
|
if(originalHeightMap) |map| map[x][y] = @intFromFloat(@as(f32, @bitCast(std.mem.readInt(u32, originalHeightData[4*(x*mapSize + y) ..][0..4], .big))));
|
|
}
|
|
}
|
|
},
|
|
1 => {
|
|
const biomeDataSize = mapSize*mapSize*@sizeOf(u32);
|
|
const heightDataSize = mapSize*mapSize*@sizeOf(i32);
|
|
const originalHeightDataSize = mapSize*mapSize*@sizeOf(i32);
|
|
|
|
const rawData: []u8 = main.stackAllocator.alloc(u8, biomeDataSize + heightDataSize + originalHeightDataSize);
|
|
defer main.stackAllocator.free(rawData);
|
|
if(try main.utils.Compression.inflateTo(rawData, fullReader.remaining) != rawData.len) return error.CorruptedFile;
|
|
|
|
var reader = BinaryReader.init(rawData);
|
|
|
|
for(0..mapSize) |x| for(0..mapSize) |y| {
|
|
self.biomeMap[x][y] = main.server.terrain.biomes.getById(biomePalette.palette.items[try reader.readInt(u32)]);
|
|
};
|
|
for(0..mapSize) |x| for(0..mapSize) |y| {
|
|
self.heightMap[x][y] = try reader.readInt(i32);
|
|
};
|
|
if(originalHeightMap) |map| for(0..mapSize) |x| for(0..mapSize) |y| {
|
|
map[x][y] = try reader.readInt(i32);
|
|
};
|
|
},
|
|
else => return error.OutdatedFileVersion,
|
|
}
|
|
self.wasStored.store(true, .monotonic);
|
|
return header.neighborInfo;
|
|
}
|
|
|
|
pub fn save(self: *MapFragment, originalData: ?*[mapSize][mapSize]i32, neighborInfo: NeighborInfo) void {
|
|
const biomeDataSize = mapSize*mapSize*@sizeOf(u32);
|
|
const heightDataSize = mapSize*mapSize*@sizeOf(i32);
|
|
const originalHeightDataSize = mapSize*mapSize*@sizeOf(i32);
|
|
|
|
var writer = BinaryWriter.initCapacity(main.stackAllocator, biomeDataSize + heightDataSize + originalHeightDataSize);
|
|
defer writer.deinit();
|
|
|
|
for(0..mapSize) |x| for(0..mapSize) |y| writer.writeInt(u32, self.biomeMap[x][y].paletteId);
|
|
for(0..mapSize) |x| for(0..mapSize) |y| writer.writeInt(i32, self.heightMap[x][y]);
|
|
for(0..mapSize) |x| for(0..mapSize) |y| writer.writeInt(i32, (if(originalData) |map| map else &self.heightMap)[x][y]);
|
|
|
|
const compressedData = main.utils.Compression.deflate(main.stackAllocator, writer.data.items, .fast);
|
|
defer main.stackAllocator.free(compressedData);
|
|
|
|
var outputWriter = BinaryWriter.initCapacity(main.stackAllocator, @sizeOf(StorageHeader) + compressedData.len);
|
|
defer outputWriter.deinit();
|
|
|
|
const header: StorageHeader = .{
|
|
.neighborInfo = neighborInfo,
|
|
};
|
|
outputWriter.writeInt(u8, header.version);
|
|
outputWriter.writeInt(u8, @bitCast(header.neighborInfo));
|
|
outputWriter.writeSlice(compressedData);
|
|
|
|
const saveFolder: []const u8 = std.fmt.allocPrint(main.stackAllocator.allocator, "saves/{s}/maps", .{main.server.world.?.path}) catch unreachable;
|
|
defer main.stackAllocator.free(saveFolder);
|
|
|
|
const path = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{}/{}/{}.surface", .{saveFolder, self.pos.voxelSize, self.pos.wx, self.pos.wy}) catch unreachable;
|
|
defer main.stackAllocator.free(path);
|
|
const folder = std.fmt.allocPrint(main.stackAllocator.allocator, "{s}/{}/{}", .{saveFolder, self.pos.voxelSize, self.pos.wx}) catch unreachable;
|
|
defer main.stackAllocator.free(folder);
|
|
|
|
main.files.makeDir(folder) catch |err| {
|
|
std.log.err("Error while writing to file {s}: {s}", .{path, @errorName(err)});
|
|
};
|
|
|
|
main.files.write(path, outputWriter.data.items) catch |err| {
|
|
std.log.err("Error while writing to file {s}: {s}", .{path, @errorName(err)});
|
|
};
|
|
}
|
|
};
|
|
|
|
/// Generates the detailed(block-level precision) height and biome maps from the climate map.
|
|
pub const MapGenerator = struct {
|
|
init: *const fn(parameters: ZonElement) void,
|
|
deinit: *const fn() void,
|
|
generateMapFragment: *const fn(fragment: *MapFragment, seed: u64) void,
|
|
|
|
var generatorRegistry: std.StringHashMapUnmanaged(MapGenerator) = .{};
|
|
|
|
fn registerGenerator(comptime Generator: type) void {
|
|
const self = MapGenerator{
|
|
.init = &Generator.init,
|
|
.deinit = &Generator.deinit,
|
|
.generateMapFragment = &Generator.generateMapFragment,
|
|
};
|
|
generatorRegistry.put(main.globalAllocator.allocator, Generator.id, self) catch unreachable;
|
|
}
|
|
|
|
pub fn getGeneratorById(id: []const u8) !MapGenerator {
|
|
return generatorRegistry.get(id) orelse {
|
|
std.log.err("Couldn't find map generator with id {s}", .{id});
|
|
return error.UnknownMapGenerator;
|
|
};
|
|
}
|
|
};
|
|
|
|
const cacheSize = 1 << 6; // Must be a power of 2!
|
|
const cacheMask = cacheSize - 1;
|
|
const associativity = 8; // ~400MiB MiB Cache size
|
|
var cache: Cache(MapFragment, cacheSize, associativity, MapFragment.decreaseRefCount) = .{};
|
|
var profile: TerrainGenerationProfile = undefined;
|
|
|
|
var memoryPool: main.heap.MemoryPool(MapFragment) = undefined;
|
|
|
|
pub fn globalInit() void {
|
|
const list = @import("mapgen/_list.zig");
|
|
inline for(@typeInfo(list).@"struct".decls) |decl| {
|
|
MapGenerator.registerGenerator(@field(list, decl.name));
|
|
}
|
|
memoryPool = .init(main.globalAllocator);
|
|
}
|
|
|
|
pub fn globalDeinit() void {
|
|
MapGenerator.generatorRegistry.clearAndFree(main.globalAllocator.allocator);
|
|
memoryPool.deinit();
|
|
}
|
|
|
|
fn cacheInit(pos: MapFragmentPosition) *MapFragment {
|
|
const mapFragment = memoryPool.create();
|
|
mapFragment.init(pos.wx, pos.wy, pos.voxelSize);
|
|
_ = mapFragment.load(main.server.world.?.biomePalette, null) catch {
|
|
profile.mapFragmentGenerator.generateMapFragment(mapFragment, profile.seed);
|
|
};
|
|
_ = @atomicRmw(u16, &mapFragment.refCount.raw, .Add, 1, .monotonic);
|
|
return mapFragment;
|
|
}
|
|
|
|
pub fn regenerateLOD(worldName: []const u8) !void { // MARK: regenerateLOD()
|
|
std.log.info("Regenerating map LODs...", .{});
|
|
// Delete old LODs:
|
|
for(1..main.settings.highestSupportedLod + 1) |i| {
|
|
const lod = @as(u32, 1) << @intCast(i);
|
|
const path = std.fmt.allocPrint(main.stackAllocator.allocator, "saves/{s}/maps", .{worldName}) catch unreachable;
|
|
defer main.stackAllocator.free(path);
|
|
const dir = std.fmt.allocPrint(main.stackAllocator.allocator, "{}", .{lod}) catch unreachable;
|
|
defer main.stackAllocator.free(dir);
|
|
main.files.deleteDir(path, dir) catch |err| {
|
|
if(err != error.FileNotFound) {
|
|
std.log.err("Error while deleting directory {s}/{s}: {s}", .{path, dir, @errorName(err)});
|
|
}
|
|
};
|
|
}
|
|
// Find all the stored maps:
|
|
var mapPositions = main.List(MapFragmentPosition).init(main.stackAllocator);
|
|
defer mapPositions.deinit();
|
|
const path = std.fmt.allocPrint(main.stackAllocator.allocator, "saves/{s}/maps/1", .{worldName}) catch unreachable;
|
|
defer main.stackAllocator.free(path);
|
|
{
|
|
var dirX = try std.fs.cwd().openDir(path, .{.iterate = true});
|
|
defer dirX.close();
|
|
var iterX = dirX.iterate();
|
|
while(try iterX.next()) |entryX| {
|
|
if(entryX.kind != .directory) continue;
|
|
const wx = std.fmt.parseInt(i32, entryX.name, 0) catch continue;
|
|
var dirY = try dirX.openDir(entryX.name, .{.iterate = true});
|
|
defer dirY.close();
|
|
var iterY = dirY.iterate();
|
|
while(try iterY.next()) |entryY| {
|
|
if(entryY.kind != .file) continue;
|
|
const nameY = entryY.name[0 .. std.mem.indexOfScalar(u8, entryY.name, '.') orelse entryY.name.len];
|
|
const wy = std.fmt.parseInt(i32, nameY, 0) catch continue;
|
|
mapPositions.append(.{.wx = wx, .wy = wy, .voxelSize = 1, .voxelSizeShift = 0});
|
|
}
|
|
}
|
|
}
|
|
// Load all the stored maps and update their next LODs.
|
|
const interpolationDistance = 64;
|
|
for(mapPositions.items) |pos| {
|
|
var neighborInfo: MapFragment.NeighborInfo = undefined;
|
|
inline for(comptime std.meta.fieldNames(MapFragment.NeighborInfo)) |name| {
|
|
var neighborPos = pos;
|
|
if(name[0] == '+') neighborPos.wx +%= MapFragment.mapSize;
|
|
if(name[0] == '-') neighborPos.wx -%= MapFragment.mapSize;
|
|
if(name[1] == '+') neighborPos.wy +%= MapFragment.mapSize;
|
|
if(name[1] == '-') neighborPos.wy -%= MapFragment.mapSize;
|
|
var isPresent: bool = false;
|
|
for(mapPositions.items) |otherPos| {
|
|
if(neighborPos.wx == otherPos.wx and neighborPos.wy == otherPos.wy) {
|
|
isPresent = true;
|
|
break;
|
|
}
|
|
}
|
|
@field(neighborInfo, name) = isPresent;
|
|
}
|
|
const mapFragment = main.stackAllocator.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)});
|
|
continue;
|
|
};
|
|
if(@as(u8, @bitCast(neighborInfo)) != @as(u8, @bitCast(oldNeighborInfo))) {
|
|
// Now we do the fun stuff
|
|
// Basically we want to only keep the interpolated map in the direction of the changes.
|
|
// Edges:
|
|
if(neighborInfo.@"+o" != oldNeighborInfo.@"+o" or neighborInfo.@"-o" != oldNeighborInfo.@"-o" or neighborInfo.@"o+" != oldNeighborInfo.@"o+" or neighborInfo.@"o-" != oldNeighborInfo.@"o-") {
|
|
for(0..interpolationDistance) |a| { // edges
|
|
for(interpolationDistance..MapFragment.mapSize - interpolationDistance) |b| {
|
|
if(neighborInfo.@"+o" and !oldNeighborInfo.@"+o") {
|
|
const x = MapFragment.mapSize - 1 - a;
|
|
const y = b;
|
|
originalHeightMap[x][y] = mapFragment.heightMap[x][y];
|
|
}
|
|
if(neighborInfo.@"-o" and !oldNeighborInfo.@"-o") {
|
|
const x = a;
|
|
const y = b;
|
|
originalHeightMap[x][y] = mapFragment.heightMap[x][y];
|
|
}
|
|
if(neighborInfo.@"o+" and !oldNeighborInfo.@"o+") {
|
|
const x = b;
|
|
const y = MapFragment.mapSize - 1 - a;
|
|
originalHeightMap[x][y] = mapFragment.heightMap[x][y];
|
|
}
|
|
if(neighborInfo.@"o-" and !oldNeighborInfo.@"o-") {
|
|
const x = b;
|
|
const y = a;
|
|
originalHeightMap[x][y] = mapFragment.heightMap[x][y];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Corners:
|
|
{
|
|
for(0..interpolationDistance) |a| { // corners:
|
|
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.
|
|
// Change coordinate system:
|
|
if(x == y) return 0.5;
|
|
const sqrt2 = @sqrt(0.5);
|
|
const k = sqrt2*x + sqrt2*y - sqrt2;
|
|
const l = -sqrt2*x + sqrt2*y;
|
|
const maxMagnitude = sqrt2 - @abs(k);
|
|
return l/maxMagnitude*0.5 + 0.5;
|
|
// if x = y:
|
|
}
|
|
}.interp;
|
|
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;
|
|
if(neighborInfo.@"+o" or neighborInfo.@"o+") {
|
|
var factor: f32 = 1;
|
|
if(neighborInfo.@"+o" and neighborInfo.@"o+" == oldNeighborInfo.@"o+" and !oldNeighborInfo.@"+o") factor = weirdSquareInterpolation(1 - factorB, 1 - factorA);
|
|
if(neighborInfo.@"o+" and neighborInfo.@"+o" == oldNeighborInfo.@"+o" and !oldNeighborInfo.@"o+") factor = weirdSquareInterpolation(1 - factorA, 1 - factorB);
|
|
if(neighborInfo.@"+o" == oldNeighborInfo.@"+o" and neighborInfo.@"o+" == oldNeighborInfo.@"o+") factor = 0;
|
|
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(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;
|
|
if(neighborInfo.@"+o" and neighborInfo.@"o-" == oldNeighborInfo.@"o-" and !oldNeighborInfo.@"+o") factor = weirdSquareInterpolation(1 - factorB, 1 - factorA);
|
|
if(neighborInfo.@"o-" and neighborInfo.@"+o" == oldNeighborInfo.@"+o" and !oldNeighborInfo.@"o-") factor = weirdSquareInterpolation(1 - factorA, 1 - factorB);
|
|
if(neighborInfo.@"+o" == oldNeighborInfo.@"+o" and neighborInfo.@"o-" == oldNeighborInfo.@"o-") factor = 0;
|
|
if(neighborInfo.@"+o" and neighborInfo.@"o-" and neighborInfo.@"+-") factor = 1;
|
|
const x = MapFragment.mapSize - 1 - a;
|
|
const y = b;
|
|
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;
|
|
if(neighborInfo.@"-o" and neighborInfo.@"o+" == oldNeighborInfo.@"o+" and !oldNeighborInfo.@"-o") factor = weirdSquareInterpolation(1 - factorB, 1 - factorA);
|
|
if(neighborInfo.@"o+" and neighborInfo.@"-o" == oldNeighborInfo.@"-o" and !oldNeighborInfo.@"o+") factor = weirdSquareInterpolation(1 - factorA, 1 - factorB);
|
|
if(neighborInfo.@"-o" == oldNeighborInfo.@"-o" and neighborInfo.@"o+" == oldNeighborInfo.@"o+") factor = 0;
|
|
if(neighborInfo.@"-o" and neighborInfo.@"o+" and neighborInfo.@"-+") factor = 1;
|
|
const x = a;
|
|
const y = MapFragment.mapSize - 1 - b;
|
|
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;
|
|
if(neighborInfo.@"-o" and neighborInfo.@"o-" == oldNeighborInfo.@"o-" and !oldNeighborInfo.@"-o") factor = weirdSquareInterpolation(1 - factorB, 1 - factorA);
|
|
if(neighborInfo.@"o-" and neighborInfo.@"-o" == oldNeighborInfo.@"-o" and !oldNeighborInfo.@"o-") factor = weirdSquareInterpolation(1 - factorA, 1 - factorB);
|
|
if(neighborInfo.@"-o" == oldNeighborInfo.@"-o" and neighborInfo.@"o-" == oldNeighborInfo.@"o-") factor = 0;
|
|
if(neighborInfo.@"-o" and neighborInfo.@"o-" and neighborInfo.@"--") factor = 1;
|
|
const x = a;
|
|
const y = b;
|
|
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.stackAllocator.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..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(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(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(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(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| {
|
|
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(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(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(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(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];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mapFragment.save(&originalHeightMap, neighborInfo); // Store the interpolated map
|
|
// Generate LODs
|
|
var cur = mapFragment;
|
|
defer if(cur.pos.voxelSize != 1) cur.decreaseRefCount();
|
|
while(cur.pos.voxelSizeShift < main.settings.highestSupportedLod) {
|
|
var nextPos = cur.pos;
|
|
nextPos.voxelSize *= 2;
|
|
nextPos.voxelSizeShift += 1;
|
|
const nextMask = ~@as(i32, nextPos.voxelSize*MapFragment.mapSize - 1);
|
|
nextPos.wx &= nextMask;
|
|
nextPos.wy &= nextMask;
|
|
const next = getOrGenerateFragmentAndIncreaseRefCount(nextPos.wx, nextPos.wy, nextPos.voxelSize);
|
|
const offSetX: usize = @intCast((cur.pos.wx -% nextPos.wx) >> nextPos.voxelSizeShift);
|
|
const offSetY: usize = @intCast((cur.pos.wy -% nextPos.wy) >> nextPos.voxelSizeShift);
|
|
for(0..MapFragment.mapSize/2) |x| {
|
|
for(0..MapFragment.mapSize/2) |y| {
|
|
var biomes: [4]?*const Biome = @splat(null);
|
|
var biomeCounts: [4]u8 = @splat(0);
|
|
var height: i32 = 0;
|
|
for(0..2) |dx| {
|
|
for(0..2) |dy| {
|
|
const curX = x*2 + dx;
|
|
const curY = y*2 + dy;
|
|
height += cur.heightMap[curX][curY];
|
|
const biome = cur.biomeMap[curX][curY];
|
|
for(0..4) |i| {
|
|
if(biomes[i] == biome) {
|
|
biomeCounts[i] += 1;
|
|
break;
|
|
} else if(biomes[i] == null) {
|
|
biomes[i] = biome;
|
|
biomeCounts[i] += 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var bestBiome: *const Biome = biomes[0].?;
|
|
var bestBiomeCount: u8 = biomeCounts[0];
|
|
for(1..4) |i| {
|
|
if(biomeCounts[i] > bestBiomeCount) {
|
|
bestBiomeCount = biomeCounts[i];
|
|
bestBiome = biomes[i].?;
|
|
}
|
|
}
|
|
const nextX = offSetX + x;
|
|
const nextY = offSetY + y;
|
|
next.heightMap[nextX][nextY] = @divFloor(height, 4);
|
|
next.biomeMap[nextX][nextY] = bestBiome;
|
|
}
|
|
}
|
|
next.save(null, .{});
|
|
next.wasStored.store(true, .monotonic);
|
|
if(cur.pos.voxelSize != 1) cur.decreaseRefCount();
|
|
cur = next;
|
|
}
|
|
}
|
|
std.log.info("Finished regenerating map LODs...", .{});
|
|
}
|
|
|
|
pub fn init(_profile: TerrainGenerationProfile) void {
|
|
profile = _profile;
|
|
}
|
|
|
|
pub fn deinit() void {
|
|
cache.clear();
|
|
}
|
|
|
|
/// Call deinit on the result.
|
|
pub fn getOrGenerateFragmentAndIncreaseRefCount(wx: i32, wy: i32, voxelSize: u31) *MapFragment {
|
|
const compare = MapFragmentPosition.init(
|
|
wx & ~@as(i32, MapFragment.mapMask*voxelSize | voxelSize - 1),
|
|
wy & ~@as(i32, MapFragment.mapMask*voxelSize | voxelSize - 1),
|
|
voxelSize,
|
|
);
|
|
const result = cache.findOrCreate(compare, cacheInit, MapFragment.increaseRefCount);
|
|
return result;
|
|
}
|