Add transition biomes and beaches.

could also help with #344 (although that might require some more system changes)
This commit is contained in:
IntegratedQuantum 2024-12-14 18:21:53 +01:00
parent 0f754c44a0
commit 60f5da1724
6 changed files with 211 additions and 34 deletions

View File

@ -36,4 +36,18 @@
.smoothness = 0.2,
},
},
.transitionBiomes = .{
.{
.id = "cubyz:beach",
.chance = 1,
.width = 1,
.properties = .{.land, .inland},
},
.{
.id = "cubyz:ocean_shelf",
.chance = 1,
.width = 2,
.properties = .{.land, .inland},
},
},
}

View File

@ -36,4 +36,18 @@
.smoothness = 0.2,
},
},
.transitionBiomes = .{
.{
.id = "cubyz:beach",
.chance = 1,
.width = 1,
.properties = .{.land, .inland},
},
.{
.id = "cubyz:ocean_shelf",
.chance = 1,
.width = 2,
.properties = .{.land, .inland},
},
},
}

View File

@ -28,4 +28,18 @@
.smoothness = 0.2,
},
},
.transitionBiomes = .{
.{
.id = "cubyz:beach",
.chance = 1,
.width = 1,
.properties = .{.land, .inland},
},
.{
.id = "cubyz:ocean_shelf",
.chance = 1,
.width = 2,
.properties = .{.land, .inland},
},
},
}

View File

