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>
This commit is contained in:
parent
672907ea0c
commit
657ca9d035
11 changed files with 845 additions and 74 deletions
|
|
@ -19,7 +19,7 @@ extends SceneTree
|
|||
# rather than checking on the same one.
|
||||
|
||||
var game_state: Node
|
||||
var lunar_shader: Shader
|
||||
var scene_shader: Shader
|
||||
var gas_shader: Shader
|
||||
var rocky_shader_after_compose: Shader
|
||||
var scene: Node
|
||||
|
|
@ -29,7 +29,17 @@ var elapsed: float = 0.0
|
|||
|
||||
func _initialize() -> void:
|
||||
game_state = root.get_node("GameState")
|
||||
lunar_shader = load("res://lunar_surface.gdshader")
|
||||
# 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()
|
||||
|
||||
# Loaded by path into a local, not referenced by its bare global
|
||||
# class_name - same load-order hazard PlanetSetups.gd and
|
||||
|
|
@ -47,8 +57,8 @@ func _initialize() -> void:
|
|||
quit(1)
|
||||
return
|
||||
gas_shader = gas_profile.background_shader
|
||||
if gas_shader == lunar_shader:
|
||||
print("TEST_SURFACE_PROFILES: FAIL (gas profile points at the lunar shader, not its own)")
|
||||
if gas_shader == scene_shader:
|
||||
print("TEST_SURFACE_PROFILES: FAIL (gas profile points at the scene's own shader, not its own)")
|
||||
quit(1)
|
||||
return
|
||||
|
||||
|
|
@ -79,7 +89,7 @@ func _process(delta: float) -> bool:
|
|||
var gas_bg: ShaderMaterial = scene.get_node("BackgroundLayer/LunarBackground").material
|
||||
scene.free()
|
||||
|
||||
if rocky_shader_after_compose != lunar_shader:
|
||||
if rocky_shader_after_compose != scene_shader:
|
||||
print("TEST_SURFACE_PROFILES: FAIL (landing on a rocky planet changed the background shader)")
|
||||
quit(1)
|
||||
return true
|
||||
|
|
@ -87,8 +97,8 @@ func _process(delta: float) -> bool:
|
|||
print("TEST_SURFACE_PROFILES: FAIL (landing on a gas giant did not swap in gas_surface.gdshader)")
|
||||
quit(1)
|
||||
return true
|
||||
if gas_bg.shader == lunar_shader:
|
||||
print("TEST_SURFACE_PROFILES: FAIL (gas surface still uses the lunar shader)")
|
||||
if gas_bg.shader == scene_shader:
|
||||
print("TEST_SURFACE_PROFILES: FAIL (gas surface still uses the scene's own lunar background)")
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue