Models 44

Mistral Large 4

by MistralReasoningClean
Open in new window

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

How it went

Total time
4 min 48 s
Cost
$0.045
Output tokens
21,452
Spent thinking
14,245 tokens
Speed
74.7 tok/s
Console errors
0
First token after
1.2 s
First visible text after
3 min 24 s
Prompt tokens
577
Code size
19.7 KB
Finish reason
stop
Served by
Mistral
Model id
mistralai/mistral-large-4-0
Reasoning sent
model default
Max output
262,144 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 · 477 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Night Campfire — Low-Poly Forest</title>
<style>
  html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #05070f; }
  #app { position: fixed; inset: 0; }
  canvas { display: block; touch-action: none; }
  #ui {
    position: fixed; top: 14px; left: 16px; z-index: 10;
    color: #cfd6e4; font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
    pointer-events: none; user-select: none; text-shadow: 0 1px 4px rgba(0,0,0,.85);
  }
  #ui h1 { font-size: 15px; margin: 0 0 4px; font-weight: 600; letter-spacing: .5px; color: #ffd9a8; }
  #ui p  { font-size: 11.5px; margin: 0; opacity: .72; }
</style>
<script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script>
</head>
<body>
<div id="app"></div>
<div id="ui">
  <h1>🔥 Night Campfire in the Forest</h1>
  <p>Drag to orbit &nbsp;·&nbsp; Scroll to zoom &nbsp;·&nbsp; Right-drag to pan</p>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

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

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

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 500);
camera.position.set(9.5, 4.6, 9.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 3.5;
controls.maxDistance = 30;
controls.minPolarAngle = 0.12;
controls.maxPolarAngle = Math.PI * 0.495;   // never go below the ground
controls.autoRotate = true;
controls.autoRotateSpeed = 0.55;
controls.screenSpacePanning = false;

/* ---------------- helpers ---------------- */
const smoothstep = (a, b, x) => { const t = Math.min(1, Math.max(0, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
function groundHeight(x, z) {
  const d = Math.hypot(x, z);
  let h = Math.sin(x * 0.22) * Math.cos(z * 0.27) * 0.55
        + Math.sin(x * 0.08 + 1.7) * Math.cos(z * 0.10 + 0.6) * 1.0
        + Math.sin(x * 0.47 + z * 0.39) * 0.18;
  return h * smoothstep(2.6, 8.5, d);   // flat clearing around the fire
}
function makeGlowTexture() {
  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, '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, 128, 128);
  return new THREE.CanvasTexture(c);
}
const glowTex = makeGlowTexture();

/* ---------------- lights ---------------- */
scene.add(new THREE.HemisphereLight(0x2a3a66, 0x0b0e09, 0.5));

const moonDir = new THREE.Vector3(0.45, 0.42, -0.78).normalize();
const moonLight = new THREE.DirectionalLight(0x9db4ff, 0.35);
moonLight.position.copy(moonDir).multiplyScalar(60);
scene.add(moonLight, moonLight.target);

const fireLight = new THREE.PointLight(0xff8433, 34, 34, 2);
fireLight.position.set(0, 1.3, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 32;
fireLight.shadow.bias = -0.0015;
fireLight.shadow.normalBias = 0.03;
scene.add(fireLight);

/* ---------------- sky dome, stars, moon ---------------- */
const sky = new THREE.Mesh(
  new THREE.SphereGeometry(220, 32, 16),
  new THREE.ShaderMaterial({
    side: THREE.BackSide, depthWrite: false, fog: false,
    vertexShader: `varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }`,
    fragmentShader: `
      varying vec3 vP;
      void main(){
        float h = normalize(vP).y;
        vec3 zen = vec3(0.006, 0.010, 0.028);
        vec3 hor = vec3(0.045, 0.062, 0.115);
        vec3 col = mix(hor, zen, pow(clamp(h, 0.0, 1.0), 0.55));
        if (h < 0.0) col = hor * 0.55;
        gl_FragColor = vec4(col, 1.0);
      }`
  })
);
scene.add(sky);

const starMats = [];
function makeStars(count, radius, size, color, opacity) {
  const pos = new Float32Array(count * 3);
  for (let i = 0; i < count; i++) {
    let x, y, z;
    do { x = Math.random() * 2 - 1; y = Math.random() * 2 - 1; z = Math.random() * 2 - 1; }
    while (x * x + y * y + z * z > 1 || x * x + y * y + z * z === 0 || y < -0.08);
    const l = Math.hypot(x, y, z);
    pos[i * 3] = x / l * radius; pos[i * 3 + 1] = y / l * radius; pos[i * 3 + 2] = z / l * radius;
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  const m = new THREE.PointsMaterial({ color, size, sizeAttenuation: false, transparent: true, opacity, depthWrite: false, fog: false });
  starMats.push({ mat: m, base: opacity });
  scene.add(new THREE.Points(g, m));
}
makeStars(550, 200, 1.5, 0xcfd8ff, 0.85);
makeStars(70, 200, 2.6, 0xffffff, 1.0);

const moonCanvas = document.createElement('canvas'); moonCanvas.width = moonCanvas.height = 256;
{
  const ctx = moonCanvas.getContext('2d');
  ctx.fillStyle = '#f2eeda'; ctx.fillRect(0, 0, 256, 256);
  for (let i = 0; i < 14; i++) {
    ctx.fillStyle = `rgba(170,168,150,${0.12 + Math.random() * 0.15})`;
    ctx.beginPath();
    ctx.arc(Math.random() * 256, Math.random() * 256, 8 + Math.random() * 26, 0, Math.PI * 2);
    ctx.fill();
  }
}
const moon = new THREE.Mesh(
  new THREE.CircleGeometry(7, 40),
  new THREE.MeshBasicMaterial({ map: new THREE.CanvasTexture(moonCanvas), fog: false })
);
moon.position.copy(moonDir).multiplyScalar(190);
moon.lookAt(0, 0, 0);
scene.add(moon);
const moonGlow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: glowTex, color: 0x8fa5e8, transparent: true, opacity: 0.35,
  blending: THREE.AdditiveBlending, depthWrite: false, fog: false
}));
moonGlow.position.copy(moon.position);
moonGlow.scale.setScalar(60);
scene.add(moonGlow);

/* ---------------- ground ---------------- */
const groundGeo = new THREE.PlaneGeometry(160, 160, 70, 70);
groundGeo.rotateX(-Math.PI / 2);
{
  const gp = groundGeo.attributes.position;
  const cols = new Float32Array(gp.count * 3);
  const cA = new THREE.Color(0x14301b), cB = new THREE.Color(0x274a26), cC = new THREE.Color(0x0d2216);
  for (let i = 0; i < gp.count; i++) {
    const x = gp.getX(i), z = gp.getZ(i);
    gp.setY(i, groundHeight(x, z));
    const n = 0.5 + 0.5 * Math.sin(x * 0.6 + z * 0.8);
    const c = cA.clone().lerp(cB, n);
    if (Math.random() < 0.06) c.lerp(cC, 0.7);
    cols[i * 3] = c.r; cols[i * 3 + 1] = c.g; cols[i * 3 + 2] = c.b;
  }
  groundGeo.setAttribute('color', new THREE.BufferAttribute(cols, 3));
  groundGeo.computeVertexNormals();
}
const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({ vertexColors: true, flatShading: true, roughness: 1 }));
ground.receiveShadow = true;
scene.add(ground);

/* ---------------- trees (40, randomly placed, none on the fire) ---------------- */
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3a2a1a, flatShading: true, roughness: 1 });
function makeTree(scale) {
  const g = new THREE.Group();
  const h = (2.6 + Math.random() * 3.2) * scale;
  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.10 * scale, 0.22 * scale, h * 0.5, 5), trunkMat);
  trunk.position.y = h * 0.25; trunk.castShadow = true; g.add(trunk);
  const fcol = new THREE.Color(0x14401f).offsetHSL(Math.random() * 0.05 - 0.025, 0.05, Math.random() * 0.06 - 0.03);
  const fmat = new THREE.MeshStandardMaterial({ color: fcol, flatShading: true, roughness: 1 });
  let y = h * 0.32;
  for (let i = 0; i < 3; i++) {
    const r = (1.15 - i * 0.32) * scale * (0.9 + Math.random() * 0.2);
    const ch = (1.5 - i * 0.3) * scale;
    const cone = new THREE.Mesh(new THREE.ConeGeometry(r, ch, 6), fmat);
    cone.position.y = y + ch / 2; cone.castShadow = true; g.add(cone);
    y += ch * 0.62;
  }
  return g;
}
for (let i = 0; i < 40; i++) {
  const a = Math.random() * Math.PI * 2;
  const r = 6.2 + Math.pow(Math.random(), 0.75) * 27;   // keep clear of the fire
  const t = makeTree(0.85 + Math.random() * 0.8);
  t.position.set(Math.cos(a) * r, 0, Math.sin(a) * r);
  t.position.y = groundHeight(t.position.x, t.position.z);
  t.rotation.y = Math.random() * Math.PI * 2;
  scene.add(t);
}

