Models 44

Claude Haiku 5.5

by AnthropicReasoningClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
7 min 8 s
Cost
$0.051
Output tokens
101,990
Spent thinking
88,999 tokens
Speed
239.5 tok/s
Console errors
0
First token after
2.1 s
First visible text after
6 min 32 s
Prompt tokens
855
Code size
24.4 KB
Finish reason
stop
Served by
Claude Platform on AWS
Model id
anthropic/claude-haiku-5.5
Reasoning sent
{"effort":"xhigh"}
Max output
128,000 tokens
Generated
Nov 10, 2026

What it was asked

The prompt
Build a low-poly 3D scene of a campfire in a forest clearing at night, with Three.js in a single HTML file. Load Three.js only through this import map (do not use any other URL or version): <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script> Requirements: - A ground with a few low-poly rocks and logs around a campfire in the center. - At least 30 low-poly trees placed randomly around the clearing (not on the fire). - The campfire has animated flames and a warm point light that flickers and casts shadows. - Sparks rising from the fire as a simple particle system. - A dark night sky with stars and a moon, plus fog that fades the distant trees. - OrbitControls to look around, with limits so the camera stays above the ground. - The renderer fills the window and handles resizing correctly.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 653 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Low-Poly Campfire at Night</title>
<style>
  html, body { margin: 0; height: 100%; overflow: hidden; background: #0a0f1d; }
  canvas { position: fixed; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; }
  .hud {
    position: fixed; z-index: 2; pointer-events: none; color: #f0e7d2;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    text-shadow: 0 1px 10px rgba(0, 0, 0, 0.8);
  }
  #title { top: 18px; left: 22px; }
  #title h1 { margin: 0; font-size: 18px; font-weight: 600; letter-spacing: 0.02em; }
  #title p { margin: 4px 0 0; font-size: 13px; opacity: 0.7; }
  #hint { left: 0; right: 0; bottom: 16px; text-align: center; font-size: 12px; opacity: 0.6; }
  @media (max-width: 480px) {
    #title { top: 12px; left: 14px; right: 14px; }
    #title h1 { font-size: 16px; }
    #title p { font-size: 12px; }
    #hint { font-size: 11px; bottom: 10px; }
  }
</style>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>
</head>
<body>
<div class="hud" id="title"><h1>Campfire in the Clearing</h1><p>Low-poly night &middot; Three.js</p></div>
<div class="hud" id="hint">Drag to orbit &middot; Scroll or pinch to zoom</div>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/* ============================================================
   Helpers (seeded random so every load looks the same)
   ============================================================ */
let seed = 20240611;
function rand() {
  seed = (seed + 0x6d2b79f5) | 0;
  let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
  t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
  return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}
const rr = (a, b) => a + (b - a) * rand();
const pick = (arr) => arr[Math.floor(rand() * arr.length)];
const clamp = (x, a, b) => Math.min(b, Math.max(a, x));
const smooth = (a, b, x) => { const t = clamp((x - a) / (b - a), 0, 1); return t * t * (3 - 2 * t); };

const UP = new THREE.Vector3(0, 1, 0);
const CAM_START = new THREE.Vector3(7, 2.6, 9);
const TARGET = new THREE.Vector3(0, 2.0, 0);
const MOON_DIR = new THREE.Vector3(-0.77, 0.15, -0.62).normalize();

// Flattened clearing in the middle, rolling forest floor outside.
function groundHeight(x, z) {
  const d = Math.hypot(x, z);
  const hills =
    Math.sin(x * 0.061 + 1.1) * 1.0 +
    Math.cos(z * 0.079 - 0.4) * 0.85 +
    Math.sin((x * 0.7 - z * 0.45) * 0.19) * 0.45 +
    Math.cos((x + z) * 0.33) * 0.16;
  return hills * smooth(6, 15, d);
}

// Turns an indexed low-poly shape into an irregular one, keeping shared corners together.
function jitter(geo, amt) {
  const p = geo.attributes.position;
  const seen = new Map();
  for (let i = 0; i < p.count; i++) {
    const key = p.getX(i).toFixed(4) + '|' + p.getY(i).toFixed(4) + '|' + p.getZ(i).toFixed(4);
    let o = seen.get(key);
    if (!o) { o = [rr(-amt, amt), rr(-amt, amt), rr(-amt, amt)]; seen.set(key, o); }
    p.setXYZ(i, p.getX(i) + o[0], p.getY(i) + o[1], p.getZ(i) + o[2]);
  }
  p.needsUpdate = true;
  geo.computeVertexNormals();
  return geo;
}

function radialTexture(stops) {
  const c = document.createElement('canvas');
  c.width = c.height = 128;
  const g = c.getContext('2d');
  const grd = g.createRadialGradient(64, 64, 0, 64, 64, 64);
  stops.forEach(([o, col]) => grd.addColorStop(o, col));
  g.fillStyle = grd;
  g.fillRect(0, 0, 128, 128);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}
const glowTex = radialTexture([
  [0, 'rgba(255,255,255,1)'], [0.2, 'rgba(255,255,255,0.7)'],
  [0.5, 'rgba(255,255,255,0.18)'], [1, 'rgba(255,255,255,0)']
]);

/* ============================================================
   Renderer, scene, camera, controls
   ============================================================ */
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;
renderer.toneMapping = THREE.NoToneMapping;
document.body.prepend(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x161e33, 14, 76);

const camera = new THREE.PerspectiveCamera(50, 1, 0.1, 500);
camera.position.copy(CAM_START);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.copy(TARGET);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 4.5;
controls.maxDistance = 34;
controls.minPolarAngle = Math.PI * 0.12;
controls.maxPolarAngle = Math.PI * 0.47;   // never dips under the horizon
controls.autoRotate = true;
controls.autoRotateSpeed = 0.35;
controls.addEventListener('start', () => { controls.autoRotate = false; });
controls.update();

function onResize() {
  const w = window.innerWidth, h = window.innerHeight;
  renderer.setSize(w, h);
  camera.aspect = w / h;
  camera.fov = camera.aspect < 0.9 ? 66 : 50;   // a little wider on portrait phones
  camera.updateProjectionMatrix();
}
window.addEventListener('resize', onResize);
onResize();

/* ============================================================
   Sky dome, stars, moon
   ============================================================ */
const sky = new THREE.Mesh(
  new THREE.SphereGeometry(380, 32, 16),
  new THREE.ShaderMaterial({
    side: THREE.BackSide,
    depthWrite: false,
    vertexShader: `
      varying vec3 vDir;
      void main() {
        vDir = normalize(position);
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }`,
    fragmentShader: `
      varying vec3 vDir;
      void main() {
        vec3 horizon = vec3(0.086, 0.118, 0.200);   // identical to fog colour
        vec3 zenith  = vec3(0.006, 0.010, 0.030);
        float t = smoothstep(0.0, 0.6, vDir.y);
        gl_FragColor = vec4(mix(horizon, zenith, t), 1.0);
      }`,
  })
);
sky.renderOrder = -10;
scene.add(sky);

function randomDir() {
  const u = rand() * 2 - 1;
  const p = rand() * Math.PI * 2;
  const s = Math.sqrt(1 - u * u);
  return new THREE.Vector3(s * Math.cos(p), u, s * Math.sin(p));
}
const MILKY_N = new THREE.Vector3(0.45, 0.85, 0.3).normalize();

function starGeometry(count, min, max, band) {
  const pos = new Float32Array(count * 3);
  const col = new Float32Array(count * 3);
  let n = 0;
  for (let guard = 0; n < count && guard < count * 200; guard++) {
    const d = randomDir();
    if (d.y < 0.03) continue;
    if (band && Math.abs(d.dot(MILKY_N)) > band) continue;
    pos[n * 3] = d.x * 360; pos[n * 3 + 1] = d.y * 360; pos[n * 3 + 2] = d.z * 360;
    const b = rr(min, max);
    const warm = rand() < 0.2;
    col[n * 3]     = warm ? b : b * 0.85;
    col[n * 3 + 1] = warm ? b * 0.9 : b * 0.92;
    col[n * 3 + 2] = warm ? b * 0.7 : b;
    n++;
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  g.setAttribute('color', new THREE.BufferAttribute(col, 3));
  g.setDrawRange(0, n);
  return g;
}
const starMat = (size) => new THREE.PointsMaterial({
  size, sizeAttenuation: false, vertexColors: true, fog: false, depthWrite: false
});
const starsDim = new THREE.Points(starGeometry(1100, 0.25, 0.7, 0), starMat(1.5));
const starsMilky = new THREE.Points(starGeometry(1800, 0.1, 0.38, 0.22), starMat(1.2));
const brightGeo = starGeometry(80, 0.8, 1.0, 0);
const brightBase = brightGeo.attributes.color.array.slice();
const starsBright = new THREE.Points(brightGeo, starMat(2.6));
[starsDim, starsMilky, starsBright].forEach((s) => { s.renderOrder = -5; scene.add(s); });

// Low-poly moon: flat-shaded faces with a few darker "maria"
const moonGeo = new THREE.IcosahedronGeometry(9, 1).toNonIndexed();
{
  const p = moonGeo.attributes.position;
  const cols = new Float32Array(p.count * 3);
  for (let f = 0; f < p.count; f += 3) {
    let s = rr(0.82, 1.0);
    if (rand() < 0.3) s *= rr(0.62, 0.8);
    for (let j = 0; j < 3; j++) {
      cols[(f + j) * 3] = s;
      cols[(f + j) * 3 + 1] = s * 0.98;
      cols[(f + j) * 3 + 2] = s * 0.93;
    }
  }
  moonGeo.setAttribute('color', new THREE.BufferAttribute(cols, 3));
}
const moon = new THREE.Mesh(moonGeo, new THREE.MeshBasicMaterial({ vertexColors: true, fog: false }));
const MOON_POS = MOON_DIR.clone().multiplyScalar(330);
moon.position.copy(MOON_POS);
scene.add(moon);

const haloTex = radialTexture([
  [0, 'rgba(255,255,255,1)'], [0.18, 'rgba(255,255,255,0.55)'],
  [0.5, 'rgba(255,255,255,0.12)'], [1, 'rgba(255,255,255,0)']
]);
function addHalo(size, color, opacity) {
  const s = new THREE.Sprite(new THREE.SpriteMaterial({
    map: haloTex, color, opacity, transparent: true, depthWrite: false,
    blending: THREE.AdditiveBlending, fog: false
  }));
  s.scale.set(size, size, 1);
  s.position.copy(MOON_POS);
  scene.add(s);
}
addHalo(70, 0x42557f, 0.55);
addHalo(26, 0xcdd9ff, 0.8);

/* ============================================================
   Lights: cold moonlight + flickering warm fire light with shadows
   ============================================================ */
scene.add(new THREE.HemisphereLight(0x7f95d6, 0x2a2015, 2.8));

const moonLight = new THREE.DirectionalLight(0xb4c8ff, 2.0);
moonLight.position.copy(MOON_DIR).multiplyScalar(80);
moonLight.target.position.set(0, 0, 0);
scene.add(moonLight, moonLight.target);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(2048, 2048);
{
  const sc = moonLight.shadow.camera;
  sc.left = -56; sc.right = 56; sc.top = 56; sc.bottom = -56;
  sc.near = 5; sc.far = 200;
  sc.updateProjectionMatrix();
}
moonLight.shadow.bias = -0.0004;
moonLight.shadow.normalBias = 0.03;

const fireLight = new THREE.PointLight(0xff8a3c, 26, 26, 2);
fireLight.position.set(0, 1.0, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(512, 512);
fireLight.shadow.bias = -0.002;
fireLight.shadow.normalBias = 0.03;
fireLight.shadow.radius = 3;
fireLight.shadow.camera.near = 0.1;
fireLight.shadow.camera.far = 26;
fireLight.shadow.camera.updateProjectionMatrix();
scene.add(fireLight);

/* ============================================================
   Ground: flat-shaded, vertex-coloured, flattened clearing
   ============================================================ */
const GROUND = 240, SEG = 96;
const groundGeo = new THREE.PlaneGeometry(GROUND, GROUND, SEG, SEG).rotateX(-Math.PI / 2);
{
  const p = groundGeo.attributes.position;
  for (let i = 0; i < p.count; i++) p.setY(i, groundHeight(p.getX(i), p.getZ(i)));
}
const ground = groundGeo.toNonIndexed();
{
  const p = ground.attributes.position;
  const cols = new Float32Array(p.count * 3);
  const moss = new THREE.Color(0x3a5a36), dark = new THREE.Color(0x2a4128);
  const litter = new THREE.Color(0x4a3b29), ashy = new THREE.Color(0x2b2622);
  const c = new THREE.Color();
  for (let f = 0; f < p.count; f += 3) {
    const cx = (p.getX(f) + p.getX(f + 1) + p.getX(f + 2)) / 3;
    const cz = (p.getZ(f) + p.getZ(f + 1) + p.getZ(f + 2)) / 3;
    const n = Math.sin(cx * 0.13 + cz * 0.07) * 0.6 + Math.cos(cz * 0.21 - cx * 0.05) * 0.4 + (rand() - 0.5) * 0.7;
    c.copy(n > 0.35 ? moss : (n > -0.25 ? dark : litter));
    c.multiplyScalar(rr(0.85, 1.15));
    c.lerp(ashy, 1 - smooth(3.5, 9, Math.hypot(cx, cz)));
    for (let j = 0; j < 3; j++) {
      cols[(f + j) * 3] = c.r;
      cols[(f + j) * 3 + 1] = c.g;
      cols[(f + j) * 3 + 2] = c.b;
    }
  }
  ground.setAttribute('color', new THREE.BufferAttribute(cols, 3));
  ground.computeVertexNormals();
}
const groundMesh = new THREE.Mesh(ground, new THREE.MeshStandardMaterial({
  vertexColors: true, flatShading: true, roughness: 1, metalness: 0
}));
groundMesh.receiveShadow = true;
scene.add(groundMesh);

const ash = new THREE.Mesh(
  new THREE.CircleGeometry(1.3, 14).rotateX(-Math.PI / 2),
  new THREE.MeshStandardMaterial({ color: 0x15120f, roughness: 1 })
);
ash.position.y = 0.015;
ash.receiveShadow = true;
scene.add(ash);

const pool = new THREE.Mesh(
  new THREE.PlaneGeometry(9, 9).rotateX(-Math.PI / 2),
  new THREE.MeshBasicMaterial({
    map: glowTex, color: 0xff7a2a, transparent: true, opacity: 0.45,
    depthWrite: false, blending: THREE.AdditiveBlending
  })
);
pool.position.y = 0.04;
scene.add(pool);

/* ============================================================
   Rocks and logs
   ============================================================ */
const rockGeos = Array.from({ length: 6 }, () => jitter(new THREE.IcosahedronGeometry(1, 0), 0.22));
const rockMats = [0x5b6068, 0x4a4f57, 0x6a6e71].map((c) =>
  new THREE.MeshStandardMaterial({ color: c, roughness: 0.95, flatShading: true }));

function addRock(x, z, sx, sy, sz) {
  const m = new THREE.Mesh(pick(rockGeos), pick(rockMats));
  m.scale.set(sx, sy, sz);
  m.rotation.set(rr(-0.15, 0.15), rr(0, Math.PI * 2), rr(-0.15, 0.15));
  m.position.set(x, groundHeight(x, z) + sy * 0.3, z);
  m.castShadow = true;
  m.receiveShadow = true;
  scene.add(m);
}

// Stone ring around the fire
for (let i = 0; i < 11; i++) {
  const a = (i / 11) * Math.PI * 2 + rr(-0.15, 0.15);
  const r = 1.6 + rr(-0.12, 0.12);
  const s = rr(0.3, 0.42);
  addRock(Math.cos(a) * r, Math.sin(a) * r, s, s * rr(0.6, 0.8), s * rr(0.8, 1.0));
}
// Scattered rocks in the forest
for (let i = 0; i < 16; i++) {
  const a = rand() * Math.PI * 2, r = rr(7.5, 34);
  const s = rr(0.45, 1.3);
  addRock(Math.cos(a) * r, Math.sin(a) * r, s, s * rr(0.5, 0.8), s * rr(0.75, 1.0));
}

const barkMat = new THREE.MeshStandardMaterial({ color: 0x4b3322, roughness: 1, flatShading: true });
const cutMat = new THREE.MeshStandardMaterial({ color: 0xa6805a, roughness: 0.9, flatShading: true });
const logGeo = new THREE.CylinderGeometry(0.15, 0.18, 1, 7, 1);
const logMats = [barkMat, cutMat, cutMat];
const logMeshes = [];

function placeLeaningLog(from, to) {
  const dir = new THREE.Vector3().subVectors(to, from);
  const len = dir.length();
  dir.divideScalar(len);
  const m = new THREE.Mesh(logGeo, logMats);
  m.scale.set(1, len, 1);
  m.position.copy(from).add(to).multiplyScalar(0.5);
  m.quaternion.setFromUnitVectors(UP, dir);
  m.castShadow = true;
  m.receiveShadow = true;
  scene.add(m);
}

// Teepee of logs over the fire
for (let k = 0; k < 4; k++) {
  const a = k * Math.PI / 2 + rr(-0.25, 0.25);
  const from = new THREE.Vector3(Math.cos(a) * 1.05, 0.12, Math.sin(a) * 1.05);
  const to = new THREE.Vector3(-Math.cos(a) * 0.08, 1.0 + rr(-0.05, 0.05), -Math.sin(a) * 0.08);
  placeLeaningLog(from, to);
}

// A few fallen logs lying around the clearing
for (let i = 0; i < 4; i++) {
  const a = (i / 4) * Math.PI * 2 + rr(-0.4, 0.4);
  const d = rr(5.8, 8.5);
  const x = Math.cos(a) * d, z = Math.sin(a) * d;
  const m = new THREE.Mesh(logGeo, logMats);
  m.scale.set(1, rr(2.4, 3.6), 1);
  m.rotation.order = 'YXZ';
  m.rotation.set(0, rand() * Math.PI, Math.PI / 2);
  m.position.set(x, groundHeight(x, z) + 0.17, z);
  m.castShadow = true;
  m.receiveShadow = true;
  scene.add(m);
}

/* ============================================================
   Campfire: flames, glow, embers, sparks
   ============================================================ */
const flameRoot = new THREE.Group();
flameRoot.position.y = 0.12;
scene.add(flameRoot);

const flames = [];
const FLAME_LAYERS = [
  { n: 3, r: 0.42,  h: 1.35, col: 0xff5a1a, op: 0.22, spread: 0.12, sp: 2.4 }, // soft outer glow
  { n: 6, r: 0.30,  h: 1.15, col: 0xff3d0a, op: 0.85, spread: 0.22, sp: 4.2 },
  { n: 7, r: 0.23,  h: 0.95, col: 0xff8a1e, op: 0.90, spread: 0.20, sp: 5.6 },
  { n: 6, r: 0.15,  h: 0.72, col: 0xffc83c, op: 0.95, spread: 0.13, sp: 7.1 },
  { n: 4, r: 0.085, h: 0.46, col: 0xfff2bd, op: 1.00, spread: 0.06, sp: 9.0 },
];
for (const L of FLAME_LAYERS) {
  const geo = new THREE.ConeGeometry(L.r, L.h, 6, 1, true).translate(0, L.h / 2, 0);
  const mat = new THREE.MeshBasicMaterial({
    color: L.col, transparent: true, opacity: L.op, side: THREE.DoubleSide, depthWrite: false
  });
  for (let i = 0; i < L.n; i++) {
    const a = (i / L.n) * Math.PI * 2 + rr(-0.35, 0.35);
    const rad = rr(0, L.spread);
    const m = new THREE.Mesh(geo, mat);
    m.position.set(Math.cos(a) * rad, 0, Math.sin(a) * rad);
    m.userData = {
      bx: m.position.x, bz: m.position.z,
      ph: rand() * Math.PI * 2, sp: L.sp * rr(0.8, 1.25), sc: rr(0.85, 1.15)
    };
    flameRoot.add(m);
    flames.push(m);
  }
}

const fireGlow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: glowTex, color: 0xff7a2a, transparent: true, opacity: 0.8,
  depthWrite: false, blending: THREE.AdditiveBlending, fog: false
}));
fireGlow.scale.set(4.4, 4.4, 1);
fireGlow.position.set(0, 0.7, 0);
scene.add(fireGlow);

