OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / demo / scenes / dc_terrain_streaming.gd

305 lines · gd ref:
extends Node3D
## Minimal DC Terrain test — fly camera + CSG edit controls.
## No player, no creatures, no clouds. Pure terrain verification.

@onready var terrain: Node3D = $DCTerrain
@onready var stats_label: Label = $CanvasLayer/StatsLabel
@onready var camera: Camera3D = $Camera3D

var _terrain_ready: bool = false
var _wait_frames: int = 0
var _wireframe: bool = false
var _edit_radius: float = 3.0

# Benchmark
var _bench_active: bool = false
var _bench_phase: int = 0  # 0=gpu_rebuild, 1=gpu_edit, 2=cpu_rebuild, 3=cpu_edit, 4=done
var _bench_start_ms: int = 0
var _bench_results: Array = []
var _bench_edit_count: int = 0

# Fly camera
var _fly_speed: float = 10.0
var _mouse_sens: float = 0.003
var _cam_yaw: float = 0.0
var _cam_pitch: float = -0.3


func _ready() -> void:
	process_mode = Node.PROCESS_MODE_ALWAYS
	terrain.rebuild()
	Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
	camera.global_position = Vector3(0, 10, 0)


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("Terrain ready (active: %d)" % active)

	# Fly camera
	_fly_camera(delta)

	# Benchmark state machine
	if _bench_active:
		_run_benchmark()

	# 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
		var gpu_str: String = "GPU" if terrain.use_gpu else "CPU"
		var bench_str: String = " [BENCH RUNNING]" if _bench_active else ""
		stats_label.text = "FPS: %d  [%s]%s\nChunks: %d (active: %d, pending: %d)\nEdits: %d\nPos: (%.0f, %.0f, %.0f)\n[R] rebuild  [C] clear  [B] bench  [F1] wire  [F2] fandisk\nRMB: dig  Shift+RMB: add  Shift+Scroll: size" % [
			fps, gpu_str, bench_str, chunks, active, pending, edits, pos.x, pos.y, pos.z]


func _start_benchmark() -> void:
	print("=== BENCHMARK START ===")
	_bench_active = true
	_bench_phase = 0
	_bench_results = []
	_bench_edit_count = 0
	# Phase 0: GPU rebuild
	terrain.use_gpu = true
	terrain.clear_edits()
	terrain.rebuild()
	_bench_start_ms = Time.get_ticks_msec()
	_terrain_ready = false
	_wait_frames = 0
	print("  Phase 0: GPU rebuild...")


func _run_benchmark() -> void:
	var active: int = terrain.get_active_chunk_count()
	var pending: int = terrain.get_pending_count()
	var collision_q: int = terrain.get_collision_queue_size()
	var settled: bool = active >= 10 and pending == 0 and collision_q == 0

	if not settled:
		return

	var elapsed: int = Time.get_ticks_msec() - _bench_start_ms

	match _bench_phase:
		0:  # GPU rebuild done
			_bench_results.append("GPU rebuild: %d ms (%d chunks)" % [elapsed, active])
			print("  GPU rebuild: %d ms (%d chunks)" % [elapsed, active])
			# Phase 1: GPU edits — 5 box edits
			_bench_phase = 1
			_bench_start_ms = Time.get_ticks_msec()
			for i in range(5):
				var pos := Vector3(i * 6.0 - 12.0, 2.0, 10.0)
				var half := Vector3(2.0, 2.0, 2.0)
				terrain.edit_terrain_box(pos, half, -3.0)
			print("  Phase 1: GPU box edits (5)...")

		1:  # GPU edits done
			_bench_results.append("GPU 5 box edits: %d ms" % elapsed)
			print("  GPU 5 box edits: %d ms" % elapsed)
			# Phase 2: GPU fandisk stamp
			_bench_phase = 2
			_bench_start_ms = Time.get_ticks_msec()
			_stamp_fandisk_at(Vector3(0, 5, 20))
			print("  Phase 2: GPU fandisk stamp...")

		2:  # GPU fandisk done
			_bench_results.append("GPU fandisk stamp: %d ms" % elapsed)
			print("  GPU fandisk stamp: %d ms" % elapsed)
			# Phase 3: CPU rebuild
			_bench_phase = 3
			terrain.use_gpu = false
			terrain.clear_edits()
			terrain.rebuild()
			_bench_start_ms = Time.get_ticks_msec()
			_terrain_ready = false
			_wait_frames = 0
			print("  Phase 3: CPU rebuild...")

		3:  # CPU rebuild done
			_bench_results.append("CPU rebuild: %d ms (%d chunks)" % [elapsed, active])
			print("  CPU rebuild: %d ms (%d chunks)" % [elapsed, active])
			# Phase 4: CPU edits — 5 box edits
			_bench_phase = 4
			_bench_start_ms = Time.get_ticks_msec()
			for i in range(5):
				var pos := Vector3(i * 6.0 - 12.0, 2.0, 10.0)
				var half := Vector3(2.0, 2.0, 2.0)
				terrain.edit_terrain_box(pos, half, -3.0)
			print("  Phase 4: CPU box edits (5)...")

		4:  # CPU edits done
			_bench_results.append("CPU 5 box edits: %d ms" % elapsed)
			print("  CPU 5 box edits: %d ms" % elapsed)
			# Done
			_bench_active = false
			terrain.use_gpu = true
			terrain.clear_edits()
			terrain.rebuild()
			print("=== BENCHMARK RESULTS ===")
			for r in _bench_results:
				print("  %s" % r)
			print("=========================")


func _stamp_fandisk_at(hit_pos: Vector3) -> void:
	var scene: PackedScene = load("res://fandisk.glb")
	if not scene:
		print("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]
	terrain.stamp_mesh_arrays(verts, idxs, hit_pos, _edit_radius, 3.0)
	instance.queue_free()


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
	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(100.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("Terrain rebuild triggered")
			KEY_C:
				terrain.clear_edits()
				print("Terrain 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
				print("Wireframe: %s" % _wireframe)
			KEY_B:
				if not _bench_active:
					_start_benchmark()
			KEY_L:
				terrain.debug_lod_colors = !terrain.debug_lod_colors
				print("LOD colors: %s" % terrain.debug_lod_colors)
			KEY_F12:
				var img: Image = get_viewport().get_texture().get_image()
				var ts: String = Time.get_datetime_string_from_system().replace(":", "-")
				var path: String = "/home/fleeky/Desktop/dc-terrain/terrain_%s.png" % ts
				DirAccess.make_dir_recursive_absolute("/home/fleeky/Desktop/dc-terrain")
				img.save_png(path)
				print("Screenshot saved: %s" % path)

	# F2 to stamp fandisk at crosshair
	if event is InputEventKey and event.pressed and event.keycode == KEY_F2:
		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 * 100.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("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:
				print("No MeshInstance3D found in fandisk.glb")
				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("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()

	# 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 * 100.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, strength=%.1f)" % [hit_pos, _edit_radius, strength])