/* ---------------- rocks & bushes ---------------- */
const rockMat = new THREE.MeshStandardMaterial({ color: 0x707580, flatShading: true, roughness: 0.95 });
for (let i = 0; i < 14; i++) {
  const a = Math.random() * Math.PI * 2, r = 3 + Math.random() * 10;
  const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(0.15 + Math.random() * 0.4, 0), rockMat);
  rock.position.set(Math.cos(a) * r, 0, Math.sin(a) * r);
  rock.position.y = groundHeight(rock.position.x, rock.position.z) + rock.geometry.parameters.radius * 0.3;
  rock.scale.set(1, 0.55 + Math.random() * 0.35, 1);
  rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
  rock.castShadow = true; rock.receiveShadow = true;
  scene.add(rock);
}
const bushMat = new THREE.MeshStandardMaterial({ color: 0x1d4423, flatShading: true, roughness: 1 });
for (let i = 0; i < 10; i++) {
  const a = Math.random() * Math.PI * 2, r = 2.6 + Math.random() * 4;
  const b = new THREE.Mesh(new THREE.IcosahedronGeometry(0.3 + Math.random() * 0.35, 0), bushMat);
  b.position.set(Math.cos(a) * r, 0, Math.sin(a) * r);
  b.position.y = groundHeight(b.position.x, b.position.z) + 0.18;
  b.scale.y = 0.7; b.castShadow = true;
  scene.add(b);
}

/* ---------------- campfire: stone ring, logs, coals ---------------- */
for (let i = 0; i < 9; i++) {
  const a = i / 9 * Math.PI * 2 + Math.random() * 0.3, r = 1.15 + Math.random() * 0.12;
  const s = new THREE.Mesh(new THREE.DodecahedronGeometry(0.17 + Math.random() * 0.1, 0), rockMat);
  s.position.set(Math.cos(a) * r, groundHeight(Math.cos(a) * r, Math.sin(a) * r) + 0.07, Math.sin(a) * r);
  s.scale.set(1, 0.6 + Math.random() * 0.3, 1);
  s.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
  s.castShadow = true; s.receiveShadow = true;
  scene.add(s);
}
const woodMat = new THREE.MeshStandardMaterial({ color: 0x5b3b21, flatShading: true, roughness: 1 });
const charMat = new THREE.MeshStandardMaterial({ color: 0x1c120b, flatShading: true, roughness: 1, emissive: 0xff4400, emissiveIntensity: 0.9 });
const UP = new THREE.Vector3(0, 1, 0);
function makeLog(len, rad, mat) {
  const m = new THREE.Mesh(new THREE.CylinderGeometry(rad, rad * 1.15, len, 7), mat);
  m.castShadow = true; return m;
}
// teepee logs
const apex = new THREE.Vector3(0, 1.25, 0);
for (let k = 0; k < 3; k++) {
  const a = k * Math.PI * 2 / 3 + 0.5;
  const base = new THREE.Vector3(Math.cos(a) * 0.5, 0.06, Math.sin(a) * 0.5);
  const dir = apex.clone().sub(base).normalize();
  const m = makeLog(base.distanceTo(apex) + 0.35, 0.085, k === 1 ? charMat : woodMat);
  m.position.copy(base).add(apex).multiplyScalar(0.5);
  m.quaternion.setFromUnitVectors(UP, dir);
  scene.add(m);
}
// lying logs around the fire
[[1.9, 0.8], [2.5, 2.6], [2.1, 4.4]].forEach(([r, a], i) => {
  const m = makeLog(1.1, 0.1, i === 1 ? charMat : woodMat);
  m.position.set(Math.cos(a) * r, groundHeight(Math.cos(a) * r, Math.sin(a) * r) + 0.1, Math.sin(a) * r);
  m.quaternion.setFromUnitVectors(UP, new THREE.Vector3(Math.cos(a), 0.03, Math.sin(a)).normalize());
  scene.add(m);
});
// glowing coals
const coalMat = new THREE.MeshStandardMaterial({ color: 0x140b06, emissive: 0xff5a00, emissiveIntensity: 2.2, roughness: 1 });
for (let i = 0; i < 7; i++) {
  const a = Math.random() * Math.PI * 2, r = 0.15 + Math.random() * 0.3;
  const c = new THREE.Mesh(new THREE.IcosahedronGeometry(0.07 + Math.random() * 0.05, 0), coalMat);
  c.position.set(Math.cos(a) * r, 0.05, Math.sin(a) * r);
  c.rotation.set(Math.random() * 3, Math.random() * 3, 0);
  scene.add(c);
}

/* ---------------- animated flames (custom shader) ---------------- */
const flameVert = `
  uniform float uTime; uniform float uHeight; uniform float uSeed;
  varying float vY;
  void main(){
    float y = clamp(position.y / uHeight, 0.0, 1.0);
    vY = y;
    vec3 p = position;
    p.x += sin(uTime * 3.1 + y * 6.0 + uSeed) * 0.16 * y;
    p.z += cos(uTime * 2.6 + y * 5.0 + uSeed * 1.7) * 0.14 * y;
    p.x += sin(uTime * 6.7 + uSeed * 2.0) * 0.10 * y * y;
    p.y += sin(uTime * 4.3 + uSeed) * 0.05 * y;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
  }`;
const flameFrag = `
  uniform vec3 uColor; uniform float uTime; uniform float uSeed; uniform float uAlpha;
  varying float vY;
  void main(){
    float a = pow(1.0 - vY, 1.7);
    a *= 0.72 + 0.28 * sin(uTime * 10.0 + uSeed * 7.0 + vY * 9.0);
    a *= uAlpha;
    if (a < 0.01) discard;
    gl_FragColor = vec4(uColor, a);
  }`;
const flames = [];
function makeFlame(radius, height, color, alpha, seed) {
  const geo = new THREE.ConeGeometry(radius, height, 7, 4, true);
  geo.translate(0, height / 2, 0);
  const mat = new THREE.ShaderMaterial({
    uniforms: {
      uTime: { value: 0 }, uHeight: { value: height }, uSeed: { value: seed },
      uColor: { value: new THREE.Color(color) }, uAlpha: { value: alpha }
    },
    vertexShader: flameVert, fragmentShader: flameFrag,
    transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide
  });
  const m = new THREE.Mesh(geo, mat);
  m.position.y = 0.32; m.renderOrder = 10;
  scene.add(m);
  flames.push({ mesh: m, mat, phase: seed });
}
makeFlame(0.55, 1.75, 0xff4d00, 0.50, 1.3);
makeFlame(0.40, 1.35, 0xff7a1a, 0.55, 2.9);
makeFlame(0.26, 1.00, 0xffb52e, 0.65, 4.1);
makeFlame(0.15, 0.62, 0xfff0b0, 0.80, 5.7);

const glow = new THREE.Sprite(new THREE.SpriteMaterial({
  map: glowTex, color: 0xff8033, transparent: true, opacity: 0.5,
  blending: THREE.AdditiveBlending, depthWrite: false, fog: false
}));
glow.position.set(0, 0.85, 0);
glow.scale.setScalar(3.4);
glow.renderOrder = 12;
scene.add(glow);

/* ---------------- sparks (particle system) ---------------- */
const SPARKS = 130;
const sGeo = new THREE.BufferGeometry();
const sPos = new Float32Array(SPARKS * 3), sVel = new Float32Array(SPARKS * 3);
const sSize = new Float32Array(SPARKS), sAlpha = new Float32Array(SPARKS);
const sLife = new Float32Array(SPARKS), sMax = new Float32Array(SPARKS), sBase = new Float32Array(SPARKS);
sGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3));
sGeo.setAttribute('aSize', new THREE.BufferAttribute(sSize, 1));
sGeo.setAttribute('aAlpha', new THREE.BufferAttribute(sAlpha, 1));
function resetSpark(i) {
  const a = Math.random() * Math.PI * 2, r = Math.random() * 0.22;
  sPos[i * 3] = Math.cos(a) * r; sPos[i * 3 + 1] = 0.5 + Math.random() * 0.4; sPos[i * 3 + 2] = Math.sin(a) * r;
  sVel[i * 3] = (Math.random() - 0.5) * 0.4;
  sVel[i * 3 + 1] = 1.1 + Math.random() * 1.7;
  sVel[i * 3 + 2] = (Math.random() - 0.5) * 0.4;
  sLife[i] = sMax[i] = 0.8 + Math.random() * 1.6;
  sBase[i] = 0.05 + Math.random() * 0.08;
  sSize[i] = sBase[i]; sAlpha[i] = 1;
}
for (let i = 0; i < SPARKS; i++) resetSpark(i);
const sparks = new THREE.Points(sGeo, new THREE.ShaderMaterial({
  uniforms: { uColor: { value: new THREE.Color(0xffb050) } },
  vertexShader: `
    attribute float aSize; attribute float aAlpha; varying float vA;
    void main(){
      vA = aAlpha;
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      gl_PointSize = clamp(aSize * (500.0 / -mv.z), 1.5, 30.0);
      gl_Position = projectionMatrix * mv;
    }`,
  fragmentShader: `
    uniform vec3 uColor; varying float vA;
    void main(){
      float d = length(gl_PointCoord - 0.5);
      float m = smoothstep(0.5, 0.05, d);
      gl_FragColor = vec4(uColor, m * vA);
    }`,
  transparent: true, depthWrite: false, blending: THREE.AdditiveBlending
}));
sparks.renderOrder = 11;
sparks.frustumCulled = false;
scene.add(sparks);

