GLM 5.3 Flash
by Z.aiReasoningClean
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.