Models 44

GLM 5.3 Flash

by Z.aiReasoningClean
Open in new window

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

How it went

Total time
47.1 s
Cost
$0.0023
Output tokens
5,979
Spent thinking
1,554 tokens
Speed
128.5 tok/s
Console errors
0
First token after
599 ms
First visible text after
16.7 s
Prompt tokens
559
Code size
13.1 KB
Finish reason
stop
Served by
CoreWeave
Model id
z-ai/glm-5.3-flash
Reasoning sent
{"effort":"high"}
Max output
64,000 tokens
Generated
Nov 10, 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 · 367 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Night Campfire — Low Poly Forest</title>
<style>
  html, body { margin:0; padding:0; width:100%; height:100%; overflow:hidden; background:#060a14; }
  #app { width:100%; height:100%; display:block; }
  #hud {
    position:fixed; left:14px; bottom:12px; z-index:10;
    font-family: Georgia, 'Times New Roman', serif;
    color:rgba(255,214,150,0.85); font-size:13px; letter-spacing:0.5px;
    text-shadow:0 1px 3px rgba(0,0,0,0.9); pointer-events:none; user-select:none;
  }
</style>
<script type="importmap">
{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}
</script>
</head>
<body>
<div id="app"></div>
<div id="hud">Campfire in the clearing · drag to orbit · scroll to zoom</div>

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

/* ---------- renderer / scene / camera ---------- */
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;
document.getElementById('app').appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x070b16);
scene.fog = new THREE.Fog(0x0a1122, 16, 68);

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

const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.1, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.06;
controls.minDistance = 4;
controls.maxDistance = 38;
controls.maxPolarAngle = Math.PI/2 - 0.06;   // camera stays above ground
controls.minPolarAngle = 0.15;

/* ---------- lights ---------- */
scene.add(new THREE.AmbientLight(0x2a3550, 0.55));

const moonLight = new THREE.DirectionalLight(0x8fa8d8, 0.55);
moonLight.position.set(-24, 30, -18);
scene.add(moonLight);

const fireLight = new THREE.PointLight(0xff7a2a, 60, 55, 2);
fireLight.position.set(0, 1.6, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.3;
fireLight.shadow.camera.far = 45;
fireLight.shadow.bias = -0.004;
scene.add(fireLight);

/* a soft warm fill so shadowed sides aren't pure black */
const warmFill = new THREE.PointLight(0xff9a44, 10, 18, 2);
warmFill.position.set(0, 2.4, 0);
scene.add(warmFill);

/* ---------- ground (low-poly displaced disc) ---------- */
const groundGeo = new THREE.CircleGeometry(70, 56);
groundGeo.rotateX(-Math.PI/2);
{
  const pos = groundGeo.attributes.position;
  for (let i=0; i<pos.count; i++){
    const x = pos.getX(i), z = pos.getZ(i);
    const r = Math.hypot(x, z);
    const falloff = Math.min(1, Math.max(0, (r-5)/14));       // keep clearing flat
    const h = (Math.sin(x*0.35)*Math.cos(z*0.31) + Math.sin(x*0.11+z*0.17)*1.4) * 0.55 * falloff;
    pos.setY(i, h);
  }
  groundGeo.computeVertexNormals();
}
const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({
  color:0x2e3b26, flatShading:true, roughness:1, metalness:0
}));
ground.receiveShadow = true;
scene.add(ground);

/* ---------- materials shared ---------- */
const trunkMat  = new THREE.MeshStandardMaterial({ color:0x4a3220, flatShading:true, roughness:1 });
const foliageMats = [
  new THREE.MeshStandardMaterial({ color:0x1d3a24, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x24482b, flatShading:true, roughness:1 }),
  new THREE.MeshStandardMaterial({ color:0x16301d, flatShading:true, roughness:1 })
];
const rockMat = new THREE.MeshStandardMaterial({ color:0x5a5f66, flatShading:true, roughness:1 });
const logMat  = new THREE.MeshStandardMaterial({ color:0x54402a, flatShading:true, roughness:1 });
const charMat = new THREE.MeshStandardMaterial({ color:0x2a1a12, flatShading:true, roughness:1 });
const emberMat = new THREE.MeshStandardMaterial({ color:0x662200, emissive:0xff5510, emissiveIntensity:1.6, flatShading:true });

