Models 44

GLM 5.3

by Z.aiReasoningClean
Open in new window

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

How it went

Total time
3 min 34 s
Cost
$0.059
Output tokens
42,040
Spent thinking
27,647 tokens
Speed
196.8 tok/s
Console errors
0
First token after
382 ms
First visible text after
2 min 59 s
Prompt tokens
559
Code size
21.3 KB
Finish reason
stop
Served by
Wafer
Model id
z-ai/glm-5.3
Reasoning sent
{"effort":"max"}
Max output
943,718 tokens
Generated
Nov 12, 2026

What it was asked

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

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

Code run-1.html · 529 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Night Camp — Low-Poly Forest Clearing</title>
<style>
  :root { color-scheme: dark; }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #060b14; }
  canvas { display: block; }
  .ui { position: fixed; inset: 0; pointer-events: none; z-index: 10;
        font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        color: #d8e2f2; }
  .vignette { position: absolute; inset: 0;
        background: radial-gradient(120% 90% at 50% 42%, rgba(0,0,0,0) 52%, rgba(3,6,14,0.55) 100%); }
  .title { position: absolute; top: 24px; left: 28px; }
  .title h1 { font-size: 15px; font-weight: 600; letter-spacing: 0.38em; text-transform: uppercase;
        text-shadow: 0 2px 14px rgba(0,0,0,0.85); }
  .title p { margin-top: 7px; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; opacity: 0.55; }
  #hint { position: absolute; bottom: 22px; right: 28px; font-size: 10.5px; letter-spacing: 0.24em;
        text-transform: uppercase; opacity: 0.65; transition: opacity 1.4s ease;
        text-shadow: 0 1px 10px rgba(0,0,0,0.9); }
  #hint.dim { opacity: 0.15; }
  @media (max-width: 560px) {
    .title { top: 16px; left: 18px; }
    .title h1 { font-size: 12px; }
    #hint { bottom: 16px; right: 16px; font-size: 9.5px; }
  }
</style>
</head>
<body>
<div class="ui">
  <div class="vignette"></div>
  <div class="title">
    <h1>Night&nbsp;Camp</h1>
    <p>Low-poly forest clearing &middot; three.js</p>
  </div>
  <div id="hint">Drag to orbit &middot; scroll to zoom</div>
</div>

<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>

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

/* ---------- soft round glow texture (used by sparks, fireflies, halos) ---------- */
function makeGlowTexture() {
  const c = document.createElement('canvas');
  c.width = c.height = 64;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  g.addColorStop(0.0, 'rgba(255,255,255,1)');
  g.addColorStop(0.35, 'rgba(255,255,255,0.45)');
  g.addColorStop(1.0, 'rgba(255,255,255,0)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, 64, 64);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}
const glowTex = makeGlowTexture();

/* ---------- renderer / scene / camera ---------- */
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.25;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x060b14);
scene.fog = new THREE.FogExp2(0x060b14, 0.022);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1200);
camera.position.set(8.5, 3.6, 10.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.5, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 4;
controls.maxDistance = 40;
controls.maxPolarAngle = Math.PI / 2 - 0.07;   // never below the horizon
controls.minPolarAngle = 0.12;
controls.autoRotate = true;
controls.autoRotateSpeed = -0.55;
controls.update();

/* ---------- lights ---------- */
scene.add(new THREE.HemisphereLight(0x3c4b68, 0x131a10, 0.7));

const moonDir = new THREE.Vector3(-0.52, 0.55, -0.78).normalize();
const moonLight = new THREE.DirectionalLight(0xa9c0ec, 0.7);
moonLight.position.copy(moonDir).multiplyScalar(80);
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xff9040, 95, 50, 2);
fireLight.position.set(0, 1.6, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 50;
fireLight.shadow.bias = -0.008;
fireLight.shadow.normalBias = 0.02;
scene.add(fireLight);

/* ---------- deterministic terrain noise ---------- */
function fract(n) { return n - Math.floor(n); }
function hash2(x, y) { return fract(Math.sin(x * 127.1 + y * 311.7) * 43758.5453); }
function noise2(x, y) {
  const xi = Math.floor(x), yi = Math.floor(y);
  const xf = x - xi, yf = y - yi;
  const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
  const a = hash2(xi, yi), b = hash2(xi + 1, yi), c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
  return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
function groundHeight(x, z) {
  const r = Math.hypot(x, z);
  const ramp = Math.min(1, Math.max(0, (r - 6) / 10));      // flat clearing at the center
  let h = (noise2(x * 0.13 + 7.3, z * 0.13 + 3.1) * 0.75 +
           noise2(x * 0.34 + 1.7, z * 0.34 + 9.2) * 0.3) * ramp;
  h += Math.pow(Math.max(0, r - 38) / 20, 2) * 1.6;          // hills hiding the horizon
  return h;
}

/* ---------- ground (vertex-coloured, flat shaded) ---------- */
const groundGeo = new THREE.PlaneGeometry(160, 160, 46, 46);
groundGeo.rotateX(-Math.PI / 2);
{
  const gp = groundGeo.attributes.position;
  const gcol = new Float32Array(gp.count * 3);
  const cA = new THREE.Color(0x243518), cB = new THREE.Color(0x2f4423), cDirt = new THREE.Color(0x3f3425);
  const tmp = new THREE.Color();
  for (let i = 0; i < gp.count; i++) {
    const x = gp.getX(i), z = gp.getZ(i);
    gp.setY(i, groundHeight(x, z));
    const r = Math.hypot(x, z);
    tmp.lerpColors(cA, cB, noise2(x * 0.21 + 4.4, z * 0.21 + 8.8));
    tmp.lerp(cDirt, 1 - THREE.MathUtils.smoothstep(r, 2.4, 5.0));      // dry dirt near the fire
    tmp.multiplyScalar(1 - 0.3 * THREE.MathUtils.smoothstep(r, 18, 70));
    gcol[i * 3] = tmp.r; gcol[i * 3 + 1] = tmp.g; gcol[i * 3 + 2] = tmp.b;
  }
  groundGeo.setAttribute('color', new THREE.BufferAttribute(gcol, 3));
  groundGeo.computeVertexNormals();
}
const ground = new THREE.Mesh(groundGeo,
  new THREE.MeshStandardMaterial({ vertexColors: true, flatShading: true, roughness: 1, metalness: 0 }));
ground.receiveShadow = true;
scene.add(ground);

/* ---------- starfield ---------- */
const STAR_N = 900;
const spArr = new Float32Array(STAR_N * 3), scArr = new Float32Array(STAR_N * 3);
{
  const sv = new THREE.Vector3();
  for (let i = 0; i < STAR_N; i++) {
    sv.randomDirection();
    if (sv.y < 0.03) { sv.y = 0.03 + Math.random() * 0.9; sv.normalize(); }
    const R = 420 + Math.random() * 90;
    spArr[i * 3] = sv.x * R; spArr[i * 3 + 1] = sv.y * R; spArr[i * 3 + 2] = sv.z * R;
    const b = 0.25 + Math.pow(Math.random(), 2.4) * 0.95;
    const tone = Math.random();
    let r = b, g = b, bl = b;
    if (tone < 0.22) { r = b * 0.8; bl = b * 1.05; }        // blue giants
    else if (tone < 0.34) { g = b * 0.92; bl = b * 0.75; }  // warm stars
    scArr[i * 3] = r; scArr[i * 3 + 1] = g; scArr[i * 3 + 2] = bl;
  }
}
const starGeo = new THREE.BufferGeometry();
starGeo.setAttribute('position', new THREE.BufferAttribute(spArr, 3));
starGeo.setAttribute('color', new THREE.BufferAttribute(scArr, 3));
const starMat = new THREE.PointsMaterial({
  size: 2.4, sizeAttenuation: false, vertexColors: true, transparent: true, opacity: 0.9,
  depthWrite: false, blending: THREE.AdditiveBlending, fog: false
});
scene.add(new THREE.Points(starGeo, starMat));

/* ---------- moon + halo ---------- */
const moon = new THREE.Mesh(new THREE.SphereGeometry(19, 24, 16),
  new THREE.MeshBasicMaterial({ color: 0xdfe6f8, fog: false }));
moon.position.copy(moonDir).multiplyScalar(460);
scene.add(moon);
const moonGlow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: glowTex, color: 0xaebfff, transparent: true, opacity: 0.35,
  depthWrite: false, blending: THREE.AdditiveBlending, fog: false
}));
moonGlow.scale.set(150, 150, 1);
moonGlow.position.copy(moonDir).multiplyScalar(440);
scene.add(moonGlow);

/* ---------- low-poly trees (42, ring around the clearing) ---------- */
const trunkGeo = new THREE.CylinderGeometry(0.16, 0.28, 2.2, 5);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3a2a1e, flatShading: true, roughness: 1 });
const foliageGeo = [
  new THREE.ConeGeometry(1.55, 2.6, 6),
  new THREE.ConeGeometry(1.15, 2.15, 6),
  new THREE.ConeGeometry(0.78, 1.75, 6)
];
const foliageY = [1.9, 3.2, 4.4];
const foliageMats = [0x1b3323, 0x22422b, 0x2a4d33, 0x173020].map(
  c => new THREE.MeshStandardMaterial({ color: c, flatShading: true, roughness: 1 })
);
const treePts = [];
let guard = 0;
while (treePts.length < 42 && guard++ < 4000) {
  const a = Math.random() * Math.PI * 2;
  const rr = Math.sqrt(THREE.MathUtils.lerp(14 * 14, 52 * 52, Math.random()));
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  let ok = true;
  for (const p of treePts) {
    const dx = p[0] - x, dz = p[1] - z;
    if (dx * dx + dz * dz < 4.8) { ok = false; break; }
  }
  if (ok) treePts.push([x, z]);
}
const trees = new THREE.Group();
scene.add(trees);
for (const [x, z] of treePts) {
  const g = new THREE.Group();
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y = 1.1;
  trunk.castShadow = trunk.receiveShadow = true;
  g.add(trunk);
  const mi = (Math.random() * 4) | 0;
  for (let k = 0; k < 3; k++) {
    const m = new THREE.Mesh(foliageGeo[k], foliageMats[(mi + k) % 4]);
    m.position.y = foliageY[k];
    m.rotation.y = Math.random() * Math.PI * 2;
    m.castShadow = m.receiveShadow = true;
    g.add(m);
  }
  g.scale.setScalar(0.85 + Math.random() * 0.75);
  g.rotation.y = Math.random() * Math.PI * 2;
  g.rotation.x = (Math.random() - 0.5) * 0.07;
  g.rotation.z = (Math.random() - 0.5) * 0.07;
  g.position.set(x, groundHeight(x, z) - 0.18, z);
  trees.add(g);
}

/* ---------- rocks ---------- */
const rockMats = [0x4e545c, 0x5a616a, 0x454b52].map(
  c => new THREE.MeshStandardMaterial({ color: c, flatShading: true, roughness: 1 })
);
function rockGeo(radius, seed) {
  const g = new THREE.IcosahedronGeometry(radius, 0);
  const p = g.attributes.position;
  for (let i = 0; i < p.count; i++) {
    const x = p.getX(i), y = p.getY(i), z = p.getZ(i);
    const h = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719 + seed) * 43758.5453;
    const f = h - Math.floor(h);
    p.setXYZ(i, x * (0.78 + f * 0.5), y * (0.7 + f * 0.55), z * (0.78 + f * 0.5));
  }
  g.computeVertexNormals();
  return g;
}
// stone ring around the fire
for (let i = 0; i < 9; i++) {
  const a = (i / 9) * Math.PI * 2 + Math.random() * 0.25;
  const m = new THREE.Mesh(rockGeo(0.3 + Math.random() * 0.1, i * 3.7), rockMats[i % 3]);
  m.position.set(Math.cos(a) * 1.5, 0.1, Math.sin(a) * 1.5);
  m.scale.y = 0.7;
  m.rotation.y = Math.random() * Math.PI;
  m.castShadow = m.receiveShadow = true;
  scene.add(m);
}
// scattered boulders
for (let i = 0; i < 10; i++) {
  const a = Math.random() * Math.PI * 2;
  const rr = 3.2 + Math.random() * 9;
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  const rad = 0.35 + Math.random() * 0.55;
  const m = new THREE.Mesh(rockGeo(rad, i * 7.13), rockMats[(i + 1) % 3]);
  m.position.set(x, groundHeight(x, z) + rad * 0.18, z);
  m.rotation.set(Math.random() * 0.4, Math.random() * Math.PI * 2, Math.random() * 0.4);
  m.castShadow = m.receiveShadow = true;
  scene.add(m);
}

/* ---------- logs: teepee firewood, fallen logs, stumps ---------- */
const woodMat = new THREE.MeshStandardMaterial({ color: 0x53392a, flatShading: true, roughness: 0.95 });
const barkMat = new THREE.MeshStandardMaterial({ color: 0x453021, flatShading: true, roughness: 1 });
const firewoodGeo = new THREE.CylinderGeometry(0.09, 0.12, 1.7, 5);
const campfire = new THREE.Group();
scene.add(campfire);
for (let i = 0; i < 7; i++) {
  const g = new THREE.Group();
  g.rotation.y = (i / 7) * Math.PI * 2 + Math.random() * 0.3;
  const log = new THREE.Mesh(firewoodGeo, woodMat);
  log.position.set(0.52, 0.8, 0);
  log.rotation.z = 0.42 + Math.random() * 0.12;  // lean tops toward the flames
  log.castShadow = true;
  g.add(log);
  campfire.add(g);
}
// glowing coals
const coalMat = new THREE.MeshStandardMaterial({
  color: 0x140a05, emissive: 0xff3c00, emissiveIntensity: 1.4, flatShading: true, roughness: 1
});
for (let i = 0; i < 6; i++) {
  const a = Math.random() * Math.PI * 2, rr = Math.random() * 0.45;
  const m = new THREE.Mesh(new THREE.IcosahedronGeometry(0.12 + Math.random() * 0.12, 0), coalMat);
  m.position.set(Math.cos(a) * rr, 0.1 + Math.random() * 0.1, Math.sin(a) * rr);
  m.rotation.set(Math.random(), Math.random(), Math.random());
  campfire.add(m);
}
// fallen logs in the clearing
const fallenGeo = new THREE.CylinderGeometry(0.16, 0.2, 2.6, 6);
for (let i = 0; i < 4; i++) {
  const a = i * 1.9 + 0.7;
  const rr = 3.6 + (i * 1.37) % 6;
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  const g = new THREE.Group();
  g.position.set(x, groundHeight(x, z) + 0.15, z);
  g.rotation.y = Math.random() * Math.PI * 2;
  const m = new THREE.Mesh(fallenGeo, barkMat);
  m.rotation.z = Math.PI / 2 + (Math.random() - 0.5) * 0.12;
  m.castShadow = m.receiveShadow = true;
  g.add(m);
  scene.add(g);
}
// stumps
const stumpGeo = new THREE.CylinderGeometry(0.28, 0.34, 0.7, 6);
for (let i = 0; i < 2; i++) {
  const a = i * 2.8 + 1.2, rr = 4.5 + i * 2.6;
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  const m = new THREE.Mesh(stumpGeo, barkMat);
  m.position.set(x, groundHeight(x, z) + 0.3, z);
  m.rotation.y = Math.random() * Math.PI;
  m.castShadow = m.receiveShadow = true;
  scene.add(m);
}

/* ---------- animated low-poly flames (additive glowing cones) ---------- */
const flames = [];
function addFlame(r, h, seg, y, color, opacity, speed, spin, off) {
  const m = new THREE.Mesh(
    new THREE.ConeGeometry(r, h, seg),
    new THREE.MeshBasicMaterial({
      color, transparent: true, opacity, depthWrite: false,
      blending: THREE.AdditiveBlending, fog: false
    })
  );
  m.position.set(off[0], y, off[2]);
  campfire.add(m);
  flames.push({ m, h, baseY: y, speed, phase: Math.random() * Math.PI * 2, spin, ox: off[0], oz: off[2] });
}
addFlame(0.72, 2.30, 6, 1.15, 0xff4200, 0.50, 5.3,  0.7, [0, 0, 0]);
addFlame(0.50, 1.95, 5, 1.00, 0xff8412, 0.65, 6.4, -0.9, [0, 0, 0]);
addFlame(0.32, 1.55, 5, 0.88, 0xffc23c, 0.80, 7.1,  1.2, [0, 0, 0]);
addFlame(0.17, 1.15, 4, 0.74, 0xffe9a6, 0.90, 7.9,  1.7, [0, 0, 0]);
for (let i = 0; i < 5; i++) {  // small licking flames around the edge
  const a = (i / 5) * Math.PI * 2 + 0.5;
  const rr = 0.34 + Math.random() * 0.22;
  addFlame(0.14 + Math.random() * 0.06, 0.7 + Math.random() * 0.5, 4, 0.62,
    0xff6b12, 0.7, 6.5 + Math.random() * 3, (Math.random() - 0.5) * 5,
    [Math.cos(a) * rr, 0, Math.sin(a) * rr]);
}
function updateFlames(t, flick) {
  for (const f of flames) {
    const s = Math.sin(t * f.speed + f.phase);
    const s2 = Math.sin(t * f.speed * 2.13 + f.phase * 1.71);
    const sy = (1 + 0.3 * s + 0.12 * s2) * flick;
    const w = (1 + 0.13 * Math.sin(t * f.speed * 0.93 + f.phase * 2.1)) * (0.85 + 0.15 * flick);
    f.m.scale.set(w, sy, w);
    f.m.position.y = f.baseY + 0.07 * Math.sin(t * f.speed * 0.8 + f.phase) + f.h * 0.5 * (sy - 1) * 0.45;
    f.m.position.x = f.ox + 0.05 * Math.sin(t * f.speed * 0.7 + f.phase);
    f.m.position.z = f.oz + 0.05 * Math.cos(t * f.speed * 0.62 + f.phase);
    f.m.rotation.y = f.phase + t * f.spin;
    f.m.rotation.x = 0.13 * Math.sin(t * f.speed * 0.5 + f.phase);
    f.m.rotation.z = 0.13 * Math.cos(t * f.speed * 0.44 + f.phase);
  }
}

