From 32a9541aad025e509e970a00fd441490d0b420d3 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Sun, 5 Jul 2026 23:48:29 +0100 Subject: [PATCH] refactor(worldgen): extract overworld lod helpers --- .../worldgen-overworld/src/lod_coloring.zig | 94 +++++ .../src/overworld_generator.zig | 383 ++---------------- modules/worldgen-overworld/src/root.zig | 2 + modules/worldgen-overworld/src/tree_hints.zig | 287 +++++++++++++ 4 files changed, 411 insertions(+), 355 deletions(-) create mode 100644 modules/worldgen-overworld/src/lod_coloring.zig create mode 100644 modules/worldgen-overworld/src/tree_hints.zig diff --git a/modules/worldgen-overworld/src/lod_coloring.zig b/modules/worldgen-overworld/src/lod_coloring.zig new file mode 100644 index 00000000..cd5701ca --- /dev/null +++ b/modules/worldgen-overworld/src/lod_coloring.zig @@ -0,0 +1,94 @@ +//! LOD surface color selection for overworld biome samples. + +const std = @import("std"); +const world_core = @import("world-core"); +const BlockType = world_core.BlockType; +const BiomeId = world_core.BiomeId; +const block_registry = world_core.block_registry; + +pub fn colorForSample(biome_id: BiomeId, surface_block: BlockType) u32 { + return switch (surface_block) { + .air => 0, + .grass => packFloatColor(grassTint(biome_id)), + .water => packFloatColor(waterTint(biome_id)), + .leaves, .mangrove_leaves, .jungle_leaves, .acacia_leaves, .birch_leaves, .spruce_leaves => packFloatColor(foliageTint(biome_id)), + else => packBlockColor(surface_block), + }; +} + +pub fn grassTint(biome_id: BiomeId) [3]f32 { + return switch (biome_id) { + .forest => .{ 0.18, 0.64, 0.16 }, + .birch_forest => .{ 0.24, 0.68, 0.18 }, + .dark_forest => .{ 0.12, 0.46, 0.12 }, + .flower_forest => .{ 0.30, 0.72, 0.18 }, + .taiga => .{ 0.24, 0.56, 0.24 }, + .snowy_taiga => .{ 0.62, 0.74, 0.70 }, + .old_growth_taiga => .{ 0.20, 0.48, 0.28 }, + .desert => .{ 0.75, 0.70, 0.35 }, + .snow_tundra, .snowy_beach, .frozen_ocean, .frozen_river => .{ 0.7, 0.75, 0.8 }, + .snowy_mountains => .{ 0.85, 0.90, 0.95 }, + .meadow => .{ 0.32, 0.74, 0.24 }, + .grove => .{ 0.20, 0.48, 0.24 }, + .snowy_slopes => .{ 0.82, 0.88, 0.94 }, + .jagged_peaks => .{ 0.56, 0.56, 0.54 }, + .frozen_peaks => .{ 0.76, 0.88, 0.96 }, + .stony_peaks => .{ 0.62, 0.58, 0.48 }, + .swamp => .{ 0.26, 0.58, 0.18 }, + .jungle => .{ 0.10, 0.76, 0.08 }, + .savanna => .{ 0.55, 0.55, 0.30 }, + .badlands => .{ 0.5, 0.4, 0.3 }, + .mushroom_fields => .{ 0.4, 0.8, 0.4 }, + .foothills => .{ 0.24, 0.62, 0.22 }, + .dry_plains => .{ 0.55, 0.50, 0.28 }, + .coastal_plains => .{ 0.24, 0.66, 0.24 }, + .stony_shore => .{ 0.48, 0.52, 0.50 }, + else => .{ 0.22, 0.72, 0.16 }, + }; +} + +pub fn foliageTint(biome_id: BiomeId) [3]f32 { + return switch (biome_id) { + .forest => .{ 0.12, 0.52, 0.12 }, + .birch_forest => .{ 0.18, 0.58, 0.14 }, + .dark_forest => .{ 0.08, 0.36, 0.08 }, + .flower_forest => .{ 0.20, 0.58, 0.12 }, + .taiga => .{ 0.18, 0.46, 0.18 }, + .meadow => .{ 0.20, 0.58, 0.18 }, + .grove => .{ 0.16, 0.38, 0.18 }, + .snowy_slopes => .{ 0.64, 0.72, 0.70 }, + .jagged_peaks => .{ 0.44, 0.46, 0.42 }, + .frozen_peaks => .{ 0.68, 0.78, 0.82 }, + .stony_peaks => .{ 0.46, 0.44, 0.34 }, + .snowy_taiga => .{ 0.16, 0.40, 0.24 }, + .old_growth_taiga => .{ 0.12, 0.34, 0.20 }, + .swamp => .{ 0.22, 0.52, 0.16 }, + .jungle => .{ 0.08, 0.62, 0.08 }, + .savanna => .{ 0.50, 0.50, 0.28 }, + .foothills => .{ 0.18, 0.50, 0.16 }, + .coastal_plains => .{ 0.18, 0.52, 0.16 }, + else => .{ 0.14, 0.58, 0.12 }, + }; +} + +pub fn waterTint(biome_id: BiomeId) [3]f32 { + return switch (biome_id) { + .deep_ocean => .{ 0.1, 0.2, 0.5 }, + .frozen_ocean, .frozen_river => .{ 0.32, 0.54, 0.74 }, + .cold_ocean, .stony_shore, .snowy_beach => .{ 0.10, 0.34, 0.62 }, + .swamp => .{ 0.16, 0.38, 0.30 }, + else => .{ 0.12, 0.38, 0.78 }, + }; +} + +pub fn packFloatColor(color: [3]f32) u32 { + const r: u32 = @intFromFloat(@round(std.math.clamp(color[0], 0.0, 1.0) * 255.0)); + const g: u32 = @intFromFloat(@round(std.math.clamp(color[1], 0.0, 1.0) * 255.0)); + const b: u32 = @intFromFloat(@round(std.math.clamp(color[2], 0.0, 1.0) * 255.0)); + return (r << 16) | (g << 8) | b; +} + +fn packBlockColor(block_type: BlockType) u32 { + const color = block_registry.getBlockDefinition(block_type).default_color; + return packFloatColor(color); +} diff --git a/modules/worldgen-overworld/src/overworld_generator.zig b/modules/worldgen-overworld/src/overworld_generator.zig index 2425efca..e7bf354b 100644 --- a/modules/worldgen-overworld/src/overworld_generator.zig +++ b/modules/worldgen-overworld/src/overworld_generator.zig @@ -17,12 +17,10 @@ const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; const BlockType = world_core.BlockType; -const block_registry = world_core.block_registry; const LODLevel = world_core.LODLevel; const LODSimplifiedData = world_core.LODSimplifiedData; const regionSizeBlocks = world_core.regionSizeBlocks; const DecorationProvider = @import("decoration_provider.zig").DecorationProvider; -const decoration_registry = @import("decoration_registry.zig"); const gen_region = @import("gen_region.zig"); const ClassificationCache = gen_region.ClassificationCache; const gen_interface = @import("generator_interface.zig"); @@ -39,7 +37,8 @@ const SurfaceBuilder = terrain_shape_mod.SurfaceBuilder; const CoastalSurfaceType = terrain_shape_mod.CoastalSurfaceType; const BiomeSource = @import("biome.zig").BiomeSource; const BiomeDecorator = @import("biome_decorator.zig").BiomeDecorator; -const tree_registry = @import("tree_registry.zig"); +const lod_coloring = @import("lod_coloring.zig"); +const tree_hints = @import("tree_hints.zig"); const LightingComputer = @import("worldgen-common").LightingComputer; const Mutex = sync.Mutex; @@ -310,7 +309,7 @@ pub const OverworldGenerator = struct { render_water_surface: bool, }; - const TreeHintChunk = [CHUNK_SIZE_X * CHUNK_SIZE_Z]world_core.LODVegetationHint; + const TreeHintChunk = tree_hints.TreeHintChunk; const TreeHintCache = std.AutoHashMap(u64, TreeHintChunk); fn sampleRepresentativeLODColumn(self: *const OverworldGenerator, wx: f32, wz: f32, cell_span: f32, sea_level: i32, controls: region_pkg.RegionControlCorners, tree_hint_cache: *TreeHintCache, lod_level: LODLevel) RepresentativeLODColumn { @@ -352,7 +351,7 @@ pub const OverworldGenerator = struct { if (block_index < block_counts.len) block_counts[block_index] += 1; biome_counts[@intFromEnum(sample.biome)] += 1; - const color = lodColorForSample(sample.biome, sample.surface_block); + const color = lod_coloring.colorForSample(sample.biome, sample.surface_block); color_r += (color >> 16) & 0xFF; color_g += (color >> 8) & 0xFF; color_b += color & 0xFF; @@ -389,7 +388,7 @@ pub const OverworldGenerator = struct { else if (compute_exact_tree_hints) self.actualTreeHintInArea(wx, wz, sample_radius, dominant_biome, tree_hint_cache) else - self.estimatedTreeHintForBiome(dominant_biome, wx, wz); + tree_hints.estimatedTreeHintForBiome(dominant_biome, wx, wz, self.terrain_shape.getSeed()); return .{ .height = height, .biome = dominant_biome, @@ -406,18 +405,13 @@ pub const OverworldGenerator = struct { }; } - const TreeBlocks = struct { - trunk: BlockType, - leaves: BlockType, - }; - fn actualTreeHintAtColumn(self: *const OverworldGenerator, target_wx: i32, target_wz: i32, cache: *TreeHintCache) world_core.LODVegetationHint { const chunk_x = @divFloor(target_wx, @as(i32, @intCast(CHUNK_SIZE_X))); const chunk_z = @divFloor(target_wz, @as(i32, @intCast(CHUNK_SIZE_Z))); const local_x: u32 = @intCast(@mod(target_wx, @as(i32, @intCast(CHUNK_SIZE_X)))); const local_z: u32 = @intCast(@mod(target_wz, @as(i32, @intCast(CHUNK_SIZE_Z)))); const idx = local_x + local_z * CHUNK_SIZE_X; - const key = treeHintCacheKey(chunk_x, chunk_z); + const key = tree_hints.cacheKey(chunk_x, chunk_z); if (cache.get(key)) |hints| return hints[idx]; @@ -449,7 +443,7 @@ pub const OverworldGenerator = struct { while (chunk_z <= max_chunk_z) : (chunk_z += 1) { var chunk_x = min_chunk_x; while (chunk_x <= max_chunk_x) : (chunk_x += 1) { - const key = treeHintCacheKey(chunk_x, chunk_z); + const key = tree_hints.cacheKey(chunk_x, chunk_z); const hints = blk: { if (cache.get(key)) |cached| break :blk cached; const computed = self.computeChunkTreeHints(chunk_x, chunk_z); @@ -483,7 +477,7 @@ pub const OverworldGenerator = struct { if (tree_count == 0) return world_core.LODVegetationHint.empty; - const blocks = if (best.leaves == .air) treeBlocksForBiome(dominant_biome) else TreeBlocks{ .trunk = best.trunk, .leaves = best.leaves }; + const blocks = if (best.leaves == .air) tree_hints.blocksForBiome(dominant_biome) else tree_hints.TreeBlocks{ .trunk = best.trunk, .leaves = best.leaves }; const area = @max(1.0, (radius * 2.0 + 1.0) * (radius * 2.0 + 1.0)); return .{ .tree_coverage = std.math.clamp(@as(f32, @floatFromInt(tree_count)) / area * 24.0, 0.0, 1.0), @@ -495,326 +489,30 @@ pub const OverworldGenerator = struct { }; } - fn estimatedTreeHintForBiome(self: *const OverworldGenerator, biome_id: BiomeId, wx: f32, wz: f32) world_core.LODVegetationHint { - const tree_types = biome_mod.getBiomeDefinition(biome_id).vegetation.tree_types; - if (tree_types.len == 0) return world_core.LODVegetationHint.empty; - - var best_type = tree_types[0]; - var best_probability: f32 = 0.0; - var probability_sum: f32 = 0.0; - for (tree_types) |tree_type| { - const def = tree_registry.getTreeDefinition(tree_type) orelse continue; - probability_sum += def.probability; - if (def.probability > best_probability) { - best_probability = def.probability; - best_type = tree_type; - } - } - - const base_coverage = std.math.clamp(probability_sum * 7.5, 0.0, 1.0); - if (base_coverage < 0.035) return world_core.LODVegetationHint.empty; - - const wx_i: i32 = @intFromFloat(@floor(wx)); - const wz_i: i32 = @intFromFloat(@floor(wz)); - const jitter = hashUnit2D(wx_i, wz_i, self.terrain_shape.getSeed()); - const placement_roll = hashUnit2D(wx_i, wz_i, self.terrain_shape.getSeed() ^ 0x6D2B79F5); - const placement_probability = std.math.clamp(base_coverage * 0.32, 0.025, 0.55); - if (placement_roll > placement_probability) return world_core.LODVegetationHint.empty; - - const blocks = treeBlocksForType(best_type); - return .{ - .tree_coverage = std.math.clamp(0.12 + base_coverage * (0.18 + jitter * 0.08), 0.0, 0.45), - .avg_tree_height = treeHeightForType(best_type), - .offset_x = (hashUnit2D(wx_i, wz_i, self.terrain_shape.getSeed() ^ 0xA53A9D13) - 0.5) * 2.0, - .offset_z = (hashUnit2D(wx_i, wz_i, self.terrain_shape.getSeed() ^ 0xC2B2AE35) - 0.5) * 2.0, - .trunk = blocks.trunk, - .leaves = blocks.leaves, - }; - } - - fn hashUnit2D(x: i32, z: i32, seed: u64) f32 { - var h = seed; - h ^= @as(u64, @bitCast(@as(i64, x))) *% 0x9E3779B97F4A7C15; - h ^= @as(u64, @bitCast(@as(i64, z))) *% 0xC2B2AE3D27D4EB4F; - h ^= h >> 33; - h *%= 0xFF51AFD7ED558CCD; - h ^= h >> 33; - const bucket: u32 = @intCast(h & 0xFFFF); - return @as(f32, @floatFromInt(bucket)) / 65535.0; - } - fn computeChunkTreeHints(self: *const OverworldGenerator, chunk_x: i32, chunk_z: i32) TreeHintChunk { const world_x = chunk_x * @as(i32, @intCast(CHUNK_SIZE_X)); const world_z = chunk_z * @as(i32, @intCast(CHUNK_SIZE_Z)); - const sea_level = self.terrain_shape.getSeaLevel(); - const controls = region_pkg.RegionControlCorners.init( - self.terrain_shape.getRegionSeed(), - world_x, - world_z, - world_x + @as(i32, @intCast(CHUNK_SIZE_X)) - 1, - world_z + @as(i32, @intCast(CHUNK_SIZE_Z)) - 1, - ); - var prng = std.Random.DefaultPrng.init(self.biome_decorator.region_seed ^ @as(u64, @bitCast(@as(i64, chunk_x))) ^ (@as(u64, @bitCast(@as(i64, chunk_z))) << 32)); - const random = prng.random(); - var tree_occupancy = [_]bool{false} ** world_core.CHUNK_VOLUME; - var hints = [_]world_core.LODVegetationHint{world_core.LODVegetationHint.empty} ** (CHUNK_SIZE_X * CHUNK_SIZE_Z); - - var lz: u32 = 0; - while (lz < CHUNK_SIZE_Z) : (lz += 1) { - var lx: u32 = 0; - while (lx < CHUNK_SIZE_X) : (lx += 1) { - const wx_i = world_x + @as(i32, @intCast(lx)); - const wz_i = world_z + @as(i32, @intCast(lz)); - const sample = self.classifyLODSample(@floatFromInt(wx_i), @floatFromInt(wz_i), sea_level, controls); - const column_controls = controls.sample(wx_i, wz_i); - const variant = self.terrain_shape.getNoiseSampler().variant_noise.get2D(@floatFromInt(wx_i), @floatFromInt(wz_i)); - _ = decoration_registry.chooseStaticSimpleDecoration(sample.biome, sample.surface_block, variant, column_controls.subbiome_mask > 0.5, column_controls.vegetation_mult, 0, random); - - const placed_tree = choosePlacedTree(sample.biome, sample.surface_block, variant, column_controls.subbiome_mask > 0.5, column_controls.vegetation_mult, lx, lz, sample.terrain_height_i, &tree_occupancy, random); - if (placed_tree) |tree| { - placeTreeOccupancy(tree.schematic, lx, @intCast(sample.terrain_height_i + 1), lz, &tree_occupancy, random); - hints[lx + lz * CHUNK_SIZE_X] = treeHintForType(tree.tree_type); - } - } - } - return hints; - } - - fn treeHintCacheKey(chunk_x: i32, chunk_z: i32) u64 { - const x: u64 = @bitCast(@as(i64, chunk_x)); - const z: u64 = @bitCast(@as(i64, chunk_z)); - return (x *% 0x9E3779B97F4A7C15) ^ (z *% 0xC2B2AE3D27D4EB4F); - } - - const PlacedTree = struct { - tree_type: tree_registry.TreeType, - schematic: @import("schematics.zig").Schematic, - }; - - fn choosePlacedTree( - biome_id: BiomeId, - surface_block: BlockType, - variant: f32, - allow_subbiomes: bool, - veg_mult: f32, - local_x: u32, - local_z: u32, - surface_y: i32, - tree_occupancy: *const [world_core.CHUNK_VOLUME]bool, - random: std.Random, - ) ?PlacedTree { - const vegetation = biome_mod.getBiomeDefinition(biome_id).vegetation; - if (vegetation.tree_types.len == 0) return null; - - for (vegetation.tree_types) |tree_type| { - const tree_def = tree_registry.getTreeDefinition(tree_type) orelse continue; - if (!treeSurfaceAllowed(tree_def.place_on, surface_block)) continue; - if (!variantAllowed(variant, allow_subbiomes, tree_def.variant_min, tree_def.variant_max)) continue; - const prob = @min(1.0, tree_def.probability * veg_mult); - if (random.float(f32) >= prob) continue; - if (tree_def.spacing_radius > 0 and !isTreeAreaClear(tree_occupancy, @intCast(local_x), surface_y, @intCast(local_z), tree_def.spacing_radius)) continue; - return .{ - .tree_type = tree_type, - .schematic = tree_def.schematic, - }; - } - return null; - } - - fn variantAllowed(variant: f32, allow_subbiomes: bool, min: f32, max: f32) bool { - if (allow_subbiomes) return variant >= min and variant <= max; - return min == -1.0 and max == 1.0; - } - - fn treeSurfaceAllowed(place_on: []const BlockType, surface_block: BlockType) bool { - for (place_on) |valid| { - if (surface_block == valid) return true; - } - return false; - } - - fn isTreeAreaClear(tree_occupancy: *const [world_core.CHUNK_VOLUME]bool, x: i32, y: i32, z: i32, radius: i32) bool { - var dz: i32 = -radius; - while (dz <= radius) : (dz += 1) { - var dx: i32 = -radius; - while (dx <= radius) : (dx += 1) { - if (dx == 0 and dz == 0) continue; - const check_x = x + dx; - const check_z = z + dz; - if (check_x >= 0 and check_x < CHUNK_SIZE_X and check_z >= 0 and check_z < CHUNK_SIZE_Z) { - var dy: i32 = 1; - while (dy <= 3) : (dy += 1) { - const check_y = y + dy; - if (check_y >= 0 and check_y < CHUNK_SIZE_Y) { - const idx: usize = @intCast(@as(u32, @intCast(check_x)) + @as(u32, @intCast(check_z)) * CHUNK_SIZE_X + @as(u32, @intCast(check_y)) * CHUNK_SIZE_X * CHUNK_SIZE_Z); - if (tree_occupancy[idx]) return false; - } - } - } - } - } - return true; - } - - fn placeTreeOccupancy(schematic: @import("schematics.zig").Schematic, x: u32, y: u32, z: u32, tree_occupancy: *[world_core.CHUNK_VOLUME]bool, random: std.Random) void { - const center_x = @as(i32, @intCast(x)); - const center_y = @as(i32, @intCast(y)); - const center_z = @as(i32, @intCast(z)); - for (schematic.blocks) |block| { - if (block.probability < 1.0) { - if (random.float(f32) >= block.probability) continue; - } - const bx = center_x + block.offset[0] - schematic.center_x; - const by = center_y + block.offset[1]; - const bz = center_z + block.offset[2] - schematic.center_z; - if (bx >= 0 and bx < CHUNK_SIZE_X and bz >= 0 and bz < CHUNK_SIZE_Z and by >= 0 and by < CHUNK_SIZE_Y and isTreeBlock(block.block)) { - const idx: usize = @intCast(@as(u32, @intCast(bx)) + @as(u32, @intCast(bz)) * CHUNK_SIZE_X + @as(u32, @intCast(by)) * CHUNK_SIZE_X * CHUNK_SIZE_Z); - tree_occupancy[idx] = true; - } - } - } - - fn isTreeBlock(block: BlockType) bool { - return switch (block) { - .wood, - .leaves, - .birch_log, - .birch_leaves, - .spruce_log, - .spruce_leaves, - .jungle_log, - .jungle_leaves, - .acacia_log, - .acacia_leaves, - .mangrove_log, - .mangrove_leaves, - => true, - else => false, - }; - } - - fn treeHintForType(tree_type: tree_registry.TreeType) world_core.LODVegetationHint { - const blocks = treeBlocksForType(tree_type); + return tree_hints.computeChunkTreeHints(.{ + .chunk_x = chunk_x, + .chunk_z = chunk_z, + .world_x = world_x, + .world_z = world_z, + .sea_level = self.terrain_shape.getSeaLevel(), + .terrain_region_seed = self.terrain_shape.getRegionSeed(), + .decorator_region_seed = self.biome_decorator.region_seed, + .noise_sampler = self.terrain_shape.getNoiseSampler(), + .classify_context = self, + .classify_fn = classifyTreeHintSample, + }); + } + + fn classifyTreeHintSample(context: *const anyopaque, wx: f32, wz: f32, sea_level: i32, controls: region_pkg.RegionControlCorners) tree_hints.ClassifiedSample { + const self: *const OverworldGenerator = @ptrCast(@alignCast(context)); + const sample = self.classifyLODSample(wx, wz, sea_level, controls); return .{ - .tree_coverage = 1.0, - .avg_tree_height = treeHeightForType(tree_type), - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = blocks.trunk, - .leaves = blocks.leaves, - }; - } - - fn biomeGrassTint(biome_id: BiomeId) [3]f32 { - return switch (biome_id) { - .forest => .{ 0.18, 0.64, 0.16 }, - .birch_forest => .{ 0.24, 0.68, 0.18 }, - .dark_forest => .{ 0.12, 0.46, 0.12 }, - .flower_forest => .{ 0.30, 0.72, 0.18 }, - .taiga => .{ 0.24, 0.56, 0.24 }, - .snowy_taiga => .{ 0.62, 0.74, 0.70 }, - .old_growth_taiga => .{ 0.20, 0.48, 0.28 }, - .desert => .{ 0.75, 0.70, 0.35 }, - .snow_tundra, .snowy_beach, .frozen_ocean, .frozen_river => .{ 0.7, 0.75, 0.8 }, - .snowy_mountains => .{ 0.85, 0.90, 0.95 }, - .meadow => .{ 0.32, 0.74, 0.24 }, - .grove => .{ 0.20, 0.48, 0.24 }, - .snowy_slopes => .{ 0.82, 0.88, 0.94 }, - .jagged_peaks => .{ 0.56, 0.56, 0.54 }, - .frozen_peaks => .{ 0.76, 0.88, 0.96 }, - .stony_peaks => .{ 0.62, 0.58, 0.48 }, - .swamp => .{ 0.26, 0.58, 0.18 }, - .jungle => .{ 0.10, 0.76, 0.08 }, - .savanna => .{ 0.55, 0.55, 0.30 }, - .badlands => .{ 0.5, 0.4, 0.3 }, - .mushroom_fields => .{ 0.4, 0.8, 0.4 }, - .foothills => .{ 0.24, 0.62, 0.22 }, - .dry_plains => .{ 0.55, 0.50, 0.28 }, - .coastal_plains => .{ 0.24, 0.66, 0.24 }, - .stony_shore => .{ 0.48, 0.52, 0.50 }, - else => .{ 0.22, 0.72, 0.16 }, - }; - } - - fn biomeFoliageTint(biome_id: BiomeId) [3]f32 { - return switch (biome_id) { - .forest => .{ 0.12, 0.52, 0.12 }, - .birch_forest => .{ 0.18, 0.58, 0.14 }, - .dark_forest => .{ 0.08, 0.36, 0.08 }, - .flower_forest => .{ 0.20, 0.58, 0.12 }, - .taiga => .{ 0.18, 0.46, 0.18 }, - .meadow => .{ 0.20, 0.58, 0.18 }, - .grove => .{ 0.16, 0.38, 0.18 }, - .snowy_slopes => .{ 0.64, 0.72, 0.70 }, - .jagged_peaks => .{ 0.44, 0.46, 0.42 }, - .frozen_peaks => .{ 0.68, 0.78, 0.82 }, - .stony_peaks => .{ 0.46, 0.44, 0.34 }, - .snowy_taiga => .{ 0.16, 0.40, 0.24 }, - .old_growth_taiga => .{ 0.12, 0.34, 0.20 }, - .swamp => .{ 0.22, 0.52, 0.16 }, - .jungle => .{ 0.08, 0.62, 0.08 }, - .savanna => .{ 0.50, 0.50, 0.28 }, - .foothills => .{ 0.18, 0.50, 0.16 }, - .coastal_plains => .{ 0.18, 0.52, 0.16 }, - else => .{ 0.14, 0.58, 0.12 }, - }; - } - - fn biomeWaterTint(biome_id: BiomeId) [3]f32 { - return switch (biome_id) { - .deep_ocean => .{ 0.1, 0.2, 0.5 }, - .frozen_ocean, .frozen_river => .{ 0.32, 0.54, 0.74 }, - .cold_ocean, .stony_shore, .snowy_beach => .{ 0.10, 0.34, 0.62 }, - .swamp => .{ 0.16, 0.38, 0.30 }, - else => .{ 0.12, 0.38, 0.78 }, - }; - } - - fn treeBlocksForBiome(biome_id: BiomeId) TreeBlocks { - return switch (biome_id) { - .taiga, .snowy_taiga, .old_growth_taiga, .snow_tundra, .snowy_mountains, .grove, .snowy_slopes => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, - .birch_forest => .{ .trunk = .birch_log, .leaves = .birch_leaves }, - .jungle => .{ .trunk = .jungle_log, .leaves = .jungle_leaves }, - .savanna => .{ .trunk = .acacia_log, .leaves = .acacia_leaves }, - .swamp, .mangrove_swamp, .marsh => .{ .trunk = .mangrove_log, .leaves = .mangrove_leaves }, - else => .{ .trunk = .wood, .leaves = .leaves }, - }; - } - - fn treeBlocksForType(tree_type: tree_registry.TreeType) TreeBlocks { - return switch (tree_type) { - .spruce => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, - .dense_spruce => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, - .birch, .dense_birch => .{ .trunk = .birch_log, .leaves = .birch_leaves }, - .jungle => .{ .trunk = .jungle_log, .leaves = .jungle_leaves }, - .acacia => .{ .trunk = .acacia_log, .leaves = .acacia_leaves }, - .mangrove => .{ .trunk = .mangrove_log, .leaves = .mangrove_leaves }, - .huge_red_mushroom => .{ .trunk = .mushroom_stem, .leaves = .red_mushroom_block }, - .huge_brown_mushroom => .{ .trunk = .mushroom_stem, .leaves = .brown_mushroom_block }, - else => .{ .trunk = .wood, .leaves = .leaves }, - }; - } - - fn treeHeightForBiome(biome_id: BiomeId) f32 { - return switch (biome_id) { - .jungle => 13.0, - .taiga, .snowy_taiga, .snow_tundra, .snowy_mountains, .grove, .snowy_slopes => 10.0, - .old_growth_taiga => 12.0, - .savanna => 8.0, - .swamp, .mangrove_swamp, .marsh => 7.0, - else => 6.0, - }; - } - - fn treeHeightForType(tree_type: tree_registry.TreeType) f32 { - return switch (tree_type) { - .jungle => 13.0, - .spruce => 10.0, - .dense_spruce => 12.0, - .acacia => 8.0, - .swamp_oak, .mangrove => 7.0, - .huge_red_mushroom, .huge_brown_mushroom => 6.0, - else => 6.0, + .biome = sample.biome, + .surface_block = sample.surface_block, + .terrain_height_i = sample.terrain_height_i, }; } @@ -896,23 +594,6 @@ pub const OverworldGenerator = struct { return (r << 16) | (g << 8) | b; } - fn lodColorForSample(biome_id: BiomeId, surface_block: BlockType) u32 { - return switch (surface_block) { - .air => 0, - .grass => packFloatColor(biomeGrassTint(biome_id)), - .water => packFloatColor(biomeWaterTint(biome_id)), - .leaves, .mangrove_leaves, .jungle_leaves, .acacia_leaves, .birch_leaves, .spruce_leaves => packFloatColor(biomeFoliageTint(biome_id)), - else => packBlockColor(surface_block), - }; - } - - fn packFloatColor(color: [3]f32) u32 { - const r: u32 = @intFromFloat(@round(std.math.clamp(color[0], 0.0, 1.0) * 255.0)); - const g: u32 = @intFromFloat(@round(std.math.clamp(color[1], 0.0, 1.0) * 255.0)); - const b: u32 = @intFromFloat(@round(std.math.clamp(color[2], 0.0, 1.0) * 255.0)); - return (r << 16) | (g << 8) | b; - } - fn makeMaterialLayers(surface_block: BlockType, biome_id: BiomeId, render_water_surface: bool) world_core.LODMaterialLayers { if (render_water_surface and (surface_block == .ice or surface_block == .packed_ice)) { return .{ @@ -970,14 +651,6 @@ pub const OverworldGenerator = struct { }; } - fn packBlockColor(block_type: BlockType) u32 { - const color = block_registry.getBlockDefinition(block_type).default_color; - const r: u32 = @intFromFloat(@round(std.math.clamp(color[0], 0.0, 1.0) * 255.0)); - const g: u32 = @intFromFloat(@round(std.math.clamp(color[1], 0.0, 1.0) * 255.0)); - const b: u32 = @intFromFloat(@round(std.math.clamp(color[2], 0.0, 1.0) * 255.0)); - return (r << 16) | (g << 8) | b; - } - fn surfaceTypeToBlock(_: *const OverworldGenerator, surface_type: SurfaceType) BlockType { return switch (surface_type) { .grass => .grass, diff --git a/modules/worldgen-overworld/src/root.zig b/modules/worldgen-overworld/src/root.zig index c5d7fda5..aceb1f79 100644 --- a/modules/worldgen-overworld/src/root.zig +++ b/modules/worldgen-overworld/src/root.zig @@ -24,6 +24,7 @@ pub const height_sampler_tests = @import("height_sampler_tests.zig"); pub const noise_sampler = @import("noise_sampler.zig"); pub const lighting_computer = @import("worldgen-common").lighting_computer; pub const lighting_interface = @import("worldgen-common").lighting_interface; +pub const lod_coloring = @import("lod_coloring.zig"); pub const mood = @import("mood.zig"); pub const noise = @import("noise.zig"); pub const overworld_generator = @import("overworld_generator.zig"); @@ -34,6 +35,7 @@ pub const terrain_shape_generator = @import("terrain_shape_generator.zig"); pub const terrain_shape_generator_tests = @import("terrain_shape_generator_tests.zig"); pub const terrain_modifier_tests = @import("terrain_modifier_tests.zig"); pub const terrain_report = @import("terrain_report.zig"); +pub const tree_hints = @import("tree_hints.zig"); pub const tree_registry = @import("tree_registry.zig"); pub const world_class = @import("world_class.zig"); pub const world_map = @import("world_map.zig"); diff --git a/modules/worldgen-overworld/src/tree_hints.zig b/modules/worldgen-overworld/src/tree_hints.zig new file mode 100644 index 00000000..cf460c93 --- /dev/null +++ b/modules/worldgen-overworld/src/tree_hints.zig @@ -0,0 +1,287 @@ +//! LOD vegetation hint computation for overworld tree placement. + +const std = @import("std"); +const biome_mod = @import("biome.zig"); +const BiomeId = biome_mod.BiomeId; +const region_pkg = @import("region.zig"); +const decoration_registry = @import("decoration_registry.zig"); +const NoiseSampler = @import("noise_sampler.zig").NoiseSampler; +const schematics = @import("schematics.zig"); +const tree_registry = @import("tree_registry.zig"); +const world_core = @import("world-core"); +const BlockType = world_core.BlockType; +const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; +const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; +const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; + +pub const TreeHintChunk = [CHUNK_SIZE_X * CHUNK_SIZE_Z]world_core.LODVegetationHint; + +pub const TreeBlocks = struct { + trunk: BlockType, + leaves: BlockType, +}; + +pub const ClassifiedSample = struct { + biome: BiomeId, + surface_block: BlockType, + terrain_height_i: i32, +}; + +pub const ClassifyFn = *const fn (*const anyopaque, f32, f32, i32, region_pkg.RegionControlCorners) ClassifiedSample; + +pub const ComputeParams = struct { + chunk_x: i32, + chunk_z: i32, + world_x: i32, + world_z: i32, + sea_level: i32, + terrain_region_seed: u64, + decorator_region_seed: u64, + noise_sampler: *const NoiseSampler, + classify_context: *const anyopaque, + classify_fn: ClassifyFn, +}; + +pub fn computeChunkTreeHints(params: ComputeParams) TreeHintChunk { + const controls = region_pkg.RegionControlCorners.init( + params.terrain_region_seed, + params.world_x, + params.world_z, + params.world_x + @as(i32, @intCast(CHUNK_SIZE_X)) - 1, + params.world_z + @as(i32, @intCast(CHUNK_SIZE_Z)) - 1, + ); + var prng = std.Random.DefaultPrng.init(params.decorator_region_seed ^ @as(u64, @bitCast(@as(i64, params.chunk_x))) ^ (@as(u64, @bitCast(@as(i64, params.chunk_z))) << 32)); + const random = prng.random(); + var tree_occupancy = [_]bool{false} ** world_core.CHUNK_VOLUME; + var hints = [_]world_core.LODVegetationHint{world_core.LODVegetationHint.empty} ** (CHUNK_SIZE_X * CHUNK_SIZE_Z); + + var lz: u32 = 0; + while (lz < CHUNK_SIZE_Z) : (lz += 1) { + var lx: u32 = 0; + while (lx < CHUNK_SIZE_X) : (lx += 1) { + const wx_i = params.world_x + @as(i32, @intCast(lx)); + const wz_i = params.world_z + @as(i32, @intCast(lz)); + const sample = params.classify_fn(params.classify_context, @floatFromInt(wx_i), @floatFromInt(wz_i), params.sea_level, controls); + const column_controls = controls.sample(wx_i, wz_i); + const variant = params.noise_sampler.variant_noise.get2D(@floatFromInt(wx_i), @floatFromInt(wz_i)); + _ = decoration_registry.chooseStaticSimpleDecoration(sample.biome, sample.surface_block, variant, column_controls.subbiome_mask > 0.5, column_controls.vegetation_mult, 0, random); + + const placed_tree = choosePlacedTree(sample.biome, sample.surface_block, variant, column_controls.subbiome_mask > 0.5, column_controls.vegetation_mult, lx, lz, sample.terrain_height_i, &tree_occupancy, random); + if (placed_tree) |tree| { + placeTreeOccupancy(tree.schematic, lx, @intCast(sample.terrain_height_i + 1), lz, &tree_occupancy, random); + hints[lx + lz * CHUNK_SIZE_X] = treeHintForType(tree.tree_type); + } + } + } + return hints; +} + +pub fn estimatedTreeHintForBiome(biome_id: BiomeId, wx: f32, wz: f32, seed: u64) world_core.LODVegetationHint { + const tree_types = biome_mod.getBiomeDefinition(biome_id).vegetation.tree_types; + if (tree_types.len == 0) return world_core.LODVegetationHint.empty; + + var best_type = tree_types[0]; + var best_probability: f32 = 0.0; + var probability_sum: f32 = 0.0; + for (tree_types) |tree_type| { + const def = tree_registry.getTreeDefinition(tree_type) orelse continue; + probability_sum += def.probability; + if (def.probability > best_probability) { + best_probability = def.probability; + best_type = tree_type; + } + } + + const base_coverage = std.math.clamp(probability_sum * 7.5, 0.0, 1.0); + if (base_coverage < 0.035) return world_core.LODVegetationHint.empty; + + const wx_i: i32 = @intFromFloat(@floor(wx)); + const wz_i: i32 = @intFromFloat(@floor(wz)); + const jitter = hashUnit2D(wx_i, wz_i, seed); + const placement_roll = hashUnit2D(wx_i, wz_i, seed ^ 0x6D2B79F5); + const placement_probability = std.math.clamp(base_coverage * 0.32, 0.025, 0.55); + if (placement_roll > placement_probability) return world_core.LODVegetationHint.empty; + + const blocks = blocksForType(best_type); + return .{ + .tree_coverage = std.math.clamp(0.12 + base_coverage * (0.18 + jitter * 0.08), 0.0, 0.45), + .avg_tree_height = heightForType(best_type), + .offset_x = (hashUnit2D(wx_i, wz_i, seed ^ 0xA53A9D13) - 0.5) * 2.0, + .offset_z = (hashUnit2D(wx_i, wz_i, seed ^ 0xC2B2AE35) - 0.5) * 2.0, + .trunk = blocks.trunk, + .leaves = blocks.leaves, + }; +} + +pub fn cacheKey(chunk_x: i32, chunk_z: i32) u64 { + const x: u64 = @bitCast(@as(i64, chunk_x)); + const z: u64 = @bitCast(@as(i64, chunk_z)); + return (x *% 0x9E3779B97F4A7C15) ^ (z *% 0xC2B2AE3D27D4EB4F); +} + +pub fn blocksForBiome(biome_id: BiomeId) TreeBlocks { + return switch (biome_id) { + .taiga, .snowy_taiga, .old_growth_taiga, .snow_tundra, .snowy_mountains, .grove, .snowy_slopes => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, + .birch_forest => .{ .trunk = .birch_log, .leaves = .birch_leaves }, + .jungle => .{ .trunk = .jungle_log, .leaves = .jungle_leaves }, + .savanna => .{ .trunk = .acacia_log, .leaves = .acacia_leaves }, + .swamp, .mangrove_swamp, .marsh => .{ .trunk = .mangrove_log, .leaves = .mangrove_leaves }, + else => .{ .trunk = .wood, .leaves = .leaves }, + }; +} + +fn hashUnit2D(x: i32, z: i32, seed: u64) f32 { + var h = seed; + h ^= @as(u64, @bitCast(@as(i64, x))) *% 0x9E3779B97F4A7C15; + h ^= @as(u64, @bitCast(@as(i64, z))) *% 0xC2B2AE3D27D4EB4F; + h ^= h >> 33; + h *%= 0xFF51AFD7ED558CCD; + h ^= h >> 33; + const bucket: u32 = @intCast(h & 0xFFFF); + return @as(f32, @floatFromInt(bucket)) / 65535.0; +} + +const PlacedTree = struct { + tree_type: tree_registry.TreeType, + schematic: schematics.Schematic, +}; + +fn choosePlacedTree( + biome_id: BiomeId, + surface_block: BlockType, + variant: f32, + allow_subbiomes: bool, + veg_mult: f32, + local_x: u32, + local_z: u32, + surface_y: i32, + tree_occupancy: *const [world_core.CHUNK_VOLUME]bool, + random: std.Random, +) ?PlacedTree { + const vegetation = biome_mod.getBiomeDefinition(biome_id).vegetation; + if (vegetation.tree_types.len == 0) return null; + + for (vegetation.tree_types) |tree_type| { + const tree_def = tree_registry.getTreeDefinition(tree_type) orelse continue; + if (!treeSurfaceAllowed(tree_def.place_on, surface_block)) continue; + if (!variantAllowed(variant, allow_subbiomes, tree_def.variant_min, tree_def.variant_max)) continue; + const prob = @min(1.0, tree_def.probability * veg_mult); + if (random.float(f32) >= prob) continue; + if (tree_def.spacing_radius > 0 and !isTreeAreaClear(tree_occupancy, @intCast(local_x), surface_y, @intCast(local_z), tree_def.spacing_radius)) continue; + return .{ + .tree_type = tree_type, + .schematic = tree_def.schematic, + }; + } + return null; +} + +fn variantAllowed(variant: f32, allow_subbiomes: bool, min: f32, max: f32) bool { + if (allow_subbiomes) return variant >= min and variant <= max; + return min == -1.0 and max == 1.0; +} + +fn treeSurfaceAllowed(place_on: []const BlockType, surface_block: BlockType) bool { + for (place_on) |valid| { + if (surface_block == valid) return true; + } + return false; +} + +fn isTreeAreaClear(tree_occupancy: *const [world_core.CHUNK_VOLUME]bool, x: i32, y: i32, z: i32, radius: i32) bool { + var dz: i32 = -radius; + while (dz <= radius) : (dz += 1) { + var dx: i32 = -radius; + while (dx <= radius) : (dx += 1) { + if (dx == 0 and dz == 0) continue; + const check_x = x + dx; + const check_z = z + dz; + if (check_x >= 0 and check_x < CHUNK_SIZE_X and check_z >= 0 and check_z < CHUNK_SIZE_Z) { + var dy: i32 = 1; + while (dy <= 3) : (dy += 1) { + const check_y = y + dy; + if (check_y >= 0 and check_y < CHUNK_SIZE_Y) { + const idx: usize = @intCast(@as(u32, @intCast(check_x)) + @as(u32, @intCast(check_z)) * CHUNK_SIZE_X + @as(u32, @intCast(check_y)) * CHUNK_SIZE_X * CHUNK_SIZE_Z); + if (tree_occupancy[idx]) return false; + } + } + } + } + } + return true; +} + +fn placeTreeOccupancy(schematic: schematics.Schematic, x: u32, y: u32, z: u32, tree_occupancy: *[world_core.CHUNK_VOLUME]bool, random: std.Random) void { + const center_x = @as(i32, @intCast(x)); + const center_y = @as(i32, @intCast(y)); + const center_z = @as(i32, @intCast(z)); + for (schematic.blocks) |block| { + if (block.probability < 1.0) { + if (random.float(f32) >= block.probability) continue; + } + const bx = center_x + block.offset[0] - schematic.center_x; + const by = center_y + block.offset[1]; + const bz = center_z + block.offset[2] - schematic.center_z; + if (bx >= 0 and bx < CHUNK_SIZE_X and bz >= 0 and bz < CHUNK_SIZE_Z and by >= 0 and by < CHUNK_SIZE_Y and isTreeBlock(block.block)) { + const idx: usize = @intCast(@as(u32, @intCast(bx)) + @as(u32, @intCast(bz)) * CHUNK_SIZE_X + @as(u32, @intCast(by)) * CHUNK_SIZE_X * CHUNK_SIZE_Z); + tree_occupancy[idx] = true; + } + } +} + +fn isTreeBlock(block: BlockType) bool { + return switch (block) { + .wood, + .leaves, + .birch_log, + .birch_leaves, + .spruce_log, + .spruce_leaves, + .jungle_log, + .jungle_leaves, + .acacia_log, + .acacia_leaves, + .mangrove_log, + .mangrove_leaves, + => true, + else => false, + }; +} + +fn treeHintForType(tree_type: tree_registry.TreeType) world_core.LODVegetationHint { + const blocks = blocksForType(tree_type); + return .{ + .tree_coverage = 1.0, + .avg_tree_height = heightForType(tree_type), + .offset_x = 0.0, + .offset_z = 0.0, + .trunk = blocks.trunk, + .leaves = blocks.leaves, + }; +} + +fn blocksForType(tree_type: tree_registry.TreeType) TreeBlocks { + return switch (tree_type) { + .spruce => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, + .dense_spruce => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, + .birch, .dense_birch => .{ .trunk = .birch_log, .leaves = .birch_leaves }, + .jungle => .{ .trunk = .jungle_log, .leaves = .jungle_leaves }, + .acacia => .{ .trunk = .acacia_log, .leaves = .acacia_leaves }, + .mangrove => .{ .trunk = .mangrove_log, .leaves = .mangrove_leaves }, + .huge_red_mushroom => .{ .trunk = .mushroom_stem, .leaves = .red_mushroom_block }, + .huge_brown_mushroom => .{ .trunk = .mushroom_stem, .leaves = .brown_mushroom_block }, + else => .{ .trunk = .wood, .leaves = .leaves }, + }; +} + +fn heightForType(tree_type: tree_registry.TreeType) f32 { + return switch (tree_type) { + .jungle => 13.0, + .spruce => 10.0, + .dense_spruce => 12.0, + .acacia => 8.0, + .swamp_oak, .mangrove => 7.0, + .huge_red_mushroom, .huge_brown_mushroom => 6.0, + else => 6.0, + }; +}