117 lines
5.3 KiB
Text
117 lines
5.3 KiB
Text
|
|
shader_type canvas_item;
|
||
|
|
|
||
|
|
// Backdrop for a landed enemy research lab.
|
||
|
|
//
|
||
|
|
// The other surfaces are NATURAL: lunar_surface.gdshader erodes craters,
|
||
|
|
// gas_surface.gdshader bands clouds. A research lab is the one place in the
|
||
|
|
// game that is BUILT, and it was previously borrowing the crater field with
|
||
|
|
// a dark red palette - which read as "rocky planet, tinted", not as walking
|
||
|
|
// onto someone's facility. So this draws a plated floor instead: panel
|
||
|
|
// seams, service conduits running through them, and lit junctions where two
|
||
|
|
// conduits cross.
|
||
|
|
//
|
||
|
|
// Deliberately loop-free. lunar_surface.gdshader needs a neighbour scan
|
||
|
|
// because a crater overlaps its cell's borders; nothing here does, so every
|
||
|
|
// feature is a function of this pixel's own cell and the shader stays a
|
||
|
|
// handful of hashes per pixel (#68's lesson, applied up front rather than
|
||
|
|
// after the fact).
|
||
|
|
//
|
||
|
|
// The conduits still have to run CONTINUOUSLY across cell borders, which is
|
||
|
|
// what a neighbour scan would normally buy. The trick is to make a
|
||
|
|
// horizontal run's existence and height depend only on cell.y, and a
|
||
|
|
// vertical run's on cell.x: every cell in a row then independently agrees on
|
||
|
|
// the same answer, so the line joins up without any cell ever looking at
|
||
|
|
// its neighbours.
|
||
|
|
|
||
|
|
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;
|
||
|
|
// Shared with the lunar background so the same camera feed drives both, and
|
||
|
|
// so the field repeats on the same period. See LunarBackground.gd.
|
||
|
|
uniform float tile_cells = 32.0;
|
||
|
|
uniform vec4 base_color : source_color = vec4(0.16, 0.12, 0.12, 1.0);
|
||
|
|
uniform vec4 crater_color : source_color = vec4(0.05, 0.03, 0.03, 1.0);
|
||
|
|
uniform vec4 highlight_color : source_color = vec4(0.75, 0.20, 0.15, 1.0);
|
||
|
|
|
||
|
|
// Plates per cell, across and down.
|
||
|
|
const float SUBPANELS = 3.0;
|
||
|
|
const float SEAM_WIDTH = 0.05;
|
||
|
|
const float CONDUIT_WIDTH = 0.055;
|
||
|
|
// How many rows/columns carry a conduit. Low enough that the floor stays
|
||
|
|
// mostly plating, high enough that a screen usually shows a junction.
|
||
|
|
const float CONDUIT_CHANCE = 0.38;
|
||
|
|
|
||
|
|
float hash11(float p) {
|
||
|
|
vec3 p3 = fract(vec3(p) * vec3(0.1031, 0.1030, 0.0973));
|
||
|
|
p3 += dot(p3, p3.yzx + 33.33);
|
||
|
|
return fract((p3.x + p3.y) * p3.z);
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
void fragment() {
|
||
|
|
vec2 screen_px = UV * viewport_size;
|
||
|
|
vec2 centered = screen_px - viewport_size * 0.5;
|
||
|
|
|
||
|
|
// Same camera-relative construction as every other background here: an
|
||
|
|
// absolute world position reaches the tens of thousands, where a mobile
|
||
|
|
// float has already lost the fractional bits this needs. camera_cell
|
||
|
|
// arrives pre-wrapped from GDScript's double precision.
|
||
|
|
vec2 rel = camera_frac + centered * (camera_zoom / cell_size);
|
||
|
|
vec2 cell_rel = floor(rel);
|
||
|
|
vec2 local = rel - cell_rel;
|
||
|
|
vec2 cell = mod(camera_cell + cell_rel, tile_cells);
|
||
|
|
|
||
|
|
// --- plating ---------------------------------------------------------
|
||
|
|
vec2 sub = local * SUBPANELS;
|
||
|
|
vec2 sub_cell = floor(sub);
|
||
|
|
vec2 sub_local = sub - sub_cell;
|
||
|
|
vec2 edge = min(sub_local, 1.0 - sub_local);
|
||
|
|
float seam = 1.0 - smoothstep(0.0, SEAM_WIDTH, min(edge.x, edge.y));
|
||
|
|
// A little per-plate variation, so a large floor does not read as one
|
||
|
|
// flat fill: plates are stamped, not poured.
|
||
|
|
float plate = hash21(cell * SUBPANELS + sub_cell);
|
||
|
|
|
||
|
|
vec3 color = mix(base_color.rgb, crater_color.rgb, plate * 0.40);
|
||
|
|
color = mix(color, crater_color.rgb, seam * 0.85);
|
||
|
|
|
||
|
|
// --- conduits --------------------------------------------------------
|
||
|
|
// Existence and offset keyed to a SINGLE axis of the cell index, which
|
||
|
|
// is what makes a run continuous across every cell it passes through.
|
||
|
|
float has_h = step(1.0 - CONDUIT_CHANCE, hash11(cell.y + 0.5));
|
||
|
|
float has_v = step(1.0 - CONDUIT_CHANCE, hash11(cell.x + 13.7));
|
||
|
|
float run_y = 0.25 + 0.5 * hash11(cell.y + 7.3);
|
||
|
|
float run_x = 0.25 + 0.5 * hash11(cell.x + 3.1);
|
||
|
|
|
||
|
|
float h_band = has_h * (1.0 - smoothstep(0.0, CONDUIT_WIDTH, abs(local.y - run_y)));
|
||
|
|
float v_band = has_v * (1.0 - smoothstep(0.0, CONDUIT_WIDTH, abs(local.x - run_x)));
|
||
|
|
|
||
|
|
// Charge travelling along the run. Phase is built from cell + local, a
|
||
|
|
// continuous coordinate, so the pulse crosses cell borders unbroken
|
||
|
|
// rather than restarting inside every cell.
|
||
|
|
float pulse_h = 0.55 + 0.45 * sin((cell.x + local.x) * 2.4 - TIME * 1.6);
|
||
|
|
float pulse_v = 0.55 + 0.45 * sin((cell.y + local.y) * 2.4 - TIME * 1.6);
|
||
|
|
float conduit = max(h_band * pulse_h, v_band * pulse_v);
|
||
|
|
// A dark channel under the glow, so a conduit reads as recessed into the
|
||
|
|
// floor rather than painted on top of it.
|
||
|
|
color = mix(color, crater_color.rgb, max(h_band, v_band) * 0.55);
|
||
|
|
color = mix(color, highlight_color.rgb, conduit * 0.75);
|
||
|
|
|
||
|
|
// Junction box wherever a horizontal and a vertical run cross.
|
||
|
|
float junction = has_h * has_v * (1.0 - smoothstep(0.0, CONDUIT_WIDTH * 2.4,
|
||
|
|
length(vec2(local.x - run_x, local.y - run_y))));
|
||
|
|
color = mix(color, highlight_color.rgb, junction * 0.9);
|
||
|
|
|
||
|
|
// Same dither as the other backgrounds: the gradients here are shallow
|
||
|
|
// and dark, which is exactly where 8-bit quantization bands visibly.
|
||
|
|
float dither = (hash21(screen_px + vec2(7.0, 13.0)) - 0.5) / 128.0;
|
||
|
|
color += vec3(dither);
|
||
|
|
|
||
|
|
COLOR = vec4(color, 1.0);
|
||
|
|
}
|