/* ---------- fire glow halo ---------- */
const fireGlow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: glowTex, color: 0xffa040, transparent: true, opacity: 0.45,
  depthWrite: false, blending: THREE.AdditiveBlending, fog: false
}));
fireGlow.scale.set(7, 7, 1);
fireGlow.position.set(0, 1.3, 0);
scene.add(fireGlow);

/* ---------- sparks (rising particle system) ---------- */
const SPARK_N = 150;
const sPos = new Float32Array(SPARK_N * 3), sCol = new Float32Array(SPARK_N * 3);
const sVel = new Float32Array(SPARK_N * 3), sLife = new Float32Array(SPARK_N), sMax = new Float32Array(SPARK_N);
function spawnSpark(i, scatter) {
  sMax[i] = 0.6 + Math.random() * 1.5;
  sLife[i] = scatter ? Math.random() * sMax[i] : sMax[i];
  const i3 = i * 3;
  sPos[i3] = (Math.random() - 0.5) * 0.5;
  sPos[i3 + 1] = 0.5 + Math.random() * 0.8;
  sPos[i3 + 2] = (Math.random() - 0.5) * 0.5;
  const a = Math.random() * Math.PI * 2, sp = 0.25 + Math.random() * 0.5;
  sVel[i3] = Math.cos(a) * sp;
  sVel[i3 + 1] = 1.4 + Math.random() * 1.9;
  sVel[i3 + 2] = Math.sin(a) * sp;
}
for (let i = 0; i < SPARK_N; i++) spawnSpark(i, true);
const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3).setUsage(THREE.DynamicDrawUsage));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sCol, 3).setUsage(THREE.DynamicDrawUsage));
const sparks = new THREE.Points(sparkGeo, new THREE.PointsMaterial({
  size: 0.1, map: glowTex, transparent: true, depthWrite: false,
  blending: THREE.AdditiveBlending, vertexColors: true, sizeAttenuation: true, fog: false
}));
sparks.frustumCulled = false;
scene.add(sparks);
function updateSparks(dt, t) {
  for (let i = 0; i < SPARK_N; i++) {
    const i3 = i * 3;
    sLife[i] -= dt;
    if (sLife[i] <= 0) spawnSpark(i, false);
    const drag = Math.exp(-1.15 * dt);
    sVel[i3]     = sVel[i3] * drag + Math.sin(t * 0.9 + i * 1.7) * 0.06 * dt;
    sVel[i3 + 1] = sVel[i3 + 1] * drag + 0.4 * dt;
    sVel[i3 + 2] = sVel[i3 + 2] * drag + Math.cos(t * 0.8 + i * 1.3) * 0.06 * dt;
    sPos[i3]     += sVel[i3] * dt;
    sPos[i3 + 1] += sVel[i3 + 1] * dt;
    sPos[i3 + 2] += sVel[i3 + 2] * dt;
    const fade = Math.max(0, sLife[i] / sMax[i]);
    const b = Math.pow(fade, 1.1);
    sCol[i3]     = b;
    sCol[i3 + 1] = b * (0.3 + 0.55 * fade);
    sCol[i3 + 2] = b * 0.12;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;
}

/* ---------- fireflies drifting in the clearing ---------- */
const FF_N = 16;
const ffPos = new Float32Array(FF_N * 3), ffCol = new Float32Array(FF_N * 3);
const ffBx = new Float32Array(FF_N), ffBy = new Float32Array(FF_N), ffBz = new Float32Array(FF_N);
const ffP = new Float32Array(FF_N), ffS = new Float32Array(FF_N), ffB = new Float32Array(FF_N);
for (let i = 0; i < FF_N; i++) {
  const a = Math.random() * Math.PI * 2, rr = 3.5 + Math.random() * 9;
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  ffBx[i] = x; ffBz[i] = z;
  ffBy[i] = groundHeight(x, z) + 0.5 + Math.random() * 1.8;
  ffP[i] = Math.random() * Math.PI * 2;
  ffS[i] = 0.4 + Math.random() * 0.5;
  ffB[i] = 1.2 + Math.random() * 2.2;
}
const ffGeo = new THREE.BufferGeometry();
ffGeo.setAttribute('position', new THREE.BufferAttribute(ffPos, 3).setUsage(THREE.DynamicDrawUsage));
ffGeo.setAttribute('color', new THREE.BufferAttribute(ffCol, 3).setUsage(THREE.DynamicDrawUsage));
const fireflies = new THREE.Points(ffGeo, new THREE.PointsMaterial({
  size: 0.16, map: glowTex, transparent: true, depthWrite: false,
  blending: THREE.AdditiveBlending, vertexColors: true, fog: false
}));
fireflies.frustumCulled = false;
scene.add(fireflies);
function updateFireflies(t) {
  for (let i = 0; i < FF_N; i++) {
    const i3 = i * 3, p = ffP[i], s = ffS[i];
    ffPos[i3]     = ffBx[i] + Math.sin(t * s * 1.3 + p) * 1.2;
    ffPos[i3 + 1] = ffBy[i] + Math.sin(t * s * 2.1 + p * 2.0) * 0.55;
    ffPos[i3 + 2] = ffBz[i] + Math.cos(t * s * 1.1 + p) * 1.2;
    const q = Math.sin(t * ffB[i] + p * 3.0);
    const b = 0.05 + Math.pow(Math.max(0, q), 3) * 1.1;
    ffCol[i3] = 0.62 * b; ffCol[i3 + 1] = b; ffCol[i3 + 2] = 0.3 * b;
  }
  ffGeo.attributes.position.needsUpdate = true;
  ffGeo.attributes.color.needsUpdate = true;
}

/* ---------- hint + idle auto-rotate management ---------- */
const hint = document.getElementById('hint');
let hintDimmed = false;
function dimHint() { if (!hintDimmed) { hintDimmed = true; hint.classList.add('dim'); } }
setTimeout(dimHint, 10000);
let idleTimer = null;
controls.addEventListener('start', () => {
  controls.autoRotate = false;
  if (idleTimer) clearTimeout(idleTimer);
  dimHint();
});
controls.addEventListener('end', () => {
  if (idleTimer) clearTimeout(idleTimer);
  idleTimer = setTimeout(() => { controls.autoRotate = true; }, 20000);
});

/* ---------- resize ---------- */
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

/* ---------- main loop ---------- */
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  // fire flicker: layered sines + jittering light position => dancing shadows
  const flick = 0.86
    + 0.10 * Math.sin(t * 11.7)
    + 0.06 * Math.sin(t * 27.3 + 1.6)
    + 0.05 * Math.sin(t * 6.9 + 3.1)
    + 0.03 * Math.sin(t * 43.1 + 0.7);
  fireLight.intensity = 95 * flick;
  fireLight.position.set(
    Math.sin(t * 3.7) * 0.14,
    1.55 + Math.sin(t * 9.3) * 0.12,
    Math.cos(t * 3.1) * 0.14
  );
  fireGlow.material.opacity = 0.26 + 0.22 * flick;
  coalMat.emissiveIntensity = 0.7 + 1.1 * flick;
  starMat.opacity = 0.75 + 0.2 * Math.sin(t * 0.7);

  updateFlames(t, 0.8 + 0.25 * flick);
  updateSparks(dt, t);
  updateFireflies(t);
  controls.update();
  renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
Reasoning trace (27,647 tokens)
The user wants a low-poly 3D campfire scene in a forest clearing at night, using Three.js r0.170.0 with the specified import map. Let me plan carefully.

Key requirements:
1. Ground with low-poly rocks and logs around campfire in center
2. At least 30 low-poly trees placed randomly around the clearing (not on fire)
3. Campfire with animated flames and warm point light that flickers and casts shadows
4. Sparks rising from fire as particle system
5. Dark night sky with stars and moon, plus fog fading distant trees
6. OrbitControls with limits so camera stays above ground
7. Renderer fills window, handles resize
8. Self-playing demo mode? The instructions say "Games must start in a self-playing demo mode" — this is a scene, not a game, but the general instruction about "Click or press a key to start" applies mainly to keyboard-dependent demos. This is OrbitControls (mouse) based. I should still have something meaningful visible on first paint (screenshot at ~3s). I'll just render the scene immediately — it's not a game so no start screen needed. But to be safe, maybe add a subtle hint overlay "Drag to orbit • Scroll to zoom" that fades. That satisfies the guidance without hiding anything.

Technical plan:

- Use module script with import map exactly as specified.
- import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

Scene setup:
- Renderer: antialias true, shadowMap enabled (PCFSoftShadowMap), setPixelRatio min(devicePixelRatio, 2), tone mapping ACESFilmic maybe, output color space default is SRGB in r170.
- Scene background: dark night color (very dark blue #05070f or similar). Fog: THREE.Fog or FogExp2 with dark color matching background. FogExp2 density ~0.028 or so — but need distant trees faded but visible. Scene radius maybe 60 units. Use Fog(color, near 20, far 90) linear maybe. Let me use FogExp2 density 0.018.

Ground:
- A large circle geometry or plane, low poly. Use CircleGeometry(60, 24 or so) rotated -PI/2, with slight vertex displacement for terrain bumps? Low-poly ground: use PlaneGeometry(120,120, 24,24), displace vertices with pseudo-random noise (except near center flat), flatShading true, material dark green/brown. Actually for low-poly look with flat shading, need non-indexed or compute flat normals — PlaneGeometry with flatShading material works if we displace vertices; flatShading uses computed face normals... Actually flatShading:true on MeshStandardMaterial makes shader use derivative-based normals? No — flatShading in three.js computes normals per face by using non-indexed geometry? Let me recall: THREE flat shading works via `flatShading: true` which uses the geometry's normals but... Actually in WebGL renderer, flatShading uses `#ifdef FLAT_SHADED` with derivatives (dFdx/dFdy) to compute face normals in the fragment shader. So it works with any geometry. Good.

Displace plane vertices: keep center region (radius < 8) flat-ish for fire area. Use a simple hash-based noise: height = noise(x,z) * 0.6, fade to 0 near center.

Rocks:
- Create low-poly rocks: IcosahedronGeometry(radius, 0) with random vertex displacement, flat shading, gray material. Place ~8-12 rocks around fire ring and scattered.

Logs:
- Cylinders (low segment count: CylinderGeometry(r, r, len, 6)) brown, positioned leaning into teepee around fire, plus a couple of fallen logs on ground elsewhere. Firewood: 5-6 logs arranged teepee style around center, tilted.

Campfire:
- Stone ring: small icosahedron rocks in circle radius ~1.2.
- Logs crossed in center.
- Flames: animated low-poly flame meshes. Approach: several cone/tetra shapes made from ConeGeometry(radius, height, 4-5 segments) with flatShading, emissive orange materials, animated scale/rotation, plus vertex animation? Simpler: a set of "flame" meshes (cones with 4 radial segments = pyramid look) with emissive materials, animate their scale.y and position.y with sine functions, and slight rotation. Also inner yellow core. Use additive blending? Emissive materials with bright colors work well. Use MeshBasicMaterial or MeshStandardMaterial with emissive. For flickering, animate intensity.

Better flame: create a custom "flame" as a few overlapping cones: outer orange (transparent, additive), inner yellow, animate each with different phase: scale y oscillates, position y bobs, rotation.y spins slowly. That gives decent animated fire.

Also maybe animate flame geometry vertices for a billow effect — keep simple: scale + slight shear via rotation.

Point light:
- PointLight(0xff8c3b or 0xffa050, intensity, distance, decay). In r170, lights use physical units by default? Since r155, `useLegacyLights` default false, so PointLight intensity is in candela; intensity needs to be large-ish. With decay=2, intensity ~ 30-60 might work for a fire radius. Let me set intensity around 40, distance 30. Animate flicker: intensity = base + noise. castShadow true, shadow.mapSize 1024, shadow bias small negative (-0.02?) to avoid acne. Point light shadows are cube shadows (6 faces) — expensive but fine.

Also add a subtle ambient/hemisphere light: very dim blue moonlight. HemisphereLight(sky dark blue, ground dark, intensity ~0.15). Plus a directional "moonlight" from moon position, dim bluish, maybe castShadow false to save perf (point light casts shadows; requirement says point light casts shadows). Could also let moon directional cast shadows but keep simple.

Stars:
- Points with random positions on sphere shell radius ~300-400, size attenuation, white/blue-ish colors. Use BufferGeometry with ~800 stars, PointsMaterial({size: 1.2, sizeAttenuation:true? at radius 400 they'd be tiny). Better sizeAttenuation false with size ~1.5-2 px? PointsMaterial size is in pixels when sizeAttenuation false. Randomize sizes: use a custom shader or vertex colors with varying sizes — simplest: two Points layers with different sizes, or use size attenuation true with size ~ 3 at distance 300... Points size attenuation: gl_PointSize = size * (scale / -mvPosition.z). At distance 400 with size 2 → tiny. Let me just use sizeAttenuation:false, size 1.6, and add a few bigger "bright stars". Or use vertexColors with slight color variation.

Alternatively make stars as small sprites... Keep Points. Twinkle: animate opacity? Could animate material opacity slightly, or per-star twinkle via shader. Keep simple: subtle global opacity sine won't be per-star but fine. Actually a nice trick: use two star point clouds and alternate opacities for twinkle effect. Fine.

Moon:
- Sphere geometry (low poly: IcosahedronGeometry(r, 1) or SphereGeometry(r, 16, 12)) with MeshBasicMaterial pale white/gray, slight emissive look, positioned far away in sky, e.g., position (60, 80, -120), scaled to look decent. Add a soft glow: a sprite or a larger sphere with additive transparent material low opacity. Moonlight directional from same direction.

Trees (≥30):
- Low-poly pine trees: trunk = CylinderGeometry(0.25, 0.35, 1.5, 5), foliage = 2-3 stacked cones ConeGeometry(1.2..0.7, 2..1.5, 6) with flat shading, dark green. Randomize heights, colors slightly. Place randomly in annulus radius 12..55 (clearing radius ~12-14 so trees surround clearing), reject positions too close to each other optionally. Count ~45 trees. Use InstancedMesh? For shadows and simplicity, regular meshes fine — 45 trees × 4 meshes = 180 draw calls, OK. Could merge but keep simple; or use groups. Should be fine performance-wise.

Also ensure trees cast shadows: point light shadow radius 30-ish covers some trees near fire; enable castShadow on trunks/foliage, receiveShadow ground.

Sparks:
- Particle system: THREE.Points with ~150 particles, each with life, velocity (up + random lateral), animate positions in JS updating buffer attribute, or use a shader with time-based motion. Simplest robust: CPU update of positions each frame; particle recycled when life ends. Color: bright orange/white, size attenuation true, additive blending, map: generate a small radial gradient canvas texture for soft round points. Sizes vary — PointsMaterial has single size; use ShaderMaterial? Keep simple: PointsMaterial with canvas-generated sprite texture, size ~0.15 with attenuation, additive, transparent, depthWrite false. Vary brightness via per-particle color attribute (vertexColors) — can fade color to black over life with additive blending (black = invisible with additive). That works nicely: additive blending, color fades from bright to black as life ends.

Fire flames also could get a glow: add a large sprite/plane with radial gradient additive texture at fire for glow, subtle.

Camera & controls:
- PerspectiveCamera fov 55, position (7, 3.5, 9) looking at fire (0,1,0).
- OrbitControls target (0, 1.2, 0), enableDamping, minDistance 4, maxDistance 40, maxPolarAngle: Math.PI/2 - 0.05 (stays above ground — actually need polar angle limited so camera doesn't go below horizon: maxPolarAngle slightly less than PI/2). Also could clamp target y. maxPolarAngle = 1.45 (~83°). Good.

Resize handler: update camera aspect, renderer size.

Tone mapping: ACESFilmic, exposure ~1.0-1.2. Night scene, exposure maybe 1.1.

Fog: FogExp2(0x070b14, 0.02)? With trees at radius up to 55 and camera at ~10, density 0.02 → at 60 distance, factor exp(-(0.02*60)^2)= exp(-1.44)=0.24 visible-ish. Maybe 0.022. Also stars at 400 would be fully fogged — but Points... fog affects points too. PointsMaterial has fog: true by default; set fog: false for stars and moon so they remain visible! Important. Stars material fog=false, moon material fog=false.

Actually with FogExp2 density 0.02, at distance 400: exp(-(8)^2) ≈ 0 — stars invisible. So yes, set fog:false on stars and moon materials.

Ground extent 120×120 with fog fading edges — good.

Fire animation details:
- Flame group: create ~7 flame cones. Each: ConeGeometry(r, h, 5) — with flatShading gives faceted. Materials: gradient from deep orange (0xff4500) outer to bright yellow (0xffdd66) inner. Use MeshBasicMaterial with additive blending? Additive on dark background looks glowy; but overlapping cones would brighten to white — could be fine (fire core white-hot). Alternative: normal blending with emissive standard materials. I'll use MeshBasicMaterial (unlit, always bright) with normal blending, colors: outer 0xff5a1f, mid 0xffa62b, inner 0xffe08a. Overlap fine.

Animate each flame: 
- scale.y = base * (1 + 0.25*sin(t*freq + phase))
- scale.x/z = base * (1 + 0.15*sin(t*freq2 + phase2))
- position.y = baseY + 0.1*sin(...)
- rotation.y += speed
Also a slight rotation.z wobble. Also to get "licking" motion, could animate geometry vertex offsets — skip.

Extra: embers glow at fire base: small emissive coals (icosahedrons) with orange emissive that pulse.

Point light flicker: intensity = base * (0.85 + 0.15 * (noise)), use sum of sines: I = 42 * (0.9 + 0.1*sin(t*11)+0.06*sin(t*23+1.7)+...) keep positive. Also light position jitter: y = 1.4 + 0.15*sin..., x/z small jitter — moves shadows nicely.

Also color flicker slight: lerp between 0xff9040 and 0xffb060? Keep constant color, intensity flicker.

Shadow setup: renderer.shadowMap.enabled = true; type PCFSoftShadowMap. PointLight.castShadow = true; light.shadow.mapSize.set(1024,1024); light.shadow.camera.near=0.5; far=40; bias = -0.005? Point light shadow bias: use -0.005 or 0. Try light.shadow.bias = -0.01; normalBias maybe not needed for point lights? shadow.normalBias supported. Keep bias small negative.

Moon directional light: DirectionalLight(0x8899ff? bluish 0x6677cc, intensity ~0.35) from moon direction, castShadow false (perf). Hemisphere light for ambient.

Also add subtle blue-ish fill so night scene not pitch black away from fire: hemi intensity 0.25.

Fireflies? Optional nice touch: a few small glowing particles drifting in clearing — adds life. Cheap to add: ~12 points with additive, slow sine motion. Not required but adds charm. Keep count small. I'll add them — improves demo quality.

UI overlay: Title "Night Camp — Forest Clearing" small, plus hint "Drag to orbit · Scroll to zoom" that fades after few seconds, and FPS? Not needed. A small info panel bottom-left with credits style. Keep minimal and elegant, dark translucent. Also note screenshot at 3s: everything visible immediately — good.

Performance: pixelRatio capped at 2.

Let me also consider "flat" low-poly look: use flatShading on all meshes; use MeshStandardMaterial with roughness ~0.9, metalness 0.

Ground color: dark forest floor #2a3324? Night: much darker, but firelight will illuminate. Base color like 0x24301f... With tone mapping and dim ambient, ground away from fire will be dark blue-green. Fine — moonlight 0.25 intensity gives shape. Add slight vertex color variation? Keep simple.

Now details of the flame animation to look good. Let me design flame structure:

fire at origin, base y=0.15 (logs). Flames:
- flameOuter: ConeGeometry(0.85, 2.2, 6, 1) — hexagonal cone, open ended? Cone geometry includes base. flatShading. Material MeshBasicMaterial color 0xff4a10, maybe slight transparent 0.9. Position y = 1.1 (center of cone: cone origin at center, so position y = h/2 + base).
- flameMid: Cone(0.6, 1.7, 5) color 0xff9a2e, y=0.9
- flameInner: Cone(0.34, 1.2, 5) color 0xffd873, y=0.7
Plus 2-3 small side flames (licks): Cone(0.25, 0.9, 4) positioned offset from center with own phase, tilt outward slightly.

Animation per frame:
t = clock elapsed.
For each flame store params: {mesh, baseH, phase, speed, baseR, y}
scale.y = 1 + 0.28*sin(t*speed + phase) + 0.12*sin(t*speed*2.7 + phase*2)
scale.x = scale.z = 1 + 0.12*sin(t*speed*1.3 + phase)
position.y = baseY + 0.08*sin(t*speed + phase)
rotation.y = t * rotSpeed + phase
rotation.z = 0.06*sin(t*speed*0.9 + phase)

For licks: also x/z position small circular motion: x = offsetX + 0.05*sin(t*sp+ph).

Also maybe animate cone geometry vertices for billowing: Instead skip — scale/rotate adequate.

Glow sprite at fire: canvas radial gradient texture (orange center → transparent), SpriteMaterial additive, opacity 0.5, scale ~6, position y=1. Flicker opacity with light. Nice cheap glow.

Also ground light circle: the point light handles it.

Sparks: 
count = 140. Each particle: life random 0..maxLife, position start near (0,0.8,0)+jitter, velocity: up 1.2-2.4 + lateral ±0.4, plus wind drift. Update: pos += vel*dt; vel.y += slight buoyancy? sparks decelerate and fade: vel *= (1 - 0.4*dt) drag; life -= dt; when life<0 respawn at fire. Color: start 0xffc864 bright → fade to red/black: color lerp toward 0 as life fraction. Set attributes each frame (position + color). Size: PointsMaterial size 0.12, sizeAttenuation true, additive, depthWrite false, transparent, map = spark texture (radial small). Use vertexColors: true.

Colors array update: c = lerp between bright orange (1, 0.7, 0.3)*intensity and black based on life fraction with ease.

Star twinkle: two Points groups; opacity = 0.7 + 0.3*sin(t*0.7 + offset) for group A, opposite for B. Fine.

Moon: position direction e.g., (-60, 70, -90) normalized*350 for sky sphere placement; actual moon mesh at (-0.5, 0.75, -0.7)*280 → visible above tree line in that direction. Radius ~14. Also crescent? Full sphere with emissive-ish basic color 0xdde6ff, plus subtle glow sprite behind it (additive, scale 60, opacity 0.25). Directional light from that direction pointing to origin.

Star positions: on sphere radius 380, y > -30 (above horizon-ish), random. Also moon area: fewer stars behind moon glow, whatever.

Fireflies: 14 points, positions around clearing radius 3..10, y 0.4..2, drift with sin/cos per particle with unique params, color greenish-yellow 0xb8ff7a additive, size 0.18 attenuated, opacity pulse per... global vertex colors static; animate positions only, and maybe blink via color update. Simple: update colors with per-particle sine blink. Small code.

Trees placement: clearing radius R_clear = 13 (open area), trees between 14 and 52, count 48. Random angle, random radius sqrt-distributed for uniform density: r = sqrt(lerp(14², 52², rand)). Also avoid camera? Camera orbits anyway. Also keep min distance between trees ~2.2 to avoid overlap heavily — do simple rejection: keep list, if any existing tree within 2.0, retry (max attempts). 

Tree construction function: 
- trunk: CylinderGeometry(0.22, 0.32, h_trunk=1.6*scale, 5) color 0x4a3325, position y = h_trunk/2.
- foliage: 3 cones: radius r1=1.5*s, heights stacked with decreasing radius. y positions: trunkTop - overlap. Cone segments 6, flatShading, color varies: pick from palette [0x1d3a2a, 0x22452e, 0x2a4f33, 0x1a3526] slightly random hue. Also snow? No.
- castShadow true, receiveShadow true.
- Slight random rotation.y and tilt (rotation.z small ±0.05).
- Scale variance 0.8..1.6.

Also far trees might be outside shadow range — fine.

Rocks: function makeRock(radius): IcosahedronGeometry(radius, 0), displace vertices randomly ±25% radially (need to modify position attribute, then computeVertexNormals not needed for flatShading? flatShading uses derivatives so normals attribute unused? Actually FLAT_SHADED uses derivatives, so normals not needed. But MeshStandardMaterial still requires normals? With flatShading true, the shader computes normal from derivatives; normals attribute not required but three may still use normal attribute for other things... Safe: after displacement call geometry.computeVertexNormals(). Icosahedron non-indexed? IcosahedronGeometry is indexed? In three, IcosahedronGeometry extends PolyhedronGeometry which is non-indexed (positions duplicated). detail 0 has 20 faces, 60 vertices, no index. Displacing shared-face vertices separately could crack edges — but since vertices are duplicated per face, random displacement of same "logical" vertex differs → cracks. To avoid cracks, displace deterministically based on position hash so duplicates get same offset: offset = f(x,y,z) deterministic from vertex position. E.g., n = sin(x*12.9 + y*78.2 + z*37.7)*43758... use pseudo: d = 0.75 + 0.5 * fract(sin(dot(pos, vec(12.9,78.2,37.7)))*43758.5453). Multiply vertex by d. Deterministic per position → no cracks. 

Rocks around fire ring: 8-9 rocks radius ~1.3-1.5 from center, small (0.22-0.35), sitting on ground y ~ 0.1. Scattered rocks: 8 more radius 3..11, bigger 0.4-0.9, partially sunk (y = r*0.4).

Logs: 
- Firewood teepee: 6 logs (CylinderGeometry(0.09, 0.11, 1.6, 5)) leaning into center: position on circle radius 0.55 at angle a, tilt toward center so top ends meet-ish above fire: rotate so axis points to center top. Compute: log at angle a, base at (cos a * 0.55, 0.1, sin a * 0.55), tilt angle ~0.5 rad inward. Set rotation: rotate around axis perpendicular. Easiest: create log mesh, then log.position.set(...), then use lookAt? Use group per log: pivot at base, rotate pivot.x by tilt after rotating group.y = a. Let me do: logMesh aligned along Y; group at base position; group.rotation.y = a; then rotate log about local X by -tilt (toward center means top leans toward origin: if log positioned at +x side (after rotation.y=a, local +x is radial outward), leaning inward = rotate around local z or x... Simpler: place group at origin with rotation.y = a, put log as child at local position (0.55, 0.8, 0) with rotation.z = -tiltTowardCenter? Let's think: log along Y. Rotating around Z by -tilt tilts top toward +x. If log's local +x after group rotation.y = a points radially outward (away from center), we want top toward center = -x direction → rotation.z = +tilt. Hmm: rotation about z by angle θ rotates +Y toward -X for positive θ? Rotation about Z: x' = x cosθ - y sinθ; point (0,1,0) → (-sinθ, cosθ, 0). So positive θ moves top toward -X. So with group rotation.y=a, local -X points toward center if local +X points outward (check: group rotation y=a rotates local axes; local +X in world = (cos a, 0, ... ) hmm with rotation.y = a, local +X maps to world (cos a, 0, -sin a)... if we place child at local (0.55, y, 0) it lands at world (0.55 cos a, y, -0.55 sin a). Radial outward from origin → yes local +X is outward. So top toward center = toward -X local = rotation.z = +tilt (θ>0 → -X). Wait above: (0,1,0) with θ>0 → (-sinθ, cosθ): top moves toward -X → toward center. Good: rotation.z = tilt where tilt ≈ 0.35–0.5 varies.
- Plus 2 fallen logs on ground elsewhere: horizontal cylinders radius 0.18 length 2.5, lying with random rotation, plus maybe one split. Place at radius ~4-6, random.

Campfire glow coals: center pile: few small dark-orange icosahedrons at center under flames, MeshStandardMaterial emissive 0xff3300, emissiveIntensity animate 0.6+0.3 sin.

Ground: PlaneGeometry(140, 140, 40, 40)? 41² verts fine. Displace: for each vertex, r = sqrt(x²+z²); if r > 8: h = noise * 0.55 * smoothstep(8, 16, r)... plus gentle rise with distance (bowl to hide horizon): h += (max(0, r-30))*0.03? Slight rise far away helps horizon. Use deterministic hash noise on grid coords. Also color variation: could add vertexColors for ground: base 0x232d1c with patches of 0x2b2a1a dirt near center and darker far. Use vertexColors on standard material with color attribute: set material.color white and vertex colors carry the tint. Nice touch: dry dirt circle near fire (radius<2.5 brownish), grass green beyond, random slight variation.

Compute normals: geometry.computeVertexNormals() — with flatShading derivative normals anyway; but standard material needs normals for... with flatShading:true normals unused (derivatives). OK but call computeVertexNormals anyway safe.

Fog color 0x0a0f1a? Should match sky background. Sky background: scene.background = new THREE.Color(0x070d18). Fog color slightly lighter 0x070d18 same, FogExp2 0x016? Let me set FogExp2(0x070d18, 0.02). Hmm at distance 60 (far trees): factor exp(-(1.2)^2)= exp(-1.44)=0.237 → far trees 76% fogged — good fade. At 30: exp(-0.36)=0.70 visible. Good.

But stars: fog:false. Moon: fog:false.

Ground plane 140 wide → edges at distance ~70-99 diag: fully fogged → blends into background. 

Now check "renderer fills window and handles resizing": standard.

Camera limits: OrbitControls maxPolarAngle = Math.PI/2 - 0.06 ensures camera above ground plane through target; but ground has bumps 0.55 — camera height could clip into a bump at far distance? minDistance 4, maxDistance 40; at max distance and polar ~84°, camera y = target.y + 40*cos(84°) = 1.2 + 4.2 = 5.4 > bumps. Fine. Also prevent panning below ground: controls.enablePan = true but clamp target? Simplify: controls.enablePan = false (or screen-space pan limited). I'll disable pan to keep target fixed — clean. Or allow pan but clamp target.y via change listener. Simplest: enablePan = false. Requirements only mention limits so camera stays above ground — polar limit + min/max distance suffice.

Damping true → need controls.update() each frame. Yes.

Also handle: hide "hint" after first interaction or timeout.

Vignette overlay: CSS radial-gradient overlay for cinematic feel — pointer-events none. Nice.

Title overlay: top-left "Forest Clearing — Night Camp" with subtle styling. Bottom-right hint "drag to look around · scroll to zoom". Fades to lower opacity.

Let me now write the code carefully.

Structure:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>...</title>
<style> ... </style>
</head>
<body>
<div id="ui"> ... </div>
<script type="importmap">...</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
...
</script>
</body>
</html>
```

Check import map URL exactly as given.

Renderer:
```js
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
document.body.appendChild(renderer.domElement);
```

Scene, camera:
```js
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x060b14);
scene.fog = new THREE.FogExp2(0x060b14, 0.02);
const camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(8.5, 3.4, 10.5);
```

Controls:
```js
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.4, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 4;
controls.maxDistance = 42;
controls.maxPolarAngle = Math.PI/2 - 0.07;
controls.minPolarAngle = 0.15;
controls.enablePan = false;
controls.update();
```

Lights:
```js
const hemi = new THREE.HemisphereLight(0x223148? , 0x0a0f08, 0.35);
```
Hmm hemisphere sky color bluish 0x2a3a5f, ground 0x11150d, intensity 0.5? With physical lights... hemisphere intensity unitless-ish. Let me pick hemi intensity 0.55? Might wash night. Try 0.4.

Moon directional: color 0x7f96d8? intensity 0.5. From direction of moon: dirLight.position.set(-40, 60, -55).

Fire point light:
```js
const fireLight = new THREE.PointLight(0xff8a3d, 60, 45, 2);
fireLight.position.set(0, 1.6, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024,1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 45;
fireLight.shadow.bias = -0.01;
```
Intensity: physical mode → candela. At distance 3 (fire to nearby ground), illuminance = 60/9 ≈ 6.7 lux-ish scaled... With ACES exposure, hmm hard to predict. Let me reason: In three r170 default (physically correct lighting), PointLight intensity 60 with decay 2 at radius 2: 60/4 = 15 → bright. Standard: rendered color = irradiance; typical scenes need ~1-5 for lit surfaces with exposure 1. Ground albedo ~0.2 dark green. irradiance 15 * albedo/π ~ 15*0.2/3.14 ≈ 0.95 → decent. At radius 6 (nearby trees): 60/36 = 1.67 * 0.2/π = 0.1 dim — realistic falloff, fine with moonlight fill. Let me use intensity 55 and see; can't test, choose 60. Also flicker ±20%.

Maybe also add second dim point light inside fire for coals? Not needed.

Ground:

```js
function groundHeight(x, z) { ... } // deterministic hash noise
```

Hash: 
```js
function hash(x, y) {
  const s = Math.sin(x*127.1 + y*311.7) * 43758.5453;
  return s - Math.floor(s);
}
function noise2(x, y) {
  const xi = Math.floor(x), yi = Math.floor(y);
  const xf = x - xi, yf = y - yi;
  const u = xf*xf*(3-2*xf), v = yf*yf*(3-2*yf);
  const a = hash(xi, yi), b = hash(xi+1, yi), c = hash(xi, yi+1), d = hash(xi+1, yi+1);
  return a + (b-a)*u + (c-a)*v + (a-b-c+d)*u*v;
}
```

groundHeight(x,z): 
```js
const r = Math.hypot(x, z);
const n = noise2(x*0.15+10, z*0.15+10)*0.9 + noise2(x*0.4, z*0.4)*0.25;
let h = n * Math.min(1, Math.max(0, (r-6)/10)); // flat within 6, ramp to 16
h += Math.max(0, r-34)*0.05; // rise far
return h;
```

Ground mesh: PlaneGeometry(150,150, 46,46), rotateX(-PI/2), displace each vertex y = groundHeight(x,z). Vertex colors: 
- base grass color varies with noise: mix of 0x1c2b18? and 0x2a3a1e... Let me define: grass = Color(0x22301d) modulated ±10%, dirt near fire: color 0x3a2c1e within r<2.2 fading to grass by r<3.2. Slight darkening with distance.
Set geometry attribute 'color'. Material: MeshStandardMaterial({vertexColors:true, roughness:1, flatShading:true}).

Note PlaneGeometry rotated: after rotateX(-PI/2), position attribute: x stays, y becomes 0, z = -original y. Then loop set posArr[i+1] = groundHeight(posArr[i], posArr[i+2]). Good.

receiveShadow = true.

Trees: build via function createTree(scale). Use geometries created once and reuse? Each tree unique scale via mesh.scale. Use shared geometries for trunk and 3 cones → materials per color variant (4 palette colors). Then per tree: group with trunk mesh + 3 cone meshes, random rotation/scale. Shadows: castShadow on all.

Cone foliage: 
- cone1: ConeGeometry(1.35, 2.1, 6) at y 2.2
- cone2: ConeGeometry(1.05, 1.8, 6) at y 3.4
- cone3: ConeGeometry(0.72, 1.5, 6) at y 4.5
trunk: CylinderGeometry(0.16, 0.24, 1.8, 5) at y 0.9.
Total height ~5.2 * scale(0.8..1.5) → 4-8 units tall. Fine relative to scene.

Actually foliage base radius 1.35 close to trunk. ok.

Place trees, count target 42:
```js
const treePos = [];
let attempts = 0;
while (treePos.length < 42 && attempts < 2000) {
  attempts++;
  const a = Math.random()*Math.PI*2;
  const r = 14 + Math.sqrt(Math.random())*(52-14); // hmm uniform in area between 14..52: r = sqrt(14² + rand*(52²-14²))
  ...
}
```
Uniform annulus: r = Math.sqrt(lerp(14², 52², rand)). Then check distance to others > 2.4, also not within... also avoid the direction where camera starts? Camera orbits so no issue; trees at r≥14 while camera maxDistance 42 at height... camera can be outside trees, fine.

Also check tree not too close to moon path? irrelevant.

Tree y position: place at groundHeight(x,z) - 0.15 (sink slightly). Ground displacement uses same function → trees sit on terrain.

Rocks placement similar with ground height.

Rocks material: color 0x4b4f55? dark gray-blue with flatShading, roughness 1. Slight color variance per rock: material clone with color multiply scalar.

Logs (fallen): CylinderGeometry(0.16, 0.2, 2.4, 6), color 0x503728, some with ends? Add end caps darker? Cylinder has caps by default (openEnded false). Position lying: rotate z = PI/2 then random y rotation; y = groundHeight+0.16. Place 3-4 around clearing radius 3..10 random angles, plus firewood teepee logs.

Firewood teepee (6 logs):
```js
for (let i=0;i<6;i++){
  const a = i/6*Math.PI*2 + rand*0.4;
  const g = new THREE.Group();
  g.rotation.y = -a; // so local +X world position = angle a
  const log = new THREE.Mesh(logGeo, logMat);
  log.position.set(0.5, 0.75, 0);
  log.rotation.z = 0.42 + rand*0.1; // lean toward center
  g.add(log);
```
Wait rotation.y = -a vs a sign: with g.rotation.y = θ, local +X maps to world (cosθ, 0, -sinθ). To place log at world angle a (i.e., world (cos a, 0, sin a)*0.5), need cosθ = cos a, -sinθ = sin a → θ = -a. OK g.rotation.y = -a works, or just not care about exact angles since uniform. Simpler: g.rotation.y = a (angles just permuted). Fine, any spread works.

Also log length 1.6 leaning: top reaches near y = 0.75 + cos(0.45)*0.8 ≈ 1.47, x = 0.5 - sin(0.45)*0.8 ≈ 0.15 → tops converge near center at y~1.5. Good, flames emanate from within.

Also add charred inner ends? skip.

Coals: 5 small icosahedrons radius 0.12-0.2 at center y=0.08, material MeshStandardMaterial({color:0x1a0d06, emissive:0xff4400, emissiveIntensity:1.2}) animate emissiveIntensity.

Flames: as planned. Add all to fireGroup.

Flame material transparent? Normal blending, opacity 1, colors bright. Additive would make them translucent-glow; I'll use additive for outer glow flames? Let me think final look: MeshBasicMaterial with color 0xff6a1a on dark bg looks solid flat orange — decent low-poly. With additive blending and slight transparency, overlapping flames → brighter yellow core, looks fiery. I'll use:
- outer: color 0xff5a00, transparent, opacity 0.85, blending additive, depthWrite false
- mid: 0xffa629, additive, 0.9
- inner: 0xffe9a0, additive 0.95
- licks: 0xff7a1a additive 0.8

Additive + depthWrite false could cause sorting issues with sparks (also additive) — fine, both glowy. But flames drawn after trees? Transparent objects sorted by distance — fine.

depthTest true default ok. Since additive on dark scene, flames will look like glowing translucent shapes. 

Also add a "glow sprite" radial texture sprite scale 7 at (0,1.2,0), additive, opacity ~0.35 flickering.

Texture helper: canvas 128, radial gradient white→transparent; for spark use same texture.

```js
function makeGlowTexture(inner = 'rgba(255,255,255,1)') {
  const c = document.createElement('canvas'); c.width = c.height = 128;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(64,64,0,64,64,64);
  g.addColorStop(0, inner);
  g.addColorStop(0.25, 'rgba(255,255,255,0.6)');
  g.addColorStop(1, 'rgba(255,255,255,0)');
  ctx.fillStyle = g; ctx.fillRect(0,0,128,128);
  const tex = new THREE.CanvasTexture(c);
  return tex;
}
```

Stars:
```js
const starGeo = new THREE.BufferGeometry();
const N = 700; positions on sphere radius 420..460:
random direction with y = rand(0.05..1) weighted... generate: 
const v = new THREE.Vector3().randomDirection(); if (v.y < 0.05) retry-ish or just set y abs.
pos = v.multiplyScalar(400 + rand*60).
colors: white with slight blue/yellow tint variation, brightness varied.
material: PointsMaterial({size: 2.2, sizeAttenuation: false, vertexColors: true, transparent: true, opacity: 0.9, fog: false, depthWrite: false, blending: additive? })
```
sizeAttenuation false → size in pixels — good for stars. Additive blending nice. Twinkle: animate opacity of material globally subtle: 0.75+0.2*sin(t*0.5). Or two groups... simple global fine. Actually per-star twinkle nicer via shader; skip, global subtle.

Hmm size 2.2 px on high-DPR... fine.

Moon: 
```js
const moonDir = new THREE.Vector3(-0.45, 0.55, -0.7).normalize();
const moon = new THREE.Mesh(new THREE.SphereGeometry(13, 20, 14), new THREE.MeshBasicMaterial({color:0xdfe8ff, fog:false}));
moon.position.copy(moonDir).multiplyScalar(430);
```
With camera far 1000, ok. Add glow sprite behind: SpriteMaterial({map:glowTex, color:0x9db8ff, transparent, opacity 0.35, fog:false, depthWrite:false, blending additive}), scale 130, at moonDir*415 (slightly closer). Moon craters? Low-poly moon: use IcosahedronGeometry(13, 1) flat shaded with MeshStandardMaterial lit by... basic material ignores light; standard material would be dark (no light reaching? directional light reaches 430? dir light parallel yes but intensity applies anywhere). Directional moonlight could light the moon mesh — but it would look flat full-bright from that direction. Simpler: MeshBasicMaterial pale — fine as stylized. Add a slightly darker "shadow" crescent? skip. Maybe add few darker spots via vertex colors? skip — simple pale disc is fine at that distance, with glow halo it reads as moon.

Directional light from moonDir: position = moonDir*60, target origin (default). intensity 0.5, color 0x8fa5e8? Let me have 0x93a7ff*? color 0x8899dd intensity 0.5.

Sparks system:

```js
const SPARKS = 160;
positions Float32Array, colors Float32Array, per-particle: life, maxLife, vel Vector3 stored in arrays, seed offsets.
function resetSpark(i) {
  life[i] = maxLife[i] = 0.8 + Math.random()*1.4;
  pos: (rand-0.5)*0.5, 0.6+rand*0.6, (rand-0.5)*0.5
  vel: ((rand-0.5)*0.7, 1.6+rand*1.6, (rand-0.5)*0.7)
}
update(dt, t):
 for each: life -= dt; if life<=0 → reset with life=maxLife (continue).
 vel *= drag; vel.y += 0.25*dt? sparks accelerate upward? ember rise with slight buoyancy: vy += 0.4*dt minus drag; net drift up. Also wind: vel.x += sin(t*0.7)*0.04*dt.
 pos += vel*dt.
 fade = life/maxLife; brightness = smooth: b = fade<0.25? ... simple: intensity = Math.pow(fade, 0.7) maybe with flicker at start. color = (1.0, 0.55+0.3*fade, 0.2) * intensity*1.2... additive so black = invisible.
 sizes constant.
```
PointsMaterial({size: 0.09, sizeAttenuation: true, map: glowTex, vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false}).

Set positions attr usage: DynamicDrawUsage.

Also mark needsUpdate each frame.

Fireflies: similar small system, N=14, but stationary positions oscillating:
pos_i(t) = base + (sin(t*w1+φ)*0.8, sin(t*w2+φ)*0.5, cos(t*w3+φ)*0.8); blink color intensity = max(0, sin(t*bw+φ))^2 * ... use color *= (0.15 + 0.85*pulse). Points size 0.13 additive with glow texture. Nice.

Flicker light update:
```js
const flick = 0.82 + 0.13*Math.sin(t*11.3) + 0.07*Math.sin(t*29.7+1.3) + 0.05*Math.sin(t*7.1+2.1);
fireLight.intensity = 62 * flick;
fireLight.position.set(Math.sin(t*3.1)*0.12, 1.5 + Math.sin(t*9.3)*0.1, Math.cos(t*2.6)*0.12);
glowSprite.material.opacity = 0.3*flick + ...;
coal emissive intensity = 1.4*flick;
```

Flame animation per frame:
each flame f: 
```js
const s = 1 + 0.3*Math.sin(t*f.speed + f.phase) + 0.12*Math.sin(t*f.speed*2.13 + f.phase*1.7);
f.mesh.scale.set(1 + 0.12*Math.sin(t*f.speed*0.9+f.phase), s, 1 + 0.12*Math.cos(t*f.speed*1.1+f.phase));
f.mesh.position.y = f.baseY + 0.1*Math.sin(t*f.speed*0.8 + f.phase);
f.mesh.rotation.y = t*f.spin + f.phase;
f.mesh.rotation.x = 0.12*Math.sin(t*f.speed*0.7+f.phase);
f.mesh.rotation.z = 0.12*Math.cos(t*f.speed*0.6+f.phase);
```

Also scale overall flames with light flicker factor a bit: fireGroup.scale.set(k,k,k) where k = 0.95+0.08*flickish.

Handle visibility: all visible at load. 

Resize:
```js
addEventListener('resize', () => {
  camera.aspect = innerWidth/innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});
```

Animation loop with clock, delta clamp 0.05.

Also slow auto-rotate for idle? controls.autoRotate maybe — since no user interaction initially, gentle autoRotate = true, autoRotateSpeed = 0.4, and stop autoRotate on first pointerdown ('start' event of controls). That gives motion in the 3-second screenshot. Nice: controls.addEventListener('start', () => controls.autoRotate = false). Good idea — cinematic.

UI: 
```html
<div id="overlay">
  <h1>Campfire · Forest Clearing</h1>
  <p>Low-poly night scene — three.js</p>
  <div id="hint">Drag to orbit · Scroll to zoom</div>
</div>
```
Vignette div with radial gradient. Styling: absolute positioned, pointer-events none, font system-ui. Keep subtle.

Also maybe a stats-ish "42 trees · 160 sparks"? gimmick, skip.

Now, double-check potential pitfalls:

1. Import map script must be placed before module script. Yes.
2. OrbitControls import path 'three/addons/controls/OrbitControls.js' — matches map.
3. PointsMaterial fog default true — stars need fog:false. Moon basic mat fog:false. Sprite glow (fire) fog? Fire near camera, fog negligible; set fog false for fire glow to keep it punchy? FogExp2 at distance ~12: exp(-(0.02*12)^2)= exp(-0.0576)=0.944, fine either way. Set fog:false for the fire glow and moon glow sprites to be safe.
4. PointsMaterial with map + sizeAttenuation: OK. Need `transparent: true` and depthWrite false. Alpha test? additive doesn't need.
5. Shadow acne with point light on flat ground: set bias -0.008, normalBias 0.02? normalBias supported for point lights? shadow.normalBias works with all shadow types (added to shader). Use bias: -0.005, normalBias: 0.02. Fine.
6. Performance: point light shadow = 6 passes, each renders whole scene ~500k tris? Trees: 42 trees × (trunk 5×2 tris? cylinder 5 segments → 5 side quads = 10 tris + caps 10 tris = ~30 tris; cones 6 seg → 12 tris each +base? ConeGeometry(radius, h, 6) has base cap 6 tris + 12 side = 18 tris each; 3 cones = 54; total per tree ~84 tris × 42 = 3.5k tris. Ground 46×46×2 = 4.2k tris. Rocks ~20 icosahedrons 20 faces = 400. Trivial. 6 shadow passes fine.

7. tone mapping + additive blending interplay fine.

8. ACESFilmic darkens; exposure 1.15; ambient hemi 0.4 might be too dark/bright — choose values reasonable: hemi (0x33415f sky, 0x101510 ground, 0.5); moon dir 0x8899dd? Let me be careful: too much ambient ruins night mood; too little makes scene pitch black except fire. Aim: trees silhouettes slightly visible, ground faint blue. hemi intensity 0.45, dir intensity 0.5 bluish 0x8fa0d8? Colors: dir 0x93a5e0.

Hmm, physically-based: DirectionalLight intensity in lux? default 1. Fine ~0.5-0.8. I'll pick moonLight intensity 0.7 color 0x9fb4e8? Slight purple? Keep 0x93a7d8. And hemi 0.35.

Also the fire glow on ground: intensity 60 might overexpose near ground (distance 1.6 to ground below → 60/2.56 = 23 lux; albedo 0.15 → radiance 23*0.15/π ≈ 1.1 → with ACES ~0.8 white-ish, hot spot under fire, plausible). ok.

9. controls.maxPolarAngle = Math.PI/2 - 0.07 → ~86°: at min distance 4, camera y = 1.4 + 4*cos(86°)= 1.4+0.28 = 1.68 above ground plane; ground bumps near fire < 0.3; trees min r=14 while camera horizontal dist = 4*sin(86°)≈4 → within clearing, no tree collision. Good. At max distance 42: horizontal 41.8 > tree ring at 14 → camera may be inside/among trees (r up to 52): at radius 42, camera passes through tree positions! Camera could clip through trees at far zoom. Acceptable (user zoomed out intentionally), but trees at r=42 could intersect camera. minDistance larger? It's fine visually — passing through foliage briefly. Most demos allow it. Could set maxDistance 38 and trees from 14. Still overlap. It's fine.

10. Fog on trees: yes fades distant ones. Trees max r=52: fog factor exp(-(0.02* (52+10 avg camera dist))²)... distance from camera ~60: exp(-1.44) = 0.24 → 76% faded. Good.

11. Stars visible: material fog false. Background 0x060b14 nearly black; stars additive white → visible.

12. Moon glow sprite scale 130 at distance 430 → angular size ~ 130/430 ≈ 17° — big halo. opacity 0.3 additive → soft. ok maybe scale 90.

13. First paint: everything in scene immediately; autoRotate gives motion. Screenshot 3s: flames animated, light flickering, sparks rising. 

14. No console errors: careful with code. Use `const` etc. Check every API name for r170: 
- THREE.FogExp2 ✓, HemisphereLight ✓, PointLight(color,intensity,distance,decay) ✓, shadow.mapSize.set ✓ (Vector2), PCFSoftShadowMap ✓, ACESFilmicToneMapping ✓, CanvasTexture ✓, colorSpace: for CanvasTexture used as sprite map, set tex.colorSpace = THREE.SRGBColorSpace for correct color. Additive glow — fine either.
- BufferGeometry.setAttribute ✓, setUsage(THREE.DynamicDrawUsage) ✓.
- SphereGeometry ✓, IcosahedronGeometry ✓, ConeGeometry ✓, CylinderGeometry ✓, PlaneGeometry ✓.
- geometry.attributes.position.array manipulation ✓, computeVertexNormals ✓.
- Points ✓, Sprite/SpriteMaterial ✓ (SpriteMaterial has fog property? SpriteMaterial supports fog — yes it has fog define... Actually SpriteMaterial fog: Material.fog default true, sprite shader supports fog. Setting fog:false works).
- PointsMaterial fog option ✓ (Material base).
- controls.autoRotate ✓ with update in loop ✓ (we call controls.update(dt?) — update() optionally takes delta for autoRotate; call controls.update() each frame fine).
- Vector3.randomDirection exists r13x+ ✓.

15. Also `renderer.outputColorSpace` default SRGB ✓.

16. `MeshStandardMaterial flatShading` ✓.

17. Ground vertexColors + flatShading: colors interpolate per vertex, flat normals — fine.

18. Moon mesh MeshBasicMaterial fog:false ✓.

19. Fireflies glow texture reuse.

Let me also add subtle blue rim light? skip.

Edge case: `sizeAttenuation: false` stars size in pixels — with pixelRatio 2, size 2.2 → drawn ~1-2px. ok maybe size 2.5.

Sparks: size 0.1 with attenuation at ~12 distance: pointSize = size * (canvasHeight/2) / dist? Formula: gl_PointSize = size * ( scale / -mvPosition.z ) where scale = height*0.5 (in pixels? uses projection). At dist 12, size 0.1 → 0.1*(400)/12 ≈ 3.3px × pixelRatio → visible. Good.

Fireflies size 0.14 → ~4px at 12. ok.

Now the hint fade: 
```css
#hint { position:absolute; bottom:18px; right:20px; ... opacity transition 1s; }
```
JS: after 8s or on controls 'start', add class fading opacity 0.35? Keep simple: reduce to 0 after first interaction or 9s.

Also title panel top-left.

Write flame params concretely:

```js
const flameDefs = [
  // [radius, height, segs, y, color, opacity, speed, phase, spin]
];
const flames = [];
function addFlame(r, h, seg, y, color, opacity, speed, spin) {
  const geo = new THREE.ConeGeometry(r, h, seg);
  const mat = new THREE.MeshBasicMaterial({color, transparent:true, opacity, blending:THREE.AdditiveBlending, depthWrite:false, fog:false});
  const m = new THREE.Mesh(geo, mat);
  m.position.y = y;
  fireGroup.add(m);
  flames.push({m, h, y, speed: speed, phase: Math.random()*Math.PI*2, spin});
}
addFlame(0.75, 2.3, 6, 1.15, 0xff4d00, 0.55, 5.2, 0.6);
addFlame(0.5, 1.9, 5, 1.0, 0xff8c1a, 0.7, 6.1, -0.8);
addFlame(0.3, 1.5, 5, 0.85, 0xffc23a, 0.85, 6.8, 1.1);
addFlame(0.16, 1.1, 4, 0.7, 0xffe89e, 0.95, 7.4, 1.6);
// licks
for (let i=0;i<4;i++){
  addFlame(0.16+Math.random()*0.06, 0.7+Math.random()*0.5, 4, 0.55, 0xff7a1a, 0.7, 7+Math.random()*3, (Math.random()-0.5)*4);
  const f = flames[flames.length-1];
  f.offset = new THREE.Vector2... 
}
```
For licks store angle offset & radius: f.r = 0.35+Math.random()*0.3, f.ang = random*2π; in update: m.position.x = Math.cos(f.ang + t*f.spin*0.3)*f.r etc. Simpler: set static x,z offset for licks and animate only scale/rot like others, plus tiny sway. I'll include offset in defs.

Let me restructure flame entries as objects with fields: mesh, baseY, baseScale, speed, phase, spin, offset(THREE.Vector3), sway.

Update code:
```js
for (const f of flames) {
  const s1 = Math.sin(t*f.speed + f.phase);
  const s2 = Math.sin(t*f.speed*2.13 + f.phase*1.7);
  f.mesh.scale.y = f.k*(1 + 0.32*s1 + 0.12*s2);
  const w = 1 + 0.14*Math.sin(t*f.speed*0.93 + f.phase) ;
  f.mesh.scale.x = f.k*w; f.mesh.scale.z = f.k*w;
  f.mesh.position.set(
    f.offset.x + 0.05*Math.sin(t*f.speed*0.7+f.phase),
    f.baseY + f.h*0.5*(f.mesh.scale.y-1)*0.6 + 0.06*Math.sin(t*f.speed*0.8+f.phase),
    f.offset.z + 0.05*Math.cos(t*f.speed*0.6+f.phase)
  );
  f.mesh.rotation.y = f.phase + t*f.spin;
  f.mesh.rotation.x = 0.14*Math.sin(t*f.speed*0.5 + f.phase);
  f.mesh.rotation.z = 0.14*Math.cos(t*f.speed*0.43 + f.phase);
}
```
Where f.k relates flame height with global flicker? Add global: fireGroup.scale.set(g,g,g) with g = 0.92+0.1*flick? That scales positions too — ok since centered at origin base.

Hmm position.y of cone: cone origin at its center; base should sit around logs. baseY values above already chosen as center positions (1.15 for h=2.3 → base at 0). Good, they emerge from logs.

Colors: with additive blending, overlapping 4 cones sum toward white-hot core — good.

Sparks code:

```js
const SPARK_N = 160;
const sparkGeo = new THREE.BufferGeometry();
const sPos = new Float32Array(SPARK_N*3);
const sCol = new Float32Array(SPARK_N*3);
const sVel = new Float32Array(SPARK_N*3);
const sLife = new Float32Array(SPARK_N);
const sMax = new Float32Array(SPARK_N);
function spawnSpark(i, randomAge){
  sLife[i] = sMax[i] = 0.7 + Math.random()*1.6;
  if (randomAge) sLife[i] = Math.random()*sMax[i];
  sPos[i*3] = (Math.random()-0.5)*0.45;
  sPos[i*3+1] = 0.55 + Math.random()*0.7;
  sPos[i*3+2] = (Math.random()-0.5)*0.45;
  sVel[i*3] = (Math.random()-0.5)*0.9;
  sVel[i*3+1] = 1.3 + Math.random()*1.8;
  sVel[i*3+2] = (Math.random()-0.5)*0.9;
}
for (let i=0;i<SPARK_N;i++) spawnSpark(i,true);
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sPos,3).setUsage(THREE.DynamicDrawUsage));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sCol,3).setUsage(THREE.DynamicDrawUsage));
const sparks = new THREE.Points(sparkGeo, new THREE.PointsMaterial({size:0.11, map:sparkTex, transparent:true, depthWrite:false, blending:THREE.AdditiveBlending, vertexColors:true, sizeAttenuation:true}));
sparks.frustumCulled = false;
```
Note: PointsMaterial `map` + `vertexColors` fine. Also material.fog default true; sparks near fire, fine, or set fog false. Set fog:false to keep bright.

Update:
```js
function updateSparks(dt, t){
  for (let i=0;i<SPARK_N;i++){
    sLife[i] -= dt;
    if (sLife[i] <= 0) { spawnSpark(i,false); }
    const i3 = i*3;
    const drag = Math.exp(-1.1*dt);
    sVel[i3] = sVel[i3]*drag + Math.sin(t*0.8 + i)*0.05*dt;
    sVel[i3+1] = sVel[i3+1]*drag + 0.35*dt;
    sVel[i3+2] = sVel[i3+2]*drag + Math.cos(t*0.7 + i*1.3)*0.05*dt;
    sPos[i3] += sVel[i3]*dt;
    sPos[i3+1] += sVel[i3+1]*dt;
    sPos[i3+2] += sVel[i3+2]*dt;
    const fade = Math.max(0, sLife[i]/sMax[i]);
    const b = Math.pow(fade, 0.9);
    // hot orange-white to red
    sCol[i3] = 1.0*b;
    sCol[i3+1] = (0.35 + 0.45*fade)*b;
    sCol[i3+2] = 0.12*b;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;
}
```
Colors up to ~ (1, 0.8, 0.12) bright yellow-orange. Good.

Fireflies:

```js
const FF_N = 16;
positions base: radius 3.5..11 random angle, y 0.5..2.4, params: phase, freq, radius of oscillation.
update: for each: x = bx + sin(t*w1+p)*1.2; y = by + sin(t*w2+p*2)*0.6; z...
brightness pulse = 0.5+0.5*sin(t*wb + p) → pow 3 → blinky: b = Math.pow(max(0, Math.sin(t*wb+p)), 4) *? Actually sin goes negative → 0. use smooth: q = Math.sin(t*wb+p); b = Math.pow(Math.max(0.05, q), 3). Colors set as greenish (0.6*b?, 1*b, 0.35*b) → (0.7b, 1b, 0.3b) times 1.
```
Set colors each frame; N small.

Fireflies also PointsMaterial size 0.16 map glowTex additive vertexColors fog false depthWrite false.

Stumps or grass tufts? skip; maybe add a few grass blades? skip — enough content.

Also maybe add a "log bench" (fallen log) — included as scattered logs.

Stars generation:

```js
const STAR_N = 900;
const sp = new Float32Array(STAR_N*3), sc = new Float32Array(STAR_N*3);
for i: 
  const v = new THREE.Vector3().randomDirection();
  if (v.y < 0.02) { i--; continue; } → careful loop; alternative: v.y = Math.abs(v.y)+0.02 then normalize.
  const r = 400 + Math.random()*80;
  pos = v*r;
  brightness = 0.4 + Math.random()*0.6? with occasional bright: if rand<0.06 → 1.0.
  color tint: mostly white-blue: pick temp: c = Color().setHSL(0.6+ (rand-0.5)*0.1, rand<0.3? 0.3..: , brightness)
```
Simpler: 
```js
const b = Math.pow(Math.random(), 1.8)*0.9 + 0.1;
const tint = Math.random();
sc[i3] = b*(tint<0.15? 1.0 : 0.85+... )
```
Just do: base white (1,1,1) scaled by b; slight blue: if rand<0.25 → (0.75, 0.85, 1)*b; if rand<0.1 warm (1,0.9,0.75)*b. fine.

Star material: PointsMaterial({size: 2.4, sizeAttenuation:false, vertexColors:true, transparent:true, opacity:0.95, depthWrite:false, fog:false, blending:AdditiveBlending}).

Twinkle: starMat.opacity = 0.7 + 0.25*Math.sin(t*0.6)? Subtle global. Or better skip changing (additive flicker of whole sky slightly odd). Use two star groups with phase-opposed opacity: groupA 700 stars opacity .9+... meh. I'll do subtle: opacity = 0.8 + 0.15*sin(t*0.9). OK.

Camera far 1000 covers stars at 480.

Check: stars at radius 400-480, moon at 430 — moon in front of some stars, fine.

Sky dome alternative: background color only. Slight gradient? Could add large sphere with gradient shader... skip, vignette CSS handles mood.

Now also consider: `scene.background` dark; fog same color; ground edges fade — cohesive.

One more required check: "At least 30 low-poly trees placed randomly around the clearing (not on the fire)" → 42 trees ≥30 ✓, min radius 14 ✓.

"Ground with a few low-poly rocks and logs around a campfire in the center" ✓ rocks ring + scattered rocks + logs.

"animated flames and warm point light that flickers and casts shadows" ✓.

"Sparks rising from fire as simple particle system" ✓.

"Dark night sky with stars and moon, plus fog that fades distant trees" ✓.

"OrbitControls with limits so camera stays above ground" ✓ maxPolarAngle + min/max distance.

"Renderer fills window and handles resizing" ✓.

Also the meta-instructions: no console errors, something visible at 3s ✓ (scene renders immediately), self-playing demo for games — this is a scene with autoRotate which serves similar purpose; hint text shown. Keyboard: not used. Good.

Now write CSS: body margin 0, overflow hidden, background #060b14. Canvas block. Overlay UI with pointer-events none. Title serif? Use system fonts. Colors pale #cfd8ea.

Maybe add subtle letterboxing? no.

Let me also add a loading consideration: module scripts from CDN — fine.

Potential issue: importmap JSON must be exact. Copy given string.

Write the final code now, carefully, mentally linting.

Extra check on ConeGeometry flatShading: MeshBasicMaterial ignores normals; flat look of cone comes from geometry facets shading — but MeshBasicMaterial = no shading, solid color; facets only visible via silhouette/additive overlaps. So low-poly facet look lost on flames — but flames as glowing shapes don't need facet shading. However for stylization, using MeshBasicMaterial means uniform color cones — looks like flat glowing triangles cone. Acceptable (they're emissive fire). To add slight facet variation, could use MeshStandardMaterial with emissive and dark color: fire would then be lit by point light at same location (fire light inside cone) creating shading gradients. Hmm. Basic + additive is standard for fire glow. Keep.

Alternatively use flat-shaded emissive standard material: color 0x000000, emissive orange, emissiveIntensity 1.5 → unlit look too (emissive uniform). Same as basic. Keep basic+additive.

One more consideration: flames with depthWrite false and additive — they render after opaque; logs inside visible through flames — ok, fire is translucent glow, good.

Fire glow sprite may render over logs additively — fine.

Shadow: pointLight at y≈1.5 inside flame cones; the flame meshes cast shadows? They're transparent additive; castShadow default false → good, flames don't cast.

Logs cast shadows onto ground → dancing shadows as light jitters. 

Tree shadows: shadow camera far 45 covers trees to r~43 from light... light near fire; trees at 14-45: within far. Their shadows will be cast long — dramatic. PointLight shadow distance property: light.shadow.camera.far = 45; distance param of PointLight is 45 → trees beyond 45 unlit anyway.

Hemi light doesn't cast shadows — fine.

Also should trees far away be lit by fire? distance cutoff 45 → beyond dark. With moonlight + hemi they're visible. Good.

Performance of 6-cube shadow with 42 trees each frame: fine.

Now UI text content:

Title: "NIGHT CAMP" / subtitle "A low-poly forest clearing · three.js". Bottom hint: "Drag to orbit · Scroll to zoom · auto-rotating". Add small stats line? Also add "fire: logs · stones" nah.

Add top-right badge "three.js r170" maybe. Keep minimal: title block top-left, hint bottom-right.

CSS:

```css
:root { color-scheme: dark; }
* { margin:0; padding:0; box-sizing:border-box; }
html,body { height:100%; overflow:hidden; background:#060b14; }
#app canvas { display:block; }
.ui { position:fixed; inset:0; pointer-events:none; z-index:5; font-family:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; color:#d7e1f2; }
.title { position:absolute; top:22px; left:26px; }
.title h1 { font-size:15px; letter-spacing:0.35em; text-transform:uppercase; font-weight:600; opacity:.9; text-shadow:0 2px 12px rgba(0,0,0,.8); }
.title p { font-size:11.5px; letter-spacing:.14em; opacity:.55; margin-top:6px; }
#hint { position:absolute; bottom:20px; right:26px; font-size:11px; letter-spacing:.22em; text-transform:uppercase; opacity:.6; transition:opacity 1.2s; text-shadow:0 1px 8px rgba(0,0,0,.9); }
#hint.dim { opacity: .18; }
.vignette { position:absolute; inset:0; background:radial-gradient(ellipse at 50% 42%, transparent 55%, rgba(2,4,10,.55) 100%); }
```
Wait vignette gradient covering canvas with transparent center → darkens edges. pointer-events none ok.

Hmm radial-gradient ellipse syntax: `radial-gradient(ellipse 120% 90% at 50% 40%, transparent 55%, rgba(3,6,12,0.6) 100%)`. Use simpler.

Media query small screens: title smaller. Fine default.

JS hint dim: 
```js
const hint = document.getElementById('hint');
let hinted = false;
function dimHint(){ if(!hinted){hinted=true; hint.classList.add('dim');} }
controls.addEventListener('start', dimHint);
setTimeout(dimHint, 9000);
```

Now assemble everything. Also wrap in error handling? Not needed.

Double-check ground displacement consistency for object placement: objects placed with y = groundHeight(x,z) (plus offsets). Ground plane vertices displaced with same function → matches (plane rotated first). Note: plane 150 size, 46 segments → vertex grid spacing ~3.26; object placement uses continuous function, mesh is linear interpolation between vertices → slight mismatch (object may float/sink ≤ ~0.15). Mitigate: sink objects slightly (rocks sunk by design, trees -0.15, logs +0.02 with small sink). Increase segments to 64 → spacing 2.34, mismatch smaller. Use 60 segments (61² = 3721 verts ok). Actually low-poly aesthetic wants big facets — 46 segs over 150 units = ~3.26u facets, chunky. Keep 46 and sink objects ~0.2. Fine.

Also flatten center: fire area r<6 flat 0 → objects near center at y=0 exact. Good because smoothstep keeps h=0 for r<6 (min(1, max(0,(r-6)/10)) = 0 for r<6). Trees at r≥14 → h up to noise. Rocks scattered r 3..11: h=0 up to r=6 then partial (r-6)/10 * noise*0.9 ≤ 0.45*noise... at r=11: factor 0.5 → ≤0.45. Sink rocks 0.2 → fine.

Fallen logs at radius 4-8: h ≈ 0..0.2; sink 0.05. fine.

Noise amplitude: n = noise*0.9 + fine*0.25 max ~1.15*scale... at full (r>16): up to ~1.0 height variation across 3-unit facets — maybe too bumpy for walkable clearing; it's beyond clearing, ok. Cap: h = n*0.55? Let me scale: h = (noise2(x*0.13, z*0.13)*0.7 + noise2(x*0.3, z*0.3)*0.3) * 0.9 * ramp. Max ~0.9. Trees sink handles. Alright.

Also gentle bowl: h += Math.pow(Math.max(0, r-38)/14, 2)*2 → at r=75: ((37)/14)^2*2 ≈ 13.9 high rim — creates hillside horizon hiding plane edge. With fog, mostly hidden. Modest: use *1.5 → 10. fine. Trees at r≤52: r-38=14 → (1)²*1.5=1.5 at r=52: extra 1.5 rise, ok trees on slope.

Hmm but tree ring max 52 and ground rises beyond 38 — trees on rising terrain, natural (forest on hills around clearing). Good.

Ground color: 
```js
const cGrass = new THREE.Color(0x2a3b22);
const cGrass2 = new THREE.Color(0x1f2d1a);
const cDirt = new THREE.Color(0x4a3categories...) 
```
per vertex: r = hypot; mix dirt near center: if r<2.8 → dirt; blend zone 2.8..4.5. Variation: n2 = noise2(x*0.5,z*0.5): color = grass.lerp... implement:
```js
const g = new THREE.Color();
const tmpA = new THREE.Color(), tmpB = new THREE.Color();
tmpA.setHex(0x26331d); tmpB.setHex(0x2e4226);
g.lerpColors(tmpA, tmpB, n1); // variation
const dirt = new THREE.Color(0x453524);
const dMix = THREE.MathUtils.smoothstep(2.6, 4.6, r); // 0 at center→ dirt
g.lerp(dirt, 1 - dMix);
```
smoothstep(x, min, max) signature: THREE.MathUtils.smoothstep(x, min, max) returns 0..1 rising. At r=2 → 0 → lerp(dirt, 1) = dirt. Good.
Also far darkening: g.multiplyScalar(1 - 0.25*smoothstep(20, 60, r))? slight. ok.

Set colors attribute.

Rocks: geometry IcosahedronGeometry(r, 0) then displace with deterministic hash:

```js
function rockGeo(radius, seed){
  const g = new THREE.IcosahedronGeometry(radius, 0);
  const p = g.attributes.position;
  for (let i=0;i<p.count;i++){
    const x=p.getX(i), y=p.getY(i), z=p.getZ(i);
    const h = Math.sin(x*12.9898 + y*78.233 + z*37.719 + seed)*43758.5453;
    const f = h - Math.floor(h);
    const s = 0.78 + f*0.55;
    p.setXYZ(i, x*s, y*(0.75+f*0.5), z*s);
  }
  g.computeVertexNormals();
  return g;
}
```
Deterministic per position → no cracks. 

Rocks material: MeshStandardMaterial({color:0x565b63, flatShading:true, roughness:1}); vary color per rock slightly: new material per rock? 20 materials fine, or share 3 variants. Use color HSL jitter: material.clone() with color offsetColorHSL. Simple: 3 shared materials with colors 0x50565e, 0x5c636b, 0x464b52.

Ring stones: 9 stones at r=1.45, scale 0.28-0.4 flattened (scale.y 0.7), y ~0.08. castShadow/receiveShadow.

Scattered rocks: 9 at r 3.5..12, scale 0.35..0.9, random rotation, sunk y = h + r*0.25.

Fallen logs: 3 logs: cylinder radius 0.17, length 2.6, rotate to lie: mesh.rotation.set(PI/2±jitter around... lying along X: rotation.z = PI/2 then group rotation.y random. y = 0.17. Add second smaller log pair crossing? Also stumps: 2 stumps (cylinder radius 0.3, h 0.5) near edge? adds story. Quick: 2 stumps at r 4..7 with lighter top color? Just cylinder with cap; add. Nice touch.

Firewood teepee: 7 logs radius0.1 length 1.7, tilt 0.5 rad. Material wood: 0x5b4130; darker charred: inner logs? Keep single color; maybe ends? fine.

Logs material roughness 0.95.

Now, code for trees:

```js
const trunkGeo = new THREE.CylinderGeometry(0.16, 0.26, 2.0, 5);
const trunkMat = new THREE.MeshStandardMaterial({color:0x3e2c20, flatShading:true, roughness:1});
const foliageGeos = [
  new THREE.ConeGeometry(1.5, 2.4, 6),
  new THREE.ConeGeometry(1.15, 2.0, 6),
  new THREE.ConeGeometry(0.8, 1.7, 6),
];
const foliageMats = [0x1c3524, 0x24422b, 0x2b4d31, 0x173021].map(c => new THREE.MeshStandardMaterial({color:c, flatShading:true, roughness:1}));
```
Cone positions y: trunk center 1.0 (h2 → 0..2). Cones: cone1 center y = 1.7 (base 0.5, h2.4 → top 2.9); cone2 y = 2.9 (base 1.9 top 3.9); cone3 y = 4.0 (base 3.15, top 4.85). Overlaps fine.

Per tree: 
```js
const tree = new THREE.Group();
const m1 = new THREE.Mesh(trunkGeo, trunkMat);
m1.castShadow = m1.receiveShadow = true;
const mat = foliageMats[i%4]; // or random
cones each mesh castShadow=true, receiveShadow=true.
tree.add(...);
const s = 0.8 + Math.random()*0.8;
tree.scale.setScalar(s);
tree.rotation.y = Math.random()*Math.PI*2;
tree.rotation.x = (Math.random()-0.5)*0.06;
tree.rotation.z = (Math.random()-0.5)*0.06;
tree.position.set(x, groundHeight(x,z)-0.15, z);
```
42 trees → 42 groups × 4 meshes = 168 draw calls + shadows ×6... 1000 draw calls per shadow pass? No — shadow pass renders casters: 42×4 + logs etc ≈ 200 casters × 6 cube faces = 1200 draw calls per frame for shadows + main ~250 → ~1450 draw calls/frame. Hmm, that's heavy-ish but Chrome can handle; but on weaker machines 60fps might suffer. Optimization: reduce shadow cost by limiting which objects cast: trees beyond r>28 could skip castShadow (they're far, shadows barely visible, and distance culls via shadow camera far=45... they'd still be rendered into shadow map if within far). Set castShadow only for trees with r < 30. Shadow map render: renderer culls objects outside shadow camera frustum per face — many trees culled per face anyway. It's fine. Alternatively lower: keep castShadow true for all — 42 trees is not many. Total meshes ~200. Modern GPU fine at 60fps. Keep all cast for correctness (shadow of trees from fire light is the money shot).

Also main render: fine.

Draw call count acceptable.

Now the moon glow: sprite scale ~120? Moon radius 13 at 430 → apparent tiny (13/430 ≈ 0.03 rad ≈ 1.7°). That's small-ish; maybe moon radius 20 → 2.7°. Camera fov 55° vertical → moon ~5% of screen height — reasonable. Glow sprite scale 110 at moon pos → halo. opacity 0.28 color 0x8fb0ff? additive.

Also, is moon possibly occluded by trees? Trees max ~8 tall at distance 52 → angle ~8.8°; moon elevation: moonDir (-0.45,0.55,-0.7): elevation asin(0.55/|v|): |v|=sqrt(0.2025+0.3025+0.49)=sqrt(0.995)≈0.997 → asin(0.5517)≈33.5°. Above trees. ✓. Camera default looks at target from (8.5,3.4,10.5) — direction toward -x,-z-ish; moon at (-0.45,0.55,-0.7)*430 = (-194, 237, -301): behind trees left — visible when orbiting. Screenshot at 3s: camera initial view includes fire center; moon may or may not be in frame — direction of view from camera (8.5,3.4,10.5) to (0,1.4,0) = (-8.5,-2,-10.5) normalized → looking toward -x,-z — moon azimuth: atan2(-194? ... moon direction azimuth atan2(x=-0.45, z=-0.70) → in three, looking direction... roughly view forward vector (-0.6, -0.14, -0.74); moon direction from origin normalized (-0.451,0.552,-0.702). Dot of view dir with moon dir (from camera): camera at ~(8.5,3.4,10.5), moon at (-194,237,-301): dir = (-202,234,-311)/norm ≈ (-0.46, 0.53, -0.71). View dir (-0.6,-0.14,-0.74). Dot = 0.276 - 0.074 + 0.525 = 0.727 → angle ≈ 43° off-center. fov 55°/aspect... horizontal fov at 1280×800: vertical 55° → horizontal ~ 2*atan(tan(27.5°)*1.6) = 2*atan(0.833) ≈ 79.5°. Half-diagonal ~45°. So moon just at edge of frame initially — nice, likely visible in corner. Fine either way; stars visible regardless. Could move moon slightly more opposite camera: set moonDir = normalize(-0.55, 0.5, -0.65)? dot similar. Or place moon so initial view includes it: view dir toward (-0.6,-0.14,-0.74): moon should be at azimuth similar: moonDir = normalize((-0.5,0.5,-0.85))? then elevation lower. Let me set moonDir = new THREE.Vector3(-0.55, 0.62, -0.8).normalize? elevation asin(0.62/norm(0.55²+0.62²+0.8²=0.3025+0.3844+0.64=1.327→sqrt=1.152))=asin(0.538)=32.6°. Direction from camera to moon: moon≈(-0.477*430... whatever, azimuth: atan2(x,z)=atan2(-0.55,-0.8): angle in xz: arctan(0.55/0.8)=34.5° in third quadrant (x,z both negative) → azimuth ~ -180+... view azimuth: atan2(-8.5? camera→target xz dir = (-8.5,-10.5) → atan2(-8.5,-10.5) similarly third quadrant, arctan(8.5/10.5)=39°. So moon azimuth 34.5° vs view 39° — nearly aligned! elevation 32° vs view pitch -8°: moon appears above center by ~40° vertical — off top of frame. Hmm vertical half-fov 27.5° + tilt... moon at 32° elevation while camera looks down 8° → offset 40° above center → outside vertical (27.5). But corner diagonal ~44° → with horizontal offset small (4.5°), vertical needed 40 < 44 → in corner barely. Eh. Stars are the main sky feature anyway; moon visible when user orbits up. That's fine and realistic. I'll position moon at moderate elevation 30°, azimuth similar to initial view so orbiting up reveals it quickly. Keep (-0.5, 0.55, -0.78) normalized... compute norm: 0.25+0.3025+0.6084=1.1609 → 1.0775. fine.

Set moon mesh radius 18 at distance 420 → angular 2.45°.

Camera far 1000 ✓.

Also: fog:false on moon material ensures visible at 420.

Star sprite: they're points at 400+ → fog false ✓.

Let me also add subtle ground mist? skip.

Now write out the final HTML mentally complete, and double check every statement.

Additional detail: `THREE.MathUtils.smoothstep(x, min, max)` — verify signature: MathUtils.smoothstep(x, min, max) yes.

`Vector3.randomDirection()` exists.

For loop `for (let i=0;i<STAR_N;i++)` with direction: 
```js
const v = new THREE.Vector3().randomDirection();
if (v.y < 0.05) { v.y = 0.05 + Math.random()*0.9; v.normalize(); }
```
Actually randomDirection gives uniform sphere; keep ones with y>0.02 else reflect: v.y = Math.abs(v.y)+0.03; v.normalize(). Fine.

Also stars shouldn't be below ground silhouette much; y>0 ok.

Also add a few big stars: separate small Points with size 4? Use same geometry with vertexColors bright ones; size uniform though. Fine.

Now fire glow sprite: 
```js
const fireGlow = new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex, color:0xff9a40, transparent:true, opacity:0.4, depthWrite:false, blending:THREE.AdditiveBlending, fog:false}));
fireGlow.scale.set(6,6,1); position (0, 1.1, 0);
```
Animate opacity 0.28+0.18*flickish.

Also glow tex colorSpace: set `glowTex.colorSpace = THREE.SRGBColorSpace`? For additive white gradient fine; set anyway.

CanvasTexture needs `needsUpdate` auto true. ok.

Define `glowTex` once before use.

Order of code: textures first, then scene objects.

Also the "coal" material animate: coalMat.emissiveIntensity = 0.8 + 0.6*Math.abs(Math.sin(t*3.7)) * flickish.

Time source: `const clock = new THREE.Clock();` in loop `const dt = Math.min(clock.getDelta(), 0.05); const t = clock.elapsedTime;` — getDelta updates elapsedTime ✓.

autoRotate: `controls.autoRotate = true; controls.autoRotateSpeed = -0.5;` negative rotates... direction irrelevant. On user start disable: also re-enable after 30s idle? keep off. Actually maybe re-enable after 20s idle for continued showcase? Simple: on 'start' set autoRotate=false; on 'end', setTimeout 15s → autoRotate=true (clear timer on start). Minor. I'll implement small idle logic.

Edge: 'end' fires when interaction stops; set timer.

Let me now also ensure no horizontal scrollbar: canvas block, body overflow hidden ✓. Mobile 375px: UI text scales ok (absolute positioned, small fonts) fine.

devicePixelRatio: cap 2.

One more: `renderer.domElement` appended; CSS canvas display block.

Antialias with pixelRatio 2 fine.

Let me now also double check ConeGeometry default: ConeGeometry(radius, height, radialSegments, heightSegments, openEnded) — openEnded default false → includes base cap. For flames additive translucent, base cap visible as disc — fine (glow disc at base, actually looks like glow).

CylinderGeometry for teepee logs: caps included → fine.

Also teepee logs intersect ground fine.

Now, "logs around a campfire" — teepee + fallen logs ✓.

Now writing final code. I'll be meticulous about syntax.

Also note: material `fog:false` on PointsMaterial for stars: PointsMaterial constructor accepts fog param? Material base class supports `fog` property; constructor setValues applies 'fog' ✓ (Material.setValues handles any known property).

SpriteMaterial fog property exists ✓.

Let me write:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Night Camp — Low-Poly Forest Clearing</title>
<style>
  :root { color-scheme: dark; }
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #060b14; }
  canvas { display: block; }
  .ui { position: fixed; inset: 0; pointer-events: none; z-index: 10;
        font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        color: #d8e2f2; }
  .vignette { position: absolute; inset: 0;
        background: radial-gradient(120% 90% at 50% 42%, rgba(0,0,0,0) 52%, rgba(3,6,14,0.55) 100%); }
  .title { position: absolute; top: 24px; left: 28px; }
  .title h1 { font-size: 15px; font-weight: 600; letter-spacing: 0.38em; text-transform: uppercase;
        text-shadow: 0 2px 14px rgba(0,0,0,0.85); }
  .title p { margin-top: 7px; font-size: 11px; letter-spacing: 0.16em; text-transform: uppercase; opacity: 0.55; }
  #hint { position: absolute; bottom: 22px; right: 28px; font-size: 10.5px; letter-spacing: 0.24em;
        text-transform: uppercase; opacity: 0.65; transition: opacity 1.4s ease;
        text-shadow: 0 1px 10px rgba(0,0,0,0.9); }
  #hint.dim { opacity: 0.15; }
  @media (max-width: 560px) {
    .title { top: 16px; left: 18px; } .title h1 { font-size: 12px; }
    #hint { bottom: 16px; right: 16px; font-size: 9.5px; }
  }
</style>
</head>
<body>
<div class="ui">
  <div class="vignette"></div>
  <div class="title">
    <h1>Night&nbsp;Camp</h1>
    <p>Low-poly forest clearing &middot; three.js</p>
  </div>
  <div id="hint">Drag to orbit &middot; scroll to zoom</div>
</div>

<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>

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

Wait: importmap JSON must be exactly as given — I'll copy verbatim.

Now full module script:

```js
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/* ---------- renderer / scene / camera ---------- */
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x060b14);
scene.fog = new THREE.FogExp2(0x060b14, 0.022);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 1200);
camera.position.set(8.5, 3.6, 10.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.5, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 4;
controls.maxDistance = 40;
controls.maxPolarAngle = Math.PI / 2 - 0.07;
controls.minPolarAngle = 0.12;
controls.autoRotate = true;
controls.autoRotateSpeed = -0.55;
controls.update();
```

Fog density 0.022: far trees at ~52+10=62: exp(-(1.364)^2)=exp(-1.86)=0.155 → 84% faded. Nice.

Lights:

```js
const hemi = new THREE.HemisphereLight(0x35435f, 0x101408, 0.5);
scene.add(hemi);

const moonDir = new THREE.Vector3(-0.52, 0.55, -0.78).normalize();
const moonLight = new THREE.DirectionalLight(0x9fb2e8, 0.55);
moonLight.position.copy(moonDir).multiplyScalar(80);
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xff9040, 65, 46, 2);
fireLight.position.set(0, 1.6, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 46;
fireLight.shadow.bias = -0.008;
fireLight.shadow.normalBias = 0.02? 
```
Hmm normalBias for point lights — supported (shadow rendering uses same material). Keep 0.02... normalBias moves sample along normal; ok.

scene.add(fireLight);

Noise & ground:

```js
function fract(n) { return n - Math.floor(n); }
function hash2(x, y) { return fract(Math.sin(x * 127.1 + y * 311.7) * 43758.5453); }
function noise2(x, y) {
  const xi = Math.floor(x), yi = Math.floor(y);
  const xf = x - xi, yf = y - yi;
  const u = xf * xf * (3 - 2 * xf), v = yf * yf * (3 - 2 * yf);
  const a = hash2(xi, yi), b = hash2(xi + 1, yi), c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
  return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
}
function groundHeight(x, z) {
  const r = Math.hypot(x, z);
  const ramp = Math.min(1, Math.max(0, (r - 6) / 10));
  let h = (noise2(x * 0.13 + 7.3, z * 0.13 + 3.1) * 0.75 + noise2(x * 0.34 + 1.7, z * 0.34 + 9.2) * 0.3) * ramp;
  h += Math.pow(Math.max(0, r - 38) / 14, 2) * 2.2;
  return h;
}
```

Ground mesh:

```js
const groundGeo = new THREE.PlaneGeometry(160, 160, 46, 46);
groundGeo.rotateX(-Math.PI / 2);
const gp = groundGeo.attributes.position;
const gcolors = new Float32Array(gp.count * 3);
const cA = new THREE.Color(0x243518), cB = new THREE.Color(0x2f4423), cDirt = new THREE.Color(0x3f3425);
const tmp = new THREE.Color();
for (let i = 0; i < gp.count; i++) {
  const x = gp.getX(i), z = gp.getZ(i);
  gp.setY(i, groundHeight(x, z));
  const r = Math.hypot(x, z);
  tmp.lerpColors(cA, cB, noise2(x * 0.21 + 4.4, z * 0.21 + 8.8));
  tmp.lerp(cDirt, 1 - THREE.MathUtils.smoothstep(r, 2.4, 5.0));
  tmp.multiplyScalar(1 - 0.3 * THREE.MathUtils.smoothstep(r, 18, 70));
  gcolors[i*3] = tmp.r; gcolors[i*3+1] = tmp.g; gcolors[i*3+2] = tmp.b;
}
groundGeo.setAttribute('color', new THREE.BufferAttribute(gcolors, 3));
groundGeo.computeVertexNormals();
const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({ vertexColors: true, flatShading: true, roughness: 1, metalness: 0 }));
ground.receiveShadow = true;
scene.add(ground);
```

Wait plane 160 with 46 segs → spacing 3.48; ground extends to ±80; rim rise at r>38: at corner r≈113 → ((113-38)/14)^2*2.2 = (5.36)^2*2.2 ≈ 63 units high! Whoa too much. Cap it: use Math.min(1.8, ...) or reduce formula: Math.pow(max(0,r-38)/22, 2)*2.0 → at 113: (75/22)^2*2 = 11.6*2=23 — still large. Want gentle hill: maybe scale 0.8: ((r-38)/20)^2*1.2: at r=60: (1.1)^2*1.2=1.45; at 113: (3.75)^2*1.2 = 16.9. Hmm corners of plane at 113 dist. 17 units high edges — like surrounding hills, fine with fog (distance 113: exp(-(2.486)^2) ≈ 0.002 → fully fogged, invisible). OK any large value is hidden by fog; but the silhouette against background: fog color matches background → invisible. Fine. But shadow pass far 46 — objects beyond far not shadowed; fine.

Actually careful: big height near edges may cause fog mismatch in tone-mapped? Fog applied per fragment color → matches bg. ok.

But also: ground vertex at r>80? plane half-diagonal 113 → far corners high → they're beyond fog. good.

Let me keep rim formula: Math.pow(Math.max(0, r - 38) / 20, 2) * 1.6.

Also trees at r up to 52 sit on h up to ((14)/20)^2*1.6 = 0.78 + noise. ok.

Trees:

```js
const trunkGeo = new THREE.CylinderGeometry(0.16, 0.28, 2.2, 5);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3a2a1e, flatShading: true, roughness: 1 });
const foliageGeo = [
  new THREE.ConeGeometry(1.55, 2.6, 6),
  new THREE.ConeGeometry(1.15, 2.15, 6),
  new THREE.ConeGeometry(0.78, 1.75, 6),
];
const foliageY = [1.9, 3.2, 4.4];
const foliageMats = [0x1b3323, 0x22422b, 0x2a4d33, 0x173020].map(
  (c) => new THREE.MeshStandardMaterial({ color: c, flatShading: true, roughness: 1 })
);

