Models 44

DeepSeek V4.1 Flash 1017

by DeepSeekReasoningClean
Open in new window

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

How it went

Total time
1 min 9 s
Cost
$0.011
Output tokens
19,384
Spent thinking
13,153 tokens
Speed
283.5 tok/s
Console errors
0
First token after
497 ms
First visible text after
51.3 s
Prompt tokens
598
Code size
19.0 KB
Finish reason
stop
Served by
StepFun
Model id
deepseek/deepseek-v4.1-flash-1017
Reasoning sent
{"effort":"low"}
Max output
64,000 tokens
Generated
Nov 11, 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 · 533 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Campfire — Low-Poly Forest Clearing</title>
<style>
  html, body { margin: 0; padding: 0; height: 100%; overflow: hidden; background: #05070d; }
  canvas { display: block; }
  #hint {
    position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%);
    color: #b9c8ee; font: 12px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
    letter-spacing: .08em; text-transform: uppercase; opacity: .45;
    pointer-events: none; user-select: none; white-space: nowrap;
  }
</style>
</head>
<body>
<div id="hint">Drag to look around · Scroll to zoom</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';

/* ------------------------------------------------------------------ */
/*  Renderer                                                           */
/* ------------------------------------------------------------------ */
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;
document.body.appendChild(renderer.domElement);

/* ------------------------------------------------------------------ */
/*  Scene & camera                                                     */
/* ------------------------------------------------------------------ */
const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a1120, 20, 78);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 800);
camera.position.set(6.6, 4.4, 8.6);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.15, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.07;
controls.enablePan = false;
controls.minDistance = 3.5;
controls.maxDistance = 26;
controls.minPolarAngle = 0.28;
controls.maxPolarAngle = Math.PI * 0.49;   // keep us above the ground
controls.autoRotate = true;
controls.autoRotateSpeed = 0.5;
controls.addEventListener('start', () => { controls.autoRotate = false; });
controls.update();

/* ------------------------------------------------------------------ */
/*  Helpers                                                            */
/* ------------------------------------------------------------------ */
function makeGlowTexture(stops, size = 128) {
  const c = document.createElement('canvas');
  c.width = c.height = size;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
  for (const [p, col] of stops) g.addColorStop(p, col);
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, size, size);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}

const sparkTexture = makeGlowTexture([
  [0.0, 'rgba(255,255,255,1)'],
  [0.28, 'rgba(255,214,140,0.95)'],
  [0.62, 'rgba(255,120,26,0.35)'],
  [1.0, 'rgba(255,60,0,0)']
], 64);

const moonGlowTexture = makeGlowTexture([
  [0.0, 'rgba(225,238,255,0.95)'],
  [0.22, 'rgba(150,190,255,0.38)'],
  [0.55, 'rgba(80,120,220,0.10)'],
  [1.0, 'rgba(40,70,160,0)']
], 256);