/* ---------- trees (40+, never on the fire) ---------- */
function makeTree(){
  const g = new THREE.Group();
  const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.14, 0.26, 1.3, 6), trunkMat);
  trunk.position.y = 0.6;
  trunk.castShadow = true;
  g.add(trunk);
  const mat = foliageMats[Math.floor(Math.random()*foliageMats.length)];
  let y = 1.1, r = 1.25 + Math.random()*0.5;
  const tiers = 2 + Math.floor(Math.random()*2);
  for (let i=0; i<tiers; i++){
    const h = 1.5 + Math.random()*0.8;
    const cone = new THREE.Mesh(new THREE.ConeGeometry(r, h, 6), mat);
    cone.position.y = y + h*0.45;
    cone.rotation.y = Math.random()*Math.PI;
    cone.castShadow = true;
    g.add(cone);
    y += h*0.55;
    r *= 0.68;
  }
  return g;
}
const placed = [];
for (let i=0; i<46; i++){
  let x, z, ok = false, tries = 0;
  while (!ok && tries++ < 60){
    const a = Math.random()*Math.PI*2;
    const r = 15 + Math.random()*46;
    x = Math.cos(a)*r; z = Math.sin(a)*r;
    ok = placed.every(p => Math.hypot(p[0]-x, p[1]-z) > 4.5);
  }
  placed.push([x, z]);
  const t = makeTree();
  const s = 0.8 + Math.random()*1.5;
  t.scale.setScalar(s);
  t.position.set(x, 0, z);
  t.rotation.y = Math.random()*Math.PI*2;
  scene.add(t);
}

/* ---------- rocks & logs around the fire ---------- */
for (let i=0; i<9; i++){
  const a = (i/9)*Math.PI*2 + Math.random()*0.4;
  const r = 1.5 + Math.random()*0.5;
  const rock = new THREE.Mesh(new THREE.DodecahedronGeometry(0.22 + Math.random()*0.2, 0), rockMat);
  rock.position.set(Math.cos(a)*r, 0.1, Math.sin(a)*r);
  rock.rotation.set(Math.random()*3, Math.random()*3, Math.random()*3);
  rock.scale.y = 0.6 + Math.random()*0.3;
  rock.castShadow = true; rock.receiveShadow = true;
  scene.add(rock);
}
for (let i=0; i<5; i++){
  const a = Math.random()*Math.PI*2;
  const r = 2.6 + Math.random()*2.2;
  const log = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.19, 1.4 + Math.random()*0.8, 6), logMat);
  log.rotation.set(Math.PI/2, 0, Math.random()*Math.PI*2);
  log.rotation.x = Math.PI/2 + (Math.random()-0.5)*0.2;
  log.position.set(Math.cos(a)*r, 0.16, Math.sin(a)*r);
  log.castShadow = true; log.receiveShadow = true;
  scene.add(log);
}
/* a couple of stumps further out */
for (let i=0; i<3; i++){
  const a = Math.random()*Math.PI*2, r = 6 + Math.random()*5;
  const stump = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.5, 0.55, 7), trunkMat);
  stump.position.set(Math.cos(a)*r, 0.26, Math.sin(a)*r);
  stump.castShadow = true; stump.receiveShadow = true;
  scene.add(stump);
}
/* small bushes */
for (let i=0; i<12; i++){
  const a = Math.random()*Math.PI*2, r = 6 + Math.random()*14;
  const bush = new THREE.Mesh(new THREE.IcosahedronGeometry(0.35 + Math.random()*0.3, 0),
    foliageMats[Math.floor(Math.random()*foliageMats.length)]);
  bush.position.set(Math.cos(a)*r, 0.2, Math.sin(a)*r);
  bush.scale.y = 0.7;
  bush.castShadow = true;
  scene.add(bush);
}

/* ---------- campfire: stone ring + teepee logs + embers ---------- */
const fire = new THREE.Group();
scene.add(fire);

for (let i=0; i<10; i++){
  const a = (i/10)*Math.PI*2;
  const s = new THREE.Mesh(new THREE.DodecahedronGeometry(0.2 + Math.random()*0.1, 0), rockMat);
  s.position.set(Math.cos(a)*0.95, 0.1, Math.sin(a)*0.95);
  s.rotation.set(Math.random()*3, Math.random()*3, Math.random()*3);
  s.castShadow = true;
  fire.add(s);
}
for (let i=0; i<5; i++){
  const a = (i/5)*Math.PI*2;
  const l = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.11, 1.25, 5), charMat);
  l.position.set(Math.cos(a)*0.22, 0.35, Math.sin(a)*0.22);
  l.rotation.z = Math.PI/2 - 0.55;
  l.rotation.y = -a;
  l.castShadow = true;
  fire.add(l);
}
for (let i=0; i<6; i++){
  const e = new THREE.Mesh(new THREE.DodecahedronGeometry(0.07 + Math.random()*0.05, 0), emberMat);
  e.position.set((Math.random()-0.5)*0.55, 0.12 + Math.random()*0.1, (Math.random()-0.5)*0.55);
  fire.add(e);
}

/* ---------- animated low-poly flames ---------- */
const flameLayers = [];
function addFlame(r, h, color, opacity, speed, phase){
  const geo = new THREE.ConeGeometry(r, h, 7, 1);
  const pos = geo.attributes.position;
  for (let i=0; i<pos.count; i++){
    pos.setX(i, pos.getX(i) * (0.85 + Math.random()*0.3));
    pos.setZ(i, pos.getZ(i) * (0.85 + Math.random()*0.3));
  }
  geo.computeVertexNormals();
  const m = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({
    color, transparent:true, opacity, blending:THREE.AdditiveBlending, depthWrite:false
  }));
  m.position.y = h*0.42;
  m.userData = { speed, phase, baseH:h };
  fire.add(m);
  flameLayers.push(m);
}
addFlame(0.62, 1.7, 0xff4a14, 0.75, 7.3, 0.0);
addFlame(0.42, 1.25, 0xff8c1e, 0.85, 9.1, 1.7);
addFlame(0.24, 0.85, 0xffd24a, 0.95, 11.4, 3.4);

/* ---------- sparks particle system ---------- */
const SPARKS = 140;
const sparkGeo = new THREE.BufferGeometry();
const sPos = new Float32Array(SPARKS*3);
const sCol = new Float32Array(SPARKS*3);
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sPos, 3));
sparkGeo.setAttribute('color', new THREE.BufferAttribute(sCol, 3));
const sparkData = [];
for (let i=0; i<SPARKS; i++){
  sparkData.push({ x:0, y:-100, z:0, vx:0, vy:0, vz:0, life:0, max:1 });
  sPos[i*3+1] = -100;
}
const sparkMat = new THREE.PointsMaterial({
  size:0.14, vertexColors:true, transparent:true,
  blending:THREE.AdditiveBlending, depthWrite:false, sizeAttenuation:true
});
const sparks = new THREE.Points(sparkGeo, sparkMat);
sparks.frustumCulled = false;
scene.add(sparks);

const tmpColor = new THREE.Color();
function respawnSpark(d){
  d.x = (Math.random()-0.5)*0.4;
  d.y = 0.7 + Math.random()*0.4;
  d.z = (Math.random()-0.5)*0.4;
  d.vx = (Math.random()-0.5)*0.5;
  d.vy = 1.6 + Math.random()*2.2;
  d.vz = (Math.random()-0.5)*0.5;
  d.max = 0.9 + Math.random()*1.1;
  d.life = d.max;
}

/* ---------- night sky: stars + moon ---------- */
{
  const N = 700;
  const starGeo = new THREE.BufferGeometry();
  const p = new Float32Array(N*3);
  for (let i=0; i<N; i++){
    const u = Math.random(), v = Math.random()*0.85 + 0.05; // upper dome
    const theta = u*Math.PI*2;
    const phi = Math.acos(1 - v);
    const r = 180;
    p[i*3]   = r*Math.sin(phi)*Math.cos(theta);
    p[i*3+1] = Math.abs(r*Math.cos(phi)) + 6;
    p[i*3+2] = r*Math.sin(phi)*Math.sin(theta);
  }
  starGeo.setAttribute('position', new THREE.BufferAttribute(p, 3));
  const starMat = new THREE.PointsMaterial({
    color:0xdfe8ff, size:1.15, sizeAttenuation:false,
    transparent:true, opacity:0.9, fog:false
  });
  scene.add(new THREE.Points(starGeo, starMat));
  window.__starMat = starMat;
}
const moon = new THREE.Mesh(
  new THREE.SphereGeometry(6, 16, 12),
  new THREE.MeshBasicMaterial({ color:0xe8eefc, fog:false })
);
moon.position.set(-95, 78, -110);
scene.add(moon);
const moonHalo = new THREE.Mesh(
  new THREE.SphereGeometry(9.5, 16, 12),
  new THREE.MeshBasicMaterial({ color:0x8fa4cc, transparent:true, opacity:0.16, fog:false })
);
moonHalo.position.copy(moon.position);
scene.add(moonHalo);

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

  /* flames flicker */
  for (const f of flameLayers){
    const u = f.userData;
    const n = Math.sin(t*u.speed + u.phase)*0.5 + Math.sin(t*u.speed*2.3 + u.phase*2)*0.5;
    f.scale.set(1 + n*0.12, 1 + n*0.28, 1 + n*0.12);
    f.rotation.y += dt*(0.6 + u.speed*0.08);
    f.position.x = Math.sin(t*u.speed*0.7 + u.phase)*0.05;
    f.position.z = Math.cos(t*u.speed*0.9 + u.phase)*0.05;
  }

  /* fire light flicker */
  const flick = Math.sin(t*9.7)*0.5 + Math.sin(t*23.3)*0.3 + Math.sin(t*4.1)*0.2;
  fireLight.intensity = 58 + flick*14;
  fireLight.position.x = Math.sin(t*13.1)*0.12;
  fireLight.position.z = Math.cos(t*11.7)*0.12;
  fireLight.position.y = 1.6 + flick*0.15;
  warmFill.intensity = 9 + flick*3;

  /* sparks */
  for (let i=0; i<SPARKS; i++){
    const d = sparkData[i];
    d.life -= dt;
    if (d.life <= 0){ respawnSpark(d); }
    d.vy -= dt*0.55;                       // sparks slow down as they rise
    d.x += d.vx*dt + Math.sin(t*3 + i)*0.002;
    d.y += d.vy*dt;
    d.z += d.vz*dt + Math.cos(t*2.6 + i)*0.002;
    sPos[i*3]   = d.x;
    sPos[i*3+1] = d.y;
    sPos[i*3+2] = d.z;
    const k = Math.max(0, d.life/d.max);   // fade to black = invisible in additive blending
    tmpColor.setHSL(0.07 + 0.04*k, 1.0, 0.25 + 0.45*k);
    sCol[i*3]   = tmpColor.r;
    sCol[i*3+1] = tmpColor.g;
    sCol[i*3+2] = tmpColor.b;
  }
  sparkGeo.attributes.position.needsUpdate = true;
  sparkGeo.attributes.color.needsUpdate = true;

  /* subtle star twinkle */
  window.__starMat.opacity = 0.82 + Math.sin(t*1.3)*0.08;

  /* embers pulse */
  emberMat.emissiveIntensity = 1.3 + flick*0.7;

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

/* ---------- resize ---------- */
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth/window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
Reasoning trace (1,554 tokens)
Create a single HTML file with Three.js via import map. Scene: night campfire, low-poly trees (30+), rocks, logs, animated flames (cone meshes with flicker), point light flicker with shadows, sparks particle system, stars, moon, fog, OrbitControls with maxPolarAngle limit, resize handler. Must be visible on first paint immediately — no start screen needed (not a game). Zero errors.

Plan:
- Import map as given.
- Renderer with shadows, PCFSoft.
- Scene fog: new THREE.FogExp2 or Fog dark blue.
- Camera positioned slightly elevated, orbit target at fire.
- Ground: large circle plane, low-poly with slight vertex displacement, dark green/brown.
- Fire: logs (cylinders crossed), stones circle, flames: 3-4 cone meshes with vertex colors / emissive orange, animate scale/rotation via time.
- Point light at fire, castShadow, flicker intensity + position jitter.
- Sparks: Points with additive material, positions recycled rising with random velocity, fade out via opacity per-particle? Use PointsMaterial with vertex colors and size attenuation; simpler: update positions, reset when above height. Use small texture-less points; additive blending orange.
- Stars: Points on sphere dome, white small.
- Moon: sphere with emissive, plus a soft directional/ambient light? Keep moon emissive + faint bluish directional light for shadows? Point light casts shadows; ambient low.
- Trees: low-poly cones stacked (2-3 cones) + trunk cylinder, randomized positions in ring between radius 15 and 55, avoid center. Use InstancedMesh? Simpler: build tree group via merged geometry or just create ~40 trees of a few geometries with random scale/rotation. Performance fine. Cast shadows on trees near fire only? Shadow camera distance — point light shadow distance limited. Set castShadow true for all trees; point light shadow map 1024, distance ~40.
- Rocks: dodecahedron/icosahedron flattened random.
- Logs: cylinders lying around.
- OrbitControls: minDistance, maxDistance, maxPolarAngle = Math.PI/2 - 0.05 to stay above ground, enableDamping.

