Files
animated_wallpaper/vendor/clouds2d.js
T
Richard Zink 595b22aac1 More cloud structure: 4 fBm layers, 4 octaves each
Density reverted to original 0.20/0.70/1.00/0.45 but structure
denser than ever:
- fbm3: 3 -> 4 octaves (more detail at fine scales)
- cloudDensity: 4 layers (base + mid 35% + fine1 18% + fine2 10%)
- smoothstep 0.24 -> 0.20 for tighter cloud edges
Cost: 4 fbm * 4 octaves = 16 noise3 samples per step * 18 steps
= 288 samples per pixel. Watch FPS on low-end GPUs.
2026-10-07 20:07:20 +02:00

281 lines
10 KiB
JavaScript

/* Texture-based 2D cloud renderer.
*
* Inspired by fizzer's tiny ShaderToy cloud shader: instead of
* computing fBm in-shader with hash functions, sample a noise
* texture (4 lookups = 4 octaves) and weight the results. This
* trades a tiny amount of texture memory for a large drop in ALU
* per pixel — significant on low-end integrated GPUs.
*
* Texture is loaded from window.__noiseDataUri (a data URI baked
* from noise.png by the build). Falls back to /noise.png if the
* URI is unavailable. Theme-agnostic: parent owns colour via
* __currentPalette.
*/
(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('clouds2d: 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('clouds2d: 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');
// 4-octave fBm by sampling a noise texture. The texture is REPEAT-
// wrapped so we can scroll through it endlessly. Scrolling uses a
// hash-based per-pixel jitter so the regular tiling pattern doesn't
// cause horizon flicker (same fix as clouds3d).
var FRAG_SRC = [
'precision highp float;',
'uniform vec2 uResolution;',
'uniform float uTime;',
'uniform sampler2D uNoise;',
'uniform vec3 uSky;',
'uniform vec3 uCloud;',
'uniform vec3 uCloudShadow;',
'uniform vec3 uLight;',
'uniform vec3 uBg;',
'uniform float uSpeed;',
'uniform float uDensity;',
'uniform float uDayProgress;',
'uniform float uSunAlt;',
'float hash21(vec2 p) {',
'p = fract(p * vec2(123.34, 456.21));',
'p += dot(p, p + 45.32);',
'return fract(p.x * p.y);',
'}',
// 4-octave texture fBm. Each octave doubles frequency and halves
// amplitude (1, 0.5, 0.25, 0.125). Total normalised by 1/0.9375.
'float fbm(vec2 p) {',
'float total = 0.0;',
'total += 1.0 * texture2D(uNoise, p * 1.0).r;',
'total += 0.5 * texture2D(uNoise, p * 2.0).r;',
'total += 0.25 * texture2D(uNoise, p * 4.0).r;',
'total += 0.125 * texture2D(uNoise, p * 8.0).r;',
'return total / 1.875;',
'}',
'void main() {',
'vec2 uv = gl_FragCoord.xy / uResolution.xy;',
// Scroll the noise field horizontally with time, plus a small
// vertical drift for slow cloud movement. Multiply by uv's
// aspect so clouds don't squash on wide displays.
'float aspect = uResolution.x / uResolution.y;',
'vec2 p = vec2(',
'(uv.x - 0.5) * aspect * 3.0 + uTime * 0.04 * uSpeed,',
'uv.y * 2.5 + uTime * 0.012 * uSpeed',
');',
// Per-pixel jitter to break the regular tiling pattern that
// would otherwise flicker at the cloud horizon.
'p += (hash21(gl_FragCoord.xy + uTime) - 0.5) * 0.015;',
'float n = fbm(p);',
// Density threshold widens with weather (sunny = sparse,
// stormy = overcast).
'float thresh = mix(0.70, 0.35, uDensity);',
'float cloud = smoothstep(thresh, thresh + 0.18, n);',
// Sky gradient: bg below, sky above
'vec3 sky = mix(uBg, uSky, smoothstep(0.0, 0.6, uv.y));',
// Cloud body: shadow on the bottom, lit on the top
'vec3 cloudCol = mix(uCloudShadow, uCloud, smoothstep(0.0, 0.7, uv.y));',
// Light tint on the lit side (always slightly, so even at
// night the cloud edge catches some illumination)
'cloudCol = mix(cloudCol, uLight, cloud * uv.y * 0.45 * max(0.15, uDayProgress));',
// Composite
'vec3 col = mix(sky, cloudCol, cloud);',
// Night darkening, driven by sun altitude (not uDayProgress
// which is a 24h clock progress, not "is it night")
'float nightFactor = smoothstep(0.05, -0.15, uSunAlt);',
'col = mix(col, mix(uBg, col, 0.4), nightFactor * 0.45);',
'gl_FragColor = vec4(col, 1.0);',
'}'
].join('\n');
function hex(rgb) {
return [((rgb >> 16) & 255) / 255,
((rgb >> 8) & 255) / 255,
(rgb & 255) / 255];
}
var WEATHER_DENSITY = {
sunny: 0.20,
cloudy: 0.70,
stormy: 1.00,
night: 0.45
};
// Load the noise image and upload it as a REPEAT-wrapped texture.
// Priority: window.__noiseDataUri (if it has real data, not
// PLACEHOLDER), else fetch /noise.png from the server.
function loadNoiseTexture(gl) {
return new Promise(function(resolve, reject) {
var inline = window.__noiseDataUri;
var hasInline = inline && inline.indexOf('PLACEHOLDER') < 0;
var src = hasInline ? inline : 'noise.png';
var img = new Image();
img.onload = function() {
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
resolve(tex);
};
img.onerror = function() {
reject(new Error('clouds2d: failed to load noise image: ' + src));
};
img.src = src;
});
}
window.clouds2dRender = function(target) {
if (!target || typeof target !== 'object') {
console.error('clouds2d: invalid target');
return;
}
var canvas = target.__clouds2dCanvas;
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.__clouds2dCanvas = canvas;
}
var gl = canvas.__gl;
if (!gl) {
gl = canvas.getContext('webgl', { antialias: false, alpha: false });
if (!gl) {
console.error('clouds2d: WebGL not available');
return;
}
canvas.__gl = gl;
}
if (!canvas.__state) {
// Compile shaders synchronously, load texture asynchronously.
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 prog = makeProgram(gl, VERT_SRC, FRAG_SRC);
if (!prog) return;
canvas.__state = {
gl: gl, prog: prog, buf: buf,
aPos: gl.getAttribLocation(prog, 'aPos'),
uRes: gl.getUniformLocation(prog, 'uResolution'),
uTime: gl.getUniformLocation(prog, 'uTime'),
uNoise: gl.getUniformLocation(prog, 'uNoise'),
uSky: gl.getUniformLocation(prog, 'uSky'),
uCloud: gl.getUniformLocation(prog, 'uCloud'),
uCloudShadow: gl.getUniformLocation(prog, 'uCloudShadow'),
uLight: gl.getUniformLocation(prog, 'uLight'),
uBg: gl.getUniformLocation(prog, 'uBg'),
uSpeed: gl.getUniformLocation(prog, 'uSpeed'),
uDensity: gl.getUniformLocation(prog, 'uDensity'),
uDayProgress: gl.getUniformLocation(prog, 'uDayProgress'),
uSunAlt: gl.getUniformLocation(prog, 'uSunAlt')
};
canvas.__noiseReady = false;
loadNoiseTexture(gl).then(function(tex) {
canvas.__noise = tex;
canvas.__noiseReady = true;
}).catch(function(err) {
console.error(err);
});
}
var s = canvas.__state;
canvas.width = target.clientWidth || window.innerWidth;
canvas.height = target.clientHeight || window.innerHeight;
gl.viewport(0, 0, canvas.width, canvas.height);
// Texture not loaded yet — just clear, frame after will draw.
if (!canvas.__noiseReady) {
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
return;
}
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.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, canvas.__noise);
gl.uniform1i(s.uNoise, 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;
var sunAlt = window.__currentSunAlt;
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.uniform1f(s.uSunAlt, (typeof sunAlt === 'number') ? sunAlt : 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
};
})();