@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)")