feat: soft cloud layer edges

This commit is contained in:
Richard Zink committed 2026-10-13 20:16:46 +02:00
1 parent e86819457c
commit f8580e595a
1 file changed
+58 -22
+58 -22
View File
@@ -85,6 +85,9 @@ var FRAG_SRC = [
'uniform float uSunAlt;',
'uniform float uCloudTop;', // cloud deck upper edge (world Y)
'uniform float uCloudBot;', // cloud deck lower edge (world Y)
'uniform float uCloudEdgeSoft;',
'uniform float uCloudHorizonSoft;',
'uniform float uCloudMaxMarch;',
'uniform float uCloudNearFadeStart;',
'uniform float uCloudNearFadeEnd;',
'uniform sampler2D uElev;', // terrarium RGB height grid, same UV as uGround
@@ -219,6 +222,14 @@ var FRAG_SRC = [
'vec2 d = xz - uCamPos.xz;',
'return dot(d, d) / (2.0 * max(1.0, uEarthRadius));',
'}',
'float cloudVerticalProfile(float alt) {',
'float pad = max(0.0, uCloudEdgeSoft);',
'float bot = uCloudBot - pad;',
'float top = uCloudTop + pad;',
'float h = (alt - bot) / max(0.01, top - bot);',
'if (h <= 0.0 || h >= 1.0) return 0.0;',
'return smoothstep(0.0, 0.25, h) * smoothstep(1.0, 0.75, h);',
'}',
'float cloudDensity(vec3 p, float baseThresh, float soft) {',
'float py = p.y + earthDrop(p.xz);',
'vec3 cp = vec3(p.x, py, p.z) + uWind * uTime;',
@@ -230,11 +241,7 @@ var FRAG_SRC = [
'float d = fbm3(cp * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));',
'n = n + d * mix(0.22, 0.0, soft / 0.75);',
'}',
// Cloud layer between uCloudBot and uCloudTop, densest in the
// middle (bell curve). The layer follows the Earth curvature.
'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);',
'float profile = cloudVerticalProfile(py);',
'return smoothstep(baseThresh, baseThresh + 0.34 + soft * 0.28, n) * profile;',
'}',
// Star field: project ray onto a far sphere, hash-grid for
@@ -421,9 +428,7 @@ var FRAG_SRC = [
'float d = fbm3(q * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));',
'n = n + d * mix(0.22, 0.0, soft / 0.75);',
'}',
'float h = clamp((alt - uCloudBot) / max(0.01, uCloudTop - uCloudBot), 0.0, 1.0);',
'float profile = 4.0 * h * (1.0 - h);',
'profile = max(profile, 0.0);',
'float profile = cloudVerticalProfile(alt);',
'return smoothstep(baseThresh, baseThresh + 0.34 + soft * 0.28, n) * profile;',
'}',
'float hazeColumnToDistance(vec3 rd, float dist) {',
@@ -677,14 +682,17 @@ var FRAG_SRC = [
'if (uMode == 1) {',
'if (uCloudsOn < 0.5) { gl_FragColor = vec4(0.0); return; }',
'float camAlt = uCamPos.y;',
'vec2 top = cloudLayerRoots(uCloudTop, rd);',
'vec2 bot = cloudLayerRoots(uCloudBot, rd);',
'float pad = max(0.0, uCloudEdgeSoft);',
'float softTop = uCloudTop + pad;',
'float softBot = uCloudBot - pad;',
'vec2 top = cloudLayerRoots(softTop, rd);',
'vec2 bot = cloudLayerRoots(softBot, rd);',
'float tEnter = 1e9;',
'float tExit = -1e9;',
'if (camAlt >= uCloudBot && camAlt <= uCloudTop) {',
'if (camAlt >= softBot && camAlt <= softTop) {',
'tEnter = 0.0;',
'tExit = max(maxPositiveRoot(top), maxPositiveRoot(bot));',
'} else if (camAlt > uCloudTop) {',
'} else if (camAlt > softTop) {',
'tEnter = minPositiveRoot(top);',
'tExit = maxPositiveRoot(bot);',
'if (tExit < 0.0) tExit = maxPositiveRoot(top);',
@@ -694,6 +702,9 @@ var FRAG_SRC = [
'if (tExit < 0.0) tExit = maxPositiveRoot(bot);',
'}',
'if (tEnter > 1e8 || tExit <= tEnter) { gl_FragColor = vec4(0.0); return; }',
'float naturalInterval = tExit - tEnter;',
'float horizonFade = smoothstep(0.0, max(0.1, uCloudHorizonSoft), naturalInterval);',
'if (horizonFade <= 0.001) { gl_FragColor = vec4(0.0); return; }',
'vec2 uv = gl_FragCoord.xy / uResolution;',
'if (uTerrainDistOk > 0.5) {',
'vec4 distTex = texture2D(uTerrainDistTex, uv);',
@@ -702,13 +713,14 @@ var FRAG_SRC = [
'float dist = distNorm * max(1.0, uFarPlane);',
'vec3 pT = uCamPos + rd * dist;',
'float terrainAlt = (pT.y + earthDrop(pT.xz)) / max(1.0, uTerrainExag);',
'if (terrainAlt > uCloudTop + 0.05) { gl_FragColor = vec4(0.0); return; }',
'if (terrainAlt > softTop + 0.05) { gl_FragColor = vec4(0.0); return; }',
'tExit = min(tExit, dist - 0.02);',
'}',
'}',
'tExit = min(tExit, tEnter + 30.0);',
'float maxMarch = max(1.0, uCloudMaxMarch);',
'tExit = min(tExit, tEnter + maxMarch);',
'if (tExit <= tEnter) { gl_FragColor = vec4(0.0); return; }',
'float stepSize = (tExit - tEnter) / 24.0;',
'float stepSize = (tExit - tEnter) / 48.0;',
'float t = tEnter + stepSize * 0.5;',
'float coverage = clamp(uDensity, 0.0, 1.0);',
'float baseThresh = mix(0.62, 0.20, coverage);',
@@ -722,11 +734,11 @@ var FRAG_SRC = [
'vec3 p = uCamPos + rd * t;',
'p += rd * (ign(gl_FragCoord.xy + vec2(float(i) * 7.13, float(i) * 3.71)) - 0.5) * stepSize * 0.25;',
'float alt = p.y + earthDrop(p.xz);',
'if (alt > uCloudTop + 0.1 || alt < uCloudBot - 0.1) break;',
'if (alt > softTop + 0.1 || alt < softBot - 0.1) break;',
'float soft = smoothstep(4.0, 20.0, t);',
'float d = cloudDensitySpherical(p, alt, baseThresh, soft);',
'if (d > 0.02) {',
'float lit = clamp((alt - uCloudBot) / max(0.01, uCloudTop - uCloudBot), 0.0, 1.0);',
'float lit = clamp((alt - softBot) / max(0.01, softTop - softBot), 0.0, 1.0);',
'vec3 albC = uCloudAlbedo * mix(0.78, 1.0, lit);',
'float selfLit = clamp(1.0 - d * 0.8, 0.18, 1.0);',
'vec3 direct = albC * uSunColor * uSunInt * phase * sunUp * selfLit;',
@@ -734,7 +746,8 @@ var FRAG_SRC = [
'vec3 moonC = albC * uMoonColor * (0.35 + 0.65 * max(0.0, dot(rd, uMoonDir))) * uMoonInt * 3.0;',
'vec3 cloudCol = direct + ambC + moonC;',
'float nearFade = smoothstep(uCloudNearFadeStart, uCloudNearFadeEnd, t);',
'float alpha = clamp(d * stepSize * 3.50 * densityGain * nearFade, 0.0, 1.0);',
'float marchFade = 1.0 - smoothstep(maxMarch * 0.75, maxMarch, t - tEnter);',
'float alpha = clamp(d * stepSize * 3.50 * densityGain * nearFade * horizonFade * marchFade, 0.0, 1.0);',
'cloudSum += cloudCol * alpha * trans;',
'trans *= (1.0 - alpha);',
'if (trans < 0.04) break;',
@@ -877,6 +890,14 @@ var CLOUD_SAMPLER_GLSL = [
'float sq = sqrt(disc);',
'return vec2((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a));',
'}',
'float cloudVerticalProfile(float alt) {',
'float pad = max(0.0, uCloudEdgeSoft);',
'float bot = uCloudBot - pad;',
'float top = uCloudTop + pad;',
'float h = (alt - bot) / max(0.01, top - bot);',
'if (h <= 0.0 || h >= 1.0) return 0.0;',
'return smoothstep(0.0, 0.25, h) * smoothstep(1.0, 0.75, h);',
'}',
'float cloudDensity(vec3 p, float baseThresh, float soft) {',
'float py = p.y + earthDrop(p.xz);',
'vec3 cp = vec3(p.x, py, p.z) + uWind * uTime;',
@@ -888,9 +909,7 @@ var CLOUD_SAMPLER_GLSL = [
'float d = fbm3(cp * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));',
'n = n + d * mix(0.22, 0.0, soft / 0.75);',
'}',
'float h = clamp((py - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);',
'float profile = 4.0 * h * (1.0 - h);',
'profile = max(profile, 0.0);',
'float profile = cloudVerticalProfile(py);',
'return smoothstep(baseThresh, baseThresh + 0.34 + soft * 0.28, n) * profile;',
'}',
].join('\n');
@@ -915,6 +934,7 @@ var TERRAIN_FRAG_SRC = [
'uniform vec3 uWind;', // cloud advection vector, world units / s
'uniform float uCloudBot;',
'uniform float uCloudTop;',
'uniform float uCloudEdgeSoft;',
'uniform float uDensity;', // cloud coverage, same as the sky pass
'uniform float uCloudShadowOn;',
'uniform float uCloudShadowDensity;', // optical depth per density sample
@@ -1146,6 +1166,9 @@ function init(canvas) {
var uSunAlt = gl.getUniformLocation(prog, 'uSunAlt');
var uCloudTop = gl.getUniformLocation(prog, 'uCloudTop');
var uCloudBot = gl.getUniformLocation(prog, 'uCloudBot');
var uCloudEdgeSoft = gl.getUniformLocation(prog, 'uCloudEdgeSoft');
var uCloudHorizonSoft = gl.getUniformLocation(prog, 'uCloudHorizonSoft');
var uCloudMaxMarch = gl.getUniformLocation(prog, 'uCloudMaxMarch');
var uElev = gl.getUniformLocation(prog, 'uElev');
var uElevOn = gl.getUniformLocation(prog, 'uElevOn');
var uElevScale = gl.getUniformLocation(prog, 'uElevScale');
@@ -1269,6 +1292,7 @@ function init(canvas) {
var terrainUwind = terrainProg ? gl.getUniformLocation(terrainProg, 'uWind') : null;
var terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null;
var terrainUcloudtop = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudTop') : null;
var terrainUcloudEdgeSoft = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudEdgeSoft') : null;
var terrainUdensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uDensity') : null;
var terrainUcloudshadow = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudShadowOn') : null;
var terrainUcsDens = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudShadowDensity') : null;
@@ -1308,6 +1332,7 @@ function init(canvas) {
uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress,
uSunAlt: uSunAlt,
uCloudTop: uCloudTop, uCloudBot: uCloudBot,
uCloudEdgeSoft: uCloudEdgeSoft, uCloudHorizonSoft: uCloudHorizonSoft, uCloudMaxMarch: uCloudMaxMarch,
uCloudNearFadeStart: gl.getUniformLocation(prog, 'uCloudNearFadeStart'),
uCloudNearFadeEnd: gl.getUniformLocation(prog, 'uCloudNearFadeEnd'),
uElev: uElev, uElevOn: uElevOn, uElevScale: uElevScale, uTerrainExag: uTerrainExag,
@@ -1372,7 +1397,7 @@ function init(canvas) {
uDistancePassOn: terrainUDistancePass, uFarPlane: terrainUFarPlane,
uStarAmb: terrainUstarAmb,
uTime: terrainUtime, uWind: terrainUwind, uCloudBot: terrainUcloudbot,
uCloudTop: terrainUcloudtop, uDensity: terrainUdensity,
uCloudTop: terrainUcloudtop, uCloudEdgeSoft: terrainUcloudEdgeSoft, uDensity: terrainUdensity,
uCloudShadowOn: terrainUcloudshadow,
uCloudShadowDensity: terrainUcsDens,
uCloudShadowFloor: terrainUcsFloor,
@@ -1505,6 +1530,13 @@ var CLOUD_NEAR_FADE_CFG = (window.CONFIG && window.CONFIG.clouds && window.CONFI
var CLOUD_NEAR_FADE_START = (typeof CLOUD_NEAR_FADE_CFG.start === 'number') ? Math.max(0, CLOUD_NEAR_FADE_CFG.start) : 0.35;
var CLOUD_NEAR_FADE_END = Math.max(CLOUD_NEAR_FADE_START + 0.1, (typeof CLOUD_NEAR_FADE_CFG.end === 'number') ? CLOUD_NEAR_FADE_CFG.end : 2.5);
var CLOUDS_ON = !(window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.enabled === false);
var CLOUD_SOFT_EDGES = !(window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.softEdges === false);
var CLOUD_EDGE_SOFT = CLOUD_SOFT_EDGES && (typeof window.CONFIG.clouds.edgeSoftness === 'number')
? Math.max(0.0, Math.min(1.0, window.CONFIG.clouds.edgeSoftness)) : (CLOUD_SOFT_EDGES ? 0.18 : 0.0);
var CLOUD_HORIZON_SOFT = CLOUD_SOFT_EDGES && (typeof window.CONFIG.clouds.horizonSoftness === 'number')
? Math.max(0.1, Math.min(40.0, window.CONFIG.clouds.horizonSoftness)) : (CLOUD_SOFT_EDGES ? 8.0 : 0.1);
var CLOUD_MAX_MARCH_UNITS = (window.CONFIG && typeof window.CONFIG.clouds.maxMarchUnits === 'number')
? Math.max(10.0, Math.min(300.0, window.CONFIG.clouds.maxMarchUnits)) : 30.0;
var CLOUD_TERRAIN_OCCLUSION = !(window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.terrainOcclusion === false);
var CLOUD_WIND_CFG = (window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.wind) || {};
var CLOUD_WIND_SCALE = (typeof CLOUD_WIND_CFG.speedScale === 'number') ? Math.max(0, CLOUD_WIND_CFG.speedScale) : 80;
@@ -7384,6 +7416,9 @@ window.clouds3dRender = function(target) {
gl.uniform1f(s.uSpeed, speed);
gl.uniform1f(s.uCloudTop, CLOUD_TOP);
gl.uniform1f(s.uCloudBot, CLOUD_BOT);
gl.uniform1f(s.uCloudEdgeSoft, CLOUD_EDGE_SOFT);
gl.uniform1f(s.uCloudHorizonSoft, CLOUD_HORIZON_SOFT);
gl.uniform1f(s.uCloudMaxMarch, CLOUD_MAX_MARCH_UNITS);
gl.uniform1f(s.uCloudNearFadeStart, CLOUD_NEAR_FADE_START);
gl.uniform1f(s.uCloudNearFadeEnd, CLOUD_NEAR_FADE_END);
@@ -7658,6 +7693,7 @@ window.clouds3dRender = function(target) {
gl.uniform3f(terr.uWind, _cloudWind[0], _cloudWind[1], _cloudWind[2]);
gl.uniform1f(terr.uCloudBot, CLOUD_BOT);
gl.uniform1f(terr.uCloudTop, CLOUD_TOP);
gl.uniform1f(terr.uCloudEdgeSoft, CLOUD_EDGE_SOFT);
gl.uniform1f(terr.uDensity, density);
gl.uniform1f(terr.uCloudShadowOn, ATMO_CLOUD_SHADOWS ? 1.0 : 0.0);
gl.uniform1f(terr.uCloudShadowDensity, ATMO_CS_DENSITY);