OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / noise_test.gd

251 lines · gd ref:
@tool
extends Node3D

## Noise visualization test — renders proc-node graphs as black/white
## vertex colors on a flat grid. Use to verify noise functions match
## between Godot and Blender.

@export_file("*.tres") var graph_file: String = "":
	set(v):
		graph_file = v
		if v != "":
			_load_graph()

@export var rebuild: bool = false:
	set(v):
		if v: _build_current()
		rebuild = false

@export_enum("Perlin fBM", "Ridged Musgrave", "Voronoi", "Phacelle Directional") var preset: int = 0:
	set(v):
		preset = v
		_build_preset(v)

@export_group("Grid")
@export var grid_size: float = 10.0
@export var grid_resolution: int = 100

@export_group("Noise")
@export var noise_scale: float = 5.0
@export var noise_detail: float = 4.0
@export var noise_roughness: float = 0.5
@export var noise_threshold: float = 0.5

var _proc: ProcGeometryNode
var _mat: StandardMaterial3D

func _ready() -> void:
	_proc = $ProcGeo as ProcGeometryNode
	if not _proc:
		return

	# Vertex color material
	_mat = StandardMaterial3D.new()
	_mat.vertex_color_use_as_albedo = true
	_mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED

	if graph_file != "" and ResourceLoader.exists(graph_file):
		_load_graph()
	elif _proc.get_graph() == null or _proc.get_graph().get_node_count() == 0:
		_build_preset(preset)

	call_deferred("_apply_material")

func _apply_material() -> void:
	if not _proc:
		return
	var mesh_out := _proc.get_node_or_null(NodePath("MeshOutput")) as MeshInstance3D
	if mesh_out and _mat:
		mesh_out.material_override = _mat

func _load_graph() -> void:
	if not _proc or graph_file == "":
		return
	if not ResourceLoader.exists(graph_file):
		print("[NoiseTest] File not found: ", graph_file)
		return
	var g := ResourceLoader.load(graph_file) as ProcNodeGraph
	if g:
		_proc.set_graph(g)
		call_deferred("_apply_material")
		print("[NoiseTest] Loaded: ", graph_file)

func _build_current() -> void:
	_build_preset(preset)

func _build_preset(p_preset: int) -> void:
	if not _proc:
		_proc = $ProcGeo as ProcGeometryNode
	if not _proc:
		return

	match p_preset:
		0: _build_perlin()
		1: _build_ridged()
		2: _build_voronoi()
		3: _build_phacelle()

	call_deferred("_apply_material")

func _build_perlin() -> void:
	# MeshGrid → Position → PerlinNoise(fBM) → Compare → StoreNamedAttribute("color")
	var g := ProcNodeGraph.new()

	var grid := g.add_node(MeshGridNode.new())
	g.get_node(grid).set_input_default(0, grid_size)
	g.get_node(grid).set_input_default(1, grid_size)
	g.get_node(grid).set_input_default(2, grid_resolution)
	g.get_node(grid).set_input_default(3, grid_resolution)

	var pos := g.add_node(PositionNode.new())
	var noise := g.add_node(PerlinNoiseNode.new())
	g.get_node(noise).set_input_default(2, noise_scale)
	g.get_node(noise).set_input_default(3, noise_detail)
	g.get_node(noise).set_input_default(4, noise_roughness)

	var compare := g.add_node(CompareNode.new())
	g.get_node(compare).set_input_default(1, noise_threshold)

	var store := g.add_node(StoreNamedAttributeNode.new())
	(g.get_node(store) as StoreNamedAttributeNode).set_attr_name("color")

	g.connect_sockets(grid, 0, pos, 0)
	g.connect_sockets(pos, 0, noise, 0)
	g.connect_sockets(noise, 0, compare, 0)
	g.connect_sockets(compare, 0, store, 1)
	g.connect_sockets(grid, 0, store, 0)

	g.get_node(grid).set_graph_position(Vector2(-400, 0))
	g.get_node(pos).set_graph_position(Vector2(-200, -80))
	g.get_node(noise).set_graph_position(Vector2(0, -80))
	g.get_node(compare).set_graph_position(Vector2(200, -80))
	g.get_node(store).set_graph_position(Vector2(400, 0))

	_proc.set_graph(g)
	print("[NoiseTest] Perlin fBM preset (scale=", noise_scale, " threshold=", noise_threshold, ")")