const trees = new THREE.Group();
scene.add(trees);
const treePts = [];
let guard = 0;
while (treePts.length < 42 && guard++ < 4000) {
  const a = Math.random() * Math.PI * 2;
  const rr = Math.sqrt(THREE.MathUtils.lerp(14 * 14, 52 * 52, Math.random()));
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  let ok = true;
  for (const p of treePts) {
    const dx = p[0] - x, dz = p[1] - z;
    if (dx * dx + dz * dz < 4.8) { ok = false; break; }  // ~2.2 min spacing
  }
  if (!ok) continue;
  treePts.push([x, z]);
}
for (const [x, z] of treePts) {
  const g = new THREE.Group();
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.position.y = 1.1;
  g.add(trunk);
  const mi = (Math.random() * 4) | 0;
  for (let k = 0; k < 3; k++) {
    const m = new THREE.Mesh(foliageGeo[k], foliageMats[(mi + k) % 4]);
    m.position.y = foliageY[k];
    m.rotation.y = Math.random() * Math.PI * 2;
    m.castShadow = true; m.receiveShadow = true;
    g.add(m);
  }
  trunk.castShadow = true; trunk.receiveShadow = true;
  const s = 0.85 + Math.random() * 0.75;
  g.scale.setScalar(s);
  g.rotation.y = Math.random() * Math.PI * 2;
  g.rotation.z = (Math.random() - 0.5) * 0.07;
  g.rotation.x = (Math.random() - 0.5) * 0.07;
  g.position.set(x, groundHeight(x, z) - 0.18, z);
  trees.add(g);
}
```

Using same material per k with offset index creates color variation vertically too. nice.

Foliage base overlaps trunk top: trunk 0..2.2, cone1 base at 1.9-1.3=0.6 → cone widest 1.55 covers trunk. good.

Rocks:

```js
const rockMats = [0x4e545c, 0x5a616a, 0x454b52].map(c => new THREE.MeshStandardMaterial({ color: c, flatShading: true, roughness: 1 }));
function rockGeo(radius, seed) { ...as above... }
const rocksGroup = new THREE.Group(); scene.add(rocksGroup);
// fire ring stones
for (let i = 0; i < 9; i++) {
  const a = (i / 9) * Math.PI * 2 + Math.random() * 0.25;
  const rr = 1.5;
  const geo = rockGeo(0.3 + Math.random() * 0.1, i * 3.7);
  const m = new THREE.Mesh(geo, rockMats[i % 3]);
  m.position.set(Math.cos(a) * rr, 0.1, Math.sin(a) * rr);
  m.scale.y = 0.7;
  m.rotation.y = Math.random() * Math.PI;
  m.castShadow = m.receiveShadow = true;
  rocksGroup.add(m);
}
// scattered rocks
for (let i = 0; i < 10; i++) {
  const a = Math.random() * Math.PI * 2;
  const rr = 3.2 + Math.random() * 9;
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  const rad = 0.35 + Math.random() * 0.55;
  const m = new THREE.Mesh(rockGeo(rad, i * 7.13), rockMats[(i+1) % 3]);
  m.position.set(x, groundHeight(x, z) + rad * 0.18, z);
  m.rotation.set(Math.random()*0.4, Math.random()*Math.PI*2, Math.random()*0.4);
  m.castShadow = m.receiveShadow = true;
  rocksGroup.add(m);
}
```

Logs:

```js
const woodMat = new THREE.MeshStandardMaterial({ color: 0x53392a, flatShading: true, roughness: 0.95 });
const barkMat = new THREE.MeshStandardMaterial({ color: 0x453021, flatShading: true, roughness: 1 });
const firewoodGeo = new THREE.CylinderGeometry(0.09, 0.12, 1.7, 5);
const campfire = new THREE.Group();
scene.add(campfire);
for (let i = 0; i < 7; i++) {
  const g = new THREE.Group();
  g.rotation.y = (i / 7) * Math.PI * 2 + Math.random() * 0.3;
  const log = new THREE.Mesh(firewoodGeo, woodMat);
  log.position.set(0.52, 0.8, 0);
  log.rotation.z = 0.42 + Math.random() * 0.12;
  log.castShadow = true;
  g.add(log);
  campfire.add(g);
}
```
Wait log rotation.z positive tilts top toward -X local (center) ✓ (as computed earlier: positive z rotation sends +Y toward -X). With g.rotation.y = θ, child at local +x 0.52 → world radial outward ✓. Top toward center ✓. 

Log tilt 0.45: top at local x = 0.52 - sin(0.45)*0.85 ≈ 0.52-0.37=0.15, y = 0.8 + cos(0.45)*0.85 ≈ 1.57. Good teepee over flames base.

Coals:

```js
const coalMat = new THREE.MeshStandardMaterial({ color: 0x140a05, emissive: 0xff3c00, emissiveIntensity: 1.2, flatShading: true, roughness: 1 });
for (let i = 0; i < 6; i++) {
  const a = Math.random() * Math.PI * 2, rr = Math.random() * 0.45;
  const m = new THREE.Mesh(new THREE.IcosahedronGeometry(0.12 + Math.random() * 0.12, 0), coalMat);
  m.position.set(Math.cos(a) * rr, 0.1 + Math.random() * 0.1, Math.sin(a) * rr);
  m.rotation.set(Math.random(), Math.random(), Math.random());
  m.castShadow = false;
  campfire.add(m);
}
```

Fallen logs & stumps:

```js
const fallenGeo = new THREE.CylinderGeometry(0.16, 0.2, 2.6, 6);
const fallen = [
  [4.6, 2.2, 0.5], [-5.5, -3.5, 2.4], [2.8, -5.8, 1.2], [-3.6, 5.9, 3.6],
];
```
Better random placement with fixed seeds: 
```js
for (let i = 0; i < 4; i++) {
  const a = (i * 1.9 + 0.7);
  const rr = 3.6 + (i * 1.37) % 6;
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  const m = new THREE.Mesh(fallenGeo, barkMat);
  m.position.set(x, groundHeight(x, z) + 0.14, z);
  m.rotation.z = Math.PI / 2;
  m.rotation.y = Math.random() * Math.PI * 2;
```
Hmm order of rotations: mesh.rotation is Euler XYZ applied... rotation.z=PI/2 lays cylinder along X; then rotation.y rotates around world Y (order XYZ means R = Ry? THREE Euler XYZ: applies R = Rx * Ry * Rz? Actually matrix = RX(x)·RY(y)·RZ(z)? THREE 'XYZ' order composes as M = Rx * Ry * Rz? I believe Euler order 'XYZ' means rotate about X first, then Y, then Z, with matrix multiplication M = Rz? Let me not overthink: simpler to lay log via group: group.rotation.y random; child log rotation.z=PI/2. Do that.

```js
const g = new THREE.Group();
g.position.set(x, groundHeight(x,z) + 0.15, z);
g.rotation.y = Math.random()*Math.PI*2;
const m = new THREE.Mesh(fallenGeo, barkMat);
m.rotation.z = Math.PI/2 + (Math.random()-0.5)*0.1;
m.castShadow = m.receiveShadow = true;
g.add(m);
scene.add(g);
```
With group rot y then child laid along local X ✓.

Stumps:
```js
for 2 stumps: cylinder(0.28, 0.34, 0.7, 6) at random pos r 4..8, y=+0.3.
```
With slightly lighter top? skip detail. add.

Flames:

```js
const flames = [];
function addFlame(r, h, seg, y, color, opacity, speed, spin, off) {
  const geo = new THREE.ConeGeometry(r, h, seg);
  const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity, depthWrite: false, blending: THREE.AdditiveBlending, fog: false });
  const m = new THREE.Mesh(geo, mat);
  m.position.set(off[0], y, off[2]);
  campfire.add(m);
  flames.push({ m, h, baseY: y, speed, phase: Math.random() * Math.PI * 2, spin, ox: off[0], oz: off[2] });
}
addFlame(0.72, 2.3, 6, 1.15, 0xff4200, 0.5, 5.3, 0.7, [0, 0, 0]);
addFlame(0.5, 1.95, 5, 1.0, 0xff8412, 0.65, 6.4, -0.9, [0, 0, 0]);
addFlame(0.32, 1.55, 5, 0.88, 0xffc23c, 0.8, 7.1, 1.2, [0, 0, 0]);
addFlame(0.17, 1.15, 4, 0.74, 0xffe9a6, 0.9, 7.9, 1.7, [0, 0, 0]);
for (let i = 0; i < 5; i++) {
  const a = (i / 5) * Math.PI * 2 + 0.5;
  const rr = 0.34 + Math.random() * 0.22;
  addFlame(0.14 + Math.random() * 0.06, 0.7 + Math.random() * 0.5, 4, 0.62,
    0xff6b12, 0.7, 6.5 + Math.random() * 3, (Math.random() - 0.5) * 5,
    [Math.cos(a) * rr, 0, Math.sin(a) * rr]);
}
```
Licks positioned around edges — represents licking flames.

Note: flame cones with additive & opacity: over logs? Logs rendered opaque first, flames additive over — flame bases at y ~0 base (cone center 1.15, half height 1.15 → base y=0) → base circle at ground level may additively tint ground — fine glow.

Flame update function:

```js
function updateFlames(t, flick) {
  for (const f of flames) {
    const s = Math.sin(t * f.speed + f.phase);
    const s2 = Math.sin(t * f.speed * 2.13 + f.phase * 1.71);
    const sy = (1 + 0.3 * s + 0.12 * s2) * flick;
    const w = (1 + 0.13 * Math.sin(t * f.speed * 0.93 + f.phase * 2.1)) * (0.85 + 0.15 * flick);
    f.m.scale.set(w, sy, w);
    f.m.position.y = f.baseY + 0.07 * Math.sin(t * f.speed * 0.8 + f.phase) + (f.h * 0.5) * (sy - 1) * 0.45;
    f.m.position.x = f.ox + 0.05 * Math.sin(t * f.speed * 0.7 + f.phase);
    f.m.position.z = f.oz + 0.05 * Math.cos(t * f.speed * 0.62 + f.phase);
    f.m.rotation.y = f.phase + t * f.spin;
    f.m.rotation.x = 0.13 * Math.sin(t * f.speed * 0.5 + f.phase);
    f.m.rotation.z = 0.13 * Math.cos(t * f.speed * 0.44 + f.phase);
  }
}
```
flick ~ 0.9-1.15 multiplier adds global pulsing.

Sparks:

```js
const glowTex = makeGlowTexture();
const SPARK_N = 150;
... arrays, spawn, update as planned ...
const sparkMat = new THREE.PointsMaterial({ size: 0.1, map: glowTex, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, vertexColors: true, sizeAttenuation: true, fog: false });
```

Hmm — vertexColors with map: color multiplies map. ✓.

Fireflies:

```js
const FF_N = 16;
const ffBase = [], ffPhase = [], ffSpeed = [];
positions & colors arrays.
for i: a=rand*2π, rr = 4+rand*8, base(x= cos*a*rr, y=groundHeight+0.6+rand*1.6, z=sin...), phase rand*2π, speed 0.5+rand*0.8, blink speed 1.5+rand*2.
update: 
px = bx + Math.sin(t*ws+p)* (0.9) ... store amp per axis? simple: 
x = bx + Math.sin(t * sp1 + p) * 1.1 where sp1 = ffSpeed[i]*1.3
y = by + Math.sin(t * ffSpeed[i]*2.1 + p*2.0) * 0.5
z = bz + Math.cos(t * ffSpeed[i]*1.1 + p) * 1.1
b = Math.pow(Math.max(0, Math.sin(t * blink[i] + p * 3)), 3) * 0.9 + 0.04;
color: (0.55, 1.0, 0.3) * b → set arrays; needsUpdate.
```
size 0.14.

Points objects frustumCulled = false (positions dynamic) — set.

Stars, moon:

```js
const STAR_N = 900;
const spArr = new Float32Array(STAR_N * 3), scArr = new Float32Array(STAR_N * 3);
const sv = new THREE.Vector3();
for (let i = 0; i < STAR_N; i++) {
  sv.randomDirection();
  if (sv.y < 0.03) { sv.y = 0.03 + Math.random() * 0.9; sv.normalize(); }
  const R = 420 + Math.random() * 90;
  spArr[i*3] = sv.x * R; spArr[i*3+1] = sv.y * R; spArr[i*3+2] = sv.z * R;
  const b = 0.25 + Math.pow(Math.random(), 2.4) * 0.95;
  const tone = Math.random();
  let r = b, g = b, bl = b;
  if (tone < 0.22) { bl = b * 1.05; r = b * 0.8; }
  else if (tone < 0.34) { r = b; g = b * 0.92; bl = b * 0.75; }
  scArr[i*3] = r; scArr[i*3+1] = g; scArr[i*3+2] = bl;
}
```
b up to 1.2 fine additive.

starGeo + PointsMaterial({ size: 2.4, sizeAttenuation: false, vertexColors: true, transparent: true, opacity: 0.9, depthWrite: false, blending: Additive, fog: false }).

Moon:

```js
const moon = new THREE.Mesh(new THREE.SphereGeometry(19, 24, 16), new THREE.MeshBasicMaterial({ color: 0xdfe6f8, fog: false }));
moon.position.copy(moonDir).multiplyScalar(460);
scene.add(moon);
const moonGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: 0xaebfff, transparent: true, opacity: 0.5, depthWrite: false, blending: THREE.AdditiveBlending, fog: false }));
moonGlow.scale.set(150, 150, 1);
moonGlow.position.copy(moonDir).multiplyScalar(440);
scene.add(moonGlow);
```
Moon apparent radius: 19/460 = 0.041 rad ≈ 2.4°. good.
Glow 150 at 440 → 19° halo.

Also craters: add a few darker patches? Use second smaller spheres? skip.

Fire glow sprite:

```js
const fireGlow = new THREE.Sprite(new THREE.SpriteMaterial({ map: glowTex, color: 0xffa040, transparent: true, opacity: 0.5, depthWrite: false, blending: THREE.AdditiveBlending, fog: false }));
fireGlow.scale.set(7, 7, 1);
fireGlow.position.set(0, 1.3, 0);
scene.add(fireGlow);
```

Loop:

```js
const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  const flick = 0.86
    + 0.10 * Math.sin(t * 11.7)
    + 0.06 * Math.sin(t * 27.3 + 1.6)
    + 0.05 * Math.sin(t * 6.9 + 3.1)
    + 0.03 * Math.sin(t * 43.1 + 0.7);
  const fl = 1.05 + 0.55 * flick; // light intensity base ~ (1.05+0.55) range