/* ---------------- fireflies ---------------- */
const FF = 22;
const fGeo = new THREE.BufferGeometry();
const fPos = new Float32Array(FF * 3);
const fBase = new Float32Array(FF * 3);
const fPh = new Float32Array(FF * 3);
for (let i = 0; i < FF; i++) {
  const a = Math.random() * Math.PI * 2, r = 2.5 + Math.random() * 7;
  fBase[i * 3] = Math.cos(a) * r;
  fBase[i * 3 + 1] = 0.6 + Math.random() * 1.8;
  fBase[i * 3 + 2] = Math.sin(a) * r;
  fPh[i * 3] = Math.random() * 6.28; fPh[i * 3 + 1] = Math.random() * 6.28; fPh[i * 3 + 2] = Math.random() * 6.28;
}
fGeo.setAttribute('position', new THREE.BufferAttribute(fPos, 3));
const fireflies = new THREE.Points(fGeo, new THREE.PointsMaterial({
  color: 0xaef07a, size: 0.07, transparent: true, opacity: 0.85,
  blending: THREE.AdditiveBlending, depthWrite: false, fog: false
}));
fireflies.frustumCulled = false;
scene.add(fireflies);

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

  // flickering warm point light
  const n = Math.sin(t * 11.3) * 0.5 + Math.sin(t * 23.7 + 1.3) * 0.3 + Math.sin(t * 5.1 + 0.7) * 0.2;
  fireLight.intensity = 32 + n * 7 + (Math.random() - 0.5) * 1.5;
  fireLight.position.set(Math.sin(t * 7.7) * 0.06, 1.28 + Math.sin(t * 13.1) * 0.05, Math.cos(t * 9.3) * 0.06);

  // flames
  for (let i = 0; i < flames.length; i++) {
    const f = flames[i];
    f.mat.uniforms.uTime.value = t;
    f.mesh.scale.set(
      1 + Math.sin(t * 9 + i * 2) * 0.12,
      1 + Math.sin(t * (6 + i * 1.7) + i) * 0.18,
      1 + Math.cos(t * 8 + i) * 0.12
    );
    f.mesh.rotation.y = t * (0.6 + i * 0.23) + i;
  }
  glow.scale.setScalar(3.2 + Math.sin(t * 7.3) * 0.25 + n * 0.35);
  glow.material.opacity = 0.42 + n * 0.08;

  // sparks
  const wind = Math.sin(t * 0.35) * 0.3 + 0.12;
  for (let i = 0; i < SPARKS; i++) {
    sLife[i] -= dt;
    if (sLife[i] <= 0) { resetSpark(i); }
    else {
      sVel[i * 3 + 1] -= dt * 0.45;
      sVel[i * 3] += wind * dt * 0.6;
      sVel[i * 3 + 2] += Math.cos(t * 0.5 + i) * dt * 0.15;
      sPos[i * 3] += sVel[i * 3] * dt;
      sPos[i * 3 + 1] += sVel[i * 3 + 1] * dt;
      sPos[i * 3 + 2] += sVel[i * 3 + 2] * dt;
      const f = sLife[i] / sMax[i];
      sAlpha[i] = f * f;
      sSize[i] = sBase[i] * (0.4 + 0.6 * f);
    }
  }
  sGeo.attributes.position.needsUpdate = true;
  sGeo.attributes.aSize.needsUpdate = true;
  sGeo.attributes.aAlpha.needsUpdate = true;

  // fireflies
  for (let i = 0; i < FF; i++) {
    fPos[i * 3]     = fBase[i * 3]     + Math.sin(t * 0.7 + fPh[i * 3]) * 1.2;
    fPos[i * 3 + 1] = fBase[i * 3 + 1] + Math.sin(t * 1.1 + fPh[i * 3 + 1]) * 0.5;
    fPos[i * 3 + 2] = fBase[i * 3 + 2] + Math.cos(t * 0.9 + fPh[i * 3 + 2]) * 1.2;
  }
  fGeo.attributes.position.needsUpdate = true;

  // star twinkle
  for (const s of starMats) s.mat.opacity = s.base * (0.78 + 0.22 * Math.sin(t * 2.1 + s.base * 10));

  controls.update();
  controls.target.x = THREE.MathUtils.clamp(controls.target.x, -20, 20);
  controls.target.y = THREE.MathUtils.clamp(controls.target.y, 0.3, 10);
  controls.target.z = THREE.MathUtils.clamp(controls.target.z, -20, 20);

  renderer.render(scene, camera);
}
animate();

/* ---------------- resize ---------------- */
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
Reasoning trace (14,245 tokens)
Let me plan this. Build a single HTML file with Three.js via import map, campfire scene at night:

- Ground plane (dark green, low-poly maybe with vertex displacement), rocks (icosahedron/dodecahedron flattened, gray), logs (cylinders) around fire.
- Campfire center: stones ring, logs leaning, animated flames (cone meshes with shader or scale animation, additive blending), point light flickering with shadows, sparks particle system (Points with custom update).
- 30+ trees: trunk cylinder + cone foliage, random positions in ring avoiding center.
- Sky: big sphere with shader or Points stars, moon (circle sprite/mesh with emissive), fog (THREE.FogExp2), dark background.
- OrbitControls with minPolarAngle/maxPolarAngle to keep above ground, min/max distance.
- Resize handler, renderer fills window.
- Self-playing demo: OrbitControls autoRotate maybe — the requirement says games need demo mode; this is a scene, but auto-rotating camera slowly is nice. Not required but nice. The instructions say "Something meaningful must be visible on first paint" — the scene itself.

Flames: use several cone geometries with MeshBasicMaterial (additive, orange/yellow), animated scale/rotation, maybe custom shader for vertex wobble. Simpler: ShaderMaterial with time uniform, flame shape. Or use cones with onBeforeCompile... Keep simple: 5-7 cones, each frame update scale.y with sin, position slight sway, material opacity. Use additive blending for glow.

Sparks: THREE.Points with ~80 particles, each with velocity, life; update positions in animate loop, reset at base. PointsMaterial with size, additive, color orange; maybe use small size attenuation. Could use custom shader for size by life but simple is fine — vary material size globally? Use vertex colors and per-point size via ShaderMaterial. I'll do a simple ShaderMaterial for points with attribute size and life fade. Or simpler: PointsMaterial with map? No external images. Use a circular sprite generated via canvas — allowed (no external). Generate a radial gradient texture via canvas. That's fine.

Shadows: renderer.shadowMap enabled, point light castShadow = true, flame meshes castShadow false (additive), trees cast shadow, rocks cast, ground receive. Point light shadow with 6 faces — set shadow.mapSize 1024, bias.

Moon: circle mesh with emissive material placed far, plus maybe a glow sprite. Directional light faint blue from moon direction for ambient fill, low intensity, castShadow maybe not needed (point light does shadows). Keep ambient very low + hemisphere light subtle.

Stars: Points on sphere dome, random, white/blueish, size attenuation false maybe.

Fog: FogExp2 dark blue-black, density ~0.02, so distant trees fade.

Trees: function makeTree() — trunk: CylinderGeometry(0.15-0.3, 0.3-0.5, height), foliage: 2-3 stacked cones (ConeGeometry) dark green with flatShading. Merge? Just group. Random rotation/scale. Place 35 trees at radius 8–30 from center, random angle, ensure not within 4 of center. Some trees at varying heights. castShadow true.

Rocks: DodecahedronGeometry random scale, gray flatShading, scattered around clearing, a ring of stones around fire (8 small stones circle radius 1.2).

Logs around fire: 3-4 cylinders leaning toward fire (like teepee) plus some lying on ground around. Burning logs: cylinders lying crossed with emissive ends? Add emissive orange material to inner log tips. Keep simple: 3 logs leaning in teepee arrangement + 2 lying flat.

Ground: PlaneGeometry 80x80, 40x40 segments, displace vertices with noise (simple sin-based pseudo-random), flatShading, dark green. Compute vertex normals. Color variation via vertex colors maybe. Keep simple: flatShading MeshStandardMaterial color #1a2f1a-ish. Add slight color variation with vertex colors.

Camera: start at (8, 5, 8) looking at fire (0,1,0). OrbitControls target (0,1.2,0), enableDamping, autoRotate true speed 0.5, minDistance 4, maxDistance 25, maxPolarAngle = Math.PI/2 - 0.05 (stay above ground), minPolarAngle 0.15.

"Click or press a key to start" — that's for games; this is a scene with OrbitControls; mouse works immediately in iframe? The note says iframe only receives keyboard after click; mouse presumably works. I'll still show a small hint overlay "Drag to orbit · scroll to zoom" that fades. Not required but nice. Actually to be safe, add a subtle overlay hint; it doesn't block.

Resize: window resize listener updating camera aspect and renderer size.

Animation loop: clock delta, flicker light: intensity = base + noise (sin combos + random), position slight jitter. Flames: update each flame mesh scale/rotation/opacity. Sparks update. Fireflies maybe? Not required. Maybe add subtle embers glow sprite at fire base.

Flame implementation detail: create flame group at fire center. 3 layers: outer cone (orange, height 1.6, radius 0.5), mid (height 1.2, radius 0.35, yellow-orange), inner (height 0.8, radius 0.2, near-white yellow). Each cone geometry translated so base at y=0 (geometry.translate(0, h/2, 0)). Material MeshBasicMaterial with additive blending, transparent, depthWrite false, color. Animate: scale.y = base * (1 + 0.25*sin(t*speed + phase)), scale.x/z pulse, rotation.y slow spin, material opacity flicker. Also slight skew via rotation.z wobble. That looks decent.