/* ------------------------------------------------------------------ */
/*  Night sky dome                                                     */
/* ------------------------------------------------------------------ */
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide,
  depthWrite: false,
  uniforms: {
    topColor:    { value: new THREE.Color(0x02040b) },
    bottomColor: { value: new THREE.Color(0x14203a) }
  },
  vertexShader: /* glsl */`
    varying float vH;
    void main() {
      vH = normalize(position).y;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */`
    uniform vec3 topColor;
    uniform vec3 bottomColor;
    varying float vH;
    void main() {
      float t = smoothstep(-0.08, 0.62, vH);
      gl_FragColor = vec4(mix(bottomColor, topColor, t), 1.0);
      #include <tonemapping_fragment>
      #include <colorspace_fragment>
    }
  `
});
const sky = new THREE.Mesh(new THREE.SphereGeometry(340, 32, 20), skyMat);
sky.renderOrder = -1;
scene.add(sky);
scene.background = new THREE.Color(0x03050c);

/* ------------------------------------------------------------------ */
/*  Stars                                                              */
/* ------------------------------------------------------------------ */
{
  const N = 1600;
  const positions = new Float32Array(N * 3);
  const colors = new Float32Array(N * 3);
  const R = 260;
  for (let i = 0; i < N; i++) {
    const u = Math.random() * 1.02 - 0.02;          // vertical bias
    const theta = Math.random() * Math.PI * 2;
    const s = Math.sqrt(Math.max(0, 1 - u * u));
    positions[i * 3]     = Math.cos(theta) * s * R;
    positions[i * 3 + 1] = u * R;
    positions[i * 3 + 2] = Math.sin(theta) * s * R;

    const b = 0.55 + Math.random() * 0.45;
    const warm = Math.random();
    colors[i * 3]     = b * (0.85 + 0.15 * warm);
    colors[i * 3 + 1] = b * 0.92;
    colors[i * 3 + 2] = b * (1.0 - 0.12 * warm);
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.BufferAttribute(positions, 3));
  g.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  const m = new THREE.PointsMaterial({
    size: 1.7, sizeAttenuation: false, vertexColors: true,
    transparent: true, opacity: 0.95, depthWrite: false, fog: false
  });
  scene.add(new THREE.Points(g, m));
}

/* ------------------------------------------------------------------ */
/*  Moon                                                               */
/* ------------------------------------------------------------------ */
const moonDir = new THREE.Vector3(-0.62, 0.46, -0.64).normalize();
const moonPos = moonDir.clone().multiplyScalar(210);
{
  const moon = new THREE.Mesh(
    new THREE.SphereGeometry(8, 24, 16),
    new THREE.MeshBasicMaterial({ color: 0xf2f4e2, fog: false })
  );
  moon.position.copy(moonPos);
  scene.add(moon);

  const glow = new THREE.Sprite(new THREE.SpriteMaterial({
    map: moonGlowTexture, color: 0xbcd4ff, transparent: true,
    blending: THREE.AdditiveBlending, depthWrite: false, opacity: 0.7, fog: false
  }));
  glow.position.copy(moonPos);
  glow.scale.setScalar(95);
  scene.add(glow);
}

/* ------------------------------------------------------------------ */
/*  Lights                                                             */
/* ------------------------------------------------------------------ */
scene.add(new THREE.HemisphereLight(0x2b3f63, 0x070b07, 0.55));

const moonLight = new THREE.DirectionalLight(0x9db6ff, 0.55);
moonLight.position.copy(moonDir.clone().multiplyScalar(40));
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xff8a3c, 42, 34, 2);
fireLight.position.set(0, 1.15, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 30;
fireLight.shadow.bias = -0.004;
fireLight.shadow.normalBias = 0.02;
scene.add(fireLight);

/* ------------------------------------------------------------------ */
/*  Ground                                                             */
/* ------------------------------------------------------------------ */
{
  const geo = new THREE.PlaneGeometry(150, 150, 72, 72);
  geo.rotateX(-Math.PI / 2);
  const pos = geo.attributes.position;
  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i);
    const z = pos.getZ(i);
    const d = Math.sqrt(x * x + z * z);
    const k = THREE.MathUtils.smoothstep(d, 3.2, 26);
    const h = (Math.sin(x * 0.21) * Math.cos(z * 0.17) + Math.sin((x + z) * 0.09) * 1.35) * 0.55;
    pos.setY(i, h * k + (Math.random() - 0.5) * 0.34 * k);
  }
  geo.computeVertexNormals();

  const ground = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({
    color: 0x33402c, roughness: 1, metalness: 0, flatShading: true
  }));
  ground.receiveShadow = true;
  scene.add(ground);
}

/* ------------------------------------------------------------------ */
/*  Shared materials                                                   */
/* ------------------------------------------------------------------ */
const barkMat  = new THREE.MeshStandardMaterial({ color: 0x4a3625, roughness: 1, flatShading: true });
const rockMat  = new THREE.MeshStandardMaterial({ color: 0x5a6060, roughness: 1, flatShading: true });
const stoneMat = new THREE.MeshStandardMaterial({ color: 0x6c6a63, roughness: 0.95, flatShading: true });
const leafMats = [0x1b3a29, 0x22472f, 0x17331f, 0x274d34].map(
  c => new THREE.MeshStandardMaterial({ color: c, roughness: 1, flatShading: true })
);

/* ------------------------------------------------------------------ */
/*  Trees                                                              */
/* ------------------------------------------------------------------ */
function makeTree() {
  const g = new THREE.Group();
  const s = 0.85 + Math.random() * 1.35;

  const trunkH = 1.7 * s;
  const trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.13 * s, 0.24 * s, trunkH, 5),
    barkMat
  );
  trunk.position.y = trunkH / 2;
  g.add(trunk);

  const mat = leafMats[(Math.random() * leafMats.length) | 0];
  let y = trunkH * 0.68;
  let r = 1.2 * s;
  let h = 2.0 * s;
  for (let i = 0; i < 3; i++) {
    const cone = new THREE.Mesh(new THREE.ConeGeometry(r, h, 6, 1), mat);
    cone.position.y = y + h / 2;
    cone.rotation.y = Math.random() * Math.PI;
    g.add(cone);
    y += h * 0.42;
    r *= 0.72;
    h *= 0.78;
  }
  return g;
}

{
  let placed = 0, attempts = 0;
  while (placed < 46 && attempts < 3000) {
    attempts++;
    const a = Math.random() * Math.PI * 2;
    const rad = 11 + Math.pow(Math.random(), 0.72) * 31;
    const x = Math.cos(a) * rad;
    const z = Math.sin(a) * rad;
    if (Math.hypot(x, z) < 10.5) continue;

    const tree = makeTree();
    tree.position.set(x, -0.15, z);
    tree.rotation.y = Math.random() * Math.PI * 2;
    if (rad < 15) {
      tree.traverse(o => { if (o.isMesh) o.castShadow = true; });
    }
    scene.add(tree);
    placed++;
  }
}

/* ------------------------------------------------------------------ */
/*  Rocks & scattered logs                                             */
/* ------------------------------------------------------------------ */
for (let i = 0; i < 16; i++) {
  const a = Math.random() * Math.PI * 2;
  const r = 3.2 + Math.random() * 10;
  const s = 0.26 + Math.random() * 0.72;
  const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(s, 0), rockMat);
  rock.position.set(Math.cos(a) * r, s * 0.42, Math.sin(a) * r);
  rock.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
  rock.scale.set(1 + Math.random() * 0.55, 0.6 + Math.random() * 0.55, 1 + Math.random() * 0.55);
  rock.castShadow = true;
  rock.receiveShadow = true;
  scene.add(rock);
}

for (let i = 0; i < 5; i++) {
  const a = (i / 5) * Math.PI * 2 + 0.9;
  const r = 2.5 + Math.random() * 1.9;
  const len = 1.5 + Math.random() * 1.3;
  const holder = new THREE.Group();
  const log = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.19, len, 6), barkMat);
  log.rotation.z = Math.PI / 2;
  log.castShadow = true;
  log.receiveShadow = true;
  holder.add(log);
  holder.position.set(Math.cos(a) * r, 0.19, Math.sin(a) * r);
  holder.rotation.y = Math.random() * Math.PI;
  scene.add(holder);
}

/* ------------------------------------------------------------------ */
/*  Campfire                                                           */
/* ------------------------------------------------------------------ */
const fireGroup = new THREE.Group();
scene.add(fireGroup);

// glowing ember bed
const bed = new THREE.Mesh(
  (() => { const g = new THREE.CircleGeometry(1.02, 14); g.rotateX(-Math.PI / 2); return g; })(),
  new THREE.MeshBasicMaterial({
    color: 0xff5c12, transparent: true, opacity: 0.55,
    blending: THREE.AdditiveBlending, depthWrite: false, fog: false
  })
);
bed.position.y = 0.055;
fireGroup.add(bed);

// ring of stones
for (let i = 0; i < 13; i++) {
  const a = (i / 13) * Math.PI * 2;
  const s = 0.15 + Math.random() * 0.11;
  const st = new THREE.Mesh(new THREE.IcosahedronGeometry(s, 0), stoneMat);
  st.position.set(Math.cos(a) * 1.28, s * 0.5, Math.sin(a) * 1.28);
  st.rotation.set(Math.random() * 3, Math.random() * 3, Math.random() * 3);
  st.scale.set(1, 0.72 + Math.random() * 0.5, 1);
  st.castShadow = true;
  st.receiveShadow = true;
  fireGroup.add(st);
}

// teepee logs
{
  const tiltAxis = new THREE.Vector3();
  for (let i = 0; i < 6; i++) {
    const a = (i / 6) * Math.PI * 2 + 0.35;
    const len = 1.55 + Math.random() * 0.35;
    const holder = new THREE.Group();
    const log = new THREE.Mesh(new THREE.CylinderGeometry(0.085, 0.115, len, 6), barkMat);
    log.position.y = len / 2;
    log.castShadow = true;
    log.receiveShadow = true;
    holder.add(log);
    holder.position.set(Math.cos(a) * 0.52, 0.02, Math.sin(a) * 0.52);
    tiltAxis.set(-Math.sin(a), 0, Math.cos(a));
    holder.quaternion.setFromAxisAngle(tiltAxis, 0.34 + Math.random() * 0.08);
    fireGroup.add(holder);
  }
}

// flames
const flames = [];
for (let i = 0; i < 9; i++) {
  const h = 1.0 + Math.random() * 1.15;
  const rad = 0.17 + Math.random() * 0.19;
  const geo = new THREE.ConeGeometry(rad, h, 5, 1);
  geo.translate(0, h / 2, 0);
  const col = new THREE.Color().setHSL(0.055 + Math.random() * 0.06, 1.0, 0.48 + Math.random() * 0.14);
  const mat = new THREE.MeshBasicMaterial({
    color: col, transparent: true, opacity: 0.52,
    blending: THREE.AdditiveBlending, depthWrite: false,
    side: THREE.DoubleSide, fog: false
  });
  const flame = new THREE.Mesh(geo, mat);
  const ang = Math.random() * Math.PI * 2;
  const rr = Math.random() * 0.33;
  flame.position.set(Math.cos(ang) * rr, 0.07, Math.sin(ang) * rr);
  flame.userData = {
    baseX: flame.position.x,
    baseZ: flame.position.z,
    phase: Math.random() * 10,
    sp: 2.0 + Math.random() * 2.6,
    hs: 0.82 + Math.random() * 0.5
  };
  fireGroup.add(flame);
  flames.push(flame);
}

// bright core
const core = new THREE.Mesh(
  new THREE.IcosahedronGeometry(0.3, 0),
  new THREE.MeshBasicMaterial({
    color: 0xfff0c0, transparent: true, opacity: 0.85,
    blending: THREE.AdditiveBlending, depthWrite: false, fog: false
  })
);
core.position.y = 0.36;
fireGroup.add(core);

/* ------------------------------------------------------------------ */
/*  Sparks                                                             */
/* ------------------------------------------------------------------ */
const SPARK_COUNT = 170;
const sparkPos  = new Float32Array(SPARK_COUNT * 3);
const sparkCol  = new Float32Array(SPARK_COUNT * 3);
const sparkVel  = new Float32Array(SPARK_COUNT * 3);
const sparkLife = new Float32Array(SPARK_COUNT);
const sparkMax  = new Float32Array(SPARK_COUNT);

function resetSpark(i, initial) {
  const a = Math.random() * Math.PI * 2;
  const r = Math.random() * 0.55;
  sparkPos[i * 3]     = Math.cos(a) * r;
  sparkPos[i * 3 + 1] = 0.22 + Math.random() * 0.55;
  sparkPos[i * 3 + 2] = Math.sin(a) * r;

  sparkVel[i * 3]     = (Math.random() - 0.5) * 0.45;
  sparkVel[i * 3 + 1] = 1.1 + Math.random() * 1.9;
  sparkVel[i * 3 + 2] = (Math.random() - 0.5) * 0.45;

  sparkMax[i]  = 1.1 + Math.random() * 1.9;
  sparkLife[i] = initial ? Math.random() * sparkMax[i] : 0;
}

for (let i = 0; i < SPARK_COUNT; i++) resetSpark(i, true);

const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkCol, 3));
const sparkMat = new THREE.PointsMaterial({
  size: 0.17, map: sparkTexture, vertexColors: true,
  transparent: true, blending: THREE.AdditiveBlending,
  depthWrite: false, sizeAttenuation: true, fog: false
});
const sparks = new THREE.Points(sparkGeo, sparkMat);
sparks.frustumCulled = false;
scene.add(sparks);

/* ------------------------------------------------------------------ */
/*  Resize                                                             */
/* ------------------------------------------------------------------ */
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

/* ------------------------------------------------------------------ */
/*  Animation loop                                                     */
/* ------------------------------------------------------------------ */
const clock = new THREE.Clock();

function animate() {
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;

  // ---- flames ----
  for (const f of flames) {
    const u = f.userData;
    const w  = Math.sin(t * u.sp + u.phase);
    const w2 = Math.sin(t * u.sp * 0.63 + u.phase * 1.7);
    f.scale.set(
      1 + w * 0.13,
      (0.72 + 0.42 * (0.5 + 0.5 * w2)) * u.hs,
      1 + w2 * 0.13
    );
    f.position.x = u.baseX + w * 0.065;
    f.position.z = u.baseZ + w2 * 0.065;
    f.rotation.y = t * 0.7 + u.phase;
  }

  core.scale.setScalar(0.85 + Math.sin(t * 9.3) * 0.12 + Math.sin(t * 14.7) * 0.07);
  core.position.y = 0.34 + Math.sin(t * 6.1) * 0.03;
  bed.material.opacity = 0.42 + Math.sin(t * 5.2) * 0.08 + Math.sin(t * 11.7) * 0.05;

  // ---- fire light flicker ----
  const flicker =
    1 +
    Math.sin(t * 11.3) * 0.10 +
    Math.sin(t * 7.7 + 1.3) * 0.08 +
    Math.sin(t * 19.1 + 2.1) * 0.05 +
    Math.sin(t * 3.1 + 0.4) * 0.06;
  fireLight.intensity = 42 * flicker;
  fireLight.position.x = Math.sin(t * 9.1) * 0.09;
  fireLight.position.z = Math.cos(t * 7.3) * 0.09;
  fireLight.position.y = 1.12 + Math.sin(t * 13.4) * 0.07;

  // ---- sparks ----
  for (let i = 0; i < SPARK_COUNT; i++) {
    sparkLife[i] += dt;
    if (sparkLife[i] >= sparkMax[i]) resetSpark(i, false);

    const i3 = i * 3;
    // rise, slowed by drag
    sparkVel[i3 + 1] *= (1 - 0.55 * dt);
    sparkVel[i3]     += Math.sin(t * 2.3 + i) * 0.35 * dt;
    sparkVel[i3 + 2] += Math.cos(t * 2.1 + i * 1.3) * 0.35 * dt;

    sparkPos[i3]     += sparkVel[i3] * dt;
    sparkPos[i3 + 1] += sparkVel[i3 + 1] * dt;
    sparkPos[i3 + 2] += sparkVel[i3 + 2] * dt;

    const life = 1 - sparkLife[i] / sparkMax[i]; // 1 -> 0
    sparkCol[i3]     = Math.min(1, life * 1.9);
    sparkCol[i3 + 1] = life * life * 0.78;
    sparkCol[i3 + 2] = life * life * life * 0.45;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;

  // ---- keep camera safely above the terrain ----
  if (camera.position.y < 0.6) camera.position.y = 0.6;

  controls.update();
  renderer.render(scene, camera);
}

renderer.setAnimationLoop(animate);
</script>
</body>
</html>
Reasoning trace (13,153 tokens)
Let me write a single HTML file with Three.js from the import map.

Key elements:
- Scene with fog (FogExp2 or Fog), night background.
- Ground: low-poly plane with jittered vertices for faceted look, MeshStandardMaterial flatShading.
- Rocks: icosahedron geometry scaled randomly, flat shading.
- Logs: cylinders around the fire.
- Trees: cones (2-3 stacked) + trunk cylinder, 30+, placed in ring radius 12-30, avoiding fire.
- Campfire: logs arranged in a teepee, flames as several cone/icosahedron meshes with additive blending, animated scale/rotation, colors orange/yellow. Plus embers particles.
- Point light with shadow, flickering intensity + position jitter.
- Stars: Points with random positions on sphere.
- Moon: sphere with emissive material, plus glow sprite maybe (canvas texture? avoid — use a simple sphere with basic material). Could add a second larger transparent sphere for glow.
- Sparks: Points system rising, recycling.
- OrbitControls with maxPolarAngle ~ Math.PI/2 - 0.05, minDistance, maxDistance, target at fire.

Also add a subtle "Click or press a key to start" message? The task is a 3D scene, not a game. Games must start in demo mode. This isn't a game but uses keyboard? Only OrbitControls (mouse). The requirement mentions the iframe only receives keyboard events after click. Not needed here. But maybe add a small overlay hint "Drag to look around". That's fine.

Let me consider performance: shadows on point light with 30 trees could be heavy. Use pointLight.castShadow = true with mapSize 1024, and only cast shadows from logs/rocks/nearby trees? Actually shadow from point light is 6 faces cubemap — expensive. Maybe limit: shadow map 512, and set castShadow only on fire logs, rocks, and a few nearby trees. Or use the point light with shadows anyway; scene is small. Let's set shadow map 1024 and castShadow on fire logs, nearby rocks. Trees castShadow false to save perf (they're far, and flicker shadows from trees at night not important). Actually shadows from the trees would be nice but expensive; keep near objects only.

Let's write code.

```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Campfire</title>
<style>
  html,body{margin:0;height:100%;overflow:hidden;background:#05060a;}
  canvas{display:block;}
  #hint{position:fixed;left:50%;bottom:18px;transform:translateX(-50%);color:#cfd8ff;font:13px/1.4 system-ui,sans-serif;opacity:.55;pointer-events:none;letter-spacing:.05em;}
</style>
</head>
<body>
<div id="hint">Drag to look around · Scroll to zoom</div>
<script type="importmap">...</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
...
</script>
</body>
</html>
```

Now the JS.

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

Scene: `scene.fog = new THREE.Fog(0x0a0e1a, 18, 60); scene.background = new THREE.Color(0x05070d);`

Hmm, fog color should match sky near horizon. Use dark blue.

Camera: PerspectiveCamera(55, aspect, 0.1, 300), position (0, 5, 12)... maybe start at (7, 4.5, 9) looking at fire.

Controls: target (0,1,0), minDistance 3, maxDistance 40, maxPolarAngle Math.PI*0.49, enablePan false, autoRotate maybe slowly? Auto-rotate could be nice for a screenshot. Let's enable autoRotate with slow speed 0.35 and it stops... Hmm, autoroate may be undesirable. Requirement says OrbitControls to look around. A gentle auto-rotate is nice for a demo screenshot showing scene. I'll add autoRotate = true with speed 0.4, and disable on user interaction ('start' event)? Actually OrbitControls autoRotate continues after interaction. I could stop autoRotate on 'start' event (pointer down). Let's do that — nice touch.

Lighting:
- Ambient/hemisphere: very dim blue: new THREE.HemisphereLight(0x223355, 0x080810, 0.35).
- Moon directional light: dim blue from moon direction, castShadow maybe false (point light handles shadows). Add a soft directional light with intensity 0.25 color 0x88aaff.
- Fire point light: color 0xff8830, intensity ~ 30 (with physically correct default in r155+, intensity in candela; distance/decay 2). Use intensity 40, distance 25, decay 2? Let's set pointLight = new THREE.PointLight(0xff9038, 25, 30, 2). Hmm with decay 2 and distance 30, at 5 units away intensity = 25/25 = 1. Reasonable. Let's tune: intensity 35.

Also add a small second flickering light for fill? Maybe just one, plus the ember glow via flames emissive.

Ground: 
```js
const groundGeo = new THREE.CircleGeometry(60, 48) // or PlaneGeometry with segments
```
Better: PlaneGeometry(140,140,60,60), rotate -PI/2, jitter vertices for low-poly, flatShading. But need clearing flat-ish near fire. I'll do radial jitter based on distance from center, small amplitude near center.

Simple approach: PlaneGeometry(120,120,48,48), rotateX(-PI/2), then for each vertex, offset y by noise * (1 - smooth), and jitter x/z.

Use a simple hash noise function.

Let's do:
```js
const gGeo = new THREE.PlaneGeometry(120,120,64,64);
gGeo.rotateX(-Math.PI/2);
const pos = gGeo.attributes.position;
for(let i=0;i<pos.count;i++){
  const x = pos.getX(i), z = pos.getZ(i);
  const d = Math.hypot(x,z);
  const falloff = THREE.MathUtils.smoothstep(d, 4, 30); // 0 near center
  const h = (Math.sin(x*0.35)*Math.cos(z*0.27) + Math.sin((x+z)*0.11)*1.5) * 0.35;
  pos.setY(i, h * falloff + (Math.random()-0.5)*0.15*falloff);
}
gGeo.computeVertexNormals();
```
Wait random per vertex creates noise; fine but seams? Plane grid, vertices shared? PlaneGeometry is indexed, so vertices are shared — jitter is consistent. Good. flatShading gives faceted.

Material: MeshStandardMaterial({color:0x2c3a22 or 0x24331f, roughness:1, flatShading:true}). Night forest ground dark green/brown. Use 0x2a3324.

Fire pit: a ring of small stones around fire. Then logs in teepee.

Flames: create group of ~6 cone meshes with additive blending, colored orange to yellow, random rotation, animated. Use IcosahedronGeometry or ConeGeometry with few segments, flatShading. Material: MeshBasicMaterial({color, transparent:true, opacity:0.85, blending: THREE.AdditiveBlending, depthWrite:false}).

Animate: each flame scale y oscillates and position jitter; also rotate.

Let me build flames:
```js
const flames = [];
const flameGroup = new THREE.Group();
const flameColors = [0xff4d00, 0xff7a00, 0xffa31a, 0xffd24a, 0xffe89a];
for (let i=0;i<7;i++){
  const h = 1.2 + Math.random()*0.9;
  const geo = new THREE.ConeGeometry(0.28+Math.random()*0.16, h, 5, 1);
  geo.translate(0, h/2, 0);
  const mat = new THREE.MeshBasicMaterial({color: flameColors[i%5], transparent:true, opacity:0.75, blending:THREE.AdditiveBlending, depthWrite:false, side:THREE.DoubleSide});
  const m = new THREE.Mesh(geo, mat);
  m.position.set((Math.random()-0.5)*0.35, 0.1, (Math.random()-0.5)*0.35);
  m.userData = {phase: Math.random()*Math.PI*2, speed: 1.5+Math.random()*2, baseY: m.position.y, h};
  flameGroup.add(m); flames.push(m);
}
```
Add central bright core sphere maybe.

Animate flames:
```js
const t = clock.getElapsedTime();
flames.forEach((f,i)=>{
  const u = f.userData;
  const s = 1 + Math.sin(t*u.speed + u.phase)*0.22;
  f.scale.set(1 + Math.sin(t*3+i)*0.08, s, 1 + Math.cos(t*2.5+i)*0.08);
  f.rotation.y += 0.01;
  f.position.x = baseX + Math.sin(t*u.speed*0.7+u.phase)*0.05;
});
```
Need to store base x/z. Fine.

Sparks: BufferGeometry with N=120 points, positions array, velocities array, life array. Use PointsMaterial with a canvas-generated circular sprite texture, additive blending, size attenuation, color orange.

Generate sprite texture via canvas — allowed (inline, no network).

```js
function makeSprite(){
  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.25,'rgba(255,220,150,0.9)');
  g.addColorStop(1,'rgba(255,120,0,0)');
  ctx.fillStyle=g; ctx.fillRect(0,0,64,64);
  return new THREE.CanvasTexture(c);
}
```

Sparks update: each has position, velocity, life. On reset: position near fire base, random velocity upward with spread, life 1.0-2.5s. Update: pos += vel*dt; vel.y -= 0.5*dt (slight gravity) ; actually sparks rise and decelerate; use vel.y *= 0.98 or apply small gravity. And add gentle sideways sway. Alpha fade — with PointsMaterial single opacity we can't fade individually unless we use vertex colors / custom shader. Simple: shrink via using a Points with per-vertex color attribute? PointsMaterial supports vertexColors, color multiplies. But alpha per point not supported without shader. Could just set dead sparks far away and reset. Alternatively use two Points systems. Simpler: keep vertexColors and set color to fade toward black (additive blending means black = invisible). That works nicely with additive blending! color *= life. 

So geometry has position and color attributes. Material: PointsMaterial({size:0.12, map: sprite, vertexColors:true, blending:AdditiveBlending, depthWrite:false, transparent:true, sizeAttenuation:true}).

Stars: BufferGeometry with 1200 points on a large sphere (radius 150), PointsMaterial size 0.8, color white, sizeAttenuation false? With sizeAttenuation false, size in pixels. Use size 1.6, sizeAttenuation:false. Add slight color variation via vertexColors. Also could add a moon.

Moon: SphereGeometry(4, 16, 12) with MeshBasicMaterial color 0xf2f3e0, positioned at (-40, 45, -70) normalized to radius ~90. Add glow: a sprite with radial gradient, scale 30, additive.

Also a subtle moon directional light: DirectionalLight(0x9fb8ff, 0.5) from moon direction. Shadows off (to keep perf) or on? Only fire casts shadows per requirement. Keep directional shadows off.

Trees: function makeTree(): group with trunk cylinder (radiusTop 0.12, radiusBottom 0.2, height 1.2, 5 seg) brown, and 3 cone layers green (flatShading). Randomize scale 0.8-2.0, rotation.y random.

Colors: dark green 0x1f3a2a with variation.

Place 40 trees at radius between 11 and 45, angle random; check not too close to another? Not necessary. Trees cast shadow? Let's set castShadow true only for trees with radius < 16 maybe, but point light shadow is expensive anyway. I'll set castShadow false for trees to keep the frame rate, and rely on distance. Hmm, but "casts shadows" requirement — the fire light casts shadows from logs, rocks, ground. Good enough. Actually shadows from nearby trees would look great. Let's enable castShadow on trees within radius 20 (maybe 8 trees) — cubemap shadow render with 8 trees is fine.

Hmm, point light shadow requires rendering scene 6 times. With ~40 trees each with ~4 meshes = 160 meshes, that's fine at 1024 map size? Might be ok. Let's set shadow map 1024 and enable castShadow for all trees but receive shadow false... Actually shadow map render cost is geometry-based; 200 meshes * 6 faces = 1200 draw calls per frame. That's too much. So limit: use shadow.mapSize 512 and only fire-side objects cast shadows. Let's keep trees castShadow = false. To fake tree shadows, no.

Alternatively: use a SpotLight? No, requirement is point light.

Decision: pointLight.castShadow = true, shadow.mapSize 1024, shadow.camera.near 0.5, far 30, bias -0.005. Objects casting: fire logs, stones, rocks, logs near fire, and the few nearest trees (I'll enable for trees within radius 14, probably 0-2 trees). Fine.

Actually with far=30 the trees far away wouldn't be in shadow range anyway. Good — set shadow.camera.far = 28.

Rocks: icosahedron geometry detail 0, radius random 0.3-1.2, scaled non-uniformly, positioned in ring radius 4-9 plus a few bigger farther. flatShading, color 0x555a5e-ish gray with variation. castShadow/receiveShadow true.

Logs: cylinders (radius 0.22, height 2.2, 6 segments) laying around fire — use a group; position at radius ~1.6, rotated to lie tangentially, tilted. Also a couple of logs in the teepee shape over the fire.

Fire pit stones: small icosahedrons in a ring radius 1.4.

Also add a small ambient dust? Not needed.

Fog: Fog(0x070a12, 25, 75). The sky is a background color; with fog color matching.

Better: use `scene.background = new THREE.Color(0x060810)` and fog color 0x0a0f1c so distant trees fade to a slightly lighter haze. Hmm, they should fade into darkness. Use fog color 0x080b14 close to background.

Sky dome: maybe use a large sphere with gradient shader? Could add a ShaderMaterial gradient sphere for a nicer sky. Let's do a simple gradient sphere with BackSide and ShaderMaterial:

```js
const skyGeo = new THREE.SphereGeometry(200, 24, 16);
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide, depthWrite:false,
  uniforms:{ top:{value:new THREE.Color(0x02040a)}, bottom:{value:new THREE.Color(0x121a2e)} },
  vertexShader: `varying float vY; void main(){ vY = normalize(position).y; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,
  fragmentShader: `uniform vec3 top; uniform vec3 bottom; varying float vY; void main(){ float t = smoothstep(-0.1,0.7,vY); gl_FragColor = vec4(mix(bottom, top, t),1.0);} `
});
```
But fog affects? With ShaderMaterial fog isn't applied by default — good, sky unaffected. But renderer.toneMapping affects? ShaderMaterial output goes through tone mapping only if... in r170 output color space conversion is applied for ShaderMaterial? Actually since r152, custom shaders need `#include <colorspace_fragment>` for proper output. If not included, colors will be treated as linear and converted? Hmm. For ShaderMaterial, the renderer appends encodings only if the shader includes the chunks. Without them, output is raw — might appear darker/lighter. Simpler to avoid and just use scene.background color plus stars. But a gradient adds nice atmosphere. Let's include the tonemapping/colorspace includes:

