79 lines
3.8 KiB
Text
79 lines
3.8 KiB
Text
|
|
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);
|
||
|
|
}
|