Better: custom shader flame with vertex displacement for licks. Could do ShaderMaterial:

vertexShader: position.y based wobble: pos.x += sin(time*3 + position.y*4)*0.1*(position.y/height); etc. Keep moderate. I'll write a ShaderMaterial with uniforms time, color; varying vUv; fragment: alpha = (1 - vUv.y) * shape... Using cone UV: vUv.y goes 0 at bottom? Cone geometry UV v=1 at top? Actually for ConeGeometry, v goes from 0 at bottom to 1 at top? Let me recall: CylinderGeometry generates v = 1 - y/height... In three, for cylinder, v = 1 - (y / height + 0.5)? uv.y = 1 - (vertex y normalized). Top has v=1? For cylinder: `uv.y = 1 - ( y / height + 0.5 )`? Hmm. Actually in CylinderGeometry code: `uv.y = 1 - (vertex.y / height + 0.5)`? Let me just use position-based: pass vY = (position.y / height + 0.5) in [0,1] with 1 at top after translating geometry so base at 0 and top at height: vY = position.y / height. Then alpha = pow(1 - vY, 1.5) * edge fade via uv.x around circumference — use sin? Simpler: use normal-based edge soft? Use vUv from geometry and compute alpha from vUv.y. For cone, I'll just compute my own varying. Fragment: color mix from deep orange at bottom to yellow at top based on vY, alpha = (1-vY)^2 * 0.9 * flicker. Additive blending makes it glow.

Wobble in vertex: 
```
float w = sin(time*4.0 + position.y*5.0 + seed) * 0.15 * position.y / height;
pos.x += w; pos.z += cos(time*3.0 + position.y*4.0) * 0.12 * position.y/height;
```
Also taper: scale x,z by (1 - vY*0.6)? Cone already tapers. Add tip flick: pos.x += sin(time*6.0)*0.2*vY*vY.

That gives nice animated flames. 4-5 flame planes? Cones all around look okay from all angles with additive. Use 3 cones with different seeds/heights + maybe 2 crossed planes with the same shader for volume. I'll do 3 cones + inner cone.

Sparks shader points:
attributes: position (start), aSeed. Per-frame update on CPU is easier: store arrays velocity, life. Points geometry with position attribute updated each frame; use ShaderMaterial with attribute aLife for size/alpha, or just PointsMaterial size fixed with opacity global and fade via color attribute... CPU update with per-point color: use vertexColors true and set color from orange to transparent? PointsMaterial doesn't support per-point alpha without shader. Use ShaderMaterial:

```
attribute float aSize; attribute float aAlpha; varying float vAlpha;
vertex: gl_PointSize = aSize * (300.0 / -mvPosition.z); vAlpha = aAlpha;
fragment: circular soft mask via gl_PointCoord dist; color uniform; alpha = mask * vAlpha;
```
Update aSize/aAlpha/positions each frame in JS. ~120 sparks. Good.

Moon: create canvas texture radial gradient for glow sprite + a circle mesh (CircleGeometry radius 3) with MeshBasicMaterial color pale, positioned at far distance e.g. (60, 45, -80), lookAt center. Plus PointLight? No—directional light from moon position, intensity 0.15, color #8fa8ff. Fog will fade moon? Fog affects MeshBasicMaterial if fog:true default — set material.fog = false for moon and stars so they stay visible. Stars: Points with fog:false.

Sky: scene.background = new THREE.Color(0x050914); plus maybe large sphere with gradient shader — background color + stars is fine. Maybe add subtle gradient via big BackSide sphere with vertex-color gradient. I'll do a simple shader sphere: top #0b1030, horizon #050810... Actually night: top darker (#030612), horizon slightly lighter blue (#0d1530). ShaderMaterial side: BackSide, fog:false, depthWrite:false.

Ground receives shadows. Trees beyond fog density 0.018 fade. Fog color should match horizon: #0a0f1e.

Lights:
- HemisphereLight(0x223355 sky, 0x0a0c12 ground, 0.35) subtle.
- DirectionalLight moon 0x9db4ff 0.25 from moon dir, no shadow (point light does shadows) — or enable shadow for moon? Point light shadow covers local. Keep directional no shadow.
- PointLight fire 0xff7722, intensity ~ 30 (physical units? renderer.useLegacyLights default false in 0.170, so intensity in candela-ish; point light intensity with distance & decay=2: intensity ~ 20-60 works). Set decay 2, distance 30. castShadow true, shadow.mapSize 1024, shadow.bias -0.005, near 0.5, far 25.

Flicker: intensity = 26 + sin(t*11)*4 + sin(t*23+1.7)*3 + random jitter*2; position.x = 0.9 + sin(t*7)*0.05 etc. Flame at y ~1.0 above logs.

Also add glow: Sprite with radial gradient texture, additive, at fire, scale pulsing — nice. Generate glow texture via canvas.

Ember bed: small circle mesh with emissive orange at base + few small emissive boxes? Add a "coal" cluster: few small dodecahedrons with emissive material, plus glow sprite.

Rocks ring: 8 stones around radius 1.1, small dodecahedron radius 0.18-0.28, gray.

Logs: teepee: 3 cylinders length 1.6, radius 0.09, leaning: position at angle 120° apart, tilted ~25° from vertical, tops meeting near (0, 1.1, 0). Compute: place cylinder, rotate. Easier: create log group, set position at base point on circle radius 0.45 at angle, rotation.z? Use lookAt approach: create cylinder along Y, then set quaternion so that its +Y points toward apex. Direction from base to apex: apex - base. quaternion.setFromUnitVectors(up, dir.normalize()). Position at midpoint. Wood material brown flatShading. Add charred tips: small sphere at top with emissive? Skip; add emissive tips via second small cylinder with emissive orange at apex end — maybe skip for simplicity, add inner glow sprite.

Lying logs: 2-3 cylinders rotated to horizontal on ground at radius 1.8-2.5 random angles, slight y=0.09.

Ground vertex displacement: use a simple hash noise: for each vertex, h = sin(x*0.3)*cos(z*0.35)*0.4 + sin(x*0.13+ z*0.17)*0.6 etc, but keep center flat (radius < 3 flatten to 0) so fire area is flat: h *= smoothstep(2.5, 6, dist). Also flatten under trees not needed.

Vertex colors on ground: mix two greens by noise + slightly lighter near fire (warm tint)? Could add warm tint near center via color attribute — nice touch: color lerp toward dark orange near fire radius 3, since light does that anyway with Standard material. Skip vertex colors, rely on lighting. But flatShading standard material with point light gives nice gradient. Good.

Trees: positions: for i in 40: angle random, radius 7 + rand*26 (up to 33), skip if dist to center < 6.5. Tree: trunk + 2-3 cones. castShadow on all meshes. Scale random 0.8–1.6. Also some trees closer (radius 6-8) framing the clearing. Fog density 0.02: at distance 30, exp(-(0.02*30)^2)= exp(-0.36)=0.7 visible; at 33 exp(-0.435)... FogExp2 factor = 1 - exp(-(d*density)^2)? THREE FogExp2: fogFactor = 1 - exp(-density^2 * d^2)? Actually `fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth )`. density 0.02, d=30: (0.6)^2=0.36 → factor 1-e^-0.36=0.30 → 30% fogged. d=40: 0.64 → 47%. Maybe density 0.025. Trees up to 35. Fine — "fades the distant trees". Use 0.022.

Also add a few distant tree silhouettes? Not needed.

Fireflies: small points wandering — adds life; optional, cheap. Add ~20 fireflies with greenish color wandering via sin. Nice but optional; include, subtle.

Overlay UI: title text top-left "Campfire in the Forest — drag to orbit, scroll to zoom". Small, unobtrusive. Also maybe "Click or press a key to start"? Not a game; but the sandbox note says keyboard only after click — we don't need keyboard. Mouse drag in iframe should work. I'll include hint text only.

Auto-rotate: controls.autoRotate = true, speed 0.6 — makes screenshot lively. Damping enabled; update in loop.

Shadow camera for point light: shadow.camera.near/far set. In three 0.170 PointLight shadow camera is CubeCamera; set shadow.camera.near = 0.5, far = 30. Also shadow.bias = -0.004, normalBias maybe 0.02 to avoid acne on flat ground with point light. Use normalBias 0.05? For point light cube shadows acne on ground; set shadow.bias -0.002 and normalBias 0.03.

Renderer: antialias true, setPixelRatio min(devicePixelRatio, 2), shadowMap.type = PCFSoftShadowMap, outputColorSpace default SRGB, toneMapping ACESFilmic, exposure 1.1.

Resize: update camera.aspect, renderer.setSize.

Check three 0.170 API: OrbitControls import from 'three/addons/controls/OrbitControls.js'. renderer.outputColorSpace = THREE.SRGBColorSpace (default). useLegacyLights removed/default false; light intensities: PointLight intensity in candela; with decay 2 default, intensity 30 at distance 2 → 30/4 = 7.5 — quite bright. Let's use intensity ~ 40 with flicker ±. Directional 0.3, hemisphere 0.4. Tone mapping ACES will handle. I'll tune: point light color 0xff8433, intensity base 35.

Flame shader material: MeshBasicMaterial-like ShaderMaterial:

