@tool
class_name ProcTerrainGenerator extends DCTerrainGeneratorScript
## Proc-nodes terrain generator (Phase 8 demonstration)
##
## A DCTerrainGeneratorScript that computes density using GDScript math.
## Assign this Resource to a DCTerrain node's "generator" property.
##
## Convention: positive = solid, negative = air.
## The surface forms where density crosses zero.
##
## NOTE: _evaluate() is called from worker threads through a mutex.
## Keep the formula simple — complex logic serializes all 4 workers.
## Ground plane height (terrain surface baseline)
@export var ground_height: float = 0.0
## Primary noise: large-scale terrain features
@export_group("Primary Noise")
@export var primary_frequency: float = 0.02
@export var primary_amplitude: float = 15.0
@export var primary_octaves: int = 4
## Detail noise: surface roughness
@export_group("Detail Noise")
@export var detail_frequency: float = 0.08
@export var detail_amplitude: float = 3.0
@export var detail_octaves: int = 3
## Ridge noise: sharp mountain ridges
@export_group("Ridge Noise")
@export var ridge_frequency: float = 0.015
@export var ridge_amplitude: float = 20.0
@export var ridge_enabled: bool = true
## Terrain bounds
@export_group("Bounds")
@export var solid_floor_depth: float = 30.0
@export var sky_ceiling: float = 50.0
func _evaluate(x: float, y: float, z: float, lod_voxel_size: float) -> float:
# Base density: ground plane. Positive below ground, negative above.
var density: float = ground_height - y
# Primary noise: rolling hills and valleys
var primary: float = _fbm(x * primary_frequency, z * primary_frequency, primary_octaves, 0.5, 2.0)
density += primary * primary_amplitude
# Ridge noise: sharp peaks (absolute value of noise creates ridges)
if ridge_enabled:
var ridge: float = _ridge_fbm(x * ridge_frequency, z * ridge_frequency, 3, 0.5, 2.0)
density += ridge * ridge_amplitude
# Detail noise: surface roughness (Nyquist-clamped by LOD)
# Skip detail when voxel size is too large to represent it
if lod_voxel_size < 1.0 / detail_frequency:
var detail: float = _fbm(
x * detail_frequency + 100.0,
z * detail_frequency + 100.0,
detail_octaves, 0.5, 2.0)
density += detail * detail_amplitude
# Hard floor: force solid below a certain depth
if y < -solid_floor_depth:
density = maxf(density, (-solid_floor_depth - y) * 2.0)
# Sky ceiling: force air above a certain height
if y > sky_ceiling:
density = minf(density, (sky_ceiling - y) * 2.0)
return density
# Fractional Brownian Motion (value noise approximation using trig)
# Uses sin-based pseudo-noise since we don't have FastNoiseLite in _evaluate()
func _fbm(px: float, pz: float, octaves: int, gain: float, lacunarity: float) -> float:
var value: float = 0.0
var amplitude: float = 1.0
var frequency: float = 1.0
var total_amplitude: float = 0.0
for i in range(octaves):
value += _hash_noise(px * frequency, pz * frequency) * amplitude
total_amplitude += amplitude
amplitude *= gain
frequency *= lacunarity
return value / total_amplitude
# Ridge noise: abs(noise) inverted to create sharp peaks
func _ridge_fbm(px: float, pz: float, octaves: int, gain: float, lacunarity: float) -> float:
var value: float = 0.0
var amplitude: float = 1.0
var frequency: float = 1.0
var total_amplitude: float = 0.0
for i in range(octaves):
var n: float = 1.0 - absf(_hash_noise(px * frequency, pz * frequency))
n = n * n # sharpen the ridges
value += n * amplitude
total_amplitude += amplitude
amplitude *= gain
frequency *= lacunarity
return value / total_amplitude
# Trig-based pseudo-noise (deterministic, no state, thread-safe)
# Not true Perlin/Simplex but adequate for terrain demonstration.
# Returns [-1, 1].
func _hash_noise(x: float, z: float) -> float:
# Multiple overlapping sine waves create pseudo-random patterns
var h: float = sin(x * 127.1 + z * 311.7) * 43758.5453
h = h - floorf(h) # fract
var h2: float = sin(x * 269.5 + z * 183.3) * 28947.7291
h2 = h2 - floorf(h2)
return (h + h2) - 1.0 # remap [0,2] -> [-1,1]