File size: 6,229 Bytes
1390db3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
import * as THREE from 'three';
import { G } from './globals.js';
import { CLOUDS } from './config.js';

const COLOR_N = new THREE.Color(CLOUDS.colorNight);
const COLOR_D = new THREE.Color(CLOUDS.colorDay);
const TMP_COLOR = new THREE.Color();

// Lightweight value-noise + FBM for soft, natural cloud edges
function hash2i(xi, yi, seed) {
  let h = Math.imul(xi, 374761393) ^ Math.imul(yi, 668265263) ^ Math.imul(seed, 2147483647);
  h = Math.imul(h ^ (h >>> 13), 1274126177);
  h = (h ^ (h >>> 16)) >>> 0;
  return h / 4294967296; // [0,1)
}

function smoothstep(a, b, t) {
  if (t <= a) return 0;
  if (t >= b) return 1;
  t = (t - a) / (b - a);
  return t * t * (3 - 2 * t);
}

function lerp(a, b, t) { return a + (b - a) * t; }

function valueNoise2(x, y, seed) {
  const xi = Math.floor(x);
  const yi = Math.floor(y);
  const xf = x - xi;
  const yf = y - yi;
  const sx = smoothstep(0, 1, xf);
  const sy = smoothstep(0, 1, yf);
  const v00 = hash2i(xi, yi, seed);
  const v10 = hash2i(xi + 1, yi, seed);
  const v01 = hash2i(xi, yi + 1, seed);
  const v11 = hash2i(xi + 1, yi + 1, seed);
  const ix0 = lerp(v00, v10, sx);
  const ix1 = lerp(v01, v11, sx);
  return lerp(ix0, ix1, sy) * 2 - 1; // [-1,1]
}

function fbm2(x, y, baseFreq, octaves, lacunarity, gain, seed) {
  let sum = 0;
  let amp = 1;
  let freq = baseFreq;
  for (let i = 0; i < octaves; i++) {
    sum += amp * valueNoise2(x * freq, y * freq, seed + i * 1013);
    freq *= lacunarity;
    amp *= gain;
  }
  return sum; // ~[-ampSum, ampSum]
}

function makeCloudTexture(size = 256, puffCount = 10) {
  const canvas = document.createElement('canvas');
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext('2d');
  if (!ctx) return null;
  ctx.clearRect(0, 0, size, size);

  // Draw several soft circles to form a cloud shape
  const r = size / 2;
  ctx.fillStyle = 'white';
  ctx.globalCompositeOperation = 'source-over';
  for (let i = 0; i < puffCount; i++) {
    const pr = r * (0.42 + Math.random() * 0.38);
    const px = r + (Math.random() * 2 - 1) * r * 0.48;
    const py = r + (Math.random() * 2 - 1) * r * 0.22; // slightly flatter vertically
    const grad = ctx.createRadialGradient(px, py, pr * 0.18, px, py, pr);
    grad.addColorStop(0, 'rgba(255,255,255,0.95)');
    grad.addColorStop(1, 'rgba(255,255,255,0.0)');
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(px, py, pr, 0, Math.PI * 2);
    ctx.fill();
  }

  // Apply subtle FBM noise to alpha for irregular, more realistic edges
  const img = ctx.getImageData(0, 0, size, size);
  const data = img.data;
  const seed = 1337;
  for (let y = 0; y < size; y++) {
    for (let x = 0; x < size; x++) {
      const idx = (y * size + x) * 4;
      const a = data[idx + 3] / 255; // base alpha from puffs
      if (a <= 0) continue;
      // FBM noise in [0..1]
      const nx = x / size;
      const ny = y / size;
      const n = fbm2(nx, ny, 8.0, 3, 2.0, 0.5, seed) * 0.5 + 0.5;
      // Edge breakup and slight interior variation
      let alpha = a * (0.78 + 0.35 * n);
      // Gentle bottom shading (darker underside)
      const shade = 0.90 + 0.10 * (1.0 - ny); // 1.0 at top -> 0.90 at bottom
      data[idx]   = Math.min(255, data[idx] * shade);
      data[idx+1] = Math.min(255, data[idx+1] * shade);
      data[idx+2] = Math.min(255, data[idx+2] * shade);
      // Contrast alpha for crisper silhouettes
      alpha = Math.pow(alpha, 0.85);
      // Hard clip tiny alphas to help alphaTest (reduces overdraw)
      if (alpha < 0.02) alpha = 0;
      data[idx + 3] = Math.round(alpha * 255);
    }
  }
  ctx.putImageData(img, 0, 0);

  const tex = new THREE.CanvasTexture(canvas);
  tex.generateMipmaps = true;
  tex.anisotropy = 2;
  tex.minFilter = THREE.LinearMipmapLinearFilter;
  tex.magFilter = THREE.LinearFilter;
  return tex;
}