```
uniforms: uTime, uColor (vec3), uAlphaMul
vertex:
uniform float uTime; attribute float aSeed? use uniform per mesh uSeed.
varying float vY;
void main(){
  float h = uHeight; // uniform
  float vY = clamp(position.y / uHeight, 0.0, 1.0);
  vec3 p = position;
  float w = uTime;
  p.x += sin(w*3.0 + vY*6.0 + uSeed) * 0.18 * vY;
  p.z += cos(w*2.3 + vY*5.0 + uSeed*1.7) * 0.15 * vY;
  p.x += sin(w*7.0 + uSeed) * 0.08 * vY * vY;
  vY = vY;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(p,1.0);
}
fragment:
uniform vec3 uColor; uniform float uTime, uSeed; varying float vY;
void main(){
  float a = pow(1.0 - vY, 1.6);
  a *= 0.75 + 0.25*sin(uTime*9.0 + uSeed*3.0 + vY*8.0);
  // soft edges around circumference? cones are solid; additive overlapping fine.
  gl_FragColor = vec4(uColor, a * uAlpha);
}
```
Blending additive, transparent true, depthWrite false, side: DoubleSide? FrontSide fine for cone; DoubleSide gives more volume when seen through? Additive with depthWrite false, render order after opaque. Use side: THREE.DoubleSide for richer look.

Cone geometry: ConeGeometry(radius, height, 7, 3) then translate(0, height/2, 0). uHeight = height.

Colors: outer #ff5a00-ish, mid #ff9a1f, inner #ffd76a, core #fff3c0. Alpha per layer 0.55, 0.6, 0.7, 0.8.

Scale animation per frame: mesh.scale.set(sx, sy, sz) with sy pulsing; also rotation.y += delta*speed.

Sparks: as planned. Spawn at (rand small, 0.9, rand small), velocity up 1.2–2.2 + slight outward, life 1–2.2s, gravity slight negative? Sparks rise then fade; slight wind drift +x. Size 0.05–0.12 world → in shader gl_PointSize = aSize * (200 / -mv.z) maybe; clamp. Alpha = life fraction smooth.

Also smoke? skip.

Moon glow sprite: SpriteMaterial with canvas radial texture, color #cfd8ff, additive, scale 20, position far, fog:false (SpriteMaterial has fog property; set fog:false). Moon disc: CircleGeometry(2.2) basic #f4f1de fog false, plus darker maria? Add second smaller circle slightly offset darker for crescent look? Full moon fine; add subtle crater circles via canvas texture: generate moon texture on canvas with radial gradient + few darker blobs. Good.

Stars: 600 points on dome radius 90 (inside sky sphere radius 120?), positions random on upper hemisphere (y> -5), sizes 1-2.5 with sizeAttenuation false? With attenuation true at distance 90 size huge; use sizeAttenuation:false and size in px 1-2.5. ShaderMaterial or PointsMaterial({size:2, sizeAttenuation:false, transparent, opacity, vertexColors, fog:false, depthWrite:false}). Twinkle: vary material opacity global slight sin — fine, or per-star via shader; global subtle twinkle ok. I'll do slight opacity oscillation.

Sky dome: SphereGeometry(150, 32, 15), ShaderMaterial BackSide:
```
varying vec3 vPos;
vertex: vPos = position; gl_Position...
fragment: float h = normalize(vPos).y; vec3 col = mix(horizon, zenith, pow(max(h,0.0), 0.6)); also below horizon dark.
```
fog:false, depthWrite:false. Radius 150, camera maxDistance 25 so fine. Stars at radius 140.

Ground size 120 to reach fog. Fog will hide edge.

Controls target (0, 1.0, 0).

Max polar angle: Math.PI * 0.495 (just above horizon → camera above ground). minPolarAngle 0.1. minDistance 3, maxDistance 28. enablePan true but limit? Pan could move target below ground; set controls.maxTargetRadius? OrbitControls 0.170 has maxTargetRadius? There's `controls.maxTargetRadius` in newer versions? There is `minTargetRadius`/`maxTargetRadius` added around r169? Not sure. Safer: on change clamp target.y between 0.5 and 6. I'll clamp in loop: target.y = clamp. Also screenSpacePanning false.

Now performance: 40 trees × 4 meshes = 160 draw calls + rest; fine.

Write code carefully.

Ground geometry: PlaneGeometry(140, 140, 60, 60), rotateX(-PI/2), then displace:

```
const pos = g.attributes.position;
const colors = [];
for i: x=pos.getX(i), z=pos.getZ(i); d = Math.hypot(x,z);
let h = (Math.sin(x*0.25)*Math.cos(z*0.3) * 0.5 + Math.sin(x*0.09 + 2.0)*Math.cos(z*0.11)*0.9 + Math.sin(x*0.5+z*0.43)*0.15);
h *= smoothstep(3, 9, d); // flat near fire
pos.setY(i, h);
```
smoothstep helper. Then computeVertexNormals. Material MeshStandardMaterial({color:0x24402a? night dark: 0x1d3320, flatShading:true, roughness:1}). Vertex colors to vary: mix 0x16301c and 0x2a4a2a by noise — need vertexColors:true and material color white. I'll add colors: base lerp between two greens by rand noise, slightly brown near paths? Keep simple two greens + random.

Also scatter small rocks: 12 rocks random radius 3–12, plus ring stones.

Fallen leaves? skip.

Fireflies: Points 25, positions random in radius 3–10, y 0.5–2.5; animate with sin offsets in shader using time uniform and seed attribute — do CPU update simple: each firefly base pos + offset sin(t*speed+phase). Use PointsMaterial size 0.12 additive, color 0x9fe870, transparent, opacity 0.9, depthWrite false, blending additive. CPU update positions each frame — cheap.

Ember coals: 6 small dodecahedron (r 0.06) dark with emissive 0xff4400, emissiveIntensity 2, positions circle radius 0.35 at y 0.05.

Glow sprite at (0, 0.9, 0), scale 3.2 pulsing.

Light position (0, 1.3, 0).

Stones ring: 9 stones, angle i/9*2π, radius 1.15 + rand*0.1, dodecahedron radius 0.16+rand*0.1, scale y 0.6, rotation random, color 0x6b6f75 flatShading, castShadow.

Logs teepee: apex at (0, 1.15, 0). For k in 0..2: base angle = k*2π/3 + 0.4; base pos = (cos*0.5, 0.07, sin*0.5). Cylinder length 1.7 radius 0.085, wood color 0x5a3a22 flatShading, with darker char: use two-tone? single color ok, plus emissive slight? Add second material? Keep one. Position midpoint = (base+apex)/2, quaternion from up vector to dir.

Also cross logs at base: 2 cylinders horizontal crossing at center y 0.06 radius 0.09 length 0.9, rotated y random, charred: color 0x2b1c12 with emissive 0xff3300 intensity 0.6 — burning logs. Good.

Trees function:

```
function makeTree(){
 const g = new THREE.Group();
 const h = 2.5 + Math.random()*3.5;
 trunk: CylinderGeometry(0.12*r, 0.22*r, h*0.45, 5) at y h*0.225, color 0x3a2a1c
 foliage cones: 3 cones: radii [0.9,0.7,0.5]*s, heights [1.6,1.4,1.2]*s stacked from y h*0.35 up.
 color 0x14351f flatShading; slight hue variation via color offset.
 castShadow = true all.
}
```
Vary foliage color: new Color(0x123420).offsetHSL(rand*0.04, 0, rand*0.05-0.02).

Place 38 trees: radius = 6.5 + Math.pow(Math.random(),0.8)*27 → up to ~33. Also 5 closer "frame" trees at radius 5.5–7? clearing edge. Ensure min dist from center 5.5 (not on fire). Count >= 30 ✓.

Also maybe bushes: small icosahedron green near clearing. optional; add 8 bushes.

Now write the animate loop:

```
const clock = new THREE.Clock();
function animate(){
 requestAnimationFrame(animate);
 const dt = Math.min(clock.getDelta(), 0.05);
 const t = clock.elapsedTime;
 // fire flicker
 const n = Math.sin(t*11.3)*0.5 + Math.sin(t*23.7+1.3)*0.3 + Math.sin(t*5.1+0.7)*0.2;
 fireLight.intensity = 34 + n*7 + (Math.random()-0.5)*1.5;
 fireLight.position.set(Math.sin(t*7.7)*0.06, 1.25 + Math.sin(t*13.1)*0.05, Math.cos(t*9.3)*0.06);
 // flames
 flames.forEach((f,i)=>{ const s = 1 + Math.sin(t*(6+i*1.7)+i)*0.18; f.mesh.scale.set(1+Math.sin(t*9+i*2)*0.12, s, 1+Math.cos(t*8+i)*0.12); f.mesh.rotation.y = t*(0.6+i*0.23)+i; });
 // sparks update
 ...
 // glow sprite scale pulse
 glow.scale.setScalar(3 + Math.sin(t*8)*0.25 + n*0.15);
 // fireflies
 // stars twinkle
 controls.update();
 renderer.render(scene, camera);
}
```

Sparks update:

