OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / terrain_test.gd

198 lines · gd ref:
@tool
extends Node3D

## Terrain generation test — evaluate proc-nodes terrain graphs.
## Switch presets or load saved .tres graphs.

@export_enum("Mountain + Erosion", "Perlin Hills", "Ridged Peaks", "Erosion Only") var preset: int = 0

@export var generate: bool = false:
	set(v):
		if v: _generate()
		generate = false

@export_file("*.tres") var load_graph: String = "":
	set(v):
		load_graph = v
		if v != "" and ResourceLoader.exists(v):
			var g := ResourceLoader.load(v) as ProcNodeGraph
			if g:
				_proc().set_graph(g)
				call_deferred("_apply_mat")
				print("[Terrain] Loaded: ", v)

@export var save_as: String = "res://graphs/terrain.tres"

@export var save: bool = false:
	set(v):
		if v: _save()
		save = false

func _ready() -> void:
	call_deferred("_setup")

func _setup() -> void:
	_apply_mat()
	var p := _proc()
	if p and (p.get_graph() == null or p.get_graph().get_node_count() == 0):
		_generate()

func _proc() -> ProcGeometryNode:
	return $Terrain as ProcGeometryNode

func _apply_mat() -> void:
	var p := _proc()
	if not p: return
	var mo := p.get_node_or_null(NodePath("MeshOutput")) as MeshInstance3D
	if mo:
		var mat := StandardMaterial3D.new()
		mat.albedo_color = Color(0.65, 0.65, 0.65)
		mat.roughness = 0.85
		mo.material_override = mat

func _save() -> void:
	var p := _proc()
	if not p or not p.get_graph(): return
	var dir := save_as.get_base_dir()
	if not DirAccess.dir_exists_absolute(dir):
		DirAccess.make_dir_recursive_absolute(dir)
	ResourceSaver.save(p.get_graph(), save_as)
	print("[Terrain] Saved: ", save_as)

func _generate() -> void:
	var p := _proc()
	if not p: return
	match preset:
		0: _mountain_erosion()
		1: _perlin_hills()
		2: _ridged_peaks()
		3: _erosion_only()
	call_deferred("_apply_mat")

func _grid(g: ProcNodeGraph, size: float = 20.0, res: int = 200) -> int:
	var idx := g.add_node(MeshGridNode.new())
	g.get_node(idx).set_input_default(0, size)
	g.get_node(idx).set_input_default(1, size)
	g.get_node(idx).set_input_default(2, res)
	g.get_node(idx).set_input_default(3, res)
	return idx

func _mountain_erosion() -> void:
	var g := ProcNodeGraph.new()
	var grid := _grid(g, 30.0, 250)

	# Base terrain — Perlin fBM, big smooth mountains
	var disp := g.add_node(DisplaceNode.new())
	g.get_node(disp).set_input_default(1, 6.0)   # Strength
	g.get_node(disp).set_input_default(2, 1.0)   # Scale (large features)
	g.get_node(disp).set_input_default(3, 7)     # Detail (high octaves)
	g.get_node(disp).set_input_default(4, 0.55)  # Roughness
	g.get_node(disp).set_input_default(5, 2.0)   # Lacunarity

	# Erosion — multi-octave analytical gullies (no SlopeGradient needed)
	# Uses runevision's defaults: scale=0.08, strength=0.16, slopePower=0.6
	var erosion := g.add_node(ErosionNoiseNode.new())
	# Defaults are already good (from Shadertoy): scale=0.08, strength=0.16,
	# slopePower=0.6, cellScale=1.0, octaves=5, gain=0.5, lacunarity=2.0

	g.connect_sockets(grid, 0, disp, 0)
	g.connect_sockets(disp, 0, erosion, 0)

	g.get_node(grid).set_graph_position(Vector2(-400, 0))
	g.get_node(disp).set_graph_position(Vector2(-200, 0))
	g.get_node(erosion).set_graph_position(Vector2(0, 0))

	_proc().set_graph(g)
	print("[Terrain] Mountain + Erosion (3 nodes, 250x250)")

