ludic/packages/ludic.render3d/shaders/sky.frag
Orkuncakilkaya 6043a67631 feat(render3d): give the moon a phase
The moon was fixed opposite the sun and worth a constant trickle of light. It
now carries a phase, 0 new .. 0.5 full .. 1 new again, and that one number
decides three things at once: the disc's terminator, the fraction of its light
that reaches the ground, and where in the sky it rides.

The moon is where the sun was `lag` of a day ago, so a full moon rises as the
sun sets and a new moon travels with the sun and is never seen. A new moon is
now a properly dark night, which is what makes a carried light worth having.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 16:15:19 +03:00

84 lines
4.2 KiB
GLSL

in vec2 v_uv;
out vec4 o_color;
uniform sampler2D u_sky;
uniform mat4 u_inv_vp;
uniform float u_sky_gain;
uniform float u_sky_sat;
// a star: one hash per cell of the direction, a few of them bright, a slow twinkle
float starField(vec3 dir, float t) {
vec3 p = dir * 230.0;
vec3 c = floor(p);
vec3 f = p - c - 0.5;
float h = fract(sin(dot(c, vec3(12.9898, 78.233, 37.719))) * 43758.5453);
float h2 = fract(h * 91.7);
float bright = smoothstep(0.972, 1.0, h);
float disc = smoothstep(0.42, 0.0, length(f));
float twinkle = 0.7 + 0.3 * sin(t * (1.5 + 3.0 * h2) + h2 * 40.0);
return bright * disc * twinkle * (0.5 + h2);
}
// The moon: a disc a little over half a degree across, shaded by a terminator taken from
// its phase, with a few soft maria on it and a halo that opens up when the air is damp.
// It is the light the night already had (daylight.ludic repoints the sun after dusk) —
// this is that light finally having something you can look at.
uniform vec3 u_moon_dir;
uniform float u_moon_haze; // the weather's overcast: dims the disc, widens the halo
uniform float u_moon_phase; // 0 new .. 0.5 full .. 1 new
uniform float u_moon_illum;
vec3 moonColour(vec3 dir, float night) {
if (night <= 0.001 || u_moon_dir.y < -0.08) return vec3(0.0);
float ang = acos(clamp(dot(dir, u_moon_dir), -1.0, 1.0));
const float R = 0.0096; // about 0.55 degrees
// the halo first: wide, weak, and thicker through damp air
float halo = exp(-ang / (R * 6.0)) * (0.02 + 0.35 * u_moon_haze) * u_moon_illum;
vec3 col = vec3(0.55, 0.60, 0.75) * halo;
if (ang < R) {
// where on the disc: a unit offset from its centre, built in the sky's own frame
vec3 up = abs(u_moon_dir.y) > 0.95 ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 1.0, 0.0);
vec3 rx = normalize(cross(up, u_moon_dir));
vec3 ry = cross(u_moon_dir, rx);
vec2 p = vec2(dot(dir, rx), dot(dir, ry)) / R;
float r2 = clamp(dot(p, p), 0.0, 1.0);
// the sphere's normal, and the direction its light comes from, turned by the phase
vec3 n = vec3(p, sqrt(max(1.0 - r2, 0.0)));
float a2 = u_moon_phase * 6.28318530718;
vec3 lit = normalize(vec3(sin(a2), 0.0, -cos(a2)));
float lam = smoothstep(-0.06, 0.10, dot(n, lit));
// maria: two or three soft dark blotches, so it is not a flat white circle
float m = 0.0;
m += smoothstep(0.34, 0.0, length(p - vec2(-0.28, 0.22)));
m += 0.7 * smoothstep(0.26, 0.0, length(p - vec2(0.24, -0.10)));
m += 0.5 * smoothstep(0.20, 0.0, length(p - vec2(0.02, 0.44)));
float edge = smoothstep(1.0, 0.86, sqrt(r2));
vec3 disc = mix(vec3(1.0, 0.98, 0.93), vec3(0.62, 0.63, 0.68), clamp(m, 0.0, 1.0));
// The lit face has to be bright enough to read as the moon and no brighter: the night
// runs at an exposure of about five, so anything much over one saturates the whole
// disc to a flat white circle and the terminator disappears. The dark limb keeps a
// trace of earthshine, which is what makes a crescent look like a sphere.
col += disc * (lam + 0.010 * (1.0 - lam)) * edge * 0.55;
}
return col * night * (1.0 - 0.9 * u_moon_haze);
}
void main() {
vec4 a = u_inv_vp * vec4(v_uv * 2.0 - 1.0, 1.0, 1.0);
vec3 dir = normalize(a.xyz / a.w - u_cam_pos);
// level 0: the equirect seam (atan wraps) would otherwise pick the smallest mip along one column
vec3 col = min(textureLod(u_sky, skyUV(dir), 0.0).rgb, vec3(4096.0)) * u_sky_gain;
float l = dot(col, vec3(0.2126, 0.7152, 0.0722));
col = max(mix(vec3(l), col, u_sky_sat), vec3(0.0));
// the photograph's sky dims with the day (daylight.ludic); the night adds its own
col *= u_ibl_scale;
float night = 1.0 - smoothstep(0.0, 0.45, u_daylight);
if (night > 0.0) {
vec3 nightCol = mix(vec3(0.012, 0.016, 0.034), vec3(0.003, 0.004, 0.010), clamp(dir.y, 0.0, 1.0));
float stars = starField(dir, u_time) * smoothstep(-0.02, 0.15, dir.y);
nightCol += stars * vec3(0.55, 0.6, 0.7) * night;
col += nightCol * night;
col += moonColour(dir, night);
}
// below the horizon the HDRI ground is replaced by the fog colour
float below = smoothstep(0.0, -0.08, dir.y);
vec3 fogCol = skyPrefiltered(vec3(dir.x, 0.02, dir.z), 0.6);
col = mix(col, fogCol, below);
o_color = vec4(sane(col), 1.0);
}