fragmentShader:
```
uniform vec3 top; uniform vec3 bottom; varying float vY;
void main(){
  float t = smoothstep(-0.15, 0.75, vY);
  gl_FragColor = vec4(mix(bottom, top, t), 1.0);
  #include <tonemapping_fragment>
  #include <colorspace_fragment>
}
```
That should work in r170 (gl_FragColor defined, then modified by chunks). Yes those chunks modify gl_FragColor.

Colors defined with THREE.Color in linear-srgb? THREE.Color(0x02040a) is sRGB interpreted... In r170, `new THREE.Color(hex)` sets via setHex which by default in working color space... Actually Color.setHex(hex, colorSpace = SRGBColorSpace) converts to working (linear) space. So uniform color is linear. Good, and colorspace_fragment converts to sRGB output. Consistent.

Also fog: with Fog and a sky sphere, the sky sphere is at radius 200 which is beyond the fog far (75), so if fog applied it'd be fully fog color. But ShaderMaterial without fog uniforms won't be fogged. 

Set `scene.background = null` then? Sky sphere covers everything. Keep a background color too as fallback.

Stars must be inside the sky sphere (radius < 200) — put them at radius 160.

Moon at radius ~150 in a direction, sphere radius 6, plus glow sprite.

Now the fog will fade the distant trees but the "distant" is 75 units — trees are placed up to radius 45. Fog near 25 far 75 means trees at 45 are substantially fogged. Good.

