Replace tiled skybox background with a procedural starfield shader

The old background tiled a single non-seamless 1024x1024 skybox render
across the map, producing visible seams and stars too faint/small to
survive minification. Replace it with a canvas shader (starfield.gdshader)
that draws layered depth stars + soft nebula tint directly, so there's
no repeating tile (no seams) and stars stay pixel-crisp at any zoom.
Applied to both the in-game map background and the main menu backdrop.

Also fixes a bug the swap introduced: world.gd cast the map's
Background node to TextureRect to read world bounds, which silently
failed once Background became a ColorRect, leaving GameConfig.world_bounds
on its tiny fallback default and hard-clamping every ship into an
invisible box near the map center. Cast to Control instead.

Also clears leftover placeholder Walls/Asteroids tile data in
map_01.tscn that sat directly in the spawn-to-spawn flight corridor —
harmless once bounds were fixed, but still untuned test content per
CLAUDE.md's own Current Tasks list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 21:41:47 -04:00
parent 5fe7c01052
commit 1ebcca3807
4 changed files with 118 additions and 17 deletions
+3 -9
View File
@@ -55,18 +55,12 @@ func _build_ui() -> void:
func _add_bg() -> void:
var bg := TextureRect.new()
bg.texture = load("res://assets/images/background/skybox/2.png")
bg.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
bg.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
var bg := ColorRect.new()
bg.material = ShaderMaterial.new()
bg.material.shader = load("res://world/starfield.gdshader")
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(bg)
var ov := ColorRect.new()
ov.color = Color(0.0, 0.0, 0.0, 0.55)
ov.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(ov)
func _add_logo() -> void:
var title := Label.new()
+6 -7
View File
@@ -1,27 +1,26 @@
[gd_scene format=4 uid="uid://cecn8fghm8h3j"]
[ext_resource type="TileSet" uid="uid://cufng7112p6a7" path="res://world/world_tileset.tres" id="1_tileset"]
[ext_resource type="Texture2D" uid="uid://d0i6jwqh131b1" path="res://assets/images/background/skybox/1.png" id="2_skybox"]
[ext_resource type="Shader" path="res://world/starfield.gdshader" id="2_starfield"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_starfield"]
shader = ExtResource("2_starfield")
[node name="Map01" type="Node2D" unique_id=1843060857]
[node name="Background" type="TextureRect" parent="." unique_id=1935208901]
texture_repeat = 2
[node name="Background" type="ColorRect" parent="." unique_id=1935208901]
material = SubResource("ShaderMaterial_starfield")
offset_left = -7008.0
offset_top = -3000.0
offset_right = 7008.0
offset_bottom = 3000.0
texture = ExtResource("2_skybox")
expand_mode = 3
[node name="Walls" type="TileMapLayer" parent="." unique_id=1965607014]
scale = Vector2(0.15, 0.15)
tile_map_data = PackedByteArray("AAAFACYAAAABAAAAAAAFABkAAAABAAAAAAAFAA0AAAABAAAAAAAFAAAAAAABAAAAAAAFAPT/AAABAAAAAAARABEAAAABAAEAAAA=")
tile_set = ExtResource("1_tileset")
[node name="Asteroids" type="TileMapLayer" parent="." unique_id=1745417803]
scale = Vector2(0.15, 0.15)
tile_map_data = PackedByteArray("AAAMAAEAAQAAAAMAAAALAPz/AQAAAAMAAAD6////AQAAAAMAAAD6/wgAAQAAAAMAAAAOAAgAAQAAAAMAAAA=")
tile_set = ExtResource("1_tileset")
[node name="SpawnPoints" type="Node2D" parent="." unique_id=1668900368]
+108
View File
@@ -0,0 +1,108 @@
shader_type canvas_item;
// Procedural starfield: draws directly over the whole background rect in one
// pass, so there's no repeating tile and therefore no seams (unlike the old
// approach of tiling a single non-seamless skybox render). Star size/glow use
// smoothstep, not texture sampling, so they stay pixel-crisp at any zoom
// instead of getting minified/blurred by texture filtering.
uniform vec4 sky_color: source_color = vec4(0.008, 0.01, 0.025, 1.0);
uniform vec4 nebula_color: source_color = vec4(0.05, 0.08, 0.16, 1.0);
uniform float nebula_scale = 0.0025;
uniform float nebula_strength = 0.5;
uniform float cell_size_far = 56.0;
uniform float density_far = 0.10;
uniform float size_far = 0.7;
uniform float brightness_far = 0.35;
uniform float cell_size_mid = 100.0;
uniform float density_mid = 0.14;
uniform float size_mid = 1.0;
uniform float brightness_mid = 0.5;
uniform float cell_size_near = 200.0;
uniform float density_near = 0.18;
uniform float size_near = 1.4;
uniform float brightness_near = 0.7;
varying vec2 world_pos;
void vertex() {
world_pos = VERTEX;
}
float hash11(float p) {
p = fract(p * 0.1031);
p *= p + 33.33;
p *= p + p;
return fract(p);
}
float hash21(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.x + p3.y) * p3.z);
}
vec2 hash22(vec2 p) {
return vec2(hash21(p), hash21(p + 19.19));
}
float value_noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash21(i);
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
float fbm(vec2 p) {
float value = 0.0;
float amp = 0.5;
for (int i = 0; i < 4; i++) {
value += amp * value_noise(p);
p *= 2.0;
amp *= 0.5;
}
return value;
}
// One depth layer of stars on an independent grid, so far/mid/near layers
// never line up with each other.
vec3 star_layer(vec2 pos, float cell_size, float density, float size, float brightness) {
vec2 cell = floor(pos / cell_size);
float roll = hash21(cell);
float has_star = step(1.0 - density, roll);
vec2 jitter = hash22(cell + 0.5);
vec2 star_pos = (cell + jitter) * cell_size;
float d = length(pos - star_pos);
float core = smoothstep(size, 0.0, d);
float halo = smoothstep(size * 3.5, 0.0, d) * 0.15;
float glow = (core + halo) * has_star * brightness;
float tint_roll = hash21(cell + 7.7);
vec3 tint = mix(vec3(0.75, 0.82, 1.0), vec3(1.0, 0.92, 0.75), step(0.5, tint_roll));
tint = mix(vec3(1.0), tint, 0.5);
return tint * glow;
}
void fragment() {
vec2 pos = world_pos;
float nebula = fbm(pos * nebula_scale);
vec3 color = mix(sky_color.rgb, nebula_color.rgb, clamp(nebula * nebula_strength, 0.0, 1.0));
vec3 stars = vec3(0.0);
stars += star_layer(pos, cell_size_far, density_far, size_far, brightness_far);
stars += star_layer(pos + vec2(500.0, 0.0), cell_size_mid, density_mid, size_mid, brightness_mid);
stars += star_layer(pos + vec2(0.0, 900.0), cell_size_near, density_near, size_near, brightness_near);
COLOR = vec4(color + stars, 1.0);
}
+1 -1
View File
@@ -20,7 +20,7 @@ var _offered_race_ids: Array = []
func _ready() -> void:
var map := MAP_SCENE.instantiate()
_map_container.add_child(map)
var background := map.get_node_or_null("Background") as TextureRect
var background := map.get_node_or_null("Background") as Control
if background:
GameConfig.world_bounds = Rect2(background.position, background.size)
_build_tile_collisions(map)