The setup editor and the landed surface drew different backgrounds for the same planet. Each scene carried its own hand-written ShaderMaterial, and only the game side ever swapped in a profile's shader, so a gas giant authored in the editor showed recoloured craters while the game showed cloud bands. Both now instance one SurfaceBackground.tscn and configure it through one method, and every profile names its own shader - the old "null means keep whatever the scene already had" convention was the drift, since each scene "already had" something different. The shared material is resource_local_to_scene, or two live surfaces would repaint each other. The gas giant's bands broke into disjoint segments once per cell: the second turbulence octave doubled only the INTEGER half of a lattice coordinate, which is a different point in every cell rather than the same point at twice the frequency. Measured, the old form steps 0.153 at a cell boundary against 0.0002 now. A second discontinuity went with it - the camera cell index arrives pre-wrapped, so hashing it raw rearranged the whole sky every 32 cells; hashes are now wrapped per octave and the field is exactly periodic. Bands also gained low-frequency octaves and per-row differential drift. WorldMap swallowed every touch in _input(), which runs before the GUI system, so CloseButton never saw one: the map could be opened and not closed. Android and desktop hid it behind Godot's emulated mouse event, which this never swallowed; a phone browser gives no such second chance. Its test had been closing the map by calling the method the button was failing to reach. Construction now draws on the fleet hold as well as the bank. With an empty starting bank and ore bankable only at a mothership, charging construction to the bank alone made the first mothership cost ore that could not yet exist. Also: a conquered planet no longer re-spawns its garrison (guarded on having capture points, since a setup with none registers as vacuously conquered); planets keep the map they drew, with the candidate list sorted rather than left in filesystem order and assignments pruned rather than cleared on rescan; and the map editor lists setups at a tappable size and can delete one, as an explicit armed mode rather than a tap away from destroying work. Full suite 106/106. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
241 lines
12 KiB
Text
241 lines
12 KiB
Text
shader_type canvas_item;
|
|
|
|
// Gas giant cloud-band backdrop for a landed gas surface. Same uniform
|
|
// interface as lunar_surface.gdshader (camera_cell/camera_frac/camera_zoom/
|
|
// viewport_size/cell_size/base_color/crater_color/highlight_color), so
|
|
// SurfaceBackground.gd feeds this shader with no changes at all - only the
|
|
// surface profile decides which shader the material uses.
|
|
//
|
|
// Renders horizontal cloud bands instead of craters: each grid row (a
|
|
// horizontal strip one cell tall) hashes its own band centre, thickness and
|
|
// drift speed, nearest-row-wins picks which band a pixel belongs to, and the
|
|
// resulting distance field drives colour exactly the way the crater distance
|
|
// field does in lunar_surface.gdshader - base_color is the deep atmosphere,
|
|
// crater_color the shadowed band body, highlight_color the lit band crest.
|
|
// Turbulence perturbs each row's edge so the bands ripple instead of reading
|
|
// as ruled lines, and each row slides sideways at its OWN rate, which is what
|
|
// gives a gas giant its sheared, differentially-rotating look.
|
|
//
|
|
// PRECISION: never reconstruct an absolute world position. Every value fed
|
|
// to a hash is either a small camera-relative fraction (local, in [0,1]) or
|
|
// an exact integer cell/row index (cell, camera_cell + cell_rel) - the same
|
|
// split lunar_surface.gdshader and Starfield.gd use, for the same reason:
|
|
// world coordinates reach the tens of thousands, where a mobile float loses
|
|
// the fractional precision this shader depends on and no `highp` fixes it.
|
|
// The one exception is TIME, which the drift is built from directly per the
|
|
// ticket - see combine_lattice() for why that's still safe to mix in.
|
|
//
|
|
// CONTINUITY is the other invariant, and it is the one that broke. Every
|
|
// lattice coordinate handed to value_noise() is an (integer, fraction) pair,
|
|
// and the noise is only continuous if crossing a cell boundary - fraction
|
|
// 1 -> 0 while the integer steps by one - lands on the same value from both
|
|
// sides. That holds for the pair as it is built, and for halve_lattice()
|
|
// below, which is written to preserve it. It did NOT hold for the old
|
|
// second octave, which doubled the integer part while
|
|
// leaving the fraction alone: at every cell boundary the noise jumped from
|
|
// hash(2i+1) to hash(2i+2) with nothing in between, so the band edges broke
|
|
// into visibly disjoint segments once per cell across the whole screen. If a
|
|
// new octave is added, rescale the coordinate with halve_lattice() (or an
|
|
// equivalent that folds the carry) - anything that touches the integer and
|
|
// the fraction independently puts the seams straight back.
|
|
|
|
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 = 220.0;
|
|
// The period the camera cell arrives wrapped to - see SurfaceBackground.gd,
|
|
// which wraps it in double precision before it ever reaches a shader. Must
|
|
// be a multiple of 4, so halving it twice for the coarse octaves below stays
|
|
// an exact integer count of cells.
|
|
uniform float tile_cells = 32.0;
|
|
uniform vec4 base_color : source_color = vec4(0.13, 0.025, 0.22, 1.0);
|
|
uniform vec4 crater_color : source_color = vec4(0.055, 0.008, 0.11, 1.0);
|
|
uniform vec4 highlight_color : source_color = vec4(0.52, 0.17, 0.7, 1.0);
|
|
|
|
// Row scan range. 4 rows x a handful of hashes each is far cheaper than
|
|
// lunar's 5x5 (25-sample) crater scan - this shader has to stay renderable
|
|
// through the softpipe software rasterizer the screenshot harness uses.
|
|
const int ROW_SCAN = 2; // rows -1..+2
|
|
const float MIN_THICKNESS = 0.25;
|
|
const float MAX_THICKNESS = 0.55;
|
|
// How far a band edge may wander, in cells. The scan above has to reach
|
|
// every row that could still change a pixel, or a band gets shaded by a row
|
|
// the loop never visited - a hard horizontal seam. The furthest normalized
|
|
// distance anything below reacts to is MAX_SIGNIFICANT_DISTANCE (the
|
|
// crest's outer edge), so a row matters out to that times MAX_THICKNESS,
|
|
// plus TURBULENCE_STRENGTH * 0.5 =
|
|
// 0.74 cells; with band centres anywhere in [0,1) that is only ever dy in
|
|
// -1..+1, inside the loop's -1..+2. Raising either constant eats that
|
|
// headroom - tests/test_gas_bands.gd fails if it runs out.
|
|
const float TURBULENCE_STRENGTH = 0.32;
|
|
const float DRIFT_SPEED = 0.045; // cells per second, lateral
|
|
// The largest normalized distance any shading term below reacts to: past
|
|
// this the crest has faded out and the pixel is plain atmosphere. Named
|
|
// because the row-scan headroom argument above is stated in terms of it,
|
|
// and tests/test_gas_bands.gd reads both out of this file to check it.
|
|
const float MAX_SIGNIFICANT_DISTANCE = 1.05;
|
|
|
|
// Same hash21 formulation as lunar_surface.gdshader (Dave Hoskins, all
|
|
// multipliers < 1 so it stays accurate at any cell-index magnitude).
|
|
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);
|
|
}
|
|
|
|
// Three independent values from one hash, so a row's centre, thickness and
|
|
// drift rate cost roughly what one of them used to. Dave Hoskins' hash23,
|
|
// same family and the same all-multipliers-below-one property.
|
|
vec3 hash23(vec2 p) {
|
|
vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
|
p3 += dot(p3, p3.yxz + 33.33);
|
|
return fract((p3.xxy + p3.yzz) * p3.zyx);
|
|
}
|
|
|
|
// Splits a float into an exact integer part and a [0,1) remainder - the
|
|
// same floor/subtract trick `rel` -> `cell_rel` + `local` already uses
|
|
// below, generalized so it can also fold TIME-based drift into a lattice
|
|
// coordinate without ever forming one large continuous float.
|
|
vec2 int_frac(float v) {
|
|
float i = floor(v);
|
|
return vec2(i, v - i);
|
|
}
|
|
|
|
// Combines two already-safe (integer, fraction) pairs - e.g. a cell index
|
|
// plus a drift offset - by adding the integers (exact, since both are
|
|
// integers) and the fractions (both in [0,1), so the sum is in [0,2)),
|
|
// then re-normalizing any carry out of the fraction back into the integer
|
|
// part. Never adds an integer directly to a fraction.
|
|
vec2 combine_lattice(vec2 base, vec2 add) {
|
|
float f = base.y + add.y;
|
|
float carry = floor(f);
|
|
return vec2(base.x + add.x + carry, f - carry);
|
|
}
|
|
|
|
// Halves a lattice coordinate: the same point, expressed on a lattice of
|
|
// twice the spacing, which is how a lower-frequency octave is taken without
|
|
// ever forming the underlying continuous coordinate.
|
|
//
|
|
// Continuous by construction. `rem` is which half of the coarse cell the
|
|
// fine index sits in (0 or 1), so the fraction is (rem + f) * 0.5, sweeping
|
|
// 0 -> 1 across the two fine cells that make up one coarse cell. Crossing
|
|
// out of the first (f: 1 -> 0, i -> i+1, rem: 0 -> 1) gives 0.5 from both
|
|
// sides; crossing out of the second gives 1.0 then 0.0 with the coarse
|
|
// index stepping, which is the same lattice point.
|
|
vec2 halve_lattice(vec2 coord) {
|
|
float half_i = floor(coord.x * 0.5);
|
|
float rem = coord.x - half_i * 2.0;
|
|
return vec2(half_i, (rem + coord.y) * 0.5);
|
|
}
|
|
|
|
// 1D value noise over an integer lattice, given as a pre-split (i, f) pair:
|
|
// hashes the two neighboring integers and blends by the fraction. `axis`
|
|
// varies the hash sequence so different callers (different rows, or the
|
|
// turbulence octaves) don't sample the same noise.
|
|
//
|
|
// Every index is taken modulo `period` first, which is what makes the field
|
|
// exactly periodic - the same trick lunar_surface.gdshader uses. It is not
|
|
// decoration: the camera cell index arrives ALREADY WRAPPED into
|
|
// [0, tile_cells), so a pixel's cell index shifts by exactly tile_cells the
|
|
// moment the camera crosses a wrap boundary. Hash on the raw index and the
|
|
// entire sky rearranges itself at that instant. Hash on the wrapped one and
|
|
// the shift lands on the same value, so the boundary cannot be seen.
|
|
//
|
|
// The +1 neighbour is wrapped too, or the seam would simply move to the
|
|
// last cell of each period instead of disappearing.
|
|
float value_noise(vec2 coord, float period, float axis) {
|
|
float a = hash21(vec2(mod(coord.x, period), axis));
|
|
float b = hash21(vec2(mod(coord.x + 1.0, period), axis));
|
|
return mix(a, b, smoothstep(0.0, 1.0, coord.y));
|
|
}
|
|
|
|
// Fractal turbulence for one row: three octaves, each an exact rescaling of
|
|
// the same lattice coordinate. The weights are deliberately bottom-heavy -
|
|
// the quarter-frequency octave (one undulation per four cells, ~880 world
|
|
// units) carries most of the amplitude, so bands sweep in long slow curves
|
|
// and the finer octaves only roughen the edge. The old version was a single
|
|
// cell-frequency wobble, which read as buzz on the band edge rather than as
|
|
// weather.
|
|
//
|
|
// Each octave carries its OWN period: halving the lattice halves how many
|
|
// cells one period spans, so the coarse octaves wrap at tile_cells/2 and
|
|
// tile_cells/4. Crossing a camera wrap shifts the fine index by exactly
|
|
// tile_cells, hence the halved indices by exactly tile_cells/2 and /4, and
|
|
// the parity halve_lattice keys its fraction off is preserved because
|
|
// tile_cells divides by 4 - so all three octaves land on the same values
|
|
// they had a moment before.
|
|
float row_turbulence(vec2 lattice, float period, float axis) {
|
|
vec2 half_freq = halve_lattice(lattice);
|
|
vec2 quarter = halve_lattice(half_freq);
|
|
return value_noise(quarter, period * 0.25, axis + 11.0) * 0.55
|
|
+ value_noise(half_freq, period * 0.5, axis + 47.0) * 0.30
|
|
+ value_noise(lattice, period, axis + 83.0) * 0.15;
|
|
}
|
|
|
|
void fragment() {
|
|
vec2 screen_px = UV * viewport_size;
|
|
vec2 centered = screen_px - viewport_size * 0.5;
|
|
|
|
// Identical split to lunar_surface.gdshader: `rel` is always small and
|
|
// camera-relative, `cell` is an exact integer built from two integers
|
|
// (camera_cell + cell_rel), and `local` is the bounded [0,1) remainder.
|
|
vec2 rel = camera_frac + centered * (camera_zoom / cell_size);
|
|
vec2 cell_rel = floor(rel);
|
|
vec2 local = rel - cell_rel;
|
|
vec2 cell = camera_cell + cell_rel;
|
|
|
|
vec2 base_lattice = vec2(cell.x, local.x);
|
|
float time_units = TIME * DRIFT_SPEED;
|
|
|
|
// Nearest-row-wins band field, the same single-scalar discipline as
|
|
// lunar_surface.gdshader's nearest-crater distance: every visual below
|
|
// is a pure function of `nearest`, so the band body and its crest can
|
|
// never describe two different bands.
|
|
float nearest = 1000.0;
|
|
for (int dy = -1; dy <= ROW_SCAN; dy++) {
|
|
float row = cell.y + float(dy);
|
|
// centre, thickness and drift rate, from one hash - on the WRAPPED
|
|
// row index, so a band keeps its identity across a camera wrap.
|
|
vec3 row_hash = hash23(vec2(mod(row, tile_cells), 5.0));
|
|
float band_centre = row_hash.x;
|
|
float thickness = mix(MIN_THICKNESS, MAX_THICKNESS, row_hash.y);
|
|
// Differential rotation: every row slides at its own rate, and some
|
|
// slide backwards. Neighbouring bands therefore shear past each
|
|
// other instead of the whole sky sliding as one sheet.
|
|
float drift_rate = mix(-1.0, 1.0, row_hash.z);
|
|
|
|
// TIME is unbounded but session-scale (the ticket calls for using it
|
|
// directly, no extra uniform), so it gets its own int/frac split and
|
|
// is combined with the cell's via combine_lattice() rather than
|
|
// added to cell.x as one continuous float.
|
|
vec2 drift = int_frac(time_units * drift_rate);
|
|
vec2 row_lattice = combine_lattice(base_lattice, drift);
|
|
|
|
// Phased per-row so neighbouring bands don't ripple in lockstep.
|
|
float turb = row_turbulence(row_lattice, tile_cells, mod(row, tile_cells) * 3.7);
|
|
|
|
// Safe: local.y is bounded [0,1), dy and row are small integers, so
|
|
// this stays a small camera-relative value with no huge magnitude.
|
|
float to_band = (local.y - float(dy)) - band_centre;
|
|
to_band += (turb - 0.5) * TURBULENCE_STRENGTH;
|
|
|
|
float normalized = abs(to_band) / thickness;
|
|
nearest = min(nearest, normalized);
|
|
}
|
|
|
|
float core = 1.0 - smoothstep(0.0, 0.6, nearest);
|
|
float crest = smoothstep(0.5, 0.78, nearest)
|
|
* (1.0 - smoothstep(0.78, MAX_SIGNIFICANT_DISTANCE, nearest));
|
|
|
|
vec3 color = base_color.rgb;
|
|
color = mix(color, crater_color.rgb, core * 0.85);
|
|
color = mix(color, highlight_color.rgb, crest * 0.6);
|
|
|
|
// Same dither as lunar_surface.gdshader, to mask 8-bit banding on the
|
|
// smooth band gradients.
|
|
float dither = (hash21(screen_px + vec2(7.0, 13.0)) - 0.5) / 128.0;
|
|
color += vec3(dither);
|
|
|
|
COLOR = vec4(color, 1.0);
|
|
}
|