const emberGeo = new THREE.IcosahedronGeometry(0.06, 0);
const emberMat = new THREE.MeshBasicMaterial({ color: 0xff6418 });
const embers = [];
for (let i = 0; i < 14; i++) {
  const a = rand() * Math.PI * 2, d = Math.sqrt(rand()) * 0.95;
  const m = new THREE.Mesh(emberGeo, emberMat);
  m.position.set(Math.cos(a) * d, 0.05, Math.sin(a) * d);
  scene.add(m);
  embers.push({ mesh: m, s: rr(0.6, 1.2), ph: rand() * Math.PI * 2 });
}

// Sparks: a simple CPU particle system rising from the fire
const SPARK_N = 240;
const sPos = new Float32Array(SPARK_N * 3);
const sCol = new Float32Array(SPARK_N * 3);
const sVel = new Float32Array(SPARK_N * 3);
const sLife = new Float32Array(SPARK_N);
const sMax = new Float32Array(SPARK_N);
const sPosAttr = new THREE.BufferAttribute(sPos, 3).setUsage(THREE.DynamicDrawUsage);
const sColAttr = new THREE.BufferAttribute(sCol, 3).setUsage(THREE.DynamicDrawUsage);
const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', sPosAttr);
sparkGeo.setAttribute('color', sColAttr);
const sparks = new THREE.Points(sparkGeo, new THREE.PointsMaterial({
  size: 0.16, map: glowTex, vertexColors: true, transparent: true,
  depthWrite: false, blending: THREE.AdditiveBlending
}));
sparks.frustumCulled = false;
scene.add(sparks);