Camera far plane must be > 220 for the sky sphere. Set far 600.

Let's now consider the fire light flicker:
```js
const flicker = 0.75 + Math.sin(t*11.3)*0.12 + Math.sin(t*7.7+1.3)*0.09 + Math.sin(t*19.1+2.1)*0.05 + (Math.random()-0.5)*0.08;
fireLight.intensity = 34 * flicker;
fireLight.position.x = Math.sin(t*9)*0.08;
fireLight.position.z = Math.cos(t*7.3)*0.08;
fireLight.position.y = 1.0 + Math.sin(t*13)*0.06;
```

Also add a second dimmer light for the glow on the ground? One is enough.

Camera start: position (6.5, 4.2, 8.5), target (0, 1.2, 0). With autoRotate slowly.

Let's also add "camera stays above the ground": maxPolarAngle = Math.PI * 0.495 (near horizon) — prevents going below ground. Also clamp camera.position.y in the animate loop to min 0.6? With polar angle limited that's handled, but the target is at y=1.2 so camera y ≥ target.y + small... Actually maxPolarAngle ~ 1.55 rad means camera y ≈ target.y + d*cos(1.55) ≈ 1.2 + d*0.02. At d=20, y = 1.6. Fine, above ground. But with polar 0.495π = 1.5551, cos = 0.0157, so at distance 40 y=1.83. OK above ground. Good.