```
Hmm compute: fireLight.intensity = 55 * flickish where flickish ∈ [0.86±0.24] → 0.62..1.10 → intensity 34..60. Set fireLight.intensity = 58 * (0.7 + 0.3 * ...) eh simpler: fireLight.intensity = 62 * flick. range 38..68.

```js
  fireLight.intensity = 62 * flick;
  fireLight.position.set(Math.sin(t * 3.7) * 0.14, 1.55 + Math.sin(t * 9.3) * 0.12, Math.cos(t * 3.1) * 0.14);
  fireGlow.material.opacity = 0.26 + 0.22 * flick;
  coalMat.emissiveIntensity = 0.7 + 1.1 * flick;
  starMat.opacity = 0.75 + 0.2 * Math.sin(t * 0.7);
  updateFlames(t, 0.8 + 0.25 * flick);
  updateSparks(dt, t);
  updateFireflies(t);
  controls.update();
  renderer.render(scene, camera);
}
animate();
```

coalMat.emissiveIntensity = 0.7+1.1*flick → flick~0.86 avg → 1.65 avg pulses. ok.

Resize:

```js
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
```

Idle autoRotate management:

```js
let idleTimer = null;
controls.addEventListener('start', () => {
  controls.autoRotate = false;
  if (idleTimer) clearTimeout(idleTimer);
  dimHint();
});
controls.addEventListener('end', () => {
  if (idleTimer) clearTimeout(idleTimer);
  idleTimer = setTimeout(() => { controls.autoRotate = true; }, 20000);
});
```

Hint dim:

```js
const hint = document.getElementById('hint');
let hintDimmed = false;
function dimHint() { if (!hintDimmed) { hintDimmed = true; hint.classList.add('dim'); } }
setTimeout(dimHint, 10000);
```
But 'start' also fires on wheel? OrbitControls dispatches 'start' on wheel/pointerdown/touch — yes wheel triggers start & end quickly. So autoRotate stops on scroll; restarts 20s later. fine.

Also keyboard? OrbitControls listens keys only if enabled with listenToKeyEvents — not called, fine.

Sparks update code (full):

```js
function updateSparks(dt, t) {
  for (let i = 0; i < SPARK_N; i++) {
    const i3 = i * 3;
    sLife[i] -= dt;
    if (sLife[i] <= 0) spawnSpark(i, false);
    const drag = Math.exp(-1.15 * dt);
    sVel[i3]     = sVel[i3] * drag + Math.sin(t * 0.9 + i * 1.7) * 0.06 * dt;
    sVel[i3 + 1] = sVel[i3 + 1] * drag + 0.4 * dt;
    sVel[i3 + 2] = sVel[i3 + 2] * drag + Math.cos(t * 0.8 + i * 1.3) * 0.06 * dt;
    sPos[i3]     += sVel[i3] * dt;
    sPos[i3 + 1] += sVel[i3 + 1] * dt;
    sPos[i3 + 2] += sVel[i3 + 2] * dt;
    const fade = Math.max(0, sLife[i] / sMax[i]);
    const b = Math.pow(fade, 1.1);
    sCol[i3]     = b;
    sCol[i3 + 1] = b * (0.3 + 0.55 * fade);
    sCol[i3 + 2] = b * 0.12;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;
}
```

spawnSpark:

```js
function spawnSpark(i, scatter) {
  sMax[i] = 0.6 + Math.random() * 1.5;
  sLife[i] = scatter ? Math.random() * sMax[i] : sMax[i];
  const i3 = i * 3;
  sPos[i3]     = (Math.random() - 0.5) * 0.5;
  sPos[i3 + 1] = 0.5 + Math.random() * 0.8;
  sPos[i3 + 2] = (Math.random() - 0.5) * 0.5;
  const a = Math.random() * Math.PI * 2, sp = 0.25 + Math.random() * 0.5;
  sVel[i3]     = Math.cos(a) * sp;
  sVel[i3 + 1] = 1.4 + Math.random() * 1.9;
  sVel[i3 + 2] = Math.sin(a) * sp;
}
```

Fireflies full:

```js
const FF_N = 16;
const ffPos = new Float32Array(FF_N * 3), ffCol = new Float32Array(FF_N * 3);
const ffBx = new Float32Array(FF_N), ffBy = new Float32Array(FF_N), ffBz = new Float32Array(FF_N);
const ffP = new Float32Array(FF_N), ffS = new Float32Array(FF_N), ffB = new Float32Array(FF_N);
for (let i = 0; i < FF_N; i++) {
  const a = Math.random() * Math.PI * 2, rr = 3.5 + Math.random() * 9;
  const x = Math.cos(a) * rr, z = Math.sin(a) * rr;
  ffBx[i] = x; ffBz[i] = z; ffBy[i] = groundHeight(x, z) + 0.5 + Math.random() * 1.8;
  ffP[i] = Math.random() * Math.PI * 2;
  ffS[i] = 0.4 + Math.random() * 0.5;
  ffB[i] = 1.2 + Math.random() * 2.2;
}
const ffGeo = new THREE.BufferGeometry();
ffGeo.setAttribute('position', new THREE.BufferAttribute(ffPos, 3).setUsage(THREE.DynamicDrawUsage));
ffGeo.setAttribute('color', new THREE.BufferAttribute(ffCol, 3).setUsage(THREE.DynamicDrawUsage));
const fireflies = new THREE.Points(ffGeo, new THREE.PointsMaterial({ size: 0.16, map: glowTex, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, vertexColors: true, fog: false }));
fireflies.frustumCulled = false;
scene.add(fireflies);
function updateFireflies(t) {
  for (let i = 0; i < FF_N; i++) {
    const i3 = i * 3, p = ffP[i], s = ffS[i];
    ffPos[i3]     = ffBx[i] + Math.sin(t * s * 1.3 + p) * 1.2;
    ffPos[i3 + 1] = ffBy[i] + Math.sin(t * s * 2.1 + p * 2.0) * 0.55;
    ffPos[i3 + 2] = ffBz[i] + Math.cos(t * s * 1.1 + p) * 1.2;
    let q = Math.sin(t * ffB[i] + p * 3.0);
    const b = 0.05 + Math.pow(Math.max(0, q), 3) * 1.1;
    ffCol[i3] = 0.62 * b; ffCol[i3 + 1] = b; ffCol[i3 + 2] = 0.3 * b;
  }
  ffGeo.attributes.position.needsUpdate = true;
  ffGeo.attributes.color.needsUpdate = true;
}
```

glow texture function:

```js
function makeGlowTexture() {
  const c = document.createElement('canvas');
  c.width = c.height = 64;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
  g.addColorStop(0, 'rgba(255,255,255,1)');
  g.addColorStop(0.35, 'rgba(255,255,255,0.45)');
  g.addColorStop(1, 'rgba(255,255,255,0)');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, 64, 64);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}
