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