Codex CLI
Initial commit: Orcs In The Forest
1390db3
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);
}
}