```

Now check: shadow of teepee logs cast by pointlight onto ground ✓.

Also ground receiveShadow ✓, trees receiveShadow ✓ (firelight dapples).

HemisphereLight intensity 0.5, moonLight 0.55 — night ambience. Might be a bit bright but shows shapes. Given ACES + dark colors, ok.

Also note MeshStandardMaterial with only hemi+dir: trees far side dark. good.

Check star points: `sizeAttenuation:false` with size 2.4 — in r170 PointsMaterial, when sizeAttenuation false, gl_PointSize = size (in device pixels? multiplied by pixelRatio? shader: gl_PointSize = size; not scaled by DPR... The canvas is scaled by DPR via setSize+setPixelRatio, so gl_PointSize is in physical pixels → at DPR2 appears 1.2 CSS px. fine.

One risk: additive stars drawn behind moon mesh — depthTest true (default) — moon mesh at 460? Moon at 460, stars 420-510 — some stars behind moon occluded correctly ✓ (moon opaque basic). MoonGlow sprite additive drawn over stars near it — halo effect ✓.

Fog:false on star material ensures stars not fogged ✓.

Draw order transparency sorting handles rest.

Now hint: also mention it auto-plays: "Drag to orbit · scroll to zoom". fine.

One more requirement re-check: "OrbitControls to look around, with limits so the camera stays above the ground." ✓ maxPolarAngle < π/2.

Also initial camera y=3.6 within limits ✓ (polar angle from target y1.5: height diff 2.1, dist horizontal ~13.5 → polar ~81° < 86° ✓).

Also `controls.minPolarAngle = 0.12` prevents top-down weirdness ✓.

Also make sure `fireLight.shadow.camera.fov`? Point light shadow uses cube 90° fixed. ok.

Potential acne on ground from point shadow with bias -0.008: ground is single-sided (front side up), shadow map from below faces? Point light above ground; ground faces up → in shadow map render, front faces toward light ✓. bias -0.008 reduces self-shadow. normalBias 0.02. Should be fine.

Self-shadow flicker on trees: normalBias helps.

Also check shadow of tree onto itself with flat facets — fine.

Now also double check the import map block placement: importmap must come before any module script that uses it — both in body; importmap first ✓. Also `type="importmap"` JSON exact copy:

```html
<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>
```
The task says "Load Three.js only through this import map (do not use any other URL or version): <script type="importmap">{...}</script>" — copy the exact JSON content. Their snippet is minified; I can format it or keep minified. Keep as given (minified) to be safe? Formatting is equivalent JSON; fine either way. I'll keep it exactly as provided on one line.

Now assemble everything and check for syntax errors mentally, variable order:

Order in script:
1. imports
2. renderer, scene, camera, controls
3. glowTex (needed by materials later) — define makeGlowTexture before use.
4. lights
5. noise functions
6. ground
7. stars, moon, moonGlow (starMat variable needed in loop — name it)
8. trees
9. rocks
10. logs/stumps/campfire/teepee/coals
11. flames
12. sparks
13. fireflies
14. fireGlow sprite
15. hint dim + idle logic
16. resize
17. loop

starMat variable: `const starMat = new THREE.PointsMaterial({...})`.

Also fireLight added to scene.

Also `moonLight` — its position at moonDir*80, default target 0 ✓.

Double-check `THREE.MathUtils.lerp(a, b, t)` ✓ exists.

Potential problem: PlaneGeometry 160 with 46 segments: PlaneGeometry(width, height, widthSegments, heightSegments) ✓.

groundGeo.attributes.position: after rotateX, y is index 1 → setY ✓.

Also `computeVertexNormals` after displacement ✓ (flatShading anyway).

BufferAttribute colors: material vertexColors true → uses 'color' attribute ✓.

Check rockGeo deterministic displacement: vertices of icosahedron detail 0: 12 unique positions but stored per-face duplicated (60 vertices, each face 3). Deterministic function of (x,y,z) → duplicates identical ✓ no cracks. computeVertexNormals on non-indexed → per-face normals consistent ✓ (even though flatShading uses derivatives).

One caveat: IcosahedronGeometry positions might have vertices at same position but different winding order — same displacement, fine.

Tree foliage cones: ConeGeometry(radius, height, 6) → side 6 tris? Cone side segments: 6 → 6 side faces (each 2 tris? Cone geometry side is 6 quads? For ConeGeometry, radial segments 6 → lateral surface 6 quads? Actually cone lateral = radialSegments quads? A cone with radialSegments n has n lateral faces (triangles to apex): apex connects to n points → n triangles. Plus base n triangles (2n? base is a disk: n triangles fan). ConeGeometry(radius, height, radialSegments=6): lateral = 6 triangles? No wait CylinderGeometry-derived: cone is cylinder with top radius 0 → lateral faces = radialSegments * heightSegments quads → each quad for cone is triangle (top degenerate) → 6 lateral triangles... Actually CylinderGeometry builds quads between top/bottom rings; with top radius 0, quads become triangles: 6 lateral triangles + 6 base cap triangles (caps fan → radialSegments triangles? cap uses 6 triangles? cap: n quads? cap = n triangles fan from center). So low-poly pyramid-ish with 6 sides ✓ good.

foliage flatShading ✓.

Trunk CylinderGeometry(0.16,0.28,2.2,5): 5 segments → chunky ✓.

Now UI: any issue with `#hint` inside `.ui` (pointer-events none) ✓.