export function setupClouds() {
  if (!CLOUDS.enabled) return;
  // Create shared texture
  const tex = makeCloudTexture(256, 12);
  if (!tex) return;

  // Wind vector
  const ang = THREE.MathUtils.degToRad(CLOUDS.windDeg || 0);
  const wind = new THREE.Vector3(Math.cos(ang), 0, Math.sin(ang));

  for (let i = 0; i < CLOUDS.count; i++) {
    const mat = new THREE.SpriteMaterial({
      map: tex,
      color: new THREE.Color(CLOUDS.colorDay),
      transparent: true,
      opacity: CLOUDS.opacityDay,
      alphaTest: 0.03, // discard near-transparent pixels, reduces fill cost
      depthTest: true,
      depthWrite: false,
      fog: false
    });
    const sp = new THREE.Sprite(mat);
    // Randomize in-texture rotation for variety without extra draw cost
    sp.material.rotation = Math.random() * Math.PI * 2;
    const size = THREE.MathUtils.lerp(CLOUDS.sizeMin, CLOUDS.sizeMax, Math.random());
    sp.scale.set(size, size * 0.6, 1); // a bit flattened
    sp.castShadow = false; sp.receiveShadow = false;
    
    // Position in ring
    const t = Math.random() * Math.PI * 2;
    const r = CLOUDS.radius * (0.6 + Math.random() * 0.4);
    sp.position.set(Math.cos(t) * r, CLOUDS.height + (Math.random() - 0.5) * 10, Math.sin(t) * r);
    sp.renderOrder = 0;
    G.scene.add(sp);

    const speed = CLOUDS.speed * (0.6 + Math.random() * 0.8);
    G.clouds.push({ sprite: sp, speed, wind: wind.clone(), size });
  }
}

export function updateClouds(delta) {
  if (!CLOUDS.enabled || G.clouds.length === 0) return;

  // Day factor for opacity/color blending
  const dayF = 0.5 - 0.5 * Math.cos(2 * Math.PI * (G.timeOfDay || 0));

  for (const c of G.clouds) {
    // Drift
    c.sprite.position.addScaledVector(c.wind, c.speed * delta);

    // Wrap around ring bounds
    const p = c.sprite.position;
    const r = Math.hypot(p.x, p.z);
    const minR = CLOUDS.radius * 0.5;
    const maxR = CLOUDS.radius * 1.1;
    if (r < minR || r > maxR) {
      // Reposition opposite side keeping height
      const ang = Math.atan2(p.z, p.x) + Math.PI;
      p.x = Math.cos(ang) * CLOUDS.radius;
      p.z = Math.sin(ang) * CLOUDS.radius;
    }

    // Blend opacity and color via day/night
    const op = CLOUDS.opacityNight * (1 - dayF) + CLOUDS.opacityDay * dayF;
    c.sprite.material.opacity = op;
    TMP_COLOR.copy(COLOR_N).lerp(COLOR_D, dayF);
    c.sprite.material.color.copy(TMP_COLOR);
  }
}