@tool
extends Node3D
## DC Terrain driven by proc-nodes erosion graph (GPU GLSL path)
@export var terrain_size: float = 12.0
# Store generator as member so it persists
var _gen: DCTerrainGeneratorProcNodes
func _ready():
# Sky3DExtended is in the .tscn — configure it
var sky3d := $Sky3DExtended as Sky3DExtended
if sky3d and sky3d.sky:
sky3d.sky.moon_phase = 0.5
sky3d.sky.moon_light_energy = 1.5
# Set up nubis clouds (compositor on Sky3DExtended)
call_deferred("_setup_nubis_clouds")
var terrain = get_node_or_null("DCTerrain")
if terrain == null:
push_warning("proc_erosion_dc_test: no DCTerrain child")
return
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()
# terrain_height = XZ scale: UV [0,1] maps to this many meters.
# 1000m means the erosion pattern spans 1km before repeating.
_gen.set_terrain_height(1000.0)
# Y range: mountains from -500m to +500m (1km total height range)
_gen.set_floor_y(-500.0)
_gen.set_ceiling_y(500.0)
# Mountain layer: large-scale ridges
_gen.set_mountain_scale(0.5)
_gen.set_mountain_height(0.8)
_gen.set_mountain_octaves(3)
_gen.set_graph(g)
var glsl = g.compile_to_glsl_function("terrain_density", true)
if glsl.contains("erosion_filter"):
print("[ErosionDC] PASS: Compiled ", glsl.length(), " chars of GLSL")
else:
print("[ErosionDC] FAIL: GLSL missing erosion functions")
# Voxel size 2m at LOD0, depth 10 → 10 LOD levels (2m to 1024m voxels)
# LOD0 cells: 28 * 2 = 56m. Half-extent: 512 * 56 = 28672m (~57km total)
# Intermediate LODs fill the horizon with progressively coarser terrain:
# LOD0: 2m (near player), LOD3: 16m, LOD5: 64m, LOD7: 256m, LOD9: 1024m
var vs: float = 2.0
var depth: int = 10
terrain.set("voxel_size", vs)
terrain.set("octree_depth", depth)
terrain.set("use_gpu", true)
terrain.set("use_noise", true)
terrain.set("generator", _gen)
terrain.set("debug_logging", false)
# Altitude color shader — map colors across the full Y range
var shader = load("res://scenes/maps/dc_erosion_color.gdshader") as Shader
if shader:
var mat = ShaderMaterial.new()
mat.shader = shader
mat.set_shader_parameter("terrain_params", Vector3(-500.0, 500.0, 0.465))
terrain.set("custom_material", mat)
# Player spawns above the surface (surface ≈ y=0 with mountains ±400m)
var player = get_node_or_null("Player")
if player:
player.position = Vector3(100, 600, 100)
if player:
player.set_multiplayer_authority(1)
if not Engine.is_editor_hint():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
var half_ext: float = (1 << (depth - 1)) * 28 * vs
print("[ErosionDC] voxel=", vs, "m depth=", depth, " extent=", half_ext * 2, "m",
" Y range=[-500, 500] terrain_height=1000m")
# Nubis clouds set up via call_deferred at top of _ready()
var _edit_radius: float = 3.0
func _unhandled_input(event: InputEvent) -> void:
var terrain = get_node_or_null("DCTerrain")
var player = get_node_or_null("Player")
# Escape to toggle mouse capture
if event.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
elif not Engine.is_editor_hint():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# T / Shift+T to change time of day
if event is InputEventKey and event.pressed and event.keycode == KEY_T:
var sky3d := get_node_or_null("Sky3DExtended") as Sky3DExtended
if sky3d:
var delta := -1.0 if event.shift_pressed else 1.0
sky3d.current_time = fmod(sky3d.current_time + delta + 24.0, 24.0)
print("[ErosionDC] Time: %.1f (%s)" % [sky3d.current_time, "day" if sky3d.is_day() else "night"])
# R to rebuild terrain
if event is InputEventKey and event.pressed and event.keycode == KEY_R:
if terrain:
terrain.rebuild()
print("[ErosionDC] Rebuild triggered")
if event is InputEventKey and event.pressed and event.keycode == KEY_F12:
var img: Image = get_viewport().get_texture().get_image()
var ts: String = Time.get_datetime_string_from_system().replace(":", "-")
var dir := "/home/fleeky/Desktop/hyper-tps"
DirAccess.make_dir_recursive_absolute(dir)
var path: String = "%s/erosion_%s.png" % [dir, ts]
img.save_png(path)
print("[ErosionDC] Screenshot: %s" % path)
# F2 to stamp fandisk at crosshair
if event is InputEventKey and event.pressed and event.keycode == KEY_F2:
var camera: Camera3D = get_viewport().get_camera_3d()
if camera and terrain:
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 * 200.0)
if player:
query.exclude = [player.get_rid()]
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
# Shift+Scroll = adjust edit size
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
if event.shift_pressed and event.button_index == MOUSE_BUTTON_WHEEL_UP:
_edit_radius = minf(_edit_radius + 0.5, 20.0)
print("Edit size: %.1f" % _edit_radius)
get_viewport().set_input_as_handled()
elif event.shift_pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
_edit_radius = maxf(_edit_radius - 0.5, 1.0)
print("Edit size: %.1f" % _edit_radius)
get_viewport().set_input_as_handled()
elif event.button_index == MOUSE_BUTTON_RIGHT:
var camera: Camera3D = get_viewport().get_camera_3d()
if not camera or not terrain:
return
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 * 200.0)
if player:
query.exclude = [player.get_rid()]
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])
if adding and player:
var dist: float = player.global_position.distance_to(hit_pos)
if dist < _edit_radius * 1.5:
player.global_position.y += _edit_radius + 1.0
func _setup_nubis_clouds() -> void:
var compositor := Compositor.new()
var nubis_comp := NubisCompositor.new()
nubis_comp.density_scale = 80.0
nubis_comp.noise_freq = 0.00005
nubis_comp.erosion_strength = 0.9
compositor.compositor_effects = [nubis_comp]
var we := $Sky3DExtended as WorldEnvironment
we.compositor = compositor
var shared := _make_schneider_sdf(32, 42, 0.5)
# Clouds at mountain altitudes (terrain Y range: -500 to +500)
var configs := [
{"pos": Vector3(0, 400, -800), "size": Vector3(1500, 600, 1500), "density": 80.0},
{"pos": Vector3(1200, 450, -300), "size": Vector3(1200, 500, 1200), "density": 75.0},
{"pos": Vector3(-900, 350, 600), "size": Vector3(1400, 700, 1300), "density": 85.0},
{"pos": Vector3(600, 300, 1000), "size": Vector3(1800, 400, 1600), "density": 70.0},
{"pos": Vector3(-1200, 500, -500), "size": Vector3(1600, 500, 1400), "density": 75.0},
{"pos": Vector3(2000, 550, -1500), "size": Vector3(1000, 800, 1000), "density": 90.0},
]
for cfg in configs:
var c := CloudVolume.new()
c.shared_sdf = shared
c.position = cfg["pos"]
c.cloud_size = cfg["size"]
c.density_scale = cfg["density"]
c.wind_speed = 0.02
add_child(c)
print("[ErosionDC] %d Nubis cloud volumes active" % configs.size())
func _make_schneider_sdf(res: int, seed_val: int, coverage: float) -> ImageTexture3D:
var perlin := FastNoiseLite.new()
perlin.noise_type = FastNoiseLite.TYPE_PERLIN
perlin.frequency = 0.08
perlin.fractal_octaves = 4
perlin.fractal_gain = 0.5
perlin.seed = seed_val
var worley := FastNoiseLite.new()
worley.noise_type = FastNoiseLite.TYPE_CELLULAR
worley.cellular_return_type = FastNoiseLite.RETURN_DISTANCE
worley.frequency = 0.12
worley.fractal_octaves = 3
worley.seed = seed_val + 10
var images: Array[Image] = []
for z in res:
var img := Image.create(res, res, false, Image.FORMAT_RF)
for y in res:
for x in res:
var uvw := Vector3(float(x) / float(res), 1.0 - float(y) / float(res), float(z) / float(res))
var sp := Vector3(float(x), float(y), float(z))
var p_val := perlin.get_noise_3d(sp.x, sp.y, sp.z) * 0.5 + 0.5
var w_val := 1.0 - (worley.get_noise_3d(sp.x, sp.y, sp.z) * 0.5 + 0.5)
var lo := -(1.0 - w_val)
var base_cloud := clampf((p_val - lo) / (1.0 - lo), 0.0, 1.0)
var hf := uvw.y
var gradient := _smoothstep(0.01, 0.15, hf) * (1.0 - _smoothstep(0.6, 1.0, hf))
base_cloud *= gradient
var threshold := 1.0 - coverage
var shaped := clampf((base_cloud - threshold) / maxf(1.0 - threshold, 0.001), 0.0, 1.0)
shaped *= _smoothstep(threshold - 0.05, threshold + 0.05, base_cloud)
var ex := _smoothstep(0.0, 0.08, uvw.x) * _smoothstep(0.0, 0.08, 1.0 - uvw.x)
var ey := _smoothstep(0.0, 0.08, 1.0 - uvw.y)
var ez := _smoothstep(0.0, 0.08, uvw.z) * _smoothstep(0.0, 0.08, 1.0 - uvw.z)
shaped *= ex * ey * ez
var sdf := pow(clampf(shaped, 0.0, 1.0), (1.0 - hf) * 0.8 + 0.5)
img.set_pixel(x, y, Color(sdf, 0, 0, 1))
images.append(img)
var tex := ImageTexture3D.new()
tex.create(Image.FORMAT_RF, res, res, res, false, images)
return tex
func _smoothstep(edge0: float, edge1: float, x: float) -> float:
var t := clampf((x - edge0) / (edge1 - edge0), 0.0, 1.0)
return t * t * (3.0 - 2.0 * t)