Flame animation: cones scale.y pulsate, slight x/z noise, hue shifting via emissiveIntensity. Use MeshBasicMaterial with color orange/yellow, transparent opacity 0.9. Or use additive blending.

Fire glow: also add a subtle flicker to a second larger light or adjust ambient.

Sparks: BufferGeometry with ~120 particles, each with velocity, life. PointsMaterial size 0.12, color 0xffaa33, blending additive, transparent, depthWrite false. Fade: move dead particles far away or set alpha via size 0? PointsMaterial has uniform opacity; per-particle fade requires shader. Simple: recycle particles, use vertex colors fading to black with additive blending (color black = invisible in additive). Good trick: attribute color, additive blending, colors fade to black as particle ages. Use vertexColors: true.

Stars twinkle: vary opacity slightly, or just static. Add slight twinkle by changing material opacity with sin — that affects all stars uniformly; fine subtle.

Moon: circle/sphere with MeshBasicMaterial pale, plus glow sprite? Skip sprite; a soft halo via second larger sphere with transparent material. Keep simple: sphere emissive + faint blue hemisphere light.

Camera auto-rotate slightly? Not needed. But first paint visible immediately — fine.

Write code carefully to avoid errors. Use module script.

Ground geometry: CircleGeometry radius 60, 48 segments, displace vertices with pseudo noise except keep center flat-ish. Rotate -PI/2. receiveShadow.

Also darker ground beyond clearing: fog handles it.

Let me write it.

Tree creation: function makeTree(): trunk CylinderGeometry(0.15,0.25,1.2,6), foliage: 2-3 ConeGeometry(r, h, 6) stacked. Materials shared. Random hue of dark green. Use flatShading true.

Place trees: random angle, radius between 14 and 58, check not too close to others optional. 45 trees.

Point light: color 0xff7733, intensity ~ flicker base 25 (with physically correct lights in 0.170, intensity units — point light intensity in candela; use decay 2 default. For visible scene use intensity ~60 with distance). Actually three r155+ uses physically correct by default; PointLight intensity ~ 50 works for radius ~10. Let's set intensity 60, distance 60, decay 2. Flicker: intensity = 50 + noise.

Ambient: 0x223355, 0.3. Moon directional light bluish 0.4, castShadow false.

Shadow: light.shadow.mapSize 1024, camera near 0.5 far 40, bias.

Flames: group at y~0.4 above logs. Outer cone radius 0.7 height 1.6 color 0xff5522 opacity 0.85; middle 0.45/1.2 color 0xff8830; inner 0.25/0.9 color 0xffcc55. Animate: scale.y = 1 + 0.25*sin(t*speed+phase), rotation.y += , position.x jitter small, scale.x/z slight. Use flatShading, ConeGeometry(r, h, 7) with random vertex jitter for low-poly look.

Logs in fire: 4 cylinders radius 0.12 length 1.4, rotated around, tilted, brown, with slightly emissive? charred color 0x3a2418, plus glowing embers: small spheres emissive orange at base.

Stones ring: 10 dodecahedrons small around radius 0.9.

Sparks: origin at fire center y~0.8, vel up 1.5-3, random horizontal 0.2, life ~1-2s, gravity slight negative? Sparks rise then slow; add slight drift. Update in loop with dt.

Resize handler.

OrbitControls import from 'three/addons/controls/OrbitControls.js'.

Fog: scene.fog = new THREE.Fog(0x0a1020, 18, 70); background color 0x060a14. Stars need to not be fully fogged — fog affects points too. Set stars material fog: false. Moon material fog false too.

Sky: use scene.background color; stars as points on large sphere radius 200... fog would fade them; set fog:false. Moon at position on dome.

Also camera maxDistance 40 so fog isn't fully escaped, minDistance 4, target (0,1,0), maxPolarAngle 1.5 (~85°), minPolarAngle 0.15.

Write final HTML. Ensure no console errors. Use flatShading on materials.

Ground vertex displacement: CircleGeometry is non-indexed? CircleGeometry has index. Displace positions attribute, then computeVertexNormals with flatShading it doesn't matter much. Keep center flat: scale displacement by min(1, (r-4)/10).

Also add grass tufts? Optional — skip for simplicity, maybe a few small cones as bushes.

Sparks color attribute update each frame — fine.

Let me write the code.