function spawnSpark(i) {
  const k = i * 3;
  const a = rand() * Math.PI * 2, r = Math.sqrt(rand()) * 0.3;
  sPos[k]     = Math.cos(a) * r;
  sPos[k + 1] = 0.5 + rand() * 0.6;
  sPos[k + 2] = Math.sin(a) * r;
  sVel[k]     = (rand() - 0.5) * 0.5;
  sVel[k + 1] = 1.1 + rand() * 1.3;
  sVel[k + 2] = (rand() - 0.5) * 0.5;
  sMax[i] = 1.6 + rand() * 2.6;
  sLife[i] = sMax[i];
}

function updateSparks(t, dt) {
  const wind = 0.12 + 0.08 * Math.sin(t * 0.37);
  for (let i = 0; i < SPARK_N; i++) {
    const k = i * 3;
    sLife[i] -= dt;
    if (sLife[i] <= 0) spawnSpark(i);

    const wob = Math.sin(t * 2.3 + i * 1.91) * 0.35 + Math.sin(t * 5.3 + i * 0.7) * 0.15;
    sVel[k]     += ((wind + wob) - sVel[k] * 0.6) * dt;
    sVel[k + 2] += (Math.cos(t * 1.7 + i * 2.3) * 0.35 - sVel[k + 2] * 0.6) * dt;
    sVel[k + 1] += (0.95 - sVel[k + 1] * 0.6) * dt;   // buoyancy with drag

    sPos[k]     += sVel[k] * dt;
    sPos[k + 1] += sVel[k + 1] * dt;
    sPos[k + 2] += sVel[k + 2] * dt;

    // Colour: hot yellow -> orange -> dark red, fading to black (invisible with additive blending)
    const age = 1 - sLife[i] / sMax[i];
    const fade = Math.pow(1 - age, 1.3) * 1.5;
    let r, g, b;
    if (age < 0.25) {
      const u = age / 0.25;
      r = 1; g = 0.92 - 0.47 * u; b = 0.55 - 0.47 * u;
    } else {
      const u = (age - 0.25) / 0.75;
      r = 1 - 0.45 * u; g = 0.45 - 0.37 * u; b = 0.08 - 0.07 * u;
    }
    sCol[k] = r * fade; sCol[k + 1] = g * fade; sCol[k + 2] = b * fade;
  }
  sPosAttr.needsUpdate = true;
  sColAttr.needsUpdate = true;
}

// Pre-simulate so sparks are already spread out on the first frame
for (let i = 0; i < SPARK_N; i++) spawnSpark(i);
for (let s = 0; s < 160; s++) updateSparks(s * 0.03 - 4.8, 0.03);

/* ============================================================
   Forest: 40+ low-poly trees, plus bushes
   ============================================================ */
const trunkGeo = new THREE.CylinderGeometry(0.11, 0.2, 2.4, 6).translate(0, 1.2, 0);
const coneGeo = new THREE.ConeGeometry(1, 1, 7, 1).translate(0, 0.5, 0);
const blobGeos = [0, 1, 2].map(() => jitter(new THREE.IcosahedronGeometry(1, 0), 0.2));

const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3e2b1d, roughness: 1, flatShading: true });
const leafMat = (c) => new THREE.MeshStandardMaterial({ color: c, roughness: 0.9, metalness: 0, flatShading: true });
const pineMats = [0x1c3b2b, 0x23462f, 0x17322a].map(leafMat);
const leafMats = [0x2c4f2c, 0x37583a, 0x284a2a].map(leafMat);

function buildTree(x, z) {
  const g = new THREE.Group();
  g.add(new THREE.Mesh(trunkGeo, trunkMat));
  if (rand() < 0.6) {
    // Pine: three stacked cones
    const mat = pick(pineMats);
    for (const [base, r, h] of [[0.9, 1.6, 2.2], [1.9, 1.25, 2.0], [2.9, 0.9, 1.7]]) {
      const m = new THREE.Mesh(coneGeo, mat);
      m.position.y = base;
      m.scale.set(r, h, r);
      m.rotation.y = rand() * Math.PI;
      g.add(m);
    }
  } else {
    // Round deciduous: a cluster of jittered icosahedrons
    const mat = pick(leafMats);
    for (const [bx, by, bz, s] of [[0, 3.0, 0, 1.5], [0.85, 2.5, 0.35, 1.05], [-0.7, 2.35, -0.45, 1.0], [0.1, 3.9, 0.1, 1.0]]) {
      const m = new THREE.Mesh(pick(blobGeos), mat);
      m.position.set(bx, by, bz);
      m.scale.setScalar(s);
      m.rotation.set(rand(), rand(), rand());
      g.add(m);
    }
  }
  g.traverse((o) => { if (o.isMesh) { o.castShadow = true; o.receiveShadow = true; } });
  g.position.set(x, groundHeight(x, z) - 0.1, z);
  g.rotation.y = rand() * Math.PI * 2;
  const s = rr(0.8, 1.3);
  g.scale.set(s, s * rr(0.9, 1.15), s);
  scene.add(g);
}

const CAM_AZ = Math.atan2(CAM_START.z, CAM_START.x);
const placed = [];
for (let tries = 0; tries < 6000 && placed.length < 44; tries++) {
  const r = Math.sqrt(rand()) * 34 + 10;          // 10 .. 44 m from the fire
  const a = rand() * Math.PI * 2;
  const x = Math.cos(a) * r, z = Math.sin(a) * r;
  const dAz = Math.abs(Math.atan2(Math.sin(a - CAM_AZ), Math.cos(a - CAM_AZ)));
  if (r < 18 && dAz < 0.3) continue;              // keep the fire unobstructed from the start view
  if (Math.hypot(x - CAM_START.x, z - CAM_START.z) < 7) continue;
  if (placed.some((p) => Math.hypot(p.x - x, p.z - z) < 2.8)) continue;
  placed.push({ x, z });
  buildTree(x, z);
}

for (let i = 0; i < 18; i++) {
  const a = rand() * Math.PI * 2, r = rr(9, 30);
  const x = Math.cos(a) * r, z = Math.sin(a) * r;
  const s = rr(0.7, 1.5);
  const b = new THREE.Mesh(pick(blobGeos), pick(leafMats));
  b.position.set(x, groundHeight(x, z) + 0.35 * s, z);
  b.scale.set(s, s * 0.7, s);
  b.rotation.set(rand(), rand(), rand());
  b.castShadow = true;
  b.receiveShadow = true;
  scene.add(b);
}

/* ============================================================
   Animation
   ============================================================ */
const T0 = performance.now();
let last = T0;

