Models 44

Grok 4.7

by SpaceXAIreasoning default1 console error
Open in new window

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

How it went

Total time
25 min 21 s
Cost
$0.582
Output tokens
96,665
Spent thinking
82,274 tokens
Speed
63.7 tok/s
Console errors
1
First token after
2.0 s
First visible text after
23 min 38 s
Prompt tokens
1,815
Code size
37.9 KB
Finish reason
stop
Served by
xAI
Model id
x-ai/grok-4.7
Reasoning sent
model default
Max output
450,000 tokens
Generated
Nov 12, 2026

Console errors

Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.

Cannot access 'campHold' before initialization

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 · 1203 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Midnight Clearing</title>
<style>
  * { box-sizing: border-box; }
  html, body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #070b14;
    background-image:
      radial-gradient(1.5px 1.5px at 12% 18%, rgba(255,255,255,.85), transparent),
      radial-gradient(1px 1px at 27% 32%, rgba(255,255,255,.55), transparent),
      radial-gradient(1.4px 1.4px at 48% 12%, rgba(255,255,255,.7), transparent),
      radial-gradient(1px 1px at 63% 22%, rgba(255,248,230,.6), transparent),
      radial-gradient(1.2px 1.2px at 78% 16%, rgba(255,255,255,.75), transparent),
      radial-gradient(1px 1px at 88% 40%, rgba(255,255,255,.45), transparent),
      radial-gradient(1px 1px at 18% 48%, rgba(255,255,255,.35), transparent),
      radial-gradient(ellipse at 50% 115%, #152238, #070b14 58%);
    color: #f4ead8;
    font-family: Georgia, "Palatino Linotype", Palatino, serif;
    user-select: none;
    -webkit-user-select: none;
    touch-action: none;
  }
  canvas {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 0;
    display: block;
    touch-action: none;
  }
  .vignette {
    position: fixed;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: radial-gradient(ellipse at center, transparent 48%, rgba(0,0,0,.42) 100%);
  }
  #hud {
    position: fixed;
    z-index: 2;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 16px;
    padding: 36px 26px 18px;
    background: linear-gradient(to top, rgba(4,6,12,.62), rgba(4,6,12,0));
    pointer-events: none;
    max-width: 100%;
  }
  .brand {
    margin: 0;
    font-size: clamp(14px, 2.1vw, 22px);
    font-weight: normal;
    letter-spacing: 0.28em;
    text-transform: uppercase;
    text-shadow: 0 2px 10px rgba(0,0,0,.75);
    white-space: nowrap;
  }
  .brand::before {
    content: "";
    display: inline-block;
    width: 7px;
    height: 7px;
    margin-right: 10px;
    background: #e39a4a;
    transform: rotate(45deg);
    box-shadow: 0 0 10px #e39a4a;
    vertical-align: middle;
  }
  .brand i {
    display: block;
    width: 46px;
    height: 1px;
    margin-top: 8px;
    background: linear-gradient(90deg, #e0a15a, transparent);
  }
  .hint {
    margin: 0;
    font-family: "Segoe UI", system-ui, sans-serif;
    font-size: 12px;
    letter-spacing: 0.08em;
    opacity: 0.78;
    text-align: right;
    text-shadow: 0 2px 8px rgba(0,0,0,.8);
    line-height: 1.45;
  }
  @media (max-width: 600px) {
    #hud {
      flex-direction: column;
      align-items: flex-start;
      padding: 24px 14px 12px;
      gap: 6px;
    }
    .brand { letter-spacing: 0.14em; font-size: 14px; }
    .hint { text-align: left; font-size: 11px; }
  }
</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="vignette" aria-hidden="true"></div>
<div id="hud">
  <h1 class="brand">Midnight Clearing<i></i></h1>
  <p class="hint">Drag to orbit · Scroll to zoom</p>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const rand = mulberry32(0x5eed21);
const FOG_COLOR = 0x1a2744;

const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(FOG_COLOR, 1);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(FOG_COLOR, 20, 54);
scene.background = new THREE.Color(FOG_COLOR);

const camera = new THREE.PerspectiveCamera(66, window.innerWidth / Math.max(window.innerHeight, 1), 0.08, 320);
camera.position.set(5.6, 2.35, 6.4);
const lookTarget = new THREE.Vector3(0, 1.05, 0);
camera.lookAt(lookTarget);
camera.updateMatrixWorld();

const viewDir = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
const camUp = new THREE.Vector3(0, 1, 0).applyQuaternion(camera.quaternion);
const moonDir = viewDir.clone().addScaledVector(camUp, Math.tan(16 * Math.PI / 180)).normalize();
const moonPos = camera.position.clone().addScaledVector(moonDir, 58);

const trunkMaterials = [
  new THREE.MeshLambertMaterial({ color: 0x6b4630, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x5a3a24, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x7a5340, flatShading: true })
];
const leafMaterials = [
  new THREE.MeshLambertMaterial({ color: 0x2f7d48, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x256b3c, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x1d5a34, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x348552, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x1a4e2e, flatShading: true })
];
const woodMaterials = [
  new THREE.MeshLambertMaterial({ color: 0x7a5234, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x64422a, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x8a6240, flatShading: true })
];
const charredMat = new THREE.MeshLambertMaterial({ color: 0x2c1e14, flatShading: true });
const rockMaterials = [
  new THREE.MeshLambertMaterial({ color: 0x8a847c, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x6e6a64, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x7d7872, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x5c5854, flatShading: true })
];
const ringRockMaterials = [
  new THREE.MeshLambertMaterial({ color: 0x8a7568, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x75655a, flatShading: true }),
  new THREE.MeshLambertMaterial({ color: 0x9a8474, flatShading: true })
];

const hemi = new THREE.HemisphereLight(0x2a3c64, 0x2a1c12, 0.62);
scene.add(hemi);
scene.add(new THREE.AmbientLight(0x1a2438, 0.28));

const moonLight = new THREE.DirectionalLight(0xa9c0e4, 2.55);
moonLight.position.copy(moonPos);
scene.add(moonLight);
scene.add(moonLight.target);

