Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions modules/world-core/src/block_registry_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
8 changes: 3 additions & 5 deletions modules/world-meshing/src/meshing/biome_color_sampler.zig
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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 };
}
Expand Down
2 changes: 1 addition & 1 deletion modules/world-meshing/src/meshing/custom_mesh_mesher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 8 additions & 2 deletions modules/world-meshing/src/meshing/greedy_mesher.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/world-worldgen/src/biome_edge_detector.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
46 changes: 28 additions & 18 deletions modules/world-worldgen/src/biome_registry.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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 = &.{} },
},
.{
Expand Down Expand Up @@ -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,
Expand All @@ -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 },
Expand All @@ -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 },
Expand All @@ -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 },
Expand Down Expand Up @@ -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 } },
},
Expand Down
17 changes: 10 additions & 7 deletions modules/world-worldgen/src/biome_registry_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -377,15 +377,16 @@ 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" {
const stony = getBiomeDefinition(.stony_shore);
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);
Expand All @@ -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" {
Expand All @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions modules/world-worldgen/src/coastal_generator_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -133,15 +133,15 @@ 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,
p.cliff_min_slope + 1,
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(
Expand Down
27 changes: 16 additions & 11 deletions modules/world-worldgen/src/height_sampler.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ============================================================================
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading