Models 44

MiMo-V2.6-Flash

by Xiaomireasoning defaultClean
Open in new window

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

How it went

Total time
3 min 47 s
Cost
$0.0050
Output tokens
17,418
Spent thinking
8,779 tokens
Speed
77.5 tok/s
Console errors
0
First token after
2.1 s
First visible text after
2 min 41 s
Prompt tokens
567
Code size
23.9 KB
Finish reason
stop
Served by
Cohere
Model id
xiaomi/mimo-v2.6-flash
Reasoning sent
model default
Max output
131,072 tokens
Generated
Nov 13, 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 · 600 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Night Clearing — Low-poly Campfire</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#05070f;
    font-family:"Trebuchet MS",Segoe UI,system-ui,sans-serif;color:#e9e2d2}
  canvas{display:block;width:100%;height:100%}
  #vignette{position:fixed;inset:0;pointer-events:none;
    background:radial-gradient(ellipse at 50% 62%, rgba(0,0,0,0) 30%, rgba(0,0,0,.35) 72%, rgba(0,0,0,.72) 100%)}
  #hud{position:fixed;top:18px;left:20px;pointer-events:none;max-width:min(70vw,340px)}
  #hud h1{margin:0;font-size:26px;letter-spacing:.14em;text-transform:uppercase;
    font-weight:700;color:#ffd9a0;text-shadow:0 0 18px rgba(255,140,40,.55),0 2px 6px rgba(0,0,0,.8)}
  #hud .sub{margin:6px 0 0;font-size:12.5px;letter-spacing:.24em;text-transform:uppercase;
    color:#8fa6c4;opacity:.9}
  #hud .tags{margin-top:12px;display:flex;flex-wrap:wrap;gap:6px}
  #hud .tags span{font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
    padding:4px 8px;border:1px solid rgba(255,205,150,.28);border-radius:999px;
    background:rgba(20,16,12,.42);color:#f0d2ab;backdrop-filter:blur(3px)}
  #hint{position:fixed;left:0;right:0;bottom:16px;text-align:center;pointer-events:none;
    font-size:12.5px;letter-spacing:.18em;text-transform:uppercase;color:#b9c6da;
    text-shadow:0 2px 8px rgba(0,0,0,.9)}
  #hint b{color:#ffc07a;font-weight:600}
  #fpsdot{position:fixed;top:20px;right:20px;pointer-events:none;font-size:11px;
    letter-spacing:.16em;color:#7e8ea6;text-transform:uppercase;opacity:.75}
  @media (max-width:520px){
    #hud{top:12px;left:12px}
    #hud h1{font-size:18px}
    #hud .sub{font-size:10px;letter-spacing:.16em}
    #hud .tags span{font-size:9px;padding:3px 6px}
    #hint{font-size:10px;letter-spacing:.12em;bottom:12px}
    #fpsdot{display:none}
  }
</style>
</head>
<body>
<div id="hud">
  <h1>Night Clearing</h1>
  <p class="sub">low-poly campfire · three.js</p>
  <div class="tags">
    <span>44 trees</span><span>flickering firelight</span><span>sparks</span>
    <span>moon &amp; stars</span><span>fog</span>
  </div>
</div>
<div id="fpsdot">drag to orbit · scroll to zoom</div>
<div id="hint"><b>Click or drag</b> to look around · scroll / pinch to zoom</div>
<div id="vignette"></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';

/* ------------------------------------------------------------------ *
 *  seeded random so the clearing always looks the same
 * ------------------------------------------------------------------ */
let _seed = 20240711;
function rnd(){ _seed = (_seed * 1664525 + 1013904223) % 4294967296; return _seed / 4294967296; }
function rng(a, b){ return a + (b - a) * rnd(); }
function hash(x, y, z){
  const s = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719) * 43758.5453;
  return s - Math.floor(s);
}

/* ------------------------------------------------------------------ *
 *  renderer / scene / camera / controls
 * ------------------------------------------------------------------ */
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.05;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a1122, 26, 130);

const camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 1600);
camera.position.set(10.5, 5.6, 13.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.5, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 5.5;
controls.maxDistance = 55;
controls.minPolarAngle = 0.18;
controls.maxPolarAngle = Math.PI * 0.485;   // never goes under the ground
controls.rotateSpeed = 0.7;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.35;
['pointerdown','wheel','touchstart'].forEach(ev =>
  renderer.domElement.addEventListener(ev, () => { controls.autoRotate = false; }, { passive:true }));

/* ------------------------------------------------------------------ *
 *  lights
 * ------------------------------------------------------------------ */
scene.add(new THREE.HemisphereLight(0x18284a, 0x0a1008, 0.35));

const moonLight = new THREE.DirectionalLight(0x8fb6ff, 0.5);
moonLight.position.set(-70, 60, -95);
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xff7a24, 42, 60, 2);
fireLight.position.set(0, 1.5, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 42;
fireLight.shadow.bias = -0.006;
fireLight.shadow.normalBias = 0.05;
scene.add(fireLight);

const fillLight = new THREE.PointLight(0xffb066, 6, 14, 2);
fillLight.position.set(0, 0.6, 0);
scene.add(fillLight);

/* ------------------------------------------------------------------ *
 *  canvas-generated sprites (no external assets)
 * ------------------------------------------------------------------ */
function radialTex(stops, size = 64){
  const c = document.createElement('canvas'); c.width = c.height = size;
  const g2 = c.getContext('2d');
  const grd = g2.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
  stops.forEach(s => grd.addColorStop(s[0], s[1]));
  g2.fillStyle = grd; g2.fillRect(0, 0, size, size);
  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  return t;
}
const sparkTex = radialTex([[0,'rgba(255,255,255,1)'],[0.35,'rgba(255,205,120,0.85)'],[1,'rgba(255,90,0,0)']]);
const glowTex  = radialTex([[0,'rgba(255,190,110,0.95)'],[0.35,'rgba(255,120,35,0.45)'],[1,'rgba(255,60,0,0)']], 128);
const starTex  = radialTex([[0,'rgba(255,255,255,1)'],[0.45,'rgba(220,235,255,0.7)'],[1,'rgba(180,210,255,0)']], 32);
const moonGlow = radialTex([[0,'rgba(215,232,255,0.85)'],[0.3,'rgba(150,185,255,0.28)'],[1,'rgba(90,130,255,0)']], 256);

/* ------------------------------------------------------------------ *
 *  night sky, stars, moon
 * ------------------------------------------------------------------ */
const sky = new THREE.Mesh(
  new THREE.SphereGeometry(700, 32, 20),
  new THREE.ShaderMaterial({
    uniforms:{ top:{ value:new THREE.Color(0x02030b) }, bottom:{ value:new THREE.Color(0x0d1730) } },
    vertexShader:`varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
    fragmentShader:`varying vec3 vP; uniform vec3 top; uniform vec3 bottom;
      void main(){ float h = clamp(normalize(vP).y * 0.5 + 0.5, 0.0, 1.0);
        vec3 c = mix(bottom, top, pow(h, 0.65));
        gl_FragColor = vec4(c, 1.0); }`,
    side: THREE.BackSide, depthWrite:false, fog:false
  })
);
scene.add(sky);

function starField(count, radius, size, opacity){
  const pos = new Float32Array(count * 3);
  for (let i = 0; i < count; i++){
    const u = rnd() * 2 - 0.05, th = rnd() * Math.PI * 2;
    const y = Math.abs(u);
    const r = Math.sqrt(Math.max(0, 1 - u * u));
    pos[i*3]   = Math.cos(th) * r * radius;
    pos[i*3+1] = y * radius * 0.95 + 8;
    pos[i*3+2] = Math.sin(th) * r * radius;
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  const m = new THREE.PointsMaterial({
    size, map: starTex, transparent:true, opacity, depthWrite:false,
    blending: THREE.AdditiveBlending, color: 0xdfe9ff, sizeAttenuation:true, fog:false
  });
  const p = new THREE.Points(g, m);
  scene.add(p);
  return p;
}
const dimStars  = starField(900, 460, 3.2, 0.75);
const brightStars = starField(90, 455, 7.0, 0.95);

const moon = new THREE.Mesh(
  new THREE.SphereGeometry(13, 16, 12),
  new THREE.MeshBasicMaterial({ color: 0xf2f6ff, fog:false })
);
moon.position.set(-165, 118, -235);
scene.add(moon);
const moonHalo = new THREE.Sprite(new THREE.SpriteMaterial({
  map: moonGlow, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, fog:false, opacity:0.75
}));
moonHalo.scale.set(95, 95, 1);
moonHalo.position.copy(moon.position);
scene.add(moonHalo);

/* ------------------------------------------------------------------ *
 *  ground (faceted, vertex coloured, flat clearing in the middle)
 * ------------------------------------------------------------------ */
const groundGeo = new THREE.PlaneGeometry(300, 300, 72, 72);
groundGeo.rotateX(-Math.PI / 2);
{
  const p = groundGeo.attributes.position;
  const cols = [];
  const cGrass = new THREE.Color(0x1d2a1c), cDark = new THREE.Color(0x101a14),
        cAsh = new THREE.Color(0x14110d), cDirt = new THREE.Color(0x2a2419),
        cFar = new THREE.Color(0x16222a), tmp = new THREE.Color();
  for (let i = 0; i < p.count; i++){
    const x = p.getX(i), z = p.getZ(i);
    const d = Math.hypot(x, z);
    let h = Math.sin(x * 0.11) * Math.cos(z * 0.09) * 0.75
          + Math.sin(x * 0.29 + z * 0.23) * 0.28
          + Math.sin(z * 0.47 - x * 0.31) * 0.14;
    h *= Math.min(1, Math.max(0, (d - 5) / 12));   // flat around the fire
    h += Math.max(0, d - 45) * 0.05;               // gentle rise far away
    h += (hash(x, 0, z) - 0.5) * 0.16;
    p.setY(i, h);

    tmp.copy(cGrass).lerp(cDirt, hash(x * 2, 1, z * 2) * 0.55);
    tmp.lerp(cDark, Math.min(1, Math.max(0, (d - 20) / 60)));
    tmp.lerp(cFar, Math.min(1, Math.max(0, (d - 55) / 70)) * 0.7);
    if (d < 5.5) tmp.lerp(cAsh, 1 - d / 5.5);
    const shade = 0.82 + hash(x, 3, z) * 0.4;
    cols.push(tmp.r * shade, tmp.g * shade, tmp.b * shade);
  }
  groundGeo.setAttribute('color', new THREE.Float32BufferAttribute(cols, 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);

/* ------------------------------------------------------------------ *
 *  shared low-poly geometries / materials
 * ------------------------------------------------------------------ */
const rockGeoBase = new THREE.IcosahedronGeometry(1, 0);
function makeRockGeo(){
  const g = rockGeoBase.clone();
  const p = g.attributes.position;
  for (let i = 0; i < p.count; i++){
    const x = p.getX(i), y = p.getY(i), z = p.getZ(i);
    const f = 0.82 + hash(Math.round(x*7), Math.round(y*7), Math.round(z*7)) * 0.42;
    p.setXYZ(i, x * f, y * f * 0.8, z * f);
  }
  g.computeVertexNormals();
  return g;
}
const rockGeos = [makeRockGeo(), makeRockGeo(), makeRockGeo()];
const rockMat = new THREE.MeshStandardMaterial({ color:0x54586a, flatShading:true, roughness:0.95 });
const rockMatDark = new THREE.MeshStandardMaterial({ color:0x3c4150, flatShading:true, roughness:1 });

const trunkMat = new THREE.MeshStandardMaterial({ color:0x4a3527, flatShading:true, roughness:1 });
const logMat   = new THREE.MeshStandardMaterial({ color:0x6a4a30, flatShading:true, roughness:1 });
const cutMat   = new THREE.MeshStandardMaterial({ color:0xb08a5c, flatShading:true, roughness:0.9 });
const foliageMats = [
  new THREE.MeshStandardMaterial({ color:0x1c3a28, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x22452e, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x17301f, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x1e3d33, flatShading:true, roughness:1 })
];

const trunkGeo = new THREE.CylinderGeometry(0.13, 0.26, 1, 5);
trunkGeo.translate(0, 0.5, 0);
const coneGeo = new THREE.ConeGeometry(1, 1, 6, 1);
coneGeo.translate(0, 0.5, 0);
const seatLogGeo = new THREE.CylinderGeometry(0.33, 0.31, 3.3, 7);
seatLogGeo.rotateZ(Math.PI / 2);
const fireLogGeo = new THREE.CylinderGeometry(0.14, 0.13, 1.7, 6);
fireLogGeo.translate(0, 0.85, 0);

/* ------------------------------------------------------------------ *
 *  trees  (44 of them, all outside the clearing)
 * ------------------------------------------------------------------ */
const treeGroup = new THREE.Group();
scene.add(treeGroup);
const TREE_COUNT = 44;
let placed = 0, guard = 0;
while (placed < TREE_COUNT && guard < 4000){
  guard++;
  const a = rnd() * Math.PI * 2;
  const r = 11 + Math.pow(rnd(), 0.65) * 52;
  const x = Math.cos(a) * r, z = Math.sin(a) * r;
  if (Math.hypot(x, z) < 10.5) continue;
  placed++;

  const s = rng(0.8, 1.7) * (r > 34 ? 1.15 : 1);
  const tree = new THREE.Group();

  const trunkH = 2.4 * s;
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.scale.set(s, trunkH, s);
  trunk.position.y = -0.1;
  tree.add(trunk);

  const mat = foliageMats[Math.floor(rnd() * foliageMats.length)];
  const layers = rnd() > 0.35 ? 3 : 2;
  for (let i = 0; i < layers; i++){
    const f = new THREE.Mesh(coneGeo, mat);
    const rad = (1.75 - i * 0.45) * s * rng(0.9, 1.12);
    const h   = (2.5 - i * 0.32) * s;
    f.scale.set(rad, h, rad);
    f.position.y = trunkH * 0.52 + i * 1.15 * s;
    f.rotation.y = rnd() * Math.PI;
    tree.add(f);
  }
  tree.position.set(x, 0, z);
  tree.rotation.y = rnd() * Math.PI * 2;
  const h = Math.hypot(x, z);
  const casts = h < 24;
  tree.traverse(o => { if (o.isMesh){ o.castShadow = casts; o.receiveShadow = true; } });
  treeGroup.add(tree);
}

/* low shrubs / boulders further out */
for (let i = 0; i < 16; i++){
  const a = rnd() * Math.PI * 2, r = rng(9, 46);
  const m = new THREE.Mesh(rockGeos[i % 3], rockMatDark);
  const s = rng(0.7, 2.3);
  m.scale.set(s * rng(0.9, 1.4), s * rng(0.6, 1), s * rng(0.9, 1.4));
  m.position.set(Math.cos(a) * r, m.scale.y * 0.35, Math.sin(a) * r);
  m.rotation.set(rnd(), rnd() * 6, rnd());
  m.castShadow = r < 24; m.receiveShadow = true;
  scene.add(m);
}

/* ------------------------------------------------------------------ *
 *  clearing props: rocks, seat logs, fire pit, firewood
 * ------------------------------------------------------------------ */
// ring of stones around the fire
for (let i = 0; i < 11; i++){
  const a = (i / 11) * Math.PI * 2 + rng(-0.12, 0.12);
  const m = new THREE.Mesh(rockGeos[i % 3], i % 2 ? rockMat : rockMatDark);
  const s = rng(0.3, 0.52);
  m.scale.set(s * rng(1, 1.5), s * rng(0.7, 1.1), s * rng(1, 1.4));
  m.position.set(Math.cos(a) * 2.05, m.scale.y * 0.45, Math.sin(a) * 2.05);
  m.rotation.set(rnd(), rnd() * 6, rnd());
  m.castShadow = true; m.receiveShadow = true;
  scene.add(m);
}
// loose rocks in the clearing
for (let i = 0; i < 9; i++){
  const a = rnd() * Math.PI * 2, r = rng(3.6, 9.5);
  const m = new THREE.Mesh(rockGeos[(i + 1) % 3], i % 3 ? rockMatDark : rockMat);
  const s = rng(0.35, 1.0);
  m.scale.set(s * rng(1, 1.6), s * rng(0.55, 0.95), s * rng(1, 1.5));
  m.position.set(Math.cos(a) * r, m.scale.y * 0.4, Math.sin(a) * r);
  m.rotation.set(rnd() * 0.6, rnd() * 6, rnd() * 0.6);
  m.castShadow = true; m.receiveShadow = true;
  scene.add(m);
}
// log seats around the fire
for (let i = 0; i < 4; i++){
  const a = (i / 4) * Math.PI * 2 + rng(-0.3, 0.3) + 0.5;
  const r = rng(4.1, 5.0);
  const g = new THREE.Group();
  const log = new THREE.Mesh(seatLogGeo, logMat);
  log.castShadow = true; log.receiveShadow = true;
  g.add(log);
  const end1 = new THREE.Mesh(new THREE.CircleGeometry(0.33, 7), cutMat);
  end1.rotation.y = Math.PI / 2; end1.position.x = 1.65; g.add(end1);
  const end2 = new THREE.Mesh(new THREE.CircleGeometry(0.31, 7), cutMat);
  end2.rotation.y = -Math.PI / 2; end2.position.x = -1.65; g.add(end2);
  g.position.set(Math.cos(a) * r, 0.33, Math.sin(a) * r);
  g.rotation.y = a + Math.PI / 2 + rng(-0.15, 0.15);
  g.rotation.z = rng(-0.04, 0.04);
  scene.add(g);
}
// scorched earth disc
const scorch = new THREE.Mesh(
  new THREE.CircleGeometry(2.9, 14),
  new THREE.MeshStandardMaterial({ color:0x0c0a09, roughness:1, flatShading:true })
);
scorch.rotation.x = -Math.PI / 2;
scorch.position.y = 0.02;
scorch.receiveShadow = true;
scene.add(scorch);

// teepee of firewood
const wood = new THREE.Group();
for (let i = 0; i < 7; i++){
  const holder = new THREE.Group();
  holder.rotation.y = (i / 7) * Math.PI * 2 + rng(-0.18, 0.18);
  const m = new THREE.Mesh(fireLogGeo, logMat);
  m.position.x = rng(0.72, 0.95);
  m.rotation.z = rng(0.5, 0.68);
  m.rotation.y = rng(-0.2, 0.2);
  m.castShadow = true;
  holder.add(m);
  wood.add(holder);
}
wood.position.y = 0.06;
scene.add(wood);

// glowing embers at the base
const emberMat = new THREE.MeshStandardMaterial({
  color:0x2a0d05, emissive:0xff3d00, emissiveIntensity:2.4, flatShading:true, roughness:1
});
const embers = new THREE.Group();
for (let i = 0; i < 18; i++){
  const a = rnd() * Math.PI * 2, r = rnd() * 1.25;
  const m = new THREE.Mesh(new THREE.IcosahedronGeometry(rng(0.09, 0.2), 0), emberMat);
  m.position.set(Math.cos(a) * r, rng(0.06, 0.42), Math.sin(a) * r);
  m.rotation.set(rnd(), rnd(), rnd());
  embers.add(m);
}
scene.add(embers);

/* ------------------------------------------------------------------ *
 *  flames
 * ------------------------------------------------------------------ */
const flameGroup = new THREE.Group();
flameGroup.position.y = 0.42;
scene.add(flameGroup);

const flameSpecs = [
  { r:0.72, h:3.1, c:0xff4a0c, o:0.34, seg:6, f1:5.2,  f2:9.1,  rot: 0.7 },
  { r:0.55, h:2.5, c:0xff8a1c, o:0.5,  seg:6, f1:6.4,  f2:11.3, rot:-0.9 },
  { r:0.40, h:1.95,c:0xffc23a, o:0.62, seg:5, f1:7.7,  f2:13.1, rot: 1.4 },
  { r:0.26, h:1.45,c:0xfff0b4, o:0.72, seg:5, f1:9.3,  f2:15.7, rot:-1.9 }
];
const flameParts = [];
flameSpecs.forEach((s, i) => {
  const geo = new THREE.ConeGeometry(s.r, s.h, s.seg, 1);
  geo.translate(0, s.h / 2, 0);
  const mat = new THREE.MeshBasicMaterial({
    color:s.c, transparent:true, opacity:s.o, depthWrite:false,
    blending:THREE.AdditiveBlending, flatShading:true, fog:false
  });
  const m = new THREE.Mesh(geo, mat);
  m.userData = {
    baseO:s.o, f1:s.f1, f2:s.f2, rot:s.rot,
    ph:rng(0, 6.28), sx:1, sy:1, sz:1
  };
  flameParts.push(m);
  flameGroup.add(m);
});

// detached tongues that rise off the fire
const tongueGeo = new THREE.OctahedronGeometry(0.4, 0);
const tongues = [];
for (let i = 0; i < 6; i++){
  const m = new THREE.Mesh(tongueGeo, new THREE.MeshBasicMaterial({
    color: i % 2 ? 0xffa027 : 0xff5f14, transparent:true, opacity:0.6,
    blending:THREE.AdditiveBlending, depthWrite:false, flatShading:true, fog:false
  }));
  m.userData = { off: rnd(), speed: rng(0.45, 0.8), wob: rng(1.4, 3.2), base: rng(0.5, 1.0) };
  tongues.push(m);
  flameGroup.add(m);
}

// soft light-bloom sprite at the heart of the fire
const bloom = new THREE.Sprite(new THREE.SpriteMaterial({
  map:glowTex, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, fog:false, opacity:0.85
}));
bloom.scale.set(7.5, 7.5, 1);
bloom.position.y = 1.4;
scene.add(bloom);

/* ------------------------------------------------------------------ *
 *  sparks — simple particle system
 * ------------------------------------------------------------------ */
const SPARK_COUNT = 210;
const sparkPos = new Float32Array(SPARK_COUNT * 3);
const sparkCol = new Float32Array(SPARK_COUNT * 3);
const sparks = [];
function resetSpark(d, initial){
  const a = rnd() * Math.PI * 2, r = rnd() * 1.05;
  d.x = Math.cos(a) * r; d.z = Math.sin(a) * r;
  d.y = 0.55 + rnd() * 1.1;
  d.vx = (rnd() - 0.5) * 0.55;
  d.vz = (rnd() - 0.5) * 0.55;
  d.vy = 1.3 + rnd() * 2.6;
  d.maxLife = 1.5 + rnd() * 2.3;
  d.life = initial ? rnd() * d.maxLife : d.maxLife;
}
for (let i = 0; i < SPARK_COUNT; i++){
  const d = { x:0, y:0, z:0, vx:0, vy:0, vz:0, life:0, maxLife:1 };
  resetSpark(d, true);
  sparks.push(d);
}
const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkCol, 3));
const sparkPoints = new THREE.Points(sparkGeo, new THREE.PointsMaterial({
  size:0.2, map:sparkTex, vertexColors:true, transparent:true,
  blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true, fog:false
}));
sparkPoints.frustumCulled = false;
scene.add(sparkPoints);

const colHot = new THREE.Color(0xfff3c0), colMid = new THREE.Color(0xff8a1e),
      colCold = new THREE.Color(0x8e1c03), tmpCol = new THREE.Color();

/* ------------------------------------------------------------------ *
 *  animation
 * ------------------------------------------------------------------ */
const clock = new THREE.Clock();
let t = 0;

function flickerNoise(x){
  return 0.62
    + 0.18 * Math.sin(x * 11.3)
    + 0.11 * Math.sin(x * 23.7 + 1.7)
    + 0.09 * Math.sin(x * 6.1 + 0.4)
    + 0.06 * Math.sin(x * 37.5 + 2.9);
}

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

  // --- firelight flicker ---
  const f = flickerNoise(t);
  fireLight.intensity = 34 + f * 24;
  fireLight.position.set(Math.sin(t * 7.3) * 0.16, 1.35 + Math.sin(t * 5.1) * 0.16, Math.cos(t * 6.4) * 0.16);
  fillLight.intensity = 3.5 + f * 4;
  bloom.material.opacity = 0.6 + f * 0.35;
  bloom.scale.setScalar(6.6 + f * 1.8);
  emberMat.emissiveIntensity = 1.6 + f * 2.2;

  // --- flames ---
  flameParts.forEach(m => {
    const u = m.userData;
    const w1 = Math.sin(t * u.f1 + u.ph);
    const w2 = Math.sin(t * u.f2 + u.ph * 1.7);
    m.scale.y = u.sy * (1 + 0.24 * w1 + 0.11 * w2);
    m.scale.x = u.sx * (1 - 0.10 * w2);
    m.scale.z = u.sz * (1 - 0.10 * w1);
    m.rotation.y += dt * u.rot;
    m.position.x = 0.11 * Math.sin(t * u.f1 * 1.5 + u.ph);
    m.position.z = 0.11 * Math.cos(t * u.f2 * 1.2 + u.ph);
    m.position.y = 0.05 * Math.sin(t * 4.2 + u.ph);
    m.material.opacity = u.baseO * (0.7 + 0.4 * (0.5 + 0.5 * Math.sin(t * u.f1 * 0.9 + u.ph)));
  });

  tongues.forEach(m => {
    const u = m.userData;
    const c = ((t * u.speed + u.off) % 1 + 1) % 1;
    m.position.y = 0.7 + c * 3.0;
    m.position.x = Math.sin(c * 6.283 * u.wob + u.off * 9) * 0.3 * (0.35 + c);
    m.position.z = Math.cos(c * 5.1 + u.off * 6) * 0.3 * (0.35 + c);
    const s = (1 - c) * 0.62 + 0.1;
    m.scale.setScalar(s * u.base);
    m.rotation.y += dt * 1.5;
    m.material.opacity = Math.max(0, (1 - c) * 0.62);
  });

  // --- sparks ---
  for (let i = 0; i < SPARK_COUNT; i++){
    const d = sparks[i];
    d.life -= dt;
    if (d.life <= 0){
      resetSpark(d, false);
    } else {
      d.vy -= 0.5 * dt;
      d.vx += Math.sin(t * 2.6 + d.y * 3.1) * 0.95 * dt;
      d.vz += Math.cos(t * 2.2 + d.y * 2.7) * 0.95 * dt;
      d.vx *= (1 - 1.1 * dt);
      d.vz *= (1 - 1.1 * dt);
      d.x += d.vx * dt; d.y += d.vy * dt; d.z += d.vz * dt;
    }
    sparkPos[i*3] = d.x; sparkPos[i*3+1] = d.y; sparkPos[i*3+2] = d.z;

    const age = 1 - d.life / d.maxLife;   // 0 fresh -> 1 burnt out
    if (age < 0.5) tmpCol.copy(colHot).lerp(colMid, age / 0.5);
    else tmpCol.copy(colMid).lerp(colCold, (age - 0.5) / 0.5);
    const fade = 0.35 + 0.65 * (1 - age);
    sparkCol[i*3] = tmpCol.r * fade;
    sparkCol[i*3+1] = tmpCol.g * fade;
    sparkCol[i*3+2] = tmpCol.b * fade;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;

  // --- gentle star twinkle ---
  brightStars.material.opacity = 0.75 + 0.25 * Math.sin(t * 1.7);
  dimStars.material.opacity = 0.7 + 0.12 * Math.sin(t * 0.9 + 1.1);

  controls.update();
  if (camera.position.y < 1.2) camera.position.y = 1.2;   // hard floor
  renderer.render(scene, camera);
}
animate();

/* ------------------------------------------------------------------ *
 *  resize
 * ------------------------------------------------------------------ */
function onResize(){
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.setSize(window.innerWidth, window.innerHeight);
}
window.addEventListener('resize', onResize);
onResize();
</script>
</body>
</html>
Reasoning trace (8,779 tokens)
Let me build a single HTML file with Three.js, import map, low-poly campfire scene.

Key elements:
- Ground: circle/plane with vertex noise, dark color, low-poly (flat shading).
- Rocks: icosahedron/dodecahedron geometry scaled randomly, flat shading.
- Logs around fire: cylinders (6 segments) laid flat radially, plus firewood in fire.
- Trees: cone/faceted trunk — at least 30, randomly placed avoiding clearing radius.
- Flames: animated — layered cone geometries with custom shader or sprite-like meshes. Low-poly flame: several cones/octahedrons scaling, jittering, additive material, emissive colors.
- Point light with flicker, castShadow.
- Sparks: Points particle system with additive blending.
- Night sky: big sphere with dark color/backside, stars Points, moon sphere with emissive/ MeshBasicMaterial + maybe glow sprite.
- Fog: scene.fog = FogExp2 or Fog.
- OrbitControls with maxPolarAngle so camera stays above ground, min/max distance, target y clamp.
- Renderer resize.

Add some UI overlay: title, hint "Drag to orbit · scroll to zoom". Not a game so no start needed, but keyboard... OrbitControls uses mouse; iframe only receives keyboard after click — mouse works without click? Pointer events in iframe should work. Keep simple.

Performance: shadow map with point light (cube shadow) — can be expensive but okay. Use 1024 map.

Let me write code.

Trees: create trunk (cylinder 5-seg) + 2-3 cones. Use shared geometries, per-tree group. 30-45 trees. Place randomly in annulus radius 14..48, avoid center 8. Also some closer rocks.

Ground: use PlaneGeometry with segments 40, displace y with noise, flat shading via computeVertexNormals with non-indexed geometry (toNonIndexed) for faceted look. Actually flatShading: true on material works for faceted look.

Ground size 160x160, fog fades distant trees.

Sky: sphere radius 300, BackSide, MeshBasicMaterial dark gradient — could use vertex colors for gradient. Simple: use a shader material or just a dark color. I'll do a gradient using ShaderMaterial inline (allowed, it's our own shader).

Stars: Points with random positions on dome, size attenuation false, white.

Moon: sphere at some position, MeshBasicMaterial light color, plus a glow sprite using canvas texture (canvas texture is generated inline — fine, no external assets). Or a ring of larger points. I'll do moon sphere + additive glow sprite with canvas radial gradient texture.

Flames: I'll make flame as a custom shader cone? Simpler: group of 5-7 cones/octahedra with MeshBasicMaterial additive blending, colors orange/yellow/red, animating scale.y, position.y, rotation.y, and opacity via sin. Plus a core.

Actually a nicer low-poly flame: use a cone with 6 segments and vertex displacement in shader for flicker. But simple approach with multiple meshes reads well.

Let's do: flame group:
- inner cone (radius 0.5, height 1.6, radialSegments 5) color 0xffe8a0 additive
- mid cone radius 0.65 height 2.4 color 0xff9a2e
- outer cone radius 0.8 height 3.0 color 0xff5a10, opacity 0.55
Animate: each scale.y oscillates with noise, rotation.y spins slowly, position jitter.

Plus a couple of small detached flame blobs rising (octahedrons) fading.

Embers/lofting flame pieces: reuse spark system.

Sparks: THREE.Points with BufferGeometry, N=180, attributes position, velocity in JS array, life. Update each frame; color per-vertex (orange/yellow), size attenuation true, additive, texture: canvas soft dot.

Fire base: logs arranged in teepee: 6 cylinders rotated forming cone, plus glowing embers (small icosahedron with emissive).

Fire bed: circle of darker ground/scorch, ash ring.

Light: PointLight 0xff7b२e intensity flickering with noise, distance 40, decay 2, castShadow true, shadow.bias -0.005. Also small ambient/hemisphere light dim blue for moonlight. Directional light from moon for shadows of distant trees? Point light shadow only covers nearby — fine.

Camera: position (10, 6, 12), target (0,1.5,0). OrbitControls: maxPolarAngle = Math.PI/2 - 0.08 (keeps above ground), minDistance 5, maxDistance 45, enableDamping, target y clamp maybe.

Also clamp camera y >= 1.2 in animation loop.

Resize: window resize handler with pixel ratio min 2.

Let me also add subtle smoke particles? Optional, keep sparks.

UI overlay: small title top-left, controls hint bottom-left, subtle vignette via CSS radial gradient overlay (pointer-events none).

Write the code carefully.

Random with seed for consistent look? Not necessary but nice. I'll use a seeded PRNG so layout is stable.

Trees placed avoiding clearing: radius > 10 and < 55, also avoid a path. Also add smaller saplings.

Ground color: dark desaturated green-brown 0x1b2418. Flat shading with slight color variation per vertex? Vertex colors: vary brightness.

Let me write ground with vertex colors: for each vertex, compute noise -> color mix between dark green and slightly lighter.

Implementation:

```js
const gGeo = new THREE.PlaneGeometry(200,200,60,60);
gGeo.rotateX(-Math.PI/2);
const pos = gGeo.attributes.position;
const colors=[];
for(i...){
 const x=pos.getX(i), z=pos.getZ(i);
 const d=Math.sqrt(x*x+z*z);
 let h = Math.sin(x*0.13)*Math.cos(z*0.11)*0.6 + Math.sin(x*0.31+z*0.27)*0.25;
 h *= Math.min(1, d/12); // flat clearing near fire
 // slight rise far away
 h += Math.max(0, d-30)*0.03;
 pos.setY(i, h);
 ...
}
gGeo.computeVertexNormals();
```
With flatShading material and vertexColors. Note: flatShading with indexed geometry still works (uses face normals via derivative? In three, flatShading uses computed flat normals in shader via derivatives — yes, WebGL2 standard). Fine.

Also need non-indexed for vertex colors per-face variation; not required.

Rocks: IcosahedronGeometry(1,0) scaled non-uniformly, flatShading, color grey 0x4a4a4f. Place 8-10 rocks around clearing at radius 4-9, plus some further.

Logs around fire: cylinders radius 0.28, length 3, radialSegments 6, lying flat, arranged tangentially around at radius 3.5? Requirement: "a few low-poly rocks and logs around a campfire". So logs lying on ground around fire, seats. Place 4 logs at radius ~4.5, oriented tangentially, at angles.

Firewood in fire: 5 logs teepee.

Trees: use Group per tree with trunk cylinder (5 seg) and 2-3 cones. Materials shared: trunkMat, foliageMat (with flat shading, colors varying green-blue for night: 0x1d3324, 0x24402c, 0x16281e).

Some trees larger scale.

Also fog: FogExp2(0x070b14, 0.016) or Fog(0x070b14, 30, 110). Requirement: "fog that fades the distant trees" — use Fog with near 25 far 110? Use FogExp2 density 0.018 — with sky sphere affected? Sky material fog:false. Points fog false too.

Stars: fog would affect them if fog enabled on material; Points material fog default true — set fog:false.

Moon: at (-60, 55, -80) radius 6, material fog:false.

Moonlight directional light with slight shadow? Adds cost. I'll add a dim directional light (no shadow) plus hemisphere.

Renderer: antialias, shadowMap PCFSoft, toneMapping ACESFilmic, outputColorSpace sRGB (default in r170). exposure 1.05.

Let's write the spark system:

```js
const SPARKS=220;
positions Float32Array(SPARKS*3), colors, sizes
state arrays: life, maxLife, vx,vy,vz
```
PointsMaterial with vertexColors, size 0.16, sizeAttenuation, map dotTexture, transparent, blending Additive, depthWrite false, fog false.

Update: life -= dt; if life<=0 respawn at random position within fire radius, velocity up 1.5-3.5, lateral small; else p += v*dt; v.y -= 0.6*dt (slight), v.x += sin(t*..)*dt turbulence. color from yellow->red->dark as life decreases; set alpha via color fade (since additive, fading color = fade out).

Also a second slow smoke layer? skip.

Flames flicker: use a helper noise function: `const flick = 0.75 + 0.25*Math.sin(t*11.3)+0.12*Math.sin(t*23.7+1.2)+0.08*Math.sin(t*7.1)` normalized.

Light intensity = base * flick; also position slight jitter.

Let me write the flame mesh animation:

```js
flameParts.forEach((m,i)=>{
  const p=m.userData;
  const s = 1 + 0.18*Math.sin(t*p.f1+p.o) + 0.1*Math.sin(t*p.f2+p.o*2);
  m.scale.set(p.sx*(2-s*0.3)... 
});
```
Simpler: scale.y = p.sy * (0.85 + 0.3*sin), scale.x/z = p.sx * (1 + 0.12*sin), rotation.y += dt*speed, position.x/z jitter = sin.

Also base flames should stay anchored at bottom: cone geometry origin is center; translate geometry up by height/2 so scaling grows upward. I'll create geometry then `.translate(0, h/2, 0)`.

Flame group at y=0.35 on top of embers.

Also glow: a small additive sprite at fire center (radial gradient) to fake light bloom, scale pulsing.

Add ember bed: ring of glowing small icosahedra with emissive material 0xff5510.

Now write HTML.

Camera clamp: after controls.update(), if(camera.position.y < 1.0) camera.position.y = 1.0. Also maxPolarAngle = Math.PI*0.48.

Also handle pixel ratio and resize.

Let me consider loading overlay? Not needed; everything is synchronous (no external textures except canvas generated). Screenshot at 3s will show scene.

Add a subtle "Click and drag to look around" hint.

Trees placement: avoid the fire area radius 9; also create a natural tree line. Generate: for i<46: angle random, radius = 12 + Math.pow(rand,0.6)*45. Ensure density. Also some far ones at radius 60-80 for silhouette? Fog far 130.

Ground 260x260 with fog far 140.

Sky sphere radius 400 — must be within camera far (default 2000? PerspectiveCamera default far=2000). Set far 1200.

Stars on dome radius 380.

Let's write final code.

Star texture: use PointsMaterial with size attenuation false, size 1.6, color white, and a round sprite? Points render as squares; small squares fine for stars. I'll give a tiny canvas texture for round-ish stars with fog false, alphaTest maybe. Use map with transparent.

Canvas textures: make function dotTexture(size, color) drawing radial gradient.

Moon glow: Sprite with glow texture, additive, scale 40.

OK, code:

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

Note importmap must come before module script; can be in head or body. Fine.

Now detailed code.

Seeded random:
```js
let seed = 1337;
function rnd(){ seed = (seed*1664525 + 1013904223) % 4294967296; return seed/4294967296; }
function rng(a,b){return a + (b-a)*rnd();}
```

Trees function:

```js
function makeTree(scale, hue){
  const g = new THREE.Group();
  const trunkH = 2.2*scale;
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.scale.set(scale*0.9, scale, scale*0.9); // trunkGeo height 1 centered? define trunkGeo = CylinderGeometry(0.16,0.26,1,5) translated y+0.5
  ...
}
```
Let's define trunkGeo = CylinderGeometry(0.14,0.24,1,5); trunkGeo.translate(0,0.5,0). Then mesh scale y = trunkH.

Foliage cones: ConeGeometry(1,1,6) translated (0,0.5,0). Layer 1: at y=trunkH*0.75, radius 1.5*scale, height 2.6*scale. Layer2 higher smaller. Layer3 top.

Actually classic: three stacked cones with decreasing radius, each base overlapping.

```js
const layers = 3;
for(let i=0;i<layers;i++){
  const f = new THREE.Mesh(coneGeo, foliageMat);
  const r = (1.7 - i*0.45)*scale;
  const h = (2.4 - i*0.35)*scale;
  f.scale.set(r, h, r);
  f.position.y = trunkH*0.6 + i*1.35*scale;
  g.add(f);
}
```
Hmm the cone geometry translated so base at 0 — position.y sets base height. Layers overlap fine.

Colors: use 2-3 foliage materials with slightly different night greens; also vary by making material per tree? Sharing is better for perf; I'll use 3 shared materials chosen randomly.

Also add slight color variation via mesh material assignment.

For shadow casting: trees cast shadows (only within point light range anyway) — set castShadow true, receiveShadow true. Ground receiveShadow.

Point light shadow camera: PointLightShadow with camera far 45, mapSize 1024, bias -0.008, normalBias 0.05? For point lights, shadow.normalBias exists. Set shadow.camera.near=0.5, far=40.

Also directional moon light: castShadow false (to save perf) — but then distant objects have no shadows, fine at night.

Now flame glow sprite texture: create radial gradient canvas 128.

Let's write the animation loop:

```js
let t=0;
function animate(){
 requestAnimationFrame(animate);
 const dt = Math.min(clock.getDelta(), 0.05);
 t += dt;
 updateFlames(t, dt);
 updateSparks(t, dt);
 controls.update();
 renderer.render(scene,camera);
}
```

Sparks respawn.

Flames: build array of meshes with userData params.

```js
function buildFlame(){
  const grp = new THREE.Group();
  const specs = [
   {r:0.55,h:2.6,c:0xff5b12,o:0.42,seg:6,sp:0.6},
   {r:0.42,h:2.1,c:0xff9422,o:0.6,seg:5,sp:-0.9},
   {r:0.3,h:1.6,c:0xffc84a,o:0.75,seg:5,sp:1.4},
   {r:0.18,h:1.15,c:0xfff0b8,o:0.9,seg:4,sp:-1.8},
  ];
  specs.forEach((s,i)=>{
    const geo = new THREE.ConeGeometry(s.r, s.h, s.seg, 2, true?) 
```
Hmm open-ended cone vs closed. Use non-open (closed) so bottom cap exists. Cone with heightSegments 1 for low-poly.

translate(0, s.h/2, 0).

material: MeshBasicMaterial({color:s.c, transparent:true, opacity:s.o, blending:AdditiveBlending, depthWrite:false, flatShading:true? (basic material supports flatShading)}).

Add per-mesh params: baseR, baseH, phase, speed, wobble.

Animation per part:
```js
const w = Math.sin(t*m.userData.f1 + m.userData.ph);
const w2 = Math.sin(t*m.userData.f2 + m.userData.ph*1.7);
m.scale.y = m.userData.sy * (1 + 0.22*w + 0.1*w2);
m.scale.x = m.userData.sx * (1 - 0.1*w2);
m.scale.z = m.userData.sz * (1 - 0.1*w);
m.rotation.y += dt*m.userData.rot;
m.position.x = 0.12*Math.sin(t*m.userData.f1*1.6 + m.userData.ph);
m.position.z = 0.12*Math.cos(t*m.userData.f2*1.3 + m.userData.ph);
m.position.y = 0.32 + 0.08*Math.sin(t*4+ph);
m.material.opacity = base * (0.75 + 0.35*(0.5+0.5*Math.sin(t*f1*1.1+ph)));
```

Plus 3 small "tongue" octahedrons rising and fading, respawn.

I'll add a separate set of rising flame blobs: array of meshes with life, respawn when life>1.

Let's keep it manageable: flame tongues: 4 meshes (OctahedronGeometry(0.3,0)) additive orange, each with own cycle: y = base + cycle*2.5, scale shrinks, opacity fades.

Fire core glow: PointLight + sprite.

Now ground scorch: circle geometry radius 2.6, color 0x0d0b0a, at y=0.02. Plus a ring of ash rocks? Fine.

Ring of stones around fire: 10 small rocks at radius 2.3.

Ok, write it all.

Colors and night look: toneMapping ACESFilmic with exposure 1.0.

Background: scene.background = sky? I'll add sky sphere mesh with ShaderMaterial gradient (fog:false, side BackSide, depthWrite false).

Gradient shader:
```js
const skyMat = new THREE.ShaderMaterial({
 uniforms:{ top:{value:new THREE.Color(0x030510)}, bottom:{value:new THREE.Color(0x0b1224)} },
 vertexShader:`varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix*modelViewMatrix*vec4(position,1.0);} `,
 fragmentShader:`varying vec3 vP; uniform vec3 top; uniform vec3 bottom; void main(){ float h = clamp(normalize(vP).y*0.5+0.5,0.0,1.0); vec3 c = mix(bottom, top, pow(h,0.7)); gl_FragColor = vec4(c,1.0);} `,
 side: THREE.BackSide, depthWrite:false, fog:false
});
```
In r170 with WebGL2, gl_FragColor still works (three injects compatibility? Actually three uses GLSL3 only for RawShaderMaterial with glslVersion set; default shaders are GLSL1 with WebGL2 backwards-compat, and three's ShaderMaterial with default glslVersion uses #version 300 es? No — three prepends "#version 300 es" only when glslVersion is GLSL3. For WebGL2 default, three converts built-in materials? Actually WebGLProgram always adds prefixVertex/prefixFragment which for WebGL2 includes... Let me recall: In three r150+, WebGLProgram's prefix includes `#version 300 es` when `isWebGL2 && ...`? Hmm.

Actually three.js: `parameters.glslVersion === GLSL3 ? '#version 300 es\n' : ''`. For default ShaderMaterial glslVersion is null → no version directive → GLSL ES 1.00 in WebGL2 context, which is supported (WebGL2 supports ES 1.00 shaders). And `gl_FragColor` works. Also three defines `#define varying` etc.? For ES1.00 in WebGL2, three's prefix for fragment includes compatibility defines when converting. In practice, custom ShaderMaterials using gl_FragColor and varying work fine in recent three. Yes, they do — three adds `#define texture2D texture` only in GLSL3 conversion path. Fine.

But `texture2D` in GLSL1 works. I'll avoid textures in sky.

Renderer: `renderer.outputColorSpace = THREE.SRGBColorSpace` default already in r152+.

Alright. Also `scene.fog = new THREE.Fog(0x0a1020, 20, 120)`.

Sky sphere radius 500 > fog... fog:false so fine. Camera far 1500.

Moon position: (-90, 80, -140) with radius 9? At distance ~190, angular size small. Let's place moon at (-140, 95, -200), radius 12 → looks decent. Actually with camera at ~15 units, moon far away appears tiny. Angular diameter = 2*12/270 ≈ 0.089 rad ≈ 5° — good.

Moonlight directional: position from moon direction, color 0x8fb4ff, intensity 0.35.

Hemisphere light: sky 0x16223a ground 0x0a0f0a intensity 0.35.

Fire light intensity: with physically correct lighting (r155+ default useLegacyLights=false), PointLight intensity is in candela; need higher values like 20-60 with decay 2. Let's set intensity ~ 35, distance 0 (infinite) or 50, decay 2. Hmm at distance 4, illuminance = 35/16 ≈ 2.2 — decent bright. Let's use base 45, flicker 30-60. I'll tune: intensity = 40 * flick.

Directional intensity 0.4 (irradiance-ish, fine). Hemisphere 0.25.

Point light shadow: mapSize 1024, camera far 40.

Sparks color: base color from life fraction: mix white-yellow→orange→dark red.

I'll precompute colors each frame for changed particles — 220 particles, fine.

Also small "embers on ground" glowing dots.

UI: top-left panel with title & bullet list; bottom hint. Keep semi-transparent, non-blocking (pointer-events:none except maybe). OrbitControls need pointer events on canvas — overlay with pointer-events:none.

Add vignette overlay div with radial-gradient.

Mobile: media query shrinks fonts.

Let's write final code now. Also handle window resize with `window.addEventListener('resize')`.

Trees count: 42 (>30) guaranteed.

Also make sure trees not on fire: radius check.

I'll also add some tree stumps / bushes? Optional — add ~12 low-poly bushes (icosahedron scaled) for variety.

Let me draft the code text fully.

```js
// ---------- setup ----------
const canvas = document.createElement('canvas'); ... 
```
Simpler: renderer = new THREE.WebGLRenderer({antialias:true}); renderer.setSize(innerWidth, innerHeight); document.body.appendChild(renderer.domElement).

Ensure body margin 0, overflow hidden, canvas display block.

Now write everything.

Rock geometry: IcosahedronGeometry(1, 0) with vertex jitter for irregularity — do once per rock variant: create 4 variants by cloning geometry and randomly moving vertices.

```js
function rockGeo(){
  const g = new THREE.PlaneGeometry... no
  const g = new THREE.IcosahedronGeometry(1,0);
  const p = g.attributes.position;
  for(let i=0;i<p.count;i++){
    p.setXYZ(i, p.getX(i)*rng(0.8,1.2), p.getY(i)*rng(0.6,1.1), p.getZ(i)*rng(0.8,1.2));
  }
  g.computeVertexNormals();
  return g;
}
```
Note: Icosahedron geometry is non-indexed? IcosahedronGeometry from PolyhedronGeometry is non-indexed (each face separate vertices), so per-vertex random scaling creates gaps. Better: displace by a deterministic function of position (hash of rounded coords) so shared positions move identically:

```js
function hash(x,y,z){ const s = Math.sin(x*12.9898 + y*78.233 + z*37.719)*43758.5453; return s - Math.floor(s); }
```
Then factor = 0.85 + 0.3*hash(...). Consistent for duplicate vertices. Good.

For ground flat shading with indexed PlaneGeometry — flatShading works.

Log: CylinderGeometry(0.3,0.3,3,7) rotate to lie along X then place.

Firewood teepee: 6 logs, each rotated: log length 2.4, positioned leaning: create group, for i: mesh.rotation.z = ... Let's do: log mesh with geometry translated so origin at one end (translate(0, 1.2, 0)), then rotate to lean inward. Set group at center; each log: rotation to tilt at angle ~30° from vertical, oriented around center by rotating group Y.

Implementation:
```js
const wood = new THREE.Group();
for(let i=0;i<6;i++){
  const m = new THREE.Mesh(logGeo, logMat); // logGeo: cylinder r .14 h 1.6, translated y+0.8
  const holder = new THREE.Group();
  holder.rotation.y = i/6*Math.PI*2 + rng(-0.2,0.2);
  m.rotation.z = rng(0.5,0.75); // tilt outward from vertical... 
  m.position.set(0.55, 0, 0); // push out along holder x
  ...
}
```
Hmm. Simpler: log leaning with base at radius 0.75 on ground and top near center at height 1.3.

Define: a pivot group at ground point (r=0.8 along +x of holder), rotate about z by angle so the log tilts toward center: if log extends +y after translate, rotating z by +angle tilts it toward -x. So rotation.z = +0.55 gives top leaning toward -x (toward center). Good: holder.rotation.y = theta; m.position at (0.8,0,0) in holder space; m.rotation.z = 0.55 ±.

But m.position should be base point; with geometry translated +y/2 base at m origin. Good. Set r=0.85, tilt 0.6 rad: top at height 1.6*cos(0.6)=1.32, x = 0.85 - 1.6*sin(0.6)=0.85-0.90=-0.05 ≈ center. 

Seat logs: 4 logs around at radius 4.2, tangent direction: rotation.y = theta + PI/2, lying along x after rotateZ(PI/2)? Cylinder default axis Y; rotate geometry: logSeatGeo = CylinderGeometry(0.34,0.30,3.2,7); logSeatGeo.rotateZ(Math.PI/2) → axis along X. Then mesh.rotation.y = theta + Math.PI/2 gives tangential. Position y=0.32.

Rocks around: 8 at radius 3.6-6, plus rocks at clearing edge.

Okay let's write the final file. I need to be careful and complete.

Sparks texture: canvas 64x64 radial gradient white→transparent.

Write code:

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

Star texture: small round white dot with sharp falloff.

Now the full HTML. Let me write carefully and completely.

Star positions: random on sphere radius 360, only y > -0.05 (above horizon). Count 900. Sizes: use per-point size attribute? PointsMaterial doesn't support per-point size without shader. Could use two Points systems: normal stars and bright stars (size bigger). I'll do 2 systems: 900 dim (size 1.4), 90 flickering bright (size 2.6). Actually simple: one with size 1.8, another with 3.0 for a few.

Twinkle: animate opacity of bright star material sin.

Moon: sphere + sprite glow + maybe a thin ring? Keep sphere+glow.

Also I should ensure the sky/stars are visible above horizon given camera looks slightly down. Camera at y=5.5 looking at (0,1.6,0) → horizon visible. Fine.

Now: performance with 42 trees * 4 meshes = 168 meshes + rocks etc. Fine.

Shadow: point light shadow renders 6 faces with all objects; set castShadow only for nearby objects (rocks, logs, firewood, trees within 20 units). Distant trees castShadow false to save time.

Let's finalize.

One more: `controls.maxPolarAngle = Math.PI/2 - 0.05` prevents going below ground plane. Also clamp camera.position.y >= 1.0 each frame after controls.update().

Write the UI:

```html
<div id="hud">
  <h1>Night Clearing</h1>
  <p>low-poly campfire · three.js</p>
</div>
<div id="hint">Drag to orbit · scroll / pinch to zoom</div>
<div id="vig"></div>
```

Add a legend of features maybe. Keep it subtle.

Now full code writing.

Also `renderer.setPixelRatio(Math.min(devicePixelRatio,2))`.

Let me write the JS in order:

1. renderer, scene, camera, controls
2. lights
3. sky, stars, moon
4. ground
5. rocks, seat logs, fire pit, firewood, ember bed
6. trees, bushes
7. flames, sparks, glow
8. animate loop
9. resize

I'll write it now.

Flame tongues (rising blobs): create 5 octahedron meshes; each has cycle offset; update:
```js
tongues.forEach(m=>{
 const c = (t*m.userData.speed + m.userData.off) % 1;
 m.position.y = 0.5 + c*2.6;
 m.position.x = Math.sin(c*6.28*m.userData.wob + m.userData.off*7)*0.25*(0.4+c);
 m.position.z = Math.cos(c*5.1 + m.userData.off*5)*0.25*(0.4+c);
 const s = (1-c)*0.55+0.15;
 m.scale.setScalar(s * m.userData.base);
 m.material.opacity = (1-c)*0.6;
});
```
Materials need per-mesh instances since opacity differs → clone material for tongues.

Sparks: also glow. Fine.

Ember bed: 14 small icosahedron meshes with MeshStandardMaterial emissive 0xff4400, emissiveIntensity animated? Shared material — animate intensity globally with flicker. Good.

Also add MeshStandardMaterial for ground etc so fire light lights them.

Materials:
- ground: MeshStandardMaterial({vertexColors:true, flatShading:true, roughness:1, metalness:0, color:0xffffff}) with vertex colors darkish.
- rock: MeshStandardMaterial({color:0x50525c, flatShading:true, roughness:0.95})
- bark: 0x4a3527 flat
- foliage: 0x1b3a26 etc flat, roughness 1
- log: 0x5a3f2a with bark.

Let's write vertex colors for ground: base color mixing between 0x16211a and 0x232b1d with noise, and near fire slightly warm/dark.

Distance-based: near fire (d<6) darker scorched 0x120f0c; mid grass green; far bluish.

Ok final code. Also add scattered grass tufts? Skip.

Let me count trees: `const TREE_COUNT = 44;`

Writing now, final answer.

Careful about `controls.target` and camera init.

const camera = new THREE.PerspectiveCamera(52, aspect, 0.1, 1600);
camera.position.set(9.5, 5.4, 12.5);

controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0,1.5,0);
controls.enableDamping = true; dampingFactor 0.06;
controls.minDistance = 5; maxDistance = 60;
controls.maxPolarAngle = Math.PI*0.49;
controls.minPolarAngle = 0.15;
controls.enablePan = false (keeps target fixed so it can't go below ground) — good idea, plus rotate speed 0.7.

Also autoRotate slowly? Might be nice for a demo (self-playing-ish). Gentle autoRotate at 0.35 speed, stops on user interaction? Could conflict with screenshot? It's nice: scene slowly orbits. I'll enable autoRotate with speed 0.4 and disable on pointerdown/wheel. Actually keep it: makes demo lively. But "click or press key" requirement is for games only. I'll keep autoRotate and disable on interaction.

Hmm, if the screenshot is taken at 3s, autoRotate gives a slightly different view — fine.

Let me write out the code.

Spark update details:

```js
const SPARK_COUNT = 200;
const sparkPos = new Float32Array(SPARK_COUNT*3);
const sparkCol = new Float32Array(SPARK_COUNT*3);
const sparkData = [];
for(let i=0;i<SPARK_COUNT;i++){
  sparkData.push({life:0, maxLife:1, x:0,y:0,z:0, vx:0,vy:0,vz:0});
  resetSpark(d, Math.random()); // random initial life offset so not all spawn at once
}
```
resetSpark(d, initial):
```js
function resetSpark(d, initial){
  const a = Math.random()*Math.PI*2;
  const r = Math.random()*1.0;
  d.x = Math.cos(a)*r; d.z = Math.sin(a)*r;
  d.y = 0.5 + Math.random()*1.2;
  d.vx = (Math.random()-0.5)*0.5;
  d.vz = (Math.random()-0.5)*0.5;
  d.vy = 1.2 + Math.random()*2.4;
  d.maxLife = 1.4 + Math.random()*2.2;
  d.life = initial ? Math.random()*d.maxLife : d.maxLife;
}
```
Update:
```js
d.life -= dt;
if(d.life<=0){ resetSpark(d,false); }
else {
 d.vy += 0.35*dt; // buoyant? sparks rise then slow; use slight upward accel small
 d.vx += Math.sin(t*2.4 + d.y*3)*0.9*dt;
 d.vz += Math.cos(t*2.1 + d.y*2.5)*0.9*dt;
 d.vx *= (1-1.2*dt); d.vz *= (1-1.2*dt);
 d.vy -= 0.55*dt;
 d.x += d.vx*dt; d.y += d.vy*dt; d.z += d.vz*dt;
}
sparkPos[i*3]=d.x; ... 
const u = d.life/d.maxLife; // 1→0
// color: white-hot -> orange -> deep red, fading
```
Color: 
```js
let c;
if(u>0.7) c = lerp yellow-white; else if(u>0.35) orange; else dark red.
```
Simplify: use THREE.Color objects and lerp:
```js
const colHot=new THREE.Color(0xfff0b0), colMid=new THREE.Color(0xff8a1e), colCold=new THREE.Color(0x9c1f04);
tmpCol.copy(colHot);
if(u<0.6) tmpCol.lerp(colMid, (0.6-u)/0.6 ... )
```
Just do: t = 1-u (0 fresh → 1 old): if t<0.5 lerp hot→mid by t/0.5 else lerp mid→cold by (t-0.5)/0.5, then multiplyScalar(0.4+0.6*(1-t)) to fade. Store.

Also y shouldn't exceed ~6, but life handles.

Set geometry attributes needsUpdate each frame.

Points material: size 0.18, sizeAttenuation true, vertexColors true, map dot, transparent, depthWrite false, blending Additive, fog false? sparks near fire so fog negligible; set fog:false to keep bright.

Ground receiveShadow true, trees receive/cast as noted.

Fire pit: stones ring radius 1.6, 9 rocks scale ~0.4.

Alright — also requirement "logs around a campfire" satisfied with 4 seat logs.

Let me now write out everything as final code. I'll be thorough but concise in comments.

Potential issue: `renderer.shadowMap.type = THREE.PCFSoftShadowMap`.

Point light shadow autoUpdate every frame with flickering position — ok.

Let me write.

Final check on adaptive: at 375px, HUD text sizes small, no horizontal scroll (overflow hidden).

Writing the answer now.
Raw response
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Night Clearing — Low-poly Campfire</title>
<style>
  *{box-sizing:border-box}
  html,body{margin:0;padding:0;height:100%;overflow:hidden;background:#05070f;
    font-family:"Trebuchet MS",Segoe UI,system-ui,sans-serif;color:#e9e2d2}
  canvas{display:block;width:100%;height:100%}
  #vignette{position:fixed;inset:0;pointer-events:none;
    background:radial-gradient(ellipse at 50% 62%, rgba(0,0,0,0) 30%, rgba(0,0,0,.35) 72%, rgba(0,0,0,.72) 100%)}
  #hud{position:fixed;top:18px;left:20px;pointer-events:none;max-width:min(70vw,340px)}
  #hud h1{margin:0;font-size:26px;letter-spacing:.14em;text-transform:uppercase;
    font-weight:700;color:#ffd9a0;text-shadow:0 0 18px rgba(255,140,40,.55),0 2px 6px rgba(0,0,0,.8)}
  #hud .sub{margin:6px 0 0;font-size:12.5px;letter-spacing:.24em;text-transform:uppercase;
    color:#8fa6c4;opacity:.9}
  #hud .tags{margin-top:12px;display:flex;flex-wrap:wrap;gap:6px}
  #hud .tags span{font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;
    padding:4px 8px;border:1px solid rgba(255,205,150,.28);border-radius:999px;
    background:rgba(20,16,12,.42);color:#f0d2ab;backdrop-filter:blur(3px)}
  #hint{position:fixed;left:0;right:0;bottom:16px;text-align:center;pointer-events:none;
    font-size:12.5px;letter-spacing:.18em;text-transform:uppercase;color:#b9c6da;
    text-shadow:0 2px 8px rgba(0,0,0,.9)}
  #hint b{color:#ffc07a;font-weight:600}
  #fpsdot{position:fixed;top:20px;right:20px;pointer-events:none;font-size:11px;
    letter-spacing:.16em;color:#7e8ea6;text-transform:uppercase;opacity:.75}
  @media (max-width:520px){
    #hud{top:12px;left:12px}
    #hud h1{font-size:18px}
    #hud .sub{font-size:10px;letter-spacing:.16em}
    #hud .tags span{font-size:9px;padding:3px 6px}
    #hint{font-size:10px;letter-spacing:.12em;bottom:12px}
    #fpsdot{display:none}
  }
</style>
</head>
<body>
<div id="hud">
  <h1>Night Clearing</h1>
  <p class="sub">low-poly campfire · three.js</p>
  <div class="tags">
    <span>44 trees</span><span>flickering firelight</span><span>sparks</span>
    <span>moon &amp; stars</span><span>fog</span>
  </div>
</div>
<div id="fpsdot">drag to orbit · scroll to zoom</div>
<div id="hint"><b>Click or drag</b> to look around · scroll / pinch to zoom</div>
<div id="vignette"></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';

/* ------------------------------------------------------------------ *
 *  seeded random so the clearing always looks the same
 * ------------------------------------------------------------------ */
let _seed = 20240711;
function rnd(){ _seed = (_seed * 1664525 + 1013904223) % 4294967296; return _seed / 4294967296; }
function rng(a, b){ return a + (b - a) * rnd(); }
function hash(x, y, z){
  const s = Math.sin(x * 12.9898 + y * 78.233 + z * 37.719) * 43758.5453;
  return s - Math.floor(s);
}

/* ------------------------------------------------------------------ *
 *  renderer / scene / camera / controls
 * ------------------------------------------------------------------ */
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.05;
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x0a1122, 26, 130);

const camera = new THREE.PerspectiveCamera(52, window.innerWidth / window.innerHeight, 0.1, 1600);
camera.position.set(10.5, 5.6, 13.5);

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.5, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.enablePan = false;
controls.minDistance = 5.5;
controls.maxDistance = 55;
controls.minPolarAngle = 0.18;
controls.maxPolarAngle = Math.PI * 0.485;   // never goes under the ground
controls.rotateSpeed = 0.7;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.35;
['pointerdown','wheel','touchstart'].forEach(ev =>
  renderer.domElement.addEventListener(ev, () => { controls.autoRotate = false; }, { passive:true }));

/* ------------------------------------------------------------------ *
 *  lights
 * ------------------------------------------------------------------ */
scene.add(new THREE.HemisphereLight(0x18284a, 0x0a1008, 0.35));

const moonLight = new THREE.DirectionalLight(0x8fb6ff, 0.5);
moonLight.position.set(-70, 60, -95);
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xff7a24, 42, 60, 2);
fireLight.position.set(0, 1.5, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.4;
fireLight.shadow.camera.far = 42;
fireLight.shadow.bias = -0.006;
fireLight.shadow.normalBias = 0.05;
scene.add(fireLight);

const fillLight = new THREE.PointLight(0xffb066, 6, 14, 2);
fillLight.position.set(0, 0.6, 0);
scene.add(fillLight);

/* ------------------------------------------------------------------ *
 *  canvas-generated sprites (no external assets)
 * ------------------------------------------------------------------ */
function radialTex(stops, size = 64){
  const c = document.createElement('canvas'); c.width = c.height = size;
  const g2 = c.getContext('2d');
  const grd = g2.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
  stops.forEach(s => grd.addColorStop(s[0], s[1]));
  g2.fillStyle = grd; g2.fillRect(0, 0, size, size);
  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  return t;
}
const sparkTex = radialTex([[0,'rgba(255,255,255,1)'],[0.35,'rgba(255,205,120,0.85)'],[1,'rgba(255,90,0,0)']]);
const glowTex  = radialTex([[0,'rgba(255,190,110,0.95)'],[0.35,'rgba(255,120,35,0.45)'],[1,'rgba(255,60,0,0)']], 128);
const starTex  = radialTex([[0,'rgba(255,255,255,1)'],[0.45,'rgba(220,235,255,0.7)'],[1,'rgba(180,210,255,0)']], 32);
const moonGlow = radialTex([[0,'rgba(215,232,255,0.85)'],[0.3,'rgba(150,185,255,0.28)'],[1,'rgba(90,130,255,0)']], 256);

/* ------------------------------------------------------------------ *
 *  night sky, stars, moon
 * ------------------------------------------------------------------ */
const sky = new THREE.Mesh(
  new THREE.SphereGeometry(700, 32, 20),
  new THREE.ShaderMaterial({
    uniforms:{ top:{ value:new THREE.Color(0x02030b) }, bottom:{ value:new THREE.Color(0x0d1730) } },
    vertexShader:`varying vec3 vP; void main(){ vP = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }`,
    fragmentShader:`varying vec3 vP; uniform vec3 top; uniform vec3 bottom;
      void main(){ float h = clamp(normalize(vP).y * 0.5 + 0.5, 0.0, 1.0);
        vec3 c = mix(bottom, top, pow(h, 0.65));
        gl_FragColor = vec4(c, 1.0); }`,
    side: THREE.BackSide, depthWrite:false, fog:false
  })
);
scene.add(sky);

function starField(count, radius, size, opacity){
  const pos = new Float32Array(count * 3);
  for (let i = 0; i < count; i++){
    const u = rnd() * 2 - 0.05, th = rnd() * Math.PI * 2;
    const y = Math.abs(u);
    const r = Math.sqrt(Math.max(0, 1 - u * u));
    pos[i*3]   = Math.cos(th) * r * radius;
    pos[i*3+1] = y * radius * 0.95 + 8;
    pos[i*3+2] = Math.sin(th) * r * radius;
  }
  const g = new THREE.BufferGeometry();
  g.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  const m = new THREE.PointsMaterial({
    size, map: starTex, transparent:true, opacity, depthWrite:false,
    blending: THREE.AdditiveBlending, color: 0xdfe9ff, sizeAttenuation:true, fog:false
  });
  const p = new THREE.Points(g, m);
  scene.add(p);
  return p;
}
const dimStars  = starField(900, 460, 3.2, 0.75);
const brightStars = starField(90, 455, 7.0, 0.95);

const moon = new THREE.Mesh(
  new THREE.SphereGeometry(13, 16, 12),
  new THREE.MeshBasicMaterial({ color: 0xf2f6ff, fog:false })
);
moon.position.set(-165, 118, -235);
scene.add(moon);
const moonHalo = new THREE.Sprite(new THREE.SpriteMaterial({
  map: moonGlow, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, fog:false, opacity:0.75
}));
moonHalo.scale.set(95, 95, 1);
moonHalo.position.copy(moon.position);
scene.add(moonHalo);

/* ------------------------------------------------------------------ *
 *  ground (faceted, vertex coloured, flat clearing in the middle)
 * ------------------------------------------------------------------ */
const groundGeo = new THREE.PlaneGeometry(300, 300, 72, 72);
groundGeo.rotateX(-Math.PI / 2);
{
  const p = groundGeo.attributes.position;
  const cols = [];
  const cGrass = new THREE.Color(0x1d2a1c), cDark = new THREE.Color(0x101a14),
        cAsh = new THREE.Color(0x14110d), cDirt = new THREE.Color(0x2a2419),
        cFar = new THREE.Color(0x16222a), tmp = new THREE.Color();
  for (let i = 0; i < p.count; i++){
    const x = p.getX(i), z = p.getZ(i);
    const d = Math.hypot(x, z);
    let h = Math.sin(x * 0.11) * Math.cos(z * 0.09) * 0.75
          + Math.sin(x * 0.29 + z * 0.23) * 0.28
          + Math.sin(z * 0.47 - x * 0.31) * 0.14;
    h *= Math.min(1, Math.max(0, (d - 5) / 12));   // flat around the fire
    h += Math.max(0, d - 45) * 0.05;               // gentle rise far away
    h += (hash(x, 0, z) - 0.5) * 0.16;
    p.setY(i, h);

    tmp.copy(cGrass).lerp(cDirt, hash(x * 2, 1, z * 2) * 0.55);
    tmp.lerp(cDark, Math.min(1, Math.max(0, (d - 20) / 60)));
    tmp.lerp(cFar, Math.min(1, Math.max(0, (d - 55) / 70)) * 0.7);
    if (d < 5.5) tmp.lerp(cAsh, 1 - d / 5.5);
    const shade = 0.82 + hash(x, 3, z) * 0.4;
    cols.push(tmp.r * shade, tmp.g * shade, tmp.b * shade);
  }
  groundGeo.setAttribute('color', new THREE.Float32BufferAttribute(cols, 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);

/* ------------------------------------------------------------------ *
 *  shared low-poly geometries / materials
 * ------------------------------------------------------------------ */
const rockGeoBase = new THREE.IcosahedronGeometry(1, 0);
function makeRockGeo(){
  const g = rockGeoBase.clone();
  const p = g.attributes.position;
  for (let i = 0; i < p.count; i++){
    const x = p.getX(i), y = p.getY(i), z = p.getZ(i);
    const f = 0.82 + hash(Math.round(x*7), Math.round(y*7), Math.round(z*7)) * 0.42;
    p.setXYZ(i, x * f, y * f * 0.8, z * f);
  }
  g.computeVertexNormals();
  return g;
}
const rockGeos = [makeRockGeo(), makeRockGeo(), makeRockGeo()];
const rockMat = new THREE.MeshStandardMaterial({ color:0x54586a, flatShading:true, roughness:0.95 });
const rockMatDark = new THREE.MeshStandardMaterial({ color:0x3c4150, flatShading:true, roughness:1 });

const trunkMat = new THREE.MeshStandardMaterial({ color:0x4a3527, flatShading:true, roughness:1 });
const logMat   = new THREE.MeshStandardMaterial({ color:0x6a4a30, flatShading:true, roughness:1 });
const cutMat   = new THREE.MeshStandardMaterial({ color:0xb08a5c, flatShading:true, roughness:0.9 });
const foliageMats = [
  new THREE.MeshStandardMaterial({ color:0x1c3a28, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x22452e, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x17301f, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x1e3d33, flatShading:true, roughness:1 })
];

const trunkGeo = new THREE.CylinderGeometry(0.13, 0.26, 1, 5);
trunkGeo.translate(0, 0.5, 0);
const coneGeo = new THREE.ConeGeometry(1, 1, 6, 1);
coneGeo.translate(0, 0.5, 0);
const seatLogGeo = new THREE.CylinderGeometry(0.33, 0.31, 3.3, 7);
seatLogGeo.rotateZ(Math.PI / 2);
const fireLogGeo = new THREE.CylinderGeometry(0.14, 0.13, 1.7, 6);
fireLogGeo.translate(0, 0.85, 0);

/* ------------------------------------------------------------------ *
 *  trees  (44 of them, all outside the clearing)
 * ------------------------------------------------------------------ */
const treeGroup = new THREE.Group();
scene.add(treeGroup);
const TREE_COUNT = 44;
let placed = 0, guard = 0;
while (placed < TREE_COUNT && guard < 4000){
  guard++;
  const a = rnd() * Math.PI * 2;
  const r = 11 + Math.pow(rnd(), 0.65) * 52;
  const x = Math.cos(a) * r, z = Math.sin(a) * r;
  if (Math.hypot(x, z) < 10.5) continue;
  placed++;

  const s = rng(0.8, 1.7) * (r > 34 ? 1.15 : 1);
  const tree = new THREE.Group();

  const trunkH = 2.4 * s;
  const trunk = new THREE.Mesh(trunkGeo, trunkMat);
  trunk.scale.set(s, trunkH, s);
  trunk.position.y = -0.1;
  tree.add(trunk);

  const mat = foliageMats[Math.floor(rnd() * foliageMats.length)];
  const layers = rnd() > 0.35 ? 3 : 2;
  for (let i = 0; i < layers; i++){
    const f = new THREE.Mesh(coneGeo, mat);
    const rad = (1.75 - i * 0.45) * s * rng(0.9, 1.12);
    const h   = (2.5 - i * 0.32) * s;
    f.scale.set(rad, h, rad);
    f.position.y = trunkH * 0.52 + i * 1.15 * s;
    f.rotation.y = rnd() * Math.PI;
    tree.add(f);
  }
  tree.position.set(x, 0, z);
  tree.rotation.y = rnd() * Math.PI * 2;
  const h = Math.hypot(x, z);
  const casts = h < 24;
  tree.traverse(o => { if (o.isMesh){ o.castShadow = casts; o.receiveShadow = true; } });
  treeGroup.add(tree);
}

/* low shrubs / boulders further out */
for (let i = 0; i < 16; i++){
  const a = rnd() * Math.PI * 2, r = rng(9, 46);
  const m = new THREE.Mesh(rockGeos[i % 3], rockMatDark);
  const s = rng(0.7, 2.3);
  m.scale.set(s * rng(0.9, 1.4), s * rng(0.6, 1), s * rng(0.9, 1.4));
  m.position.set(Math.cos(a) * r, m.scale.y * 0.35, Math.sin(a) * r);
  m.rotation.set(rnd(), rnd() * 6, rnd());
  m.castShadow = r < 24; m.receiveShadow = true;
  scene.add(m);
}

/* ------------------------------------------------------------------ *
 *  clearing props: rocks, seat logs, fire pit, firewood
 * ------------------------------------------------------------------ */
// ring of stones around the fire
for (let i = 0; i < 11; i++){
  const a = (i / 11) * Math.PI * 2 + rng(-0.12, 0.12);
  const m = new THREE.Mesh(rockGeos[i % 3], i % 2 ? rockMat : rockMatDark);
  const s = rng(0.3, 0.52);
  m.scale.set(s * rng(1, 1.5), s * rng(0.7, 1.1), s * rng(1, 1.4));
  m.position.set(Math.cos(a) * 2.05, m.scale.y * 0.45, Math.sin(a) * 2.05);
  m.rotation.set(rnd(), rnd() * 6, rnd());
  m.castShadow = true; m.receiveShadow = true;
  scene.add(m);
}
// loose rocks in the clearing
for (let i = 0; i < 9; i++){
  const a = rnd() * Math.PI * 2, r = rng(3.6, 9.5);
  const m = new THREE.Mesh(rockGeos[(i + 1) % 3], i % 3 ? rockMatDark : rockMat);
  const s = rng(0.35, 1.0);
  m.scale.set(s * rng(1, 1.6), s * rng(0.55, 0.95), s * rng(1, 1.5));
  m.position.set(Math.cos(a) * r, m.scale.y * 0.4, Math.sin(a) * r);
  m.rotation.set(rnd() * 0.6, rnd() * 6, rnd() * 0.6);
  m.castShadow = true; m.receiveShadow = true;
  scene.add(m);
}
// log seats around the fire
for (let i = 0; i < 4; i++){
  const a = (i / 4) * Math.PI * 2 + rng(-0.3, 0.3) + 0.5;
  const r = rng(4.1, 5.0);
  const g = new THREE.Group();
  const log = new THREE.Mesh(seatLogGeo, logMat);
  log.castShadow = true; log.receiveShadow = true;
  g.add(log);
  const end1 = new THREE.Mesh(new THREE.CircleGeometry(0.33, 7), cutMat);
  end1.rotation.y = Math.PI / 2; end1.position.x = 1.65; g.add(end1);
  const end2 = new THREE.Mesh(new THREE.CircleGeometry(0.31, 7), cutMat);
  end2.rotation.y = -Math.PI / 2; end2.position.x = -1.65; g.add(end2);
  g.position.set(Math.cos(a) * r, 0.33, Math.sin(a) * r);
  g.rotation.y = a + Math.PI / 2 + rng(-0.15, 0.15);
  g.rotation.z = rng(-0.04, 0.04);
  scene.add(g);
}
// scorched earth disc
const scorch = new THREE.Mesh(
  new THREE.CircleGeometry(2.9, 14),
  new THREE.MeshStandardMaterial({ color:0x0c0a09, roughness:1, flatShading:true })
);
scorch.rotation.x = -Math.PI / 2;
scorch.position.y = 0.02;
scorch.receiveShadow = true;
scene.add(scorch);

// teepee of firewood
const wood = new THREE.Group();
for (let i = 0; i < 7; i++){
  const holder = new THREE.Group();
  holder.rotation.y = (i / 7) * Math.PI * 2 + rng(-0.18, 0.18);
  const m = new THREE.Mesh(fireLogGeo, logMat);
  m.position.x = rng(0.72, 0.95);
  m.rotation.z = rng(0.5, 0.68);
  m.rotation.y = rng(-0.2, 0.2);
  m.castShadow = true;
  holder.add(m);
  wood.add(holder);
}
wood.position.y = 0.06;
scene.add(wood);

// glowing embers at the base
const emberMat = new THREE.MeshStandardMaterial({
  color:0x2a0d05, emissive:0xff3d00, emissiveIntensity:2.4, flatShading:true, roughness:1
});
const embers = new THREE.Group();
for (let i = 0; i < 18; i++){
  const a = rnd() * Math.PI * 2, r = rnd() * 1.25;
  const m = new THREE.Mesh(new THREE.IcosahedronGeometry(rng(0.09, 0.2), 0), emberMat);
  m.position.set(Math.cos(a) * r, rng(0.06, 0.42), Math.sin(a) * r);
  m.rotation.set(rnd(), rnd(), rnd());
  embers.add(m);
}
scene.add(embers);

/* ------------------------------------------------------------------ *
 *  flames
 * ------------------------------------------------------------------ */
const flameGroup = new THREE.Group();
flameGroup.position.y = 0.42;
scene.add(flameGroup);

const flameSpecs = [
  { r:0.72, h:3.1, c:0xff4a0c, o:0.34, seg:6, f1:5.2,  f2:9.1,  rot: 0.7 },
  { r:0.55, h:2.5, c:0xff8a1c, o:0.5,  seg:6, f1:6.4,  f2:11.3, rot:-0.9 },
  { r:0.40, h:1.95,c:0xffc23a, o:0.62, seg:5, f1:7.7,  f2:13.1, rot: 1.4 },
  { r:0.26, h:1.45,c:0xfff0b4, o:0.72, seg:5, f1:9.3,  f2:15.7, rot:-1.9 }
];
const flameParts = [];
flameSpecs.forEach((s, i) => {
  const geo = new THREE.ConeGeometry(s.r, s.h, s.seg, 1);
  geo.translate(0, s.h / 2, 0);
  const mat = new THREE.MeshBasicMaterial({
    color:s.c, transparent:true, opacity:s.o, depthWrite:false,
    blending:THREE.AdditiveBlending, flatShading:true, fog:false
  });
  const m = new THREE.Mesh(geo, mat);
  m.userData = {
    baseO:s.o, f1:s.f1, f2:s.f2, rot:s.rot,
    ph:rng(0, 6.28), sx:1, sy:1, sz:1
  };
  flameParts.push(m);
  flameGroup.add(m);
});

// detached tongues that rise off the fire
const tongueGeo = new THREE.OctahedronGeometry(0.4, 0);
const tongues = [];
for (let i = 0; i < 6; i++){
  const m = new THREE.Mesh(tongueGeo, new THREE.MeshBasicMaterial({
    color: i % 2 ? 0xffa027 : 0xff5f14, transparent:true, opacity:0.6,
    blending:THREE.AdditiveBlending, depthWrite:false, flatShading:true, fog:false
  }));
  m.userData = { off: rnd(), speed: rng(0.45, 0.8), wob: rng(1.4, 3.2), base: rng(0.5, 1.0) };
  tongues.push(m);
  flameGroup.add(m);
}

// soft light-bloom sprite at the heart of the fire
const bloom = new THREE.Sprite(new THREE.SpriteMaterial({
  map:glowTex, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false, fog:false, opacity:0.85
}));
bloom.scale.set(7.5, 7.5, 1);
bloom.position.y = 1.4;
scene.add(bloom);

/* ------------------------------------------------------------------ *
 *  sparks — simple particle system
 * ------------------------------------------------------------------ */
const SPARK_COUNT = 210;
const sparkPos = new Float32Array(SPARK_COUNT * 3);
const sparkCol = new Float32Array(SPARK_COUNT * 3);
const sparks = [];
function resetSpark(d, initial){
  const a = rnd() * Math.PI * 2, r = rnd() * 1.05;
  d.x = Math.cos(a) * r; d.z = Math.sin(a) * r;
  d.y = 0.55 + rnd() * 1.1;
  d.vx = (rnd() - 0.5) * 0.55;
  d.vz = (rnd() - 0.5) * 0.55;
  d.vy = 1.3 + rnd() * 2.6;
  d.maxLife = 1.5 + rnd() * 2.3;
  d.life = initial ? rnd() * d.maxLife : d.maxLife;
}
for (let i = 0; i < SPARK_COUNT; i++){
  const d = { x:0, y:0, z:0, vx:0, vy:0, vz:0, life:0, maxLife:1 };
  resetSpark(d, true);
  sparks.push(d);
}
const sparkGeo = new THREE.BufferGeometry();
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkCol, 3));
const sparkPoints = new THREE.Points(sparkGeo, new THREE.PointsMaterial({
  size:0.2, map:sparkTex, vertexColors:true, transparent:true,
  blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true, fog:false
}));
sparkPoints.frustumCulled = false;
scene.add(sparkPoints);

const colHot = new THREE.Color(0xfff3c0), colMid = new THREE.Color(0xff8a1e),
      colCold = new THREE.Color(0x8e1c03), tmpCol = new THREE.Color();

/* ------------------------------------------------------------------ *
 *  animation
 * ------------------------------------------------------------------ */
const clock = new THREE.Clock();
let t = 0;

function flickerNoise(x){
  return 0.62
    + 0.18 * Math.sin(x * 11.3)
    + 0.11 * Math.sin(x * 23.7 + 1.7)
    + 0.09 * Math.sin(x * 6.1 + 0.4)
    + 0.06 * Math.sin(x * 37.5 + 2.9);
}

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

  // --- firelight flicker ---
  const f = flickerNoise(t);
  fireLight.intensity = 34 + f * 24;
  fireLight.position.set(Math.sin(t * 7.3) * 0.16, 1.35 + Math.sin(t * 5.1) * 0.16, Math.cos(t * 6.4) * 0.16);
  fillLight.intensity = 3.5 + f * 4;
  bloom.material.opacity = 0.6 + f * 0.35;
  bloom.scale.setScalar(6.6 + f * 1.8);
  emberMat.emissiveIntensity = 1.6 + f * 2.2;

  // --- flames ---
  flameParts.forEach(m => {
    const u = m.userData;
    const w1 = Math.sin(t * u.f1 + u.ph);
    const w2 = Math.sin(t * u.f2 + u.ph * 1.7);
    m.scale.y = u.sy * (1 + 0.24 * w1 + 0.11 * w2);
    m.scale.x = u.sx * (1 - 0.10 * w2);
    m.scale.z = u.sz * (1 - 0.10 * w1);
    m.rotation.y += dt * u.rot;
    m.position.x = 0.11 * Math.sin(t * u.f1 * 1.5 + u.ph);
    m.position.z = 0.11 * Math.cos(t * u.f2 * 1.2 + u.ph);
    m.position.y = 0.05 * Math.sin(t * 4.2 + u.ph);
    m.material.opacity = u.baseO * (0.7 + 0.4 * (0.5 + 0.5 * Math.sin(t * u.f1 * 0.9 + u.ph)));
  });

  tongues.forEach(m => {
    const u = m.userData;
    const c = ((t * u.speed + u.off) % 1 + 1) % 1;
    m.position.y = 0.7 + c * 3.0;
    m.position.x = Math.sin(c * 6.283 * u.wob + u.off * 9) * 0.3 * (0.35 + c);
    m.position.z = Math.cos(c * 5.1 + u.off * 6) * 0.3 * (0.35 + c);
    const s = (1 - c) * 0.62 + 0.1;
    m.scale.setScalar(s * u.base);
    m.rotation.y += dt * 1.5;
    m.material.opacity = Math.max(0, (1 - c) * 0.62);
  });

  // --- sparks ---
  for (let i = 0; i < SPARK_COUNT; i++){
    const d = sparks[i];
    d.life -= dt;
    if (d.life <= 0){
      resetSpark(d, false);
    } else {
      d.vy -= 0.5 * dt;
      d.vx += Math.sin(t * 2.6 + d.y * 3.1) * 0.95 * dt;
      d.vz += Math.cos(t * 2.2 + d.y * 2.7) * 0.95 * dt;
      d.vx *= (1 - 1.1 * dt);
      d.vz *= (1 - 1.1 * dt);
      d.x += d.vx * dt; d.y += d.vy * dt; d.z += d.vz * dt;
    }
    sparkPos[i*3] = d.x; sparkPos[i*3+1] = d.y; sparkPos[i*3+2] = d.z;

    const age = 1 - d.life / d.maxLife;   // 0 fresh -> 1 burnt out
    if (age < 0.5) tmpCol.copy(colHot).lerp(colMid, age / 0.5);
    else tmpCol.copy(colMid).lerp(colCold, (age - 0.5) / 0.5);
    const fade = 0.35 + 0.65 * (1 - age);
    sparkCol[i*3] = tmpCol.r * fade;
    sparkCol[i*3+1] = tmpCol.g * fade;
    sparkCol[i*3+2] = tmpCol.b * fade;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;

  // --- gentle star twinkle ---
  brightStars.material.opacity = 0.75 + 0.25 * Math.sin(t * 1.7);
  dimStars.material.opacity = 0.7 + 0.12 * Math.sin(t * 0.9 + 1.1);

  controls.update();
  if (camera.position.y < 1.2) camera.position.y = 1.2;   // hard floor
  renderer.render(scene, camera);
}
animate();

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