clouds: follow spherical Earth curvature

This commit is contained in:
Richard Zink committed 2026-10-12 14:30:26 +02:00
1 parent c8fa1b3260
commit d59df7f4d1
1 file changed
+81 -12
+81 -12
View File
@@ -93,6 +93,8 @@ var FRAG_SRC = [
'uniform float uAtmoGrayTau;',
'uniform float uAtmoGain;',
'uniform float uEarthRadius;',
'uniform float uCurvatureOn;',
'uniform vec3 uCamPos;',
'uniform vec3 uAtmoBlueColor;',
'uniform vec3 uAtmoGrayColor;',
'uniform vec3 uSunColor;', // direct beam after atmospheric extinction
@@ -172,6 +174,11 @@ var FRAG_SRC = [
// shimmering speckle once a cloud only covers a few pixels, so it is
// dropped there and the threshold is widened - the large shapes stay,
// the fine structure that cannot be resolved disappears.
'float earthDrop(vec2 xz) {',
'if (uCurvatureOn < 0.5) return 0.0;',
'vec2 d = xz - uCamPos.xz;',
'return dot(d, d) / (2.0 * max(1.0, uEarthRadius));',
'}',
'float cloudDensity(vec3 p, float baseThresh, float soft) {',
'float n = fbm3(p * 0.35 + vec3(0.0, 0.0, uTime * 0.06));',
// The detail octave weighs at most 0.20 and fades to zero with
@@ -182,8 +189,9 @@ var FRAG_SRC = [
'n = n + d * mix(0.20, 0.0, soft);',
'}',
// Cloud layer between uCloudBot and uCloudTop, densest in the
// middle (bell curve).
'float h = clamp((p.y - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);',
// middle (bell curve). The layer follows the Earth curvature.
'float py = p.y + earthDrop(p.xz);',
'float h = clamp((py - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);',
'float profile = 4.0 * h * (1.0 - h); // bell curve',
'profile = max(profile, 0.0);',
'return smoothstep(baseThresh, baseThresh + 0.30 + soft * 0.25, n) * profile;',
@@ -217,6 +225,21 @@ var FRAG_SRC = [
'vec3 t = texture2D(uElev, euv).rgb;',
'return (t.r * 256.0 + t.g + t.b / 256.0) * 255.0 - 32768.0;',
'}',
'vec2 curvedLayerRoots(float layerY, vec3 ro, vec3 rd) {',
'float R = max(1.0, uEarthRadius);',
'float a = (uCurvatureOn > 0.5) ? dot(rd.xz, rd.xz) / (2.0 * R) : 0.0;',
'float b = rd.y;',
'float c = ro.y - layerY;',
'if (abs(a) < 1e-6) {',
'if (abs(b) < 1e-6) return vec2(-1.0, -1.0);',
'float t = -c / b;',
'return vec2(t, t);',
'}',
'float disc = b * b - 4.0 * a * c;',
'if (disc < 0.0) return vec2(-1.0, -1.0);',
'float sq = sqrt(disc);',
'return vec2((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a));',
'}',
// Two modes driven by uMode:
// 0 = SKY: full-screen pass that paints the sky gradient and
// stars into the framebuffer (opaque, alpha = 1).
@@ -419,7 +442,8 @@ var FRAG_SRC = [
// Cloud fully behind the terrain -> discard (framebuffer keeps
// the terrain color from the mesh pass).
'if (tTerrain > 0.0) {',
'float tCloudTop = (ro.y - uCloudTop) / -rd.y;',
'vec2 topRoots = curvedLayerRoots(uCloudTop, ro, rd);',
'float tCloudTop = max(topRoots.x, topRoots.y);',
'if (tCloudTop > tTerrain) discard;',
'}',
@@ -435,10 +459,17 @@ var FRAG_SRC = [
// own bottom and weighting each step by its path length is independent
// of the view angle, and grazing rays get the long path they really
// have.
'float tA = (abs(rd.y) < 0.0005) ? 0.0 : (uCloudBot - ro.y) / rd.y;',
'float tB = (abs(rd.y) < 0.0005) ? 14.0 : (uCloudTop - ro.y) / rd.y;',
'float tEnter = max(0.0, min(tA, tB));',
'float tExit = max(tA, tB);',
'vec2 bot = curvedLayerRoots(uCloudBot, ro, rd);',
'vec2 top = curvedLayerRoots(uCloudTop, ro, rd);',
'float tEnter = 1e9;',
'if (bot.x > 0.0) tEnter = min(tEnter, bot.x);',
'if (bot.y > 0.0) tEnter = min(tEnter, bot.y);',
'if (top.x > 0.0) tEnter = min(tEnter, top.x);',
'if (top.y > 0.0) tEnter = min(tEnter, top.y);',
'float tExit = max(max(bot.x, bot.y), max(top.x, top.y));',
'float roPy = ro.y + earthDrop(ro.xz);',
'if (roPy >= uCloudBot && roPy <= uCloudTop) tEnter = 0.0;',
'if (tEnter > 1e8) { tEnter = 0.0; tExit = 14.0; }',
'if (tTerrain > 0.0) tExit = min(tExit, tTerrain);',
// A grazing ray would sample a path of well over a thousand km with
// only 24 steps - coarse blobs along the horizon. Past this length
@@ -451,12 +482,13 @@ var FRAG_SRC = [
'for (int i = 0; i < 24; i++) {',
'vec3 p = ro + rd * t;',
'p.y += (ign(gl_FragCoord.xy + vec2(float(i) * 7.13, float(i) * 3.71)) - 0.5) * stepSize;',
'if (p.y > uCloudTop || p.y < uCloudBot - 0.1) break;',
'float py = p.y + earthDrop(p.xz);',
'if (py > uCloudTop || py < uCloudBot - 0.1) break;',
'if (tTerrain > 0.0 && t >= tTerrain) break;',
'float cSoft = smoothstep(7.0, 20.0, t);',
'float d = cloudDensity(p, baseThresh, cSoft);',
'if (d > 0.005) {',
'float lit = clamp((p.y - uCloudBot) / ((uCloudTop - uCloudBot) * 1.167), 0.0, 1.0);',
'float lit = clamp((py - uCloudBot) / ((uCloudTop - uCloudBot) * 1.167), 0.0, 1.0);',
'vec3 cloudCol;',
'if (uPhysical > 0.5) {',
// The night palette colours are already darkened for
@@ -626,6 +658,26 @@ var CLOUD_SAMPLER_GLSL = [
// as hard spots on the ridges.
'return mix(uTerrainShadowFloor, 1.0, smoothstep(0.0, 0.22, clearance));',
'}',
'float earthDrop(vec2 xz) {',
'if (uCurvatureOn < 0.5) return 0.0;',
'vec2 d = xz - uCamPos.xz;',
'return dot(d, d) / (2.0 * max(1.0, uEarthRadius));',
'}',
'vec2 curvedLayerRoots(float layerY, vec3 ro, vec3 rd) {',
'float R = max(1.0, uEarthRadius);',
'float a = (uCurvatureOn > 0.5) ? dot(rd.xz, rd.xz) / (2.0 * R) : 0.0;',
'float b = rd.y;',
'float c = ro.y - layerY;',
'if (abs(a) < 1e-6) {',
'if (abs(b) < 1e-6) return vec2(-1.0, -1.0);',
'float t = -c / b;',
'return vec2(t, t);',
'}',
'float disc = b * b - 4.0 * a * c;',
'if (disc < 0.0) return vec2(-1.0, -1.0);',
'float sq = sqrt(disc);',
'return vec2((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a));',
'}',
'float cloudDensity(vec3 p, float baseThresh, float soft) {',
'float n = fbm3(p * 0.35 + vec3(0.0, 0.0, uTime * 0.06));',
// The detail octave weighs at most 0.20 and fades to zero with
@@ -635,7 +687,8 @@ var CLOUD_SAMPLER_GLSL = [
'float d = fbm3(p * 1.4 + vec3(uTime * 0.04, 7.3, uTime * 0.03));',
'n = n + d * mix(0.20, 0.0, soft);',
'}',
'float h = clamp((p.y - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);',
'float py = p.y + earthDrop(p.xz);',
'float h = clamp((py - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);',
'float profile = 4.0 * h * (1.0 - h);',
'profile = max(profile, 0.0);',
'return smoothstep(baseThresh, baseThresh + 0.30 + soft * 0.25, n) * profile;',
@@ -673,6 +726,8 @@ var TERRAIN_FRAG_SRC = [
'uniform vec3 uAirColor;', // Rayleigh airlight (sun-tinted, blue-biased)
'uniform float uAirDensity;', // slant optical depth per world unit
'uniform vec3 uCamPos;', // world-space camera, for the view direction
'uniform float uEarthRadius;',
'uniform float uCurvatureOn;',
'uniform float uEdgeFadeTiles;',
'uniform vec3 uWireColor;', // non-zero -> debug wireframe overlay (flat colour)
'varying vec3 vNormal;',
@@ -720,8 +775,17 @@ var TERRAIN_FRAG_SRC = [
// samples under the layer, where the layer profile is zero,
// so the accumulated density was near nothing and no shade
// appeared at all.
'float tBot = max(0.0, (uCloudBot - vY) / uSunDir.y);',
'float tTop = (uCloudTop - vY) / uSunDir.y;',
'vec2 bot = curvedLayerRoots(uCloudBot, vWorld, uSunDir);',
'vec2 top = curvedLayerRoots(uCloudTop, vWorld, uSunDir);',
'float tBot = 1e9;',
'if (bot.x > 0.0) tBot = min(tBot, bot.x);',
'if (bot.y > 0.0) tBot = min(tBot, bot.y);',
'if (top.x > 0.0) tBot = min(tBot, top.x);',
'if (top.y > 0.0) tBot = min(tBot, top.y);',
'float tTop = max(max(bot.x, bot.y), max(top.x, top.y));',
'float groundPy = vY + earthDrop(vWorld.xz);',
'if (groundPy >= uCloudBot && groundPy <= uCloudTop) tBot = 0.0;',
'if (tBot > 1e8) { tBot = 0.0; tTop = 14.0; }',
'if (tTop > 0.0 && tTop > tBot && tTop < 90.0) {',
'float dens = 0.0;',
'for (int ci = 0; ci < 5; ci++) {',
@@ -840,6 +904,8 @@ function init(canvas) {
var uAtmoGrayTau = gl.getUniformLocation(prog, 'uAtmoGrayTau');
var uAtmoGain = gl.getUniformLocation(prog, 'uAtmoGain');
var uEarthRadius = gl.getUniformLocation(prog, 'uEarthRadius');
var uCurvatureOn = gl.getUniformLocation(prog, 'uCurvatureOn');
var uCamPos = gl.getUniformLocation(prog, 'uCamPos');
var uAtmoBlueColor = gl.getUniformLocation(prog, 'uAtmoBlueColor');
var uAtmoGrayColor = gl.getUniformLocation(prog, 'uAtmoGrayColor');
var uSkyTex = gl.getUniformLocation(prog, 'uSkyTex');
@@ -957,6 +1023,7 @@ function init(canvas) {
uAtmoScaleHeightUnits: uAtmoScaleHeightUnits, uAtmoDensity: uAtmoDensity,
uAtmoGrayTau: uAtmoGrayTau, uAtmoGain: uAtmoGain,
uEarthRadius: uEarthRadius,
uCurvatureOn: uCurvatureOn, uCamPos: uCamPos,
uAtmoBlueColor: uAtmoBlueColor, uAtmoGrayColor: uAtmoGrayColor,
uSunColor: uSunColor, uSunInt: uSunInt, uMoonColor: uMoonColor,
uMoonInt: uMoonInt, uSkyAmb: uSkyAmb, uStarAmb: uStarAmb,
@@ -6322,6 +6389,8 @@ window.clouds3dRender = function(target) {
gl.enableVertexAttribArray(s.aPos);
gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0);
gl.uniformMatrix4fv(s.uViewProjInv, false, viewProjInv);
gl.uniform3f(s.uCamPos, camX, camY, camZ);
gl.uniform1f(s.uCurvatureOn, CURVATURE_ON ? 1.0 : 0.0);
gl.disable(gl.DEPTH_TEST);
gl.depthMask(false);
gl.enable(gl.BLEND);