function frame() {
  const now = performance.now();
  const t = (now - T0) / 1000;
  const dt = Math.min(0.05, (now - last) / 1000);
  last = now;

  // Flames: each tongue breathes, sways and jitters on its own phase
  for (const m of flames) {
    const u = m.userData;
    const a = Math.sin(t * u.sp + u.ph);
    const b = Math.sin(t * u.sp * 1.73 + u.ph * 2.1);
    const c = Math.sin(t * u.sp * 3.1 + u.ph * 0.7);
    const hgt = 0.92 + 0.2 * a + 0.08 * b + 0.04 * c;
    m.scale.set(u.sc * (1 - 0.07 * a), u.sc * hgt, u.sc * (1 - 0.07 * a));
    m.rotation.set(0.07 * c, 0, 0.1 + 0.1 * b);
    m.position.set(u.bx + 0.025 * c, 0, u.bz + 0.025 * b);
  }

  // Fire light flicker: light, glow and ground pool share one noise signal
  const flick = 0.55 * Math.sin(t * 9.1) + 0.3 * Math.sin(t * 17.3 + 1.3) + 0.15 * Math.sin(t * 33.7 + 0.4);
  fireLight.intensity = 26 * (1 + 0.1 * flick);
  fireGlow.material.opacity = 0.75 + 0.12 * flick;
  pool.material.opacity = 0.42 + 0.06 * flick;

  for (const e of embers) e.mesh.scale.setScalar(e.s * (0.75 + 0.35 * Math.sin(t * 5 + e.ph)));

  updateSparks(t, dt);

  // Gentle twinkle on the brightest stars
  const bcol = brightGeo.attributes.color;
  for (let i = 0; i < bcol.count; i++) {
    const f = 0.72 + 0.28 * Math.sin(t * (1.1 + (i % 7) * 0.27) + i * 1.9);
    bcol.array[i * 3]     = brightBase[i * 3] * f;
    bcol.array[i * 3 + 1] = brightBase[i * 3 + 1] * f;
    bcol.array[i * 3 + 2] = brightBase[i * 3 + 2] * f;
  }
  bcol.needsUpdate = true;

  controls.update();
  // Never let the camera sink into the terrain (hills far out)
  const floor = groundHeight(camera.position.x, camera.position.z) + 1.2;
  if (camera.position.y < floor) camera.position.y = floor;

  renderer.render(scene, camera);
}

renderer.setAnimationLoop(frame);
</script>
</body>
</html>
Reasoning trace (88,999 tokens)
I'm working out the setup for a single self-contained HTML file that loads Three.js 0.170.0 through an import map, with OrbitControls and no console errors. The scene needs a ground, low-poly rocks and logs around a central campfire, and at least 30 low-poly trees scattered around the clearing that aren't on fire. I'm also planning an animated campfire flame.

I'm laying out the rest of the requirements: a warm campfire light that flickers and casts shadows, rising spark particles, a night sky with stars, a moon, and fog that fades out distant trees, plus orbit controls that keep the camera above the ground and a renderer that resizes with the window. I'm also deciding on a small title overlay with a drag-and-scroll hint instead of any click-to-start step, since the scene should animate on its own.

Next I'm settling the lighting: the moon gets a directional light with shadows, and the fire gets a point light that casts shadows too, at a modest map size so it stays cheap enough for the 30-plus trees and the rocks. For the flame itself I'm weighing a group of low-poly cones animated each frame against a vertex-shader approach, leaning toward the cones for simplicity.

I'm settling on each flame layer as a low-poly cone with its base anchored at zero, and animating its height, width, position, and tilt with sine-based noise each frame. Using basic materials keeps the flames glowing regardless of the lights, and the outer, middle, and inner layers each get a warmer color.

Next I'm settling the flame itself: each tongue will be a low-poly cone with its base at zero, pulsing in height with a per-tongue phase and wobbling slightly at the tip. I'm also adding a soft additive glow behind the fire, built from a procedural canvas gradient so nothing external is needed.

Now I'm settling the sparks: a points system of about 200 particles that rise with a bit of wobble and respawn at the fire's base. I'm leaning toward a round glow texture for the points so they don't render as square dots, and fading their color from yellow to red as they age.

Next I'm working out the ground: a large plane with rolling low-poly hills, flattened in a ring around the fire so the clearing stays level, with trees kept outside that radius.

I'm working out how to build the ground: a large plane with its height set per vertex, converted to non-indexed geometry so each triangle gets its own flat-shaded normal and a random green or dark-brown color.

Next I'm settling the height function for the ground: a sum of sines and cosines, flattened with a smoothstep so the area around the fire stays level. Rocks and logs will sit on that same height, and trees will stay outside the clearing.

I'm sizing the ground at 160 units with 64 segments, which works out to about 8,000 triangles, and then I'm setting the fog to match the dark sky. The night backdrop will be a large inverted sphere with a gradient shader, plus a star field and a moon with a soft glow.

Next I'm settling the stars: two point layers, one with smaller and one with larger stars, using vertex colors for brightness so I don't need a custom shader. I'm placing them mostly above the horizon, with some variation, and making sure they don't bleed through the fog.

I'm placing the moon up in the sky, far outside the fog, with a small low-poly sphere and a halo around it. The stars and the sky backdrop also stay clear of the fog so they don't fade with the trees.

I'm checking where the moon sits in the sky relative to the camera's view direction, using a normalized direction that puts it up and behind the scene.

I'm checking where the moon sits against the camera's forward direction, and I'm settling on a direction that puts it up and off to the upper left of the view.

I'm checking the moon's direction against the camera's view. Its elevation works out to about 27° above the horizon, which is roughly 34° above the camera's centre line, so it would sit outside the vertical field of view. I need to lower it or tilt it toward the view.

I'm settling the moon's position at about 11° above the horizon, which puts it in the upper part of the view, a little high for the camera's current pitch. I'm checking whether that still sits within the vertical field of view, and I'm weighing whether it's worth nudging toward the upper-left, since OrbitControls lets the user rotate anyway and the initial screenshot is what matters.

I'm settling the camera at a lower height, around (10, 3.2, 13), aimed at a point just above the fire so the moon falls in the upper part of the view. I'm checking the pitch and the moon's direction against that view, adjusting its elevation to about 14° so it stays inside the vertical field of view.

I'm checking the moon's screen position by taking the dot product of its direction with the camera's right vector, which comes out to about -0.4, so it sits left of centre.

I'm checking the moon's screen position: its direction against the camera's right vector gives a value near zero, so it sits close to horizontal centre. Its elevation of about 14° puts it roughly 20° above the view centre, which is near the top of the frame but still inside it.

I'm allowing the moon to sit a bit high in frame, since the screenshot is taken from the initial view. I'll try an elevation of about 10° with fov 55, and if it still reads as too high, I'll aim the camera slightly upward so the composition is better.

I'm checking the moon's screen position against the trees: with the camera aimed slightly upward, the moon should sit around three-quarters of the way up the frame, above the treeline at roughly 60% height. I'm also planning a halo sprite around the moon and a sky sphere of radius 380.

I'm setting the sky's gradient from dark navy at the top to a slightly lighter purple-blue at the horizon, and matching the fog colour to that horizon so distant trees fade into it. The camera's far plane is 500, which comfortably clears the 380-radius sky sphere, and the moon sits at radius 320.

I'm setting up the trees as pine-style stacks of cones or rounder icosahedron foliage, around forty of them, placed at random radii between 11 and 42 with a minimum spacing so they don't overlap. I'll use a seeded random generator so the layout stays the same for screenshots, and I'm also considering whether the trees should cast shadows from the moonlight.

I'm setting the lighting: a dim blue hemisphere light for the sky and ground, a pale blue moonlight directional light with shadows, and a warm flickering point light at the fire with physically based decay.

I'm settling the fire light as a warm point light at (0, 0.8, 0) with intensity 30, a 28 range, and decay 2, which should look bright close in and fall off quickly with distance.

I'm settling the lighting levels now: the moonlight at about 0.6 in a bluish tone and the hemisphere light at 0.35. Next I'm making the campfire flicker by modulating its intensity with layered sine noise, and enabling shadows on the fire's point light with a 512 shadow map and a small negative bias and normal bias to avoid acne.

I'm settling on PCFShadowMap rather than the soft variant to avoid deprecation issues, since the shadow radius should still work with it. I'm also planning to keep the flame meshes out of the shadow pass, while the logs and rocks will cast shadows.

I'm looking at the renderer settings now: antialiasing on, pixel ratio capped at 2, and ACES filmic tone mapping at about 1.1 exposure. I'm checking how that affects the bright orange flame colour, since tone mapping will wash out the brightest values a little.

I'm checking that ShaderMaterial won't get tone mapping, since my custom sky shader doesn't include the tonemapping chunk, while the stars' PointsMaterial still will. I'm also weighing the colour space issue: uniform colours passed to the sky shader come through in linear space, so the output could look darker than intended unless I handle it.

I'm weighing the sky colours next: sRGB values written straight into the shader, so they appear as designed rather than getting converted, while the fog colour goes through Three's linear pipeline.

I'm writing the sky shader's gradient so it runs from a dark blue-purple at the horizon to a deeper navy overhead, with the colours given as raw values so they match the fog hex directly.

I'm checking how the fog falloff lines up with the sky dome: past about 90 units everything is fully fog-coloured, so the ground edge around 80 units out should blend into the horizon colour.

I'm checking the ground size and fog distance so the terrain edge fades fully into the horizon, and I'm settling on a ground plane of about 240 units with roughly 3-metre cells. I'm also keeping the clearing flat near the centre, with the hills rising only about 1.5 metres and the trees starting at radius 11.

I'm settling the fog as a range from 16 to 78 units, which keeps the nearby campfire area clear while distant trees fade into the horizon colour. I'm also setting the flat clearing around the fire, with the hills only rising beyond about 5 units out, and I'm starting to lay out the campfire's stone ring of low-poly rocks.

I'm adding the campfire's stone ring of dark grey low-poly rocks around the fire, then the logs: three or four leaning inward in a teepee, plus a couple of lying logs further out in the clearing. For the embers I'm considering small glowing orange blobs at the base.

Still laying out the flame geometry: about fourteen tongues grouped near y = 0.3, each a five-sided cone with its base at the origin, colored from deep orange-red on the outside through to a pale yellow-white core. For the animation I'm driving each tongue's vertical scale with two sine waves at different speeds and phases, so the flames flicker out of step with one another.

I'm adding the flame's sway and tilt to each tongue, plus a translucent outer sheath for glow, and I'm considering a soft radial glow sprite at the fire's centre.

I'm setting the teepee logs around the fire: each one starts at a base point about 0.7 units out on the ground and leans in toward the centre, with its orientation worked out from the direction between base and top.

I'm placing the stone ring of rocks at a radius of about 1.6, using icosahedron shapes with jittered vertices, flat shading, and a squashed vertical scale. The logs are cylinders scaled to their length and oriented along the direction from base to top.

