extends "res://scenes/maps/map_dc_level.gd"
## (map_dc_level since 2026-10-06, with level_import OFF — the mountain's pairing:
## map_core brings terrain CSG, map_dc_level the brush store / editor / journal, and
## the DCLevel starts empty. gate_geiss_editable.)
## OUR GEISS TERRAIN, meshed by dc-terrain (demo sprint §4; rebuilt 2026-10-06).
##
## The field is the graph in blender/undersea_geiss.tscn (UnderseaGeissGraph.load_graph,
## a bridged scene like the mountain's) — UnderseaGeissGraph follows the old Geiss
## density stage for stage (regional masks, pre-warp of the ground itself, hard floor,
## shelf, continuous terraces, ridges, eight octaves) with our calibrated Perlin — run
## on the GPU by DCTerrainGeneratorProcNodesBounded (no bounds declared, so it streams
## unbounded). It replaced DCTerrainGeneratorGeiss, which ran NVIDIA's GPU Gems 3
## density.h port on their noise volumes; the shading (undersea_rock.gdshader, field
## normal + our palette) replaced their draw_rock.psh port and altmap.tga ramp.
##
## SCALE. As the old map: the surface sits near ground_y (0) with ~±15 m of lumps and
## flat floors at ~-14 m; the finest octave's lattice is ~0.6 m, so voxel_size 0.5 (in
## the .tscn) resolves the mesh and the field normal carries what is finer.
##
## The fly camera, HUD, autoshot and chunk counters are the old A/B harness, kept:
## they measure meshing and frame time whatever the field is.
@onready var terrain: Node3D = $DCTerrain
func _ready() -> void:
var _as := OS.get_environment("GEISS_AUTOSHOT")
if _as != "": _autoshot = float(_as)
# BEFORE super: map_dc_level reads it in _ready (daylight) and in the deferred
# _import_level. There is no .MAP here: brushes are built from scratch.
level_import = false
_use_shared_sky()
super()
var gen: Object = ClassDB.instantiate("DCTerrainGeneratorProcNodesBounded")
if gen == null:
push_error("[geiss-map] DCTerrainGeneratorProcNodesBounded is not registered — rebuild dc-terrain")
return
var graph := _field_graph()
if graph == null:
push_error("[geiss-map] no field graph (see the warning above for the scene)")
return
gen.set("world_scale", field_scale)
gen.set("graph", graph)
# GPU-only generator: a failed compile is air everywhere, so say WHY, loudly.
if not bool(gen.call("has_compiled_shader")):
push_error("[geiss-map] the undersea graph did not compile: %s" % String(gen.call("get_last_compile_error")))
# `generator` is the one DCTerrain property safe to set from a script; every
# other setter is DC_SET_CONFIG and re-fires rebuild(), which restarts the
# mesher before it converges. The rest live in the .tscn.
terrain.set("generator", gen)
var mat := _field_material(gen, graph)
terrain.set("custom_material", mat)
_mat = mat
print("[geiss-map] undersea generator installed (compiled: %s, %d bytes of GLSL)"
% [str(gen.call("has_compiled_shader")), String(gen.call("get_compiled_glsl")).length()])
_t0 = Time.get_ticks_msec()
# A CAMERA, or nothing is drawn and the screen is the clear colour -- which
# is exactly what "geiss-dc is grey" was. map_dc_terrain.gd:159 makes its own
# when standalone; this map had none.
#
# (The start and controls once matched the NVIDIA raymarcher's viewer, removed
# 2026-10-04; launch this map with scripts/undersea-map.sh.)
if get_viewport().get_camera_3d() == null:
_cam = Camera3D.new()
var start := _camera_start()
_cam.position = start[0]
_pitch = float(start[1])
_cam.rotation_degrees = Vector3(_pitch, 0.0, 0.0)
_cam.fov = 70.0
_cam.far = 300000.0
_cam.current = true
add_child(_cam)
_hud = _make_hud()
add_child(_hud)
# GEISS_CAM="x,y,z,yaw,pitch" places the camera for a reproducible capture.
# Without it an autoshot lands wherever spawn happens to look, which is
# useless for comparing a shader change against an artifact that only
# appears at close range.
var camenv := OS.get_environment("GEISS_CAM")
if camenv != "":
var f := camenv.split(",")
if f.size() >= 3:
_cam.position = Vector3(float(f[0]), float(f[1]), float(f[2]))
if f.size() >= 5:
_yaw = float(f[3]); _pitch = float(f[4])
_cam.rotation_degrees = Vector3(_pitch, _yaw, 0.0)
print("[geiss-map] GEISS_CAM -> pos %s yaw %.1f pitch %.1f"
% [str(_cam.position), _yaw, _pitch])
print("[geiss-map] fly camera at %s — CLICK to capture mouse · WASD · SHIFT fast · ESC release" % str(_cam.position))
## ---- the world's sky -------------------------------------------------------------
const _CloudBank := preload("res://scenes/maps/cloud_bank.gd")
## A cloud layer over the whole map (CloudBank, footprint "everywhere": the field is
## unbounded, so no donut). Null resource = CloudBank.default_resource().
@export var clouds_enabled: bool = true
@export var clouds_resource: SunshineCloudsGD
## IN THE GAME, THE GAME'S SKY. The .tscn's WorldEnvironment (a ProceduralSky) and Sun
## exist so the map renders lit when run standalone (gates, scripts/undersea-map.sh).
## Under the game they must go BEFORE game.gd looks: a map that owns a WorldEnvironment
## makes it detach the shared Sky3D — no day/night, the Atmosphere panel drives a dead
## node, and nowhere for the clouds to go (gate_geiss_editable E3/E4).
func _use_shared_sky() -> void:
if get_node_or_null(_CloudBank.GAME_SKY) == null:
return
for c in get_children():
if c is WorldEnvironment or c is DirectionalLight3D:
remove_child(c)
c.queue_free()
if clouds_enabled:
var cb: Node = _CloudBank.new()
cb.name = "CloudBank"
cb.set("footprint", _CloudBank.EVERYWHERE)
cb.set("resource", clouds_resource)
cb.set("log_tag", "geiss-map")
add_child(cb)
## ---- what a map built on this one overrides (map_geiss_terraces.gd) --------------
## The density graph to mesh. Here: the bridged Geiss scene.
func _field_graph() -> ProcNodeGraph:
return UnderseaGeissGraph.load_graph()
## HOW MUCH LARGER THAN THE GRAPH the world stands (user 2026-10-06: "everything in
## geiss map is a little too small / high frequency ... 2x yes"). The generator emits
## f_world(p) = f_graph(p / s) * s, so every feature — lumps, terraces, caves, the world
## floor — scales together. CPU code that samples the GRAPH for a world position (the
## start camera, map_geiss_world's surface placement, shader bands) must use world(p) =
## graph(p / s) * s too: see _graph_y().
@export var field_scale: float = 2.0
## A graph-space height function's answer at a WORLD column, in world metres.
func _graph_y(f: Callable, x: float, z: float) -> float:
return float(f.call(x / field_scale, z / field_scale)) * field_scale
## The terrain's material. Here: undersea_rock with the scene's water line and ground
## plane, so the shading tracks values tuned in Blender.
## ⛔ PLAIN shader (mesh normals), NOT _rock_shader(gen): the field-normal splice compiles
## to a pipeline the driver takes MINUTES to build (measured 2026-10-06: ~10 min to first
## draw), and nothing draws meanwhile — a Home whose saved map was map_geiss sat on a
## blank screen ("hung"). Re-enable only once the splice inlines ONE density evaluation.
func _field_material(_gen: Object, graph: ProcNodeGraph) -> ShaderMaterial:
var mat := ShaderMaterial.new()
mat.shader = ROCK_SHADER
var kn := UnderseaGeissGraph.knobs_for(graph)
mat.set_shader_parameter(&"water_y", float(kn["water_y"]) * field_scale)
mat.set_shader_parameter(&"ground_y", float(kn["ground_y"]) * field_scale)
return mat
## [position, pitch] of the fly camera when nothing places it. Here: the old map's start,
## just above the surface, looking along it (scaled with the field).
func _camera_start() -> Array:
return [Vector3(0.0, 6.0, 16.0) * field_scale, -10.0]
## preload, not load: export_filter is "selected_scenes", so only DEPENDENCIES reach the
## PCK, and a runtime load() of a path is not one (map_dc_erosion.gd records the
## exported build failing exactly so).
const ROCK_SHADER := preload("res://scenes/maps/undersea/undersea_rock.gdshader")
const DENSITY_MARKER := "// @DENSITY@"
## undersea_rock with the generator's own density spliced in at its marker, so the
## shading takes its normal from the FIELD (see the shader's header). The text is
## get_compiled_glsl() — what the mesher runs — through BridgedDensityMaterial's two
## measured transforms (function-like macros, mutable globals); that file owns them and
## its gate tests them, so they are called, not copied. Any refusal or compile failure
## falls back to the plain shader (mesh normals) and SAYS so: a terrain that silently
## lost its field normal is the faceting this exists to remove.
func _rock_shader(gen: Object) -> Shader:
var density := String(gen.call("get_compiled_glsl"))
if density.is_empty():
push_warning("[geiss-map] no compiled density — rock shading uses mesh normals")
return ROCK_SHADER
var exp := BridgedDensityMaterial.expand_function_macros(density)
var con := BridgedDensityMaterial.constify_globals(exp["code"])
if not con["reassigned"].is_empty():
push_warning("[geiss-map] density globals are reassigned (%s) — rock shading uses mesh normals"
% ", ".join(con["reassigned"]))
return ROCK_SHADER
var base := ROCK_SHADER.code
if base.find(DENSITY_MARKER) == -1:
push_warning("[geiss-map] undersea_rock has no %s marker — rock shading uses mesh normals" % DENSITY_MARKER)
return ROCK_SHADER
var sh := Shader.new()
sh.code = base.replace(DENSITY_MARKER, "#define HAS_DENSITY\n" + String(con["code"]))
# get_shader_uniform_list is empty when the shader failed to compile (the errors
# are already on stderr).
if sh.get_shader_uniform_list().is_empty():
push_warning("[geiss-map] the spliced rock shader did not compile — rock shading uses mesh normals")
return ROCK_SHADER
print("[geiss-map] rock shading: field normal from the density (%d macro expansions, %d consts, %d bytes)"
% [int(exp["count"]), int(con["count"]), sh.code.length()])
return sh
## A/B INSTRUMENTATION. Printed from the map because dc-terrain attaches chunk
## meshes through RenderingServer.instance_create() rather than as scene nodes,
## so nothing outside a real running scene can see them -- a --script harness
## reports 1 chunk and 0 primitives.
var _t0 := 0
var _settled := false
var _frame_ms: Array[float] = []
var _cam: Camera3D = null
var _yaw := 0.0
var _pitch := -10.0
var _mouse := Vector2.ZERO
var _mat: ShaderMaterial
var _hud: Label
var _elapsed := 0.0
func _process(dt: float) -> void:
super._process(dt) # map_dc_level: the face_sel keymap context
_fly(dt)
_elapsed += dt
_autoshot_check(_elapsed)
var pending: int = terrain.call("get_pending_count")
var active: int = terrain.call("get_active_chunk_count")
_frame_ms.append(dt * 1000.0)
if _frame_ms.size() > 120: _frame_ms.pop_front()
if not _settled and active > 0 and pending == 0:
_settled = true
print("[geiss-ab] 1 BUILD %.0f ms to steady state, %d chunks -> %.1f ms/chunk"
% [float(Time.get_ticks_msec() - _t0), active,
float(Time.get_ticks_msec() - _t0) / float(active)])
if _hud != null:
var f := Engine.get_frames_per_second()
_hud.text = "DUAL CONTOURING %d fps (%.1f ms)\nchunks %d prims %s\ncam %s\nF2 screenshot · ESC" % [
f, 1000.0 / maxf(f, 1.0), active,
str(int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME))),
str(_cam.position.round()) if _cam != null else "(game camera)"]
if Engine.get_process_frames() % 180 != 0: return
var sorted := _frame_ms.duplicate(); sorted.sort()
var med: float = sorted[sorted.size() / 2] if not sorted.is_empty() else 0.0
print("[geiss-ab] chunks=%d pending=%d | prims=%d objects=%d draws=%d"
% [active, pending,
int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)),
int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))])
print("[geiss-ab] buffers %.1f MB | textures %.1f MB | frame %.2f ms median"
% [Performance.get_monitor(Performance.RENDER_BUFFER_MEM_USED) / 1048576.0,
Performance.get_monitor(Performance.RENDER_TEXTURE_MEM_USED) / 1048576.0, med])
## Keyboard only, matching the raymarch viewer: WASD move, Q/E or CTRL/SPACE
## down/up, ARROWS look, SHIFT fast.
## On-screen readout, because a number in a terminal you cannot see while flying
## is not a counter.
func _make_hud() -> Label:
var l := Label.new()
l.position = Vector2(12, 8)
l.add_theme_color_override("font_color", Color(1, 1, 0.55))
l.add_theme_color_override("font_outline_color", Color(0, 0, 0))
l.add_theme_constant_override("outline_size", 4)
return l
## AUTO-CAPTURE, so a debug view can be compared against another debug view at
## the SAME viewpoint without a human holding the camera still.
##
## GEISS_AUTOSHOT=12 capture at t=12 s, then quit
## GEISS_SHOT_NAME=ao5 write user://ao5.png instead of geiss_dc_NNN.png
##
## Exists because four rounds of this were argued from frames taken at
## different places, and because asking for a capture is asking the wrong
## party -- the shader has debug views precisely so this does not need a
## judgement call about a gameplay screenshot.
var _autoshot := -1.0
var _autoshot_done := false
func _autoshot_check(t: float) -> void:
if _autoshot_done or _autoshot < 0.0 or t < _autoshot:
return
_autoshot_done = true
var img := get_viewport().get_texture().get_image()
if img == null:
print("[geiss-map] autoshot FAILED — no viewport image"); get_tree().quit(1); return
var nm := OS.get_environment("GEISS_SHOT_NAME")
var path := "user://%s.png" % (nm if nm != "" else "geiss_autoshot")
img.save_png(path)
print("[geiss-map] autoshot -> %s" % ProjectSettings.globalize_path(path))
get_tree().quit(0)
func _unhandled_input(e: InputEvent) -> void:
# map_core's FIRST: the universal carve (player.fire), the terrain panel, F6 and
# the rest live there. This override never called it, so no Geiss map could carve
# (gate_geiss_editable E1: edits 0 -> 0).
super._unhandled_input(e)
if get_viewport().is_input_handled():
return
if e is InputEventKey and (e as InputEventKey).pressed \
and (e as InputEventKey).keycode == KEY_F2:
var img := get_viewport().get_texture().get_image()
if img != null:
var n := 0
while FileAccess.file_exists("user://geiss_dc_%03d.png" % n): n += 1
img.save_png("user://geiss_dc_%03d.png" % n)
print("[geiss-map] screenshot -> %s"
% ProjectSettings.globalize_path("user://geiss_dc_%03d.png" % n))
if e is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
_mouse += (e as InputEventMouseMotion).relative
elif e is InputEventMouseButton and (e as InputEventMouseButton).pressed and _cam != null:
# Only the standalone harness's fly camera grabs the mouse; in the game the
# player owns capture (and build mode wants the cursor).
if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _fly(dt: float) -> void:
if _cam == null: return
if Input.is_key_pressed(KEY_ESCAPE):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
# SAY SO: this quit was silent, and two runs that ended early with exit 0
# and no result could not be told apart from a crash or a gate bug.
print("[geiss-map] Escape held — quitting")
get_tree().quit()
# Same controls as the raymarch viewer, so flying the two compares the
# RENDERING and not two different sets of controls.
_yaw -= _mouse.x * 0.12
_pitch = clampf(_pitch - _mouse.y * 0.12, -89.0, 89.0)
_mouse = Vector2.ZERO
var look := 90.0 * dt
if Input.is_key_pressed(KEY_SHIFT): look *= 2.5
if Input.is_key_pressed(KEY_LEFT): _yaw += look
if Input.is_key_pressed(KEY_RIGHT): _yaw -= look
if Input.is_key_pressed(KEY_UP): _pitch = clampf(_pitch + look, -89.0, 89.0)
if Input.is_key_pressed(KEY_DOWN): _pitch = clampf(_pitch - look, -89.0, 89.0)
var b := Basis.from_euler(Vector3(deg_to_rad(_pitch), deg_to_rad(_yaw), 0.0))
var speed := (40.0 if Input.is_key_pressed(KEY_SHIFT) else 6.0) * dt
var p := _cam.position
if Input.is_key_pressed(KEY_W): p -= b.z * speed
if Input.is_key_pressed(KEY_S): p += b.z * speed
if Input.is_key_pressed(KEY_A): p -= b.x * speed
if Input.is_key_pressed(KEY_D): p += b.x * speed
if Input.is_key_pressed(KEY_E) or Input.is_key_pressed(KEY_SPACE): p.y += speed
if Input.is_key_pressed(KEY_Q) or Input.is_key_pressed(KEY_CTRL): p.y -= speed
_cam.position = p
_cam.basis = b