const fireLight = new THREE.PointLight(0xff6a1a, 32, 20, 2);
fireLight.position.set(0, 1.48, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.width = 1024;
fireLight.shadow.mapSize.height = 1024;
fireLight.shadow.camera.near = 0.3;
fireLight.shadow.camera.far = 24;
fireLight.shadow.bias = -0.002;
fireLight.shadow.normalBias = 0.025;
scene.add(fireLight);

createSky(moonPos);
createStars();
createMoon(moonPos);
createGround();
const camp = createCampfire();
createProps();
const swayTrees = [];
const grassTufts = [];
createForest();
createAccentTrees();
const sparks = createSparkPoints();
const embers = createEmbers();
const fireflies = createFireflies();

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.copy(lookTarget);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.28;
controls.rotateSpeed = 0.65;
controls.zoomSpeed = 0.7;
controls.minDistance = 3.2;
controls.maxDistance = 24;
controls.minPolarAngle = 0.12;
controls.maxPolarAngle = Math.PI / 2 - 0.07;
controls.update();

renderer.domElement.addEventListener('pointerdown', () => {
  controls.autoRotate = false;
});

window.addEventListener('resize', onResize);

const clock = new THREE.Clock();
const fireColorA = new THREE.Color(0xff5511);
const fireColorB = new THREE.Color(0xffcc66);
const coalHot = new THREE.Color(0xfff0b0);
const coalCool = new THREE.Color(0xff3a00);
const emberHot = new THREE.Color(0xfff6d0);
const emberCool = new THREE.Color(0xff4a08);

animate();

function onResize() {
  camera.aspect = window.innerWidth / Math.max(window.innerHeight, 1);
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const time = clock.elapsedTime;
  const flicker = flameFlicker(time);

  fireLight.intensity = 26 + flicker * 14;
  fireLight.color.copy(fireColorA).lerp(fireColorB, 0.35 + flicker * 0.4);
  fireLight.position.set(
    Math.sin(time * 9.1) * 0.07,
    1.46 + Math.sin(time * 7.4) * 0.06,
    Math.cos(time * 8.2) * 0.07
  );

  for (const f of camp.flames) {
    const u = f.userData;
    const s = 0.82 + Math.sin(time * u.speed + u.phase) * 0.2 + Math.sin(time * u.speed * 2.17 + u.phase * 1.7) * 0.08;
    f.scale.y = Math.max(0.42, s);
    const sx = 0.78 + Math.sin(time * u.speed * 1.63 + u.phase * 2.1) * 0.18;
    f.scale.x = Math.max(0.4, sx);
    f.scale.z = Math.max(0.4, sx * (0.88 + 0.12 * Math.sin(time * 1.4 + u.phase)));
    f.rotation.y = u.rot + Math.sin(time * 0.9 + u.phase) * 0.35;
    f.material.opacity = u.opacity * (0.72 + 0.28 * (0.5 + 0.5 * Math.sin(time * u.speed + u.phase)));
  }

  for (const coal of camp.coals) {
    const u = coal.userData;
    const k = 0.5 + 0.5 * Math.sin(time * u.speed + u.phase);
    coal.material.color.copy(coalCool).lerp(coalHot, k);
    const sc = u.base * (0.85 + k * 0.35);
    coal.scale.setScalar(sc);
  }

  camp.pool.material.opacity = 0.05 + flicker * 0.05;
  camp.poolInner.material.opacity = 0.1 + flicker * 0.1;

  updateSparks(sparks, dt, time);
  updateEmbers(embers, dt, time);
  updateFireflies(fireflies, time);

  for (const t of swayTrees) {
    t.mesh.rotation.x = t.bx + Math.sin(time * 0.45 + t.phase) * 0.012;
    t.mesh.rotation.z = t.bz + Math.cos(time * 0.38 + t.phase) * 0.012;
  }
  for (const g of grassTufts) {
    g.mesh.rotation.z = Math.sin(time * 1.25 + g.phase) * 0.07;
  }

  const moonGlowPulse = 0.92 + Math.sin(time * 0.6) * 0.08;
  camp.moonGlows[0].material.opacity = 0.11 * moonGlowPulse;
  camp.moonGlows[1].material.opacity = 0.055 * moonGlowPulse;
  camp.moonGlows[2].material.opacity = 0.028 * moonGlowPulse;
  const toCam = camTo(moonPos);
  for (const g of camp.moonGlows) {
    g.position.copy(moonPos).addScaledVector(toCam, 0.4);
    g.lookAt(camera.position);
  }
  for (const s of camp.brightStars) {
    const k = 0.75 + 0.25 * Math.sin(time * s.userData.speed + s.userData.phase);
    s.scale.setScalar(k);
  }
  camp.starMatA.opacity = 0.62 + Math.sin(time * 1.35) * 0.2;
  camp.starMatB.opacity = 0.55 + Math.sin(time * 1.9 + 1.4) * 0.28;

  controls.update();
  if (camera.position.y < 0.45) camera.position.y = 0.45;
  renderer.render(scene, camera);
}

function createSky(moonPosition) {
  const geo = new THREE.SphereGeometry(140, 30, 20);
  const pos = geo.attributes.position;
  const colors = new Float32Array(pos.count * 3);
  const zenith = new THREE.Color(0x080c18);
  const horizon = new THREE.Color(FOG_COLOR);
  const glow = new THREE.Color(0x445580);
  const moonN = moonPosition.clone().normalize();
  const v = new THREE.Vector3();
  const col = new THREE.Color();
  for (let i = 0; i < pos.count; i++) {
    v.fromBufferAttribute(pos, i);
    const yn = v.y / 140;
    const t = smoothstep(-0.08, 0.62, yn);
    col.copy(horizon).lerp(zenith, Math.pow(t, 0.72));
    const d = Math.max(0, v.clone().normalize().dot(moonN));
    col.lerp(glow, Math.pow(d, 5) * 0.62);
    colors[i * 3] = col.r;
    colors[i * 3 + 1] = col.g;
    colors[i * 3 + 2] = col.b;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  const mat = new THREE.MeshBasicMaterial({
    vertexColors: true,
    side: THREE.BackSide,
    fog: false,
    depthWrite: false,
    toneMapped: false
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.renderOrder = -2;
  scene.add(mesh);
}

function createStars() {
  campHold.starMatA = makeStarLayer(420, 1.55, 78, 108, 0.992);
  campHold.starMatB = makeStarLayer(90, 2.35, 72, 100, 0.985);
  campHold.brightStars = [];
  const armMat = new THREE.MeshBasicMaterial({ color: 0xfff8e8, fog: false, toneMapped: false });
  const armMatCool = new THREE.MeshBasicMaterial({ color: 0xdbe8ff, fog: false, toneMapped: false });
  const moonN = moonPos.clone().normalize();
  let made = 0;
  let guard = 0;
  while (made < 11 && guard < 400) {
    guard++;
    const p = randomSkyPoint(62, 88);
    if (p.clone().normalize().dot(moonN) > 0.975) continue;
    const g = new THREE.Group();
    const mat = rand() > 0.8 ? armMatCool : armMat;
    const len = 0.85 + rand() * 0.9;
    const h = new THREE.Mesh(new THREE.BoxGeometry(len, 0.07, 0.07), mat);
    const v = new THREE.Mesh(new THREE.BoxGeometry(0.07, len, 0.07), mat);
    g.add(h, v);
    g.position.copy(p);
    g.lookAt(0, 0, 0);
    g.userData.phase = rand() * Math.PI * 2;
    g.userData.speed = 0.8 + rand() * 1.6;
    scene.add(g);
    campHold.brightStars.push(g);
    made++;
  }
}

const campHold = { starMatA: null, starMatB: null, brightStars: [], moonGlows: [] };

function makeStarLayer(count, size, rMin, rMax, avoidDot) {
  const positions = new Float32Array(count * 3);
  const colors = new Float32Array(count * 3);
  const palette = [
    new THREE.Color(0xffffff),
    new THREE.Color(0xfff4e0),
    new THREE.Color(0xd7e6ff),
    new THREE.Color(0xffffff),
    new THREE.Color(0xffe8c4)
  ];
  const moonN = moonPos.clone().normalize();
  let i = 0;
  let guard = 0;
  while (i < count && guard < count * 20) {
    guard++;
    const p = randomSkyPoint(rMin, rMax);
    if (p.clone().normalize().dot(moonN) > avoidDot) continue;
    positions[i * 3] = p.x;
    positions[i * 3 + 1] = p.y;
    positions[i * 3 + 2] = p.z;
    const c = palette[Math.floor(rand() * palette.length)];
    colors[i * 3] = c.r;
    colors[i * 3 + 1] = c.g;
    colors[i * 3 + 2] = c.b;
    i++;
  }
  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  const mat = new THREE.PointsMaterial({
    vertexColors: true,
    size,
    sizeAttenuation: false,
    transparent: true,
    opacity: 0.85,
    depthWrite: false,
    fog: false,
    toneMapped: false
  });
  const pts = new THREE.Points(geo, mat);
  pts.renderOrder = -1;
  pts.frustumCulled = false;
  scene.add(pts);
  return mat;
}

function createMoon(position) {
  const moon = new THREE.Mesh(
    new THREE.SphereGeometry(3.7, 8, 6),
    new THREE.MeshBasicMaterial({ color: 0xfff3c8, fog: false, toneMapped: false })
  );
  moon.position.copy(position);
  scene.add(moon);

  const craterMats = [
    new THREE.MeshBasicMaterial({ color: 0xe4d4a8, fog: false, toneMapped: false }),
    new THREE.MeshBasicMaterial({ color: 0xcbb892, fog: false, toneMapped: false })
  ];
  for (let i = 0; i < 8; i++) {
    const dir = new THREE.Vector3(rand() - 0.5, rand() - 0.5, rand() - 0.5).normalize();
    const c = new THREE.Mesh(
      new THREE.SphereGeometry(0.28 + rand() * 0.42, 5, 4),
      craterMats[i % 2]
    );
    c.position.copy(dir.multiplyScalar(3.45));
    moon.add(c);
  }

  const radii = [7.2, 11.5, 16.5];
  const opacities = [0.11, 0.055, 0.028];
  campHold.moonGlows = radii.map((r, idx) => {
    const mat = new THREE.MeshBasicMaterial({
      color: 0xfff1c2,
      transparent: true,
      opacity: opacities[idx],
      side: THREE.DoubleSide,
      depthWrite: false,
      fog: false,
      toneMapped: false,
      blending: THREE.AdditiveBlending
    });
    const mesh = new THREE.Mesh(new THREE.CircleGeometry(r, 22), mat);
    mesh.position.copy(position);
    mesh.renderOrder = -1;
    scene.add(mesh);
    return mesh;
  });
}

function createGround() {
  const geo = new THREE.PlaneGeometry(140, 140, 40, 40);
  geo.rotateX(-Math.PI / 2);
  const pos = geo.attributes.position;
  const colors = new Float32Array(pos.count * 3);
  const dirt = new THREE.Color(0x8a6244);
  const dirtDark = new THREE.Color(0x4a3426);
  const grass = new THREE.Color(0x1c4a2c);
  const grassL = new THREE.Color(0x2d6840);
  const moss = new THREE.Color(0x3a7048);
  const col = new THREE.Color();
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i);
    const z = pos.getZ(i);
    pos.setY(i, terrainHeight(x, z));
    const d = Math.hypot(x, z);
    const n = rand();
    if (d < 1.5) {
      col.copy(dirtDark);
    } else if (d < 7.5) {
      col.copy(dirt).lerp(grass, smoothstep(3.2, 8.2, d));
      col.offsetHSL(0, 0, (n - 0.5) * 0.04);
    } else {
      col.copy(n > 0.84 ? moss : n > 0.45 ? grassL : grass);
      col.offsetHSL(0, 0, (rand() - 0.5) * 0.05);
    }
    colors[i * 3] = col.r;
    colors[i * 3 + 1] = col.g;
    colors[i * 3 + 2] = col.b;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  pos.needsUpdate = true;
  geo.computeVertexNormals();
  geo.computeBoundingSphere();
  const mesh = new THREE.Mesh(
    geo,
    new THREE.MeshLambertMaterial({ vertexColors: true, flatShading: true })
  );
  mesh.receiveShadow = true;
  scene.add(mesh);
}

function createCampfire() {
  const flames = [];
  const flameGroup = new THREE.Group();
  flameGroup.position.y = 0.28;
  flameGroup.renderOrder = 2;
  const specs = [
    { color: 0xfff8e0, h: 1.15, r: 0.13, op: 0.95, x: 0.0, z: 0.0, speed: 7.2 },
    { color: 0xffe08a, h: 1.5, r: 0.2, op: 0.82, x: 0.06, z: 0.03, speed: 6.1 },
    { color: 0xffa030, h: 1.72, r: 0.32, op: 0.62, x: -0.07, z: 0.05, speed: 5.4 },
    { color: 0xff7a18, h: 1.38, r: 0.28, op: 0.55, x: 0.11, z: -0.06, speed: 6.6 },
    { color: 0xff4e00, h: 1.18, r: 0.26, op: 0.46, x: -0.1, z: -0.05, speed: 5.1 },
    { color: 0xff2e00, h: 0.95, r: 0.22, op: 0.38, x: 0.03, z: 0.11, speed: 7.8 },
    { color: 0xffc860, h: 1.55, r: 0.14, op: 0.74, x: -0.04, z: -0.09, speed: 8.2 },
    { color: 0xfff0c0, h: 0.82, r: 0.1, op: 0.9, x: 0.08, z: 0.06, speed: 9.0 }
  ];
  for (const spec of specs) {
    const geo = flameGeometry(spec.r, spec.h);
    const mat = new THREE.MeshBasicMaterial({
      color: spec.color,
      transparent: true,
      opacity: spec.op,
      side: THREE.DoubleSide,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      toneMapped: false
    });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.set(spec.x, 0, spec.z);
    mesh.userData = {
      phase: rand() * Math.PI * 2,
      speed: spec.speed,
      opacity: spec.op,
      rot: rand() * Math.PI
    };
    mesh.renderOrder = 2;
    flameGroup.add(mesh);
    flames.push(mesh);
  }
  scene.add(flameGroup);

  const up = new THREE.Vector3(0, 1, 0);
  for (let i = 0; i < 6; i++) {
    const a = (i / 6) * Math.PI * 2 + 0.25;
    const len = 0.92 + rand() * 0.28;
    const geo = new THREE.CylinderGeometry(0.028, 0.055, len, 4);
    geo.translate(0, len / 2, 0);
    const stick = new THREE.Mesh(geo, charredMat);
    const base = new THREE.Vector3(Math.cos(a) * 0.4, 0.02, Math.sin(a) * 0.4);
    const tip = new THREE.Vector3(Math.cos(a) * 0.05, 0.92 + rand() * 0.12, Math.sin(a) * 0.05);
    stick.quaternion.setFromUnitVectors(up, tip.sub(base).normalize());
    stick.position.copy(base);
    stick.castShadow = true;
    stick.receiveShadow = true;
    scene.add(stick);
  }

  for (let i = 0; i < 5; i++) {
    const log = createLog(1.35 + rand() * 0.35, 0.08 + rand() * 0.04, i % 2 ? charredMat : woodMaterials[2]);
    log.rotation.y = (i / 5) * Math.PI + rand() * 0.2;
    log.rotation.z = (rand() - 0.5) * 0.18;
    log.position.set((rand() - 0.5) * 0.12, 0.1 + (i % 3) * 0.07, (rand() - 0.5) * 0.12);
    scene.add(log);
  }

  for (let i = 0; i < 10; i++) {
    const a = (i / 10) * Math.PI * 2 + 0.15;
    const rr = 1.55 + rand() * 0.28;
    const rock = makeRock(0.3 + rand() * 0.26, ringRockMaterials);
    rock.position.set(Math.cos(a) * rr, -0.02, Math.sin(a) * rr);
    scene.add(rock);
  }

  const scorch = new THREE.Mesh(
    new THREE.CircleGeometry(1.45, 10),
    new THREE.MeshLambertMaterial({ color: 0x3a2c22, flatShading: true })
  );
  scorch.rotation.x = -Math.PI / 2;
  scorch.position.y = 0.025;
  scorch.receiveShadow = true;
  scene.add(scorch);

  const pool = new THREE.Mesh(
    new THREE.CircleGeometry(3.6, 20),
    new THREE.MeshBasicMaterial({
      color: 0xff6a1a,
      transparent: true,
      opacity: 0.08,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      toneMapped: false
    })
  );
  pool.rotation.x = -Math.PI / 2;
  pool.position.y = 0.04;
  pool.renderOrder = 1;
  scene.add(pool);

  const poolInner = new THREE.Mesh(
    new THREE.CircleGeometry(1.7, 14),
    new THREE.MeshBasicMaterial({
      color: 0xff9340,
      transparent: true,
      opacity: 0.16,
      depthWrite: false,
      blending: THREE.AdditiveBlending,
      toneMapped: false
    })
  );
  poolInner.rotation.x = -Math.PI / 2;
  poolInner.position.y = 0.05;
  poolInner.renderOrder = 1;
  scene.add(poolInner);

  const coals = [];
  for (let i = 0; i < 9; i++) {
    const coal = new THREE.Mesh(
      new THREE.IcosahedronGeometry(0.07 + rand() * 0.05, 0),
      new THREE.MeshBasicMaterial({ color: 0xff5500, toneMapped: false })
    );
    const a = rand() * Math.PI * 2;
    const rr = rand() * 0.38;
    coal.position.set(Math.cos(a) * rr, 0.08, Math.sin(a) * rr);
    coal.userData = { phase: rand() * 6, speed: 4 + rand() * 6, base: 0.8 + rand() * 0.5 };
    scene.add(coal);
    coals.push(coal);
  }

  return {
    flames,
    coals,
    pool,
    poolInner,
    moonGlows: campHold.moonGlows,
    brightStars: campHold.brightStars,
    get starMatA() { return campHold.starMatA; },
    get starMatB() { return campHold.starMatB; }
  };
}

function createProps() {
  const seatA = createLog(2.35, 0.18, woodMaterials[0]);
  seatA.rotation.y = 0.45;
  seatA.position.set(2.55, 0.18, 3.05);
  scene.add(seatA);

  const seatB = createLog(2.15, 0.16, woodMaterials[1]);
  seatB.rotation.y = -0.7;
  seatB.position.set(-3.35, 0.16, 1.55);
  scene.add(seatB);

  const stump = new THREE.Mesh(
    new THREE.CylinderGeometry(0.28, 0.34, 0.42, 6),
    woodMaterials[2]
  );
  stump.position.set(3.15, 0.21, -0.35);
  stump.castShadow = true;
  stump.receiveShadow = true;
  scene.add(stump);

  const roast = new THREE.Group();
  const stickMesh = new THREE.Mesh(
    new THREE.CylinderGeometry(0.02, 0.032, 2.15, 5),
    woodMaterials[0]
  );
  stickMesh.castShadow = true;
  stickMesh.position.y = 1.05;
  const mallow = new THREE.Mesh(
    new THREE.SphereGeometry(0.082, 6, 5),
    new THREE.MeshLambertMaterial({ color: 0xfff8f3, flatShading: true })
  );
  mallow.position.y = 2.12;
  mallow.castShadow = true;
  const toast = new THREE.Mesh(
    new THREE.SphereGeometry(0.036, 4, 3),
    new THREE.MeshLambertMaterial({ color: 0xc48458, flatShading: true })
  );
  toast.position.set(0.03, 2.16, 0.025);
  roast.add(stickMesh, mallow, toast);
  roast.position.set(1.45, 0.02, 0.95);
  roast.rotation.z = -0.58;
  roast.rotation.y = -0.55;
  scene.add(roast);

  const mug = new THREE.Mesh(
    new THREE.CylinderGeometry(0.09, 0.075, 0.13, 6),
    new THREE.MeshLambertMaterial({ color: 0xc5d0d6, flatShading: true })
  );
  mug.position.set(2.15, 0.08, 2.15);
  mug.castShadow = true;
  mug.receiveShadow = true;
  scene.add(mug);

  const bed = new THREE.Mesh(
    new THREE.CylinderGeometry(0.2, 0.2, 0.85, 6),
    new THREE.MeshLambertMaterial({ color: 0x8d3a4c, flatShading: true })
  );
  bed.geometry.rotateZ(Math.PI / 2);
  bed.position.set(1.85, 0.2, 4.15);
  bed.rotation.y = 0.4;
  bed.castShadow = true;
  bed.receiveShadow = true;
  scene.add(bed);

  const pile = new THREE.Group();
  for (let i = 0; i < 4; i++) {
    const log = createLog(1.15, 0.09, woodMaterials[i % 3]);
    log.rotation.y = 0.25 + (i % 2) * 0.2;
    log.position.y = 0.09 + i * 0.15;
    pile.add(log);
  }
  pile.position.set(-3.7, 0, -1.8);
  pile.rotation.y = 0.4;
  scene.add(pile);

  const boulder = makeRock(0.85, rockMaterials);
  boulder.position.set(-5.4, 0.05, 2.2);
  scene.add(boulder);

  for (let i = 0; i < 7; i++) {
    const a = rand() * Math.PI * 2;
    const rr = 5.2 + rand() * 4.5;
    const rock = makeRock(0.22 + rand() * 0.38, rockMaterials);
    const x = Math.cos(a) * rr;
    const z = Math.sin(a) * rr;
    rock.position.set(x, terrainHeight(x, z) - 0.04, z);
    scene.add(rock);
  }

  for (let i = 0; i < 14; i++) {
    const a = rand() * Math.PI * 2;
    const rr = 2.4 + rand() * 6.5;
    const pebble = makeRock(0.06 + rand() * 0.07, rockMaterials);
    pebble.position.set(Math.cos(a) * rr, 0.02, Math.sin(a) * rr);
    pebble.castShadow = false;
    scene.add(pebble);
  }

  for (let i = 0; i < 7; i++) {
    const a = rand() * Math.PI * 2;
    const rr = 5.5 + rand() * 3.2;
    addMushroom(Math.cos(a) * rr, Math.sin(a) * rr, 1.1 + rand() * 0.8);
  }

  for (let i = 0; i < 20; i++) {
    const a = rand() * Math.PI * 2;
    const rr = 2.8 + rand() * 7.5;
    const x = Math.cos(a) * rr;
    const z = Math.sin(a) * rr;
    if (Math.hypot(x - camera.position.x, z - camera.position.z) < 1.6) continue;
    const tuft = createTuft(0.85 + rand() * 0.7);
    tuft.position.set(x, terrainHeight(x, z), z);
    tuft.rotation.y = rand() * Math.PI;
    scene.add(tuft);
    grassTufts.push({ mesh: tuft, phase: rand() * Math.PI * 2 });
  }

  for (let i = 0; i < 3; i++) {
    const a = rand() * Math.PI * 2;
    const rr = 14 + rand() * 6;
    const log = createLog(2.2 + rand() * 1.3, 0.12 + rand() * 0.05, woodMaterials[i]);
    const x = Math.cos(a) * rr;
    const z = Math.sin(a) * rr;
    log.rotation.y = rand() * Math.PI;
    log.position.set(x, terrainHeight(x, z) + 0.14, z);
    scene.add(log);
  }
}

function createForest() {
  const occupied = [];
  let count = 0;
  let attempts = 0;
  while (count < 46 && attempts < 4000) {
    attempts++;
    const inner = rand() < 0.72;
    const r = inner ? 12.2 + rand() * 10 : 22 + rand() * 18;
    const a = rand() * Math.PI * 2;
    const x = Math.cos(a) * r;
    const z = Math.sin(a) * r;
    if (blocksMoon(x, z)) continue;
    if (Math.hypot(x - camera.position.x, z - camera.position.z) < 3.2) continue;
    let ok = true;
    for (const p of occupied) {
      const dx = p.x - x;
      const dz = p.z - z;
      if (dx * dx + dz * dz < 4.4) { ok = false; break; }
    }
    if (!ok) continue;
    const scale = 0.78 + rand() * 1.05;
    addTree(x, z, scale, rand() < 0.22);
    occupied.push({ x, z });
    count++;
  }
  if (count < 36) {
    for (let i = 0; count < 40 && i < 80; i++) {
      const a = (i / 40) * Math.PI * 2;
      const r = 13 + (i % 6) * 1.6;
      const x = Math.cos(a) * r;
      const z = Math.sin(a) * r;
      if (blocksMoon(x, z)) continue;
      addTree(x, z, 0.9 + (i % 3) * 0.28, false);
      count++;
    }
  }
}

function createAccentTrees() {
  for (const sign of [-1, 1]) {
    const p = camera.position.clone()
      .addScaledVector(viewDir, 4.8)
      .addScaledVector(right, sign * 3.6);
    p.y = 0;
    let dist = Math.hypot(p.x, p.z);
    if (dist < 5.2) {
      p.x *= 5.2 / Math.max(dist, 0.001);
      p.z *= 5.2 / Math.max(dist, 0.001);
    }
    if (Math.hypot(p.x, p.z) < 3.2) continue;
    addTree(p.x, p.z, sign < 0 ? 1.35 : 1.2, false);
  }
}

function addTree(x, z, scale, broadleaf) {
  const tree = broadleaf ? createBroadleaf(scale) : createPine(scale);
  tree.position.set(x, terrainHeight(x, z) - 0.04, z);
  tree.rotation.y = rand() * Math.PI * 2;
  const bx = (rand() - 0.5) * 0.07;
  const bz = (rand() - 0.5) * 0.07;
  tree.rotation.x = bx;
  tree.rotation.z = bz;
  scene.add(tree);
  swayTrees.push({ mesh: tree, phase: rand() * Math.PI * 2, bx, bz });
}

function createPine(scale) {
  const g = new THREE.Group();
  const trunkMat = trunkMaterials[Math.floor(rand() * trunkMaterials.length)];
  const leafMat = leafMaterials[Math.floor(rand() * leafMaterials.length)];
  const near = true;
  const trunkH = 1.15 * scale;
  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.09 * scale, 0.15 * scale, trunkH, 5), trunkMat);
  trunk.position.y = trunkH * 0.5;
  trunk.castShadow = near;
  trunk.receiveShadow = true;
  g.add(trunk);
  for (let i = 0; i < 3; i++) {
    const t = i / 2;
    const radius = (1.15 - t * 0.55) * scale;
    const height = (1.35 - t * 0.25) * scale;
    const cone = new THREE.Mesh(new THREE.ConeGeometry(radius, height, 6), leafMat);
    cone.position.y = 0.85 * scale + i * 0.62 * scale;
    cone.rotation.y = rand() * Math.PI;
    cone.castShadow = near;
    cone.receiveShadow = true;
    g.add(cone);
  }
  g.userData.near = near;
  return g;
}

function createBroadleaf(scale) {
  const g = new THREE.Group();
  const trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.11 * scale, 0.17 * scale, 1.35 * scale, 5),
    trunkMaterials[Math.floor(rand() * trunkMaterials.length)]
  );
  trunk.position.y = 0.67 * scale;
  trunk.castShadow = true;
  trunk.receiveShadow = true;
  g.add(trunk);
  const leafMat = leafMaterials[Math.floor(rand() * leafMaterials.length)];
  for (let i = 0; i < 3; i++) {
    const canopy = new THREE.Mesh(new THREE.IcosahedronGeometry((0.62 + rand() * 0.28) * scale, 0), leafMat);
    canopy.position.set((rand() - 0.5) * 0.28 * scale, (1.35 + i * 0.32) * scale, (rand() - 0.5) * 0.28 * scale);
    canopy.castShadow = true;
    canopy.receiveShadow = true;
    g.add(canopy);
  }
  return g;
}

function createSparkPoints() {
  const count = 120;
  const positions = new Float32Array(count * 3);
  const vel = new Float32Array(count * 3);
  const life = new Float32Array(count);
  const maxLife = new Float32Array(count);
  for (let i = 0; i < count; i++) resetSpark(i, positions, vel, life, maxLife, true);
  const geo = new THREE.BufferGeometry();
  const posAttr = new THREE.BufferAttribute(positions, 3);
  posAttr.setUsage(THREE.DynamicDrawUsage);
  geo.setAttribute('position', posAttr);
  const mat = new THREE.PointsMaterial({
    color: 0xffcc77,
    size: 0.16,
    transparent: true,
    opacity: 0.92,
    depthWrite: false,
    blending: THREE.AdditiveBlending,
    sizeAttenuation: true,
    fog: false,
    toneMapped: false
  });
  const pts = new THREE.Points(geo, mat);
  pts.frustumCulled = false;
  pts.renderOrder = 4;
  scene.add(pts);
  return { positions, vel, life, maxLife, posAttr, count };
}

function createEmbers() {
  const list = [];
  const geo = new THREE.OctahedronGeometry(0.07, 0);
  for (let i = 0; i < 18; i++) {
    const mesh = new THREE.Mesh(
      geo,
      new THREE.MeshBasicMaterial({ color: 0xffb347, transparent: true, opacity: 1, toneMapped: false, fog: false })
    );
    mesh.renderOrder = 5;
    resetEmber(mesh, true);
    scene.add(mesh);
    list.push(mesh);
  }
  return list;
}