Also clamp minPolarAngle to 0.15 to avoid top-down? Not needed but fine.

Resize handler.

Let's write the tree function with flat shading and low poly.

```js
const trunkMat = new THREE.MeshStandardMaterial({color:0x4a3826, roughness:1, flatShading:true});
const leafMats = [0x1b3a2a, 0x21472f, 0x16321f, 0x264b33].map(c=> new THREE.MeshStandardMaterial({color:c, roughness:1, flatShading:true}));
```
Better to reuse materials for perf. Yes create shared materials.

makeTree(s):
```js
function makeTree(){
  const g = new THREE.Group();
  const s = 0.8 + Math.random()*1.4;
  const trunkH = 1.6*s;
  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.14*s,0.24*s,trunkH,5), trunkMat);
  trunk.position.y = trunkH/2;
  g.add(trunk);
  const mat = leafMats[(Math.random()*leafMats.length)|0];
  let y = trunkH*0.75;
  let r = 1.15*s;
  let h = 1.9*s;
  for(let i=0;i<3;i++){
    const cone = new THREE.Mesh(new THREE.ConeGeometry(r, h, 6, 1), mat);
    cone.position.y = y + h/2;
    g.add(cone);
    y += h*0.42; r *= 0.72; h *= 0.78;
  }
  g.userData.radius = 1.0*s;
  return g;
}
```
Geometry creation per tree — 40 trees * 4 geometries = 160 geometries. Fine but could be shared. Not a big deal.

Placement: 
```js
const trees = [];
let attempts = 0;
while(trees.length < 42 && attempts < 2000){
  attempts++;
  const a = Math.random()*Math.PI*2;
  const rad = 11 + Math.pow(Math.random(),0.7)*30;
  const x = Math.cos(a)*rad, z = Math.sin(a)*rad;
  // avoid fire
  if (Math.hypot(x,z) < 10) continue;
  const t = makeTree();
  t.position.set(x, -0.1, z);
  t.rotation.y = Math.random()*Math.PI*2;
  scene.add(t);
  trees.push(t);
}
```

Also add a few bushes/ferns? Optional, skip. Maybe add small grass tufts using cones — adds detail. Could add 60 tiny dark cones as grass. Meh, keep it.

Actually adding a few fallen logs and rocks scattered helps. Requirements: "A ground with a few low-poly rocks and logs around a campfire".

Fire logs: 3-4 logs forming teepee over fire, plus 2 logs lying outside near the fire.

Teepee logs: cylinder length 1.8, radius 0.12, positioned around the fire at radius 0.5 tilted inward, top near apex. Let's compute: for i in 0..4, angle = i*2π/5, position (cos*0.55, 0.55, sin*0.55), rotation: tilt so top converges. Use `log.lookAt` trick: create log along Y axis, then set position at base and rotate.

Simplest: create a group at origin, add cylinder with translate up h/2, then rotate group.

For teepee: each log group rotate around... Let's do:
```js
for(let i=0;i<5;i++){
  const a = i/5*Math.PI*2 + 0.3;
  const g = new THREE.Group();
  const len = 1.7;
  const cyl = new THREE.Mesh(new THREE.CylinderGeometry(0.09,0.11,len,6), logMat);
  cyl.position.y = len/2;
  g.add(cyl);
  g.position.set(Math.cos(a)*0.52, 0, Math.sin(a)*0.52);
  g.rotation.y = -a;   // hmm
  g.rotateOnAxis?
}
```
To tilt inward toward center: after positioning at (cos a, 0, sin a)*0.52, we want the log to lean toward the center (opposite direction of a). Rotate about the axis perpendicular to the radial direction, which is the tangent axis. If group rotation.y = -a + something... Easier: use `g.rotation.set(0,0,0)` then `g.rotateOnWorldAxis(new THREE.Vector3(-Math.sin(a),0,Math.cos(a)), tilt)` — rotating about tangential axis tilts the log in the radial direction. Let's use quaternion approach:

axis = new THREE.Vector3(Math.cos(a+... )). Simply: the tilt axis should be perpendicular to the radial direction and horizontal: tangent = (-sin a, 0, cos a). Rotating about tangent by angle θ tilts the up vector toward... Let's check: rotating up (0,1,0) about axis (0,0,1) by θ gives (-sinθ, cosθ, 0), i.e., tilts toward -x. With axis t = (-sin a, 0, cos a): rotate up vector by θ about t: up × t? Rodrigues: v_rot = v cosθ + (k×v) sinθ. k = (-sin a, 0, cos a), v = (0,1,0). k×v = (0*0 - cos a*1, cos a*0 - (-sin a)*0, -sin a*1 - 0*0) = (-cos a, 0, -sin a). So v_rot = (0,cosθ,0) + sinθ*(-cos a, 0, -sin a). So it tilts toward (-cos a, -sin a) — inward direction (since the base is at +cos a). 

