shader_type canvas_item; // The lunar backdrop as it is actually drawn every frame: one texture fetch // plus a dither, and nothing else. // // Forgejo #68. lunar_surface.gdshader derives the crater field procedurally, // which costs a 9-sample Worley scan on every pixel of a fullscreen rect, // every frame, forever - and it recomputes the exact same answer each time, // because the field is a fixed function of world position. So it is // evaluated ONCE, into a square texture covering one period of the field // (LunarBackground.gd does the bake at scene load), and this shader scrolls // that texture with repeat wrapping. The canvas stays infinite; the per- // frame cost stops scaling with how complicated the field is. // // Two things pay for that, both stated plainly because they are real: // // 1. The field REPEATS, every tile_cells * cell_size world units. It is // seamless (lunar_surface.gdshader wraps the cell index before hashing, // so the periodicity is exact rather than blended), but a player who // flies far enough passes the same crater arrangement again. Lunar // regolith is about the most forgiving possible content for that: no // landmarks, no horizon, no orientation cues, nothing to anchor a // memory of "I have seen this crater before" to. // 2. The texture has a fixed resolution, so zooming in past its native // density softens the craters rather than resolving finer ones. The // field is smooth gradients by construction - the sharpest thing in it // is the rim band, and that still spans several texels - so bilinear // magnification reproduces it closely instead of showing blocks. // // Mipmaps handle the other direction (zooming out), which is where a // high-frequency crater field would otherwise alias into shimmer. uniform sampler2D field_texture : filter_linear_mipmap, repeat_enable; 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; uniform float tile_cells = 32.0; 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 the procedural shader, and for // the same reason: an absolute world position reaches the tens of // thousands, where a mobile float has lost the fractional bits this // needs. camera_cell arrives ALREADY WRAPPED into [0, tile_cells) - // LunarBackground.gd does that wrap in GDScript's double precision, so // the large number never enters the shader at all. vec2 rel = camera_frac + centered * (camera_zoom / cell_size); vec2 uv = (camera_cell + rel) / tile_cells; // repeat_enable does the wrapping, so uv running outside [0,1] is not // merely tolerated, it is the mechanism. vec3 color = texture(field_texture, uv).rgb; // 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. Doubly needed here - the baked texture is // itself 8-bit, so it quantizes the gradient a second time. // // Deliberately keyed to the SCREEN pixel, not the world position: it has // to stay fixed to the display or it turns into crawling noise as the // camera moves, and it must not be baked into the tile or it would // repeat along with everything else. float dither = (hash21(screen_px + vec2(7.0, 13.0)) - 0.5) / 128.0; color += vec3(dither); COLOR = vec4(color, 1.0); }