Nemotron 3 Ultra (free)
by NVIDIAReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 2 s
- Cost
- $0
- Output tokens
- 6,411
- Spent thinking
- 348 tokens
- Speed
- 105.3 tok/s
- Console errors
- 1
- First token after
- 803 ms
- First visible text after
- 4.2 s
- Prompt tokens
- 584
- Code size
- 17.2 KB
- Finish reason
- stop
- Served by
- Nvidia
- Model id
nvidia/nemotron-3-ultra-550b-a55b:free- Reasoning sent
{"effort":"medium"}- Max output
- 32,000 tokens
- Generated
- Nov 9, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
THREE.WebGLProgram: Shader Error 0 - VALIDATE_STATUS false
Material Name:
Material Type: ShaderMaterial
Program Info Log: Vertex shader is not compiled.
VERTEX
ERROR: 0:77: 'uTime' : undeclared identifier
ERROR: 0:78: 'uTime' : undeclared identifier
72: varying float vY;
73: void main() {
74: vUv = uv;
75: vY = position.y;
76: vec3 pos = position;
> 77: pos.y += sin(uTime * 3.0 + position.x * 5.0 + uv.y * 10.0) * 0.1 * uv.y;
78: 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 · 515 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Low-Poly Campfire</title>
<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>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #0a0a1a; font-family: system-ui, sans-serif; }
canvas { display: block; }
#ui {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
pointer-events: none; color: #fff; text-align: center; z-index: 10;
}
#ui h1 { font-size: clamp(1.5rem, 5vw, 3rem); font-weight: 300; letter-spacing: 0.1em; text-shadow: 0 0 20px #ff6600, 0 0 40px #ff3300; margin-bottom: 1rem; }
#ui p { font-size: clamp(0.9rem, 2.5vw, 1.2rem); opacity: 0.7; text-shadow: 0 0 10px #000; }
#ui.hidden { opacity: 0; pointer-events: none; transition: opacity 0.5s ease; }
</style>
</head>
<body>
<div id="ui">
<h1>Low-Poly Campfire</h1>
<p>Click or press a key to explore</p>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// Scene
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a1a, 0.008);
// Renderer
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.appendChild(renderer.domElement);
// Camera
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 500);
camera.position.set(0, 15, 25);
// Controls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.enablePan = false;
controls.minDistance = 8;
controls.maxDistance = 50;
controls.maxPolarAngle = Math.PI / 2 - 0.05; // Stay above ground
controls.minPolarAngle = 0.2;
controls.target.set(0, 2, 0);
controls.update();
// Materials
const groundMat = new THREE.MeshStandardMaterial({
color: 0x1a1510, roughness: 0.9, metalness: 0.1
});
const rockMat = new THREE.MeshStandardMaterial({
color: 0x3a3530, roughness: 0.95, metalness: 0.05, flatShading: true
});
const logMat = new THREE.MeshStandardMaterial({
color: 0x2a1a0a, roughness: 0.9, metalness: 0.1, flatShading: true
});
const barkMat = new THREE.MeshStandardMaterial({
color: 0x2d1a0f, roughness: 0.95, metalness: 0.05, flatShading: true
});
const leafMat = new THREE.MeshStandardMaterial({
color: 0x0f3d1a, roughness: 0.9, metalness: 0.05, flatShading: true
});
const leafMat2 = new THREE.MeshStandardMaterial({
color: 0x1a4a20, roughness: 0.9, metalness: 0.05, flatShading: true
});
const leafMat3 = new THREE.MeshStandardMaterial({
color: 0x0d2e12, roughness: 0.9, metalness: 0.05, flatShading: true
});
// Ground with displacement
function createGround() {
const geometry = new THREE.PlaneGeometry(200, 200, 80, 80);
const pos = geometry.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), z = pos.getY(i);
const dist = Math.sqrt(x * x + z * z);
if (dist > 12) {
let h = (Math.sin(x * 0.1) * Math.cos(z * 0.1) + Math.sin(x * 0.05) * Math.cos(z * 0.07)) * 0.8;
h += (Math.random() - 0.5) * 0.3;
pos.setZ(i, h);
}
}
geometry.computeVertexNormals();
const ground = new THREE.Mesh(geometry, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
return ground;
}
// Low-poly rock
function createRock(scale = 1) {
const geo = new THREE.DodecahedronGeometry(1, 0);
const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
pos.setX(i, pos.getX(i) + (Math.random() - 0.5) * 0.4);
pos.setY(i, pos.getY(i) + (Math.random() - 0.5) * 0.4);
pos.setZ(i, pos.getZ(i) + (Math.random() - 0.5) * 0.4);
}
geo.computeVertexNormals();
const rock = new THREE.Mesh(geo, rockMat);
rock.scale.setScalar(scale * (0.7 + Math.random() * 0.6));
rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
rock.castShadow = true;
rock.receiveShadow = true;
return rock;
}
// Low-poly log
function createLog(length = 1, radius = 0.2) {
const geo = new THREE.CylinderGeometry(radius, radius * 0.9, length, 6, 1, false);
const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
pos.setX(i, pos.getX(i) + (Math.random() - 0.5) * 0.05);
pos.setZ(i, pos.getZ(i) + (Math.random() - 0.5) * 0.05);
}
geo.computeVertexNormals();
const log = new THREE.Mesh(geo, logMat);
log.castShadow = true;
log.receiveShadow = true;
return log;
}
// Low-poly tree
function createTree() {
const group = new THREE.Group();
const trunkH = 2 + Math.random() * 2;
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.4, trunkH, 6), barkMat);
trunk.position.y = trunkH / 2;
trunk.castShadow = true;
trunk.receiveShadow = true;
group.add(trunk);
const layers = 3 + Math.floor(Math.random() * 3);
const mats = [leafMat, leafMat2, leafMat3];
for (let i = 0; i < layers; i++) {
const size = 1.5 - i * 0.35;
const h = 0.8 + Math.random() * 0.4;
const geo = new THREE.ConeGeometry(size, h, 6);
geo.translate(0, h / 2, 0);
const mesh = new THREE.Mesh(geo, mats[Math.floor(Math.random() * mats.length)]);
mesh.position.y = trunkH + i * (h * 0.7);
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
}
return group;
}
// Fire pit with logs
function createFirePit() {
const group = new THREE.Group();
const ringGeo = new THREE.TorusGeometry(2.2, 0.25, 6, 12);
const ring = new THREE.Mesh(ringGeo, rockMat);
ring.rotation.x = -Math.PI / 2;
ring.position.y = 0.15;
ring.receiveShadow = true;
group.add(ring);
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2 + Math.random() * 0.3;
const log = createLog(2.5, 0.18);
log.position.set(Math.cos(angle) * 1.2, 0.4, Math.sin(angle) * 1.2);
log.rotation.z = angle + Math.PI / 2 + (Math.random() - 0.5) * 0.5;
log.rotation.x = (Math.random() - 0.5) * 0.3;
group.add(log);
}
for (let i = 0; i < 3; i++) {
const angle = (i / 3) * Math.PI * 2 + Math.PI / 6 + Math.random() * 0.3;
const log = createLog(2, 0.15);
log.position.set(Math.cos(angle) * 0.6, 0.9, Math.sin(angle) * 0.6);
log.rotation.z = angle + Math.PI / 2 + (Math.random() - 0.5) * 0.5;
log.rotation.x = (Math.random() - 0.5) * 0.4;
group.add(log);
}
return group;
}
// Animated flames using planes with custom shader
function createFlames() {
const flameGeo = new THREE.PlaneGeometry(1, 2);
const flameMat = new THREE.ShaderMaterial({
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
uniforms: {
uTime: { value: 0 },
uColor1: { value: new THREE.Color(0xff6600) },
uColor2: { value: new THREE.Color(0xffaa00) },
uColor3: { value: new THREE.Color(0xffdd44) }
},
vertexShader: `
varying vec2 vUv;
varying float vY;
void main() {
vUv = uv;
vY = position.y;
vec3 pos = position;
pos.y += sin(uTime * 3.0 + position.x * 5.0 + uv.y * 10.0) * 0.1 * uv.y;
pos.x += sin(uTime * 2.0 + uv.y * 8.0) * 0.05 * uv.y;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform vec3 uColor3;
varying vec2 vUv;
varying float vY;
float noise(vec2 p) {
return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}
void main() {
float n = noise(vUv * 10.0 + uTime * 0.5);
float shape = smoothstep(0.5, 0.0, abs(vUv.x - 0.5) * 2.0) * vUv.y;
shape *= 0.7 + 0.3 * sin(uTime * 8.0 + vUv.y * 20.0 + n * 5.0);
float alpha = shape * (1.0 - vUv.y * 0.3);
vec3 color = mix(uColor1, uColor2, vUv.y);
color = mix(color, uColor3, pow(vUv.y, 2.0));
gl_FragColor = vec4(color, alpha * 0.9);
}
`
});
const group = new THREE.Group();
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const flame = new THREE.Mesh(flameGeo, flameMat);
flame.position.set(Math.cos(angle) * 0.3, 1.2, Math.sin(angle) * 0.3);
flame.rotation.y = angle;
flame.userData.baseAngle = angle;
flame.userData.offset = Math.random() * Math.PI * 2;
group.add(flame);
}
// Cross flames
for (let i = 0; i < 4; i++) {
const angle = (i / 4) * Math.PI * 2;
const flame = new THREE.Mesh(flameGeo, flameMat);
flame.position.set(Math.cos(angle) * 0.15, 1.5, Math.sin(angle) * 0.15);
flame.rotation.y = angle + Math.PI / 2;
flame.userData.baseAngle = angle;
flame.userData.offset = Math.random() * Math.PI * 2;
flame.scale.setScalar(0.7);
group.add(flame);
}
group.material = flameMat;
return group;
}
// Sparks particle system
function createSparks() {
const count = 200;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const velocities = new Float32Array(count * 3);
const lifetimes = new Float32Array(count);
const sizes = new Float32Array(count);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
resetParticle(i);
positions[i * 3] = (Math.random() - 0.5) * 0.5;
positions[i * 3 + 1] = Math.random() * 3 + 1;
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
lifetimes[i] = Math.random();
}
function resetParticle(i) {
const angle = Math.random() * Math.PI * 2;
const speed = 0.5 + Math.random() * 1.5;
velocities[i * 3] = Math.cos(angle) * speed * 0.3;
velocities[i * 3 + 1] = 0.8 + Math.random() * 1.2;
velocities[i * 3 + 2] = Math.sin(angle) * speed * 0.3;
lifetimes[i] = 0;
sizes[i] = 0.05 + Math.random() * 0.1;
const c = new THREE.Color();
c.setHSL(0.05 + Math.random() * 0.1, 1, 0.5 + Math.random() * 0.3);
colors[i * 3] = c.r;
colors[i * 3 + 1] = c.g;
colors[i * 3 + 2] = c.b;
}
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('velocity', new THREE.BufferAttribute(velocities, 3));
geo.setAttribute('lifetime', new THREE.BufferAttribute(lifetimes, 1));
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const mat = new THREE.PointsMaterial({
size: 0.1, vertexColors: true, transparent: true,
blending: THREE.AdditiveBlending, depthWrite: false,
sizeAttenuation: true
});
const sparks = new THREE.Points(geo, mat);
sparks.userData.resetParticle = resetParticle;
sparks.userData.velocities = velocities;
sparks.userData.lifetimes = lifetimes;
sparks.userData.positions = positions;
return sparks;
}
// Stars
function createStars() {
const count = 2000;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const sizes = new Float32Array(count);
for (let i = 0; i < count; i++) {
const r = 80 + Math.random() * 40;
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = r * Math.cos(phi) * 0.5 + 30;
positions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
const temp = Math.random();
const c = new THREE.Color();
if (temp < 0.5) c.setHSL(0.6, 0.2, 0.9);
else if (temp < 0.8) c.setHSL(0.1, 0.3, 0.95);
else c.setHSL(0.6, 0.1, 1);
colors[i * 3] = c.r;
colors[i * 3 + 1] = c.g;
colors[i * 3 + 2] = c.b;
sizes[i] = 0.5 + Math.random() * 1.5;
}
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const mat = new THREE.PointsMaterial({
size: 1, vertexColors: true, transparent: true,
blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: false
});
return new THREE.Points(geo, mat);
}
// Moon
function createMoon() {
const geo = new THREE.SphereGeometry(4, 16, 16);
const mat = new THREE.MeshBasicMaterial({
color: 0xfff8e7, transparent: true, opacity: 0.9
});
const moon = new THREE.Mesh(geo, mat);
moon.position.set(-40, 50, -30);
return moon;
}
// Lighting
const ambient = new THREE.AmbientLight(0x1a1520, 0.3);
scene.add(ambient);
const fireLight = new THREE.PointLight(0xff6600, 50, 40, 2);
fireLight.position.set(0, 3, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.width = 1024;
fireLight.shadow.mapSize.height = 1024;
fireLight.shadow.camera.near = 0.5;
fireLight.shadow.camera.far = 40;
fireLight.shadow.bias = -0.001;
scene.add(fireLight);
const moonLight = new THREE.DirectionalLight(0x445588, 0.15);
moonLight.position.set(-40, 50, -30);
scene.add(moonLight);
// Build scene
scene.add(createGround());
scene.add(createFirePit());
const flames = createFlames();
scene.add(flames);
const sparks = createSparks();
scene.add(sparks);
scene.add(createStars());
scene.add(createMoon());
// Trees
const trees = [];
for (let i = 0; i < 35; i++) {
let x, z, dist;
do {
const angle = Math.random() * Math.PI * 2;
const r = 15 + Math.random() * 70;
x = Math.cos(angle) * r;
z = Math.sin(angle) * r;
dist = Math.sqrt(x * x + z * z);
} while (dist < 14);
const tree = createTree();
tree.position.set(x, 0, z);
tree.rotation.y = Math.random() * Math.PI * 2;
tree.scale.setScalar(0.7 + Math.random() * 0.6);
scene.add(tree);
trees.push(tree);
}
// Rocks around clearing
for (let i = 0; i < 20; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 5 + Math.random() * 20;
const rock = createRock(0.5 + Math.random() * 1.5);
rock.position.set(Math.cos(angle) * r, 0, Math.sin(angle) * r);
scene.add(rock);
}
// Logs around clearing
for (let i = 0; i < 12; i++) {
const angle = Math.random() * Math.PI * 2;
const r = 8 + Math.random() * 25;
const log = createLog(1.5 + Math.random() * 1.5, 0.15 + Math.random() * 0.1);
log.position.set(Math.cos(angle) * r, 0.15, Math.sin(angle) * r);
log.rotation.y = Math.random() * Math.PI;
log.rotation.z = (Math.random() - 0.5) * 0.3;
scene.add(log);
}
// UI handling
const ui = document.getElementById('ui');
let hasInteracted = false;
function hideUI() {
if (!hasInteracted) {
hasInteracted = true;
ui.classList.add('hidden');
}
}
window.addEventListener('click', hideUI);
window.addEventListener('keydown', hideUI);
renderer.domElement.addEventListener('pointerdown', hideUI);
// Animation
let time = 0;
function animate() {
requestAnimationFrame(animate);
const dt = 1/60;
time += dt;
// Flames
flames.material.uniforms.uTime.value = time;
flames.children.forEach((flame, i) => {
flame.rotation.y = flame.userData.baseAngle + Math.sin(time * 2 + flame.userData.offset) * 0.15;
flame.position.y = 1.2 + Math.sin(time * 3 + flame.userData.offset) * 0.15;
flame.scale.y = 0.8 + Math.sin(time * 4 + flame.userData.offset) * 0.2;
});
// Fire light flicker
fireLight.intensity = 40 + Math.sin(time * 4.7) * 8 + Math.sin(time * 8.3) * 4 + Math.random() * 3;
fireLight.position.x = Math.sin(time * 1.3) * 0.15;
fireLight.position.z = Math.cos(time * 1.7) * 0.15;
// Sparks
const pos = sparks.geometry.attributes.position;
const vel = sparks.userData.velocities;
const life = sparks.userData.lifetimes;
const sizes = sparks.geometry.attributes.size;
const positions = sparks.userData.positions;
for (let i = 0; i < life.length; i++) {
life[i] += dt;
positions[i * 3] += vel[i * 3] * dt * 10;
positions[i * 3 + 1] += vel[i * 3 + 1] * dt * 10;
positions[i * 3 + 2] += vel[i * 3 + 2] * dt * 10;
vel[i * 3 + 1] -= dt * 2.5;
vel[i * 3] *= 0.99;
vel[i * 3 + 2] *= 0.99;
if (life[i] > 1.5 + Math.random() * 1.5 || positions[i * 3 + 1] < 0.2) {
sparks.userData.resetParticle(i);
positions[i * 3] = (Math.random() - 0.5) * 0.5;
positions[i * 3 + 1] = 0.5 + Math.random() * 1.5;
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
}
}
pos.needsUpdate = true;
// Stars subtle twinkle
const stars = scene.children.find(c => c.isPoints && c.geometry.attributes.size.count > 1000);
if (stars) {
const starSizes = stars.geometry.attributes.size.array;
for (let i = 0; i < starSizes.length; i++) {
starSizes[i] += (Math.random() - 0.5) * 0.02;
starSizes[i] = Math.max(0.3, Math.min(2, starSizes[i]));
}
stars.geometry.attributes.size.needsUpdate = true;
stars.rotation.y += 0.00001;
}
// Moon light subtle
moonLight.intensity = 0.15 + Math.sin(time * 0.2) * 0.02;
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 (348 tokens)
The user wants a low-poly 3D campfire scene with Three.js. Let me break down the requirements: 1. Ground with low-poly rocks and logs around a campfire 2. At least 30 low-poly trees placed randomly around the clearing (not on the fire) 3. Animated flames with warm point light that flickers and casts shadows 4. Sparks particle system rising from fire 5. Dark night sky with stars and moon, plus fog 6. OrbitControls with limits (camera stays above ground) 7. Renderer fills window, handles resizing 8. Single HTML file with inline CSS/JS 9. Use the specified import map for Three.js I need to create a visually impressive scene that works immediately on load. Let me plan the implementation: **Scene Setup:** - Three.js scene with fog - Perspective camera - WebGLRenderer with shadows enabled - OrbitControls with min/max polar angle limits **Ground:** - Low-poly plane with some vertex displacement for terrain variation - Rocks and logs as simple geometric shapes **Trees:** - Low-poly tree models (trunk + cone layers) - 30+ trees placed randomly in a ring around the center (avoiding fire area) **Campfire:** - Fire pit with logs - Animated flames using planes with animated shader or geometry manipulation - Point light with flickering intensity - Sparks particle system **Sky:** - Dark blue/black background - Stars as points - Moon as a sphere **Animation Loop:** - Flame animation - Light flicker - Spark particle updates - Camera controls update Let me write this as a single HTML file with everything inline.