function createFireflies() {
  const list = [];
  const coreGeo = new THREE.OctahedronGeometry(0.055, 0);
  const glowGeo = new THREE.SphereGeometry(0.16, 6, 4);
  for (let i = 0; i < 16; i++) {
    const mat = new THREE.MeshBasicMaterial({
      color: 0xe8ff9a,
      transparent: true,
      opacity: 0.95,
      toneMapped: false,
      fog: false
    });
    const mesh = new THREE.Mesh(coreGeo, mat);
    const glow = new THREE.Mesh(
      glowGeo,
      new THREE.MeshBasicMaterial({
        color: 0xb6e86a,
        transparent: true,
        opacity: 0.28,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        toneMapped: false,
        fog: false
      })
    );
    mesh.add(glow);
    const a = rand() * Math.PI * 2;
    const rr = 4.3 + rand() * 7.2;
    mesh.userData = {
      baseX: Math.cos(a) * rr,
      baseY: 0.55 + rand() * 1.5,
      baseZ: Math.sin(a) * rr,
      phase: rand() * Math.PI * 2,
      speed: 0.35 + rand() * 0.55,
      radius: 0.35 + rand() * 0.7
    };
    scene.add(mesh);
    list.push(mesh);
  }
  return list;
}

function updateSparks(s, dt, time) {
  for (let i = 0; i < s.count; i++) {
    s.life[i] += dt;
    const i3 = i * 3;
    if (s.life[i] > s.maxLife[i] || s.positions[i3 + 1] > 4.6) {
      resetSpark(i, s.positions, s.vel, s.life, s.maxLife, false);
      continue;
    }
    s.vel[i3] += Math.sin(time * 3 + i) * 0.35 * dt;
    s.vel[i3 + 2] += Math.cos(time * 2.4 + i * 0.7) * 0.35 * dt;
    s.vel[i3 + 1] += 0.15 * dt;
    s.positions[i3] += s.vel[i3] * dt;
    s.positions[i3 + 1] += s.vel[i3 + 1] * dt;
    s.positions[i3 + 2] += s.vel[i3 + 2] * dt;
  }
  s.posAttr.needsUpdate = true;
}

function updateEmbers(list, dt, time) {
  for (const e of list) {
    const u = e.userData;
    u.life += dt;
    if (u.life > u.maxLife || e.position.y > 4.4) {
      resetEmber(e, false);
      continue;
    }
    u.vx += Math.sin(time * 3.2 + u.phase) * 0.4 * dt;
    u.vz += Math.cos(time * 2.6 + u.phase) * 0.4 * dt;
    e.position.x += u.vx * dt;
    e.position.y += u.vy * dt;
    e.position.z += u.vz * dt;
    const k = 1 - u.life / u.maxLife;
    e.material.opacity = Math.max(0, k);
    e.material.color.copy(emberCool).lerp(emberHot, k);
    e.scale.setScalar(u.baseScale * (0.55 + k * 0.7));
  }
}

function updateFireflies(list, time) {
  for (const f of list) {
    const u = f.userData;
    f.position.set(
      u.baseX + Math.sin(time * u.speed + u.phase) * u.radius,
      u.baseY + Math.sin(time * u.speed * 1.7 + u.phase) * 0.28,
      u.baseZ + Math.cos(time * u.speed * 0.85 + u.phase) * u.radius
    );
    const blink = 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(time * (1.6 + u.speed) + u.phase));
    f.material.opacity = blink;
    f.children[0].material.opacity = blink * 0.34;
  }
}

function resetSpark(i, positions, vel, life, maxLife, scatter) {
  const a = Math.random() * Math.PI * 2;
  const rad = Math.random() * 0.28;
  const vx = (Math.random() - 0.5) * 0.55;
  const vy = 1.35 + Math.random() * 2.1;
  const vz = (Math.random() - 0.5) * 0.55;
  const max = 1.05 + Math.random() * 1.45;
  const t = scatter ? Math.random() * max : 0;
  const i3 = i * 3;
  positions[i3] = Math.cos(a) * rad + vx * t;
  positions[i3 + 1] = 0.35 + vy * t;
  positions[i3 + 2] = Math.sin(a) * rad + vz * t;
  vel[i3] = vx;
  vel[i3 + 1] = vy;
  vel[i3 + 2] = vz;
  life[i] = t;
  maxLife[i] = max;
}

function resetEmber(e, scatter) {
  const a = Math.random() * Math.PI * 2;
  const rad = Math.random() * 0.24;
  const vx = (Math.random() - 0.5) * 0.4;
  const vy = 1.15 + Math.random() * 1.9;
  const vz = (Math.random() - 0.5) * 0.4;
  const max = 1.15 + Math.random() * 1.35;
  const t = scatter ? Math.random() * max : 0;
  e.userData.vx = vx;
  e.userData.vy = vy;
  e.userData.vz = vz;
  e.userData.life = t;
  e.userData.maxLife = max;
  e.userData.phase = Math.random() * 6;
  e.userData.baseScale = 0.75 + Math.random() * 0.7;
  e.position.set(Math.cos(a) * rad + vx * t, 0.32 + vy * t, Math.sin(a) * rad + vz * t);
  e.scale.setScalar(e.userData.baseScale);
}

function flameGeometry(radius, height) {
  const geo = new THREE.ConeGeometry(radius, height, 4 + Math.floor(rand() * 3));
  const pos = geo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    if (pos.getY(i) < height * 0.15) {
      const wobble = 0.7 + rand() * 0.6;
      pos.setX(i, pos.getX(i) * wobble);
      pos.setZ(i, pos.getZ(i) * wobble);
    }
  }
  geo.translate(0, height / 2, 0);
  pos.needsUpdate = true;
  geo.computeVertexNormals();
  return geo;
}

