Source code for terrain_gen

"""
Core procedural terrain and biome generation mechanics.

This module utilizes high-performance `@njit` (Numba) compilation to execute
complex 3D Simplex noise and fractional Brownian motion calculations across the CPU.
It dictates the shaping of landmasses, continentalness modifiers, biome distributions
(sand, snow, grass), cave carving logic, and structural tree generation entirely
lock-free to prevent main-thread latency.
"""

# """
from random import random
from typing import Any, Tuple

from numba import njit

from noise import noise2, noise3
from settings import (
    AIR,
    CENTER_Y,
    CHUNK_AREA,
    CHUNK_SIZE,
    DIRT,
    GLASS,
    GRASS,
    LEAVES,
    SAND,
    SNOW,
    STONE,
    STONE_LVL,
    TREE_H_HEIGHT,
    TREE_H_WIDTH,
    TREE_HEIGHT,
    WATER,
    WATER_LINE,
    WOOD,
    WORLD_HEIGHT,
)


# Terrain generator with temperature, moisture, and continentalness to create distinct biomes and landforms.
# Has Biome Dithering to create more natural transitions and less blocky borders.
[docs] @njit(cache=True, fastmath=True, nogil=True) def get_biome(x: float, z: float, perm_array: Any) -> Tuple[float, float]: """ Evaluates Simplex noise to determine the overarching temperature and moisture levels of a specific vertical column, shaping its respective biome. """ # Calculate temperature temp = noise2(x * 0.002, z * 0.002, perm_array) # Calculate moisture with spatial offset moist = noise2(x * 0.002 + 100.0, z * 0.002 + 100.0, perm_array) return temp, moist
# Procedural Terrain Generation using Noise # This function calculates the Y-height of the terrain for any given (X, Z) coordinate. # # How it works: # 1. It uses Fractional Brownian Motion (fBm) by layering multiple "octaves" of # Simplex noise. Each subsequent octave has double the frequency (f2, f4, f8) # and half the amplitude (a2, a4, a8). # 2. Summing these layers creates natural-looking fractal terrain, where the first # layer defines the massive mountains/valleys, and the last layer defines small bumps. # 3. We then use a "Continentalness" noise map to warp the final height. If the # continentalness is low, we forcefully squash the height map to create flat # oceans. If it's high, we amplify the amplitude to create towering peaks. # # References: # - Making Maps with Noise (Amazing visual guide): https://www.redblobgames.com/maps/terrain-from-noise/ # - Fractional Brownian Motion: https://en.wikipedia.org/wiki/Fractional_Brownian_motion # - Simplex Noise overview: https://en.wikipedia.org/wiki/Simplex_noise
[docs] @njit(cache=True, fastmath=True, nogil=True) def get_height(x: float, z: float, perm_array: Any) -> int: """ Calculates the absolute maximum surface elevation of the terrain at a specific X,Z coordinate using fractional Brownian motion and continentalness modifiers. """ # Calculate continentalness cont = noise2(x * 0.003 + 100.0, z * 0.003 + 100.0, perm_array) # Setup noise octaves a1 = CENTER_Y a2, a4, a8 = a1 * 0.5, a1 * 0.25, a1 * 0.125 f1 = 0.005 f2, f4, f8 = f1 * 2, f1 * 4, f1 * 8 # Evaluate base height and details base_h = noise2(x * f1, z * f1, perm_array) * a1 + a1 detail_1 = noise2(x * f2, z * f2, perm_array) * a2 - a2 detail_2 = noise2(x * f4, z * f4, perm_array) * a4 + a4 detail_3 = noise2(x * f8, z * f8, perm_array) * a8 - a8 height = base_h + detail_1 + detail_2 + detail_3 # Shape terrain by continentalness if cont < -0.2: w = min((-0.2 - cont) * 5.0, 1.0) target_h = WATER_LINE - 2 + detail_2 * 0.3 + detail_3 * 0.3 height = height * (1.0 - w) + target_h * w elif cont > 0.4: w = min((cont - 0.4) * 5.0, 1.0) target_h = base_h * 1.5 + detail_1 * 2.0 + detail_2 + detail_3 + 30 height = height * (1.0 - w) + target_h * w elif 0.1 < cont <= 0.3: w = min((cont - 0.1) * 10.0, 1.0) * min((0.3 - cont) * 10.0, 1.0) plat_h = CENTER_Y + 12 if height > plat_h: flattened = plat_h + (height - plat_h) * 0.1 height = height * (1.0 - w) + flattened * w # Apply limits and micro-noise height = max(height, noise2(x * f8, z * f8, perm_array) + 2) height = min(height, WORLD_HEIGHT * CHUNK_SIZE - 2) height = max(height, 2.0) return int(height)
[docs] @njit(cache=True, fastmath=True, nogil=True) def get_index(x: int, y: int, z: int) -> int: """ Translates a localized 3D chunk coordinate (x, y, z) into a flattened 1D array index for highly optimized, contiguous memory access. """ return x + CHUNK_SIZE * z + CHUNK_AREA * y
[docs] @njit(cache=True, fastmath=True, nogil=True) def set_voxel_column( voxels: Any, x: int, z: int, chunk_x: int, chunk_y: int, chunk_z: int, perm_array: Any, perm_grad_array: Any ) -> None: """ Procedurally generates a single vertical column of blocks within a chunk. Applies complex biome mapping, depth stratification, and 3D cave carving logic. """ # Calculate global coordinates and height world_x = x + chunk_x world_z = z + chunk_z world_height = get_height(world_x, world_z, perm_array) max_h = max(world_height, int(WATER_LINE) + 1) local_height = min(max_h - chunk_y, CHUNK_SIZE) if local_height <= 0: return # Determine biome properties temp, moist = get_biome(world_x, world_z, perm_array) dither = ( noise2(world_x * 0.2, world_z * 0.2, perm_array) * 0.05 + noise2(world_x * 0.8, world_z * 0.8, perm_array) * 0.03 ) temp += dither moist += dither is_desert = temp > 0.3 and moist < -0.2 is_snow = temp < -0.2 # Identify terrain features is_underwater = world_height <= WATER_LINE is_beach = world_height <= WATER_LINE + 2 and not is_underwater # Assign surface blocks based on biome if is_underwater or is_beach or is_desert: surface_id = SAND subsurface_id = SAND elif is_snow: surface_id = SNOW subsurface_id = DIRT else: surface_id = GRASS subsurface_id = DIRT # Evaluate depth and noise masks dirt_depth = int((noise2(world_x * 0.1, world_z * 0.1, perm_array) * 0.5 + 0.5) * 5) + 3 entrance_mask = noise2(world_x * 0.02 + 200.0, world_z * 0.02 + 200.0, perm_array) crust = noise2(world_x * 0.1, world_z * 0.1, perm_array) * 3 + 3 # Generate vertical column blocks for y in range(local_height): world_y = y + chunk_y voxel_id = 0 if world_y > world_height - 1: if world_y <= WATER_LINE: voxel_id = WATER else: if world_y == world_height - 1: voxel_id = surface_id elif world_y >= world_height - dirt_depth: voxel_id = subsurface_id else: voxel_id = STONE if world_y > crust: surface_dist = world_height - world_y if not ((is_underwater or is_beach) and surface_dist <= dirt_depth): cave_noise = noise3(world_x * 0.09, world_y * 0.09, world_z * 0.09, perm_array, perm_grad_array) cave_threshold = 0.0 if surface_dist < 14: taper_factor = (14 - surface_dist) / 14.0 target_threshold = 0.3 + max(0.0, 0.5 - entrance_mask) * 4.0 cave_threshold = target_threshold * taper_factor if cave_noise > cave_threshold: voxel_id = 0 # Set block ID if voxel_id: voxels[get_index(x, y, z)] = voxel_id # Place vegetation if ( world_y == world_height - 1 and voxel_id == surface_id and not is_underwater and not is_beach and world_y < STONE_LVL ): tree_prob = 0.0 if surface_id == GRASS: if moist > 0.4: tree_prob = 0.04 elif moist > 0.0: tree_prob = 0.005 else: tree_prob = 0.0001 if tree_prob > 0: place_tree(voxels, x, y, z, surface_id, tree_prob)
[docs] @njit(cache=True, fastmath=True, nogil=True) def place_tree(voxels: Any, x: int, y: int, z: int, voxel_id: int, tree_prob: float) -> None: """ Constructs a localized tree structure (wood trunk and spherical leaf crown) within the chunk volume if probability and physical boundaries allow for it. """ # Evaluate probability and bounds rnd = random() if rnd > tree_prob: return None if y + TREE_HEIGHT >= CHUNK_SIZE: return None if x - TREE_H_WIDTH < 0 or x + TREE_H_WIDTH >= CHUNK_SIZE: return None if z - TREE_H_WIDTH < 0 or z + TREE_H_WIDTH >= CHUNK_SIZE: return None # Place dirt foundation voxels[get_index(x, y, z)] = DIRT # Generate leaf crown m = 0 for n, iy in enumerate(range(TREE_H_HEIGHT, TREE_HEIGHT - 1)): k = iy % 2 rng = int(random() * 2) for ix in range(-TREE_H_WIDTH + m, TREE_H_WIDTH - m * rng): for iz in range(-TREE_H_WIDTH + m * rng, TREE_H_WIDTH - m): if (ix + iz) % 4: voxels[get_index(x + ix + k, y + iy, z + iz + k)] = LEAVES m += 1 if n > 0 else 3 if n > 1 else 0 # Generate wood trunk for iy in range(1, TREE_HEIGHT - 2): voxels[get_index(x, y + iy, z)] = WOOD # Place top leaf voxels[get_index(x, y + TREE_HEIGHT - 2, z)] = LEAVES
[docs] @njit(cache=True, fastmath=True, nogil=True) def fill_initial_sunlight( voxels: Any, lightmap: Any, chunk_x: int, chunk_y: int, chunk_z: int, perm_array: Any ) -> None: """ Initializes a newly generated chunk's lightmap by simulating direct, overhead sunlight falling vertically onto the procedural terrain layout. """ # Evaluate sunlight per column for x in range(CHUNK_SIZE): for z in range(CHUNK_SIZE): world_x = x + chunk_x world_z = z + chunk_z world_height = get_height(world_x, world_z, perm_array) # Assign sunlight based on block depth for y in range(CHUNK_SIZE): world_y = y + chunk_y index = get_index(x, y, z) if world_y >= world_height: voxel_id = voxels[index] if voxel_id == AIR or voxel_id == GLASS: lightmap[index] = (15 << 4) | 0 elif voxel_id == WATER: depth = world_height - world_y sun = max(0, 15 + depth * 2) lightmap[index] = (sun << 4) | 0 elif voxel_id == LEAVES: lightmap[index] = (14 << 4) | 0 else: lightmap[index] = 0 else: lightmap[index] = 0