```
for i: life[i] -= dt; if life<=0 reset: pos at fire base random radius 0.15, y 0.7+rand*0.3; vel = (rand-0.5)*0.3, 1.2+rand*1.4, (rand-0.5)*0.3; life = 1+rand*1.5; maxLife=life;
else: vel.y += dt*0.4 (slight accel? sparks decelerate: vel.y -= dt*0.5?) Actually sparks rise fast then slow: vel.y *= (1 - dt*0.6)? Simpler: vel.y -= dt*0.9 (decelerate), vel.x += wind.
pos += vel*dt; alpha = life/maxLife; size shrink.
set attributes needsUpdate.
```
Wind: global windX = sin(t*0.3)*0.2.

Point shader:

```
vertex:
attribute float aSize; attribute float aAlpha; varying float vA;
void main(){ vA = aAlpha; vec4 mv = modelViewMatrix * vec4(position,1.0); gl_PointSize = aSize * (240.0 / -mv.z); gl_Position = projectionMatrix*mv; }
fragment:
uniform vec3 uColor; varying float vA;
void main(){ vec2 c = gl_PointCoord - 0.5; float d = length(c); float m = smoothstep(0.5, 0.05, d); gl_FragColor = vec4(uColor, m*vA); }
```
transparent, depthWrite false, blending additive.

gl_PointSize clamp maybe large when close; clamp: gl_PointSize = min(gl_PointSize, 64.0)? ok add.

Canvas glow texture:

```
function makeGlowTexture(){ canvas 128, radial gradient white center → transparent; return CanvasTexture }
```
Sprite material map, color 0xff7733, blending additive, transparent, depthWrite false, fog false? SpriteMaterial fog param exists — set fog:false.

Moon canvas texture: 256 canvas, fill #f5f2e4, add few gray blobs alpha 0.15. Circle mesh with map, fog false. Plus glow sprite scale 26 color 0xaabbff opacity... use same glow texture tinted.

Lights summary ok.

Sky shader:

```
vertexShader: `varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);}`
fragment: `varying vec3 vP; void main(){ float h = normalize(vP).y; vec3 zen = vec3(0.008,0.012,0.03); vec3 hor = vec3(0.05,0.07,0.13); vec3 col = mix(hor, zen, pow(clamp(h,0.0,1.0), 0.55)); if(h<0.0) col = hor*0.6; gl_FragColor = vec4(col,1.0); }`
```
side BackSide, depthWrite false, fog false.

Stars: BufferGeometry positions: random dir on sphere radius 140, keep y > -10. PointsMaterial({color:0xcfd8ff, size:1.6, sizeAttenuation:false, transparent:true, opacity:0.9, fog:false, depthWrite:false, blending:Additive? normal fine}). Twinkle: starMat.opacity = 0.75+0.25*sin(t*2). Add second smaller star layer brighter few.

Camera: fov 55, near 0.1, far 400. position (9, 4.5, 9).

Overlay: div with title + hint, pointer-events none, font system-ui. Maybe bottom hint. Keep minimal.

Resize handler + initial.

Also handle context: import map exactly as given. Use modules script.

Edge: `renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;`

Point light shadow: `fireLight.castShadow = true; fireLight.shadow.mapSize.set(1024,1024); fireLight.shadow.camera.near=0.4; far=30; fireLight.shadow.bias=-0.0015; fireLight.shadow.normalBias=0.02;`

Ground receiveShadow = true; trees/rocks/logs castShadow = true. Flame meshes castShadow false.

Check: with additive transparent materials and shadows — flame won't cast, fine.

Tree shadow from point light — nice.

Now count draw calls fine.

Write the HTML. Also meta viewport. Body margin 0, overflow hidden, background #050810.

Hint overlay: 
```
<div id="ui"><h1>Night Campfire</h1><p>Drag to orbit · Scroll to zoom · Right-drag to pan</p></div>
```
Styled small, top-left, rgba.

Let me now write everything concretely.

Also clamp controls target each frame:
```
controls.target.y = THREE.MathUtils.clamp(controls.target.y, 0.4, 8);
```

autoRotate true.

One more: "The iframe only receives keyboard events after the user clicks it" — no keyboard needed.

Sparks count 140.

Fireflies count 22.

Let me write flame creation:

```
function makeFlame(radius, height, color, alpha, seed){
 const geo = new THREE.ConeGeometry(radius, height, 7, 4, true);
 geo.translate(0, height/2, 0);
 const mat = new THREE.ShaderMaterial({
   uniforms:{ uTime:{value:0}, uHeight:{value:height}, uSeed:{value:seed}, uColor:{value:new THREE.Color(color)}, uAlpha:{value:alpha} },
   vertexShader:..., fragmentShader:...,
   transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, side:THREE.DoubleSide
 });
 const m = new THREE.Mesh(geo, mat);
 return m;
}
```
openEnded true to avoid bottom cap weirdness.

Flames list: [makeFlame(0.55, 1.7, 0xff4d00, 0.5, 1.3), (0.4,1.35, 0xff7a1a, 0.55, 2.9), (0.26, 1.0, 0xffb52e, 0.65, 4.1), (0.15, 0.65, 0xfff0b0, 0.8, 5.7)] all at y=0.35 (base above logs). Group at fire.

In animate: mat.uniforms.uTime.value = t.

Vertex shader:

```
uniform float uTime; uniform float uHeight; uniform float uSeed;
varying float vY;
void main(){
 float y = clamp(position.y / uHeight, 0.0, 1.0);
 vY = y;
 vec3 p = position;
 p.x += sin(uTime*3.1 + y*6.0 + uSeed) * 0.16 * y;
 p.z += cos(uTime*2.6 + y*5.0 + uSeed*1.7) * 0.14 * y;
 p.x += sin(uTime*6.7 + uSeed*2.0) * 0.10 * y * y;
 p.y += sin(uTime*4.3 + uSeed) * 0.05 * y;
 gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
}
```

Fragment:

```
uniform vec3 uColor; uniform float uTime; uniform float uSeed; uniform float uAlpha;
varying float vY;
void main(){
 float a = pow(1.0 - vY, 1.7);
 a *= 0.72 + 0.28 * sin(uTime*10.0 + uSeed*7.0 + vY*9.0);
 gl_FragColor = vec4(uColor, a * uAlpha);
 if(gl_FragColor.a < 0.01) discard;
}
```

Good.

Sparks geometry attributes: position Float32Array(N*3), aSize, aAlpha. Arrays in JS for velocity/life.

Reset spark:
```
positions[i3]= (Math.random()-0.5)*0.3; y=0.55+Math.random()*0.35; z...
vel: x=(r-0.5)*0.5, y=1.0+Math.random()*1.6, z=(r-0.5)*0.5; life = 0.9+Math.random()*1.6; max=life;
size base = 0.05+Math.random()*0.07;
```
Update:
```
life-=dt; if(life<=0) reset else {
 vel[i3+1] += dt*0.55; // slight upward accel? sparks decelerate upward... use vel.y -= dt*0.35 to slow
 actually: vel.y -= dt*0.5; vel.x += wind*dt where wind = Math.sin(t*0.4)*0.35 + 0.15;
 pos += vel*dt;
 alpha = Math.max(life/max,0); alpha*=alpha? smooth: a = life/max; a = a*a*(3-2a)? just a.
 aSize = base * (0.5+0.5*a);
}
```
Set needsUpdate.

gl_PointSize = aSize * (300.0 / -mv.z) clamped [1, 40]? sizes: aSize ~0.05-0.12, distance ~12 → 0.1*300/12=2.5px small. Want ~3-6px: factor 500. gl_PointSize = aSize * (500.0 / -mv.z). At d=12: 0.1*41=4px good. clamp min 1.5 max 30.

Fireflies: positions updated: for each: pos = base + (sin(t*sp+ph)*1.2, sin(t*sp2+ph2)*0.6, cos(t*sp3+ph3)*1.2). size 0.14, additive, color 0xaef07a, opacity 0.8. PointsMaterial with sizeAttenuation true default — at distance 10, size 0.14 world → px = 0.14 * (height/2)/ (tan(fov/2)*d) ≈ 0.14*400/(0.53*10)=10px too big; set size 0.06 → ~4.5px ok.

Moon position: dir normalize (0.45, 0.5, -0.75)*120 → place disc there, lookAt(0,0,0)? Circle faces +z by default; set mesh.lookAt(0,0,0) after position. Glow sprite at same pos scaled 30, opacity 0.5. DirectionalLight position = moon pos * 0.5? Directional light position (40, 50, -70), target origin.

Fog color: 0x0a0e1a. scene.fog = new THREE.FogExp2(0x0a0e1a, 0.02). Background handled by sky dome; set scene.background = 0x05070f anyway (dome covers).

Ground color at night with only point light nearby: distant ground lit by hemisphere 0.3 — visible dark green, fog fades. Good.

Hemisphere: new THREE.HemisphereLight(0x2a3a66, 0x0c0f0a, 0.5).

Ambient maybe 0x111522, 0.4? Hemisphere covers.

Tone mapping ACESFilmic exposure 1.15.

Now write final HTML. Also ensure no console errors: careful with three r170 API — ConeGeometry, DodecahedronGeometry fine. `THREE.MathUtils.clamp` fine. Sprite scale setScalar fine. CanvasTexture fine.

OrbitControls import path: 'three/addons/controls/OrbitControls.js'.

