OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / proc_terrain_generator.gd

120 lines · gd ref:
@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]