Vignette overlaying canvas: additive dark radial — subtle ✓.

Also add `user-select:none`? fine.

Also `touch-action: none`? OrbitControls sets touch-action itself on domElement (style.touchAction = 'none') ✓ automatic.

Also address: `document.body.appendChild(renderer.domElement)` after body div exists — script at end of body ✓.

Edge: `window.devicePixelRatio` in sandbox ~1-2 ✓.

Tone check: flames additive over black bg = bright ✓.

Also flame flick param: updateFlames(t, 0.8+0.25*flick) → flick ∈ [0.62,1.10] → flameGlobal ∈ [0.955, 1.075] subtle global pulse ✓.

Sparks colors bright near spawn: fade=1 → b=1 → (1, 0.85, 0.12) bright yellow ✓ fading to 0.

Now count trees = 42 ✓ ≥30. Also ensure treePts fill: min spacing 2.2 in annulus area π(52²−14²)=π*2548=8003 units² / 42 → 190 per tree, spacing fine; guard 4000 attempts enough ✓.

Also trees near fire min 14 — clearing ✓. Camera maxDistance 40 could reach into trees — acceptable.

Fallen logs positions r 3.6..9.6 — inside clearing, on flat-ish ground ✓. Also they must not collide with ring rocks (r 1.5) — min 3.6 ✓.

