Aerial perspective is a curve now. A low sun shines through far more air than a high one and shines ALONG the ground rather than down onto it, so density, height falloff and forward scatter all ride the sun's elevation; overcast thickens the air and flattens the scatter, because a grey sky has no disc to scatter from. `lowsun` falls away BELOW the horizon as well as above it, or the middle of the night gets a dawn's haze with no dawn to justify it. The term that was missing entirely is distance DESATURATION. Blending a saturated green ridge toward a saturated blue noon sky leaves a saturated ridge - which is why the same valley read as a photograph at dusk, where the fog colour happened to be a warm grey, and as a toy at one o'clock. A surface is now pulled toward its own luminance faster than the fog itself arrives. Measured far/near saturation at the camp: 07:00 1.11 -> 0.89, 09:00 1.04 -> 0.93, 13:00 0.98 -> 0.89. The grade is the hour's too - nine literals bound at the draw, written by daylight_set now. Noon is the case worth naming: direct sun is warm-white and the only thing filling a midday shadow is a blue sky, so noon gets a cool balance over a blue-lifted shadow with hard contrast, and dawn and dusk the reverse. Ground R-B, lit vs shadowed: 07:00 +42.8/+14.2 -> +48.9/+15.1, 13:00 +32.2/+14.8 -> +25.2/+2.9. Gain is left alone deliberately: the grade is `c * gain + lift * (1 - c)`, so warming it warms the whole frame, and warming it at noon made one o'clock yellower than seven in the morning - the opposite of the point. The visible sky is relit. Turning a photograph on its axis does not change what colour it was taken at, so every sunset had a mid-morning blue overhead. An analytic sky supplies the chroma and the photograph keeps the luminance: the cloud stays where it is and goes orange at dusk, the zenith goes deep blue at noon, and no second sky is shipped. It fades out under the horizon and eases off under cloud. The ground bounce follows the ground, crossing meadow to rock at the map's treeline instead of being one green constant everywhere including above the scree. R3D_NOAIR=1 restores all of it, so a before-and-after comes from one binary at one hour; it joins R3D_NOCLOUD / R3D_NOSHADOW / R3D_NOGI. Verified on macOS OpenGL, macOS Vulkan (MoltenVK) and Windows Vulkan (RTX 3070 Ti). Backends agree: mean difference 0.15-0.88/255 within a machine. Across machines the ORIGINAL renderer already differed by 5.02/255 at 19:12 and this build differs by 2.80, so cross-platform variance is pre-existing and did not grow. 400 frames: GL 7.4 s before and after, VK 7.0 s before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
116 lines
6.1 KiB
GLSL
116 lines
6.1 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);
|
|
}
|
|
|
|
// ---- relighting the photograph ---------------------------------------------------------------
|
|
// The sky is ONE HDRI - a South African morning - turned on its axis so its sun disc sits where
|
|
// the hour wants it. Turning a photograph does not change what colour it was taken at, so every
|
|
// hour of the day was lit by the same late morning: a sunset with a mid-morning blue overhead.
|
|
//
|
|
// What is worth keeping from the photograph is its STRUCTURE - the cloud, the gradient, the
|
|
// sun's own disc - and what is worth replacing is its COLOUR. So the analytic sky below supplies
|
|
// the chroma and the photograph keeps the luminance: clouds stay exactly where they are and go
|
|
// orange at dusk the way real ones do, and the zenith goes deep blue at noon without a second
|
|
// photograph being shipped.
|
|
uniform float u_sky_relight; // 0 leaves the photograph alone; the day fades this in
|
|
vec3 analyticSky(vec3 dir) {
|
|
float ct = clamp(dir.y, 0.0, 1.0);
|
|
float cs = clamp(dot(dir, u_sun_dir), -1.0, 1.0);
|
|
float lowsun = 1.0 - smoothstep(0.0, 0.45, u_sun_dir.y);
|
|
// Rayleigh: deep overhead, pale at the horizon where the line of sight is longest
|
|
vec3 base = mix(vec3(0.60, 0.70, 0.85), vec3(0.16, 0.33, 0.74), pow(ct, 0.55));
|
|
// the sun's warmth spreads into the sky around it, and spreads FURTHER the lower it is
|
|
float glow = pow(max(cs, 0.0), mix(9.0, 2.0, lowsun));
|
|
base = mix(base, vec3(1.0, 0.55, 0.22), glow * mix(0.22, 0.88, lowsun));
|
|
// and at dusk the whole horizon band warms, not only the part near the sun
|
|
float band = (1.0 - smoothstep(0.0, 0.32, ct)) * lowsun;
|
|
base = mix(base, vec3(0.96, 0.52, 0.28), band * 0.55);
|
|
return base;
|
|
}
|
|
|
|
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;
|
|
if (u_sky_relight > 0.001) {
|
|
vec3 sk = analyticSky(dir);
|
|
float sl = max(dot(sk, vec3(0.2126, 0.7152, 0.0722)), 1e-4);
|
|
float cl = dot(col, vec3(0.2126, 0.7152, 0.0722));
|
|
col = mix(col, sk * (cl / sl), u_sky_relight);
|
|
}
|
|
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);
|
|
}
|