diff --git a/modules/world-core/src/block_registry_tests.zig b/modules/world-core/src/block_registry_tests.zig index 7106c77e..1ed7818c 100644 --- a/modules/world-core/src/block_registry_tests.zig +++ b/modules/world-core/src/block_registry_tests.zig @@ -162,6 +162,8 @@ test "custom mesh blocks do not fully occlude neighboring cube faces" { } test "core natural block pack registry properties" { + try testing.expectEqualStrings("grass_side", block_registry.getBlockDefinition(.grass).texture_side); + try testing.expectEqual(block_registry.RenderShape.flat_quad, block_registry.getBlockDefinition(.snow_layer).render_shape); try testing.expectEqual(block_registry.RenderPass.cutout, block_registry.getBlockDefinition(.snow_layer).render_pass); try testing.expect(!block_registry.getBlockDefinition(.snow_layer).is_solid); diff --git a/modules/world-meshing/src/meshing/biome_color_sampler.zig b/modules/world-meshing/src/meshing/biome_color_sampler.zig index 2458dc3b..9182ef58 100644 --- a/modules/world-meshing/src/meshing/biome_color_sampler.zig +++ b/modules/world-meshing/src/meshing/biome_color_sampler.zig @@ -1,7 +1,7 @@ //! Biome color blending for chunk meshing. //! //! Computes biome-tinted colors for blocks using 3x3 biome averaging. -//! Only grass (top face), leaves, and water receive biome tints. +//! Grass top faces, leaves, and water receive biome tints. const std = @import("std"); const world_core = @import("world-core"); @@ -15,11 +15,9 @@ const NeighborChunks = boundary.NeighborChunks; /// Calculate the biome-tinted color for a block face. /// Returns {1, 1, 1} (no tint) for blocks that don't receive biome coloring. /// `s`, `u`, `v` are local coordinates on the slice plane (depending on `axis`). -pub inline fn getBlockColor(chunk: *const Chunk, neighbors: NeighborChunks, axis: Face, s: i32, u: u32, v: u32, block: BlockType) [3]f32 { - // Only apply biome tint to top face of grass, and all faces of leaves/water +pub inline fn getBlockColor(chunk: *const Chunk, neighbors: NeighborChunks, axis: Face, face: Face, s: i32, u: u32, v: u32, block: BlockType) [3]f32 { if (block == .grass) { - // Grass: only tint the top face, sides and bottom get no tint - if (axis != .top) return .{ 1.0, 1.0, 1.0 }; + if (face != .top) return .{ 1.0, 1.0, 1.0 }; } else if (block != .leaves and block != .water) { return .{ 1.0, 1.0, 1.0 }; } diff --git a/modules/world-meshing/src/meshing/custom_mesh_mesher.zig b/modules/world-meshing/src/meshing/custom_mesh_mesher.zig index bda0cdb6..497d259f 100644 --- a/modules/world-meshing/src/meshing/custom_mesh_mesher.zig +++ b/modules/world-meshing/src/meshing/custom_mesh_mesher.zig @@ -60,7 +60,7 @@ pub fn meshCustomMeshBlocks( const entrance_bounce = sampleCustomEntranceBounce(chunk, neighbors, xi, y, zi); const entrance_dir = boundary.getEntranceDirCross(chunk, neighbors, xi, y, zi); const norm_light = lighting_sampler.normalizeLightValues(light, entrance_bounce, entrance_dir); - const color = biome_color_sampler.getBlockColor(chunk, neighbors, .top, y + 1, x, z, block); + const color = biome_color_sampler.getBlockColor(chunk, neighbors, .top, .top, y + 1, x, z, block); const xf: f32 = @floatFromInt(x); const yf: f32 = @floatFromInt(y); diff --git a/modules/world-meshing/src/meshing/greedy_mesher.zig b/modules/world-meshing/src/meshing/greedy_mesher.zig index 8540c7db..1f685576 100644 --- a/modules/world-meshing/src/meshing/greedy_mesher.zig +++ b/modules/world-meshing/src/meshing/greedy_mesher.zig @@ -92,13 +92,19 @@ pub fn meshSlice( const light = lighting_sampler.sampleLightAtBoundary(chunk, neighbors, axis, s, u, v, si, true); const entrance_bounce = lighting_sampler.sampleEntranceBounceAtBoundary(chunk, neighbors, axis, s, u, v, si, true); const entrance_dir = lighting_sampler.sampleEntranceDirAtBoundary(chunk, neighbors, axis, s, u, v, si, true); - const color = biome_color_sampler.getBlockColor(chunk, neighbors, axis, s - 1, u, v, b1); + const color = biome_color_sampler.getBlockColor(chunk, neighbors, axis, axis, s - 1, u, v, b1); mask[u + v * du] = .{ .block = b1, .side = true, .light = light, .entrance_bounce = entrance_bounce, .entrance_dir = entrance_dir, .color = color }; } else if (boundary.isEmittingSubchunk(axis, s, u, v, y_min, y_max) and b2_emits and b2_cube and !b1_def.occludes(b2_def, axis)) { const light = lighting_sampler.sampleLightAtBoundary(chunk, neighbors, axis, s, u, v, si, false); const entrance_bounce = lighting_sampler.sampleEntranceBounceAtBoundary(chunk, neighbors, axis, s, u, v, si, false); const entrance_dir = lighting_sampler.sampleEntranceDirAtBoundary(chunk, neighbors, axis, s, u, v, si, false); - const color = biome_color_sampler.getBlockColor(chunk, neighbors, axis, s, u, v, b2); + const face = switch (axis) { + .top => Face.bottom, + .east => Face.west, + .south => Face.north, + else => unreachable, + }; + const color = biome_color_sampler.getBlockColor(chunk, neighbors, axis, face, s, u, v, b2); mask[u + v * du] = .{ .block = b2, .side = false, .light = light, .entrance_bounce = entrance_bounce, .entrance_dir = entrance_dir, .color = color }; } } diff --git a/modules/world-worldgen/src/biome_edge_detector.zig b/modules/world-worldgen/src/biome_edge_detector.zig index 0d4df9ce..5cdc9399 100644 --- a/modules/world-worldgen/src/biome_edge_detector.zig +++ b/modules/world-worldgen/src/biome_edge_detector.zig @@ -110,9 +110,9 @@ pub const TRANSITION_RULES = [_]TransitionRule{ // Cold/coastal <-> temperate/coastal .{ .biome_a = .frozen_ocean, .biome_b = .ocean, .transition = .cold_ocean }, .{ .biome_a = .cold_ocean, .biome_b = .warm_ocean, .transition = .ocean }, - .{ .biome_a = .snowy_beach, .biome_b = .beach, .transition = .stony_shore }, + .{ .biome_a = .snowy_beach, .biome_b = .beach, .transition = .coastal_plains }, .{ .biome_a = .snow_tundra, .biome_b = .beach, .transition = .snowy_beach }, - .{ .biome_a = .taiga, .biome_b = .beach, .transition = .stony_shore }, + .{ .biome_a = .taiga, .biome_b = .beach, .transition = .coastal_plains }, .{ .biome_a = .beach, .biome_b = .plains, .transition = .coastal_plains }, .{ .biome_a = .beach, .biome_b = .forest, .transition = .coastal_plains }, .{ .biome_a = .beach, .biome_b = .birch_forest, .transition = .coastal_plains }, diff --git a/modules/world-worldgen/src/biome_registry.zig b/modules/world-worldgen/src/biome_registry.zig index 5de8a8ae..7825d705 100644 --- a/modules/world-worldgen/src/biome_registry.zig +++ b/modules/world-worldgen/src/biome_registry.zig @@ -74,6 +74,17 @@ pub const TerrainModifier = struct { if (self.clamp_to_sea_level) height = sea_level; return height + self.height_offset; } + + /// Blend two terrain modifiers for biome-boundary height shaping. + pub fn blend(a: TerrainModifier, b: TerrainModifier, t_raw: f32) TerrainModifier { + const t = std.math.clamp(t_raw, 0.0, 1.0); + return .{ + .height_amplitude = std.math.lerp(a.height_amplitude, b.height_amplitude, t), + .smoothing = std.math.lerp(a.smoothing, b.smoothing, t), + .clamp_to_sea_level = if (t >= 0.5) b.clamp_to_sea_level else a.clamp_to_sea_level, + .height_offset = std.math.lerp(a.height_offset, b.height_offset, t), + }; + } }; /// Surface block configuration @@ -224,12 +235,11 @@ pub const BIOME_POINTS = [_]BiomePoint{ .{ .id = .cold_ocean, .heat = 22, .humidity = 55, .weight = 1.1, .min_continental = 0.10, .max_continental = 0.37 }, .{ .id = .ocean, .heat = 50, .humidity = 50, .weight = 1.5, .min_continental = 0.20, .max_continental = 0.37 }, .{ .id = .warm_ocean, .heat = 85, .humidity = 75, .weight = 0.9, .min_continental = 0.20, .max_continental = 0.37 }, - .{ .id = .tropical, .heat = 95, .humidity = 90, .weight = 0.7, .min_continental = 0.30, .max_continental = 0.48, .max_slope = 3, .y_max = 72 }, + .{ .id = .tropical, .heat = 95, .humidity = 90, .weight = 0.7, .min_continental = 0.30, .max_continental = 0.41, .max_slope = 3, .y_max = 70 }, // === Coastal Biomes === - .{ .id = .snowy_beach, .heat = 8, .humidity = 45, .weight = 0.7, .max_slope = 2, .min_continental = 0.37, .max_continental = 0.44, .y_max = 70 }, - .{ .id = .stony_shore, .heat = 30, .humidity = 45, .weight = 0.7, .min_continental = 0.37, .max_continental = 0.46, .y_max = 82 }, - .{ .id = .beach, .heat = 60, .humidity = 50, .weight = 0.6, .max_slope = 2, .min_continental = 0.37, .max_continental = 0.44, .y_max = 70 }, + .{ .id = .snowy_beach, .heat = 8, .humidity = 45, .weight = 0.7, .max_slope = 2, .min_continental = 0.37, .max_continental = 0.41, .y_max = 68 }, + .{ .id = .beach, .heat = 60, .humidity = 50, .weight = 0.6, .max_slope = 2, .min_continental = 0.37, .max_continental = 0.41, .y_max = 68 }, // === Cold Biomes === .{ .id = .snow_tundra, .heat = 5, .humidity = 30, .weight = 1.0, .min_continental = 0.42 }, @@ -307,7 +317,7 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .elevation = .{ .min = 0.0, .max = 0.30 }, .continentalness = .{ .min = 0.0, .max = 0.37 }, .priority = 1, - .surface = .{ .top = .sand, .filler = .sand, .depth_range = 3 }, + .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, .vegetation = .{ .tree_types = &.{} }, }, .{ @@ -366,11 +376,11 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .name = "Tropical", .temperature = .{ .min = 0.85, .max = 1.0 }, .humidity = .{ .min = 0.75, .max = 1.0 }, - .elevation = .{ .min = 0.25, .max = 0.42 }, - .continentalness = .{ .min = 0.32, .max = 0.48 }, + .elevation = .{ .min = 0.25, .max = 0.38 }, + .continentalness = .{ .min = 0.32, .max = 0.41 }, .max_slope = 3, .priority = 8, - .surface = .{ .top = .sand, .filler = .sand, .depth_range = 3 }, + .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, .vegetation = .{ .tree_types = &.{.jungle}, .seagrass_density = 0.14, @@ -389,8 +399,8 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .temperature = .{ .min = 0.2, .max = 1.0 }, .humidity = Range.any(), .elevation = .{ .min = 0.28, .max = 0.38 }, - .continentalness = .{ .min = 0.37, .max = 0.44 }, // NARROW beach band - .max_height = 70, + .continentalness = .{ .min = 0.37, .max = 0.41 }, // Narrow beach band + .max_height = 68, .max_slope = 2, .priority = 10, .surface = .{ .top = .sand, .filler = .sand, .depth_range = 2 }, @@ -402,10 +412,10 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .temperature = .{ .min = 0.20, .max = 0.45 }, .humidity = Range.any(), .elevation = .{ .min = 0.28, .max = 0.45 }, - .continentalness = .{ .min = 0.37, .max = 0.46 }, - .max_height = 82, - .max_slope = 8, - .priority = 11, + .continentalness = .{ .min = -1.0, .max = -0.5 }, // Edge-injection/debug only; never natural coast fill + .max_height = 78, + .max_slope = 4, + .priority = 0, .surface = .{ .top = .stone, .filler = .gravel, .depth_range = 2 }, .vegetation = .{ .tree_types = &.{} }, .terrain = .{ .height_amplitude = 0.8, .smoothing = 0.1 }, @@ -417,8 +427,8 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .temperature = .{ .min = 0.0, .max = 0.18 }, .humidity = Range.any(), .elevation = .{ .min = 0.28, .max = 0.38 }, - .continentalness = .{ .min = 0.37, .max = 0.44 }, - .max_height = 70, + .continentalness = .{ .min = 0.37, .max = 0.41 }, + .max_height = 68, .max_slope = 2, .priority = 12, .surface = .{ .top = .snow_block, .filler = .sand, .depth_range = 2 }, @@ -1014,8 +1024,8 @@ pub const BIOME_REGISTRY: []const BiomeDefinition = &.{ .continentalness = .{ .min = -1.0, .max = -0.5 }, // IMPOSSIBLE: edge-injection only .ruggedness = .{ .min = 0.0, .max = 0.35 }, .priority = 0, // Lowest priority - .surface = .{ .top = .sand, .filler = .sand, .depth_range = 3 }, - .vegetation = .{ .tree_types = &.{} }, + .surface = .{ .top = .grass, .filler = .dirt, .depth_range = 3 }, + .vegetation = .{ .tree_types = &.{.sparse_oak}, .decoration_rules = &.{.{ .block = .tall_grass, .place_on = &.{.grass}, .chance = 0.2 }} }, .terrain = .{ .height_amplitude = 0.5, .smoothing = 0.3 }, .colors = .{ .grass = .{ 0.24, 0.66, 0.24 }, .foliage = .{ 0.18, 0.52, 0.16 } }, }, diff --git a/modules/world-worldgen/src/biome_registry_tests.zig b/modules/world-worldgen/src/biome_registry_tests.zig index 4ba8beaf..a26c17ff 100644 --- a/modules/world-worldgen/src/biome_registry_tests.zig +++ b/modules/world-worldgen/src/biome_registry_tests.zig @@ -377,6 +377,8 @@ test "getBiomeDefinition beach has narrow continentalness band" { const beach = getBiomeDefinition(.beach); try testing.expect(beach.continentalness.min >= 0.30); try testing.expect(beach.continentalness.max <= 0.45); + try testing.expectEqual(.sand, beach.surface.top); + try testing.expectEqual(.sand, beach.surface.filler); } test "getBiomeDefinition cold coastal variants have expected surfaces" { @@ -384,8 +386,7 @@ test "getBiomeDefinition cold coastal variants have expected surfaces" { try testing.expectEqualStrings("Stony Shore", stony.name); try testing.expectEqual(.stone, stony.surface.top); try testing.expectEqual(.gravel, stony.surface.filler); - try testing.expect(stony.continentalness.min >= 0.37); - try testing.expect(stony.continentalness.max <= 0.46); + try testing.expect(stony.continentalness.max < 0.0); const snowy = getBiomeDefinition(.snowy_beach); try testing.expectEqualStrings("Snowy Beach", snowy.name); @@ -394,12 +395,12 @@ test "getBiomeDefinition cold coastal variants have expected surfaces" { try testing.expect(snowy.temperature.max <= 0.20); } -test "getBiomeDefinition coastal plains is sandy transition" { +test "getBiomeDefinition coastal plains is grassy beach transition" { const coastal = getBiomeDefinition(.coastal_plains); try testing.expectEqualStrings("Coastal Plains", coastal.name); - try testing.expectEqual(.sand, coastal.surface.top); - try testing.expectEqual(.sand, coastal.surface.filler); - try testing.expectEqual(@as(usize, 0), coastal.vegetation.decoration_rules.len); + try testing.expectEqual(.grass, coastal.surface.top); + try testing.expectEqual(.dirt, coastal.surface.filler); + try testing.expect(coastal.vegetation.decoration_rules.len > 0); } test "getBiomeDefinition frozen river is river override only" { @@ -413,7 +414,9 @@ test "getBiomeDefinition frozen river is river override only" { test "getBiomeDefinition tropical has aquatic vegetation and coastal range" { const tropical = getBiomeDefinition(.tropical); try testing.expect(tropical.continentalness.min >= 0.30); - try testing.expect(tropical.continentalness.max <= 0.50); + try testing.expect(tropical.continentalness.max <= 0.42); + try testing.expectEqual(.grass, tropical.surface.top); + try testing.expectEqual(.dirt, tropical.surface.filler); try testing.expect(tropical.vegetation.coral_density > 0.0); try testing.expect(tropical.vegetation.decoration_rules.len > 0); } diff --git a/modules/world-worldgen/src/coastal_generator_tests.zig b/modules/world-worldgen/src/coastal_generator_tests.zig index 14902faa..623676ef 100644 --- a/modules/world-worldgen/src/coastal_generator_tests.zig +++ b/modules/world-worldgen/src/coastal_generator_tests.zig @@ -133,7 +133,7 @@ test "getSurfaceType delegates to SurfaceBuilder" { ); try testing.expectEqual(CoastalSurfaceType.sand_beach, sand); - // Cliff: same coastal zone but slope >= cliff_min_slope + // Steep coasts keep their biome surface; cliffs are explicit biome choices. const cliff = CoastalGenerator.getSurfaceType( &surface_builder, near_ocean_continentalness, @@ -141,7 +141,7 @@ test "getSurfaceType delegates to SurfaceBuilder" { p.sea_level + 1, p.gravel_erosion_threshold - 0.1, ); - try testing.expectEqual(CoastalSurfaceType.cliff, cliff); + try testing.expectEqual(CoastalSurfaceType.none, cliff); // Not coastal: height exceeds beach_max_height_above_sea above sea_level const inland = CoastalGenerator.getSurfaceType( diff --git a/modules/world-worldgen/src/height_sampler.zig b/modules/world-worldgen/src/height_sampler.zig index 3d73ae04..364ae3d1 100644 --- a/modules/world-worldgen/src/height_sampler.zig +++ b/modules/world-worldgen/src/height_sampler.zig @@ -18,6 +18,8 @@ const PathInfo = region_pkg.PathInfo; const RegionControls = region_pkg.RegionControls; const TerrainModifier = @import("biome_registry.zig").TerrainModifier; const world_class = @import("world_class.zig"); + +const COASTAL_DETAIL_BLEND_WEIGHT: f32 = 0.45; const ContinentalZone = world_class.ContinentalZone; // ============================================================================ @@ -135,27 +137,25 @@ pub const HeightSampler = struct { // Coastal zone: rises from sea level if (c < p.continental_coast_max) { const range = p.continental_coast_max - p.ocean_threshold; - const t = (c - p.ocean_threshold) / range; - return sea + t * 8.0; // 0 to +8 blocks + const t = smoothstep(0.0, 1.0, (c - p.ocean_threshold) / range); + return sea + t * 2.5; // Narrow shore lift only } - // Inland Low: plains/forests + // Inland lowlands use terrain noise for relief, not a continental ramp. if (c < p.continental_inland_low_max) { - const range = p.continental_inland_low_max - p.continental_coast_max; - const t = (c - p.continental_coast_max) / range; - return sea + 8.0 + t * 12.0; // +8 to +20 + return sea + 2.5; } // Inland High: hills if (c < p.continental_inland_high_max) { const range = p.continental_inland_high_max - p.continental_inland_low_max; - const t = (c - p.continental_inland_low_max) / range; - return sea + 20.0 + t * 15.0; // +20 to +35 + const t = smoothstep(0.0, 1.0, (c - p.continental_inland_low_max) / range); + return sea + 2.5 + t * 27.5; // +2.5 to +30 } // Mountain Core const t = smoothstep(p.continental_inland_high_max, 1.0, c); - return sea + 35.0 + t * 25.0; // +35 to +60 + return sea + 30.0 + t * 30.0; // +30 to +60 } /// Process path system effects on terrain @@ -271,7 +271,10 @@ pub const HeightSampler = struct { // Blend terrain_base and terrain_alt using height_select // ============================================================ const mood_mult = controls.height_mult; - const v7_terrain = computeV7Terrain(noise, mood_mult); + const lowland_mask = smoothstep(p.ocean_threshold, p.continental_coast_max, noise.continentalness) * + (1.0 - smoothstep(p.continental_inland_low_max, p.continental_inland_high_max, noise.continentalness)); + const terrain_mood = std.math.lerp(mood_mult, @max(mood_mult, 0.7), lowland_mask); + const v7_terrain = computeV7Terrain(noise, terrain_mood); // ============================================================ // STEP 4: LAND - Combine V7 terrain with continental base @@ -303,7 +306,9 @@ pub const HeightSampler = struct { // ============================================================ const erosion_smooth = smoothstep(0.5, 0.75, noise.erosion); const land_factor = smoothstep(p.continental_coast_max, p.continental_inland_low_max, noise.continentalness); - const hills_atten = (1.0 - erosion_smooth) * land_factor * coastal_ramp * (1.0 - path_effects.slope_suppress); + const coastal_detail_factor = coastal_ramp * (1.0 - land_factor) * COASTAL_DETAIL_BLEND_WEIGHT; + const lowland_detail_factor = @max(land_factor, coastal_detail_factor); + const hills_atten = (1.0 - erosion_smooth) * lowland_detail_factor * coastal_ramp * (1.0 - path_effects.slope_suppress); // Small-scale detail const elev01 = clamp01((height - sea) / p.highland_range); diff --git a/modules/world-worldgen/src/height_sampler_tests.zig b/modules/world-worldgen/src/height_sampler_tests.zig index 4415de6d..8df912d7 100644 --- a/modules/world-worldgen/src/height_sampler_tests.zig +++ b/modules/world-worldgen/src/height_sampler_tests.zig @@ -11,11 +11,29 @@ const testing = std.testing; const height_sampler_mod = @import("height_sampler.zig"); const world_class_mod = @import("world_class.zig"); const noise_sampler_mod = @import("noise_sampler.zig"); +const region_mod = @import("region.zig"); const HeightSampler = height_sampler_mod.HeightSampler; const HeightParams = height_sampler_mod.HeightParams; const ContinentalZone = world_class_mod.ContinentalZone; const ColumnNoiseValues = noise_sampler_mod.ColumnNoiseValues; +const NoiseSampler = noise_sampler_mod.NoiseSampler; +const RegionControls = region_mod.RegionControls; +const PathInfo = region_mod.PathInfo; + +const flat_controls = RegionControls{ + .height_mult = 1.0, + .vegetation_mult = 1.0, + .drama_mask = 0.0, + .river_mask = 0.0, + .subbiome_mask = 0.0, +}; + +const no_path = PathInfo{ + .path_type = .none, + .influence = 0.0, + .direction = .{ 0.0, 0.0 }, +}; // ============================================================================ // HeightSampler Initialization Tests @@ -200,3 +218,61 @@ test "HeightSampler computeHeightSimple peak compression caps very high terrain" try testing.expect(height < very_high); try testing.expect(height > peak_start); } + +fn coastalTestNoise(warped_x: f32, warped_z: f32) ColumnNoiseValues { + return .{ + .warp = .{ .x = 0.0, .z = 0.0 }, + .warped_x = warped_x, + .warped_z = warped_z, + .continentalness = 0.43, + .erosion = 0.45, + .peaks_valleys = 0.5, + .temperature = 0.5, + .humidity = 0.5, + .river_mask = 0.0, + .terrain_base = 0.0, + .terrain_alt = 0.0, + .height_select = 0.0, + .terrain_persist = 1.0, + .variant = 0.0, + }; +} + +fn lowlandRampNoise(continentalness: f32) ColumnNoiseValues { + var noise = coastalTestNoise(64.0, 96.0); + noise.continentalness = continentalness; + noise.erosion = 1.0; + return noise; +} + +test "HeightSampler computeHeight avoids monotonic inland-low ramp" { + const sampler = HeightSampler.init(); + const noise_sampler = NoiseSampler.init(1357); + + const coastal_edge = sampler.computeHeightWithTerrainModifier(&noise_sampler, lowlandRampNoise(0.44), flat_controls, no_path, 0, null); + const inland_low = sampler.computeHeightWithTerrainModifier(&noise_sampler, lowlandRampNoise(0.58), flat_controls, no_path, 0, null); + + try testing.expect(@abs(inland_low - coastal_edge) < 1.0); +} + +test "HeightSampler computeHeight keeps detail active on coastal land" { + const sampler = HeightSampler.init(); + const noise_sampler = NoiseSampler.init(2468); + const positions = [_][2]f32{ + .{ 17.0, 29.0 }, + .{ 53.0, 71.0 }, + .{ 109.0, 31.0 }, + .{ 157.0, 149.0 }, + .{ 211.0, 83.0 }, + }; + + var min_height: f32 = std.math.inf(f32); + var max_height: f32 = -std.math.inf(f32); + for (positions) |pos| { + const height = sampler.computeHeightWithTerrainModifier(&noise_sampler, coastalTestNoise(pos[0], pos[1]), flat_controls, no_path, 0, null); + min_height = @min(min_height, height); + max_height = @max(max_height, height); + } + + try testing.expect(max_height - min_height > 0.5); +} diff --git a/modules/world-worldgen/src/overworld_generator.zig b/modules/world-worldgen/src/overworld_generator.zig index 200b5c0d..e567facf 100644 --- a/modules/world-worldgen/src/overworld_generator.zig +++ b/modules/world-worldgen/src/overworld_generator.zig @@ -883,11 +883,31 @@ pub const OverworldGenerator = struct { return .{ .surface = surface_block, - .subsurface = biome_id.getFillerBlock(), + .subsurface = registryFillerBlock(biome_id), .foundation = .stone, }; } + fn registrySurfaceBlock(biome_id: BiomeId) BlockType { + return biome_mod.getBiomeDefinition(biome_id).surface.top; + } + + fn registryFillerBlock(biome_id: BiomeId) BlockType { + return biome_mod.getBiomeDefinition(biome_id).surface.filler; + } + + fn surfaceTypeForBlock(block: BlockType) SurfaceType { + return switch (block) { + .sand, .red_sand => .sand, + .snow_block, .ice, .packed_ice, .blue_ice => .snow, + .stone, .cobblestone, .mossy_cobblestone => .stone, + .gravel => .rock, + .dirt, .coarse_dirt, .rooted_dirt, .podzol, .mud, .mycelium => .dirt, + .water => .water_shallow, + else => .grass, + }; + } + fn makeLightingHint(render_water_surface: bool) world_core.LODLightingHint { return .{ .sky_light = 15, @@ -920,18 +940,7 @@ pub const OverworldGenerator = struct { if (render_water_surface and biome_id == .frozen_ocean) return .ice; if (render_water_surface and biome_id == .frozen_river) return .ice; if (render_water_surface or height < sea_level) return .water; - return switch (biome_id) { - .desert, .badlands => .sand, - .snow_tundra, .snowy_taiga, .snowy_mountains, .snowy_slopes, .snowy_beach => .snow_block, - .frozen_ocean => .packed_ice, - .frozen_river => .ice, - .grove => .podzol, - .frozen_peaks => .packed_ice, - .jagged_peaks, .stony_peaks => .stone, - .beach => .sand, - .stony_shore => .stone, - else => .grass, - }; + return registrySurfaceBlock(biome_id); } fn populateClassificationCache( @@ -1011,15 +1020,9 @@ pub const OverworldGenerator = struct { .none => {}, } - return switch (biome_id) { - .desert, .badlands, .beach => .sand, - .snow_tundra, .snowy_taiga, .snowy_mountains, .snowy_slopes => .snow, - .mountains, .jagged_peaks, .stony_peaks => if (height > 120) .rock else .stone, - .frozen_peaks => .snow, - .grove => .dirt, - .deep_ocean, .ocean => .sand, - else => .grass, - }; + const surface_block = registrySurfaceBlock(biome_id); + if ((biome_id == .mountains or biome_id == .jagged_peaks or biome_id == .stony_peaks) and height > 120) return .rock; + return surfaceTypeForBlock(surface_block); } pub fn generator(self: *OverworldGenerator) Generator { diff --git a/modules/world-worldgen/src/surface_builder.zig b/modules/world-worldgen/src/surface_builder.zig index 056885cf..a189d1d3 100644 --- a/modules/world-worldgen/src/surface_builder.zig +++ b/modules/world-worldgen/src/surface_builder.zig @@ -23,8 +23,8 @@ const Chunk = world_core.Chunk; pub const CoastalSurfaceType = enum { none, // Not in coastal zone OR near inland water (use biome default) sand_beach, // Gentle slope near sea level, adjacent to OCEAN -> sand - gravel_beach, // High erosion coastal area adjacent to OCEAN -> gravel - cliff, // Steep slope in coastal zone -> stone + gravel_beach, // Reserved for explicit gravel shore biomes; not auto-painted broadly + cliff, // Reserved for explicit cliff biomes; not auto-painted broadly }; // ============================================================================ @@ -37,14 +37,14 @@ pub const SurfaceParams = struct { sea_level: i32 = 64, // Beach constraints - beach_max_height_above_sea: i32 = 6, - beach_max_slope: i32 = 2, - cliff_min_slope: i32 = 5, - gravel_erosion_threshold: f32 = 0.7, + beach_max_height_above_sea: i32 = 3, + beach_max_slope: i32 = 1, + cliff_min_slope: i32 = 5, // Reserved; structural coasts no longer auto-paint cliffs. + gravel_erosion_threshold: f32 = 0.7, // Reserved for explicit shore biomes. // Coastal zone (continentalness thresholds) ocean_threshold: f32 = 0.37, - beach_band: f32 = 0.07, // Width of beach zone in continentalness units + beach_band: f32 = 0.035, // Width of beach zone in continentalness units }; // ============================================================================ @@ -75,7 +75,7 @@ pub const SurfaceBuilder = struct { /// 1. This block is LAND (above sea level) /// 2. This block is near OCEAN (continentalness indicates ocean proximity) /// 3. Height is within beach_max_height_above_sea of sea level - /// 4. Slope is gentle + /// 4. Slope is very gentle /// /// Inland water (lakes/rivers) get grass/dirt banks, NOT sand. pub fn getCoastalSurfaceType( @@ -105,25 +105,15 @@ pub const SurfaceBuilder = struct { return .none; } - // CONSTRAINT 3: Classify based on slope and erosion + _ = erosion; - // Steep slopes become cliffs (stone) - if (slope >= p.cliff_min_slope) { - return .cliff; - } - - // High erosion areas become gravel beaches - if (erosion >= p.gravel_erosion_threshold and slope <= p.beach_max_slope + 1) { - return .gravel_beach; - } + // Do not auto-paint cliffs or gravel beaches from slope/erosion here. + // Broad structural overrides made coastlines look like concrete slabs; + // biome-specific shore types should opt into those materials explicitly. + if (slope > p.beach_max_slope) return .none; // Gentle slopes at sea level become sand beaches - if (slope <= p.beach_max_slope) { - return .sand_beach; - } - - // Moderate slopes - no special treatment - return .none; + return .sand_beach; } /// Get block type at a specific Y coordinate. @@ -142,6 +132,8 @@ pub const SurfaceBuilder = struct { ) BlockType { const sea_level = self.params.sea_level; const sea: f32 = @floatFromInt(sea_level); + const biome_id: BiomeId = @enumFromInt(@intFromEnum(biome)); + const biome_def = biome_mod.getBiomeDefinition(biome_id); // Bedrock floor if (y == 0) return .bedrock; @@ -156,7 +148,7 @@ pub const SurfaceBuilder = struct { // Ocean floor: sand in shallow water, clay/gravel in deep if (is_ocean_water and is_underwater and y == terrain_height) { const depth: f32 = sea - @as(f32, @floatFromInt(terrain_height)); - if (depth <= 12) return .sand; // Shallow ocean: sand + if (depth <= 5) return .sand; // Only the immediate waterline should be sandy. if (depth <= 30) return .clay; // Medium depth: clay return .gravel; // Deep: gravel } @@ -164,7 +156,7 @@ pub const SurfaceBuilder = struct { // Ocean shallow underwater filler for continuity if (is_ocean_water and is_underwater and y > terrain_height - 3) { const depth: f32 = sea - @as(f32, @floatFromInt(terrain_height)); - if (depth <= 12) return .sand; + if (depth <= 5) return .sand; } // INLAND WATER (lakes/rivers): dirt/gravel banks, NOT sand @@ -191,11 +183,11 @@ pub const SurfaceBuilder = struct { if (biome == .snowy_mountains or biome == .snow_tundra or biome == .snowy_taiga or biome == .snowy_slopes or biome == .snowy_beach) return .snow_block; if (biome == .frozen_ocean) return .packed_ice; if (biome == .frozen_river) return .ice; - return biome.getSurfaceBlock(); + return biome_def.surface.top; } // Filler blocks (dirt layer under surface) - if (y > terrain_height - filler_depth) return biome.getFillerBlock(); + if (y > terrain_height - filler_depth) return biome_def.surface.filler; // Deep underground return .stone; @@ -216,19 +208,18 @@ pub const SurfaceBuilder = struct { var block = self.getBlockAt(y, terrain_height, biome, filler_depth, is_ocean_water, is_underwater); const is_surface = (y == terrain_height); - const coastal_fill_depth = @max(filler_depth, 6); + const coastal_fill_depth = @max(filler_depth, 3); const is_coastal_fill = (y > terrain_height - coastal_fill_depth and y <= terrain_height); // Apply structural coastal surface types (ocean beaches only) if (is_surface and block != .air and block != .water and block != .bedrock) { switch (coastal_type) { .sand_beach => block = .sand, - .gravel_beach => block = .gravel, - .cliff => block = .stone, + .gravel_beach, .cliff => {}, .none => {}, } - } else if (is_coastal_fill and (coastal_type == .sand_beach or coastal_type == .gravel_beach) and block != .air and block != .water and block != .bedrock) { - block = if (coastal_type == .gravel_beach) .gravel else .sand; + } else if (is_coastal_fill and coastal_type == .sand_beach and block != .air and block != .water and block != .bedrock) { + block = .sand; } return block; @@ -252,13 +243,13 @@ test "SurfaceBuilder coastal type detection" { const sand = builder.getCoastalSurfaceType(0.37, 1, 65, 0.3); try std.testing.expectEqual(CoastalSurfaceType.sand_beach, sand); - // Cliff: high slope + // Steep coasts keep their biome surface; broad auto-painted stone looked artificial. const cliff = builder.getCoastalSurfaceType(0.37, 6, 65, 0.3); - try std.testing.expectEqual(CoastalSurfaceType.cliff, cliff); + try std.testing.expectEqual(CoastalSurfaceType.none, cliff); - // Gravel beach: high erosion + // High erosion no longer auto-paints gravel slabs. const gravel = builder.getCoastalSurfaceType(0.37, 2, 65, 0.8); - try std.testing.expectEqual(CoastalSurfaceType.gravel_beach, gravel); + try std.testing.expectEqual(CoastalSurfaceType.none, gravel); // Too far inland: no coastal type const inland = builder.getCoastalSurfaceType(0.50, 1, 70, 0.3); @@ -272,10 +263,10 @@ test "SurfaceBuilder coastal type detection" { test "SurfaceBuilder beach band matches coastal biome range" { const builder = SurfaceBuilder.init(); - const upper_beach = builder.getCoastalSurfaceType(0.41, 1, 70, 0.3); + const upper_beach = builder.getCoastalSurfaceType(0.40, 1, 67, 0.3); try std.testing.expectEqual(CoastalSurfaceType.sand_beach, upper_beach); - const inland = builder.getCoastalSurfaceType(0.45, 1, 70, 0.3); + const inland = builder.getCoastalSurfaceType(0.41, 1, 67, 0.3); try std.testing.expectEqual(CoastalSurfaceType.none, inland); } diff --git a/modules/world-worldgen/src/terrain_modifier_tests.zig b/modules/world-worldgen/src/terrain_modifier_tests.zig index 92a735d0..b17ff8d8 100644 --- a/modules/world-worldgen/src/terrain_modifier_tests.zig +++ b/modules/world-worldgen/src/terrain_modifier_tests.zig @@ -57,6 +57,33 @@ test "TerrainModifier combines amplitude smoothing clamp and offset deterministi try testing.expectApproxEqAbs(@as(f32, 66.0), clamped.applyHeight(102.0, SEA_LEVEL), 0.0001); } +test "TerrainModifier blend interpolates shaping for biome seams" { + const plains = TerrainModifier{ .height_amplitude = 0.7, .smoothing = 0.2 }; + const coastal = TerrainModifier{ .height_amplitude = 0.5, .smoothing = 0.3 }; + const blended = TerrainModifier.blend(plains, coastal, 0.5); + + try testing.expectApproxEqAbs(@as(f32, 0.6), blended.height_amplitude, 0.0001); + try testing.expectApproxEqAbs(@as(f32, 0.25), blended.smoothing, 0.0001); + + const base_height: f32 = 92.0; + const plains_height = plains.applyHeight(base_height, SEA_LEVEL); + const coastal_height = coastal.applyHeight(base_height, SEA_LEVEL); + const blended_height = blended.applyHeight(base_height, SEA_LEVEL); + + try testing.expect(blended_height > coastal_height); + try testing.expect(blended_height < plains_height); +} + +test "TerrainModifier blend clamps t and switches sea-level clamp at midpoint" { + const raised = TerrainModifier{ .height_offset = 10.0 }; + const wetland = TerrainModifier{ .clamp_to_sea_level = true }; + + try testing.expectApproxEqAbs(@as(f32, 10.0), TerrainModifier.blend(raised, wetland, -1.0).height_offset, 0.0001); + try testing.expectApproxEqAbs(@as(f32, 0.0), TerrainModifier.blend(raised, wetland, 2.0).height_offset, 0.0001); + try testing.expect(!TerrainModifier.blend(raised, wetland, 0.49).clamp_to_sea_level); + try testing.expect(TerrainModifier.blend(raised, wetland, 0.5).clamp_to_sea_level); +} + test "wetland terrain modifiers flatten and lower sampled heights" { const base_low: f32 = 80.0; const base_high: f32 = 112.0; diff --git a/modules/world-worldgen/src/terrain_shape_generator.zig b/modules/world-worldgen/src/terrain_shape_generator.zig index 0db11955..be4be1a2 100644 --- a/modules/world-worldgen/src/terrain_shape_generator.zig +++ b/modules/world-worldgen/src/terrain_shape_generator.zig @@ -26,6 +26,13 @@ pub const SurfaceBuilder = surface_builder_mod.SurfaceBuilder; pub const CoastalSurfaceType = surface_builder_mod.CoastalSurfaceType; const CoastalGenerator = @import("coastal_generator.zig").CoastalGenerator; +const BIOME_INFLUENCE_SAMPLE_OFFSET: f32 = 8.0; +const BIOME_INFLUENCE_SAMPLE_OFFSET_I: i32 = 8; +const BIOME_TERRAIN_CACHE_SIZE: u32 = CHUNK_SIZE_X + @as(u32, @intCast(BIOME_INFLUENCE_SAMPLE_OFFSET_I * 2)); +const OCEAN_PROXIMITY_RADIUS: i32 = 2; +const OCEAN_PROXIMITY_GRID_SIZE: u32 = CHUNK_SIZE_X + @as(u32, @intCast(OCEAN_PROXIMITY_RADIUS * 2)); +const CLAMP_TO_SEA_LEVEL_BLEND_THRESHOLD: f32 = 0.65; + pub const Params = struct { temp_lapse: f32 = 0.25, sea_level: i32 = 64, @@ -69,6 +76,9 @@ pub const ChunkPhaseData = struct { coastal_types: [CHUNK_SIZE_X * CHUNK_SIZE_Z]CoastalSurfaceType, }; +const TerrainModifierCache = [BIOME_TERRAIN_CACHE_SIZE * BIOME_TERRAIN_CACHE_SIZE]biome_mod.TerrainModifier; +const OceanProximityGrid = [OCEAN_PROXIMITY_GRID_SIZE * OCEAN_PROXIMITY_GRID_SIZE]bool; + pub const TerrainShapeGenerator = struct { noise_sampler: NoiseSampler, height_sampler: HeightSampler, @@ -134,28 +144,26 @@ pub const TerrainShapeGenerator = struct { return true; } - fn applyWetlandTerrainModifiers(self: *const TerrainShapeGenerator, phase_data: *ChunkPhaseData) void { - const sea: f32 = @floatFromInt(self.params.sea_level); - - var local_z: u32 = 0; - while (local_z < CHUNK_SIZE_Z) : (local_z += 1) { - var local_x: u32 = 0; - while (local_x < CHUNK_SIZE_X) : (local_x += 1) { - const idx = local_x + local_z * CHUNK_SIZE_X; - const biome_id = phase_data.biome_ids[idx]; - const is_wetland = biome_id == .swamp or - biome_id == .mangrove_swamp or - biome_id == .marsh; - if (!is_wetland) continue; - - const biome_def = biome_mod.getBiomeDefinition(biome_id); - const base_height: f32 = @floatFromInt(phase_data.surface_heights[idx]); - const terrain_height = biome_def.terrain.applyHeight(base_height, sea); - - phase_data.surface_heights[idx] = @intFromFloat(terrain_height); - phase_data.is_underwater_flags[idx] = terrain_height < sea; + fn hasNearbyOceanWater( + ocean_grid: *const OceanProximityGrid, + local_x: u32, + local_z: u32, + ) bool { + const center_x: i32 = @intCast(local_x); + const center_z: i32 = @intCast(local_z); + + var dz: i32 = -OCEAN_PROXIMITY_RADIUS; + while (dz <= OCEAN_PROXIMITY_RADIUS) : (dz += 1) { + var dx: i32 = -OCEAN_PROXIMITY_RADIUS; + while (dx <= OCEAN_PROXIMITY_RADIUS) : (dx += 1) { + if (dx == 0 and dz == 0) continue; + const gx: u32 = @intCast(center_x + dx + OCEAN_PROXIMITY_RADIUS); + const gz: u32 = @intCast(center_z + dz + OCEAN_PROXIMITY_RADIUS); + if (ocean_grid[gx + gz * OCEAN_PROXIMITY_GRID_SIZE]) return true; } } + + return false; } pub fn getNoiseSampler(self: *const TerrainShapeGenerator) *const NoiseSampler { @@ -183,10 +191,105 @@ pub const TerrainShapeGenerator = struct { } pub fn sampleColumnDataWithControls(self: *const TerrainShapeGenerator, wx: f32, wz: f32, reduction: u8, controls: region_pkg.RegionControls) ColumnData { + const terrain_modifier = self.sampleBlendedTerrainModifier(wx, wz, reduction, controls); + return self.sampleColumnDataWithControlsAndTerrainModifier(wx, wz, reduction, controls, terrain_modifier); + } + + fn terrainInfluenceWeight(dx: f32, dz: f32) f32 { + const d2 = dx * dx + dz * dz; + return 1.0 / (1.0 + d2 / (BIOME_INFLUENCE_SAMPLE_OFFSET * BIOME_INFLUENCE_SAMPLE_OFFSET)); + } + + fn sampleTerrainModifierAt( + self: *const TerrainShapeGenerator, + wx: f32, + wz: f32, + reduction: u8, + ) biome_mod.TerrainModifier { + const wx_i: i32 = @intFromFloat(@floor(wx)); + const wz_i: i32 = @intFromFloat(@floor(wz)); + const controls = region_pkg.getBlendedControls(self.getRegionSeed(), wx_i, wz_i); const base_column = self.sampleColumnDataWithControlsAndTerrainModifier(wx, wz, reduction, controls, null); - const preliminary_biome = self.selectBiomeForColumn(base_column, 1); - const biome_def = biome_mod.getBiomeDefinition(preliminary_biome); - return self.sampleColumnDataWithControlsAndTerrainModifier(wx, wz, reduction, controls, biome_def.terrain); + const biome_id = self.selectBiomeForColumn(base_column, 1); + return biome_mod.getBiomeDefinition(biome_id).terrain; + } + + fn blendTerrainModifiers(samples: [9]biome_mod.TerrainModifier) biome_mod.TerrainModifier { + var total_weight: f32 = 0.0; + var height_amplitude: f32 = 0.0; + var smoothing: f32 = 0.0; + var clamp_weight: f32 = 0.0; + var height_offset: f32 = 0.0; + + const offsets = [_][2]f32{ + .{ 0.0, 0.0 }, + .{ -BIOME_INFLUENCE_SAMPLE_OFFSET, 0.0 }, + .{ BIOME_INFLUENCE_SAMPLE_OFFSET, 0.0 }, + .{ 0.0, -BIOME_INFLUENCE_SAMPLE_OFFSET }, + .{ 0.0, BIOME_INFLUENCE_SAMPLE_OFFSET }, + .{ -BIOME_INFLUENCE_SAMPLE_OFFSET, -BIOME_INFLUENCE_SAMPLE_OFFSET }, + .{ BIOME_INFLUENCE_SAMPLE_OFFSET, -BIOME_INFLUENCE_SAMPLE_OFFSET }, + .{ -BIOME_INFLUENCE_SAMPLE_OFFSET, BIOME_INFLUENCE_SAMPLE_OFFSET }, + .{ BIOME_INFLUENCE_SAMPLE_OFFSET, BIOME_INFLUENCE_SAMPLE_OFFSET }, + }; + + for (samples, offsets) |terrain, offset| { + const weight = terrainInfluenceWeight(offset[0], offset[1]); + + total_weight += weight; + height_amplitude += terrain.height_amplitude * weight; + smoothing += terrain.smoothing * weight; + if (terrain.clamp_to_sea_level) clamp_weight += weight; + height_offset += terrain.height_offset * weight; + } + + if (total_weight <= 0.0) return .{}; + return .{ + .height_amplitude = height_amplitude / total_weight, + .smoothing = smoothing / total_weight, + .clamp_to_sea_level = clamp_weight / total_weight >= CLAMP_TO_SEA_LEVEL_BLEND_THRESHOLD, + .height_offset = height_offset / total_weight, + }; + } + + fn sampleBlendedTerrainModifier( + self: *const TerrainShapeGenerator, + wx: f32, + wz: f32, + reduction: u8, + center_controls: region_pkg.RegionControls, + ) biome_mod.TerrainModifier { + _ = center_controls; + const samples = [_]biome_mod.TerrainModifier{ + self.sampleTerrainModifierAt(wx, wz, reduction), + self.sampleTerrainModifierAt(wx - BIOME_INFLUENCE_SAMPLE_OFFSET, wz, reduction), + self.sampleTerrainModifierAt(wx + BIOME_INFLUENCE_SAMPLE_OFFSET, wz, reduction), + self.sampleTerrainModifierAt(wx, wz - BIOME_INFLUENCE_SAMPLE_OFFSET, reduction), + self.sampleTerrainModifierAt(wx, wz + BIOME_INFLUENCE_SAMPLE_OFFSET, reduction), + self.sampleTerrainModifierAt(wx - BIOME_INFLUENCE_SAMPLE_OFFSET, wz - BIOME_INFLUENCE_SAMPLE_OFFSET, reduction), + self.sampleTerrainModifierAt(wx + BIOME_INFLUENCE_SAMPLE_OFFSET, wz - BIOME_INFLUENCE_SAMPLE_OFFSET, reduction), + self.sampleTerrainModifierAt(wx - BIOME_INFLUENCE_SAMPLE_OFFSET, wz + BIOME_INFLUENCE_SAMPLE_OFFSET, reduction), + self.sampleTerrainModifierAt(wx + BIOME_INFLUENCE_SAMPLE_OFFSET, wz + BIOME_INFLUENCE_SAMPLE_OFFSET, reduction), + }; + return blendTerrainModifiers(samples); + } + + fn getCachedBlendedTerrainModifier(cache: *const TerrainModifierCache, local_x: u32, local_z: u32) biome_mod.TerrainModifier { + const center_x = local_x + @as(u32, @intCast(BIOME_INFLUENCE_SAMPLE_OFFSET_I)); + const center_z = local_z + @as(u32, @intCast(BIOME_INFLUENCE_SAMPLE_OFFSET_I)); + const step: u32 = @intCast(BIOME_INFLUENCE_SAMPLE_OFFSET_I); + const samples = [_]biome_mod.TerrainModifier{ + cache[center_x + center_z * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x - step + center_z * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x + step + center_z * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x + (center_z - step) * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x + (center_z + step) * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x - step + (center_z - step) * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x + step + (center_z - step) * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x - step + (center_z + step) * BIOME_TERRAIN_CACHE_SIZE], + cache[center_x + step + (center_z + step) * BIOME_TERRAIN_CACHE_SIZE], + }; + return blendTerrainModifiers(samples); } fn sampleColumnDataWithControlsAndTerrainModifier( @@ -300,6 +403,22 @@ pub const TerrainShapeGenerator = struct { if (!updateSlopes(phase_data, stop_flag)) return false; + var terrain_modifier_cache: TerrainModifierCache = undefined; + var terrain_cache_z: u32 = 0; + while (terrain_cache_z < BIOME_TERRAIN_CACHE_SIZE) : (terrain_cache_z += 1) { + if (stop_flag) |sf| if (sf.*) return false; + var terrain_cache_x: u32 = 0; + while (terrain_cache_x < BIOME_TERRAIN_CACHE_SIZE) : (terrain_cache_x += 1) { + const sample_x = world_x + @as(i32, @intCast(terrain_cache_x)) - BIOME_INFLUENCE_SAMPLE_OFFSET_I; + const sample_z = world_z + @as(i32, @intCast(terrain_cache_z)) - BIOME_INFLUENCE_SAMPLE_OFFSET_I; + terrain_modifier_cache[terrain_cache_x + terrain_cache_z * BIOME_TERRAIN_CACHE_SIZE] = self.sampleTerrainModifierAt( + @floatFromInt(sample_x), + @floatFromInt(sample_z), + 0, + ); + } + } + local_z = 0; while (local_z < CHUNK_SIZE_Z) : (local_z += 1) { if (stop_flag) |sf| if (sf.*) return false; @@ -327,15 +446,15 @@ pub const TerrainShapeGenerator = struct { phase_data.secondary_biome_ids[idx] = biome_id; phase_data.biome_blends[idx] = 0.0; - const biome_def = biome_mod.getBiomeDefinition(biome_id); const wx_i = world_x + @as(i32, @intCast(local_x)); const wz_i = world_z + @as(i32, @intCast(local_z)); + const terrain_modifier = getCachedBlendedTerrainModifier(&terrain_modifier_cache, local_x, local_z); const column = self.sampleColumnDataWithControlsAndTerrainModifier( @floatFromInt(wx_i), @floatFromInt(wz_i), 0, controls.sample(wx_i, wz_i), - biome_def.terrain, + terrain_modifier, ); phase_data.surface_heights[idx] = column.terrain_height_i; phase_data.is_underwater_flags[idx] = column.is_underwater; @@ -395,7 +514,8 @@ pub const TerrainShapeGenerator = struct { const lz = gz * biome_mod.EDGE_STEP + cell_z; if (lx < CHUNK_SIZE_X and lz < CHUNK_SIZE_Z) { const cell_idx = lx + lz * CHUNK_SIZE_X; - phase_data.secondary_biome_ids[cell_idx] = phase_data.biome_ids[cell_idx]; + const original_biome = phase_data.biome_ids[cell_idx]; + phase_data.secondary_biome_ids[cell_idx] = original_biome; phase_data.biome_ids[cell_idx] = transition_biome; phase_data.biome_blends[cell_idx] = switch (edge_info.edge_band) { .inner => 0.3, @@ -413,9 +533,38 @@ pub const TerrainShapeGenerator = struct { } } - self.applyWetlandTerrainModifiers(phase_data); if (!updateSlopes(phase_data, stop_flag)) return false; + var ocean_grid: OceanProximityGrid = undefined; + var ocean_grid_z: u32 = 0; + while (ocean_grid_z < OCEAN_PROXIMITY_GRID_SIZE) : (ocean_grid_z += 1) { + if (stop_flag) |sf| if (sf.*) return false; + var ocean_grid_x: u32 = 0; + while (ocean_grid_x < OCEAN_PROXIMITY_GRID_SIZE) : (ocean_grid_x += 1) { + const local_sample_x = @as(i32, @intCast(ocean_grid_x)) - OCEAN_PROXIMITY_RADIUS; + const local_sample_z = @as(i32, @intCast(ocean_grid_z)) - OCEAN_PROXIMITY_RADIUS; + const in_chunk = local_sample_x >= 0 and local_sample_x < @as(i32, @intCast(CHUNK_SIZE_X)) and + local_sample_z >= 0 and local_sample_z < @as(i32, @intCast(CHUNK_SIZE_Z)); + const has_ocean_water = if (in_chunk) blk: { + const idx = @as(u32, @intCast(local_sample_x)) + @as(u32, @intCast(local_sample_z)) * CHUNK_SIZE_X; + break :blk phase_data.is_ocean_water_flags[idx] and phase_data.is_underwater_flags[idx]; + } else blk: { + const sample_wx = world_x + local_sample_x; + const sample_wz = world_z + local_sample_z; + const sample_controls = region_pkg.getBlendedControls(self.getRegionSeed(), sample_wx, sample_wz); + const column = self.sampleColumnDataWithControlsAndTerrainModifier( + @floatFromInt(sample_wx), + @floatFromInt(sample_wz), + 0, + sample_controls, + null, + ); + break :blk column.is_ocean and column.is_underwater; + }; + ocean_grid[ocean_grid_x + ocean_grid_z * OCEAN_PROXIMITY_GRID_SIZE] = has_ocean_water; + } + } + local_z = 0; while (local_z < CHUNK_SIZE_Z) : (local_z += 1) { if (stop_flag) |sf| if (sf.*) return false; @@ -424,13 +573,16 @@ pub const TerrainShapeGenerator = struct { const idx = local_x + local_z * CHUNK_SIZE_X; const biome_def = biome_mod.getBiomeDefinition(phase_data.biome_ids[idx]); phase_data.filler_depths[idx] = biome_def.surface.depth_range; - phase_data.coastal_types[idx] = CoastalGenerator.getSurfaceType( - &self.surface_builder, - phase_data.continentalness_values[idx], - phase_data.slopes[idx], - phase_data.surface_heights[idx], - phase_data.erosion_values[idx], - ); + phase_data.coastal_types[idx] = if (hasNearbyOceanWater(&ocean_grid, local_x, local_z)) + CoastalGenerator.getSurfaceType( + &self.surface_builder, + phase_data.continentalness_values[idx], + phase_data.slopes[idx], + phase_data.surface_heights[idx], + phase_data.erosion_values[idx], + ) + else + .none; } } diff --git a/src/world_inline_tests.zig b/src/world_inline_tests.zig index afc52067..1ab79b14 100644 --- a/src/world_inline_tests.zig +++ b/src/world_inline_tests.zig @@ -419,7 +419,7 @@ test "normalizeLightValues RGB channels" { test "getBlockColor returns no tint for stone" { var chunk = Chunk.init(0, 0); - const color = biome_color_sampler.getBlockColor(&chunk, .empty, .top, 0, 8, 8, .stone); + const color = biome_color_sampler.getBlockColor(&chunk, .empty, .top, .top, 0, 8, 8, .stone); try testing.expectApproxEqAbs(@as(f32, 1.0), color[0], 0.001); try testing.expectApproxEqAbs(@as(f32, 1.0), color[1], 0.001); try testing.expectApproxEqAbs(@as(f32, 1.0), color[2], 0.001); @@ -427,7 +427,7 @@ test "getBlockColor returns no tint for stone" { test "getBlockColor returns no tint for grass side face" { var chunk = Chunk.init(0, 0); - const color = biome_color_sampler.getBlockColor(&chunk, .empty, .east, 0, 8, 8, .grass); + const color = biome_color_sampler.getBlockColor(&chunk, .empty, .east, .east, 0, 8, 8, .grass); try testing.expectApproxEqAbs(@as(f32, 1.0), color[0], 0.001); try testing.expectApproxEqAbs(@as(f32, 1.0), color[1], 0.001); try testing.expectApproxEqAbs(@as(f32, 1.0), color[2], 0.001); @@ -435,7 +435,7 @@ test "getBlockColor returns no tint for grass side face" { test "getBlockColor returns biome tint for grass top face" { var chunk = Chunk.init(0, 0); - const color = biome_color_sampler.getBlockColor(&chunk, .empty, .top, 64, 8, 8, .grass); + const color = biome_color_sampler.getBlockColor(&chunk, .empty, .top, .top, 64, 8, 8, .grass); // Plains biome grass color should not be {1, 1, 1} (it should be tinted) try testing.expect(color[0] != 1.0 or color[1] != 1.0 or color[2] != 1.0); } diff --git a/src/worldgen_tests.zig b/src/worldgen_tests.zig index 042756ec..1eb247ab 100644 --- a/src/worldgen_tests.zig +++ b/src/worldgen_tests.zig @@ -218,9 +218,9 @@ test "WorldGen stable chunk fingerprints for known seed" { }; const expected = [_]u64{ - 11844084116277698429, - 9139389730069537271, - 8491863551076282083, + 2995571678741719148, + 13866304399676112481, + 7446572877677241388, }; for (positions, 0..) |pos, i| { @@ -885,10 +885,10 @@ test "SurfaceBuilder coastal type detection" { try testing.expectEqual(CoastalSurfaceType.sand_beach, sand); const cliff = builder.getCoastalSurfaceType(0.37, 6, 65, 0.3); - try testing.expectEqual(CoastalSurfaceType.cliff, cliff); + try testing.expectEqual(CoastalSurfaceType.none, cliff); const gravel = builder.getCoastalSurfaceType(0.37, 2, 65, 0.8); - try testing.expectEqual(CoastalSurfaceType.gravel_beach, gravel); + try testing.expectEqual(CoastalSurfaceType.none, gravel); const inland = builder.getCoastalSurfaceType(0.50, 1, 70, 0.3); try testing.expectEqual(CoastalSurfaceType.none, inland);