@ -17,6 +17,7 @@ pub const BiomeSample = struct {
roughness: f32,
hills: f32,
mountains: f32,
seed: u64,
};
const ClimateMapFragmentPosition = struct {
@ -42,8 +43,10 @@ pub const ClimateMapFragment = struct {
pub const mapSize = 1 << mapShift;
pub const mapMask: i32 = mapSize - 1;
pub const mapEntrysSize = mapSize >> MapFragment.biomeShift;
pos: ClimateMapFragmentPosition,
map: [mapSize >> MapFragment.biomeShift][mapSize >> MapFragment.biomeShift]BiomeSample = undefined,
map: [mapEntrysSize][mapEntrysSize]BiomeSample = undefined,
refCount: Atomic(u16) = .init(0),

View File

@ -160,6 +160,7 @@ fn hashGeneric(input: anytype) u64 {
.pointer => switch(@typeInfo(T).pointer.size) {
.One => blk: {
if(@typeInfo(@typeInfo(T).pointer.child) == .@"fn") break :blk 0;
if(@typeInfo(T).pointer.child == Biome) return hashGeneric(input.id);
if(@typeInfo(T).pointer.child == anyopaque) break :blk 0;
break :blk hashGeneric(input.*);
},
@ -206,21 +207,25 @@ fn u32ToVec3(color: u32) Vec3f {
/// A climate region with special ground, plants and structures.
pub const Biome = struct { // MARK: Biome
const GenerationProperties = packed struct(u8) {
const GenerationProperties = packed struct(u12) {
// pairs of opposite properties. In-between values are allowed.
hot: bool = false,
temperate: bool = false,
cold: bool = false,
inland: bool = false,
land: bool = false,
ocean: bool = false,
wet: bool = false,
neitherWetNorDry: bool = false,
dry: bool = false,
mountain: bool = false,
lowTerrain: bool = false,
antiMountain: bool = false, //???
pub fn fromZon(zon: ZonElement) GenerationProperties {
pub fn fromZon(zon: ZonElement, initMidValues: bool) GenerationProperties {
var result: GenerationProperties = .{};
for(zon.toSlice()) |child| {
const property = child.as([]const u8, "");
@ -230,6 +235,13 @@ pub const Biome = struct { // MARK: Biome
}
}
}
if(initMidValues) {
// Fill all mid values if no value was specified in a group:
const val: u12 = @bitCast(result);
const mask: u12 = 0b001001001001;
const empty = ~val & ~val >> 1 & ~val >> 2 & mask;
result = @bitCast(val | empty << 1);
}
return result;
}
};
@ -260,6 +272,7 @@ pub const Biome = struct { // MARK: Biome
vegetationModels: []SimpleStructureModel = &.{},
stripes: []Stripe = &.{},
subBiomes: main.utils.AliasTable(*const Biome) = .{.items = &.{}, .aliasData = &.{}},
transitionBiomes: []TransitionBiome = &.{},
maxSubBiomeCount: f32,
subBiomeTotalChance: f32 = 0,
preferredMusic: []const u8, // TODO: Support multiple possibilities that are chosen based on time and danger.
@ -272,7 +285,7 @@ pub const Biome = struct { // MARK: Biome
self.* = Biome {
.id = main.globalAllocator.dupe(u8, id),
.paletteId = paletteId,
.properties = GenerationProperties.fromZon(zon.getChild("properties")),
.properties = GenerationProperties.fromZon(zon.getChild("properties"), true),
.isCave = zon.get(bool, "isCave", false),
.radius = (maxRadius + minRadius)/2,
.radiusVariation = (maxRadius - minRadius)/2,
@ -304,6 +317,26 @@ pub const Biome = struct { // MARK: Biome
result.value_ptr.append(main.globalAllocator, .{.biomeId = self.id, .chance = parent.get(f32, "chance", 1)});
}
const transitionBiomeList = zon.getChild("transitionBiomes").toSlice();
if(transitionBiomeList.len != 0) {
const transitionBiomes = main.globalAllocator.alloc(UnfinishedTransitionBiomeData, transitionBiomeList.len);
for(transitionBiomes, transitionBiomeList) |*dst, src| {
dst.* = .{
.biomeId = src.get([]const u8, "id", ""),
.chance = src.get(f32, "chance", 1),
.propertyMask = GenerationProperties.fromZon(src.getChild("properties"), false),
.width = src.get(u8, "width", 2),
};
// Fill all unspecified property groups:
var properties: u12 = @bitCast(dst.propertyMask);
const mask: u12 = 0b001001001001;
const empty = ~properties & ~properties >> 1 & ~properties >> 2 & mask;
properties |= empty | empty << 1 | empty << 2;
dst.propertyMask = @bitCast(properties);
}
unfinishedTransitionBiomes.put(main.globalAllocator.allocator, self.id, transitionBiomes) catch unreachable;
}
self.structure = BlockStructure.init(main.globalAllocator, zon.getChild("ground_structure"));
const structures = zon.getChild("structures");
@ -333,6 +366,7 @@ pub const Biome = struct { // MARK: Biome
pub fn deinit(self: *Biome) void {
self.subBiomes.deinit(main.globalAllocator);
self.structure.deinit(main.globalAllocator);
main.globalAllocator.free(self.transitionBiomes);
main.globalAllocator.free(self.vegetationModels);
main.globalAllocator.free(self.stripes);
main.globalAllocator.free(self.preferredMusic);
@ -438,16 +472,16 @@ pub const TreeNode = union(enum) { // MARK: TreeNode
var chanceMiddle: f32 = 0;
var chanceUpper: f32 = 0;
for(currentSlice) |*biome| {
var properties: u32 = @as(u8, @bitCast(biome.properties));
var properties: u32 = @as(u12, @bitCast(biome.properties));
properties >>= parameterShift;
properties = properties & 3;
if(properties == 0) {
chanceMiddle += biome.chance;
} else if(properties == 1) {
properties = properties & 7;
if(properties == 1) {
chanceLower += biome.chance;
} else if(properties == 2) {
} else if(properties == 4) {
chanceUpper += biome.chance;
} else unreachable;
} else {
chanceMiddle += biome.chance;
}
}
const totalChance = chanceLower + chanceMiddle + chanceUpper;
chanceLower /= totalChance;
@ -476,10 +510,10 @@ pub const TreeNode = union(enum) { // MARK: TreeNode
list.deinit(main.stackAllocator);
};
for(currentSlice) |biome| {
var properties: u32 = @as(u8, @bitCast(biome.properties));
var properties: u32 = @as(u12, @bitCast(biome.properties));
properties >>= parameterShift;
const valueMap = [_]usize{1, 0, 2, 1};
lists[valueMap[properties & 3]].appendAssumeCapacity(biome);
const valueMap = [8]usize{1, 0, 1, 1, 2, 1, 1, 1};
lists[valueMap[properties & 7]].appendAssumeCapacity(biome);
}
lowerIndex = lists[0].items.len;
@memcpy(currentSlice[0..lowerIndex], lists[0].items);
@ -488,9 +522,9 @@ pub const TreeNode = union(enum) { // MARK: TreeNode
@memcpy(currentSlice[upperIndex..], lists[2].items);
}
self.branch.children[0] = TreeNode.init(allocator, currentSlice[0..lowerIndex], parameterShift+2);
self.branch.children[1] = TreeNode.init(allocator, currentSlice[lowerIndex..upperIndex], parameterShift+2);
self.branch.children[2] = TreeNode.init(allocator, currentSlice[upperIndex..], parameterShift+2);
self.branch.children[0] = TreeNode.init(allocator, currentSlice[0..lowerIndex], parameterShift+3);
self.branch.children[1] = TreeNode.init(allocator, currentSlice[lowerIndex..upperIndex], parameterShift+3);
self.branch.children[2] = TreeNode.init(allocator, currentSlice[upperIndex..], parameterShift+3);
return self;
}
@ -538,6 +572,7 @@ var biomes: main.List(Biome) = undefined;
var caveBiomes: main.List(Biome) = undefined;
var biomesById: std.StringHashMap(*Biome) = undefined;
pub var byTypeBiomes: *TreeNode = undefined;
const UnfinishedSubBiomeData = struct {
biomeId: []const u8,
chance: f32,
@ -547,6 +582,20 @@ const UnfinishedSubBiomeData = struct {
};
var unfinishedSubBiomes: std.StringHashMapUnmanaged(main.ListUnmanaged(UnfinishedSubBiomeData)) = .{};
const UnfinishedTransitionBiomeData = struct {
biomeId: []const u8,
chance: f32,
propertyMask: Biome.GenerationProperties,
width: u8,
};
const TransitionBiome = struct {
biome: *const Biome,
chance: f32,
propertyMask: Biome.GenerationProperties,
width: u8,
};
var unfinishedTransitionBiomes: std.StringHashMapUnmanaged([]UnfinishedTransitionBiomeData) = .{};
pub fn init() void {
biomes = .init(main.globalAllocator);
caveBiomes = .init(main.globalAllocator);
@ -631,6 +680,32 @@ pub fn finishLoading() void {
subBiomeDataList.deinit(main.globalAllocator);
}
unfinishedSubBiomes.clearAndFree(main.globalAllocator.allocator);
var transitionBiomeIterator = unfinishedTransitionBiomes.iterator();
while(transitionBiomeIterator.next()) |transitionBiomeData| {
const parentBiome = biomesById.get(transitionBiomeData.key_ptr.*) orelse unreachable;
const transitionBiomes = transitionBiomeData.value_ptr.*;
parentBiome.transitionBiomes = main.globalAllocator.alloc(TransitionBiome, transitionBiomes.len);
for(parentBiome.transitionBiomes, transitionBiomes) |*res, src| {
res.* = .{
.biome = biomesById.get(src.biomeId) orelse {
std.log.warn("Skipping transition biome with unknown id {s}", .{src.biomeId});
res.* = .{
.biome = &biomes.items[0],
.chance = 0,
.propertyMask = .{},
.width = 0,
};
continue;
},
.chance = src.chance,
.propertyMask = src.propertyMask,
.width = src.width,
};
}
main.globalAllocator.free(transitionBiomes);
}
unfinishedTransitionBiomes.clearAndFree(main.globalAllocator.allocator);
}
pub fn hasRegistered(id: []const u8) bool {

View File

@ -188,13 +188,13 @@ const GenerationStructure = struct {
pub fn init(allocator: NeverFailingAllocator, wx: i32, wy: i32, width: u31, height: u31, tree: *TreeNode, worldSeed: u64) GenerationStructure {
const self: GenerationStructure = .{
.chunks = Array2D(*Chunk).init(allocator, 2 + @divExact(width, chunkSize), 2 + @divExact(height, chunkSize)),
.chunks = Array2D(*Chunk).init(allocator, 4 + @divExact(width, chunkSize), 4 + @divExact(height, chunkSize)),
};
var x: u31 = 0;
while(x < self.chunks.width) : (x += 1) {
var y: u31 = 0;
while(y < self.chunks.height) : (y += 1) {
self.chunks.ptr(x, y).* = Chunk.init(allocator, tree, worldSeed, wx +% x*chunkSize -% chunkSize, wy +% y*chunkSize -% chunkSize);
self.chunks.ptr(x, y).* = Chunk.init(allocator, tree, worldSeed, wx +% x*chunkSize -% 2*chunkSize, wy +% y*chunkSize -% 2*chunkSize);
}
}
return self;
@ -207,7 +207,7 @@ const GenerationStructure = struct {
self.chunks.deinit(allocator);
}
fn findClosestBiomeTo(self: GenerationStructure, wx: i32, wy: i32, relX: u31, relY: u31) BiomeSample {
fn findClosestBiomeTo(self: GenerationStructure, wx: i32, wy: i32, relX: i32, relY: i32, worldSeed: u64) BiomeSample {
const x = wx +% relX*terrain.SurfaceMap.MapFragment.biomeSize;
const y = wy +% relY*terrain.SurfaceMap.MapFragment.biomeSize;
var closestDist = std.math.floatMax(f32);
@ -218,15 +218,15 @@ const GenerationStructure = struct {
var hills: f32 = 0;
var mountains: f32 = 0;
var totalWeight: f32 = 0;
const cellX: i32 = relX/(chunkSize/terrain.SurfaceMap.MapFragment.biomeSize);
const cellY: i32 = relY/(chunkSize/terrain.SurfaceMap.MapFragment.biomeSize);
const cellX: i32 = @divFloor(relX, (chunkSize/terrain.SurfaceMap.MapFragment.biomeSize));
const cellY: i32 = @divFloor(relY, (chunkSize/terrain.SurfaceMap.MapFragment.biomeSize));
// Note that at a small loss of details we can assume that all BiomePoints are withing ±1 chunks of the current one.
var dx: i32 = 0;
while(dx <= 2) : (dx += 1) {
var dx: i32 = 1;
while(dx <= 3) : (dx += 1) {
const totalX = cellX + dx;
if(totalX < 0 or totalX >= self.chunks.width) continue;
var dy: i32 = 0;
while(dy <= 2) : (dy += 1) {
var dy: i32 = 1;
while(dy <= 3) : (dy += 1) {
const totalY = cellY + dy;
if(totalY < 0 or totalY >= self.chunks.height) continue;
const chunk = self.chunks.get(@intCast(totalX), @intCast(totalY));
@ -242,7 +242,7 @@ const GenerationStructure = struct {
}
weight *= weight;
// The important bit is the ocean height, that's the only point where we actually need the transition point to be exact for beaches to occur.
weight /= @abs(biomePoint.height - 16);
weight /= @abs(biomePoint.height - 12);
height += biomePoint.height*weight;
roughness += biomePoint.biome.roughness*weight;
hills += biomePoint.biome.hills*weight;
@ -265,6 +265,7 @@ const GenerationStructure = struct {
.roughness = roughness/totalWeight,
.hills = hills/totalWeight,
.mountains = mountains/totalWeight,
.seed = random.initSeed2D(worldSeed, closestBiomePoint.pos),
};
}
@ -279,12 +280,14 @@ const GenerationStructure = struct {
while(y < max[1]) : (y += 1) {
const distSquare = vec.lengthSquare(Vec2f{x, y} - relPos);
if(distSquare < relRadius*relRadius) {
var seed = map.map[@intFromFloat(x)][@intFromFloat(y)].seed;
map.map[@intFromFloat(x)][@intFromFloat(y)] = .{
.biome = biome,
.roughness = biome.roughness,
.hills = biome.hills,
.mountains = biome.mountains,
.height = (@as(f32, @floatFromInt(biome.minHeight)) + @as(f32, @floatFromInt(biome.maxHeight)))/2, // TODO: Randomize
.height = @as(f32, @floatFromInt(biome.minHeight)) + @as(f32, @floatFromInt(biome.maxHeight - biome.minHeight))*random.nextFloat(&seed),
.seed = map.map[@intFromFloat(x)][@intFromFloat(y)].seed,
};
}
}
@ -323,14 +326,68 @@ const GenerationStructure = struct {
}
}
pub fn toMap(self: GenerationStructure, map: *ClimateMapFragment, width: u31, height: u31, worldSeed: u64) void {
var x: u31 = 0;
while(x < width/terrain.SurfaceMap.MapFragment.biomeSize) : (x += 1) {
var y: u31 = 0;
while(y < height/terrain.SurfaceMap.MapFragment.biomeSize) : (y += 1) {
map.map[x][y] = self.findClosestBiomeTo(map.pos.wx, map.pos.wy, x, y);
fn addTransitionBiomes(comptime size: usize, comptime margin: usize, map: *[size][size]BiomeSample) void {
const neighborData = main.stackAllocator.create([16][size][size]u12);
defer main.stackAllocator.free(neighborData);
for(0..size) |x| {
for(0..size) |y| {
neighborData[0][x][y] = @bitCast(map[x][y].biome.properties);
}
}
for(1..neighborData.len) |i| {
for(1..size - 1) |x| {
for(1..size - 1) |y| {
neighborData[i][x][y] = neighborData[i-1][x][y] | neighborData[i-1][x-1][y] | neighborData[i-1][x+1][y] | neighborData[i-1][x][y-1] | neighborData[i-1][x][y+1];
}
}
}
for(margin..size - margin) |x| {
for(margin..size - margin) |y| {
const point = map[x][y];
if(point.biome.transitionBiomes.len == 0) {
std.debug.assert(!std.mem.eql(u8, "cubyz:ocean", point.biome.id));
continue;
}
var seed = point.seed;
for(point.biome.transitionBiomes) |transitionBiome| {
const biomeMask: u12 = @bitCast(transitionBiome.propertyMask);
const neighborMask = neighborData[@min(neighborData.len - 1, transitionBiome.width)][x][y];
// Check if all triplets have a matching entry:
const mask: u12 = 0b001001001001;
var result = biomeMask & neighborMask;
result = (result | result >> 1 | result >> 2);
if(result & mask == mask) {
if(random.nextFloat(&seed) < transitionBiome.chance) {
map[x][y] = .{
.biome = transitionBiome.biome,
.roughness = transitionBiome.biome.roughness,
.hills = transitionBiome.biome.hills,
.mountains = transitionBiome.biome.mountains,
.height = @as(f32, @floatFromInt(transitionBiome.biome.minHeight)) + @as(f32, @floatFromInt(transitionBiome.biome.maxHeight - transitionBiome.biome.minHeight))*random.nextFloat(&seed),
.seed = map[x][y].seed,
};
break;
}
}
}
}
}
}
pub fn toMap(self: GenerationStructure, map: *ClimateMapFragment, width: u31, height: u31, worldSeed: u64) void {
const margin: u31 = chunkSize >> terrain.SurfaceMap.MapFragment.biomeShift;
var preMap: [ClimateMapFragment.mapEntrysSize + 2*margin][ClimateMapFragment.mapEntrysSize + 2*margin]BiomeSample = undefined;
var x: i32 = -@as(i32, margin);
while(x < width/terrain.SurfaceMap.MapFragment.biomeSize + margin) : (x += 1) {
var y: i32 = -@as(i32, margin);
while(y < height/terrain.SurfaceMap.MapFragment.biomeSize + margin) : (y += 1) {
preMap[@intCast(x + margin)][@intCast(y + margin)] = self.findClosestBiomeTo(map.pos.wx, map.pos.wy, x, y, worldSeed);
}
}
addTransitionBiomes(ClimateMapFragment.mapEntrysSize + 2*margin, margin, &preMap);
for(0..ClimateMapFragment.mapEntrysSize) |_x| {
@memcpy(&map.map[_x], preMap[_x + margin][margin..][0..ClimateMapFragment.mapEntrysSize]);
}
// Add some sub-biomes:
var extraBiomes = main.List(BiomePoint).init(main.stackAllocator);