File size: 5,152 Bytes
a97ef0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// ♈ 白羊座 —— 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;
    },
  };
}