Now I'm placing the scattered rocks and the lying logs across the clearing, then adding a stump or two.

Still writing the tree foliage... Adding pine and deciduous variants...

I'm settling the tree placement now: two or three shared icosahedron variants with jittered vertices, a minimum radius of about 9 from the fire, and roughly 2.8 units between trees. I'm aiming for around 42 trees, sampling until I reach that count.

I'm setting the moon's directional light up with a far-away shadow camera and a 2048 shadow map, so the trees cast shadows toward the camera while their lit sides face the moon.

I'm placing the point light for the fire at about (0, 1.4, 0), just above the flames, with a 512 shadow map. Its shadows come from the logs, rocks and nearby trees, and the flame meshes are kept out of the shadow pass. I'm also keeping casting limited to the trees, logs and rocks to control the number of render passes.

Still writing the sparks... Adding the particle system for the embers.

Still writing the sparks... Setting each particle's turbulence and fade, with a glow texture as the point sprite.

I'm setting the sparks to additive points with a glow texture, depth writes off, and a size around 0.2 so they show up clearly against the night sky.

I'm settling the camera controls now: damping on for smoother orbiting, a distance range of about 4 to 40, and a max polar angle just short of the horizon so the camera stays above the ground. I'm also considering a small clamp in the animation loop as a backstop.

I'm weighing whether a clamp on the camera's height is needed, since the damping in OrbitControls might fight it, and the hills could still put the camera below ground at the far end of its range, so I'll add a small height floor based on the terrain.

I'm setting the camera's polar limit just under the horizon and the distance range to about 5 to 45, with panning turned off so the fire stays centred. I'm also weighing a plain window resize handler for the renderer and camera, and a subtle overlay with a title and a short control hint.

I'm checking mobile layout next: the overlay text needs to stay small and not block touch input, and the canvas should handle touch orbiting without issues. I'm also making sure the Three.js APIs I plan to use, like the canvas texture with its colour space, are used correctly so there are no console errors.

I'm adding a faint starfield over the night sky: about 1,400 small points scattered across the upper hemisphere, tinted slightly blue and white, plus a sparser set of larger stars. I'm using unattenuated point sizes so the stars stay crisp regardless of camera distance.

I'm checking how Three.js scales point sizes when sizeAttenuation is off, and I believe the renderer multiplies the material size by the pixel ratio, so the stars should stay crisp on high-DPI screens. I'm also making sure the sky sphere draws first with depth writes off, so the stars sit behind everything else.

I'm working out the moon's look now: a pale sphere with low-poly faces, and some darker grey patches on the faces to suggest craters. I'm using a flat-looking icosahedron-style shape rather than a smooth sphere so it matches the rest of the scene.

I'm giving the moon a low-poly look with random grey shades on each face, then adding a halo sprite around it.

I'm checking the moon's apparent size against the camera's field of view, which works out to a few degrees, so the halo sprite needs to be large relative to it. I'm also making sure the star field, sky, moon halo, tree fog and ground fog are set up consistently with how fog should affect each one.

Still writing the fire glow and ground light... Adding the flickering glow sprite and the warm light pool on the ground.

Still working through the embers and ground details, adding the ash disk at the centre.

I'm setting the ground's vertex colours to a dark grass green with some random jitter, and mixing in brownish leaf-litter patches. Since the scene is lit at night, the ground will read as dark, using a flat-shaded standard material.

Still writing the page markup and styles... Adding the HUD overlay.

Still writing the page styles and the HUD overlay...

Still writing the page script... Adding the RNG and scene setup.

Still writing the scene code... Setting up the renderer and the random number generator.

Still writing the scene code... Setting up the sky dome shader.

Still writing the sky shader... Setting the horizon colour to match the fog so the dome blends into the distance.

I'm checking how the sky's fog colour interacts with tone mapping, since ACES runs after fog and could shift the colour away from the dark blue I want at the horizon.

I'm checking whether tone mapping is shifting the fog colour, since ACES runs after fog and would make the distant trees tint differently from the sky at the horizon. Switching tone mapping off would keep the two matched, and I'd tune the light intensities by hand instead, watching that the bright flame materials don't clip.

I'm going to skip tone mapping entirely so the fog and sky colours stay matched, and lean on moderate light intensities instead. The star and moon materials don't need tone mapping either. I still need to get the sky dome's depth ordering right for the first frame.

Still writing the star layers... Setting up the random star positions and brightness on the sky dome.

Still writing the star colours... Tinting each point between white and a faint blue or warm shade.

Still writing the star colours... Tinting the points and setting up the point material.

Still writing the moon... Shading the low-poly faces with random grey tones and darker patches for craters.

Still writing the moon's radial halo texture... Setting up the glow gradient stops.

Still writing the lights... Setting up the hemisphere light for the night sky.

Still writing the moon's lighting... Setting up the hemisphere light for the night sky and a dim blue moonlight.

Still working out the ground colours: I'm going to build them with THREE.Color objects, since those convert the hex values to linear space automatically and that's what the vertex attribute needs.

I'm settling on the ground's brightness now: the dark green base will look almost black under moonlight alone, so I'll raise the moonlight intensity and let the campfire glow lift it further.

Still working out the fire's point light: I'm tuning its colour and intensity so it lights the ground and trunks without blowing out the flame materials.

Still working out the fire light: I'm sizing the point light's intensity so the ground and trunks near the campfire glow warm orange, while the light fades out over a few metres.

I'm settling on the point light's shadow setup: a 512 map with near 0.1 and far 25 to match the light's distance, plus a small negative bias and a normal bias. I'm also checking whether the soft-edge radius actually takes effect with the basic PCF shadow type.

Still working out the shadow settings: I'm checking whether PCF soft shadows honour the radius, and I'm leaning towards PCFSoftShadowMap for the cleaner edges.

Checking whether the directional light's target needs to be in the scene for its shadow to update correctly, since the default target sits at the origin anyway.

Still writing the ground height function... Adding the rolling hills that fade out towards the edge of the clearing.

Still writing the ground height function... Adding the hill scale and the flat clearing around the fire.

Still writing the ground height function... Rotating the plane so its vertices lie flat, then raising each one by its height value.

Still writing the ground mesh... Colouring each triangle with a mix of grass greens, dirt and moss.

Still writing the ground colours... Blending in dirt tones near the fire.

Still writing the ground colours... Assigning each triangle a shade of grass, dirt or moss, with the dirt blending in near the fire and a little brightness jitter per face.

Still writing the ground material... Adding the flat-shaded vertex-coloured standard material and the ash disk under the fire.

Still writing the rock geometry... Jittering the icosahedron vertices so each rock has an irregular, faceted shape.

Still writing the rock geometry... Jittering the vertices so each rock looks irregular, and checking whether the duplicated corners will open up cracks between faces.

Still writing the rock geometry... Jittering each shared vertex once by its position, so neighbouring faces stay joined and no gaps open up.

Still writing the ground jitter... Keying each shared vertex so neighbouring faces move together.

Still writing the rock geometry... Jittering each rock's shared vertices together, then spacing eleven of them around the fire with varied scales.

Still writing the rock and log props... Jittering the rock vertices, then placing the ring of rocks around the fire and the scattered ones in the clearing.

Still writing the log teepee... Angling four logs against the central ring, with a lighter cut end on each.

Still writing the logs... Setting each teepee log to lean from the ground ring in towards a point near the top, with a little random variation.

Still writing the log teepee... Working out the rotation for the fallen logs: I'll set the order to 'YXZ' so the log lies flat along X and then turns around the vertical axis.

Still working out the fallen logs: I'm placing them on the ground about five to seven units from the fire, with their centres kept clear of the clearing.

Still writing the tree placement... Picking random points across the clearing and keeping each tree at least a few units from its neighbours.

Still writing the tree geometry... Building the pine trees from a trunk and three stacked cone tiers that shrink as they rise.

Still writing the tree tiers... Stacking three cone tiers on the trunk, each smaller than the one below.

Still writing the tree materials... Setting up the foliage palette and the trunk colour for the trees.

Still working out the tree placement: I'm aiming for about 44 trees, with a cap of 4000 attempts, and I'm keeping them out of the wedge in front of the camera so the fire stays visible at the start.

Still working out tree placement: I'm skipping spots too close to the camera, and I'm also keeping trees out of the sightline between the camera and the fire so the flames stay visible.

Still writing the flame layers... Stacking four cone layers from red-orange at the base to pale yellow at the tip, each smaller than the one below.

Still writing the flame tongues... Giving each one an open-ended cone, then animating its position and scale.

Still writing the flame animation... Working out the per-tongue flicker formula from a few overlapping sine waves.

Still writing the flame animation... Tilting each tongue slightly around its base and flickering the height and width.

Still writing the flame glow... Adding a soft additive halo sprite above the fire, with a larger faint one behind it.

Still writing the spark system... Setting up the particle buffers for positions, colours, velocities and lifetimes.

Still writing the spark system... Setting up the particle respawn logic at the fire.

Still writing the spark system... Setting each spark's starting point near the fire and giving it an upward velocity with a bit of sideways spread and wind drift.

Still writing the spark system... Setting each particle's velocity to drift upward and slow down over time, with a little turbulence sideways.

Still writing the spark colours... Fading each particle from hot yellow to dark red as it ages, and flickering the fire light.

Still writing the camera controls... Setting the orbit target near the fire's upper flame, with damping on, panning off, and distance and polar angle limits so the view stays sensible. Then I'm adding a clamp that keeps the camera above the ground height at its position.

Checking the camera's minimum height now: the controls' target sits at 1.8 and the polar limit keeps the camera above that, so the ground clamp mostly matters on the hills, where I'm setting the floor at about 4.

Still writing the animation loop... Setting the renderer to call a per-frame function, which will update the flames, sparks and camera clamp.

Still writing the autorotate setting... Deciding whether the camera should slowly orbit on its own for the demo.