Write code:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Night Campfire — Low-Poly Forest</title>
<style>
 html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#05070f;}
 #app{position:fixed;inset:0;}
 canvas{display:block;}
 #ui{position:fixed;top:14px;left:16px;color:#cfd6e4;font-family:system-ui,...;pointer-events:none;user-select:none;text-shadow:0 1px 3px rgba(0,0,0,.8);}
 #ui h1{font-size:15px;margin:0 0 4px;font-weight:600;letter-spacing:.4px;color:#ffd9a8;}
 #ui p{font-size:11.5px;margin:0;opacity:.75;}
</style>
<script type="importmap">...</script>
</head>
<body>
<div id="app"></div>
<div id="ui"><h1>🔥 Night Campfire</h1><p>Drag to orbit · Scroll to zoom · Right-drag to pan</p></div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
... scene setup ...
</script>
</body>
</html>
```

Now full JS. Let me write it out carefully.

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

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x05070f);
scene.fog = new THREE.FogExp2(0x0a0e1a, 0.02);

const camera = new THREE.PerspectiveCamera(55, innerWidth/innerHeight, 0.1, 500);
camera.position.set(9.5, 4.6, 9.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 3.5;
controls.maxDistance = 30;
controls.minPolarAngle = 0.12;
controls.maxPolarAngle = Math.PI * 0.495;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.55;
controls.screenSpacePanning = false;
```

Lights:

```js
scene.add(new THREE.HemisphereLight(0x2a3a66, 0x0b0e09, 0.55));

const moonDir = new THREE.Vector3(0.45, 0.42, -0.78).normalize();
const moonLight = new THREE.DirectionalLight(0x9db4ff, 0.35);
moonLight.position.copy(moonDir).multiplyScalar(60);
scene.add(moonLight); scene.add(moonLight.target);

const fireLight = new THREE.PointLight(0xff8433, 36, 34, 2);
fireLight.position.set(0, 1.3, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 32;
fireLight.shadow.bias = -0.0015;
fireLight.shadow.normalBias = 0.03;
scene.add(fireLight);
```

Sky dome + stars + moon:

```js
const sky = new THREE.Mesh(new THREE.SphereGeometry(220, 32, 16), new THREE.ShaderMaterial({...side:BackSide, depthWrite:false, fog:false}));
scene.add(sky);
```
Note ShaderMaterial ignores fog by default? ShaderMaterial has fog:false default unless fog:true and includes. Default fog false. Good, but set explicitly anyway not needed.

Stars:
```js
function makeStars(count, radius, size, opacity){
 const pos = new Float32Array(count*3);
 for(...){ random unit vector; p = dir*radius*(0.9+0.2rand); if(p.y<-20) p.y = Math.abs(p.y)*0.3? simpler: generate with y = Math.abs? Use: theta random, phi limited: y = radius*Math.sin? Let's do: 
   const v = new THREE.Vector3().randomDirection? r170 has randomDirection? Vector3.randomDirection exists since r131? yes exists. then scale radius; if (v.y < 0.02) v.y = Math.abs(v.y)*0.5+0.02? Simplest: v.y = Math.abs(v.y)*0.9 + 0.05;
 }
 geometry, PointsMaterial({color, size, sizeAttenuation:false, transparent:true, opacity, depthWrite:false, fog:false})
}
```
Two layers: 500 stars size 1.5 opacity .85; 60 bright size 2.6 opacity 1 color 0xffffff.

Twinkle: store mats, opacity = base*(0.8+0.2 sin).

Moon:
```js
const moonCanvas = document.createElement('canvas'); 256; ctx; fill '#f2eeda'; blobs: for 12 random circles fillStyle rgba(180,180,170,0.25) radius 8-30.
const moonTex = new THREE.CanvasTexture(moonCanvas);
const moon = new THREE.Mesh(new THREE.CircleGeometry(7, 40), new THREE.MeshBasicMaterial({map:moonTex, fog:false}));
moon.position.copy(moonDir).multiplyScalar(190);
moon.lookAt(0,0,0); scene.add(moon);
const moonGlow = new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex, color:0x8fa5e8, transparent:true, opacity:0.35, blending:Additive, depthWrite:false, fog:false}));
moonGlow.position.copy(moon.position); moonGlow.scale.setScalar(55); scene.add(moonGlow);
```
glowTex defined before.

Ground:

```js
const groundGeo = new THREE.PlaneGeometry(160, 160, 70, 70);
groundGeo.rotateX(-Math.PI/2);
const gp = groundGeo.attributes.position;
const gcol = new Float32Array(gp.count*3);
const cA = new THREE.Color(0x14301b), cB = new THREE.Color(0x274a26), cC = new THREE.Color(0x0e2417);
for(let i=0;i<gp.count;i++){
 const x=gp.getX(i), z=gp.getZ(i);
 const d=Math.hypot(x,z);
 let h = Math.sin(x*0.22)*Math.cos(z*0.27)*0.55 + Math.sin(x*0.08+1.7)*Math.cos(z*0.1+0.6)*1.0 + Math.sin(x*0.47+z*0.39)*0.18;
 const s = smoothstep(2.6, 8.5, d);
 h*=s;
 gp.setY(i, h);
 const n = 0.5+0.5*Math.sin(x*0.6+z*0.8);
 const c = cA.clone().lerp(cB, n);
 if(Math.random()<0.06) c.lerp(cC, 0.7);
 gcol[i*3]=c.r; gcol[i*3+1]=c.g; gcol[i*3+2]=c.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);
```
smoothstep helper: `const smoothstep=(a,b,x)=>{const t=Math.min(1,Math.max(0,(x-a)/(b-a)));return t*t*(3-2*t);};`

Trees:

```js
const trunkMat = new THREE.MeshStandardMaterial({color:0x3a2a1a, flatShading:true, roughness:1});
function makeTree(scale){
 const g = new THREE.Group();
 const h = (2.6 + Math.random()*3.2) * scale;
 const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.10*scale, 0.22*scale, h*0.5, 5), trunkMat);
 trunk.position.y = h*0.25; trunk.castShadow = true; g.add(trunk);
 const fcol = new THREE.Color(0x14401f).offsetHSL(Math.random()*0.05-0.025, 0.05, Math.random()*0.06-0.03);
 const fmat = new THREE.MeshStandardMaterial({color:fcol, flatShading:true, roughness:1});
 let y = h*0.32;
 const layers = 3;
 for(let i=0;i<layers;i++){
   const r = (1.15 - i*0.32)*scale*(0.9+Math.random()*0.2);
   const ch = (1.5 - i*0.3)*scale;
   const cone = new THREE.Mesh(new THREE.ConeGeometry(r, ch, 6), fmat);
   cone.position.y = y + ch/2; cone.castShadow = true; g.add(cone);
   y += ch*0.62;
 }
 g.userData.baseY = 0;
 return g;
}
const trees = [];
for(let i=0;i<38;i++){
 const a = Math.random()*Math.PI*2;
 const r = 6.2 + Math.pow(Math.random(), 0.75)*27;
 const t = makeTree(0.85 + Math.random()*0.8);
 t.position.set(Math.cos(a)*r, 0, Math.sin(a)*r);
 t.rotation.y = Math.random()*Math.PI*2;
 // ground height at position: compute same formula? approximate: sample h
 t.position.y = groundHeight(t.position.x, t.position.z);
 scene.add(t); trees.push(t);
}
```
Need groundHeight function shared with displacement: define function groundHeight(x,z){...same formula...}. Refactor: use it in geometry loop too. 

Bushes:
```js
for(let i=0;i<10;i++){ const b=new THREE.Mesh(new THREE.IcosahedronGeometry(0.3+Math.random()*0.35,0), bushMat green); pos radius 2.5–6, y = h+0.15; castShadow; }
```

Rocks:
```js
const rockMat = new THREE.MeshStandardMaterial({color:0x707580, flatShading:true, roughness:0.95});
for(let i=0;i<14;i++){ a,r=3+r*9; rock dodecahedron radius 0.15+rand*0.4; scale y 0.55+rand*0.3; rotation random; y=groundHeight+ r*0.3; cast/receive shadow }
```

Fire ring stones:
```js
for(let i=0;i<9;i++){ a=i/9*2π + rand*0.3; r=1.15+rand*0.12; stone dodeca(0.17+rand*0.1) scale(1,0.6+rand*0.3,1); pos y = gh + 0.06; }
```

Logs:
```js
const woodMat = new THREE.MeshStandardMaterial({color:0x5b3b21, flatShading:true, roughness:1});
const charMat = new THREE.MeshStandardMaterial({color:0x1c120b, flatShading:true, roughness:1, emissive:0xff4400, emissiveIntensity:0.9});
function log(len, rad, mat){ const m=new THREE.Mesh(new THREE.CylinderGeometry(rad, rad*1.15, len, 7), mat); m.castShadow=true; return m; }
// teepee
const apex = new THREE.Vector3(0, 1.25, 0);
for(let k=0;k<3;k++){
 const a = k*Math.PI*2/3 + 0.5;
 const base = new THREE.Vector3(Math.cos(a)*0.5, 0.06, Math.sin(a)*0.5);
 const dir = apex.clone().sub(base).normalize();
 const L = base.distanceTo(apex) + 0.35;
 const m = log(L, 0.085, k===0? charMat: woodMat);
 m.position.copy(base).add(apex).multiplyScalar(0.5);
 m.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), dir);
 scene.add(m);
}
// lying logs
const lying = [[1.9, 0.8],[2.4, 2.6],[2.1, 4.4]];
lying.forEach(([r,a],i)=>{
 const m = log(1.1, 0.1, i%2? woodMat: charMat);
 m.position.set(Math.cos(a)*r, 0.1, Math.sin(a)*r);
 m.rotation.y = a + Math.PI/2 + (Math.random()-0.5)*0.4;
 m.rotation.z = (Math.random()-0.5)*0.06;
 scene.add(m);
});
```
Wait lying log rotation: cylinder axis is Y; to lay flat rotate z by π/2 then rotation.y... Euler order XYZ: set rotation.set(0, a, Math.PI/2)? That rotates around z after y? Euler XYZ applies... simpler: m.rotation.order? Use quaternion: first rotate so axis horizontal: q1 = setFromUnitVectors(up, (1,0,0)) then rotate around Y by angle. Compose: m.quaternion.setFromUnitVectors(up, new Vector3(Math.cos(a),0,Math.sin(a)))? That maps Y axis to horizontal direction — cylinder lies along that direction. 

