// ♈ 白羊座 —— WebGL2 背景:域扭曲分形噪声星云 + 上升的火焰流 + 程序化星点 // 不支持 WebGL2 时返回 null,由调用方降级到 Canvas 渐变。 const VERT = `#version 300 es in vec2 p; out vec2 uv; void main(){ uv = p * .5 + .5; gl_Position = vec4(p, 0., 1.); }`; const FRAG = `#version 300 es precision highp float; in vec2 uv; out vec4 o; uniform vec2 res; uniform float t; uniform vec3 a0, a1, a2, b0, b1, b2; // 两套调色板(深/中/亮) uniform float mixk, heat, stars, bright; float h21(vec2 p){ p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } float vnoise(vec2 p){ vec2 i = floor(p), f = fract(p); vec2 u = f * f * (3. - 2. * f); return mix(mix(h21(i), h21(i + vec2(1,0)), u.x), mix(h21(i + vec2(0,1)), h21(i + vec2(1,1)), u.x), u.y); } float fbm(vec2 p){ float v = 0., a = .5; mat2 m = mat2(1.6, 1.2, -1.2, 1.6); for (int k = 0; k < 5; k++){ v += a * vnoise(p); p = m * p; a *= .5; } return v; } void main(){ vec2 q = (uv - .5) * vec2(res.x / res.y, 1.); float asp = res.x / res.y; // 域扭曲星云 vec2 w = q * 1.6; vec2 d1 = vec2(fbm(w + vec2(0., t * .02)), fbm(w + vec2(5.2, 1.3) - t * .015)); float n = fbm(w + 2.2 * d1 + vec2(t * .01, 0.)); // 向上流动的火焰丝 vec2 fq = vec2(q.x * 2.2, q.y * 1.4 - t * .12); float flame = fbm(fq + fbm(fq * 1.7 + t * .05)); flame = smoothstep(.45, 1., flame) * smoothstep(.75, -.6, q.y) * heat; vec3 c0 = mix(a0, b0, mixk), c1 = mix(a1, b1, mixk), c2 = mix(a2, b2, mixk); vec3 col = mix(c0, c1, smoothstep(.25, .75, n)); col = mix(col, c2, smoothstep(.62, .95, n) * .85); col += c2 * flame * 1.1; // 中心体积光 float glow = exp(-dot(q * vec2(.8, 1.1), q * vec2(.8, 1.1)) * 2.2); col += c1 * glow * .35 * bright; // 程序化星点(三层) float s = 0.; for (int L = 0; L < 3; L++){ float sc = 90. + float(L) * 70.; vec2 g = (uv * vec2(asp, 1.) + float(L) * 7.13 + vec2(t * .002 * float(L + 1), 0.)) * sc; vec2 id = floor(g), f = fract(g) - .5; float r = h21(id); if (r > .965){ vec2 o2 = vec2(h21(id + 3.1), h21(id + 7.7)) - .5; float dd = length(f - o2 * .6); float tw = .6 + .4 * sin(t * (1.5 + r * 4.) + r * 50.); s += smoothstep(.09, 0., dd) * tw * (1.3 - float(L) * .3); } } col += vec3(1., .95, .88) * s * stars; // 暗角 + 胶片颗粒 float vig = smoothstep(1.25, .25, length(q * vec2(.9, 1.15))); col *= mix(.35, 1., vig); col += (h21(uv * res + fract(t)) - .5) * .025; col = 1. - exp(-col * 1.25); // 柔和色调映射 o = vec4(col, 1.); }`; // 场景色调(深 / 中 / 亮) export const TONES = { ember: [[.06, .01, .02], [.55, .10, .05], [1.0, .62, .22]], dawn: [[.05, .03, .10], [.48, .16, .26], [1.0, .74, .42]], night: [[.01, .02, .07], [.12, .12, .34], [.55, .62, 1.0]], sea: [[.00, .03, .08], [.04, .24, .40], [.55, .90, 1.0]], grove: [[.01, .04, .03], [.06, .26, .16], [.95, .80, .38]], royal: [[.05, .01, .07], [.38, .06, .22], [1.0, .78, .38]], }; export function createSky() { const cv = document.createElement('canvas'); const gl = cv.getContext('webgl2', { preserveDrawingBuffer: true, antialias: false, premultipliedAlpha: false }); if (!gl) return null; const sh = (type, src) => { const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s); if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s)); return s; }; const prog = gl.createProgram(); gl.attachShader(prog, sh(gl.VERTEX_SHADER, VERT)); gl.attachShader(prog, sh(gl.FRAGMENT_SHADER, FRAG)); gl.linkProgram(prog); if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog)); gl.useProgram(prog); const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW); const loc = gl.getAttribLocation(prog, 'p'); gl.enableVertexAttribArray(loc); gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0); const U = {}; for (const n of ['res', 't', 'a0', 'a1', 'a2', 'b0', 'b1', 'b2', 'mixk', 'heat', 'stars', 'bright']) U[n] = gl.getUniformLocation(prog, n); return { canvas: cv, // scale:背景以较低分辨率渲染再放大(星云本身很柔和) render(t, W, H, from, to, k, opt = {}) { const sc = opt.scale ?? .5; const w = Math.max(64, Math.round(W * sc)), h = Math.max(64, Math.round(H * sc)); if (cv.width !== w || cv.height !== h) { cv.width = w; cv.height = h; } gl.viewport(0, 0, w, h); gl.uniform2f(U.res, w, h); gl.uniform1f(U.t, t); const A = TONES[from] || TONES.ember, B = TONES[to] || A; gl.uniform3fv(U.a0, A[0]); gl.uniform3fv(U.a1, A[1]); gl.uniform3fv(U.a2, A[2]); gl.uniform3fv(U.b0, B[0]); gl.uniform3fv(U.b1, B[1]); gl.uniform3fv(U.b2, B[2]); gl.uniform1f(U.mixk, k); gl.uniform1f(U.heat, opt.heat ?? .6); gl.uniform1f(U.stars, opt.stars ?? 1); gl.uniform1f(U.bright, opt.bright ?? 1); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); return cv; }, }; }