Stumps: 2 cylinders.

Check no name collisions: variables: hemi, moonDir, moonLight, fireLight, glowTex, groundGeo, ground, treePts, trunkGeo... `flames` array vs function name updateFlames ✓ distinct. `campfire` group ✓.

`starMat` used in loop ✓ defined.

`fireGlow` defined before loop ✓.

`coalMat` defined ✓.

Also `sparks` Points var name; `sparkGeo`.

`moon`, `moonGlow`.

One more consideration — "click or press a key to start" message requirement is for games using keyboard; we don't use keyboard. We have a hint about orbit. Good.

Also "self-playing demo mode (a simple AI plays)" for games — this is a scene, autoRotate provides motion. OK.

Also ensure no console warnings that matter... Color management default fine.

Also `renderer.setPixelRatio` before or after setSize — order fine either.

`THREE.SRGBColorSpace` on canvas texture ✓.

`SpriteMaterial` rotation not needed.

Also check `PointsMaterial({map})` — with alpha in texture; transparent:true, depthWrite:false, blending additive → glowy points ✓. PointsMaterial needs `alphaTest`? no.

Also depth sorting: sparks additive over flames — both additive, fine.

One more: fireflies at r 3.5..12.5 might sit inside trees at r≥14 — no ✓.

Now, does anything throw on load? `clock.getDelta()` first call ok.