func _perlin_hills() -> void:
	var g := ProcNodeGraph.new()
	var grid := _grid(g)

	var disp := g.add_node(DisplaceNode.new())
	g.get_node(disp).set_input_default(1, 3.0)
	g.get_node(disp).set_input_default(2, 2.0)
	g.get_node(disp).set_input_default(3, 5)
	g.get_node(disp).set_input_default(4, 0.5)

	g.connect_sockets(grid, 0, disp, 0)
	g.get_node(grid).set_graph_position(Vector2(-200, 0))
	g.get_node(disp).set_graph_position(Vector2(0, 0))

	_proc().set_graph(g)
	print("[Terrain] Perlin Hills (2 nodes)")

func _ridged_peaks() -> void:
	var g := ProcNodeGraph.new()
	var grid := _grid(g)

	var pos := g.add_node(PositionNode.new())
	# MusgraveTextureNode was deleted from proc-nodes on 2026-09-04: Blender
	# removed ShaderNodeTexMusgrave in 4.1 and folded it into Noise Texture's
	# noise_type, so it cannot round-trip. PerlinNoiseNode ("Noise Texture")
	# carries the same five variants -- but NOT in the same order, and NOT at
	# the same socket indices:
	#
	#   type   Musgrave 0=FBM 1=MULTIFRACTAL 2=HETERO 3=HYBRID 4=RIDGED
	#          Noise    0=Multifractal 1=fBM 2=Hybrid 3=Ridged 4=Hetero
	#   socket Noise Texture inserts W at index 1, so Scale/Detail/Roughness/
	#          Lacunarity move from 1/2/3/4 to 2/3/4/5.
	#
	# So RIDGED is 3 here, not 4. A mechanical rename would have left this
	# graph on Hetero with Scale in the W socket, and nothing would have said so.
	var musg := g.add_node(PerlinNoiseNode.new())
	(g.get_node(musg) as PerlinNoiseNode).set_noise_type(3) # Ridged
	g.get_node(musg).set_input_default(2, 1.5)
	g.get_node(musg).set_input_default(3, 6.0)
	g.get_node(musg).set_input_default(4, 0.5)
	g.get_node(musg).set_input_default(5, 2.0)

	var mul := g.add_node(MathNode.new())
	(g.get_node(mul) as MathNode).set_operation(2) # Multiply
	g.get_node(mul).set_input_default(1, 5.0)

	var comb := g.add_node(CombineXYZNode.new())
	var setp := g.add_node(SetPositionNode.new())

	g.connect_sockets(grid, 0, pos, 0)
	g.connect_sockets(pos, 0, musg, 0)
	g.connect_sockets(musg, 0, mul, 0)
	g.connect_sockets(mul, 0, comb, 1) # Y channel
	g.connect_sockets(comb, 0, setp, 1)
	g.connect_sockets(grid, 0, setp, 0)

	g.get_node(grid).set_graph_position(Vector2(-500, 0))
	g.get_node(pos).set_graph_position(Vector2(-300, -80))
	g.get_node(musg).set_graph_position(Vector2(-100, -80))
	g.get_node(mul).set_graph_position(Vector2(100, -80))
	g.get_node(comb).set_graph_position(Vector2(100, 40))
	g.get_node(setp).set_graph_position(Vector2(300, 0))

	_proc().set_graph(g)
	print("[Terrain] Ridged Peaks (6 nodes)")

func _erosion_only() -> void:
	## Flat grid + erosion only — test the erosion pattern in isolation.
	## First displace slightly (erosion needs slope to work on)
	var g := ProcNodeGraph.new()
	var grid := _grid(g)

	var disp := g.add_node(DisplaceNode.new())
	g.get_node(disp).set_input_default(1, 2.0)   # Mild height
	g.get_node(disp).set_input_default(2, 1.0)   # Large features
	g.get_node(disp).set_input_default(3, 3)     # Low detail
	g.get_node(disp).set_input_default(4, 0.5)

	var erosion := g.add_node(ErosionNoiseNode.new())
	# Use defaults (scale=0.08, strength=0.16, etc.)

	g.connect_sockets(grid, 0, disp, 0)
	g.connect_sockets(disp, 0, erosion, 0)

	g.get_node(grid).set_graph_position(Vector2(-400, 0))
	g.get_node(disp).set_graph_position(Vector2(-200, 0))
	g.get_node(erosion).set_graph_position(Vector2(0, 0))

	_proc().set_graph(g)
	print("[Terrain] Erosion Only (3 nodes)")