spaceship/shaders/lunar_surface.gdshader

302 lines
16 KiB
Text
Raw Permalink Normal View History

shader_type canvas_item;
// Lunar/crater backdrop for the "Big Stable Asteroid" atmosphere. Reacts
// to camera position/zoom exactly like starfield.gdshader (world-space
// sampling from camera uniforms), so it parallaxes correctly as the fleet
// flies around instead of being a static picture.
//
// Structured as a Worley / cellular distance field, following the design
// of the "Floating Bubbles" shader on godotshaders.com: a neighbor scan
// reduces everything to ONE scalar - the normalized distance to the
// nearest crater - and every visual (floor darkness, rim light, rim
// shadow) is then derived from that single value.
//
// This single-field structure matters, and is the whole reason for the
// rewrite. The previous version accumulated three fields independently
// (floor via max() over all neighbors, rim light/shadow via a separate
// "strongest crater wins" test). Those could disagree: one crater's dark
// rim band could be composited over a different, nearer crater's floor,
// cutting a hard wrongly-shaped bite into it. Deriving everything from
// the nearest crater makes that class of bug structurally impossible -
// the floor and the rim always describe the same crater.
//
// Where two craters overlap, the nearest-crater field has a crease (normal
// for Worley noise) and the lighting direction flips across it, which drew
// a thin bright rim wedge through the middle of the merged basin. The
// second-nearest distance is tracked purely to detect that case and fade
// the rim out there - see the note further down.
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
//
// This runs on EVERY pixel of a fullscreen rect on a phone, so it is the
// single most expensive thing in the surface scene and the subject of
// Forgejo #68 ("the moon game is a bit slow"). The three optimizations
// that brought it down - a bounded 3x3 scan, one hash per cell instead of
// three, and one sqrt per pixel instead of one per sample - are each
// justified at the point they appear below. Time it with
#84: move 215 root files into a folder hierarchy Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
// bash tests/profile_shader.sh res://shaders/lunar_surface.gdshader
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;
// Sampled from a NASA Artemis lunar photograph: the regolith is a
// bright neutral grey with a very faint cool cast (blue a touch above
// red), craters read as mid-grey floors rather than black, and the lit
// rims are only slightly brighter than the surface.
uniform vec4 base_color : source_color = vec4(0.75, 0.75, 0.76, 1.0);
uniform vec4 crater_color : source_color = vec4(0.44, 0.44, 0.45, 1.0);
uniform vec4 highlight_color : source_color = vec4(0.89, 0.89, 0.90, 1.0);
// DIAGNOSTIC ONLY (set in PlanetSurface.tscn for a debug build; 0 = off).
// 1 = tint alternate cells, to show where cell boundaries fall.
// 2 = output the raw normalized distance field as grayscale, with NO
// rim, lighting, dither or tint. This bisects the shader: the field
// and the shading built on top of it are independent halves, so if
// cuts appear in this grayscale output the FIELD is at fault, and if
// it is smooth the field is fine and the SHADING is at fault.
uniform float debug_mode = 0.0;
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
// Width of the repeating tile, in cells. The crater field is made PERIODIC
// with this period (see the wrap in the loop), which is what lets
// LunarBackground.gd bake one square of it into a texture and then scroll
// that texture forever with repeat wrapping, instead of re-deriving the
// field per pixel per frame. Set from LunarBackground.TILE_CELLS, which is
// the single source of truth shared with lunar_tiled.gdshader.
uniform float tile_cells = 32.0;
// BAKE MODE. 0 = normal camera-relative sampling for direct display.
// Above zero, UV is mapped straight onto [0, tile_cells] instead, so
// rendering this shader to a square SubViewport produces exactly one period
// of the field - the texture the runtime shader then samples.
uniform float bake_cells = 0.0;
const vec2 LIGHT_DIR = vec2(-0.6, -0.8);
const float MIN_RADIUS = 0.16;
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
// MUST stay below 1.0. The 3x3 scan below is only correct because a crater
// cannot reach out of its own cell's immediate neighbourhood; see the long
// note at the loop. tests/test_lunar_field.gd asserts this bound directly
// against the shader source so it cannot drift unnoticed.
const float MAX_RADIUS = 0.38;
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
// Sentinel for "this cell holds no crater", in SQUARED normalized units to
// match the loop. Must stay well inside mediump range (max ~65504) so it
// can never become +Inf - see the note in the loop. Only needs to exceed
// the largest real squared normalized distance, which for a 3x3 scan is
// (2.83 cells / 0.16 min radius)^2 ~= 313.
const float MISS_DISTANCE_SQ = 10000.0;
// Loop-carried initial value. Above any real value INCLUDING one carrying
// the miss sentinel (~10313), so a pixel whose whole neighbourhood is
// empty still leaves the "winning" offset populated with something finite.
const float START_DISTANCE_SQ = 20000.0;
// One hash call yields all four values a cell needs (crater centre x/y,
// whether the cell holds a crater at all, and its radius). The previous
// version called two different hashes three times per cell with different
// constant offsets, which was three quarters of the loop body's arithmetic
// for no extra information - a single hash with four independent output
// lanes decorrelates just as well.
//
// Multipliers chosen for precision safety on mobile GPUs: they are all
// < 1, so every intermediate stays small no matter how large the cell
// index gets. An earlier version multiplied by ~123/456 first, which lands
// the intermediate in the thousands where a mobile float loses its
// fractional bits entirely. (Standard "hash without sine" formulation,
// Dave Hoskins.)
vec4 hash42(vec2 p) {
vec4 p4 = fract(vec4(p.xyxy) * vec4(0.1031, 0.1030, 0.0973, 0.1099));
p4 += dot(p4, p4.wzxy + 33.33);
return fract((p4.xxyz + p4.yzzw) * p4.zywx);
}
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;
// 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.
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
//
// In bake mode none of that applies: the target is one tile of the field
// in isolation, so UV maps directly onto [0, bake_cells] and there is no
// camera in the picture at all.
bool baking = bake_cells > 0.0;
vec2 rel = baking
? UV * bake_cells
: 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.
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
vec2 cell = baking ? cell_rel : camera_cell + cell_rel;
// Single scalar field: normalized distance to the nearest crater.
// < 1.0 means this pixel is inside that crater; 1.0 is exactly on its
// edge. Normalizing by each crater's own radius is what lets craters
// vary in size while still collapsing to one comparable number.
//
// The SECOND-nearest crater is tracked alongside the nearest, purely so
// the rim can be suppressed inside overlaps. Where two craters overlap,
// the nearest-crater test flips along the line between them; the
// lighting direction flips with it and paints a thin bright rim wedge
// through the middle of what should read as one merged depression.
// A pixel in that region is inside BOTH craters, i.e. second_nearest is
// also < 1.0 - which is precisely the condition to fade the rim out.
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
//
// Tracked SQUARED, and square-rooted once at the end. Ordering is
// unchanged by squaring (both sides are non-negative), so the same
// crater wins - but this turns one sqrt per sample into two per pixel,
// and sqrt is one of the more expensive things a fragment shader can
// ask for.
float nearest_sq = START_DISTANCE_SQ;
float second_nearest_sq = START_DISTANCE_SQ;
vec2 nearest_offset = vec2(0.0, 1.0);
// 3x3 is provably sufficient, and provably so only because of the two
// clamps in the loop body. A crater's centre is clamped into its own
// cell and its radius is clamped to at most MAX_RADIUS (0.38) cells, so
// the furthest a crater can reach past its own cell is 0.38 cells. This
// pixel's `local` lies in [0,1], so a crater two cells out is at least
// 1.0 cells away - a normalized distance of at least 1/0.38 = 2.6,
// which is past every threshold used below (the floor fades out by 0.9,
// the rim band by 1.0, and the overlap suppression saturates at 1.15).
// Such a crater therefore cannot change a single pixel of the output.
//
// This replaces an earlier 5x5 scan. That width was not chosen from the
// geometry - 3x3 was already the geometric answer - but as insurance
// against craters that were observed clipping at cell boundaries
// on-device, which meant some crater's influence was reaching further
// than its nominal radius allowed, e.g. a hash returning a value outside
// [0,1] on that GPU and extrapolating mix() into a huge radius. Clamping
// both the radius AND the centre fixes that at the source and makes the
// bound above hold no matter what the hash returns, so paying for 25
// samples per pixel to hide a driver quirk is no longer necessary.
// tests/test_lunar_field.gd checks the two scans agree exactly.
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
vec2 offset = vec2(float(dx), float(dy));
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
// Wrapping the cell index before hashing is the entire trick that
// makes the field tileable: cell N and cell N + tile_cells hash
// identically, so the pattern repeats exactly with that period.
// It works on a Worley field specifically because the scan only
// ever reads immediate neighbours - wrap those consistently and
// the seam is not merely hidden, it does not exist. GLSL mod()
// returns a non-negative result for a positive divisor, so
// negative cell indices wrap correctly too.
highp vec2 neighbor_cell = mod(cell + offset, tile_cells);
vec4 h = hash42(neighbor_cell);
// Cells without a crater are pushed far away rather than
// branched around, keeping the loop body uniform. NOTE the
// sentinel is deliberately a small number, not something like
// 1e9: mediump floats top out around 65504, so a huge literal
// becomes +Inf there, and (1.0 - has_crater) * Inf evaluates to
// 0 * Inf = NaN for every cell that DOES hold a crater - which
// compares false against everything and would silently discard
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
// every crater.
float has_crater = step(0.35, h.z);
// clamp() on both of these is what bounds a crater's reach to
// its own cell plus MAX_RADIUS, which is what lets the scan
// above be 3x3. Without them a driver returning a hash outside
// [0,1] could place a centre, or inflate a radius, far enough to
// reach past the scan - which is exactly the clipping that was
// seen on-device.
vec2 crater_center = offset + clamp(h.xy, 0.0, 1.0);
float radius = clamp(mix(MIN_RADIUS, MAX_RADIUS, h.w), MIN_RADIUS, MAX_RADIUS);
vec2 to_sample = local - crater_center;
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
float normalized_sq = dot(to_sample, to_sample) / (radius * radius)
+ (1.0 - has_crater) * MISS_DISTANCE_SQ;
if (normalized_sq < nearest_sq) {
second_nearest_sq = nearest_sq;
nearest_sq = normalized_sq;
// Offset from the winning crater's centre to this pixel,
// kept so the rim's light/shadow split can be derived from
// the SAME crater that won the distance test - the shading
// can then never describe one crater while the floor
// describes another.
nearest_offset = to_sample;
} else if (normalized_sq < second_nearest_sq) {
second_nearest_sq = normalized_sq;
}
}
}
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
float nearest = sqrt(nearest_sq);
float second_nearest = sqrt(second_nearest_sq);
// normalize() would divide by zero for a pixel sitting exactly on a
// crater centre; the guard keeps that pixel's rim term merely arbitrary
// rather than NaN. It is deep inside the floor, where the rim band is
// zero anyway.
float offset_length = length(nearest_offset);
vec2 nearest_dir = offset_length > 0.0001 ? nearest_offset / offset_length : vec2(0.0, 1.0);
vec2 light_dir = normalize(LIGHT_DIR);
float facing = dot(nearest_dir, light_dir);
// Everything below is a pure function of `nearest` (plus the winning
// crater's direction), so all three visuals stay mutually consistent.
float floor_amount = 1.0 - smoothstep(0.45, 0.9, nearest);
// Thin raised-rim band right at the crater's edge, split into a lit
// half (facing LIGHT_DIR) and a shadowed half (facing away), giving a
// sense of a raised lip catching light.
float rim_band = smoothstep(0.55, 0.85, nearest) * (1.0 - smoothstep(0.85, 1.0, nearest));
// Fade the rim out where this pixel also lies inside a second crater:
// that is the overlap region, where a rim would be an internal crease
// rather than a real outer lip. Craters there merge into one basin.
rim_band *= smoothstep(0.85, 1.15, second_nearest);
float rim_light = rim_band * clamp(facing, 0.0, 1.0);
float rim_shadow = rim_band * clamp(-facing, 0.0, 1.0);
vec3 color = base_color.rgb;
color = mix(color, crater_color.rgb, floor_amount * 0.85);
color = mix(color, highlight_color.rgb, rim_light * 0.6);
color = mix(color, crater_color.rgb * 0.7, rim_shadow * 0.5);
// Tiny per-pixel dither to mask 8-bit colour-quantization banding: the
// crater shading is a smooth gradient over a fairly large area, which
// can visibly "step" into faint bands on real hardware. A small random
// jitter well under one visible step breaks the stepping up without
// being perceptible as noise.
Make the lunar backdrop cheap: 3x3 field, then bake and tile it (#68) The crater shader ran on every pixel of a fullscreen rect every frame and was by far the most expensive thing in the surface scene. Nothing else came close: RockField keeps 14 rocks alive, Rock/Ship/Enemy already avoid per-frame queue_redraw() for transform-only changes, and LunarBackground.gd is a handful of scalar ops. Three reductions to the field itself: - 5x5 -> 3x3 neighbour scan. That width was never the geometric answer; it was insurance against craters seen clipping at cell boundaries on-device, i.e. a crater reaching past its nominal radius because a hash returned a value outside [0,1] and extrapolated mix(). Clamping the crater CENTRE into its own cell, alongside the radius clamp already there, bounds the reach whatever the hash returns - so a crater two cells out is >= 1.0 cells away, a normalized distance of >= 2.63, past every threshold the shading reacts to (the last saturates at 1.15). The outer ring provably cannot change a pixel. This removes the clipping bug's cause instead of hiding it behind 16 extra samples. - One hash42 per cell instead of hash21 twice plus hash22, decorrelating on output lanes rather than input offsets. - Squared distances in the loop, square-rooted twice at the end rather than once per sample. Measured with the new tests/profile_shader.sh, above softpipe's own ~657ms fullscreen floor: 9482ms -> 405ms of shader work, a 23x reduction. Then the larger change: the field is a fixed function of world position, so every frame recomputed an identical answer. It is now made periodic (the cell index is wrapped before hashing, which makes a Worley field tile exactly rather than merely blend), baked once at load into one tile, and scrolled with repeat wrapping by lunar_tiled.gdshader - one texture fetch per pixel. Two honest costs: the field repeats every 7040 world units, and a fixed-resolution tile softens when zoomed past 1:1. Lunar regolith is forgiving of both, having no landmarks and being smooth gradients already. The bake waits a frame and asks the material whether it actually samples a field_texture, so a gas giant - which swaps in its own background shader (#51) - does not pay for a 2048-square tile it will never read. Headless runs skip it entirely, having nothing to rasterize. tests/test_lunar_field.gd guards the reasoning, since a fragment shader cannot be asserted on directly: the premises are still in the source, the worst-case ring-2 geometry stays clear of every threshold independent of the hash, a 3x3 and a 5x5 scan render identically over 4000 sampled points, the field repeats exactly in both axes, and the miss sentinel still cannot overflow mediump into +Inf and then NaN. test_surface_profiles.gd asserted "a rocky landing leaves the background alone" by naming lunar_surface.gdshader; it now reads the shader from the scene, which is the property it actually cares about. PlanetSurface.tscn's edit also carries the planet-name relocation described in the HUD commit - same scene file, so the two cannot be split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:20:14 +02:00
// Skipped when baking: dither is a per-SCREEN-pixel jitter, and baking it
// would freeze the noise into the texture, where magnification smears it
// into visible blotches and tiling repeats it. lunar_tiled.gdshader
// applies the identical term live instead, which is where it belongs.
if (!baking) {
float dither = (hash21(screen_px + vec2(7.0, 13.0)) - 0.5) / 128.0;
color += vec3(dither);
}
// Diagnostic checkerboard: alternate cells tinted, plus a bright line
// right at each cell edge. Makes the grid unmistakable in a screenshot.
if (debug_mode > 0.5 && debug_mode < 1.5) {
float checker = mod(cell.x + cell.y, 2.0);
color = mix(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));
color = mix(color, vec3(1.0, 0.25, 0.25), on_edge * 0.9);
}
// Bisection output: the field alone, with none of the shading above
// applied. Godot forbids `return` inside fragment(), so this overrides
// the computed colour rather than exiting early.
if (debug_mode > 1.5) {
color = vec3(clamp(nearest / 2.0, 0.0, 1.0));
}
COLOR = vec4(color, 1.0);
}