extends Node3D
## Proc-nodes erosion terrain test — fly camera + CSG edits.
## Requires both dc-terrain and proc-nodes GDExtensions.
@onready var terrain: Node3D = $DCTerrain
@onready var stats_label: Label = $CanvasLayer/StatsLabel
@onready var camera: Camera3D = $Camera3D
var _gen: DCTerrainGeneratorProcNodes
var _terrain_ready: bool = false
var _wait_frames: int = 0
var _wireframe: bool = false
var _edit_radius: float = 3.0
# Fly camera
var _fly_speed: float = 20.0
var _mouse_sens: float = 0.003
var _cam_yaw: float = 0.0
var _cam_pitch: float = -0.2
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# Build proc-nodes erosion graph
var g = ProcNodeGraph.new()
var noise = g.add_node(GradientNoiseNode.new())
g.get_node(noise).set_input_default(1, 3.0) # Scale
g.get_node(noise).set_input_default(2, 3) # Detail
g.get_node(noise).set_input_default(3, 0.1) # Gain
var erosion = g.add_node(ErosionNoiseNode.new())
g.get_node(erosion).set_input_default(1, 5.0) # HeightScale
g.get_node(erosion).set_input_default(2, 0.16) # ErosionStr
g.get_node(erosion).set_input_default(3, 0.6) # SlopePower
g.get_node(erosion).set_input_default(4, 1.0) # CellScale
g.get_node(erosion).set_input_default(5, 5) # Octaves
g.get_node(erosion).set_input_default(6, 0.5) # Gain
g.connect_sockets(noise, 0, erosion, 7) # Height
g.connect_sockets(noise, 1, erosion, 8) # Slope
_gen = DCTerrainGeneratorProcNodes.new()
_gen.set_terrain_height(1000.0)
_gen.set_floor_y(-500.0)
_gen.set_ceiling_y(500.0)
_gen.set_mountain_scale(0.5)
_gen.set_mountain_height(0.8)
_gen.set_mountain_octaves(3)
_gen.set_graph(g)
# Verify GLSL compilation
var glsl = g.compile_to_glsl_function("terrain_density", true)
if glsl.contains("erosion_filter"):
print("[ErosionDemo] PASS: Compiled %d chars of GLSL" % glsl.length())
else:
print("[ErosionDemo] FAIL: GLSL missing erosion functions")
# Configure terrain: 2m voxels, depth 10
terrain.set("voxel_size", 2.0)
terrain.set("octree_depth", 10)
terrain.set("use_gpu", true)
terrain.set("use_noise", true)
terrain.set("generator", _gen)
terrain.set("debug_logging", false)
terrain.set("max_splits_per_frame", 16)
terrain.rebuild()
# Start above the surface
camera.global_position = Vector3(100, 600, 100)
var half_ext: float = (1 << 9) * 28 * 2.0
print("[ErosionDemo] voxel=2m depth=10 extent=%dm" % [half_ext * 2])
func _process(delta: float) -> void:
if not _terrain_ready:
var active: int = terrain.get_active_chunk_count()
var collision_q: int = terrain.get_collision_queue_size()
if active >= 10 and collision_q == 0:
_wait_frames += 1
if _wait_frames >= 3:
_terrain_ready = true
print("[ErosionDemo] Terrain ready (active: %d)" % active)
_fly_camera(delta)
# HUD
if stats_label and terrain:
var chunks: int = terrain.get_chunk_count()
var active: int = terrain.get_active_chunk_count()
var pending: int = terrain.get_pending_count()
var edits: int = terrain.get_edit_count()
var fps: int = Engine.get_frames_per_second()
var pos: Vector3 = camera.global_position
stats_label.text = "FPS: %d [EROSION GPU]\nChunks: %d (active: %d, pending: %d)\nEdits: %d\nPos: (%.0f, %.0f, %.0f)\n[R] rebuild [C] clear [F1] wire\nRMB: dig Shift+RMB: add Shift+Scroll: size" % [
fps, chunks, active, pending, edits, pos.x, pos.y, pos.z]
func _fly_camera(delta: float) -> void:
var input := Vector3.ZERO
if Input.is_key_pressed(KEY_W): input.z -= 1.0
if Input.is_key_pressed(KEY_S): input.z += 1.0
if Input.is_key_pressed(KEY_A): input.x -= 1.0
if Input.is_key_pressed(KEY_D): input.x += 1.0
if Input.is_key_pressed(KEY_E) or Input.is_key_pressed(KEY_SPACE): input.y += 1.0
if Input.is_key_pressed(KEY_Q) or Input.is_key_pressed(KEY_CTRL): input.y -= 1.0
var speed := _fly_speed
if Input.is_key_pressed(KEY_SHIFT): speed *= 3.0
if input.length_squared() > 0.0:
input = input.normalized()
camera.global_position += camera.global_transform.basis * input * speed * delta
func _unhandled_input(event: InputEvent) -> void:
# Mouse look
if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
_cam_yaw -= event.relative.x * _mouse_sens
_cam_pitch = clampf(_cam_pitch - event.relative.y * _mouse_sens, -PI * 0.49, PI * 0.49)
camera.rotation = Vector3(_cam_pitch, _cam_yaw, 0)
# Mouse wheel: fly speed or edit size
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
if event.shift_pressed:
_edit_radius = minf(_edit_radius + 0.5, 20.0)
print("Edit size: %.1f" % _edit_radius)
get_viewport().set_input_as_handled()
else:
_fly_speed = minf(200.0, _fly_speed * 1.2)
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
if event.shift_pressed:
_edit_radius = maxf(_edit_radius - 0.5, 1.0)
print("Edit size: %.1f" % _edit_radius)
get_viewport().set_input_as_handled()
else:
_fly_speed = maxf(1.0, _fly_speed / 1.2)
# Escape
if event.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
if not event is InputEventKey or not event.pressed:
if not (event is InputEventMouseButton and event.pressed):
return
# Key controls
if event is InputEventKey and event.pressed:
match event.keycode:
KEY_R:
terrain.rebuild()
print("[ErosionDemo] Rebuild triggered")
KEY_C:
terrain.clear_edits()
print("[ErosionDemo] Edits cleared")
KEY_F1:
_wireframe = !_wireframe
if _wireframe:
get_viewport().debug_draw = Viewport.DEBUG_DRAW_WIREFRAME
else:
get_viewport().debug_draw = Viewport.DEBUG_DRAW_DISABLED
KEY_F2:
_stamp_fandisk()
# Right click = dig box, Shift+Right = add box
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
if event.button_index == MOUSE_BUTTON_RIGHT:
var viewport_center: Vector2 = get_viewport().get_visible_rect().size * 0.5
var from: Vector3 = camera.project_ray_origin(viewport_center)
var dir: Vector3 = camera.project_ray_normal(viewport_center)
var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, from + dir * 500.0)
var result: Dictionary = space_state.intersect_ray(query)
if result.size() > 0:
var hit_pos: Vector3 = result["position"]
var half_ext: Vector3 = Vector3(_edit_radius, _edit_radius, _edit_radius)
var adding: bool = event.shift_pressed
var strength: float = 3.0 if adding else -3.0
terrain.edit_terrain_box(hit_pos, half_ext, strength)
print("Box edit at %s (size=%.1f)" % [hit_pos, _edit_radius])
func _stamp_fandisk() -> void:
var viewport_center: Vector2 = get_viewport().get_visible_rect().size * 0.5
var from: Vector3 = camera.project_ray_origin(viewport_center)
var dir: Vector3 = camera.project_ray_normal(viewport_center)
var space_state: PhysicsDirectSpaceState3D = get_world_3d().direct_space_state
var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, from + dir * 500.0)
var result: Dictionary = space_state.intersect_ray(query)
if result.size() > 0:
var hit_pos: Vector3 = result["position"]
var scene: PackedScene = load("res://fandisk.glb")
if not scene:
print("[ErosionDemo] Failed to load fandisk.glb")
return
var instance: Node3D = scene.instantiate()
var mesh_inst: MeshInstance3D = null
for child in instance.get_children():
if child is MeshInstance3D:
mesh_inst = child
break
if not mesh_inst or not mesh_inst.mesh:
instance.queue_free()
return
var mesh: Mesh = mesh_inst.mesh
var arrays: Array = mesh.surface_get_arrays(0)
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
var idxs: PackedInt32Array = arrays[Mesh.ARRAY_INDEX]
hit_pos.y += _edit_radius * 0.8
print("[ErosionDemo] Fandisk: %d verts, %d indices at %s scale=%.1f" % [verts.size(), idxs.size(), hit_pos, _edit_radius])
terrain.stamp_mesh_arrays(verts, idxs, hit_pos, _edit_radius, 3.0)
instance.queue_free()