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); }