Give the gas planet surface its own background shader (#51)
lunar_surface.gdshader recoloured was the whole gas surface backdrop,
so a landed gas giant read as regolith - the crater field never went
away, just changed colour. New gas_surface.gdshader replaces it for
gas profiles only: a nearest-row-wins horizontal band field (same
single-scalar discipline as lunar's nearest-crater field), with
drifting per-row turbulence so the bands ripple instead of reading as
ruled lines. Keeps the exact same uniform interface as lunar
(camera_cell/camera_frac/camera_zoom/viewport_size/cell_size/
base_color/crater_color/highlight_color), so LunarBackground.gd needs
no changes at all.
PlanetSurfaceProfile gains background_shader (default null = keep the
scene's existing lunar shader); PlanetSurfaceController._compose_surface()
swaps the background material's shader in only when a profile names one.
GasSurfaceProfile.tres points at the new shader and retunes its palette
to a deep-atmosphere/shadowed-band/lit-crest read; RockySurfaceProfile.tres
is untouched, so the rocky surface renders exactly as before.
Precision: every hash input is either an exact integer cell/row index or
a bounded [0,1) local fraction, same split as lunar_surface.gdshader and
Starfield.gd use for the same reason (world coordinates reach the tens of
thousands, where mobile float precision collapses). TIME-based lateral
drift is folded into the lattice coordinate via the same floor/remainder
trick rather than ever being added to a large cell index directly - see
combine_lattice()'s header note.
Tests: new tests/test_surface_profiles.gd (rocky resolves no
background_shader and keeps the lunar shader; gas resolves and applies
its own, different from lunar's). Full suite: 78/78 passed. Screenshot
of the gas surface confirms drifting bands with no craters, rendered
well inside the softpipe performance budget (20 frames in ~2m40s,
against lunar's "minutes per frame" baseline the ticket warns about).
Repo notes for the next agent, confirmed empirically this session:
- Never reference a plain (non-autoload) class_name class by its bare
global name from a -s entry-point test script - `load()` it by path
into a local var instead, same as PlanetSurfaceController.gd and
PlanetSetups.gd already do for exactly this reason. Doing it the
unsafe way here didn't fail to compile - it corrupted something in
the resource-loading state badly enough that an UNRELATED scene's
Rock.tscn lost its script attachment and RockField's rock-spawning
loop exhausted the physics server's entire RID pool. If a test's
crash trace implicates code you never touched, check every bare
global class_name reference in the test first.
- Running a `tests/screenshot.sh` render (Mesa softpipe, genuinely
minutes for a crater-shader scene) at the same time as a
`run_tests.sh` full-suite run starves both for CPU on this device -
kill one before waiting on the other rather than running them
concurrently.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:05:31 +02:00
|
|
|
extends SceneTree
|
|
|
|
|
|
|
|
|
|
# Headless gameplay test: run with
|
|
|
|
|
# godot --headless -s res://tests/test_surface_profiles.gd
|
|
|
|
|
# Forgejo #51: a landed gas giant needs its own background shader instead of
|
|
|
|
|
# lunar_surface.gdshader recoloured (craters reading as regolith, not
|
|
|
|
|
# atmosphere). Verifies:
|
|
|
|
|
# 1. the gas profile resolves a background_shader different from rocky's
|
|
|
|
|
# (rocky's stays null - "keep the scene's existing shader")
|
|
|
|
|
# 2. PlanetSurfaceController._compose_surface() actually swaps the
|
|
|
|
|
# background material's shader when landing on a gas giant
|
|
|
|
|
# 3. landing on a rocky planet leaves the scene's own lunar shader in
|
|
|
|
|
# place - a null background_shader is a no-op, not a forced reset
|
|
|
|
|
#
|
|
|
|
|
# PlanetSurfaceController's _ready() (which calls _compose_surface()) does
|
|
|
|
|
# not fire synchronously inside add_child() for a -s entry-point script -
|
|
|
|
|
# it's deferred to the next frame, same as ResourceReadout's was for #49 -
|
|
|
|
|
# so every read of the composed shader below waits a frame after add_child()
|
|
|
|
|
# rather than checking on the same one.
|
|
|
|
|
|
|
|
|
|
var game_state: Node
|
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68)
The crater shader ran on every pixel of a fullscreen rect every frame and
was by far the most expensive thing in the surface scene. Nothing else came
close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid
per-frame queue_redraw() for transform-only changes, and LunarBackground.gd
is a handful of scalar ops.
Three reductions to the field itself:
- 5x5 -> 3x3 neighbour scan. That width was never the geometric answer;
it was insurance against craters seen clipping at cell boundaries
on-device, i.e. a crater reaching past its nominal radius because a hash
returned a value outside [0,1] and extrapolated mix(). Clamping the
crater CENTRE into its own cell, alongside the radius clamp already
there, bounds the reach whatever the hash returns - so a crater two cells
out is >= 1.0 cells away, a normalized distance of >= 2.63, past every
threshold the shading reacts to (the last saturates at 1.15). The outer
ring provably cannot change a pixel. This removes the clipping bug's
cause instead of hiding it behind 16 extra samples.
- One hash42 per cell instead of hash21 twice plus hash22, decorrelating
on output lanes rather than input offsets.
- Squared distances in the loop, square-rooted twice at the end rather
than once per sample.
Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms
fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction.
Then the larger change: the field is a fixed function of world position, so
every frame recomputed an identical answer. It is now made periodic (the
cell index is wrapped before hashing, which makes a Worley field tile
exactly rather than merely blend), baked once at load into one tile, and
scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch
per pixel. Two honest costs: the field repeats every 7040 world units, and
a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is
forgiving of both, having no landmarks and being smooth gradients already.
The bake waits a frame and asks the material whether it actually samples a
field_texture, so a gas giant - which swaps in its own background shader
(#51) - does not pay for a 2048-square tile it will never read. Headless
runs skip it entirely, having nothing to rasterize.
tests/test_lunar_field.gd guards the reasoning, since a fragment shader
cannot be asserted on directly: the premises are still in the source, the
worst-case ring-2 geometry stays clear of every threshold independent of
the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points,
the field repeats exactly in both axes, and the miss sentinel still cannot
overflow mediump into +Inf and then NaN.
test_surface_profiles.gd asserted "a rocky landing leaves the background
alone" by naming lunar_surface.gdshader; it now reads the shader from the
scene, which is the property it actually cares about.
PlanetSurface.tscn's edit also carries the planet-name relocation described
in the HUD commit - same scene file, so the two cannot be split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
|
|
|
var scene_shader: Shader
|
Give the gas planet surface its own background shader (#51)
lunar_surface.gdshader recoloured was the whole gas surface backdrop,
so a landed gas giant read as regolith - the crater field never went
away, just changed colour. New gas_surface.gdshader replaces it for
gas profiles only: a nearest-row-wins horizontal band field (same
single-scalar discipline as lunar's nearest-crater field), with
drifting per-row turbulence so the bands ripple instead of reading as
ruled lines. Keeps the exact same uniform interface as lunar
(camera_cell/camera_frac/camera_zoom/viewport_size/cell_size/
base_color/crater_color/highlight_color), so LunarBackground.gd needs
no changes at all.
PlanetSurfaceProfile gains background_shader (default null = keep the
scene's existing lunar shader); PlanetSurfaceController._compose_surface()
swaps the background material's shader in only when a profile names one.
GasSurfaceProfile.tres points at the new shader and retunes its palette
to a deep-atmosphere/shadowed-band/lit-crest read; RockySurfaceProfile.tres
is untouched, so the rocky surface renders exactly as before.
Precision: every hash input is either an exact integer cell/row index or
a bounded [0,1) local fraction, same split as lunar_surface.gdshader and
Starfield.gd use for the same reason (world coordinates reach the tens of
thousands, where mobile float precision collapses). TIME-based lateral
drift is folded into the lattice coordinate via the same floor/remainder
trick rather than ever being added to a large cell index directly - see
combine_lattice()'s header note.
Tests: new tests/test_surface_profiles.gd (rocky resolves no
background_shader and keeps the lunar shader; gas resolves and applies
its own, different from lunar's). Full suite: 78/78 passed. Screenshot
of the gas surface confirms drifting bands with no craters, rendered
well inside the softpipe performance budget (20 frames in ~2m40s,
against lunar's "minutes per frame" baseline the ticket warns about).
Repo notes for the next agent, confirmed empirically this session:
- Never reference a plain (non-autoload) class_name class by its bare
global name from a -s entry-point test script - `load()` it by path
into a local var instead, same as PlanetSurfaceController.gd and
PlanetSetups.gd already do for exactly this reason. Doing it the
unsafe way here didn't fail to compile - it corrupted something in
the resource-loading state badly enough that an UNRELATED scene's
Rock.tscn lost its script attachment and RockField's rock-spawning
loop exhausted the physics server's entire RID pool. If a test's
crash trace implicates code you never touched, check every bare
global class_name reference in the test first.
- Running a `tests/screenshot.sh` render (Mesa softpipe, genuinely
minutes for a crater-shader scene) at the same time as a
`run_tests.sh` full-suite run starves both for CPU on this device -
kill one before waiting on the other rather than running them
concurrently.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:05:31 +02:00
|
|
|
var gas_shader: Shader
|
|
|
|
|
var rocky_shader_after_compose: Shader
|
|
|
|
|
var scene: Node
|
|
|
|
|
var stage: int = 0
|
|
|
|
|
var elapsed: float = 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _initialize() -> void:
|
|
|
|
|
game_state = root.get_node("GameState")
|
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68)
The crater shader ran on every pixel of a fullscreen rect every frame and
was by far the most expensive thing in the surface scene. Nothing else came
close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid
per-frame queue_redraw() for transform-only changes, and LunarBackground.gd
is a handful of scalar ops.
Three reductions to the field itself:
- 5x5 -> 3x3 neighbour scan. That width was never the geometric answer;
it was insurance against craters seen clipping at cell boundaries
on-device, i.e. a crater reaching past its nominal radius because a hash
returned a value outside [0,1] and extrapolated mix(). Clamping the
crater CENTRE into its own cell, alongside the radius clamp already
there, bounds the reach whatever the hash returns - so a crater two cells
out is >= 1.0 cells away, a normalized distance of >= 2.63, past every
threshold the shading reacts to (the last saturates at 1.15). The outer
ring provably cannot change a pixel. This removes the clipping bug's
cause instead of hiding it behind 16 extra samples.
- One hash42 per cell instead of hash21 twice plus hash22, decorrelating
on output lanes rather than input offsets.
- Squared distances in the loop, square-rooted twice at the end rather
than once per sample.
Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms
fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction.
Then the larger change: the field is a fixed function of world position, so
every frame recomputed an identical answer. It is now made periodic (the
cell index is wrapped before hashing, which makes a Worley field tile
exactly rather than merely blend), baked once at load into one tile, and
scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch
per pixel. Two honest costs: the field repeats every 7040 world units, and
a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is
forgiving of both, having no landmarks and being smooth gradients already.
The bake waits a frame and asks the material whether it actually samples a
field_texture, so a gas giant - which swaps in its own background shader
(#51) - does not pay for a 2048-square tile it will never read. Headless
runs skip it entirely, having nothing to rasterize.
tests/test_lunar_field.gd guards the reasoning, since a fragment shader
cannot be asserted on directly: the premises are still in the source, the
worst-case ring-2 geometry stays clear of every threshold independent of
the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points,
the field repeats exactly in both axes, and the miss sentinel still cannot
overflow mediump into +Inf and then NaN.
test_surface_profiles.gd asserted "a rocky landing leaves the background
alone" by naming lunar_surface.gdshader; it now reads the shader from the
scene, which is the property it actually cares about.
PlanetSurface.tscn's edit also carries the planet-name relocation described
in the HUD commit - same scene file, so the two cannot be split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
|
|
|
# Whatever PlanetSurface.tscn itself ships with, read from the scene
|
|
|
|
|
# rather than named by path. The property under test is "a null
|
|
|
|
|
# background_shader leaves the scene's own background alone" - naming a
|
|
|
|
|
# specific file here instead made this fail the moment the lunar
|
|
|
|
|
# background changed implementation (#68 swapped the per-pixel
|
|
|
|
|
# procedural shader for a baked, tiled one), which is a change this test
|
|
|
|
|
# has no business having an opinion about.
|
|
|
|
|
var packed: PackedScene = load("res://PlanetSurface.tscn")
|
|
|
|
|
var probe: Node = packed.instantiate()
|
|
|
|
|
scene_shader = probe.get_node("BackgroundLayer/LunarBackground").material.shader
|
|
|
|
|
probe.free()
|
Give the gas planet surface its own background shader (#51)
lunar_surface.gdshader recoloured was the whole gas surface backdrop,
so a landed gas giant read as regolith - the crater field never went
away, just changed colour. New gas_surface.gdshader replaces it for
gas profiles only: a nearest-row-wins horizontal band field (same
single-scalar discipline as lunar's nearest-crater field), with
drifting per-row turbulence so the bands ripple instead of reading as
ruled lines. Keeps the exact same uniform interface as lunar
(camera_cell/camera_frac/camera_zoom/viewport_size/cell_size/
base_color/crater_color/highlight_color), so LunarBackground.gd needs
no changes at all.
PlanetSurfaceProfile gains background_shader (default null = keep the
scene's existing lunar shader); PlanetSurfaceController._compose_surface()
swaps the background material's shader in only when a profile names one.
GasSurfaceProfile.tres points at the new shader and retunes its palette
to a deep-atmosphere/shadowed-band/lit-crest read; RockySurfaceProfile.tres
is untouched, so the rocky surface renders exactly as before.
Precision: every hash input is either an exact integer cell/row index or
a bounded [0,1) local fraction, same split as lunar_surface.gdshader and
Starfield.gd use for the same reason (world coordinates reach the tens of
thousands, where mobile float precision collapses). TIME-based lateral
drift is folded into the lattice coordinate via the same floor/remainder
trick rather than ever being added to a large cell index directly - see
combine_lattice()'s header note.
Tests: new tests/test_surface_profiles.gd (rocky resolves no
background_shader and keeps the lunar shader; gas resolves and applies
its own, different from lunar's). Full suite: 78/78 passed. Screenshot
of the gas surface confirms drifting bands with no craters, rendered
well inside the softpipe performance budget (20 frames in ~2m40s,
against lunar's "minutes per frame" baseline the ticket warns about).
Repo notes for the next agent, confirmed empirically this session:
- Never reference a plain (non-autoload) class_name class by its bare
global name from a -s entry-point test script - `load()` it by path
into a local var instead, same as PlanetSurfaceController.gd and
PlanetSetups.gd already do for exactly this reason. Doing it the
unsafe way here didn't fail to compile - it corrupted something in
the resource-loading state badly enough that an UNRELATED scene's
Rock.tscn lost its script attachment and RockField's rock-spawning
loop exhausted the physics server's entire RID pool. If a test's
crash trace implicates code you never touched, check every bare
global class_name reference in the test first.
- Running a `tests/screenshot.sh` render (Mesa softpipe, genuinely
minutes for a crater-shader scene) at the same time as a
`run_tests.sh` full-suite run starves both for CPU on this device -
kill one before waiting on the other rather than running them
concurrently.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:05:31 +02:00
|
|
|
|
|
|
|
|
# Loaded by path into a local, not referenced by its bare global
|
|
|
|
|
# class_name - same load-order hazard PlanetSetups.gd and
|
|
|
|
|
# PlanetSurfaceController.gd already work around: the class_name cache
|
|
|
|
|
# isn't guaranteed warm before an import pass has run.
|
|
|
|
|
var surface_profiles: Script = load("res://PlanetSurfaceProfiles.gd")
|
|
|
|
|
var rocky_profile: Resource = surface_profiles.get_profile("rocky")
|
|
|
|
|
var gas_profile: Resource = surface_profiles.get_profile("gas")
|
|
|
|
|
if rocky_profile.background_shader != null:
|
|
|
|
|
print("TEST_SURFACE_PROFILES: FAIL (rocky profile sets a background_shader - it should stay null)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return
|
|
|
|
|
if gas_profile.background_shader == null:
|
|
|
|
|
print("TEST_SURFACE_PROFILES: FAIL (gas profile has no background_shader set)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return
|
|
|
|
|
gas_shader = gas_profile.background_shader
|
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68)
The crater shader ran on every pixel of a fullscreen rect every frame and
was by far the most expensive thing in the surface scene. Nothing else came
close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid
per-frame queue_redraw() for transform-only changes, and LunarBackground.gd
is a handful of scalar ops.
Three reductions to the field itself:
- 5x5 -> 3x3 neighbour scan. That width was never the geometric answer;
it was insurance against craters seen clipping at cell boundaries
on-device, i.e. a crater reaching past its nominal radius because a hash
returned a value outside [0,1] and extrapolated mix(). Clamping the
crater CENTRE into its own cell, alongside the radius clamp already
there, bounds the reach whatever the hash returns - so a crater two cells
out is >= 1.0 cells away, a normalized distance of >= 2.63, past every
threshold the shading reacts to (the last saturates at 1.15). The outer
ring provably cannot change a pixel. This removes the clipping bug's
cause instead of hiding it behind 16 extra samples.
- One hash42 per cell instead of hash21 twice plus hash22, decorrelating
on output lanes rather than input offsets.
- Squared distances in the loop, square-rooted twice at the end rather
than once per sample.
Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms
fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction.
Then the larger change: the field is a fixed function of world position, so
every frame recomputed an identical answer. It is now made periodic (the
cell index is wrapped before hashing, which makes a Worley field tile
exactly rather than merely blend), baked once at load into one tile, and
scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch
per pixel. Two honest costs: the field repeats every 7040 world units, and
a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is
forgiving of both, having no landmarks and being smooth gradients already.
The bake waits a frame and asks the material whether it actually samples a
field_texture, so a gas giant - which swaps in its own background shader
(#51) - does not pay for a 2048-square tile it will never read. Headless
runs skip it entirely, having nothing to rasterize.
tests/test_lunar_field.gd guards the reasoning, since a fragment shader
cannot be asserted on directly: the premises are still in the source, the
worst-case ring-2 geometry stays clear of every threshold independent of
the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points,
the field repeats exactly in both axes, and the miss sentinel still cannot
overflow mediump into +Inf and then NaN.
test_surface_profiles.gd asserted "a rocky landing leaves the background
alone" by naming lunar_surface.gdshader; it now reads the shader from the
scene, which is the property it actually cares about.
PlanetSurface.tscn's edit also carries the planet-name relocation described
in the HUD commit - same scene file, so the two cannot be split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
|
|
|
if gas_shader == scene_shader:
|
|
|
|
|
print("TEST_SURFACE_PROFILES: FAIL (gas profile points at the scene's own shader, not its own)")
|
Give the gas planet surface its own background shader (#51)
lunar_surface.gdshader recoloured was the whole gas surface backdrop,
so a landed gas giant read as regolith - the crater field never went
away, just changed colour. New gas_surface.gdshader replaces it for
gas profiles only: a nearest-row-wins horizontal band field (same
single-scalar discipline as lunar's nearest-crater field), with
drifting per-row turbulence so the bands ripple instead of reading as
ruled lines. Keeps the exact same uniform interface as lunar
(camera_cell/camera_frac/camera_zoom/viewport_size/cell_size/
base_color/crater_color/highlight_color), so LunarBackground.gd needs
no changes at all.
PlanetSurfaceProfile gains background_shader (default null = keep the
scene's existing lunar shader); PlanetSurfaceController._compose_surface()
swaps the background material's shader in only when a profile names one.
GasSurfaceProfile.tres points at the new shader and retunes its palette
to a deep-atmosphere/shadowed-band/lit-crest read; RockySurfaceProfile.tres
is untouched, so the rocky surface renders exactly as before.
Precision: every hash input is either an exact integer cell/row index or
a bounded [0,1) local fraction, same split as lunar_surface.gdshader and
Starfield.gd use for the same reason (world coordinates reach the tens of
thousands, where mobile float precision collapses). TIME-based lateral
drift is folded into the lattice coordinate via the same floor/remainder
trick rather than ever being added to a large cell index directly - see
combine_lattice()'s header note.
Tests: new tests/test_surface_profiles.gd (rocky resolves no
background_shader and keeps the lunar shader; gas resolves and applies
its own, different from lunar's). Full suite: 78/78 passed. Screenshot
of the gas surface confirms drifting bands with no craters, rendered
well inside the softpipe performance budget (20 frames in ~2m40s,
against lunar's "minutes per frame" baseline the ticket warns about).
Repo notes for the next agent, confirmed empirically this session:
- Never reference a plain (non-autoload) class_name class by its bare
global name from a -s entry-point test script - `load()` it by path
into a local var instead, same as PlanetSurfaceController.gd and
PlanetSetups.gd already do for exactly this reason. Doing it the
unsafe way here didn't fail to compile - it corrupted something in
the resource-loading state badly enough that an UNRELATED scene's
Rock.tscn lost its script attachment and RockField's rock-spawning
loop exhausted the physics server's entire RID pool. If a test's
crash trace implicates code you never touched, check every bare
global class_name reference in the test first.
- Running a `tests/screenshot.sh` render (Mesa softpipe, genuinely
minutes for a crater-shader scene) at the same time as a
`run_tests.sh` full-suite run starves both for CPU on this device -
kill one before waiting on the other rather than running them
concurrently.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:05:31 +02:00
|
|
|
quit(1)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
game_state.planet_definition_id = "rocky"
|
|
|
|
|
scene = load("res://PlanetSurface.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process(delta: float) -> bool:
|
|
|
|
|
elapsed += delta
|
|
|
|
|
match stage:
|
|
|
|
|
0:
|
|
|
|
|
if elapsed < 0.2:
|
|
|
|
|
return false
|
|
|
|
|
var rocky_bg: ShaderMaterial = scene.get_node("BackgroundLayer/LunarBackground").material
|
|
|
|
|
rocky_shader_after_compose = rocky_bg.shader
|
|
|
|
|
scene.free()
|
|
|
|
|
|
|
|
|
|
game_state.planet_definition_id = "gas_giant"
|
|
|
|
|
scene = load("res://PlanetSurface.tscn").instantiate()
|
|
|
|
|
root.add_child(scene)
|
|
|
|
|
elapsed = 0.0
|
|
|
|
|
stage = 1
|
|
|
|
|
return false
|
|
|
|
|
1:
|
|
|
|
|
if elapsed < 0.2:
|
|
|
|
|
return false
|
|
|
|
|
var gas_bg: ShaderMaterial = scene.get_node("BackgroundLayer/LunarBackground").material
|
|
|
|
|
scene.free()
|
|
|
|
|
|
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68)
The crater shader ran on every pixel of a fullscreen rect every frame and
was by far the most expensive thing in the surface scene. Nothing else came
close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid
per-frame queue_redraw() for transform-only changes, and LunarBackground.gd
is a handful of scalar ops.
Three reductions to the field itself:
- 5x5 -> 3x3 neighbour scan. That width was never the geometric answer;
it was insurance against craters seen clipping at cell boundaries
on-device, i.e. a crater reaching past its nominal radius because a hash
returned a value outside [0,1] and extrapolated mix(). Clamping the
crater CENTRE into its own cell, alongside the radius clamp already
there, bounds the reach whatever the hash returns - so a crater two cells
out is >= 1.0 cells away, a normalized distance of >= 2.63, past every
threshold the shading reacts to (the last saturates at 1.15). The outer
ring provably cannot change a pixel. This removes the clipping bug's
cause instead of hiding it behind 16 extra samples.
- One hash42 per cell instead of hash21 twice plus hash22, decorrelating
on output lanes rather than input offsets.
- Squared distances in the loop, square-rooted twice at the end rather
than once per sample.
Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms
fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction.
Then the larger change: the field is a fixed function of world position, so
every frame recomputed an identical answer. It is now made periodic (the
cell index is wrapped before hashing, which makes a Worley field tile
exactly rather than merely blend), baked once at load into one tile, and
scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch
per pixel. Two honest costs: the field repeats every 7040 world units, and
a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is
forgiving of both, having no landmarks and being smooth gradients already.
The bake waits a frame and asks the material whether it actually samples a
field_texture, so a gas giant - which swaps in its own background shader
(#51) - does not pay for a 2048-square tile it will never read. Headless
runs skip it entirely, having nothing to rasterize.
tests/test_lunar_field.gd guards the reasoning, since a fragment shader
cannot be asserted on directly: the premises are still in the source, the
worst-case ring-2 geometry stays clear of every threshold independent of
the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points,
the field repeats exactly in both axes, and the miss sentinel still cannot
overflow mediump into +Inf and then NaN.
test_surface_profiles.gd asserted "a rocky landing leaves the background
alone" by naming lunar_surface.gdshader; it now reads the shader from the
scene, which is the property it actually cares about.
PlanetSurface.tscn's edit also carries the planet-name relocation described
in the HUD commit - same scene file, so the two cannot be split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
|
|
|
if rocky_shader_after_compose != scene_shader:
|
Give the gas planet surface its own background shader (#51)
lunar_surface.gdshader recoloured was the whole gas surface backdrop,
so a landed gas giant read as regolith - the crater field never went
away, just changed colour. New gas_surface.gdshader replaces it for
gas profiles only: a nearest-row-wins horizontal band field (same
single-scalar discipline as lunar's nearest-crater field), with
drifting per-row turbulence so the bands ripple instead of reading as
ruled lines. Keeps the exact same uniform interface as lunar
(camera_cell/camera_frac/camera_zoom/viewport_size/cell_size/
base_color/crater_color/highlight_color), so LunarBackground.gd needs
no changes at all.
PlanetSurfaceProfile gains background_shader (default null = keep the
scene's existing lunar shader); PlanetSurfaceController._compose_surface()
swaps the background material's shader in only when a profile names one.
GasSurfaceProfile.tres points at the new shader and retunes its palette
to a deep-atmosphere/shadowed-band/lit-crest read; RockySurfaceProfile.tres
is untouched, so the rocky surface renders exactly as before.
Precision: every hash input is either an exact integer cell/row index or
a bounded [0,1) local fraction, same split as lunar_surface.gdshader and
Starfield.gd use for the same reason (world coordinates reach the tens of
thousands, where mobile float precision collapses). TIME-based lateral
drift is folded into the lattice coordinate via the same floor/remainder
trick rather than ever being added to a large cell index directly - see
combine_lattice()'s header note.
Tests: new tests/test_surface_profiles.gd (rocky resolves no
background_shader and keeps the lunar shader; gas resolves and applies
its own, different from lunar's). Full suite: 78/78 passed. Screenshot
of the gas surface confirms drifting bands with no craters, rendered
well inside the softpipe performance budget (20 frames in ~2m40s,
against lunar's "minutes per frame" baseline the ticket warns about).
Repo notes for the next agent, confirmed empirically this session:
- Never reference a plain (non-autoload) class_name class by its bare
global name from a -s entry-point test script - `load()` it by path
into a local var instead, same as PlanetSurfaceController.gd and
PlanetSetups.gd already do for exactly this reason. Doing it the
unsafe way here didn't fail to compile - it corrupted something in
the resource-loading state badly enough that an UNRELATED scene's
Rock.tscn lost its script attachment and RockField's rock-spawning
loop exhausted the physics server's entire RID pool. If a test's
crash trace implicates code you never touched, check every bare
global class_name reference in the test first.
- Running a `tests/screenshot.sh` render (Mesa softpipe, genuinely
minutes for a crater-shader scene) at the same time as a
`run_tests.sh` full-suite run starves both for CPU on this device -
kill one before waiting on the other rather than running them
concurrently.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:05:31 +02:00
|
|
|
print("TEST_SURFACE_PROFILES: FAIL (landing on a rocky planet changed the background shader)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
if gas_bg.shader != gas_shader:
|
|
|
|
|
print("TEST_SURFACE_PROFILES: FAIL (landing on a gas giant did not swap in gas_surface.gdshader)")
|
|
|
|
|
quit(1)
|
|
|
|
|
return true
|
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68)
The crater shader ran on every pixel of a fullscreen rect every frame and
was by far the most expensive thing in the surface scene. Nothing else came
close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid
per-frame queue_redraw() for transform-only changes, and LunarBackground.gd
is a handful of scalar ops.
Three reductions to the field itself:
- 5x5 -> 3x3 neighbour scan. That width was never the geometric answer;
it was insurance against craters seen clipping at cell boundaries
on-device, i.e. a crater reaching past its nominal radius because a hash
returned a value outside [0,1] and extrapolated mix(). Clamping the
crater CENTRE into its own cell, alongside the radius clamp already
there, bounds the reach whatever the hash returns - so a crater two cells
out is >= 1.0 cells away, a normalized distance of >= 2.63, past every
threshold the shading reacts to (the last saturates at 1.15). The outer
ring provably cannot change a pixel. This removes the clipping bug's
cause instead of hiding it behind 16 extra samples.
- One hash42 per cell instead of hash21 twice plus hash22, decorrelating
on output lanes rather than input offsets.
- Squared distances in the loop, square-rooted twice at the end rather
than once per sample.
Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms
fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction.
Then the larger change: the field is a fixed function of world position, so
every frame recomputed an identical answer. It is now made periodic (the
cell index is wrapped before hashing, which makes a Worley field tile
exactly rather than merely blend), baked once at load into one tile, and
scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch
per pixel. Two honest costs: the field repeats every 7040 world units, and
a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is
forgiving of both, having no landmarks and being smooth gradients already.
The bake waits a frame and asks the material whether it actually samples a
field_texture, so a gas giant - which swaps in its own background shader
(#51) - does not pay for a 2048-square tile it will never read. Headless
runs skip it entirely, having nothing to rasterize.
tests/test_lunar_field.gd guards the reasoning, since a fragment shader
cannot be asserted on directly: the premises are still in the source, the
worst-case ring-2 geometry stays clear of every threshold independent of
the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points,
the field repeats exactly in both axes, and the miss sentinel still cannot
overflow mediump into +Inf and then NaN.
test_surface_profiles.gd asserted "a rocky landing leaves the background
alone" by naming lunar_surface.gdshader; it now reads the shader from the
scene, which is the property it actually cares about.
PlanetSurface.tscn's edit also carries the planet-name relocation described
in the HUD commit - same scene file, so the two cannot be split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
|
|
|
if gas_bg.shader == scene_shader:
|
|
|
|
|
print("TEST_SURFACE_PROFILES: FAIL (gas surface still uses the scene's own lunar background)")
|
Give the gas planet surface its own background shader (#51)
lunar_surface.gdshader recoloured was the whole gas surface backdrop,
so a landed gas giant read as regolith - the crater field never went
away, just changed colour. New gas_surface.gdshader replaces it for
gas profiles only: a nearest-row-wins horizontal band field (same
single-scalar discipline as lunar's nearest-crater field), with
drifting per-row turbulence so the bands ripple instead of reading as
ruled lines. Keeps the exact same uniform interface as lunar
(camera_cell/camera_frac/camera_zoom/viewport_size/cell_size/
base_color/crater_color/highlight_color), so LunarBackground.gd needs
no changes at all.
PlanetSurfaceProfile gains background_shader (default null = keep the
scene's existing lunar shader); PlanetSurfaceController._compose_surface()
swaps the background material's shader in only when a profile names one.
GasSurfaceProfile.tres points at the new shader and retunes its palette
to a deep-atmosphere/shadowed-band/lit-crest read; RockySurfaceProfile.tres
is untouched, so the rocky surface renders exactly as before.
Precision: every hash input is either an exact integer cell/row index or
a bounded [0,1) local fraction, same split as lunar_surface.gdshader and
Starfield.gd use for the same reason (world coordinates reach the tens of
thousands, where mobile float precision collapses). TIME-based lateral
drift is folded into the lattice coordinate via the same floor/remainder
trick rather than ever being added to a large cell index directly - see
combine_lattice()'s header note.
Tests: new tests/test_surface_profiles.gd (rocky resolves no
background_shader and keeps the lunar shader; gas resolves and applies
its own, different from lunar's). Full suite: 78/78 passed. Screenshot
of the gas surface confirms drifting bands with no craters, rendered
well inside the softpipe performance budget (20 frames in ~2m40s,
against lunar's "minutes per frame" baseline the ticket warns about).
Repo notes for the next agent, confirmed empirically this session:
- Never reference a plain (non-autoload) class_name class by its bare
global name from a -s entry-point test script - `load()` it by path
into a local var instead, same as PlanetSurfaceController.gd and
PlanetSetups.gd already do for exactly this reason. Doing it the
unsafe way here didn't fail to compile - it corrupted something in
the resource-loading state badly enough that an UNRELATED scene's
Rock.tscn lost its script attachment and RockField's rock-spawning
loop exhausted the physics server's entire RID pool. If a test's
crash trace implicates code you never touched, check every bare
global class_name reference in the test first.
- Running a `tests/screenshot.sh` render (Mesa softpipe, genuinely
minutes for a crater-shader scene) at the same time as a
`run_tests.sh` full-suite run starves both for CPU on this device -
kill one before waiting on the other rather than running them
concurrently.
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 09:05:31 +02:00
|
|
|
quit(1)
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
print("TEST_SURFACE_PROFILES: PASS")
|
|
|
|
|
quit(0)
|
|
|
|
|
return true
|
|
|
|
|
return false
|