Still writing the auto-rotate setup... Adding a slow orbit that stops once the user starts dragging the view.

Still working on the window resize handling and the canvas sizing, keeping the renderer and camera aspect in step.

Now I'm settling the mobile camera: on a narrow portrait screen the horizontal field of view gets too tight, so I'll widen the vertical FOV when the aspect is below about 0.9 and keep it at 50 otherwise. Trees, sparks and the star layers look fine as they are, and I'm checking that the fog colour still suits the overall lighting.

Now I'm checking the sky-to-fog match at the horizon: the sky dome's gradient darkens quickly above the horizon, so the band right above the fog colour won't blend seamlessly.

Smoothing the sky's gradient near the horizon: a square-root curve darkens it too fast just above the line, so I'll switch to a smoothstep blend between the horizon and zenith colours, which keeps the transition from the fog-coloured ground much more even.

Next I'm checking how the fog and ground meet the horizon, so the far edge of the clearing blends into the sky. I'll set the fog to start at 14 and reach full strength at 75, and the trees around 40 to 45 units out will fade partway.

Still writing the tree merging... Looking at whether to combine each tree's trunk and foliage into one mesh with two material groups, which would cut the draw calls a lot.

Checking whether merging geometries would be worth the extra complexity. Each tree is four separate meshes, so 44 trees come to about 176 draw calls, which is fine, and I'd rather keep the code simple.

Still writing the minimum-height and fog check... Shrinking the sky gradient so it blends smoothly into the fog colour at the horizon.

Still writing the seeded random helpers... Setting up a small generator for repeatable numbers, plus range, clamp and smoothstep utilities for the scene.

Still writing the renderer setup... Creating a WebGL renderer with antialiasing, capping the pixel ratio at 2, enabling soft shadows, and attaching the canvas to the page.

Still writing the sky dome... Creating a large sphere around the scene, so the horizon blends into the fog.

Still writing the sky shader... Blending the horizon and zenith colours with a smoothstep, plus a faint warm haze near the horizon.

Still writing the sky shader... Using a smoothstep blend from the horizon colour up to the zenith, so the fog and sky meet without a visible seam.

Still writing the star field... Scattering points across the upper sky with a seeded spread, weighted toward the zenith, and filling their position and brightness buffers.

Still writing the star field... Setting up each star's position on the sky dome, with a random brightness and a slight colour tint.

Still writing the star field... Adding a faint Milky Way band by rejection-sampling points close to a tilted great circle, so the dense stars form a soft streak across the sky.

Still writing the star field... Adding a faint Milky Way band by rejection-sampling directions near a tilted plane, then giving those dimmer, bluish stars a random spread.

Still writing the lighting... Setting up a cool hemisphere light for the sky and a moonlight directional light aimed at the clearing.

Still writing the shadow camera setup... Sizing the moon's shadow box so it covers the clearing, then working out the fire light next.

Still writing the fire light... Setting up a point light at the flame with shadows, tuned so the logs cast soft shadows from the inside out.

Checking the point light's height now: at 1.0 it sits inside the ring of logs, so the logs will cast their shadows outward from the fire, and the inner faces will catch the most light.

Still writing the terrain... Shaping the ground height with a few layered sine and cosine hills, flattened near the centre so the clearing stays level.

Now I'm setting up the ground as a large plane, shaped by the terrain height function and kept flat near the clearing.

Still writing the ground colouring... Assigning each triangle a shade from a few earthy greens and browns, with some random variation so the clearing doesn't look flat.

Still writing the ground colouring... Shading each triangle from a few earthy greens and browns, with random jitter and a warmer tint near the fire.

Still writing the ground colouring... Adding a dark ash tint near the fire ring so the clearing darkens toward the hearth.

Still writing the light pool... Adding a soft additive orange disc on the ground around the fire, using a radial gradient texture.

Still writing the rock materials... Defining a few grey stone tones for the rocks around the fire.

Still writing the rock placement... Setting up the ring of stones around the fire, each one scaled and partly buried in the ground.

Still writing the log placement... Setting up the bark and cut-face materials for the logs around the fire.

Still writing the log placement... Defining a helper that sets each log's length and radius, then positions it between its two endpoints.

Still writing the log placement... Positioning each log between its two endpoints, with the bark on the sides and cut faces on the ends.

Still writing the log placement... Working out the teepee arrangement now, with four logs leaning in from the ground to a point just above the fire.

Still writing the teepee logs... Adding a crossing log at the base to lock the arrangement together.

Still writing the lying logs... Placing four more logs at random spots around the clearing, each one laid on its side at a random angle.

Still writing the log stumps... Adding two short cylinders with flat tops at a distance from the fire, to break up the clearing.

Still writing the tree models... Setting up a trunk cylinder, a cone for the foliage, and a few jittered icosahedron blobs, with the cone's base lifted so it sits on the ground.

Still writing the tree models... Adding a pine variant with three stacked cones of decreasing size, each rotated randomly so the tiers don't line up.

Still writing the tree models... Adding a leafy variant with three jittered blobs, each at its own height and scale, then giving every mesh in the tree shadows.

Still writing the tree placement... Looping to scatter tree spots across the clearing, keeping them clear of the fire.

Still writing the tree placement... Rejecting spots that fall in the sightline to the fire or too close to the camera, and keeping trees apart from one another.

Still writing the tree placement... Checking that the trees far out at radius 30 to 44 fall into the fog, so the distant ones fade as they recede.

Still writing the tree placement... Positioning each tree at a random angle and scale, with a slight stretch so no two look identical, and adding a few low bushes near the edges of the clearing.

Still writing the flame... Adding a set of flame layers around the fire, each with its own colour, opacity, and flicker speed.

Still writing the flame layers... Adding the per-layer flicker setup for each cone.

Still writing the flame layers... Giving each flame cone its own random offset, radius, and flicker phase and speed, then adding it to the flame group.

Still writing the flame animation... Stepping each flame's flicker with two sine waves so the tongues sway at slightly different rates.

Still writing the flame animation... Stepping the flame tongues with sine waves, scaling each one's height and slightly narrowing it, and tilting it sideways.

Still writing the flame animation... Tilting each flame tongue slightly with the wind and nudging its base position so the fire wobbles.

Still writing the flame glow... Adding an additive sprite behind the fire with a soft radial gradient, tinted orange so it lights the area around the flames.

Still writing the ember placement... Scattering a few small glowing embers around the base of the fire, each with a random size and a gentle pulse.

Still writing the animation loop... Stepping the sparks forward and respawning any that have burned out.

Still writing the spark spawn... Setting each spark's starting position, upward velocity, and lifetime so the sparks rise from the fire and fade out.

Still writing the spark prewarm... Running the spark update loop for a few seconds up front, so the sparks are already spread out when the first frame draws.

Still writing the spark update... Advancing each spark's life and respawning the ones that have burned out, with its velocity pulled toward a terminal rise speed.

Still writing the spark physics... Adding a sideways drag and a slight upward buoyancy, so the sparks settle at a steady rise speed instead of racing off.

Still writing the spark motion... Adding a slow sideways wind and drag so the sparks drift and settle at a steady rise, then fading their colour from hot to orange as they age.

Still writing the spark physics... Checking that the points geometry's bounding sphere is stale, since the sparks drift outside it and could get culled.

Still writing the scene code... Setting the sparks' drawing bounds to off, since they drift outside the initial bounding sphere, and leaving the far-off stars to render normally.

Still working on the camera framing now. I'm checking how the view looks at a wide desktop size and at a narrow portrait one, with the fire near the middle and the ground filling the lower part of the frame.

I'm checking whether the trees near the camera side are a problem, and I'm weighing whether a closer foreground would help. The camera is about 16.6 units from the fire, which makes the fire and flames look fairly small on screen, so I'm considering moving the camera closer.

I'm settling the camera at about (7, 2.6, 9) looking at a point just above the fire, which puts it roughly 11 units away so the flames read at a decent size. Trees within about 7 units of the camera will be skipped so nothing crowds the lens. I'll also recompute the view direction from the new position to check the tilt.

Working out where the moon sits in the frame: its elevation puts it around 80% of the screen height, which is higher than I'd like against the tree tops. I'm considering lowering it to roughly 70% by reducing its elevation a bit.

I'm checking whether the moon, sitting low over the far tree line, would be partly hidden by those distant trees, and I'm weighing a small nudge to keep it clear.

Checking the moon's light direction: it comes from the far side, so the tree shadows fall toward the camera and make clear silhouettes. The trees on the far side will be backlit, which gives a darker, moodier look, and the ones off to the sides pick up a bluish moonlight.

Checking the lighting now: the directional moonlight at intensity 1.1 against the dark foliage colour, and how the Lambert diffuse term works out in three.js.

Checking the light intensity now: with the moon at 1.1, the foliage and ground come out quite dim, so I'm bumping the moonlight to 1.6 to keep the ground readable without losing the night mood.

Still checking the lighting: the hemisphere fill is an ambient term, and I'm working out whether its irradiance gets the extra π factor in three.js's physically correct lighting path.

I'm checking the hemisphere fill's intensity, since I'm not sure how three.js scales ambient irradiance by π in its physically correct path, so I'm leaning toward setting it around 1.2.

I'm settling the hemisphere fill at a moderate level, with a blue sky tint and a dark ground colour. The exact brightness is hard to pin down without rendering, so I'll go with a middle value. Next is the fire's point light, where I'm working out how its falloff with distance lights the ground near the flames.

Next I'm checking the fire's point light falloff: at about 2 units from the logs it should give a bright orange glow, and by 10 units it should be a faint warm tint. Three.js uses physically correct decay by default, so I'll keep the 26 intensity and the decay of 2. Then I'm adding a flicker term to the intensity, starting from a base near 26 with a small sine-driven wobble.

Still writing the moon halos... Adding two additive glow sprites behind the moon disc, a small bright one and a larger faint bluish one, and sizing the disc to match.