```
m.quaternion.setFromUnitVectors(new THREE.Vector3(0,1,0), new THREE.Vector3(Math.cos(a), 0.02, Math.sin(a)).normalize());
```
Position y = 0.1. Good.

Coals:
```js
const coalMat = new THREE.MeshStandardMaterial({color:0x140b06, emissive:0xff5a00, emissiveIntensity:2.2, roughness:1});
for(let i=0;i<7;i++){ a=rand*2π, r=0.15+rand*0.3; c = Mesh(IcosahedronGeometry(0.07+rand*0.05,0), coalMat); c.position.set(cos*r, 0.05, sin*r); c.rotation random; scene.add }
```

Flames + glow as planned. Flame group position y 0.32.

Sparks, fireflies as planned.

Animate loop with all updates.

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

Check groundHeight used before defined — define function first.

Flame render order: set mesh.renderOrder = 10; sparks renderOrder 11; glow sprite renderOrder 12. With depthWrite false and additive, fine.

Also glow sprite: 
```js
const glow = new THREE.Sprite(new THREE.SpriteMaterial({map:glowTex, color:0xff8033, transparent:true, opacity:0.55, blending:THREE.AdditiveBlending, depthWrite:false, fog:false}));
glow.position.set(0, 0.85, 0); glow.scale.setScalar(3.4); glow.renderOrder=12;
```

Ember light flicker also affects glow scale.

Now spark reset positions relative to flame base y ~0.55.

Fireflies base positions: radius 2.5–9, y 0.6–2.2; store base array; update.

Also clamp controls.target.y each frame after update? controls.update() may change target with pan; clamp after: 
```
controls.update();
controls.target.y = THREE.MathUtils.clamp(controls.target.y, 0.3, 10);
controls.target.x = clamp(..., -20, 20); z similarly.
```

Star twinkle: keep array starMats.

Let me also double-check ConeGeometry openEnded param: ConeGeometry(radius, height, radialSegments, heightSegments, openEnded). Yes.

ShaderMaterial for flames: need `uniforms` with THREE.Color. Fine.

One concern: additive flame cones with DoubleSide — inner faces add brightness, ok.

Point light decay 2 default; intensity 36, distance 34. At d=1.3, illuminance = 36/1.69 ≈ 21 — with ACES will be bright hot. Maybe 30 base. Flicker 30 + n*6. Ground at d=2: 30/4=7.5 bright. ok tune base 32.

Hemisphere 0.55 might make ground too visible blue; night ok. Keep 0.45.

Directional 0.35 moonlight gives cool fill and tree visibility from moon side.

Now stars randomDirection: exists in r170? Vector3.randomDirection() added in r129? I believe yes (r126+). To be safe, implement manually:
```
const v = new THREE.Vector3(Math.random()*2-1, Math.random()*2-1, Math.random()*2-1);
if(v.lengthSq()===0||v.lengthSq()>1) i-- ... 
```
Simpler: 
```
let x,y,z; do{ x=Math.random()*2-1; y=Math.random()*2-1; z=Math.random()*2-1; }while(x*x+y*y+z*z>1 || x*x+y*y+z*z===0);
v.set(x,y,z).normalize();
if(v.y<0) v.y = -v.y*0.4+0.03; // keep above horizon-ish
```
Actually just require v.y > -0.1: loop while(v.y < -0.1). Fine.

Star radius 200 (inside sky 220).

Moon at 190 within sky 220, camera far 500 fine.

Fog affects moon? fog:false set. Sky shader fog default false for ShaderMaterial. Stars fog:false. Glow sprites fog:false.

Trees at r up to 33.2; ground extends to 80 (plane 160). Fog 0.02: at 33, factor=1-exp(-(0.02*33)^2)=1-exp(-0.4356)=0.353. Visible fade ✓. Maybe increase density 0.024 → at 33: (0.792)^2=0.627→46% fog. ok use 0.022: (0.726)^2=.527→41%. Use 0.022.

Camera maxDistance 30 — can't see far trees much but they frame edges. Fine.

Also add subtle fire reflection on stones? skip.

Now write the sparks shader code and buffers.

```js
const SPARKS = 130;
const sGeo = new THREE.BufferGeometry();
const sPos = new Float32Array(SPARKS*3);
const sSize = new Float32Array(SPARKS);
const sAlpha = new Float32Array(SPARKS);
const sVel = new Float32Array(SPARKS*3);
const sLife = new Float32Array(SPARKS);
const sMax = new Float32Array(SPARKS);
const sBase = new Float32Array(SPARKS);
sGeo.setAttribute('position', new THREE.BufferAttribute(sPos,3));
sGeo.setAttribute('aSize', new THREE.BufferAttribute(sSize,1));
sGeo.setAttribute('aAlpha', new THREE.BufferAttribute(sAlpha,1));
function resetSpark(i, t){
 const a=Math.random()*Math.PI*2, r=Math.random()*0.22;
 sPos[i*3]=Math.cos(a)*r; sPos[i*3+1]=0.5+Math.random()*0.4; sPos[i*3+2]=Math.sin(a)*r;
 sVel[i*3]=(Math.random()-0.5)*0.4; sVel[i*3+1]=1.1+Math.random()*1.7; sVel[i*3+2]=(Math.random()-0.5)*0.4;
 sLife[i]=sMax[i]=0.8+Math.random()*1.6; sBase[i]=0.05+Math.random()*0.08;
 sSize[i]=sBase[i]; sAlpha[i]=1;
}
for(let i=0;i<SPARKS;i++) resetSpark(i);
const sMat = new THREE.ShaderMaterial({ uniforms:{uColor:{value:new THREE.Color(0xffb050)}}, vertexShader:..., fragmentShader:..., transparent:true, depthWrite:false, blending:THREE.AdditiveBlending });
const sparks = new THREE.Points(sGeo, sMat); sparks.renderOrder=11; sparks.frustumCulled=false; scene.add(sparks);
```
frustumCulled false since positions change.

Update in loop:
```
const wind = Math.sin(t*0.35)*0.3 + 0.12;
for(let i=0;i<SPARKS;i++){
 sLife[i]-=dt;
 if(sLife[i]<=0){ resetSpark(i); }
 else{
  sVel[i*3+1] -= dt*0.45;           // drag
  sVel[i*3] += wind*dt*0.6;
  sVel[i*3+2] += Math.cos(t*0.5+i)*dt*0.15;
  sPos[i*3]+=sVel[i*3]*dt; sPos[i*3+1]+=sVel[i*3+1]*dt; sPos[i*3+2]+=sVel[i*3+2]*dt;
  const f = sLife[i]/sMax[i];
  sAlpha[i]=f*f; sSize[i]=sBase[i]*(0.4+0.6*f);
 }
}
sGeo.attributes.position.needsUpdate=true; ... aSize, aAlpha
```

Fireflies:
```
const FF=22; positions base arrays; geometry; PointsMaterial({color:0xaef07a, size:0.07, transparent:true, opacity:0.85, blending:Additive, depthWrite:false, sizeAttenuation:true, fog:false});
update: pos = base + offsets sin.
```

Flame list with uniforms refs to update uTime.

Glow pulse: `glow.scale.setScalar(3.2 + Math.sin(t*7.3)*0.25 + n*0.35); glow.material.opacity = 0.45 + n*0.08;`

Flame mesh scale pulse as described.

OK, also initial camera lookAt via controls target.

Write the final file now. Also make sure `#app` fills. Add `touch-action:none` on canvas via OrbitControls default.

Double-check ShaderMaterial flame vertex uses `position` attribute — fine. Need `uniform float uHeight` etc.

Potential pitfall: In fragment shader for sparks, use gl_PointCoord; with additive blending and premultiplied? fine.

Edge: `renderer.useLegacyLights` — in r170 property removed? In r155 default false, deprecated warning if set; don't set.

PointLight(color, intensity, distance, decay) — decay default 2. ok.

Shadow on point light with many casters — 40 trees × 4 meshes = fine.

One more: ground plane 160 wide but fog hides beyond ~40; sky radius 220 > camera far 500 fine.

Let me also ensure trees not inside fire: min radius 6.2 ✓ (fire radius ~1.3).

Write HTML fully.

Also the UI hint mentions controls; fine.

Let me write out and mentally check syntax.

For flame ShaderMaterial strings use template literals.

Final check on import map exact string:
```
<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>
```

Now produce final answer: single html code block.