diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 38523cb..36b6df7 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -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);