So: `g.position.set(cos a*0.52, 0, sin a*0.52); g.quaternion.setFromAxisAngle(new THREE.Vector3(-Math.sin(a),0,Math.cos(a)), 0.32);`

That tilts inward. The tops will converge roughly. Good enough.

Logs lying around: use cylinder rotated with rotation.z = Math.PI/2 then rotation.y random, positioned at radius 2.5-3.5.

Also add a "seat" log — log lying with radius 0.25, length 2.5 at distance 3.

Now the fire base: a bed of embers — small dark red glowing plane/disc? Add a circle at y=0.02 with emissive-ish MeshBasicMaterial color 0x552200. Or a few small glowing ember spheres. Let's add a disc with additive orange that pulses.

Stone ring: 10 stones in ring radius 1.35, small icosahedron scale 0.25-0.4.

Rocks: 8 bigger rocks at radius 4-10.

Now shadows: ground.receiveShadow = true; rocks/logs/fire objects castShadow & receiveShadow.

Let me handle the point light shadow camera far 25, near 0.3, bias -0.003.

Note: point light shadows in three r170 use a cube shadow map; `shadow.camera.fov` n/a. Set `light.shadow.mapSize.set(1024,1024)`.

Performance: 6 renders of the scene with limited casters (about 30 meshes) fine.

Now write the animate loop with clock.

Sparks: 150 particles.

```js
const SPARK_COUNT = 160;
const sparkPos = new Float32Array(SPARK_COUNT*3);
const sparkCol = new Float32Array(SPARK_COUNT*3);
const sparkVel = new Float32Array(SPARK_COUNT*3);
const sparkLife = new Float32Array(SPARK_COUNT);
const sparkMax = new Float32Array(SPARK_COUNT);
```
reset function:
```js
function resetSpark(i, initial){
  const a = Math.random()*Math.PI*2, r = Math.random()*0.55;
  sparkPos[i*3] = Math.cos(a)*r;
  sparkPos[i*3+1] = 0.25 + Math.random()*0.5;
  sparkPos[i*3+2] = Math.sin(a)*r;
  sparkVel[i*3] = (Math.random()-0.5)*0.35;
  sparkVel[i*3+1] = 1.0 + Math.random()*1.6;
  sparkVel[i*3+2] = (Math.random()-0.5)*0.35;
  sparkMax[i] = 1.2 + Math.random()*1.8;
  sparkLife[i] = initial ? Math.random()*sparkMax[i] : 0;
}
```
Update per frame with dt:
```js
for(let i=0;i<SPARK_COUNT;i++){
  sparkLife[i] += dt;
  if(sparkLife[i] > sparkMax[i]) resetSpark(i,false);
  const t = sparkLife[i]/sparkMax[i];
  // motion
  sparkVel[i*3+1] += (0.4 - ... 
```
Let's do: vertical velocity decays toward 0.3 (like rising with hot air): `vel.y += (0.35 - vel.y) * 0.9 * dt`? Hmm simpler: pos += vel*dt, and vel.y *= (1 - 0.35*dt); plus sway with sin.

Color: fade from bright yellow-white at start to red at end and fade to black:
```js
const k = 1 - t; // brightness
// color: mix from (1,0.9,0.5) to (0.6,0.12,0)
sparkCol[i*3] = (1.0*k)*(0.9+0.1*Math.random())... 
```
Random per frame is flickery — better deterministic. Use: r = 1.0*k*k... let's do:
r = Math.min(1, 2*k) * 1.0; g = k*k*0.8; b = k*k*k*0.5. Good enough for additive.

Set attributes needsUpdate.

Also add "smoke"? Skip.

Hmm, note buffer positions: since I'm setting positions directly from sparkPos each frame — I can just use sparkPos as the attribute array. Yes: `geometry.setAttribute('position', new THREE.BufferAttribute(sparkPos,3))`. And color similarly. Then set needsUpdate each frame.

Now the moon and stars.

Stars:
```js
const starGeo = new THREE.BufferGeometry();
const N = 1500;
const sp = new Float32Array(N*3);
for(...){
  // random on sphere
  const u = Math.random()*2-1, theta = Math.random()*Math.PI*2;
  const r = 160;
  const s = Math.sqrt(1-u*u);
  sp[..] = Math.cos(theta)*s*r; y = u*r (but keep above horizon mostly: u from -0.1 to 1)
}
```
Use u = Math.random()*1.1 - 0.1 to bias above horizon. If u<0 hemisphere... Actually y = u*r; only want y>0-ish. u from 0.02 to 1: y positive. But then stars only above horizon — right, ground blocks below.

Colors slight variation: white to bluish. Use vertexColors with random brightness.

PointsMaterial({size: 1.6, sizeAttenuation: false, vertexColors: true, transparent:true, depthWrite:false, opacity: 0.9}).

Note: stars at radius 160 inside sky sphere 200, and camera far 600. Good. But fog won't affect Points without fog? PointsMaterial has fog:true by default; with fog far 75, stars at 160 would be fully fogged to fog color — but the fog color (0x0a0f1c) on additive? Points aren't additive here; they'd be drawn as fog color... which is nearly background so stars disappear! Must set `fog: false` on the star material and moon material. Yes.

Same for the moon — set fog:false.

Flame materials: MeshBasicMaterial fog true default — flames at close range, fog near is 25 so unaffected. Fine, but embers/sparks use PointsMaterial — set fog:false for sparks too (they're close, fine either way). Set fog false to be safe.

Moon: let's place at direction (-0.55, 0.45, -0.7) normalized * 150. Radius 7. MeshBasicMaterial color 0xeef0d8, fog:false. Add glow sprite with the sprite texture (bluish-white radial gradient), scale 60, additive, opacity 0.55, fog:false.

Hmm the sprite texture I built is orange-ish white. Make a separate white glow texture.

Moon light: DirectionalLight(0x8ea8ff, 0.6) positioned along moon direction, target origin. No shadows.

Hemisphere light 0x1a2a44 / 0x0a0f0a intensity 0.5? With physically correct lighting default in r155+, intensities behave as before for hemisphere/ambient. Fine.

Since tone mapping ACES is used with exposure 1, colors get compressed; fire light intensity may need to be higher. Let's set exposure 1.15.

Let me estimate: point light intensity 40, decay 2, at 3 units -> 40/9 ≈ 4.4 lux-ish. With MeshStandardMaterial albedo 0.3, that's bright. Good.

Also set `renderer.toneMappingExposure = 1.0`.

Let's write the final code.

One more: `ground` receives shadows. Ground material color: 0x2b3a26 maybe too bright for night. With fire light at close range it'd be lit warm. Let's use 0x30402c... I'll pick 0x2e3b28.

Actually low-poly forest floor at night with warm firelight — a dark olive/brown 0x39402f. Let's use 0x333d2a.

Now writing full code.

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

const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
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.1;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a1020, 22, 78);