func _build_ridged() -> void:
	# MeshGrid → Position → Musgrave(Ridged) → Compare → StoreNamedAttribute("color")
	var g := ProcNodeGraph.new()

	var grid := g.add_node(MeshGridNode.new())
	g.get_node(grid).set_input_default(0, grid_size)
	g.get_node(grid).set_input_default(1, grid_size)
	g.get_node(grid).set_input_default(2, grid_resolution)
	g.get_node(grid).set_input_default(3, grid_resolution)

	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 noise := g.add_node(PerlinNoiseNode.new())
	(g.get_node(noise) as PerlinNoiseNode).set_noise_type(3)  # Ridged
	g.get_node(noise).set_input_default(2, noise_scale)
	g.get_node(noise).set_input_default(3, noise_detail)
	g.get_node(noise).set_input_default(4, noise_roughness)

	var compare := g.add_node(CompareNode.new())
	g.get_node(compare).set_input_default(1, noise_threshold)

	var store := g.add_node(StoreNamedAttributeNode.new())
	(g.get_node(store) as StoreNamedAttributeNode).set_attr_name("color")

	g.connect_sockets(grid, 0, pos, 0)
	g.connect_sockets(pos, 0, noise, 0)
	g.connect_sockets(noise, 0, compare, 0)
	g.connect_sockets(compare, 0, store, 1)
	g.connect_sockets(grid, 0, store, 0)

	g.get_node(grid).set_graph_position(Vector2(-400, 0))
	g.get_node(pos).set_graph_position(Vector2(-200, -80))
	g.get_node(noise).set_graph_position(Vector2(0, -80))
	g.get_node(compare).set_graph_position(Vector2(200, -80))
	g.get_node(store).set_graph_position(Vector2(400, 0))

	_proc.set_graph(g)
	print("[NoiseTest] Ridged Musgrave preset (scale=", noise_scale, ")")

func _build_voronoi() -> void:
	# MeshGrid → Position → Voronoi → Compare → StoreNamedAttribute("color")
	var g := ProcNodeGraph.new()

	var grid := g.add_node(MeshGridNode.new())
	g.get_node(grid).set_input_default(0, grid_size)
	g.get_node(grid).set_input_default(1, grid_size)
	g.get_node(grid).set_input_default(2, grid_resolution)
	g.get_node(grid).set_input_default(3, grid_resolution)

	var pos := g.add_node(PositionNode.new())
	var noise := g.add_node(VoronoiTextureNode.new())
	g.get_node(noise).set_input_default(1, noise_scale)

	var compare := g.add_node(CompareNode.new())
	g.get_node(compare).set_input_default(1, noise_threshold)

	var store := g.add_node(StoreNamedAttributeNode.new())
	(g.get_node(store) as StoreNamedAttributeNode).set_attr_name("color")

	g.connect_sockets(grid, 0, pos, 0)
	g.connect_sockets(pos, 0, noise, 0)
	g.connect_sockets(noise, 0, compare, 0)
	g.connect_sockets(compare, 0, store, 1)
	g.connect_sockets(grid, 0, store, 0)

	g.get_node(grid).set_graph_position(Vector2(-400, 0))
	g.get_node(pos).set_graph_position(Vector2(-200, -80))
	g.get_node(noise).set_graph_position(Vector2(0, -80))
	g.get_node(compare).set_graph_position(Vector2(200, -80))
	g.get_node(store).set_graph_position(Vector2(400, 0))

	_proc.set_graph(g)
	print("[NoiseTest] Voronoi preset (scale=", noise_scale, ")")

func _build_phacelle() -> void:
	# MeshGrid → Position → PhacelleNoise → Compare → StoreNamedAttribute("color")
	# Uses a fixed direction (1,0,0) — stripes along X
	var g := ProcNodeGraph.new()

	var grid := g.add_node(MeshGridNode.new())
	g.get_node(grid).set_input_default(0, grid_size)
	g.get_node(grid).set_input_default(1, grid_size)
	g.get_node(grid).set_input_default(2, grid_resolution)
	g.get_node(grid).set_input_default(3, grid_resolution)

	# Phacelle needs geometry input (not position field) — it does its own per-vertex eval
	# But for black/white visualization we need its Value output → Compare → Store
	# PhacelleNoise takes Geometry socket 0, outputs Value field at socket 1
	var phacelle := g.add_node(ErosionNoiseNode.new())
	g.get_node(phacelle).set_input_default(1, noise_scale)  # Scale
	g.get_node(phacelle).set_input_default(2, Vector3(1, 0, 0))  # Direction
	g.get_node(phacelle).set_input_default(3, 1.0)  # Frequency
	g.get_node(phacelle).set_input_default(6, 0.0)  # Strength = 0 (no displacement)

	var compare := g.add_node(CompareNode.new())
	g.get_node(compare).set_input_default(1, 0.0)  # Threshold at 0 (Phacelle outputs [-1,1])

	var store := g.add_node(StoreNamedAttributeNode.new())
	(g.get_node(store) as StoreNamedAttributeNode).set_attr_name("color")

	g.connect_sockets(grid, 0, phacelle, 0)       # Grid geo → Phacelle geo
	g.connect_sockets(phacelle, 1, compare, 0)     # Phacelle Value field → Compare A
	g.connect_sockets(compare, 0, store, 1)         # Compare result → Store value
	g.connect_sockets(phacelle, 0, store, 0)        # Phacelle geo (passthrough) → Store geo

	g.get_node(grid).set_graph_position(Vector2(-400, 0))
	g.get_node(phacelle).set_graph_position(Vector2(-100, 0))
	g.get_node(compare).set_graph_position(Vector2(200, -80))
	g.get_node(store).set_graph_position(Vector2(400, 0))

	_proc.set_graph(g)
	print("[NoiseTest] Phacelle directional preset (scale=", noise_scale, " dir=(1,0,0))")