function createLog(length, radius, mat) {
  const geo = new THREE.CylinderGeometry(radius * 0.92, radius, length, 6);
  geo.rotateZ(Math.PI / 2);
  const mesh = new THREE.Mesh(geo, mat);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

function makeRock(size, mats) {
  const geo = rand() > 0.45
    ? new THREE.DodecahedronGeometry(1, 0)
    : new THREE.IcosahedronGeometry(1, 0);
  const mesh = new THREE.Mesh(geo, mats[Math.floor(rand() * mats.length)]);
  mesh.scale.set(
    size * (0.8 + rand() * 0.45),
    size * (0.42 + rand() * 0.4),
    size * (0.8 + rand() * 0.5)
  );
  mesh.rotation.set(rand() * Math.PI, rand() * Math.PI, rand() * Math.PI);
  mesh.castShadow = true;
  mesh.receiveShadow = true;
  return mesh;
}

function createTuft(scale) {
  const g = new THREE.Group();
  const mat = leafMaterials[Math.floor(rand() * leafMaterials.length)];
  const n = 3 + Math.floor(rand() * 2);
  for (let i = 0; i < n; i++) {
    const h = (0.28 + rand() * 0.34) * scale;
    const blade = new THREE.Mesh(new THREE.ConeGeometry(0.045 * scale, h, 4), mat);
    blade.position.set((rand() - 0.5) * 0.16, h * 0.5, (rand() - 0.5) * 0.16);
    blade.rotation.z = (rand() - 0.5) * 0.4;
    blade.rotation.x = (rand() - 0.5) * 0.4;
    g.add(blade);
  }
  return g;
}

function addMushroom(x, z, s) {
  const g = new THREE.Group();
  const stem = new THREE.Mesh(
    new THREE.CylinderGeometry(0.04 * s, 0.06 * s, 0.2 * s, 5),
    new THREE.MeshLambertMaterial({ color: 0xefe6d8, flatShading: true })
  );
  stem.position.y = 0.1 * s;
  const cap = new THREE.Mesh(
    new THREE.ConeGeometry(0.16 * s, 0.12 * s, 6),
    new THREE.MeshLambertMaterial({ color: 0xe24b3a, flatShading: true })
  );
  cap.position.y = 0.2 * s;
  cap.castShadow = true;
  g.add(stem, cap);
  g.position.set(x, terrainHeight(x, z), z);
  scene.add(g);
}

function terrainHeight(x, z) {
  const d = Math.hypot(x, z);
  const mask = smoothstep(8, 14, d);
  const n =
    Math.sin(x * 0.28) * 0.42 +
    Math.cos(z * 0.24) * 0.34 +
    Math.sin(x * 0.63 + z * 0.47) * 0.16;
  return n * mask;
}

function blocksMoon(x, z) {
  _treeVec.set(x, terrainHeight(x, z) + 2.4, z).sub(camera.position).normalize();
  return _treeVec.dot(moonDir) > 0.962;
}

function randomSkyPoint(rMin, rMax) {
  const r = rMin + rand() * (rMax - rMin);
  const y = 0.08 + rand() * 0.92;
  const ring = Math.sqrt(Math.max(0, 1 - y * y));
  const theta = rand() * Math.PI * 2;
  return new THREE.Vector3(r * ring * Math.cos(theta), r * y, r * ring * Math.sin(theta));
}

function flameFlicker(t) {
  return 0.82
    + Math.sin(t * 8.0) * 0.1
    + Math.sin(t * 13.7) * 0.07
    + Math.sin(t * 21.3) * 0.05
    + Math.sin(t * 3.1) * 0.12;
}

function smoothstep(e0, e1, x) {
  const t = Math.min(1, Math.max(0, (x - e0) / (e1 - e0)));
  return t * t * (3 - 2 * t);
}

function camTo(pos) {
  return _toCam.copy(camera.position).sub(pos).normalize();
}

function mulberry32(a) {
  return function () {
    a |= 0;
    a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const _treeVec = new THREE.Vector3();
const _toCam = new THREE.Vector3();
</script>
</body>
</html>
Reasoning trace (82,274 tokens)
I'm starting to build a low-poly Three.js campfire scene in a forest clearing at night.

I'm planning the scene structure: ground with rocks and logs, 30 random low-poly trees, animated flickering flames with warm point light and shadows, rising spark particles, a dark night sky with stars and moon, fog for distant trees, and OrbitControls with camera limits. I'll make the visuals visually striking for a screenshot comparison.

I'll use flat-shaded Lambert or Phong materials for the low-poly look. The palette will contrast warm orange fire glow against cool moonlight and deep forest blues. I'm planning a dim ambient light for the moon, a directional light for the moon, and a warm flickering point light at the fire. Fog will use a dark blue exponential gradient to fade distant trees.

The ground will be a subdivided plane with vertex displacement for natural variation. The clearing center will be lighter brown dirt against darker grass outside. Rocks will be irregularly scaled icosahedrons or dodecahedrons in brown and gray. Logs will be cylinders arranged around the fire. Pine trees will use stacked cones or simple trunks with foliage. All trees will be placed randomly at least a few units away from the fire.

Flames will use multiple cone meshes that scale and rotate with oscillation, rendered in orange, yellow, and red with additive blending or emissive materials. Sparks will be small rising particles with gravity and fade. The sky will be a dark sphere with point stars and a moon. OrbitControls will be imported from the three/addons/controls module, with camera limits set to keep the view above ground. Shadow mapping will be enabled, and the fire point light will cast shadows.

A subtle ground shadow will be added. The scene must look complete and beautiful immediately, with animations running so the flames are visible right away. I'll keep the code free of console errors. The import map will be followed by a module script importing Three.js.

I've locked in the exact hex color values for the palette. The sky and fog will use deep midnight blues like #0a0e1a. Ground dirt will be #3d2b1f, grass #1a3a22, and tree trunks #2a1a10. Foliage will range from #1a4a28 to #245c30. Rocks will be #4a4a52 and #3a3a42, logs #5c3a1e, and fire will use #ff6a00, #ffcc33, and #ff3300. The moon will be pale #e8e4d0 with cool #6a7a9a moonlight. I'm also deciding between a circular or noise-displaced plane for the ground.

I'll use a CircleGeometry with vertex displacement for the uneven ground, coloring vertices by distance from center to create a dirt center and grass edges. The trees will be stacked pine cones on trunk cylinders, placed in a ring from radius 8 to 45 with 50 to 60 instances, randomly rotated and scaled, and kept clear of the fire area. Rocks will be 8 to 12 irregular icosahedrons, logs 4 to 6 circular logs with slight rotation and bark variation, and a couple of seating logs.

The campfire will use a stone ring and crossed logs. The point light will sit at fire height with color 0xff7722 and intensity oscillating between 8 and 15. Shadow map resolution will be 1024 or 2048. Spark particles will use BufferGeometry points with additive blending, updating each frame to rise and drift before resetting.

Stars will be placed in a hemisphere above the horizon. The moon will be a slightly emissive sphere mesh with a directional light. Fog will use a dark blue exponential gradient. OrbitControls will be configured with damping, a target at the clearing center, and camera limits from 4 to 40 units.

The camera will start at (12, 6, 14) aimed at the fire for a strong initial screenshot. The renderer will use ACESFilmicToneMapping with exposure near 1.0 to preserve the warm fire glow.

I'm setting the renderer output color space to SRGBColorSpace for Three.js r170. The lighting plan is now more specific: a dim ambient light at 0x1a2030 with intensity 0.4, a cool moon directional light at 0x8899bb with intensity 0.6, and the fire point light at intensity 12–20 with decay 2. I'm also verifying whether Three.js r155+ requires higher physical-intensity values for point lights.

Three.js r155+ uses physically correct lighting where PointLight intensity is in candela. A value of 1 is extremely dim, so the campfire point light will need a base intensity around 40 with a flicker range of 30 to 55 to remain visible at distance.

A hemisphere light will provide a subtle fill with sky color 0x1a2848 and ground color 0x1a1008 at intensity 0.4. A second non-shadow-casting orange point light will sit slightly above the fire for extra fill. The shadow map resolution will be 512 with a bias of -0.005. The flame meshes will use emissive materials so they remain visible even if the scene lighting is disabled.

I'll limit shadow casting to nearby objects only to keep the scene performant. The fire point light will cast shadows, and logs, rocks, and trees within a certain radius will cast shadows too. Ground and distant trees will still receive shadows. I'm now designing the low-poly pine tree geometry.

I'm now writing the `createTree` function. Each tree will be a group with a flat-shaded trunk cylinder and randomly colored foliage.

The `createTree` function now stacks three flat-shaded cones at different heights to form the foliage. Scale will vary from 0.8 to 2.2 so the forest looks natural and not uniform.

Tree placement will use rejection sampling to keep instances at least a few units apart. The ground will be a 100×100 PlaneGeometry with 40×40 segments, rotated flat and vertex-displaced to create a low-poly uneven surface.

Ground vertices are displaced with a layered sine and cosine noise function, scaled by distance from center so the clearing stays flatter than the surrounding grass. Colors are blended from a warm brown dirt core into a darker grass edge, with slight random variation across the plane.

The ground mesh will use vertex colors and flat shading for a low-poly look. Rocks will be randomly scaled icosahedrons placed in a ring around the fire. Logs will be horizontal cylinders arranged in a crisscross fire pit with charred dark coloring, plus larger seating logs further out. The flames will be a group of five to seven cones.

The flame cones will use a MeshBasicMaterial with transparent orange coloring and double-sided rendering so they glow independently of the scene lighting. Each cone will animate with a unique time offset, scaling up and down in height and width while gently rotating and bobbing vertically to mimic flickering.

The inner flame will be a bright yellow cone, while the outer edges fade to a more transparent red. For the spark particles, I am setting up a BufferGeometry with 80 points, each initialized with a random position and velocity vector so they rise and drift away from the fire.

Spark particles will use a PointsMaterial with size 0.08, orange color, additive blending, and size attenuation. Stars will be 400 points in a buffer geometry, randomly placed in an upper hemisphere around the scene.

Stars will be placed on a sphere with random spherical coordinates, rejecting any that fall below y=8 to keep them above the ground. They will use white or pale blue colors with varying sizes, including a few brighter points.

A few larger octahedron meshes will serve as twinkling stars. The moon will be a low-poly sphere at (-25, 28, -20) with a pale yellow basic material and a soft transparent glow. Moonlight will cast from the moon toward the clearing. The renderer clear color will match the fog gradient.

A large inside-out sphere will serve as the sky with vertex colors fading from dark blue-black at the horizon to a fainter blue at the zenith. The sky material will disable fog so the stars and moon remain visible while the fog only affects the forest.

The camera will start at (14, 7, 16) aimed at the clearing center with a 50-degree field of view. The scene will remain static for the three-second screenshot, relying on animated flames and rising sparks to create visual interest.

A slow automatic camera orbit will keep the scene alive for video recordings. OrbitControls will handle this via autoRotate, which pauses automatically when the user starts dragging.

I will disable the automatic camera orbit on pointer or keydown events so the user can take full control. No click-to-start overlay is needed because the scene relies on mouse orbit rather than keyboard input.

A minimal bottom overlay will display the title "Night Clearing" and a subtle hint to drag and scroll for interaction. This keeps the scene uncluttered while still guiding the viewer.

The overlay text will be styled to avoid default browser fonts and placed so it does not obscure the campfire. The visual plan now emphasizes strong contrast between the warm fire and cool moonlit forest, with every element kept in a consistent low-poly faceted style. Nearby logs and rocks will catch the fire's orange glow. A ring of stones will surround the clearing, and simple grass tufts or embers may add subtle life. Smoke planes were considered but set aside as too messy without textures.

A fallen log will sit in the mid-ground, and a few scattered rocks will sit further out. Grass tufts will be small cones placed in the clearing. A low-poly owl was considered but set aside to keep the focus on the campfire. The lighting setup will remain limited to point, directional, ambient, and hemisphere lights, avoiding RectAreaLight to prevent shadow casting issues. Shadow acne will be reduced with polygonOffset or a negative shadow bias.

The fire point light will use a 1024×1024 shadow map with a bias of -0.01 and a camera range of 0.5 to 30. MeshLambertMaterial with flat shading will be used for the ground, rocks, and logs. MeshPhongMaterial is being considered for the rocks to add a subtle wet, glossy finish.

The renderer will use ACES filmic tone mapping with SRGB color space and an exposure of 1.1. A second point light above the fire will add ambient warmth to the distant trees without casting shadows.

Spark particles will be assigned random lifespans between 1.2 and 3.0 seconds. Each frame they rise with a slight upward acceleration, then gradually slow as gravity pulls them down. When a spark expires, it respawns at the fire base. Because PointsMaterial applies a single opacity to all particles, the fading effect will be achieved by animating vertex colors toward black rather than using a separate alpha attribute.

A custom ShaderMaterial will replace the PointsMaterial for the spark particles. This allows per-particle opacity and fading by animating a life attribute in the vertex shader.

The spark shader fades particles toward black as they age and scales their size with distance for depth. Each point is drawn as a soft circular glow with additive blending.

The spark particles will use a simple PointsMaterial with no custom shader, relying on a single opacity attribute to fade them out each frame. The moon will be a basic pale yellow sphere with a few darker offset circles to suggest craters. A second layer of twinkling stars may be added for extra depth.

The moon will use a darker beige material for its craters. Five small offset spheres will be placed on the surface to suggest impact holes. Three concentric transparent circles will form a soft glow disc around the moon, fading in opacity with distance.

The moon group will be oriented to face the camera so its glow discs point toward the clearing. The sky gradient sphere will use a 95-unit radius with vertex colors fading from dark blue-black at the horizon to a fainter blue at the zenith.

Fog will use a linear gradient from 0x0b1020 between 20 and 58 units so distant trees fade into the night sky. The ground plane will be 120 units wide so its edges dissolve into the mist. A subtle orange light pool will sit near the fire to enhance the warm glow.

A transparent orange emissive circle will sit just above the ground under the fire, flickering to keep the glow visible even in dim light. The fire logs will be three or four dark cylinders crossed at the center.

A helper function creates low-poly logs as short cylinders with flat Lambert shading. Five logs are arranged in a circle around the fire, each tilted slightly and staggered vertically for a natural stacked look.

A second log will sit at distance four as a seat. Rocks will be dodecahedrons with flat Lambert materials in muted browns.

Rocks will be randomly rotated and slightly flattened for a natural look. A ring of ten stones will sit at radius 1.6 to 2.0 around the fire, with six to eight scattered further out at radius 5 to 12, plus a couple of boulders. Grass tufts will be small groups of three low-poly cones in a dark green shade.

Grass tufts will be small groups of three low-poly cones in a dark green shade.
Twenty to thirty of them will be scattered in the clearing, not on the fire.
A slight wind animation will sway the grass blades during the render loop.
A tree placement algorithm is being drafted to randomly distribute the thirty or more trees around the clearing.

The tree placement algorithm now uses a radius of 8 to 42 units with a square-root distribution for even spacing. Each new tree is checked against existing positions to prevent overlap. Scales range from 0.7 to 2.3.

A ground height function will keep trees at the correct elevation. The terrain will stay flat near the fire and rise slightly further out, so each tree sits naturally on the surface rather than floating at y=0.

The ground will be a 110-unit plane with 28 by 28 segments so the low-poly facets remain visible. A noise function will keep the terrain flat near the fire and gently slope outward, with vertex colors derived from height and distance. The same height function will also position the trees.

A dark ash circle will sit in the center of the clearing to suggest a burned-out fire bed. The flames will be built from stacked low-poly cones with layered red-orange transparency, offset outward to create depth.

A `createFlame` helper will generate each flame layer from a five-sided cone. It distorts the lower vertices to irregularly widen the base and then computes normals. The flame stack will use three orange mid-flames and two bright yellow-white inner cores.

Flame meshes will use additive blending and double-sided transparent materials so the stacked layers glow. Each flame stores animation state in userData for phase, speed, and scale. The fire light will flicker via layered sine waves at 11 and 23 Hz plus a small random offset, avoiding noisy per-frame randomness.

The flicker function now blends sine waves at 9, 17.3, 31, and 5.2 Hz with a base value of 0.75. The fire light will also drift slightly in the horizontal and vertical axes. The camera will start at (11, 5.5, 13) aimed at (0, 1.2, 0) to frame the fire, moon, and surrounding trees.

The camera field of view is set to 48 degrees to avoid distortion while keeping the surrounding trees visible. A faint directional light from the moon will cast a rim light across the right-side trees, and the fire will light the front of the scene.

The moon light will not cast shadows to keep the scene lightweight. A hemisphere light and ambient light will fill in shadowed areas so the forest does not look completely black.

Three.js now uses physically correct lighting by default. DirectionalLight intensity is measured in lux, so a value of 1 is extremely dim. A stylized moonlight of 0.5 to 2 is more appropriate than a full daylight value. The campfire point light will need a much higher intensity, around 50 to 200, to appear bright in the scene.

The lighting values are now set. The moon directional light will use intensity 1.2. Ambient light is 0.15 and hemisphere light is 0.35. The main fire point light will have intensity 80 with a distance of 25 and decay 2. A secondary fill point light at intensity 30 will sit at distance 18 with decay 2 and no shadows.

The fire light intensity is raised to 150 to keep distant trees warm under the default decay-2 falloff. A second fill light at intensity 40 and distance 40 will provide extra reach. A faint ground glow will be added so the fire remains visible even in dim light.

Tone mapping exposure will be set to 1.25 so the scene stays readable. The palette will stay mid-dark rather than near-black so the trees, rocks, moon, and stars remain visible. Lambert materials will multiply light by color, keeping the forest greens visible under the moonlight while the fire remains the bright focal point.

The material colors are now tuned to stay visible under the dim moonlight. Foliage will use dark greens, trunks browns, grass and dirt earth tones, and rocks muted grays. The renderer will enable soft PCF shadow maps. A resize handler is being added to keep the scene full-screen.

A resize listener updates the camera aspect ratio and renderer size whenever the window changes. CSS locks the page to full viewport with a dark background and hides overflow. A fixed HUD overlay at the bottom left will display instructions in a serif font with a dark text shadow.

OrbitControls will be imported from `three/addons/controls/OrbitControls.js`. The HUD overlay will use responsive CSS with a small font size and reduced opacity on mobile screens.

Post-processing effects like bloom are being skipped to avoid extra complexity and potential errors. Additive blending on the flame layers will provide the glow without needing extra passes. The import map must be placed before the module script, and OrbitControls in r170 requires `controls.update()` if damping is enabled.

Ground vertex normals must be recalculated after terrain height changes, with `needsUpdate` set on the position attribute first. The ground will use vertex colors and flat shading, but fewer segments will keep the color variation chunky and low-poly.

A single ground color and a separate dirt circle were considered, but vertex colors will stay for richer detail. To keep the scene lightweight, tree materials will be pooled from a small palette of shared greens and browns rather than creating unique materials for each of the forty-five trees. Shadow casting from all the trees and the point light may strain weaker hardware, though it should still run.

Shadow map resolution will be capped at 512 to keep the point light shadows from straining the GPU. The renderer pixel ratio will be clamped to 1.5 so the scene stays smooth on high-DPI displays.

The renderer pixel ratio will be capped at 2 to match the 1280 by 800 screenshot viewport. Auto-rotation will spin the scene slowly at speed 0.45, but it will pause on any pointer or keyboard input. Polar angle limits are being set so the camera stays above the ground.

OrbitControls will be restricted to a vertical range of 0.15 to 89.95 degrees and a distance of 5 to 35 units. Panning will be disabled to prevent the camera from dropping below the ground. A hard y-position clamp at 0.8 will be added as a fallback safeguard.

A listener will clamp the camera target to a minimum height of 0.5 so the view never drops below the ground. Twelve subtle fireflies will drift slowly across the clearing to add a magical, living feel to the forest.

The flame group will sit at a height that places the fire on top of the logs. The final output must be a single fenced code block containing the complete HTML file. The scene will adapt to mobile viewports through the renderer resize handler, and OrbitControls will handle touch input. The HTML structure is being drafted with the import map placed correctly before the module script.

A CSS night sky with a moon will render immediately on first paint so the page is never blank. The Three.js canvas will overlay this background once the library loads, ensuring the scene is visible before the three-second screenshot is taken.

Radial gradients will render the starfield and moon in CSS so the night sky is visible before Three.js initializes. A transparent canvas is unnecessary because the renderer uses a solid background color. Low-poly mountain silhouettes will be added in the far distance to deepen the forest depth.

Smoke particles and extra decorative items like an axe or marshmallow stick were considered but rejected to keep the scene focused. The distant cone mountains will sit at radius 50–60 and may fade into the fog.

A low-poly roasting stick with a marshmallow will sit over the fire to add narrative charm. Dead branches will be scattered on the ground for extra detail. The plan is now complete.

Five flame layers are being defined with staggered heights, radii, opacities, and phase offsets to create a layered flicker effect.

Two additional flame layers are being added to the five already defined, completing the flickering stack. Additive blending will let the overlapping layers glow brighter and form a hot core, with depth writes disabled to avoid sorting issues. The spark particle shader must be written in GLSL1 to match Three.js r170 defaults.

Spark particles will be initialized with random lifetimes so they are already visible at the three-second mark rather than all spawning at the base. Their position and life attributes will be marked for update after each frame.

The starfield will use a PointsMaterial with sizeAttenuation disabled so the stars stay a fixed pixel size across the viewport. A few brighter stars will be rendered as small octahedron meshes with MeshBasicMaterial and will twinkle by scaling or fading opacity during the animation loop.

The moon is placed at (-22, 22, -16) so it sits in the upper-left sky as the camera faces the campfire. A dot-product check confirms it lies in front of the camera view direction.

The moon position is being refined to (-18, 24, -22) so it sits clearly inside the camera frame. A careful angle calculation between the view direction and the moon direction is being performed to ensure it remains visible within the 50-degree vertical field of view.

The moon is calculated to sit roughly 37 degrees off-center and 36 degrees above the camera’s view direction. That exceeds the 25-degree half vertical field of view, so it may fall just outside the top of the frame. The position needs to be adjusted so the moon remains fully visible.

The camera is being repositioned to (10, 4.2, 12) aimed at (0, 2.2, 0) so the view looks slightly downward and reveals more sky. The moon is being moved to (-8, 14, -10) to sit higher in the upper portion of the frame. This keeps the moon fully visible within the vertical field of view.

The moon is being lowered to y=11 so it sits roughly 18 degrees above the camera view direction, keeping it fully inside the vertical field of view. It will be placed at world coordinates (-6, 11, -8) with a radius of 2.2 to subtend about 4.3 degrees from the camera distance.

The camera is being repositioned to (9, 3.8, 11) aimed at (0, 1.5, 0) with a 55-degree field of view to keep the moon visible. The moon is being moved to (-8, 10, -9) so it sits roughly 13 degrees above the camera view direction, keeping it safely inside the vertical field of view.

The camera is now set to (8.5, 4.5, 11) aimed at (0, 1.4, 0) with a 52-degree field of view. The moon is placed at (-7, 9.5, -8), sitting roughly 24 degrees above the camera view direction. It sits near the horizontal center, directly above the campfire, keeping it fully inside the frame.

The moon at (-7, 9.5, -8) sits too close to the scene and risks intersecting the trees. It will be scaled outward to (-21, 28.5, -24) so it floats safely in the sky beyond the forest canopy while keeping the same screen position.

The moon is being placed at (-33.1, 20.7, -42.9), roughly 70 units above the camera along a direction that mixes the view direction with an upward offset. This keeps the moon safely in the upper sky without intersecting the forest canopy.

The camera is set to a 60-degree field of view. The moon sits roughly 25.6 degrees above the view center, keeping it safely inside the upper frame. The starfield will wrap around the entire sky sphere so the stars remain visible around the moon.

A transparent ground mist disc was rejected in favor of fog. Dark cone hills will sit at radius 48–55 with height 8–14 to add horizon depth. The fog material will fade them into the distance. Each tree will store a random sway phase and amplitude so they gently rotate during the animation loop.

The fire light will use a warm orange color temperature. The scene will be built in five ordered sections: renderer and camera setup, sky and stars, ground, campfire, and remaining objects.

A roasting stick and marshmallow are being added to the fire. Small orange embers will pulse in the ash. The scene will be built in nine ordered sections: renderer setup, sky, ground, campfire, seating logs, trees, sparks, controls, and the animation loop.

The marshmallow is parented to the stick and offset to the tip. A small brown sphere will mark the toasted side. The geometry constructors for the stick and marshmallow were verified against Three.js r170.

OrbitControls is confirmed as a named export from the three/addons/ import map. The controls target will be set and updated once before the first render so the camera is correct immediately. The animation loop will start right away so shadows and pixel ratio are ready on the first frame.

Mobile drag will be locked to the canvas by disabling touch scrolling. A simple HUD with a Georgia or system-serif title and subtitle will sit at the bottom left. A CSS vignette overlay will dim the edges without blocking interactions.

The vignette will use a radial gradient fading from transparent to dark gray. Tone mapping will compress the additive bright flames. The fire light will cast shadows onto the ground and away from the campfire.

Ambient light is being set to 0.12 and the moon light to 0.8 so fire shadows stay visible near the camp while the moon provides overall visibility. The fire light will use PCFSoftShadowMap with a negative bias of -0.005 to prevent shadow acne.

A normalBias of 0.04 will be added to the fire light to prevent shadow acne on flat-shaded geometry. The light distance is set to 28 so distant trees remain moonlit rather than washed out. The spark system will use 100 particles and be initialized so they are visible immediately and at the three-second mark.

The spark system is being implemented with Float32Array buffers for positions, lifetimes, max lifetimes, and velocities. A resetSpark helper places each particle in a tight cluster around the fire base and gives it a small upward velocity.

Spark velocities are being tuned to a strong upward burst with slight horizontal drift, and lifetimes are randomized between 1 and 2.6 seconds. The update loop will advance each particle’s position using its velocity and lifetime, applying a small downward acceleration to simulate gravity.

Spark particles will be reset when they exceed their lifetime or rise above five units. A slight upward acceleration will be added to each spark to simulate buoyancy. Horizontal drift will be driven by sine and cosine waves keyed to time and particle index. The spark shader will interpolate between hot white-yellow and orange based on each particle’s remaining lifetime.

The spark fragment shader mixes hot white-yellow with orange based on lifetime, discards points outside a circular boundary, and blends them additively using alpha. Eighteen fireflies will be added to the clearing, bobbing at heights of 0.4 to 2 units within a radius of 4 to 12.

Fireflies will use a simple green PointsMaterial with sine-wave bobbing. Twenty-five grass tufts will gently rotate to show wind. A ring of stones will form the fire pit with varied sizes and partial burial. Logs will be arranged in a classic crisscross pattern. The log material will use a dark brown flat-shaded color.

The fire logs are being placed in a crisscross pattern around the campfire. A helper function creates six cylindrical logs with alternating dark and charcoal brown materials, each rotated to a different angle and positioned at a slight height above the ground.

The fire log rotation order is being switched to YXZ to avoid ambiguity with Three.js default XYZ Euler angles.

The fire logs will be created with their geometry already rotated so each cylinder lies horizontally along the X axis. This avoids ambiguity with Three.js Euler angle ordering. A helper function will bake the rotation into each log's geometry before creating the mesh.

The helper function now returns each log mesh with shadow receiving enabled. The crisscross fire logs are centered at the fire pit. Two horizontal seating logs are placed at a slight offset from the center. A vertical stump is added as a low-poly seat alternative.

A few low-poly mushrooms will be placed near the trees. Each one uses a white stem and a red cap with flat shading.

Mushrooms will be placed at radius 5–8 with a scale of 1.5–2 so they remain visible during orbit. A custom ground shader was considered but rejected as too complex.

The fire point light is set to orange with intensity 120 and distance 22. A warm fill light at intensity 40 will sit below the fire without casting shadows. The moon light uses a pale blue tint. The hemispherical light uses deep blue and brown tones. Ambient light is set to dark gray. Exposure is 1.15. Three.js r170 DirectionalLight units are being double-checked.

Three.js r170 DirectionalLight intensity defaults to 1 and is measured in lux. MeshLambertMaterial reacts to light similarly to MeshStandardMaterial, so directional light values of 1–3 are sufficient for proper illumination.

The fire point light intensity is being set to 140. At a distance of 10 meters the light will still provide moonlight-level brightness, which is appropriate for a campfire glow.

The ground height function is being implemented with a smoothstep mask to keep the clearing flat near the fire pit and gently rise outward. A smoothstep helper is being defined to support this terrain masking.

The smoothstep helper is now fully defined. Ground vertices and tree positions will both use terrainHeight so logs and trunks sit naturally at the clearing floor. Rocks in the fire ring will rest at terrain level. The ground color is set to a dark brown dirt shade.

Grass and moss colors are being mixed into the ground vertices using distance and noise. A warm orange tint near the fire was considered but rejected to avoid looking painted-on; the existing warm brown dirt color is sufficient.

The camera auto-rotation speed is being calculated so the scene settles into a nice viewing angle by the three-second mark. OrbitControls autoRotateSpeed of 0.4 is being checked against the library's internal formula to confirm the exact rotation rate.

The auto-rotate speed is frame-rate dependent. A value of 2.0 completes one full orbit in 30 seconds. A value of 0.5 would take 120 seconds. The 0.4 speed chosen earlier will therefore rotate the camera much faster than intended.

The camera will start at a pre-set spherical position matching the intended screenshot angle. Auto-rotate will be slowed so the scene settles into that view within three seconds. Polar angle limits are being set to keep the camera above the ground and prevent straight-down views.

The camera height calculations confirm the view stays well above the ground even at the closest zoom distance. A resize observer will be added to handle window resizing.

The resize handler will use a standard window resize event instead of a ResizeObserver. Console errors will be avoided by checking that OrbitControls is initialized before use, by avoiding deprecated Geometry, and by using existing methods like Color.setHSL and Math.hypot. To prevent additive blending from washing out the dark background, only five to seven flame meshes will be used rather than twenty. A light shaft effect was considered but skipped. Occasional extra spark bursts will be added to the animation loop to simulate crackling without audio.

A custom star shader will give each point a unique phase attribute so the night sky twinkle looks natural instead of synchronized. The full HTML file is now being written.

The canvas will be fixed to fill the viewport, and the body margin will be zeroed to prevent scrollbars. The moon will use a low-poly MeshBasicMaterial so it remains visible regardless of scene lighting, with darker patches for craters. A separate glowing sprite for the moon was considered but set aside.

A glowing moon sprite will face the camera each frame to create a soft halo effect. The moon will be placed at (-33, 21, -43) and its craters will use a slightly darker brown material.

The moon glow will use three concentric circles with additive blending and decreasing opacity. The moon anchor will face the camera each frame so the glow sprites stay oriented toward the viewer.

The moon anchor will face the camera each frame so the glow sprites stay oriented toward the viewer. The moon light will be a directional light aimed at the scene center from the moon's position.

The moon light will keep its default target at the scene origin. No extra target object is needed in the scene. The light will shine from the moon toward the clearing.

The moon position at (-33, 21, -43) aligns with the camera view direction. At a distance of about 70 meters the 3.2-unit radius yields an angular size of roughly 2.6 degrees, which is already five times larger than the real moon. A slightly larger radius of 4.5 would give the stylized moon a more prominent presence.

The moon radius is finalized at 4.2 units, giving it a 3.4-degree angular size. The glow circles will use radii of 8, 14, and 22. A few brighter stars will be placed near the moon. Distant hill silhouettes will be added at a radius of 52–60 with random heights of 6–16.

Hill silhouettes will use cone geometry with random heights and radii. Fog far is set to 70 so distant hills remain faintly visible. The scene will contain 48 trees placed at radius 40. Minimum spacing between trees is being calculated based on their scale.

The 48-tree placement area provides roughly 4,800 square units, so rejection sampling will easily avoid collisions. A concern arose that trees placed in the foreground could block the fire, since the camera sits at a radius of about 14.

The camera will sit at radius 12 inside the clearing, with trees placed from radius 15 outward. This keeps the fire and rocks visible while forming a natural wall of trees around the campfire.

The camera is now fixed at (7.5, 4.0, 8.5), about 11.3 units from the center. Trees will start at radius 13.5, leaving a two-unit gap between the camera and the fire so nothing blocks the view. Side trees will frame the scene. The moon visibility is being recalculated for this new camera position.

The moon was moved to (-28, 14, -36). It now sits at a comfortable 10-degree elevation above the horizon, away from the camera's top edge. The angular size is about 3.7 degrees.

The moon sits at y=14, well above the tallest trees at y=7. The ray from the camera toward the moon passes over the tree line, so the moon will remain visible above the clearing.

The camera ray toward the moon is checked at distance 15. The ray is at roughly (-1.7, 6.6, -3.1), still inside the clearing. The moon sits at y=14, so it remains visible above the tallest trees along this path.

The camera target is fixed at (0, 1.15, 0) and the field of view is set to 58 degrees. The moon radius is finalized at 3.6 units.

Two accent trees will be placed at (-6, 0, 10) and (11, 0, 2) to frame the left and right edges of the clearing.

A fallen log will sit at (-4, 0, 3) in the left mid-ground. The artistic plan is complete and the code is ready to be written.

Tree geometry will be shared and scaled per instance for efficiency. Foliage cones use six sides and trunks five, both with flat shading. The renderer will use ACES filmic tone mapping with exposure 1.05. Shadows will be enabled on the ground and point lights, though a warning may appear on the first frames before the shadow map initializes.

A CSS starfield will render immediately behind the canvas so the night sky is visible even if JavaScript is slow. The canvas will cover it once the scene loads, and the HUD will sit on top.

The flame geometry will be translated so its base sits at y=0.35, keeping the log base fixed while the cone scales upward.

Flame groups will sit at y=0.28 with their base at y=0.25, scaling upward from the log base. Flicker opacity will vary slightly. The scene will contain 44 trees total, including 42 random and 2 framing, using rejection sampling with a forced fallback if needed.

Ten low-poly bushes will be placed at the tree line. Each bush is a cluster of three icosahedron meshes with randomized positions and a dark green material.

Bushes will sit at radius 12–16 along the clearing edge so they do not block the camera. The code will use minimal comments and avoid alerts or localStorage. A safety clamp will keep the camera above ground at 0.6 units during the animation loop.

OrbitControls will disable panning and use reduced zoom and rotation speeds with damping for a smoother feel. The HUD text will be constrained to prevent overflow on small screens. A clamp-based font size will keep the title responsive. The author is now weighing whether to switch to MeshStandardMaterial for better lighting response.

Lambert material was chosen over MeshStandardMaterial for its low-poly simplicity and predictable lighting response. A warm spotlight from above the fire will be added to prevent the scene from becoming too dark under ACES tone mapping.

A warm spotlight was considered but rejected to avoid redundancy with the point light. An orange ground glow disc with additive blending will serve as the primary warm light source on the ground.

The point light intensity is set to 200 to ensure the fire area glows orange even if the ACES tone mapping overbrights it. The orange ground glow disc will also flicker to reinforce the warm fire effect.

The point light intensity is set to 160 and the renderer exposure to 1.0. Shadow map settings are configured for the fire light with a 1024×1024 resolution, a near plane of 0.2, a far plane of 30, and a negative bias of -0.002. The author is now ready to write the complete code.

Eight small orange icosahedron meshes will sit at the base of the flame group to represent flickering embers. They will be placed in a tight circle around the fire and animated with a phase offset so their scale pulses independently.

The embers will pulse smoothly between red and yellow using a sine-wave lerp. A second smaller flame cluster will sit offset from the main cone to create a more natural fire shape. Antialiasing will be enabled for cleaner low-poly edges, and the renderer will use alpha: false so the clear background shows through.

The import map will be placed in the head element to satisfy sandbox requirements. The Three.js module script will be appended at the end of the body. A firefly particle system will be added, using a green shader similar to the spark particles but moving more slowly.

The fireflies will be 16 small transparent green octahedron meshes. They will orbit the clearing at a radius of 5 to 13 units with randomized speeds and phases.

Each firefly will carry a faint green glow sphere. They will orbit the clearing at a radius of 0.4 to 1.2 units, kept clear of the fire area. Their motion will use a slow sine-wave path with vertical bobbing.

Firefly opacity will pulse between 0.45 and 0.85. Auto-rotation is disabled so the camera stays static and the screenshot captures the intended composition. The animated fire, sparks, and fireflies will provide all motion.

Keyboard controls are disabled to avoid sandbox pointer-event restrictions. A "Drag to look around" hint will be shown so users know they can orbit with the mouse.

The HUD will display "Click and drag to look around" to help users activate pointer events inside iframes. Tree instances will vary in height, color, and lean angle for a more natural forest feel.

Tree cone centers will sit at trunkH×0.72 so the first layer overlaps the trunk. Scale ranges from 0.85 to 2.1. Shared materials are created once. Snow is omitted because the scene is a forest night, not a winter scene. The leaf color palette is now finalized.

The final color palette is set. Forest greens will be mid-dark shades like 0x1e5c34 and 0x184a2c. Trunks will use browns like 0x5c3a22. Rocks will be grays like 0x7a756e. Logs will be browns and charred 0x2a1c14. A ring of nine small rocks will sit just outside the fire base.

Rocks will use dodecahedron or icosahedron geometry for a chunky low-poly look. They will sit slightly inside the clearing edge and be irregularly shaped. Extra boulders will be placed at (-5.5, 0, -2), (4, 0, -5), and (6, 0, 5). A small log pile of three stacked logs will sit at (4.5, 0, -2) as a firewood detail.

The log pile at (4.5, -2) sits to the right-front of the fire and remains visible. The camera at (7.5, 4, 8.5) is checked against the two framing trees at (-2.5, 0, 9.5) and (9.2, 0, 3.2) to ensure they do not block the fire.

The right-side framing tree at z=3.2 sits 6.4 units off the camera’s view line at a distance of 5.6 units. It will frame the right edge without blocking the fire.

The lateral offset of 6.4 units for the right-side framing tree was incorrect. A proper 2D angular check is being recalculated to verify whether the tree sits inside the camera’s field of view.

The right-side framing tree at (9.2, 3.2) sits 59 degrees off the camera’s view line. With a horizontal field of view of roughly 83 degrees, it falls outside the frame and will not frame the clearing. The author is recalculating the framing tree positions to keep them within about 35 degrees of center.

The recalculated framing tree positions at (-1.16, 5.48) and (5.58, -0.46) sit only 5.6 units from the fire. They would block the side view and dominate the frame. The author is now searching for a larger radius that keeps them outside the clearing without falling outside the camera’s field of view.

The recalculated framing trees sit 29 degrees off the camera axis, well inside the 41-degree horizontal field of view. At a distance of 9.2 units, they will frame the clearing edge without blocking the fire. A tree at this radius would appear as a natural foreground element beside the campfire.

The framing trees will sit at (5.6, 0, -0.5) and (-1.2, 0, 5.5) with scales 1.7 and 1.9. They are confirmed to be outside the fire radius and will not overlap the seating logs.

Seating logs will be placed at angles like 40° and 200° to avoid the framing trees. Random trees will be generated from radius 13 outward so they do not collide with the seating area. A tall background tree at (-4, 0, -6) with scale 2.2 may partially overlap the moon for dramatic silhouette effect.

The background tree at (-4, -6) is dropped to keep the moon clear. The ground plane will be 120 by 120 with 36 segments. Rotating the geometry 90 degrees on the X axis makes Y the vertical axis.

The ground plane will be 130 by 130 with 36 segments. It is rotated 90 degrees on the X axis so Y becomes vertical. Vertex normals will be computed after displacement for correct lighting. The author is checking whether computeVertexNormals is still needed with flatShading enabled.

computeVertexNormals() would smooth normals and defeat flatShading. Adjacent faces need distinct normals to render as faceted polygons.

Three.js computes flat-shading normals in the fragment shader using dFdx and dFdy derivatives. This means computeVertexNormals is unnecessary and would actually smooth the ground.

The ground will use polygonOffset to prevent z-fighting with the fire decal and glow. The renderer will sort objects by default.

A dark edge ring of slightly taller grass around the clearing was considered but skipped to avoid clutter. The animation loop will use a THREE.Clock with a 0.05-second delta cap to prevent large jumps. The spark particle shader must declare the `aLife` attribute carefully, since Three.js r170 may prepend shader code.

The spark particle vertex shader explicitly declares `attribute float aLife` and `attribute float aMaxLife`. It computes `vT` as the normalized lifetime and `vAlpha` with a smoothstep fade. Point size scales with depth and lifetime.

The spark fragment shader discards points beyond the circle edge and blends warm yellow-orange to red based on lifetime. The point size formula was revised to 90.0 divided by depth so sparks stay visible at distance while growing larger on close approach.

Star particles will use a twinkle shader with aSize varying by time and phase, sized between 1.2 and 2.8 pixels. AColor will be passed as a vec3 attribute to tint some stars blue or warm. Fog color #101828 will blend into the sky horizon #1a2340 for a seamless misty fade.

The sky sphere will sit at radius 100 with BackSide material, depthWrite disabled, and renderOrder set to -1 so it stays behind the scene. Stars and the moon will render at radii 80–90, remaining in front of the sky because the sky does not write depth.

The moon will sit at a distance of roughly 58 units, placing it in front of the stars at 85 units. The camera will stay within about 40 units of the origin, so the 100-unit sky sphere will fully contain the scene.

The sky sphere will remain static rather than parenting to the camera to avoid fog glitches. A shooting star was considered but skipped to prevent a glitchy appearance in the screenshot. The renderer will fill the window and handle resizing correctly. The author is now ready to write the complete HTML file.

The warm point light will naturally cast a ring of glow onto the tree trunks facing the fire. The fire light shadow radius is set to 2 for softness, and this property is safely ignored by PCFSoftShadowMap in Three.js r170.

The ground will receive shadows so the stone ring and logs cast visible shadows downward. The fire light sits at y=1.4 above the ground plane, logs at y=0.2, and rocks at y=0.2. Shadow acne will be reduced with a normalBias of 0.05. Rocks will be raised slightly to prevent z-fighting with the ground.

Rocks will be partially buried in the ground using a y-position formula based on their scale. The scene will be fully initialized at startup so the three-second screenshot captures the fire, sparks, fireflies, moon, and trees in their active states.

The spark particle count is raised to 120 so the column above the fire is clearly visible in the still image. A small cylinder mug will sit beside the logs as a charming foreground detail. The author expects the final file to be roughly 700–900 lines.

The mug will be scaled up and colored tin gray. A low-poly backpack will sit beside a seating log. Shadow map resolution is set to 512. Only objects within 18 units will cast shadows to keep the scene performant.

Shadow casters are now limited to objects within 20 units, reducing the total to roughly 90. The page will display the title "Midnight Clearing" in a simple HUD.

The HUD will sit fixed at the bottom of the viewport with a warm, semi-transparent film-subtitle style. It uses a serif typeface, a subtle gradient background, and pointer-events disabled so it does not block mouse input. The hint text will wrap cleanly on small screens.

The HUD will stack vertically on small screens to avoid awkward wrapping. The canvas will be set to display block, and the page will disable touch scrolling and overscroll to keep the scene stable. A faint pulsing halo will surround the moon for subtle glow.

The spark particles will use AdditiveBlending with transparent depth-write disabled. The star shader will receive a live uTime uniform so the twinkle animation updates each frame. Three.js automatically prepends WebGL precision qualifiers, so explicit declarations are unnecessary.

Custom ShaderMaterial fog is disabled by default, so stars and sparks will not be affected by the scene fog. The author confirmed that Three.js automatically prepends WebGL precision qualifiers to custom shaders.

The star dome will use a uniform distribution in vertical position rather than area. This clusters more stars near the horizon, which is acceptable for a night sky.

The author realized a uniform vertical distribution would make the horizon appear too dense. They switched to a true spherical randomization with a retry loop to keep stars above the tree line.

The star dome will hold 500 points at radius 90, with the lowest visible stars sitting about 4.5 units above the horizon. To stand out in the still image, eight larger cross-shaped bright stars will be added alongside the point stars. These will twinkle by scaling up and down.

The bright star cross was initially too small at radius 85. Angular size calculations showed a 0.55-unit box would be nearly invisible. The arm length is now 1.6 units so the stars remain clearly visible in the still image.

Ten bright stars will sit closer at radius 60 in the upper sky, with arm lengths between 1.2 and 2.8 units. The moon will sit at (-28, 14, -36), and a few bright stars will cluster near it without cluttering. One star will be tinted blue and another gold for contrast. The author is ready to write the full scene.

A seeded random generator will be used so the tree layout stays deterministic and the camera composition remains consistent across reloads. The minimum tree radius is set to 13.5 units to keep the clearing clear.

A cone-shaped rejection filter is being added to keep random trees from blocking the moon. The camera-to-moon ray will define a cone, and any tree falling inside it will be discarded to preserve the moon view.

The moon-blocking cone is now defined as a 16-degree cone within 45 units of the camera-to-moon ray. A 4-unit camera proximity guard will also reject trees too close to the origin. A seeded mulberry32 RNG is being implemented so the tree layout stays deterministic across reloads.

The seeded random generator is now initialized with a fixed seed so the tree layout stays deterministic. The author decided against adding a heat haze effect to avoid rendering issues. They are now drafting the `createHorizontalLog` function to build the log geometry.

The log geometry is now defined as a short cylinder rotated to lie along the X axis. The fire logs will be arranged in a pile at the origin, with each log slightly offset and rotated around the Y axis so they intersect in the center. A seating log is also being drafted.

The seating logs are placed at three positions around the fire, keeping them clear of the framing trees. A thin fallen branch will sit near the fire as kindling. The author decided against adding more objects to avoid cluttering the flames.

The camera will sit at (7.2, 3.6, 8.2) aimed at (0, 1.0, 0) so the viewer looks at the flames rather than the ground. A firewood pile will sit at (-4.2, 0, -1.5) beside the fire. The author is verifying that the moon remains visible from this lower, closer angle.

The camera-to-moon angle is 23.8 degrees, comfortably inside the 58-degree field of view. The near plane is set to 0.1 and the far plane to 200. The author is ready to write the code.

A single point light is sufficient for the flickering fire glow. A modest fill light at (0.3, 0.8, 0.2) will brighten the core without flattening shadows. The renderer clear color will match the fog color, and the scene background will be set to null so the sky sphere shows through. The cone geometry is being verified so its base sits at y=0 and its tip reaches the intended height.

The flame geometry is being randomized before translation. Vertex positions are adjusted to create a wobbly, flickering cone shape. The cone is centered on the origin, so the translation and normal computation follow the vertex deformation.

Flame segments will vary between 4 and 6 to keep the fire low-poly. The seeded random generator is called in a fixed initialization order so layout stays deterministic. Window resize handling will also account for device pixel ratio.

The resize handler updates the camera aspect ratio and renderer size. Pixel ratio will be set once at startup. A few tiny icosahedron pebbles will be scattered on the ground for extra low-poly texture.

Ground glow opacity and coal brightness will track the fire light flicker. Shadows will flicker automatically as light intensity changes. The author is checking for a potential bug with point light intensity dropping to zero.

The flicker function now returns values between roughly 0.48 and 1.16, keeping the fire light above zero. Each flame will store phase, offset, and height in its userData. The animation loop will update these values each frame to give the flames subtle movement and variation.

Flame geometry will scale and rotate in the animation loop to create movement. Inner flames will use a higher opacity and yellow tint, while outer flames will use lower opacity and red, blending additively so the center appears hottest.

Eight inner flames will use a yellow-to-white gradient, and eight outer flames will use a red-to-orange gradient. Both groups will sit at y=0.22 within a 0.25-unit radius. The author is now writing the complete HTML file and will wrap the entire output in a single fenced code block.

Some trees will be backlit to add depth in the dark forest. The clearing ground will stay flat. The moon will be a simple low-poly full sphere in a warm cream tone.

Dark crater dents will be added to the moon sphere. The moon pivot will use lookAt to face the camera directly, since the glow circle geometry sits in the XY plane.

Checking Three.js source code to verify whether Object3D.lookAt rotates the object's positive or negative Z axis toward the target.

Three.js lookAt rotates the object's negative Z axis toward the target, so the circle geometry's positive Z normal faces away from the camera. DoubleSide material will render both faces, keeping the moon glow visible.

The author realized that parenting the moon's crater spheres to a rotating pivot could hide them if the pivot's negative Z axis faces away from the camera. To avoid this, the moon sphere itself will remain static, and only the glow discs will rotate to face the camera.

Crater spheres will be placed on the moon's side facing the clearing at initialization. The author is considering updating their positions during the animation loop so they remain visible as the camera orbits.

Craters will be scattered randomly across the moon sphere so they remain visible from any viewing angle. The moon itself will stay static. The glow discs will rotate to face the camera each frame, but they will be offset slightly toward the camera to avoid z-fighting with the moon sphere.

A soft, larger secondary glow will be added behind the moon. The logarithmic depth buffer will be skipped to avoid shadow artifacts. The fire point light will shift slightly between orange and yellow during flicker, using setHSL with a hue offset of 0.07 and a lightness of 0.55.

The fire point light will interpolate between two fixed colors instead of using HSL, blending from orange to yellow based on flicker. The light’s decay is set to 2, its distance to 24, and its intensity scales with flicker so the scene does not blow out.

The fire point light intensity is being reduced from 180 to 60. At 3 meters the light would otherwise hit the rocks at 20, which ACES tone mapping could compress into pure white. A lower intensity keeps the fire hot while preventing the surrounding ground from being overexposed.

The fire light intensity is set to 70 with a 25 fill light, plus emissive flames and ground glow to keep the scene readable without overexposure. Forest lighting will use a 2.0 directional light, 0.15 ambient, and 0.35 hemisphere. The author is checking whether Three.js treats the chosen green as sRGB or linear.

Three.js interprets hex colors as sRGB, so the chosen forest greens will render correctly under the scene lighting. The author is slightly lightening the foliage palette to #2f7a48 and #267044 to prevent the trees from looking like black blobs in the dark forest. The final set of four green shades will keep distant trees fading into the fog while nearby ones remain visible under moonlight and fire glow.

Grass and dirt colors are finalized at 0x1c4a2e and 0x7a5538. Moon light is raised to 2.5 and ambient to 0.25. Fire shadows will still show because the fire light remains much stronger than the ambient and moon light in the camp area.

The final lighting values are set: moon light at 2.2, ambient at 0.2, hemisphere at 0.45, and fire light at 80. The hemisphere light will use a cool sky color and a warm ground color to fill shadows without making them black. The author is now writing the complete HTML file.

Grass tufts will gently sway in the wind using a sine wave. Each tree will also rock slightly in the animation loop, with saved base rotations to avoid overwriting the original lean.

Flame scaling will create occasional crackle effects. The renderer will use PCFSoftShadowMap for softer shadows. Shadow casters are capped at roughly eighty to protect sandbox performance. A mushroom cluster and extra pebbles will be placed near the frame edges for foreground detail.

Grass tufts will be scattered in the lower center between the camera and the fire, with angles aimed toward the camera so they stay visible in the foreground. The author decided against using InstancedMesh for the trees; individual meshes are simpler and allow better variation.

The author decided against adding an owl or god-ray effects to keep the scene clean and avoid visual clutter. They confirmed that all assets must come strictly from the provided import map, with no external CDNs for fonts or textures.

The HTML structure now includes a vignette overlay and a HUD container. The script imports Three.js and OrbitControls from the provided import map. A try-catch block will display a plain text message if WebGL fails, avoiding browser alerts.

The author is now writing the ground geometry generation code. They are carefully mapping vertex positions to terrain heights after the initial rotateX transform, and assigning vertex colors to create a layered dirt, grass, and moss palette across the clearing.

The author is verifying that vertex colors will render correctly under MeshLambertMaterial. In Three.js r170, vertex colors may require explicit sRGB handling or the material may need to be set to vertexColors mode to avoid washed-out or dark foliage.

The author is reconsidering the vertex color approach for the ground. Three.js r152 and later may expect vertex colors to be linear, which could cause the dirt and grass to appear too dark or washed out. To avoid potential color-space bugs, they are leaning toward using a single MeshLambertMaterial color for the ground instead, though they note this sacrifices the richer, varied look of per-vertex coloring.

The author confirmed that Three.js r170 converts sRGB hex values to linear internally when using THREE.Color.setHex. Writing those linear values directly to the vertex color buffer is correct for the shader lighting multiplication. The per-vertex coloring approach for the ground is therefore safe to proceed with.

A flat, dark scorch circle will sit at the fire center to mark the pit. It uses a flat-shaded Lambert material and receives shadows.

A warm, semi-transparent glow disc will sit above the scorch mark, using additive blending and a 4.2-unit radius. A smaller, brighter pool at radius 1.8 with 0.28 opacity will flicker alongside it. Both pools will animate to keep the fire glow dynamic.

The author is now systematically verifying that every requirement is met. The scene includes the ground, rocks, logs, and a campfire with animated flames and flickering warm light. It has a particle spark system, a dark night sky with stars and a moon, and fog that fades distant trees. OrbitControls limits the camera above the ground, and the renderer fills the window with correct resizing. All assets come strictly from the provided import map, and the page renders immediately with a CSS sky overlay before the Three.js module loads.

OrbitControls damping is enabled and will be updated each frame. Camera limits are set to a 4–28 unit distance, a 0.25–1.49 radian vertical range, and no panning. The camera target is clamped to keep the view above ground.

Touch support is enabled by default in OrbitControls. The camera up vector is set to (0,1,0). The author will keep the code compact and readable to avoid mistakes, then review the full script before submitting. The 42 trees include 40 randomly placed ones plus 2 framing trees, satisfying the minimum of 30. Tree variety will mix pines with a few low-poly icosahedron canopies for deciduous shapes.

The spark particle system will use a dynamic buffer attribute for per-particle lifetimes, updating it each frame to keep the rising spark effect alive.

The spark system will mark its position and lifetime buffer attributes as needing update each frame. Twenty larger orange tetrahedron embers will be added alongside the point particles so the rising fire glow remains visible even at a small point size.

The spark system will use a single points shader with generous point size. Twelve small mesh embers will be added as a backup visual layer so rising fire particles remain clearly visible in the screenshot.

The author is now writing the complete HTML file. They are carefully checking brackets and syntax. The HUD will be elegant.

A CSS night sky with many star dots will render immediately on first paint so the screenshot shows a dark forest before Three.js loads. The canvas is appended to the body and covers the viewport via fixed positioning. The vignette overlay and HUD are placed in the HTML before the script tag so they stack above the canvas.

The author is checking that the canvas does not paint over the HUD. Since the renderer appends the canvas after the HUD in the DOM and the canvas defaults to static positioning, the HUD should remain visible above the viewport.

The canvas will be fixed to the viewport with `z-index: 0`, while the vignette and HUD will sit at `z-index: 2` to stay above it. Overflow will be hidden. The author is now ready to write the final code.

A faint, transparent orange sphere will sit above the fire pit to simulate the warm light volume. It uses additive blending and low opacity, and its opacity will flicker with the fire to keep the glow dynamic.

The warm light-volume sphere is being dropped to keep the fire glow crisp. The renderer will use a deep blue-black background. The sky sphere radius is set to 140 and the camera far plane to 250 so distant trees stay within view. A subtle horizon glow will be baked into the sky vertex colors.

A custom smoothstep helper will be written to avoid signature mistakes with THREE.MathUtils.smoothstep. The moon-side sky variation is skipped for simplicity.

The terrain height function is deterministic and uses no random values, so it matches the ground mesh exactly. Objects placed at terrainHeight will sit slightly above the faceted ground surface, so they will be lowered by a small offset to sit flush.

Terrain height values are being reduced to keep the clearing walkable. The noise amplitudes are set to 0.35, 0.28, and 0.12, yielding a maximum elevation of roughly 0.75 units. This creates gentle hills rather than steep peaks.

Six larger rock outcrops will sit on the hills for visual variety. The camera will be positioned at (7.2, 3.6, 8.2) and the controls target set to (0, 1.05, 0) before the first animation frame. The script will be kept clean and comment-free. Materials will be grouped into five leaf variants, three trunk variants, and four rock variants.

Rock geometry will use dodecahedra and icosahedra with random selection. Each rock mesh is scaled non-uniformly to create irregular shapes. The rock material uses flat shading.

Rock meshes will be scaled non-uniformly and rotated randomly for irregularity. Each rock will cast and receive shadows. The author decided to create fresh Dodecahedron and Icosahedron geometries for every rock rather than sharing them, since twenty instances are negligible. Wood grain was considered but skipped.

Log end caps were considered but skipped as too fussy. The roasting stick will sit at (2.4, 0.7, 1.3), pointing toward the fire from the right seat. A marshmallow will be parented to a group for animation.

The roasting stick is being built as a Group with a CylinderGeometry mesh rotated and positioned at the fire edge. A white marshmallow sphere is placed at the stick tip, and a smaller toast sphere is added to the group.

The marshmallow and toast will be placed as children of the stick group at local Y = 1.1 so they sit at the cylinder tip. The group will then be rotated and positioned at the fire edge. This keeps the low-poly stick and marshmallow aligned without manual coordinate math.

The roasting stick will lean toward the fire at a slight upward angle. A second stick without a marshmallow was considered but skipped. A mug will sit beside the seat. Optional chaining is acceptable for the latest Chrome target.

The moon light will be tinted cool blue to contrast with the warm fire. Distant trees will fade to a cool blue-gray mist.

A subtle blue ground reflection from the moon will be added via the hemi light. The script will be kept around 800 lines. Fireflies will replace butterflies for the night scene. They will be placed in the mid-ground with a green glow, varying in size and height, some facing the camera.

Fourteen fireflies will be placed at negative and positive x and z coordinates so they sit in the background and foreground. Their core radius is 0.1 and glow radius 0.28 to keep them small and readable at distance. They will pulse in scale.

No keyboard start prompt is needed because the demo relies on mouse and touch controls.

Spark particles will use depth testing so they hide behind trees when they drift away from the fire. The flame material will keep the default tone-mapping setting to preserve the additive glow.

The moon, stars, and ground glow will all use `toneMapped: false` so they stay bright and readable against the dark sky. The fire will also skip tone mapping to remain brilliantly hot and visually dominant.

The author confirmed `toneMapped` is available in r170. They decided against adding subtle low-poly clouds to the sky to avoid clutter.

Three small cloud clusters will be added to the sky as subtle silhouette shapes. Each cluster uses four IcosahedronGeometry pieces with a dark blue-gray material, positioned away from the moon so the star field remains visible.

Clouds were abandoned to keep the sky clean and avoid a muddy look. The author is now writing the final code.

A ring of ten larger stones will form a visible campfire circle. Logs will sit around and inside the fire. Four short dark kindling sticks will be arranged in a teepee shape beneath the flames.

The author realized that aligning a vertical cylinder to point inward toward the fire using `lookAt` and `rotateX` is error-prone. A simpler approach is being considered for the kindling sticks.

The kindling sticks will be placed in a ring at radius 0.32 and rotated with YXZ order. Y rotation spins them around the vertical axis. Z rotation leans them inward toward the fire. They will stand at height 0.5 with length 1.1.

The kindling sticks will sit at radius 0.38 with a 0.035–0.055 base-to-tip diameter. The author abandoned the `lookAt` approach for the sticks because it risks misaligning the cylinder axis, and is now computing a normalized direction vector to point each stick directly at the fire center.

The kindling sticks will be aligned using a quaternion that points the cylinder's local Y axis toward the fire center. Five sticks will sit at radius 0.4 with their bases at ground level. Horizontal logs will form the fuel bed beneath the flames.

The kindling geometry will use only four segments for a low-poly look. A consistent seed will be used for deterministic randomness. The camera will be elevated to y=4.2 and positioned at a distance of about 11, aimed at y=1, so the stone ring and flames are visible from above. The scene will be captured at the three-second mark, when the flames are mid-wave and the spark particles are in the air.

The camera will be placed at (8.0, 4.4, 9.0) with a 64-degree field of view. This wide angle keeps the moon in view while showing trees on both sides of the clearing.

The camera target is fixed at (0, 1.1, 0). The moon is raised to (-28, 15, -36) with a radius of 4.0. Its angular size is about 3.9 degrees. The field of view is 64 degrees. The moon-blocking tree rejection logic is being updated to match these positions.

A thin gold line will be added under the title for design polish. Georgia is selected as the main font, and a subtle hint text in Segoe UI will be styled.

The HUD will be right-aligned on desktop and left-aligned on mobile. The title is "Midnight Clearing" with the hint "Drag to orbit · Scroll to zoom". No emoji will be added. The mulberry32 generator will drive all layout randomness. Spark particles will use typed arrays instead of Vector3 objects to avoid garbage collection.

A reusable Vector3 will be used in the animation loop to avoid creating temporary objects each frame. Each flame will receive a cloned material so that per-flame opacity changes do not affect shared instances.

Flame and coal materials will be instantiated as unique MeshBasicMaterial instances rather than cloned, ensuring each can animate its own opacity without side effects. Shared Lambert materials will remain static.

Tree sway will only rotate the geometry. Spark particles will have frustum culling disabled so they do not vanish when their positions drift far from the original bounding sphere.

Spark particles will be drawn above the flames with renderOrder 2. The ground glow and fireflies will sit at renderOrder 1 so they appear beneath the transparent fire.

OrbitControls damping will be enabled so the camera target updates smoothly each frame. The warm pool sits at a different height than the flames, so no z-fighting occurs. Local clipping is unnecessary.

The camera's maximum polar angle is set to 82 degrees. This keeps the camera above the horizon relative to the target and prevents it from dropping too low.

Camera height is calculated from the target position, distance, and polar angle. At the minimum distance the camera sits at least 1.42 units above the ground, so an extra safety clamp to 0.5 is added. The minimum polar angle is set to 0.35 radians to prevent a pure top-down view that would hide the moon and flames.

A `place` helper will drop objects onto the terrain height at the given X and Z coordinates. The stone ring will sit slightly above the ground so the rocks rest on top of the clearing.

The stone ring will sit slightly below ground level so the rocks nestle into the clearing. A grid-based collision check will keep the random trees from overlapping the campfire. The author realized the camera sits at roughly 12.04 units from the center, which is very close to the 12.5-unit minimum tree radius. This risks clipping the camera against a tree. The plan is to move the camera closer to the fire and start the trees at a larger radius.

The camera will sit at (6.2, 3.8, 7.0) with trees placed from a radius of 13. This leaves a 3.6-unit gap around the fire and keeps the scene open. The moon is 27.7 degrees from the camera, so the field of view is being widened to 68 degrees to prevent the moon from being clipped.

The camera is locked at (6.4, 3.7, 7.2) with a 66-degree field of view. The target is fixed at (0, 1.05, 0). Trees will be placed from radius 13.2 to 40. The moon is positioned at (-30, 16, -38). A 70-degree field of view was considered to give a comfortable margin, but 66 degrees is sufficient.

The author decided to use a 62-degree field of view instead of 70 to avoid distortion. The moon will be lowered in the sky so it remains visible within that narrower angle.

The moon direction is being recalculated to sit about 18 degrees above the camera view line. This places it at roughly 2.6 degrees of elevation, keeping it safely inside the 31-degree half-fov of the 62-degree camera angle.

The moon would sit behind the trees if it were placed just above the horizon. The camera currently looks down 15 degrees, so the horizon sits 15 degrees above center. To see more sky, the author is considering raising the camera to y=5.2 and lowering the target to y=1, which would make the view more level and reveal more sky.

The camera is now fixed at (7.5, 2.4, 8.5) aimed at (0, 1.6, 0). This creates a near-level eye-level shot with only a slight downward angle. The sky fills the upper half of the frame while the ground and fire remain visible in the lower half. The moon can sit comfortably in the upper sky without clipping.

The moon is fixed at (-27.3, 18.7, -30.9), 55 units away. It sits 22 degrees above the camera view line, safely inside the 60-degree half-fov. Its angular size is 4.0 degrees.

The moon elevation is raised to 24 degrees to prevent clipping by tall trees at the clearing edge. At 14 units away, the ray reaches 8.6 units high, safely above 7-unit trees. The angular difference from the camera view line is 28 degrees, which fits comfortably within the 32-degree half-fov.

The camera is being raised further to (7.8, 2.3, 8.6) aimed at (0, 1.7, 0). This creates a near-level eye-level shot with only a slight downward angle. The field of view is narrowed to 56 degrees, giving a comfortable 28-degree half-angle. The moon is fixed at 20 degrees of elevation, leaving a 23-degree angular margin that fits safely within the frame.

The camera is fixed at (7.8, 2.5, 8.6) aimed at (0, 1.55, 0). The field of view is set to 62 degrees. The moon is placed at (-29.9, 23.7, -33.0) at 22 degrees of elevation. This leaves a 3.3 degree margin above the camera view line.

The author calculated that a 22-degree moon ray reaches roughly 8.2 units high at the 14-unit tree line. Pine trees scaled up to 2.2 could reach about 7.5 units, risking a clip. To keep the moon corridor clear, tree scale will be capped at 1.9.

The author checked that the 13-unit tree ring leaves enough gaps for the eye-level camera to see between trees. The fire sits 11 units away, so its angular size is roughly 9.3 degrees, or about 15% of the 62-degree field of view.

The camera is being moved closer to the fire at (5.5, 2.2, 6.2) with a 60-degree field of view. This brings the campfire to roughly 19% of the screen height and the spark particles to about 35%, making them more prominent. The tree ring is shifted outward to radius 12.5, leaving a 4-unit clearance around the fire. The moon is recalculated to sit at 20 degrees of elevation, which fits comfortably within the 30-degree half-angle.

The author abandoned the manual moon placement calculations. Instead, the moon will be positioned dynamically by tilting the camera's view direction upward by roughly 22 degrees. This keeps the moon reliably in the upper center of the frame regardless of camera angle.

The camera is fixed at (5.6, 2.35, 6.4) aimed at (0, 1.15, 0) with a 64-degree field of view. The moon is placed 62 units away by tilting the camera's view direction upward by roughly 25.6 degrees. This keeps the moon reliably in the upper center of the frame regardless of exact numbers. The tree corridor rejection uses the same moon direction. The camera sits 8.5 units from the center, leaving a 3.3-unit clearance around the fire.

The camera sits 8.5 units from the fire, giving the flames a prominent 8-degree downward angle. The moon will be computed dynamically in code from the camera direction to guarantee it stays in frame. A 12-degree cone rejection around the moon ray will prevent trees from blocking it.

A 20-degree cone will be cleared around the moon to prevent trees from clipping the glow. Framing trees will be placed to the sides using the camera's right vector, and all tree positions will be generated in code.

Side trees will sit 3.2 units forward and 2.8 units to the side of the camera. At a 64-degree field of view, this places them at roughly 41 degrees from center, just inside the 45-degree horizontal edge. They will land at a radius of about 6 to 8 units, beside the clearing rather than inside it.

The side trees are being scaled down to 1.3 and placed 5 units forward and 3.5 units to the side. This keeps them at a 35-degree angle from center and roughly 6.1 units away, creating a subtle cinematic frame without overwhelming the edge.

The side trees were too close to the fire. At radius 3.5 they would sit inside the campsite circle. They will be placed at the tree line to the sides instead.

The side framing trees will sit 4 units forward and 3.2 units to the side of the camera. This places them at a radius of 5.5 units from the center, just inside the 12-unit tree line, keeping them outside the campsite circle while still framing the edge.

The side accent trees will sit at a radius of roughly 5.5 units, just inside the 11.8-unit tree line. They will be hard-coded to avoid the fire and camera, keeping them outside the campsite while framing the clearing edge.

Side trees will be generated programmatically from the camera's view and right vectors. A helper function will place them 4.5 units forward and 3.4 units to the side, then push them outward to a minimum radius of 5.5 units if they land too close to the center. Any tree within 3 units of the camera will be discarded to avoid clipping.

The side trees will be scaled to 1.4. They sit at roughly 37 degrees from horizontal center, safely outside the 20-degree moon corridor. The minimum tree radius is set to 11 units so the forest wall is visible just behind the fire.

The fog will fade trees between 16 and 52 units, keeping the 11–16 unit ring clear and the 30–40 unit ring soft. The total tree count is raised to 48 to fill the forest. The author is now ready to write the code.

The author is finalizing implementation details. They will use const and let properly, avoid console logs, and ensure the ground material faces the camera with shadow receiving enabled. A potential issue with PointLight shadow maps inside the flame geometry is noted.

Flames, sparks, and glow discs will not cast shadows. Only opaque objects like logs, rocks, trees, and sticks will cast shadows, preventing the transparent fire from blocking the warm light and keeping the scene bright.

The point light is placed at y=1.6, just above the kindling teepee tips at y=1.1. This keeps the light source outside the sticks, so the teepee does not cast shadows that would darken the entire scene. Shadows from logs and sticks will still fall downward onto the ground around the pit.

The kindling teepee will be 1.0 units long with its base at radius 0.42 and tip reaching roughly 0.9 units high, keeping the point light at 1.5 units safely above it. Horizontal fuel logs will sit at y=0.1–0.28 to cast shadows beneath the fire. A gentle wind effect will animate the fireflies.

The WebGLRenderer will use antialiasing and a high-performance power preference. The device pixel ratio will be capped at 2 to keep shadow rendering fast and ensure the scene renders within the 3-second limit.

Bright stars will be rendered as crosses with arm lengths of 0.9 to 1.6 units at distances of 45 to 70 units, sized to appear as roughly 12 pixels. The remaining 450 point stars will be 1.5 to 2.5 pixels with size attenuation disabled. Bright stars will twinkle by scaling their size each frame.

Bright stars will be randomly positioned on the upper hemisphere and rejected if they fall within 7 units of the moon to avoid overlapping the glow. The author is now proceeding to write the final HTML file, carefully checking that shader strings contain no accidental template literal interpolation.

The author will use template literals for shader readability while avoiding accidental `${}` interpolation. The scene background will be set to the fog color as a fallback behind the sky sphere. The sky material will render on the BackSide so the camera sits inside the sphere. Vertex colors on the sky will be subtly brightened near the moon direction to create a soft moon glow.

The moon glow will be procedural: each sky vertex is dotted with the normalized moon direction, raised to the eighth power, and blended toward a soft yellow at roughly 35 percent intensity. The scene will be built in this order: renderer, camera, fog, then the moon position derived from the camera, then the sky sphere, then the stars, then the moon mesh and glows, then the lights, and finally the ground.

A subtle CSS vignette will be added to darken the viewport edges without obscuring the fire. The page background will be set to the night color. The remaining scene objects—campfire, trees, particles, controls, and animation—will be created in that order.

The scorch circle will receive shadows. The warm pool sits at y=0.06 and uses a basic material, so it will remain visible above the ground.

The warm pool will sit at y=0.08 and flicker with a second inner pool. Grass will be drawn as low-poly cone tufts rather than flat planes. Seven mushrooms will be scattered around the clearing at radius 6–9.

Mushrooms will use a bright red MeshPhongMaterial at 0xe24b3a with a faint emissive glow so they remain visible against the dark forest floor.

A firewood stack will be built from four logs arranged in a staggered pile. The author decided against adding a lantern because no trees are close enough to support one.

A left-side firewood pile will sit at (-3.8, 0, -2.2), safely outside the fire radius. A mug will be placed at (2.8, 0.08, 2.0). A rolled blanket will be added at (2.2, 0.22, 3.4) as a dark red-blue accent beside a seat.

The blanket is placed in the foreground at (2.2, 0.22, 3.4) so it sits in the lower right of the frame. The author is checking that the seat positions do not overlap the bedroll. The scene is being kept deliberately curated with only essential storytelling props to avoid looking like a random tech demo.

The code will be organized into focused functions to avoid a monolithic blob. A `createCampfire` helper will return the flame, light, coals, and pools as a single object for animation. Shared geometry will not be mutated, and flame geometries will not be reused across instances.

Tree shadows will be cast only for trees within 18 units of the fire. The author is ready to output the final HTML. Window resize alone will handle canvas scaling. Mobile touch controls and a responsive HUD will prevent horizontal scrolling.

A meta viewport tag will be added for correct mobile scaling. The spark particle system will use DynamicDrawUsage on its Float32Array buffer so Three.js preserves custom attribute names in the shader. All spark positions will be initialized along the fire path before the first render.

Ember particles will be updated in the same animation loop as the sparks. The author is verifying that using the seeded random generator during the reset phase will not disrupt the stable layout sequence, since all initial positions are assigned before the animation begins.

Runtime particle resets will use Math.random() to avoid depending on call counts. Ember particles will use unique materials with slight color variation between yellow and orange based on their life. A shared octahedron geometry will be reused for all embers. The implementation plan is now complete.

The fire point light will be set to an intensity of 80 so its shadows remain visible against the ambient and moon lighting.

The fire pit will glow very bright at the center, roughly thirty times brighter than the surrounding forest floor. Shadows cast beneath the fire will be noticeably darker, which is acceptable for a campfire scene.

The fire light intensity was reduced from 80 to 28. At 1.5 meters the ground reaches roughly 3.3 brightness, while shadows stay at 0.3, giving a 10:1 contrast ratio that keeps shadows visible without becoming pitch black. Rocks at 2 meters will glow bright orange-white but retain enough detail. The flickering point light will scale this base intensity.

The fire light intensity will range from 20 to 31. An extra fill light was considered but rejected to preserve shadow contrast. The ground additive glow may wash out shadows because it is not shadowed.

The warm pool will use a very subtle additive material with opacity set to flicker times 0.08. The inner pool will be slightly more visible at 0.12. This keeps the glow hint faint enough to preserve shadow contrast.

The warm pool radius will be reduced to 2.2 so rock shadows cast beyond it. This keeps shadows visible on the ground outside the pool while the real point light remains the primary light source.

The inner warm pool radius is set to 1.6 and the outer glow radius to 3.5 with 0.05 opacity. Shadow map resolution is 1024 for crisp quality. Shadow camera near and far planes are 0.4 and 25. Bias values are -0.004 and 0.03 to reduce acne. A second point light was rejected to keep shadows clean.

The lighting values are finalized. Moon intensity is 2.4, ambient 0.22, and hemisphere 0.55. The forest stays readable without looking like a black hole. The fire remains the bright local center. Exposure is 1.05 with ACES tone mapping. Deciduous and pine trees will use lighter greens.

The moon will sit in front of the camera in the sky. This places it opposite the fire, so trees between the viewer and the moon receive rim lighting. The camera-facing sides of the trees stay visible rather than becoming dark silhouettes. The fire will light the lower front faces of nearby trees, keeping the forest readable.

Pine trees will receive subtle random Y rotations on their upper cones and scale variations between 0.75 and 1.85 for organic variety. The author is now writing the code.

The tallest pine reaches roughly 6.0 units, so it clears the moonlight corridor. A `createPine` helper is being drafted to build each tree from a trunk and layered cones, with random trunk and leaf materials for variety.

The pine cone layers were slightly overlapping, so the vertical spacing between tiers is being tightened to eliminate visible gaps.

Pine cone spacing is finalized at 0.85 and 0.62 times scale, yielding a total height of roughly 2.64 times scale. Four fallen logs will be scattered across the forest floor at radii 14–20 for extra detail.

Three fallen logs will be scattered in the forest for extra detail. Each log will use a random wood material and a slight tilt. Camera height limits will be added to the HUD.

Auto-rotation will be enabled at a very slow speed so the scene turns slightly over time. At 0.5 speed the camera rotates only about 9 degrees in 3 seconds, which is negligible for the static screenshot but adds subtle motion for video playback.

Auto-rotation is set to a speed of 0.4. The camera will rotate roughly seven degrees in three seconds, keeping the moon in frame. Dragging the scene will disable auto-rotation.

Auto-rotation speed is set to 0.28 for a gentler turn. Controls will be updated every frame. Particle counts are fixed at 16 fireflies, 90 sparks, 16 embers, and 7 inner flame pieces. A soft dark shadow will sit beneath the fire as a fallback.

A dark fallback shadow beneath the fire was considered but rejected to avoid washing out the warm glow. Real shadow maps will provide the required ground shadows.

The fire point light will be added directly to the scene rather than to the flame group, preventing shadow distortion from parent scaling. Its position will shift slightly over time to create a dancing flicker effect. The author is now writing the final code and will review it for errors. A loading-less approach is planned so the scene initializes immediately and the first frame is complete.

A CSS fallback is being added so the page shows a night sky with a moon immediately on first paint, even if JavaScript hasn't loaded yet. The moon will be rendered as a fixed pseudo-element with a soft glow.

The CSS moon fallback is being simplified. A z-index conflict with the canvas layer is too messy to resolve, so the first-paint CSS will use only a dark background with star gradients. The Three.js scene will render its own moon.

Canvas z-index is 0 and HUD is 2, so the CSS fallback background remains visible only if the canvas fails to cover the window. The renderer clears the canvas, so a transparent background is unnecessary. A bug with ConeGeometry height after translation will be fixed by storing the height in userData. Quaternion.setFromUnitVectors is confirmed available in r170. The author is now drafting the complete HTML file and beginning the kindling code.

The kindling code is being written. Six charred sticks are placed in a circle around the fire. Each stick uses a short cylinder geometry with a flat Lambert material. The sticks are positioned at random angles and lengths to create a natural pile.

The kindling sticks are now placed in the scene with proper quaternion alignment and shadow flags. Horizontal base logs will be added around the kindling for extra detail. The author considers the code ready.

Ten larger rocks will form a ring around the fire, sized 0.32 to 0.58 so they remain visible at camera distance. A warmer wood tone will tint the ring rocks to help them read against the dark ground. The author is now writing the complete HTML file.

A window pointerdown listener will stop auto-rotation on any click. Keyboard listeners were intentionally omitted to avoid implying keyboard interaction is required. Mobile touch defaults are left unchanged.

Twelve bright stars will be added to the sky. The moon will have a soft halo created by three layered glow circles with reduced opacity values. Its surface will show eight randomly placed craters in warm browns.

A flat dark mare on the camera-facing side of the moon was considered but rejected. The moon will use a smooth sphere geometry with a warm off-white material. A faceted low-poly sphere was briefly considered to match the scene style.

The moon will use a low-poly sphere geometry with 8 segments across and 6 down, giving it a stylized faceted look that matches the scene. Its craters will be small icosahedrons. The soft glow will use 24 segments for a smooth light halo.

The sky sphere will use 28 width and 18 height segments for a smooth gradient. Stars will sit at radii 70–95 inside the sky. Camera far is set to 300. Fog will fade distant trees without affecting the sky. Render order is assigned so the sky draws first, stars second, and the moon last. Depth writes are disabled on the sky to prevent depth conflicts.

Star sprites can overlap the moon edge because their size extends beyond the vertex depth. Disabling star depth writes would prevent this, but the moon renders after stars so it would still cover them. The author notes this as a minor issue.

Star depth writes will be disabled to prevent sprite overlap artifacts. The moon render order will remain at zero. The author is now writing the complete source file. Small orange octahedrons will sit at the base of the fire as pulsing coals.

Eight coals will sit at the fire base and pulse in scale and color during animation. The spark particle system will use a separate `setUsage` call for clarity. The author is also verifying that the additive blending shader material handles alpha correctly.

The custom spark shader may trigger WebGL2 compatibility warnings because `gl_FragColor` is deprecated in WebGL2. To avoid any shader compile errors, the author is considering switching the spark particle system to Three.js `PointsMaterial` instead.

The spark particle system will use Three.js PointsMaterial instead of a custom shader. This avoids any risk of WebGL2 shader compile errors and guarantees zero console warnings. The rising sparks will be implemented as animated ember meshes with fading opacity.

The spark material uses a warm amber color with additive blending and disabled depth writes. A size of 0.12 world units may appear too small at distance, so the author is checking the point sprite size attenuation formula.

Spark size is fixed at 0.16 world units, yielding roughly 10 pixels at the camera distance. The 90 spark particles will pop in and out as a whole rather than fading individually. Mesh embers will fade via opacity for a softer rising effect. Stars will use a 1.8-unit size with sizeAttenuation disabled.

Two star layers with staggered sine phases will create a gentle, non-uniform twinkle. This keeps the scene free of custom shaders and avoids console errors.

Bright star meshes will scale dynamically to enhance the twinkle effect. The flame animation will use a mesh-based approach without any custom shaders. All materials will be standard Three.js built-ins to eliminate shader compile risk entirely. Deprecated APIs such as `renderer.outputEncoding` will be avoided to prevent console warnings.

The renderer will use `outputColorSpace` explicitly while avoiding deprecated light settings like `physicallyCorrectLights` and `useLegacyLights`. Geometry classes will use modern names without the `Buffer` suffix to prevent potential warnings.

The spark particle system will use a single 110-point layer with warm amber color. Eighteen mesh embers will rise from the fire with fading opacity. The author is now writing the ember reset function to scatter them and assign random horizontal velocities.

The ember reset function assigns random horizontal and vertical velocities, plus a brief scatter delay for the initial burst. During animation, each ember integrates its position, fades opacity, and resets once it exceeds its lifetime or rises above 4.2 units. Their color shifts from warm amber to deep orange, and they scale down as they age. A gentle sine sway is added to the horizontal velocity for natural movement.

The author is finalizing the scene setup. Ember frustum culling is enabled since their positions update each frame. A shared initialization function will populate both the point sprite arrays and the mesh object arrays to keep the logic clear. The tree placement loop is being tightened to guarantee at least 30 trees, with 44 total including two accent trees.

A spiral fallback will place any remaining trees if the random placement loop fails to reach the target count. A `moonCorridor` check will ensure trees do not block the moon's light path.

A single reused vector will test whether a tree sits within the moon's 17-degree light cone. Trees closer than 3.5 units to the camera, or farther than 40 units, will be rejected. The minimum spacing between trees is 2.2 units. Accent trees will be placed first and added to the occupied list so random trees avoid them. The author is also considering whether to block trees directly behind the fire.

The fire sits at the center while the camera looks at it, so trees behind it serve as background rather than blocking the view. The camera-to-fire line is clear because no trees fall within radius 11.8, and the camera itself is only 8.5 units away. This guarantees a perfect unobstructed view of the campfire. No additional trees are needed on the far side.

The author is refining the forest layout. The background tree line at radius 12 sits roughly 20 units behind the camera, which feels too distant. To make the surrounding forest feel denser and closer, the minimum placement radius may be tightened to 10.5 units.

A 10.5-unit minimum radius keeps the fire view unobstructed while allowing trees to frame the scene from the side.

The minimum tree radius is set to 11.0 units. This keeps the camera-to-fire ray completely clear and prevents tree foliage from extending into the view center.

The minimum tree radius is raised to 12.5 units. This guarantees a 1.9-unit clearance between the camera-fire ray and the largest tree foliage, keeping the view completely unobstructed.

The minimum tree radius is fixed at 12.2 units. Accent trees at radius 6 with 1.1-unit foliage sit safely outside the center ray. The forest will contain 46 trees arranged in a surrounding wall. A power-biased radius distribution will place more trees closer to the clearing and fewer farther out.

The tree radius distribution is switched to a uniform square-root spread for even area coverage. The author is now finalizing the code and adding a resize handler with pixel ratio updates, plus camera vertical and polar angle limits.

A y-position clamp will be added after `controls.update()` to prevent the camera from dropping below 0.45 units and clipping underground. The author also plans to set `minDistance` to 3.5 and `maxDistance` to 26 so the player can get close to the fire while staying within the forest clearing.

OrbitControls damping is set to 0.06, rotate speed to 0.65, and zoom speed to 0.7. The roasting stick will sit on the right side, not blocking the fire. The author considers the camera side as positive x and z. The scene design is now complete and ready to write.

The author is checking that low objects do not block the fire. Seats, mug, and bedroll sit below 0.4 units and will not obscure the flames. The woodpile is placed behind the fire at roughly 0.7 units high. The roasting stick extends into the fire area at about 1 unit high, with the marshmallow positioned beside the flames for a natural storytelling effect.

The roasting stick tip will sit near (0.4, 0.9, 0.3) beside the flame column. The mug will use a metallic gray-blue color to catch light. The bedroll will be burgundy. Grass tufts will be added in the foreground along the camera side to fill the ground.

The author is now writing the `createTuft` function to generate the 18 foreground grass tufts. Each tuft uses a random leaf material and three to five blades of varying heights. Mushrooms and pebbles will also be scattered across the ground to complete the forest floor.

Grass tufts will reuse the tree leaf material and cast no shadows to avoid noise. Mushrooms will cast shadows for consistency.

The author is finalizing the scene and stopping at the current feature set. They noted that Three.js uses vertical field of view, so the 62-degree angle will render the same vertical scene on both landscape and portrait screens. On a portrait phone the horizontal view will simply be narrower.

The camera aspect ratio will be set from the window dimensions at initialization to match the 1280x800 landscape screenshot. The vertical field of view keeps the moon and fire clearly visible on both portrait and landscape screens.

The moon is placed roughly 20 degrees above the vertical center of the view. This offset keeps it safely inside the 62-degree vertical field of view on both landscape and portrait screens. The tilt is computed by adding a scaled upward vector to the camera's forward direction and normalizing the result.

The moon is tilted 15 degrees above the vertical center so its glow stays fully inside the 62-degree vertical field of view.

The vertical field of view is set to 66 degrees. The camera will sit at (5.6, 2.35, 6.4) aimed at (0, 1.15, 0). The moon is placed 58 units away with a 3.6-unit radius and a 16-degree upward tilt. The glow radius is capped at 12 units to keep the entire moon fully inside the 33-degree half-angle.

Moon glow radii will be 7, 11, and 16 units at low opacity so any slight clipping stays soft. The camera near plane is set to 0.08 to prevent clipping during close orbit. A `buildTree` helper will randomly select pine or broadleaf types, with accent trees placed at the sides as larger pines. An occupied positions array will track placed trees to enforce spacing. The author is now ready to write the complete scene code.

The campfire will use multiple overlapping bright additive cones with tone mapping disabled so the glow remains vivid at any freeze frame. The inner core will be a tall almost-white-yellow cone at 1.15 units high, followed by a warmer orange cone at 1.45 units high. Flame heights will range from 0.8 to 1.7 units, and the spark column will rise to 4 units to create a strong vertical focal point.

The campfire flame is built from eight overlapping additive cones in varying shades of yellow, orange, and red. They are offset slightly and scaled to 1.15 so the glow reads as a bright, oversized hero column above the logs.

The flame group will be scaled to 1.15 so the glow reads as an oversized hero column above the logs. Each cone will receive a random rotation speed between 4 and 8 so the fire shape churns continuously. The author is now writing the complete HTML file with a single `import * as THREE from 'three'` statement and a few section comments for maintainability.

The author finalized the color palette and lighting values. The sky will use deep midnight and slate tones with a soft moon glow. The fire light will be a bright orange point source with high intensity. Foliage colors are set to various greens. A fallback HUD message will be added for non-3D environments.

The author finalized the complete color palette for the scene. Foliage, trunks, rocks, wood, dirt, grass, bedroll, mug, marshmallow, and fireflies all have specific hex values. Ground vertex colors will use this palette for subtle variation. The author noted that if vertex colors have color space issues, the scene will still look fine because the geometry shapes remain visible.

The author verified that Three.js r170 stores color values in LinearSRGBColorSpace by default. Vertex colors written directly to geometry buffers will therefore be linear, which matches WebGLRenderer expectations and avoids color-space conversion issues.

The ground material will use flat shading and vertex colors. Vertex normals will be computed after displacement for correctness, even though the flat-shading shader derives them from position derivatives.

The ground will receive point-light shadows but cast none. The shadow camera far plane is set to 30 units so the ground stays within the 20-unit light radius. The ground material will use a matte Lambert shader to avoid unwanted shine.

The author confirmed the 130-unit ground plane is safely within the point light shadow cube. They set the renderer tone mapping exposure to 1.08. The HUD will use a compact fixed bottom bar with a dark gradient background and flex layout, designed for small screens.

The author finalized the HUD CSS with a dark gradient background, pale text, and a subtle text shadow. On small screens the bar switches to a vertical stack with tighter padding and left-aligned hints. Global `box-sizing: border-box` prevents overflow. The author is now ready to write the complete HTML file, carefully matching every brace.

The author added 22 rocks total: 10 ring stones, 7 scattered around the clearing, and 5 pebbles. A larger left-side boulder will serve as a landmark. The scene is ready to be coded.

Fallen logs will be scattered around the clearing. Grass and tree animation arrays will be stored for per-frame updates. Fireflies will be placed in the midground, with half biased toward the background so they appear around the fire. They will avoid the center radius of 2 and use a 4.5-unit base radius with a 0.6-unit motion radius.

Sixteen fireflies will orbit the campfire at a radius of 4.2 to 11.7 units. Each uses MeshBasicMaterial with toneMapped disabled so they glow independently of the scene lighting. A secondary glow child with additive blending and 0.28 opacity adds extra sparkle.

The firefly opacity will use a sine-wave pattern that never fully fades to zero, keeping them visible at all times.

The author confirmed that disabling tone mapping on MeshBasicMaterial works correctly. They also decided to set the flame materials to depthWrite false so the additive fire columns do not occlude the rising spark particles.

The author assigned renderOrder values to keep the spark and ember particles clearly visible above the additive flame columns.

The camera will automatically rotate around the campfire while the moon stays fixed in world space. This keeps the moon visible in the frame as the view turns. Damping will smooth manual camera movement.

The author finalized the fire light shadow settings to prevent peter-panning. The shadow map is 1024 by 1024 pixels. The camera near plane is 0.3 and the far plane is 24. The bias is -0.002 and the normal bias is 0.025.

The renderer will use PCFSoftShadowMap for softer shadows. The author is now writing the final HTML file. They are also considering MeshPhongMaterial with flat shading for the rocks to add subtle fire-induced gloss.

The author decided to use MeshLambertMaterial for all lit objects to maintain a consistent low-poly matte look. This avoids mixing Phong and Lambert shaders and keeps the scene visually unified.

The scorch circle will be a dark Lambert plane that receives light and shadows, appearing burned and dim. The warm pool will flicker in sync with the fire light. A faint orange ambient tint will be added to the scene near the fire.

The animation loop will use `THREE.Clock` with a 0.05-second delta cap to prevent large jumps when the tab is backgrounded or initialization takes time.

The vignette opacity will be 0.35 so distant trees remain visible at the edges. The moon light will use a directional light with intensity 2.3 aimed at the moon position.

The moon light will explicitly cast no shadows. The author revised the moon intensity down to 1.8 and the ambient light to 0.25, keeping the fire light at 32 so the scene stays dimly lit and readable.

The author increased the moon light intensity to 2.6 and raised the ambient light to 0.28. The renderer exposure is set to 1.12. The sky uses a dark MeshBasic material that ignores light, so the contrast between the dark sky and the bright fire and moderately lit forest creates the night atmosphere.

Fog will fade distant trees at 50 units while keeping near trees fully visible. Edge trees at 4 to 15 units stay clear, and distant ones at 40 units will be softly blurred.

The author realized that MeshBasicMaterial enables fog by default. Since the moon and stars sit beyond the fog far plane, they would be completely hidden. They will set fog: false on the sky, moon, stars, and bright glows so they remain visible.

The author will also disable fog on the firefly particles and the star point cloud so they remain bright when the camera zooms out. Flames will keep fog enabled, allowing distant columns to fade naturally into the atmosphere.

The fog range is tightened to 20–54 units so the camp clearing stays clear when zoomed out. Spark particles will also have fog disabled to remain bright. The sky sphere at radius 120 sits safely beyond the fog far plane.

The sky sphere will use depthWrite false and renderOrder -1 so it stays behind the scene. A small CSS brand mark will be added as a rotated square. The author is now ready to write the complete HTML file.

The author verified that no `</script>` strings appear inside the JavaScript to accidentally close the HTML script tag. The CSS brand mark is a small rotated fire-colored diamond with a soft glow.

The author will use a seeded random generator only during initialization to keep the campfire layout reproducible, while relying on Math.random for all animation logic. They are now beginning to type the complete HTML file, carefully organizing the JavaScript into a clear flow and ensuring no accidental `</script>` or `<!--` strings appear inside the script blocks.

OrbitControls will target the canvas element directly. The renderer will fill the viewport using the default style update. CSS will pin the canvas to the window edges with full width and height.

The author decided against using `!important` on the canvas CSS width and height. `renderer.setSize` already sets the correct pixel dimensions, and overriding them with CSS could cause stretching or layout conflicts.

The author decided to pin the canvas with `position: fixed; top: 0; left: 0` instead of `inset: 0`. This avoids conflicts between CSS width and height styles and the canvas dimensions set by `setSize`, preventing scrollbars while the renderer fills the viewport. The author is now ready to write the final HTML file.

The author confirmed the OrbitControls import path resolves correctly through the provided import map. They also decided to add a warm light ring on the ground near the campfire, complementing the existing warm pool.

The fire pit will be a low-poly circular stone ring. Individual flame wobbles will replace a slow group rotation for a more natural look. The flame group will sit at y 0.3, and the coals will be placed at y 0.08 in world space.

The script will run as top-level code without a main function or top-level await. The ground will stay flat so objects sit correctly at y=0, with only a slight slope near the edges.

The terrain function is finalized. The campsite stays flat within 8 units so the fire pit and logs sit level. Trees will sit on rolling ground beyond that radius. The author is now writing the complete HTML file.

The author is finalizing the tree distribution. A dense inner ring of about fifteen trees will sit at radius twelve to eighteen, while forty-eight outer trees will fill the wider clearing. The inner band has roughly five times less area than the outer zone, so uniform placement would leave it nearly empty. They are weighing whether to add extra trees to the inner ring or rely on fog to create depth.

The author realized that uniform placement would leave the inner ring of trees nearly empty, since it covers only twelve percent of the total area. They will instead use a biased distribution: seventy-two percent of the forty-six trees will sit in the inner clearing at radius twelve to twenty-two, while the remaining thirteen will fill the deeper forest. This creates a dense wall of trees around the campfire and preserves distant depth through fog.

A spiral fallback will place the remaining trees if random attempts fail. The author is now defining the `addTree` helper to build each tree mesh.

The author finalized the `blocksMoon` helper to prevent trees from obscuring the moon. It checks if a tree center at height 2.5 lies within a 16-degree cone in front of the camera along the moon direction. Trees behind the camera are automatically excluded. The author is now ready to produce the final HTML code.

Each tree will receive a slight random lean on its x and z axes for a natural look. A subtle sway animation will gently rock the trees back and forth using sine and cosine offsets. The author is now beginning to plan the grass details.

Grass tufts will sway in the wind using a gentle rotation.z offset while keeping their initial random facing intact. Six transparent gray smoke puffs will rise slowly from the fire, expanding and fading as they drift upward.

The author decided against adding smoke particles. Gray smoke against the dark night sky would look dirty and cluttered, so they will rely solely on the spark particles for the required particle system.

A roasting stick will sit beside the fire. A thin cylinder forms the stick. A small sphere sits at the top for the mallow. Both cast shadows.

Two horizontal log seats and a stump will sit in the foreground around the campfire so the scene reads as a campfire clearing. The `createLog` helper will build logs as cylinders lying along the X axis, with rotation.y orienting them toward the fire.

The `createLog` helper now rotates logs 90 degrees so they lie along the X axis, then orients them with `rotation.y`. Seats will sit at `position.y = 0.18` to rest on the ground. The fire bed logs will tilt slightly on the X axis and rotate on Y to create a messy, uneven pile. Some logs will also be darkened at their centers for extra realism.

The camera target will be shifted slightly to (0, 1.0, 0) so the ground and flames sit vertically centered in the frame. The view angle drops about nine degrees from the camera position. The moon will sit sixteen degrees above the view center. Two of the five fire bed logs will use a charred material for extra realism.

The camera target is fixed at (0, 1.05, 0) to keep the upper sky and ground in a balanced composition. The existing slight asymmetry in the camera position is sufficient, so no extra offset is needed. Horizontal rotation will remain unbounded so the user can look all around the campfire.

OrbitControls will disable panning so the campfire cannot be lost or the camera dropped below the ground. Zoom limits will keep the camera between 3.2 and 24 units from the fire, with the minimum distance ensuring the view stays above the terrain.

The author is now generating the final code. They are adding craters to the moon mesh and implementing a vertex-colored sky sphere that fades from deep blue at the zenith to purple at the horizon, with a subtle glow near the moon.

The author will reuse a single Color object for the sky gradient to avoid repeated allocations. They are now writing the complete HTML file in one pass. They also plan to add warm and cool vertex colors to the star field for a more natural night sky.

The star field will use two layers: 380 stars at size 1.5 and 80 at size 2.4, plus 10 mesh sparkle stars. A `createStarField` helper will place stars at random radii while rejecting any that fall too close to the moon direction so they do not sit on the moon disc.

The author finalized the star-field rejection logic. Small stars will be excluded if they fall within 0.992 of the moon direction, while bright mesh stars will be excluded at 0.97. This prevents them from sitting on the moon disc. They are now writing the complete HTML file, keeping the code compact and ensuring every requirement is clearly implemented.

The spark count is raised to 120 points plus 18 embers for a dense rising column. Ember size is set to 0.07 radius so they remain visible as distinct orange-yellow particles at distance.

Ember sizes will vary from 0.7 to 1.3 times the base radius. The spark system will use a vertical velocity of 1.2 to 2.4 units per second with a 1.4 second lifetime, creating a visible column rising 2 to 3.5 units above the fire. The author is now finalizing the HTML file and adding a `user-select: none` style to the body so canvas dragging does not accidentally select text.

The author decided to set `toneMapped` and `fog` after material construction rather than inside the constructor, to avoid relying on Three.js internal parameter handling.

The author checked the Three.js Material source and confirmed that constructor parameters are assigned directly, so `toneMapped` and `fog` can be set safely inside the constructor rather than after instantiation.

The ground geometry will have its position attribute marked for update before computing normals, and its bounding sphere recalculated after displacement so frustum culling stays accurate.

The author calculated that a 0.8-unit ground displacement barely exceeds the original 91.9-unit bounding sphere radius. Recomputing the sphere after displacement is therefore unnecessary.

The author will recompute the ground bounding sphere after displacement as a safeguard. They decided against adding separate hill meshes and instead rely on the terrain's gentle undulations. The ground edge at radius 65 might still be visible at maximum zoom before the fog fully masks it.

The fog color is being adjusted to 0x16243f so it matches the sky horizon and the ground fades seamlessly into the night sky.

The author finalized the sky and fog palette. The horizon and fog color will both be 0x1a2744, the zenith will be 0x080c18, and the renderer clear color will match. This ensures the ground fades seamlessly into the night sky. The author is now writing the complete HTML file.

The author will add a persistent drag-to-orbit hint that does not block the view. The scene title will be "Midnight Clearing". The animation loop will keep running even when the document is hidden. The hint text may wrap to two lines on small screens, but this is acceptable.

The author confirmed the drag hint and title fit on 375px screens. They will reduce letter-spacing and font size for small screens to prevent overflow. The HTML file is now being written.

A subtle bottom gradient will be added to the HUD background so text stays readable over the 3D scene. The author is carefully checking variable ordering to avoid using moonDir before it is defined, and ensuring accent trees are placed only after the camera view vectors exist.

The author is reconsidering the moon direction calculation. Manual cross products risk a handedness error in Three.js camera space, so they will instead rely on `camera.lookAt(target)` and `camera.updateMatrixWorld()` to derive the screen-up vector directly from the camera's world matrix.

The author will derive the camera's local axes from its quaternion after `lookAt` and `updateMatrixWorld`. Local -Z becomes the view direction, local +Y becomes screen up, and local +X becomes screen right. This avoids manual cross-product errors in Three.js camera space.

The moon will be placed 16 degrees above the center of the initial view using the camera quaternion axes. OrbitControls will be initialized with the camera position and target set before the first update so the look direction stays stable.

The author drafted the `accentPosition` helper to place accent trees at 4.8 units ahead of the camera and 3.6 units to the side, snapping them outward if they fall too close to the fire.