OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / demo / scenes / proc_erosion_test.gd

218 lines · gd ref:
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()