Still writing the scene setup... Placing the moon's light and disc far out along its direction, then adding the tree count loop along with bushes and rocks.

Still writing the star field... Setting up a generator that places stars in a band across the sky and tints some of them.

Still writing the star field... Building a band of stars along a tilted Milky Way plane and keeping only the ones close to it.

Still writing the star generator... Filling in each star's brightness and a warm or cool tint, then packing the positions and colours into buffers.

Still writing the star layer... Setting the draw range so only the stars actually placed get rendered, then adding a twinkling bright layer whose colours pulse over time.

Still writing the star layers... Switching the points to default blending with opaque colours, using a size of 1.5 for the dim layer and 2.5 for the bright one, then adding a Milky Way band of about 1600 stars.

Still writing the render loop... Setting up the frame function to advance the fire, sparks and twinkling each tick, with the controls updated and the ground height clamped.

Still writing the render loop... Hooking up the animation callback, clamping the camera above the ground, and setting the controls to auto-rotate until the user drags.

Still writing the render loop... Checking the fire's glow sprite opacity flicker and the ember pulses, and wondering whether auto-rotate should stop once the user starts dragging.

I'm double-checking the API calls now: setUsage returns the attribute so chaining works, setDrawRange and CanvasTexture are fine, and the OrbitControls path resolves through the import map.

Checking the jitter helper: the map keys on the rounded vertex position, so duplicate vertices from the non-indexed icosahedron get the same offset and the blob stays watertight. A detail-0 icosahedron gives 20 faces, which is low-poly enough for the tree and rock shapes.

Still writing the rock and tree layout... Checking that ten rocks around the ring leave enough spacing, and that the trees stay clear of the fire.

Checking how the tree and rock placement sits against the camera now: the camera's spot is close enough to the hills that the ground could rise a little under it, so I'm making sure the camera stays above the terrain at that point.

Still writing the render setup... Adding the ground height clamp for the camera and prewarming the sparks, then checking the fog range so the distant scenery fades in properly.

Still writing the flame layers... Adding a faint warm halo over the flames and keeping the base of the fire wide enough to read clearly.

Still checking the shadow setup: I'm wondering whether PCF filtering in r170 actually uses the shadow radius for its softness, and how that applies to the point light's shadow.

Checking the shadow camera now: after changing the directional light's shadow bounds, I need to call updateProjectionMatrix on it, since the projection won't pick up those changes on its own. I'm also confirming that the point light's shadow camera far value follows its distance.

Still writing the HTML head and the shadow setup... Expanding the directional shadow bounds to about ±56 so the trees near the edge are covered, with a 2048 map.

Still writing the page styles... Setting up the dark background, the full-screen canvas, and the HUD text, with the mobile layout rules starting to take shape.

Still writing the page styles and HTML shell... Adding the mobile layout rules for the title and hint text, then wiring up the import map and the body with its two HUD overlays.

Still writing the scene setup... Adding a title overlay in the top left and a hint line at the bottom that explains how to orbit and zoom, then starting the main script with the deterministic random generator.

Still writing the terrain... Defining the hill height function from a few overlapping sine waves, then checking that the ground stays clear of the camera's starting spot.

Still writing the terrain... Adding a jitter helper that offsets each shared vertex by the same random amount, so the rocks and trees keep a watertight low-poly look.

Still writing the jitter helper... Looping through the vertices so duplicate points get the same random offset, then recomputing the normals.

Still writing the renderer and scene setup... Creating the WebGL renderer with antialiasing and shadows, prepending its canvas to the page, and setting up the fog and perspective camera at the starting spot.

Still writing the controls... Setting up damping, disabling panning, and limiting zoom and vertical angle, with auto-rotate on until the user starts dragging.

Still writing the sky and resize handling... Setting up the sky dome and resize handler so the camera's aspect and field of view adjust to the window.

Still writing the sky... Setting up a backside dome shader that shades the sky from the horizon colour upward.

Still writing the sky... Adding a star field on a random sphere of directions so the points sit evenly over the dome.

Still writing the star field... Generating points on the sky dome, rejecting those below the horizon and those outside the Milky Way band when one is set, and assigning each star a brightness and a warm or cool tint.

Still writing the star field... Setting each star's brightness and warm or cool tint, then building the geometry with the draw range covering only the stars actually placed.

Still working through the star placement... checking that the bounding sphere still covers the camera so the points aren't culled.

Still writing the star field... Setting up two point layers, a dim one and a fainter Milky Way band, each with its own point size.

Still writing the star layers... Making the bright stars a separate set with a larger point size, and turning off depth writes so the stars sit cleanly over the sky.

Still writing the star field... Setting each bright star's base colour from the initial values, then animating a gentle twinkle in the update loop by scaling each colour with a slightly different sine wave.

Still writing the moon... Building a faceted icosahedron and giving each face a random brightness, with a few darker patches for the maria.

Still writing the moon... Shading each face of the icosahedron with a random brightness and a few darker patches, then setting up the moon mesh with vertex colours and no fog.

Still writing the moon... Moving it into place along its direction at distance 330, and adding a couple of additive halo sprites around it.

Still writing the lights... Placing the moon's direction at distance 330 and setting the moon's directional light from the same side.

Still writing the moon lighting... Attaching the moon light and its target to the scene, enabling shadows with a 2048 map, and tightening the shadow frustum before the fire light.

Still writing the fire light... Setting its position, enabling shadows on a 512 map, and adjusting the near and far planes, bias, and softness.

Still writing the rocks... Setting up six jittered icosahedron rock shapes to scatter around the fire.

Still writing the rocks... Adding a helper that places each rock with a random tilt and scale, using a random pick from the jittered shapes and a few grey materials.

Still writing the rocks... Placing eleven rocks in a ring around the fire at a radius of about 1.6, each with a jittered angle, radius, and size.

Still writing the rocks... Scattering sixteen more rocks around the wider clearing with random positions, sizes, and proportions, and wondering whether to keep them clear of the trees placed later.

Still writing the campfire area... Adding the ash disc just above the ground, sitting slightly below the water pool plane.

Still writing the trees... Setting up a trunk cylinder, a cone and three jittered blob shapes for the foliage, plus a few dark green materials for the pines.

Still writing the trees... Setting up a pine-style branch layout with a few random tilts, and a rounder foliage variant for the other trees.

Still writing the trees... Filling in the foliage blobs with a jittered position, scale, and tilt, then turning on shadows for every mesh in the tree group and placing it on the ground.

Still writing the tree builder... Adding a trunk and either a pine of stacked cones or a round tree of jittered blobs, then scaling, rotating, and placing it on the ground.

Still writing the trees... Placing bushes as small blob clusters that sit on the ground.

Still writing the sparks... Setting up the particle buffers for the spark system, with position, colour, velocity, and life arrays sized for 240 particles.

Still writing the spark system... Wiring up the spark points with a glow texture, vertex colours, and additive blending, and turning off frustum culling so they're always drawn.

Still writing the spark update... Computing each spark's colour from its age, so it starts hot yellow and fades through orange as it dies.

Still writing the sparks... Updating each spark's life and respawning it when it dies, with a wind drift and a little wobble.

Still writing the spark update... Working out the vertical drift, which settles toward a terminal rise speed of about 1.6.

I'm checking the sparks' vertical rise, working out that they'll top out around five metres above the ground, which seems about right.

Still writing the sparks... Tuning the wind drift and wobble so the sparks only wander about half a metre sideways.

Still writing the sparks... Tuning the wind so they drift only gently, then prewarming the particle system so the fire already looks alive when the scene first loads.

Still writing the spark prewarm... Running the particle system forward for a few seconds before the first frame, so the sparks are already spread out and rising when the scene loads.

Still writing the animate loop... Updating the flame and fire light flicker, then stepping the sparks and checking the camera against the ground.

Still writing the animate loop... Checking the ground clamp so the camera stays above the hills, then rendering the frame.

Still writing the flame animation... Scaling and tilting each flame mesh with overlapping sine waves so the fire flickers, and nudging its position sideways.

Still writing the flame animation... Adding a slow flicker to the ember sizes, then wrapping up the star twinkle.

Still writing the star twinkle... Setting each bright star's colour to flicker at its own speed, using the saved base colours.

Still writing the radial glow texture... Building a 128-pixel canvas and filling its radial gradient with the given colour stops.

Still writing the pool... Creating a nine-by-nine ground plane for the firelight pool, using the glow texture with an orange tint and additive-style blending.

Still writing the pool... Adding the ember geometry next, after checking that the additive pool plane sits flat over the ground.

Still writing the ember geometry... Checking that the additive pool and the fire glow don't clash, and thinking about how the logs should be shaded with the firelight on them.

Still writing the logs... Placing each leaning log from its base to its tip, with a scale and a tilt toward the centre.

Still writing the teepee... Setting up the four leaning logs around the fire, each with a slight random angle.

Still writing the teepee logs... Placing each leaning log from a base point near the fire up to a tip close to the centre, with a slight random radius.

Still writing the teepee logs... Setting each log's scale to radius one and checking where the bottom ends sit relative to the ground.

Still writing the fallen logs... Placing each one on the ground at a random angle around the clearing, with a random length and a rotation that lays it on its side.

Still writing the log placement... Checking the axis rotation for the fallen logs and how the scale interacts with it.

Still writing the tree and bush placement... Working out where the logs and bushes go around the clearing and checking the spacing for the tree loop.

I'm checking the tree spacing now: a ring of 44 trees with a 2.8 minimum gap fits easily in the 10–44 band. I'm also thinking about the initial camera view, since trees sitting far behind the fire on the opposite side could still be visible from there.

I'm checking the tree placement against the camera's starting view, and the trees that sit on the far side of the fire are behind the camera, so they won't be visible. The ones near the camera's sides should show at the edges, which is fine. I'm also confirming that the fog starts around 14 and the ground extends out to 120, so the distant edge fades properly.

