Add volumetric clouds3d renderer
Raymarches a horizontal cloud layer (28 steps, 4-octave 3D fBm).
Camera path is sinusoidal in altitude so the view dips into the
cloud deck at intervals for a 'fly through the edges' feel.
Density threshold from weather: sunny -> sparse, stormy -> overcast.
Renderer is selected via CONFIG.renderer ('clouds2d' or 'clouds3d').
This commit is contained in:
1 parent
3812f80f40
commit
0648676974
4 files changed
+289
-12
No files matched your search
Vendored
+269
@@ -0,0 +1,269 @@
|
||||
/* Volumetric 3D cloud renderer.
|
||||
* Raymarches a horizontal cloud layer with the camera flying
|
||||
* slowly above and through the cloud deck. Density threshold
|
||||
* comes from the weather theme, so 'sunny' gives breaks in the
|
||||
* deck and 'stormy' puts you inside overcast most of the time.
|
||||
*
|
||||
* Reads palette + weather + speed from window globals (same
|
||||
* contract as clouds2d.js, theme-agnostic).
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
function makeShader(gl, type, src) {
|
||||
var s = gl.createShader(type);
|
||||
gl.shaderSource(s, src);
|
||||
gl.compileShader(s);
|
||||
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
|
||||
console.error('clouds3d: Shader compile error:', gl.getShaderInfoLog(s));
|
||||
gl.deleteShader(s);
|
||||
return null;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function makeProgram(gl, vsSrc, fsSrc) {
|
||||
var vs = makeShader(gl, gl.VERTEX_SHADER, vsSrc);
|
||||
var fs = makeShader(gl, gl.FRAGMENT_SHADER, fsSrc);
|
||||
if (!vs || !fs) return null;
|
||||
var p = gl.createProgram();
|
||||
gl.attachShader(p, vs);
|
||||
gl.attachShader(p, fs);
|
||||
gl.linkProgram(p);
|
||||
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
|
||||
console.error('clouds3d: Program link error:', gl.getProgramInfoLog(p));
|
||||
gl.deleteProgram(p);
|
||||
return null;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
var VERT_SRC = [
|
||||
'attribute vec2 aPos;',
|
||||
'void main() { gl_Position = vec4(aPos, 0.0, 1.0); }'
|
||||
].join('\n');
|
||||
|
||||
// Volumetric raymarch through a horizontal cloud layer.
|
||||
// Cheap 3D value noise + 4-octave fbm. 28 raymarch steps.
|
||||
// Camera path is sinusoidal so the view occasionally dips
|
||||
// into the cloud deck for the "fly through the edges" feel.
|
||||
var FRAG_SRC = [
|
||||
'precision highp float;',
|
||||
'uniform vec2 uResolution;',
|
||||
'uniform float uTime;',
|
||||
'uniform vec3 uSky;',
|
||||
'uniform vec3 uCloud;',
|
||||
'uniform vec3 uCloudShadow;',
|
||||
'uniform vec3 uLight;',
|
||||
'uniform vec3 uBg;',
|
||||
'uniform float uSpeed;',
|
||||
'uniform float uDensity;',
|
||||
'uniform float uDayProgress;',
|
||||
// 3D hash and value noise
|
||||
'float hash3(vec3 p) {',
|
||||
'p = fract(p * vec3(0.1031, 0.1030, 0.0973));',
|
||||
'p += dot(p, p.yxz + 33.33);',
|
||||
'return fract((p.x + p.y) * p.z);',
|
||||
'}',
|
||||
'float noise3(vec3 p) {',
|
||||
'vec3 i = floor(p);',
|
||||
'vec3 f = fract(p);',
|
||||
'f = f * f * (3.0 - 2.0 * f);',
|
||||
'return mix(',
|
||||
'mix(mix(hash3(i + vec3(0,0,0)), hash3(i + vec3(1,0,0)), f.x),',
|
||||
'mix(hash3(i + vec3(0,1,0)), hash3(i + vec3(1,1,0)), f.x), f.y),',
|
||||
'mix(mix(hash3(i + vec3(0,0,1)), hash3(i + vec3(1,0,1)), f.x),',
|
||||
'mix(hash3(i + vec3(0,1,1)), hash3(i + vec3(1,1,1)), f.x), f.y),',
|
||||
'f.z);',
|
||||
'}',
|
||||
'float fbm3(vec3 p) {',
|
||||
'float v = 0.0;',
|
||||
'float a = 0.5;',
|
||||
'for (int i = 0; i < 4; i++) {',
|
||||
'v += a * noise3(p);',
|
||||
'p = p * 2.03 + vec3(13.7, 7.1, 19.3);',
|
||||
'a *= 0.5;',
|
||||
'}',
|
||||
'return v;',
|
||||
'}',
|
||||
// Density at a point in the cloud layer.
|
||||
'float cloudDensity(vec3 p, float baseThresh) {',
|
||||
'float n = fbm3(p * 0.35 + vec3(0.0, 0.0, uTime * 0.06));',
|
||||
// Cloud layer thickness: between y=-0.3 (deck bottom)
|
||||
// and y=1.8 (top), densest in the middle.
|
||||
'float h = clamp((p.y + 0.3) / 2.1, 0.0, 1.0);',
|
||||
'float profile = 4.0 * h * (1.0 - h); // bell curve',
|
||||
'profile = max(profile, 0.0);',
|
||||
'return smoothstep(baseThresh, baseThresh + 0.18, n) * profile;',
|
||||
'}',
|
||||
'void main() {',
|
||||
'vec2 uv = (gl_FragCoord.xy / uResolution.xy) * 2.0 - 1.0;',
|
||||
'uv.x *= uResolution.x / uResolution.y;',
|
||||
// Camera path: slow drift in X, sinusoidal altitude.
|
||||
// The altitude dip drives the "fly through cloud edges" effect.
|
||||
'float drift = uTime * uSpeed * 0.45;',
|
||||
'vec3 ro = vec3(',
|
||||
'drift,',
|
||||
// Altitude oscillates: above (1.5), at cloud-top (0.6), inside (0.1)
|
||||
'1.5 - 1.4 * (0.5 + 0.5 * sin(uTime * 0.07)),',
|
||||
'drift * 0.7',
|
||||
');',
|
||||
// Ray direction: gentle downward tilt so we see the cloud deck
|
||||
// and the sky horizon at once.
|
||||
'vec3 rd = normalize(vec3(uv.x * 0.55, -0.18 + uv.y * 0.25, 1.0));',
|
||||
// Sky gradient based on ray Y (background).
|
||||
'float skyT = clamp(rd.y * 0.7 + 0.5, 0.0, 1.0);',
|
||||
'vec3 col = mix(uBg, uSky, skyT);',
|
||||
// Density threshold: weather 'sunny' -> high (sparse clouds),
|
||||
// 'stormy' -> low (overcast deck).
|
||||
'float baseThresh = mix(0.72, 0.40, uDensity);',
|
||||
// March the cloud layer (bounded in Y).
|
||||
'float t = 0.0;',
|
||||
'float stepSize = 0.55;',
|
||||
'float trans = 1.0;',
|
||||
// Pre-compute ray entry/exit on cloud layer for early out
|
||||
'for (int i = 0; i < 28; i++) {',
|
||||
'vec3 p = ro + rd * t;',
|
||||
'if (p.y > 2.0 || p.y < -0.4) break;',
|
||||
'float d = cloudDensity(p, baseThresh);',
|
||||
'if (d > 0.005) {',
|
||||
// Cloud shading: bottom = shadow, top = lit
|
||||
'float lit = clamp((p.y + 0.3) / 2.1, 0.0, 1.0);',
|
||||
'vec3 cloudCol = mix(uCloudShadow, uCloud, lit);',
|
||||
// Sun tint on the lit side, fades at night
|
||||
'cloudCol = mix(cloudCol, uLight, uDayProgress * 0.35 * lit);',
|
||||
'float alpha = clamp(d * stepSize * 0.9, 0.0, 1.0);',
|
||||
'col = mix(col, cloudCol, alpha * trans);',
|
||||
'trans *= (1.0 - alpha);',
|
||||
'if (trans < 0.04) break;',
|
||||
'}',
|
||||
't += stepSize;',
|
||||
'}',
|
||||
// Night darkening
|
||||
'col = mix(col, mix(uBg, col, 0.4), (1.0 - uDayProgress) * 0.55);',
|
||||
'gl_FragColor = vec4(col, 1.0);',
|
||||
'}'
|
||||
].join('\n');
|
||||
|
||||
function init(canvas) {
|
||||
var gl = canvas.getContext('webgl', { antialias: false, alpha: false });
|
||||
if (!gl) {
|
||||
console.error('clouds3d: WebGL not available');
|
||||
return null;
|
||||
}
|
||||
var prog = makeProgram(gl, VERT_SRC, FRAG_SRC);
|
||||
if (!prog) return null;
|
||||
|
||||
var buf = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,
|
||||
new Float32Array([ -1,-1, 3,-1, -1,3 ]),
|
||||
gl.STATIC_DRAW);
|
||||
|
||||
var aPos = gl.getAttribLocation(prog, 'aPos');
|
||||
var uRes = gl.getUniformLocation(prog, 'uResolution');
|
||||
var uTime = gl.getUniformLocation(prog, 'uTime');
|
||||
var uSky = gl.getUniformLocation(prog, 'uSky');
|
||||
var uCloud = gl.getUniformLocation(prog, 'uCloud');
|
||||
var uCloudShadow = gl.getUniformLocation(prog, 'uCloudShadow');
|
||||
var uLight = gl.getUniformLocation(prog, 'uLight');
|
||||
var uBg = gl.getUniformLocation(prog, 'uBg');
|
||||
var uSpeed = gl.getUniformLocation(prog, 'uSpeed');
|
||||
var uDensity = gl.getUniformLocation(prog, 'uDensity');
|
||||
var uDayProgress = gl.getUniformLocation(prog, 'uDayProgress');
|
||||
|
||||
return {
|
||||
gl: gl, prog: prog, buf: buf, aPos: aPos,
|
||||
uRes: uRes, uTime: uTime, uSky: uSky, uCloud: uCloud,
|
||||
uCloudShadow: uCloudShadow, uLight: uLight, uBg: uBg,
|
||||
uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress
|
||||
};
|
||||
}
|
||||
|
||||
function hex(rgb) {
|
||||
return [((rgb >> 16) & 255) / 255,
|
||||
((rgb >> 8) & 255) / 255,
|
||||
(rgb & 255) / 255];
|
||||
}
|
||||
|
||||
// Same density mapping as clouds2d so swapping renderers doesn't
|
||||
// change the visual weather.
|
||||
var WEATHER_DENSITY = {
|
||||
sunny: 0.20,
|
||||
cloudy: 0.70,
|
||||
stormy: 1.00,
|
||||
night: 0.45
|
||||
};
|
||||
|
||||
window.clouds3dRender = function(target) {
|
||||
if (!target || typeof target !== 'object') {
|
||||
console.error('clouds3d: invalid target');
|
||||
return;
|
||||
}
|
||||
var canvas = target.__clouds3dCanvas;
|
||||
if (!canvas) {
|
||||
canvas = document.createElement('canvas');
|
||||
canvas.style.position = 'absolute';
|
||||
canvas.style.top = '0';
|
||||
canvas.style.left = '0';
|
||||
canvas.style.width = '100%';
|
||||
canvas.style.height = '100%';
|
||||
while (target.firstChild) target.removeChild(target.firstChild);
|
||||
target.appendChild(canvas);
|
||||
target.style.position = 'relative';
|
||||
target.__clouds3dCanvas = canvas;
|
||||
}
|
||||
|
||||
if (!canvas.__state) {
|
||||
canvas.__state = init(canvas);
|
||||
if (!canvas.__state) return;
|
||||
}
|
||||
|
||||
var s = canvas.__state;
|
||||
var gl = s.gl;
|
||||
canvas.width = target.clientWidth || window.innerWidth;
|
||||
canvas.height = target.clientHeight || window.innerHeight;
|
||||
gl.viewport(0, 0, canvas.width, canvas.height);
|
||||
|
||||
gl.useProgram(s.prog);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, s.buf);
|
||||
gl.enableVertexAttribArray(s.aPos);
|
||||
gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.uniform2f(s.uRes, canvas.width, canvas.height);
|
||||
gl.uniform1f(s.uTime, performance.now() / 1000);
|
||||
|
||||
var pal = window.__currentPalette;
|
||||
if (!pal) {
|
||||
gl.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
return;
|
||||
}
|
||||
|
||||
var weather = window.__currentWeatherName;
|
||||
var dayProgress = window.__currentDayProgress;
|
||||
var themeSpeed = window.__currentThemeSpeed;
|
||||
|
||||
gl.uniform3fv(s.uSky, hex(pal.sky));
|
||||
gl.uniform3fv(s.uCloud, hex(pal.cloud));
|
||||
gl.uniform3fv(s.uCloudShadow, hex(
|
||||
pal.cloudShadow !== undefined ? pal.cloudShadow : pal.sky));
|
||||
gl.uniform3fv(s.uLight, hex(pal.light));
|
||||
gl.uniform3fv(s.uBg, hex(pal.bg));
|
||||
|
||||
var density = (weather && WEATHER_DENSITY[weather] !== undefined)
|
||||
? WEATHER_DENSITY[weather] : 0.5;
|
||||
gl.uniform1f(s.uDensity, density);
|
||||
|
||||
var day = (typeof dayProgress === 'number')
|
||||
? Math.max(0, Math.min(1, dayProgress)) : 0.5;
|
||||
gl.uniform1f(s.uDayProgress, day);
|
||||
|
||||
var speed = (typeof themeSpeed === 'number')
|
||||
? Math.max(0.1, Math.min(3, themeSpeed)) : 1.0;
|
||||
gl.uniform1f(s.uSpeed, speed);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in new issue
Block a user