const camera = new THREE.PerspectiveCamera(55, window.innerWidth/window.innerHeight, 0.1, 700);
camera.position.set(6.5, 4.5, 8.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 3.5;
controls.maxDistance = 26;
controls.minPolarAngle = 0.25;
controls.maxPolarAngle = Math.PI * 0.49;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.45;
controls.addEventListener('start', () => { controls.autoRotate = false; });
controls.update();
```

Careful: `controls.addEventListener('start')` fires on pointer down. Good.

Sky sphere shader as described.

Let's write it all out.

For the ground jitter, use a deterministic-ish random.

Also make sure the ground plane is big (120) and fog hides the edge. With fog far 78, ground at 60 radius is fully fogged. Good, but plane corners at 85 distance fully fogged. Fine.

Let me write helper random with seed for consistency? Not needed.

Write the code now.

```js
// ---------- Sky ----------
const skyMat = new THREE.ShaderMaterial({
  side: THREE.BackSide,
  depthWrite: false,
  uniforms: {
    topColor: { value: new THREE.Color(0x03050c) },
    bottomColor: { value: new THREE.Color(0x152238) }
  },
  vertexShader: `
    varying float vH;
    void main() {
      vH = normalize(position).y;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }`,
  fragmentShader: `
    uniform vec3 topColor;
    uniform vec3 bottomColor;
    varying float vH;
    void main() {
      float t = smoothstep(-0.05, 0.6, vH);
      gl_FragColor = vec4(mix(bottomColor, topColor, t), 1.0);
      #include <tonemapping_fragment>
      #include <colorspace_fragment>
    }`
});
const sky = new THREE.Mesh(new THREE.SphereGeometry(300, 32, 20), skyMat);
scene.add(sky);
```
Wait: camera far 700, sky radius 300 — ok.

Hmm, careful: the sky sphere with depthWrite false and BackSide renders first? It's fine since it's opaque and rendered with other opaque objects; depthWrite false means it won't occlude. But being far away, it will fail depth test against nearer objects if drawn after. Render order for opaque is by distance front-to-back typically, so sky drawn last would be depth-tested against already drawn objects -> occluded properly. But since it doesn't write depth, objects drawn after it would still draw over it. Fine.

Actually potential issue: with fog color 0x0a1020 and the sky bottom 0x152238, distant trees fade to 0x0a1020 which differs from sky at horizon. Looks okay.

Stars must be in front of the sky sphere? Stars at radius ~200, sky at 300 — stars are inside, so they render in front. Good.

Moon at radius 200 too... but the fog affects nothing as we set fog false. Good.

Wait, moon glow sprite at distance 200 with sizeAttenuation... Sprite scale 60 at distance 200 — will look decent.

Let me place moon: dir = new THREE.Vector3(-0.6, 0.42, -0.68).normalize(); moonPos = dir.multiplyScalar(190).

Moon radius 8 → angular size ~ 8/190 = 0.042 rad ≈ 2.4°, moon is 0.5° in reality but bigger looks nicer.

Now writing everything out. I'll be careful with syntax.

Tree cast shadows: set `t.castShadow = false` but for trees within radius 16, enable on the meshes.

Let me just do: after creating tree group, if (rad < 17) { t.traverse(o => { if (o.isMesh) o.castShadow = true; }); }

Ah but the shadow camera far is 25, so those may be out of range partially. Fine.

Hmm, but performance: how many trees within 17? radius 11-41 with pow(0.7) distribution biased to smaller radii. Maybe 12 trees → 48 meshes ×6 faces = 288 extra draws. Acceptable but maybe reduce: only within radius 14. OK.

Actually let me just set shadow map size to 1024 and be done.

Now details of fire:

```js
const fireGroup = new THREE.Group();
scene.add(fireGroup);

// ember bed
const bedGeo = new THREE.CircleGeometry(1.0, 12);
bedGeo.rotateX(-Math.PI/2);
const bedMat = new THREE.MeshBasicMaterial({ color: 0xff5a10, transparent:true, opacity:0.6, blending: THREE.AdditiveBlending, depthWrite:false, fog:false });
const bed = new THREE.Mesh(bedGeo, bedMat);
bed.position.y = 0.06;
fireGroup.add(bed);
```

Flames group at y 0.

Stones ring:
```js
const stoneMat = new THREE.MeshStandardMaterial({color:0x6b6b66, roughness:0.95, flatShading:true});
for (let i=0;i<12;i++){
  const a = i/12*Math.PI*2;
  const s = 0.16 + Math.random()*0.12;
  const m = new THREE.Mesh(new THREE.IcosahedronGeometry(s, 0), stoneMat);
  m.position.set(Math.cos(a)*1.25, s*0.5, Math.sin(a)*1.25);
  m.rotation.set(Math.random(),Math.random(),Math.random());
  m.scale.set(1, 0.7+Math.random()*0.5, 1);
  m.castShadow = m.receiveShadow = true;
  fireGroup.add(m);
}
```

Teepee logs (5) as described, plus 2 lying.

Fire light:
```js
const fireLight = new THREE.PointLight(0xff8a3c, 40, 34, 2);
fireLight.position.set(0, 1.1, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024,1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 30;
fireLight.shadow.bias = -0.004;
fireLight.shadow.radius = 3;  // only for PCF? radius works with PCFSoft? Actually shadow.radius works with PCFShadowMap (not PCFSoft). skip or keep.
scene.add(fireLight);
```
Color 0xff8a3c — note point light color also is sRGB→linear converted.

Also a small warm ambient light near fire: `new THREE.PointLight(0xff6a20, 8, 12, 2)` at y=0.5 for ground glow — adds cost. Skip; one light + hemisphere is fine. Actually the shadow-casting light gives enough.

Hmm — with only one point light plus hemisphere + moon directional, the scene should look decent.

Now the animate function.

```js
const clock = new THREE.Clock();

function animate(){
  const dt = Math.min(clock.getDelta(), 0.05);
  const t = clock.elapsedTime;
  ...
  renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
```

Use clock.getElapsedTime? getDelta updates elapsedTime too. `clock.elapsedTime` is valid after getDelta. Yes.

Flames animation with base positions stored.

Let me also add subtle scale pulsing to the bed material opacity.

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

Let's finalize numbers and write.

Sparks material size: with sizeAttenuation true and size 0.14, at distance 10 that's small. Let's use size 0.16. Hmm, PointsMaterial size in world units when sizeAttenuation true. 0.16 world units at 10m → appears small. Maybe 0.2. I'll use 0.18.

Also note: the sprite texture used with additive blending — the texture is a radial gradient with orange. For sparks we want bright. Fine.

Let me now write out the complete code carefully.

Additional detail: a subtle "campfire smoke" — skip.

Also worth adding: fireflies? Not required. Skip.

Write final.

One concern: `THREE.MathUtils.smoothstep(x, min, max)` exists. Yes.

Ground vertex loop:

```js
const groundGeo = new THREE.PlaneGeometry(140, 140, 70, 70);
groundGeo.rotateX(-Math.PI / 2);
const gpos = groundGeo.attributes.position;
for (let i = 0; i < gpos.count; i++) {
  const x = gpos.getX(i);
  const z = gpos.getZ(i);
  const d = Math.sqrt(x*x + z*z);
  const k = THREE.MathUtils.smoothstep(d, 3.5, 26);
  const h = (Math.sin(x * 0.21) * Math.cos(z * 0.17) + Math.sin((x + z) * 0.09) * 1.4) * 0.55;
  gpos.setY(i, h * k + (Math.random() - 0.5) * 0.35 * k);
}
groundGeo.computeVertexNormals();
```
Ground near fire is flat. Good. But the fire at y=0 with ground flat at center — good.

Note the ground plane has 70×70 = 4900 quads; flatShading means non-indexed? flatShading works with indexed geometry in three via derivatives... Actually flatShading in three uses `#ifdef FLAT_SHADED` with dFdx normals computed in fragment shader. Yes it works on indexed geometry. Good.

Ground material: `new THREE.MeshStandardMaterial({ color: 0x2f3a28, roughness: 1, metalness: 0, flatShading: true })`. ground.receiveShadow = true.

Ok. Now writing the code in full.

For the moon glow sprite texture, create a white radial gradient canvas.

I'll write a function `makeGlowTexture(inner, outer)`.

Let me write everything.

```js
function radialTexture(innerColor, outerColor, size=128){
  const c = document.createElement('canvas');
  c.width = c.height = size;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(size/2,size/2,0,size/2,size/2,size/2);
  g.addColorStop(0, innerColor);
  g.addColorStop(0.35, ...)
```
Simpler: two-stop with inner and transparent outer:
```js
g.addColorStop(0, innerColor);
g.addColorStop(0.5, midColor);
g.addColorStop(1, outerColor);
```
I'll pass an array of stops.

Fine, write generic:
```js
function makeGlow(stops, size = 128) {
  const c = document.createElement('canvas');
  c.width = c.height = size;
  const ctx = c.getContext('2d');
  const g = ctx.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
  stops.forEach(([p, col]) => g.addColorStop(p, col));
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, size, size);
  const tex = new THREE.CanvasTexture(c);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}
```

sparkTex = makeGlow([[0,'rgba(255,255,255,1)'],[0.3,'rgba(255,200,120,0.95)'],[0.7,'rgba(255,110,20,0.35)'],[1,'rgba(255,60,0,0)']], 64);

moonGlowTex = makeGlow([[0,'rgba(220,235,255,0.9)'],[0.25,'rgba(150,190,255,0.35)'],[1,'rgba(60,90,180,0)']], 256);

Now the full file.

Order of operations: create renderer, scene, camera, controls, sky, stars, moon, lights, ground, rocks, trees, fire, sparks, animate.

Let me write out.

For the logs lying around and rocks, I'll add a function to scatter.

Rocks:
```js
const rockMat = new THREE.MeshStandardMaterial({ color: 0x5a5f5c, roughness: 1, flatShading: true });
const rocks = [];
for (let i = 0; i < 14; i++) {
  const a = Math.random() * Math.PI * 2;
  const r = 3.5 + Math.random() * 9;
  const s = 0.28 + Math.random() * 0.7;
  const m = new THREE.Mesh(new THREE.IcosahedronGeometry(s, 0), rockMat);
  m.position.set(Math.cos(a)*r, s*0.45, Math.sin(a)*r);
  m.rotation.set(Math.random()*3, Math.random()*3, Math.random()*3);
  m.scale.set(1 + Math.random()*0.5, 0.6 + Math.random()*0.5, 1 + Math.random()*0.5);
  m.castShadow = true; m.receiveShadow = true;
  scene.add(m);
}
```
Careful about rocks overlapping the fire — r starts at 3.5, fine.

Logs lying: 
```js
const logMat = new THREE.MeshStandardMaterial({ color: 0x4a3524, roughness: 1, flatShading: true });
for (let i = 0; i < 4; i++) {
  const a = (i / 4) * Math.PI * 2 + 0.7;
  const r = 2.6 + Math.random() * 1.6;
  const len = 1.6 + Math.random() * 1.2;
  const m = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.2, len, 6), logMat);
  m.rotation.z = Math.PI / 2;
  m.rotation.y = Math.random() * Math.PI;
  // need to set rotation order? use quaternion
  m.position.set(Math.cos(a)*r, 0.19, Math.sin(a)*r);
  ...
}
```
Setting rotation.z = π/2 then rotation.y — Euler order XYZ default: applies X then Y then Z? Three's Euler XYZ means rotation matrix R = Rx*Ry*Rz? Actually THREE.Euler order 'XYZ' produces matrix R = Rx * Ry * Rz applied... the result: object rotated by Z first in local? Let's not think; simpler use `m.rotateZ(Math.PI/2)` then `m.rotateY(...)` which are post-multiplications of local axes. Using `rotateZ` then `rotateY` gives rotations in local space after the first. That works fine visually (cylinder lying, then spun about its own... no).

Simplest reliable: put the cylinder mesh in a group; set mesh.rotation.z = Math.PI/2 (lies along X), then group.rotation.y = random. Position the group. That's clean.

I'll do that.

Now let's write everything out for real.

Also I should double-check the `#include <tonemapping_fragment>` in a ShaderMaterial — the chunks reference `gl_FragColor` and require `toneMappingExposure` uniform which the renderer injects automatically? The renderer adds tone mapping uniforms to all materials? For ShaderMaterial, WebGLRenderer's `refreshUniformsCommon`... Actually the tonemapping_fragment chunk uses `toneMapping` define-based code: `#if defined( TONE_MAPPING ) gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); #endif`, and `toneMappingExposure` is a global uniform injected by the renderer for all programs? In WebGLProgram, `toneMappingExposure` is added in the common uniforms for all shaders? Let me recall: in WebGLRenderer, `refreshUniforms`... Actually `toneMappingExposure` is set via `p_uniforms.setValue(_gl, 'toneMappingExposure', ...)` for every material in `setProgram`:

```js
if ( material.isShaderMaterial || ... ) ... 
p_uniforms.setValue( _gl, 'toneMappingExposure', _this.toneMappingExposure );
```
Hmm, in setProgram there is:
```js
if ( refreshMaterial ) {
  ...
  if ( material.isMeshStandardMaterial ... )
```
Actually there's a block: `p_uniforms.setValue( _gl, 'toneMappingExposure', _this.toneMappingExposure );` inside `if (refreshMaterial)` for all materials. I believe yes — it's set unconditionally when refreshMaterial. And the TONE_MAPPING define is added in the program prefix based on renderer.toneMapping !== NoToneMapping for all materials. Yes: getProgramCacheKey and prefixFragment includes `parameters.toneMapping !== NoToneMapping ? '#define TONE_MAPPING' : ''` and `ShaderChunk['tonemapping_pars_fragment']`. This is included for ShaderMaterial too (unless material.toneMapped === false).

So including the chunk works. But safer alternative: set `gl_FragColor` and don't include the chunks, and set the material's `toneMapped = false`? Then colors would be output linear→ but the renderer still applies colorspace conversion? For ShaderMaterial the output color space conversion is done via the `colorspace_fragment` chunk which we'd have to include. Without it, the framebuffer would show linear values as sRGB → too dark/bright.

I'll include both chunks. It's standard practice. Fine.

Alright — write it.

One more consideration: sky sphere renderOrder. Set sky.renderOrder = -1 to draw it first. But with depthWrite false, it doesn't matter much. I'll set renderOrder = -1.

Hmm, if sky is drawn first with depthWrite false, then stars drawn after will be visible (they're inside). Fine.

Final code writing.

For the flames' color: with additive blending and basic material, colors 0xff4d00 etc. Add tone mapping — MeshBasicMaterial is tone mapped, so bright additive stuff gets compressed. Fine.

Let me write out the flame update loop storing base positions.

```js
const flames = [];
for (let i = 0; i < 8; i++) {
  const h = 1.0 + Math.random() * 1.1;
  const rad = 0.18 + Math.random() * 0.18;
  const geo = new THREE.ConeGeometry(rad, h, 5, 1);
  geo.translate(0, h / 2, 0);
  const col = new THREE.Color().setHSL(0.06 + Math.random() * 0.06, 1.0, 0.5 + Math.random()*0.12);
  const mat = new THREE.MeshBasicMaterial({
    color: col, transparent: true, opacity: 0.55, blending: THREE.AdditiveBlending,
    depthWrite: false, side: THREE.DoubleSide, fog: false
  });
  const m = new THREE.Mesh(geo, mat);
  const ang = Math.random() * Math.PI * 2;
  const rr = Math.random() * 0.32;
  m.position.set(Math.cos(ang) * rr, 0.08, Math.sin(ang) * rr);
  m.userData = {
    baseX: m.position.x, baseZ: m.position.z,
    phase: Math.random() * 10, sp: 2.0 + Math.random() * 2.5, hs: 0.8 + Math.random()*0.5
  };
  fireGroup.add(m);
  flames.push(m);
}
```
HSL with lightness 0.5 and additive → colors like orange. setHSL uses sRGB by default? In r170 `setHSL(h,s,l, colorSpace=SRGBColorSpace)` — converts to working color space. Fine.

Update:
```js
for (const f of flames) {
  const u = f.userData;
  const w = Math.sin(t * u.sp + u.phase);
  const w2 = Math.sin(t * u.sp * 0.6 + u.phase * 1.7);
  f.scale.set(1 + w * 0.12, (0.75 + 0.35 * (0.5 + 0.5*w2)) * u.hs, 1 + w2 * 0.12);
  f.position.x = u.baseX + w * 0.06;
  f.position.z = u.baseZ + w2 * 0.06;
  f.rotation.y = t * 0.6 + u.phase;
}
```
Scale y range: 0.75..1.1 times hs.

The cone geometry is translated so base at y=0 — scaling y works from the base. Good.

Also add a bright core: a small sphere with additive white-orange at y 0.35, scale pulsing.

OK. Writing final answer now.

I need to double check the point light shadow: `fireLight.shadow.camera.far = 30` — but shadow camera for PointLightShadow has near/far both used. Also `shadow.bias`. Good.

Also the fire light needs to actually illuminate the flames? Flames are MeshBasicMaterial so unaffected. Good.

Finally, check `controls.maxPolarAngle = Math.PI * 0.49` = 1.539, cos = 0.0314 → camera y = target.y + d*0.0314. At d=3.5 → 1.11+0.11 = 1.22. Fine, above ground.

Also I should clamp camera.position.y just in case (damping overshoot). Add in animate: `if (camera.position.y < 0.5) camera.position.y = 0.5;` — but that fights controls. It's fine to leave since the polar limit prevents going under. I'll add a gentle clamp anyway.

Write it.