Checking the moon and fog settings next: the moon sits at 330 and the sky dome at 380, and the moon shouldn't be fogged. The halo sprite is 24 units across, which works out to about four degrees at that distance. I also need to confirm that the color space constant exists in this Three.js version and that the scene won't throw console warnings.

Still checking the page setup: the canvas gets prepended to the body above the HUD, and the pixel ratio is set before the resize handler runs.

Still checking the moon and sky materials: the sky sphere uses back-face rendering so its interior shows, and the star points stay fixed in pixel size. The moon's vertex colours and its low-poly icosahedron detail are set, and the flame layers are transparent with depth writes off.

Still writing the flame layers... Checking the flame cluster's size against the logs so the fire reads clearly from the camera.

Still writing the flame layers... Adding a translucent outer flame shell with a few cone layers to give the fire more body and glow.

Checking the ground lighting next: the moon sits very low, so the upward-facing ground only gets a weak, grazing light.

I'm checking how the moon's low angle affects the ground: it only grazes the upward-facing surfaces, so the ground stays dark and the fire light ends up doing most of the lighting. Raising the moon's intensity should help the terrain read a bit better.

Checking whether the moon can sit higher for better ground light, but the top of the frame would clip it, so I'll keep it low and raise its intensity to about 2.4 to brighten the terrain.

Checking the ground's brightness now: at intensity 2.4 the moon only grazes the upward faces, so the terrain lands around 15 to 25 out of 255. That's dark but readable, and the fire lights the near ground. I'm also weighing whether the hemisphere sky fill would brighten the far ground enough.

Still checking the shader lighting... I'm working out how three.js handles ambient irradiance and the Lambert diffuse term, since the hemisphere fill's brightness on the ground depends on it.

I'm checking the ambient fill numbers now: the sky hemisphere light's contribution to the ground should be tiny, around a fraction of a percent of radiance, so the ground depends mostly on the fire and the moon.

I'm estimating how much the hemisphere sky fill lights the ground. Working through the numbers, a fill of around 2.2 with a bluish sky tone gives the upward-facing terrain a faint but visible cool glow, so I'll bump it up from the smaller value I had.

Working through the sRGB conversion for the ground colour: a linear value of about 0.021 comes out near 39 in 8-bit, with the red and blue channels landing around 20 and 30. That gives a dark teal-green night tone, which suits the scene, and the fire and moon will add warmth and highlights on top.

I'm estimating the moon's contribution to the ground at intensity 1.5, and it comes out very small, so the moon's shadows would be hard to see against the hemisphere fill. I'm thinking about raising the moon intensity to 2.0 so its shadows read clearly.

I'm weighing whether the moon's roughly 25% contribution is enough to show shadows, and I'm thinking about nudging its elevation up to about 11.5 degrees so the shadows and ground light read better. I'd set the direction to (-0.6, 0.2, -0.77), which is already normalized.

I'm checking whether the moon at about 9° of elevation gives a good balance, since the moon's screen height and the ground light both depend on that angle.

I'm settling the moon direction and camera target now, aiming for the moon to sit around three-quarters up the frame. Pitching the camera target slightly upward should keep the fire from sitting too low on the screen.

I'm checking where the fire lands on screen: from the camera it sits about 7 degrees below the frame's centre, which puts it roughly 37% up from the bottom. That's a lower-middle position, with the trees behind it. The horizon should fall near 55% up the frame.

Checking whether trees close to the camera, around 15 units out, would rise high enough in the frame to cover the moon at about 73% height.

Now I'm checking whether the trees behind the fire, at about ten units out, would rise high enough in the frame to block the moon, since the moon sits in the same centre column.

Now I'm working out whether the moon can sit slightly to the left of centre so it doesn't compete with the trees behind the fire, which top out just below it.

I'm settling the moon's direction by nudging it toward the upper left, so it sits roughly a third of the way across the frame and clears the tree line behind the fire. I'm normalising the vector after shifting it slightly along the camera's right axis.

Now I'm settling the moon's direction: shifting it toward the left so it clears the tree line behind the fire, then normalising the vector.

I'm working out the moon's screen position by projecting its direction onto the camera's right axis, to see how far left it lands in the frame.

Working out the moon's screen position now, measuring its offset from the camera's view direction to find where it lands vertically in the frame.

I'm checking the moon's projected position: it lands about 72% up the frame and roughly a third of the way in from the left, clear of the HUD title. With that settled, I'm leaning toward a directional light intensity of 2.0 and a hemisphere fill of 2.2, though I'm still weighing how the fill interacts with the ground.

I'm settling the ground lighting now: the foliage albedo is so low that the trees will read as dark silhouettes, so I'll raise the hemisphere fill to 2.8 to bring out their green-teal tone. The fire light at intensity 26 should give a warm, bright pool of ground near the campfire, and I'm setting the moon colour to a bluish 0xb4c8ff.

Checking the sky hemisphere fill and the moon's screen position against the tree line, then finalising the lighting values.

Still writing the HTML page styles...

Still writing the page styles...

Still writing the page styles and the title overlay...

Still writing the page's JavaScript, now the render setup and the sky and moon.

Still working out the page's JavaScript: the imports, the deterministic random helper, and the textures for the glow effects.

Still writing the seeded random helper and the small math utilities.

Still writing the JavaScript: the seeded random helpers, the camera and moon setup, and the terrain height function.

Still writing the terrain and renderer setup.

Still writing the scene setup: the renderer, the fog, and the camera's starting position, then the orbit controls.

Still writing the sky shader, setting the horizon and zenith colours for the gradient.

Still writing the sky shader and the star field setup.

Still writing the star setup: the dim field, the milky band, and a set of brighter stars, all added to the scene.

I'm checking how the star band is oriented, since the milky band's normal points mostly upward and that could place the band near the horizon instead of across the sky.

Still writing the moon's geometry, building the icosphere and filling in its vertex colours.

Still writing the moon's vertex colours, adding darker patches to suggest the maria.

Still writing the moon's halo sprites, setting up a soft glow texture and its additive material.

Still writing the lighting: a hemisphere fill at 2.8 and a bluish moonlight directional light, which I'm placing along the moon's direction.

Still writing the directional moonlight: positioning it along the moon's direction, setting up its shadow camera bounds, and tuning the shadow bias.

Still writing the fire light: a warm point light with shadows enabled and its shadow camera tuned.

Still writing the ground: setting its size and segments, then lowering each vertex to the terrain height.

Still writing the ground colours, mixing moss, litter and ash tones per triangle.

Still writing the ground colours, shading each triangle with moss, litter and ash tones.

Still writing the ground colours, shading each triangle with moss, litter and ash tones.

Still writing the ash disk under the fire, a dark circle slightly above the ground.

Still writing the ash disk, pool glow, and the log and rock placement.

Still writing the log materials, the bark and cut-face colours, and the shared cylinder geometry.

Still writing the rock geometry, jittering a set of icosahedrons and picking their materials.

Still writing the rock setup: a few jittered icosahedrons, their materials, and a helper that places each rock on the terrain.

Still writing the rock ring: placing a set of jittered rocks around the central clearing at a fixed radius, each scaled to vary in size.

Still writing the scattered rocks, placing a random set farther out from the clearing at varied sizes.

Still writing the tree placement: scattering trunks and canopies at random radii around the clearing, skipping spots too close to the camera's starting angle.

Still placing the trees: I'm skipping spots that sit on the camera's start line and crowd the fire, and keeping each new tree a few metres from the others.

Still placing the bushes: a ring of blob shrubs scattered farther out, each sized and set on the terrain.

Still writing the bushes and checking that they sit outside the clearing, away from the fire.

Still writing the sparks: the update function, with the wind drift and the per-particle loop.

Still writing the spark update: applying the wind and wobble to each particle's velocity.

Still writing the spark update: the drift, the per-particle fade, and the colour ramp from bright yellow to dim red.

Still writing the spark update: the colour ramp and fade per particle, then flagging the attributes for upload.

Still writing the animation loop: I'm reading the clock for the elapsed time and a clamped frame delta, then updating the flames first.

Still writing the animation loop: driving the campfire flicker, the ember pulse and the sparks, then starting on the camera controls and the ground height check.

Still writing the twinkle loop for the bright stars, varying each one's brightness over time.

Still writing the animation loop's end: the bright stars' twinkle, the resize handler, and the first render call.

Still working through the camera limits: the polar angles and the maximum distance for the orbit controls, checking that the camera stays above the ground.

Still checking the tree and light details, making sure the shadows and lighting setup hold up.

Still writing the ember setup: a small pool of glowing icosahedrons scattered around the fire, each with its own size and phase.

Still placing the embers: I'm scattering a small set of glowing spheres around the fire base, each with its own size and pulse phase.

Still checking the ground and moon geometry, confirming the non-indexed buffers and the pixel ratio setup before assembling the file.

Still writing the animation loop's flame section: the per-tongue breathing, the fire flicker that drives the light and glow, and the ember pulse.

Still writing the flame layers: a stack of hexagonal cone meshes, each with its own colour, opacity and flicker speed, from a faint orange outer glow down to a pale yellow-white core.

Still building the flame cones: giving each one a random spin and a per-cone flicker speed, scattered inside its layer's radius.

Still writing the flame layers: giving each cone its own spin, a random scatter inside its layer's radius, and a size variation, and adding them to the flame group with their flicker data.

Still writing the flame cones: the ring of cones at each layer's radius, with the random spin and per-cone flicker speed.

Still writing the star field: generating random directions, skipping anything below the horizon, and filling the position and colour arrays for each star.

Still writing the star field's colour and position arrays, with the bright stars and the Milky Way band.

Still writing the star helpers: a random direction on the sphere, with the stars that fall below the horizon thrown out.

Still checking the tree and rock helpers, and the camera's maximum distance against the sky sphere so the backdrop never gets cut off.

Still checking the camera's auto-rotate speed and how the moon and fog settings sit against the far plane.

Final code writing now: I'll put the whole file together and keep it concise but complete.