`controls.update()` with autoRotate requires delta for speed? autoRotate uses internal clock via `update(deltaTime?)` — calling update() without arg uses performance.now internally? In recent versions, OrbitControls.update(deltaTime = null) uses getDelta from internal clock... Actually current implementation: update(deltaTime = null); if deltaTime not null uses it else uses 2π*... it uses `_currentTime`? Let me recall r170 OrbitControls: function update(deltaTime = null) { if (this.autoRotate && state === STATE.NONE) rotateLeft(getAutoRotationAngle(deltaTime)); ... } and getAutoRotationAngle(deltaTime) { if (deltaTime !== null) return (2*Math.PI/60*autoRotateSpeed)*deltaTime... else return 2*Math.PI/60/60*autoRotateSpeed }. So calling update() each frame gives fixed per-frame rotation ~ deg/frame — fine.

Damping needs update() each frame ✓.

Now the initial camera: distance from target (0,1.5,0): pos (8.5,3.6,10.5): dist = sqrt(8.5²+2.1²+10.5²)= sqrt(72.25+4.41+110.25)= sqrt(186.9)=13.67 ✓ within [4,40].

Polar angle: cos = (camera.y - target.y)/dist = 2.1/13.67 = 0.154 → polar ≈ 81.2° < 86° ✓.

Good.

Ground receives shadows of trees from moon? moonLight castShadow false → fine.

Now write final. Also I should double check `renderer.outputColorSpace` default srgb — ok.

Also for aesthetics, set `scene.background` slightly gradient? Keep color.

Possible improvement: add subtle blue rim via `hemi` ground color — done.

Final check of HTML/CSS for 375px: title small, canvas full — ✓ no horizontal scroll (overflow hidden).

Also I will add `<div id="app">` not needed.

Write final answer with a single code block, brief no extra text (rules: exactly one fenced code block, no text before/after). I'll output only the code block.

Let me write the complete code now, carefully.

Also one more check on `addFlame` defaults — off param arrays length 3 with [x, 0, z]; I index off[0], off[2] ✓.

For licks: baseY 0.62 with h ~0.7-1.2: cone center 0.62 → base ~0.62-h/2 ≈ 0.05 ok above ground.

Also licks ox ~0.34-0.56 from center; may stick through ring stones (r1.5) — no, inside ✓.

Double-check updateSparks when life resets: spawnSpark sets pos/vel; then immediately continues with fade = sLife/sMax = 1 ✓.

Wind drift: fine.

Also cap spark height: they rise ~ vy start 1.4-3.3, drag → rise maybe 2-4 units over 2.1s max life — good, sparks rise above flames then fade.

Also add slight gravity? Buoyant +0.4*dt vs drag net ~ +0.35/s² → ok embers rise.

Now finalize CSS/HTML text content.

One more thought on requirement "logs around a campfire": teepee logs ✓ plus "a few low-poly rocks and logs around a campfire in the center" — ring rocks ✓.

Also "ground with a few low-poly rocks and logs around a campfire" — interpreted same ✓.

Also maybe add small glow light under? no.

Final code assembly. Also verify no `const` reassignments (flame param names etc.).

Also `let hintDimmed` etc.

Also the loop uses `flick` in updateFlames & light. ✓

Also `moonLight` intensity maybe 0.5.

Ready. Also double-check `PlaneGeometry` vertex count 47*47=2209 — colors array fine.

Also `THREE.MathUtils.smoothstep(r, 2.4, 5.0)` — signature smoothstep(x, min, max) ✓ returns 0 below min.

Dirt lerp: `tmp.lerp(cDirt, 1 - smoothstep(r,2.4,5))` — at r<2.4 → lerp factor 1 → dirt ✓; beyond 5 → 0 grass ✓.

Also darken far: multiplyScalar(1 - 0.3*smoothstep(r,18,70)) → at 70+ 0.7× darker ✓ (helps distance fade too).

Trees on ground with receiveShadow — fine.

Also one subtle thing: fog affects ground far → good.

Write it out.

Actually also add `renderer.shadowMap.autoUpdate` default true — needed since light moves ✓.

Performance ok.

Also, I realize the moon MeshBasicMaterial will be tone-mapped (color 0xdfe6f8 → ACES slightly) fine.

Also moonGlow opacity 0.5 additive with color 0xaebfff — halo. Might be strong; 0.35. ok.

Let me also reconsider fireLight intensity: physical mode. Ground albedo ~0.1 (dark colors 0x243518 → linear ~ (0.02,0.04,0.01)? sRGB 0x24 = 36/255 = 0.14 → linear ≈ 0.017). Radiance = E*albedo/π: at distance 3, E = 62/9 = 6.9 → radiance ≈ 6.9*0.03/π ≈ 0.066 → tone mapped → dim-ish. Hmm maybe too dim! Let me reconsider: with ACES exposure 1.12, radiance 0.066 → ACES(0.066) ≈ 0.05-ish → dark. That's quite dim. But real campfire at 3m is dim compared to daylight... but stylized scene should show visible warm glow radius. Let me raise intensity: at distance 3 with intensity 200: E=22 → radiance ~0.2 → ACES ~0.17 → visible warm. At 6m: E=5.5 → 0.05 dim. Hmm.

Also flame materials are basic (unlit) so they're bright regardless.

Also note hemisphere light adds: hemi 0.5 with sky 0x35435f (linear ~ (0.03,0.05,0.09)) irradiance ~ intensity*color ≈ 0.5*0.05π? Hemisphere irradiance = intensity * color... In three physical mode HemisphereLight contributes irradiance = color*intensity (π factor?). Net effect: ground lit by hemi ≈ 0.03*0.1/π ≈ tiny. Scene might be VERY dark overall. Hmm. Let me reconsider brightness values.

Actually in three.js r155+ (color management + physical lights), a HemisphereLight intensity 0.5 gives dim but visible fill on a 0.1-albedo object? The final pixel = radiance ≈ (E_hemi + ...)·albedo/π. E_hemi = hemiColor·intensity·π? I recall that with legacy lights removed, to keep visual parity they multiplied things by π internally... e.g., DirectionalLight intensity 1 ≈ old 1. HemisphereLight similar. Old-style scene: dir light 0.5 on 0.2 albedo → radiance 0.5*0.2 = 0.1 → mid-dark. So values like classic ones translate similar since three normalizes. Indeed r155 release notes: "the default of useLegacyLights changed to false; light intensities now in physical units but approximately the old values are preserved by internal scaling of π" — hmm not exactly; they said point/spot need intensity *= π? Let me recall concretely: In r155 migration guide: "With useLegacyLights false, to get same result multiply PointLight/SpotLight intensity by π (decay=2 still)". Something like that. Directional/hemisphere unaffected mostly.

Classic point light intensity 1, distance 0, decay 1(old physical?) In old default decay=1 linear falloff. New decay=2 physical. To match old look intensity must be scaled up ~π·r².

OK, practical approach: point light intensity such that E at 3m ≈ 8-15 "lux-classic". I = E·d² = 10·9 = 90. So ~90 gives solid lighting at 3m, 30 at 6m (E=2.5), 10 at 15m (E=0.4 dim). With albedo 0.03-0.1 → radiance at 3m: 10*0.05/π = 0.16 → ACES(0.16·1.12) ≈ 0.14 → visible. Hmm still dimish but night scene.

Also ground albedo in sRGB 0x2f4423 → linear roughly (0.033, 0.063, 0.017). /π → 0.01-0.02·E. With E=10 → 0.1-0.2 radiance → decent mid-dark after ACES. OK.

I'll set base intensity 90 * flick. Distance param 46. Also maybe bump exposure to 1.25 and hemi to 0.6, moon dir 0.6 to make night scene visible. Trees foliage albedo 0x1b3323 (dark) lit by moon 0.6: E=0.6·colorless → radiance ≈ 0.6*0.05/π ≈ 0.01 → very dark silhouettes. Hemi adds similar magnitude. OK night.

Hmm, to hedge against overly dark render, let me raise: hemi intensity 0.7 (sky 0x3a4a6a, ground 0x141a10), moonLight 0.8 color 0xa8bce8, exposure 1.3. Fire intensity 95·flick. Stars additive bright independent ✓. Flames basic bright ✓. Moon basic ✓.

Ground near fire: E at 2m = 95/4 ≈ 24; albedo_linear ~0.05/π → 0.38 → warm bright pool ✓. At 8m: 1.5 → dim. Nice falloff.

Fine, also give foliage slightly brighter palette: 0x23422c etc. ok existing.

ACES + exposure 1.3, sky bg 0x060b14 remains near-black ✓.

Also toneMapping affects additive blending (applied after? tone mapping applied in shader per material — additive sum then tone map? Each material tonemapped individually then blended → additive in tonemapped space → glow compresses to white less. Whatever, looks fine typically.)

Set final values:
- exposure 1.25
- hemi (0x3c4b68, 0x131a10, 0.7)
- moonLight (0xa9c0ec, 0.7)
- fireLight intensity base 95, distance 50, decay 2.

Shadow far must ≥ distance 50 → set shadow.camera.far = 50. PointLight distance 50 cuts light at 50.

Alright.

Also small note: `fireLight.shadow.bias = -0.008`, normalBias 0.02.

Double-check flatShading ground with vertexColors + shadows fine.

Also maybe raise fog slightly lighter to see silhouettes: bg 0x060b14.

I think good. Also final visual QA thought: initial camera at 13.7 distance, ~81° polar, fire centered with teepee logs, stones, flames ~2-3 units tall visible; trees ring at 14+ behind; stars up top corners; moon up-left possibly in frame; vignette dark corners. Looks solid.

One more: at 3s screenshot, sparks visible mid-air ✓, flames mid-animation ✓.

Also to make trees more obviously "low-poly", foliage cones with 6 segments ✓, flat shading ✓.

Edge: OrbitControls damping + autoRotate combined works.

Also disable `controls.enableZoom`? keep enabled ✓.

Write the final file now.

Let me also handle possible WebGL context creation failure? Not needed.

Count code size ~ fine.

Final code:

(writing carefully, checking braces)

Also in updateFlames I use f.m.scale etc — keys: m, h, baseY, speed, phase, spin, ox, oz ✓ set in addFlame ✓.

Also make sure `addFlame` pushes with ox/oz from off ✓.

Double-check `updateFlames` signature (t, flick) and call ✓.

Sparks: `const sparks = new THREE.Points(...)` add to scene ✓.

Everything referenced defined before animate() runs (animate called at end) ✓.

Also `moonDir` reused for moon position ✓.

Also `THREE.MathUtils.smoothstep` imported via THREE ✓.

I'm confident. Compose final output.