125 lines
5.9 KiB
Text
125 lines
5.9 KiB
Text
|
|
shader_type canvas_item;
|
||
|
|
|
||
|
|
// Procedural infinite starfield. Reconstructs a world-space coordinate for
|
||
|
|
// every screen pixel from the camera's current position/zoom (the exact
|
||
|
|
// same mapping used for every other object in the scene - ships, enemies,
|
||
|
|
// etc.), then hashes a grid cell in world space to decide whether that
|
||
|
|
// cell holds a star, and how big/bright it is. Cost is one fragment
|
||
|
|
// shader pass over the screen - constant regardless of zoom level or how
|
||
|
|
// many stars are "visible", with zero per-star CPU bookkeeping.
|
||
|
|
//
|
||
|
|
// Star size is a fixed WORLD-space radius (not compensated for zoom), so
|
||
|
|
// stars shrink/grow exactly in sync with ships and everything else when
|
||
|
|
// the camera zooms - zooming out reveals more of the field (unavoidable -
|
||
|
|
// a wider view always shows more of a spatially spread-out field) but
|
||
|
|
// each star also gets visibly smaller, so it still reads as "zoomed out"
|
||
|
|
// consistently with the foreground instead of feeling mismatched.
|
||
|
|
|
||
|
|
// Stars are found with a 3x3-neighbor cellular lookup: each pixel checks
|
||
|
|
// its own grid cell AND all 8 neighbors for a nearby star center. Checking
|
||
|
|
// only the pixel's own cell (which this shader did originally) hard-clips
|
||
|
|
// any star whose glow crosses a cell boundary, leaving a star with sharp
|
||
|
|
// straight edges on one or two sides and normal soft falloff on the others
|
||
|
|
// - which is exactly the artifact that was reported on-device here, and
|
||
|
|
// the reason the same technique was already used in lunar_surface.gdshader.
|
||
|
|
//
|
||
|
|
// The camera position arrives pre-split into a cell index plus a fraction
|
||
|
|
// within that cell, so this shader never handles an absolute world
|
||
|
|
// coordinate (which reaches the tens of thousands once the fleet has
|
||
|
|
// explored, where mobile float precision degrades). See Starfield.gd.
|
||
|
|
uniform vec2 camera_cell = vec2(0.0, 0.0);
|
||
|
|
uniform vec2 camera_frac = vec2(0.0, 0.0);
|
||
|
|
uniform float camera_zoom = 1.0;
|
||
|
|
uniform vec2 viewport_size = vec2(720.0, 1280.0);
|
||
|
|
uniform float cell_size = 60.0;
|
||
|
|
uniform vec4 bg_color : source_color = vec4(0.02, 0.02, 0.08, 1.0);
|
||
|
|
// DIAGNOSTIC ONLY - see the same uniform in lunar_surface.gdshader.
|
||
|
|
uniform float debug_mode = 0.0;
|
||
|
|
|
||
|
|
// Hash pair chosen for precision safety on mobile GPUs. The previous
|
||
|
|
// version multiplied the cell coordinate by ~123/456 before fract(), which
|
||
|
|
// lands the intermediate in the thousands where a mobile float loses its
|
||
|
|
// fractional bits entirely. These multipliers are all < 1, so every
|
||
|
|
// intermediate stays small no matter how large the cell index gets.
|
||
|
|
// (Standard "hash without sine" formulation, Dave Hoskins.)
|
||
|
|
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) {
|
||
|
|
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
||
|
|
p3 += dot(p3, p3.yzx + 33.33);
|
||
|
|
return fract((p3.xx + p3.yz) * p3.zy);
|
||
|
|
}
|
||
|
|
|
||
|
|
void fragment() {
|
||
|
|
vec2 screen_px = UV * viewport_size;
|
||
|
|
vec2 centered = screen_px - viewport_size * 0.5;
|
||
|
|
|
||
|
|
// Position is expressed RELATIVE TO THE CAMERA'S CELL, never as an
|
||
|
|
// absolute world coordinate. camera_frac is the camera's offset inside
|
||
|
|
// its own cell (always in [0,1]) and `centered` spans at most a few
|
||
|
|
// cells, so every value here stays small and exactly representable even
|
||
|
|
// on a mobile GPU - whereas an absolute world position reaches the tens
|
||
|
|
// of thousands, where float precision degrades and no `highp` qualifier
|
||
|
|
// can help (it widens the type, but the magnitude is the problem).
|
||
|
|
// GDScript does the large-number split in double precision; see
|
||
|
|
// LunarBackground.gd / Starfield.gd.
|
||
|
|
//
|
||
|
|
// cell and local also MUST come from one shared value, with local
|
||
|
|
// derived as (rel - cell_rel) rather than an independent fract():
|
||
|
|
// calling floor(x) and fract(x) separately lets the GPU round the two
|
||
|
|
// evaluations differently, which right at a cell boundary makes them
|
||
|
|
// disagree by a full cell and snaps the sampled point one cell sideways.
|
||
|
|
vec2 rel = camera_frac + centered * (camera_zoom / cell_size);
|
||
|
|
vec2 cell_rel = floor(rel);
|
||
|
|
vec2 local = rel - cell_rel;
|
||
|
|
// Absolute cell index, used only for hashing. Built from exact integers.
|
||
|
|
vec2 cell = camera_cell + cell_rel;
|
||
|
|
|
||
|
|
float star = 0.0;
|
||
|
|
|
||
|
|
for (int dx = -1; dx <= 1; dx++) {
|
||
|
|
for (int dy = -1; dy <= 1; dy++) {
|
||
|
|
vec2 offset = vec2(float(dx), float(dy));
|
||
|
|
highp vec2 neighbor_cell = cell + offset;
|
||
|
|
|
||
|
|
float has_star = step(0.86, hash21(neighbor_cell + vec2(99.0, 7.0)));
|
||
|
|
// Star center expressed in THIS pixel's cell-local space, so a
|
||
|
|
// star living in a neighboring cell is still measured correctly
|
||
|
|
// from here instead of being clipped away at the cell edge.
|
||
|
|
vec2 star_center = offset + hash22(neighbor_cell);
|
||
|
|
float dist = length(local - star_center);
|
||
|
|
|
||
|
|
// Fixed world-space size (fraction of cell) - no zoom
|
||
|
|
// compensation, so it scales exactly like any other world object
|
||
|
|
// when the camera zooms.
|
||
|
|
float size = mix(0.12, 0.22, hash21(neighbor_cell + vec2(3.0, 5.0)));
|
||
|
|
|
||
|
|
float brightness = mix(0.35, 1.0, hash21(neighbor_cell + vec2(11.0, 13.0)));
|
||
|
|
// smoothstep() edges must be given with edge0 < edge1 - GLSL
|
||
|
|
// leaves the reversed-edge case undefined, and different GPUs are
|
||
|
|
// free to compute something else for it. Use correctly-ordered
|
||
|
|
// edges and invert the result arithmetically instead of swapping
|
||
|
|
// the edges.
|
||
|
|
float s = has_star * (1.0 - smoothstep(0.0, size, dist)) * brightness;
|
||
|
|
star = max(star, s);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
vec3 out_color = bg_color.rgb + vec3(star);
|
||
|
|
|
||
|
|
// Diagnostic checkerboard - makes cell boundaries directly visible so a
|
||
|
|
// screenshot can settle whether the reported cuts lie on them.
|
||
|
|
if (debug_mode > 0.5 && debug_mode < 1.5) {
|
||
|
|
float checker = mod(cell.x + cell.y, 2.0);
|
||
|
|
out_color = mix(out_color, vec3(0.10, 0.30, 0.55), checker * 0.35);
|
||
|
|
vec2 edge = min(local, 1.0 - local);
|
||
|
|
float on_edge = 1.0 - smoothstep(0.0, 0.012, min(edge.x, edge.y));
|
||
|
|
out_color = mix(out_color, vec3(1.0, 0.25, 0.25), on_edge * 0.9);
|
||
|
|
}
|
||
|
|
|
||
|
|
COLOR = vec4(out_color, 1.0);
|
||
|
|
}
|