Models 44

Nemotron 3 Ultra (free)

by NVIDIAReasoningClean
Open in new window

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

How it went

Total time
1 min 16 s
Cost
$0
Output tokens
6,473
Spent thinking
0 tokens
Speed
85.4 tok/s
Console errors
0
First token after
667 ms
First visible text after
667 ms
Prompt tokens
584
Code size
21.1 KB
Finish reason
stop
Served by
Nvidia
Model id
nvidia/nemotron-3-ultra-550b-a55b:free
Reasoning sent
{"effort":"none"}
Max output
32,000 tokens
Generated
Nov 9, 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 · 610 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>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { overflow: hidden; background: #0a0a1a; font-family: sans-serif; }
        canvas { display: block; }
        #info {
            position: fixed;
            top: 20px;
            left: 50%;
            transform: translateX(-50%);
            color: #fff;
            text-align: center;
            pointer-events: none;
            text-shadow: 0 0 10px rgba(0,0,0,0.8);
            z-index: 10;
        }
        #info h1 { font-size: 1.5rem; font-weight: 300; letter-spacing: 2px; margin-bottom: 5px; }
        #info p { font-size: 0.85rem; color: #aaa; }
    </style>
</head>
<body>
    <div id="info">
        <h1>Low Poly Campfire</h1>
        <p>Drag to orbit • Scroll to zoom</p>
    </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';
    
    // Scene setup
    const scene = new THREE.Scene();
    scene.fog = new THREE.Fog(0x0a0a1a, 15, 60);
    
    // Renderer
    const renderer = new THREE.WebGLRenderer({ antialias: 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, 100);
    camera.position.set(0, 8, 12);
    
    // Controls
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.enablePan = false;
    controls.minDistance = 5;
    controls.maxDistance = 30;
    controls.maxPolarAngle = Math.PI / 2 - 0.05;
    controls.minPolarAngle = 0.1;
    controls.target.set(0, 1, 0);
    
    // Materials
    const groundMat = new THREE.MeshStandardMaterial({ 
        color: 0x1a1510, 
        roughness: 0.9, 
        metalness: 0,
        flatShading: true 
    });
    const rockMat = new THREE.MeshStandardMaterial({ 
        color: 0x3a3530, 
        roughness: 0.95, 
        metalness: 0,
        flatShading: true 
    });
    const logMat = new THREE.MeshStandardMaterial({ 
        color: 0x2a1a0a, 
        roughness: 0.9, 
        metalness: 0,
        flatShading: true 
    });
    const trunkMat = new THREE.MeshStandardMaterial({ 
        color: 0x2a1f15, 
        roughness: 0.95, 
        metalness: 0,
        flatShading: true 
    });
    const leafMat = new THREE.MeshStandardMaterial({ 
        color: 0x1a3a1a, 
        roughness: 0.9, 
        metalness: 0,
        flatShading: true 
    });
    
    // Ground
    const groundGeo = new THREE.CircleGeometry(50, 64);
    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.rotation.x = -Math.PI / 2;
    ground.receiveShadow = true;
    scene.add(ground);
    
    // Campfire base - rocks in a circle
    const fireRocks = new THREE.Group();
    for (let i = 0; i < 12; i++) {
        const angle = (i / 12) * Math.PI * 2;
        const radius = 1.2 + Math.random() * 0.3;
        const rockGeo = new THREE.DodecahedronGeometry(0.4 + Math.random() * 0.3, 0);
        const rock = new THREE.Mesh(rockGeo, rockMat);
        rock.position.set(
            Math.cos(angle) * radius,
            0.15 + Math.random() * 0.1,
            Math.sin(angle) * radius
        );
        rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
        rock.scale.setScalar(0.7 + Math.random() * 0.5);
        rock.castShadow = true;
        rock.receiveShadow = true;
        fireRocks.add(rock);
    }
    scene.add(fireRocks);
    
    // Logs around fire
    const logs = new THREE.Group();
    for (let i = 0; i < 6; i++) {
        const angle = (i / 6) * Math.PI * 2 + Math.random() * 0.3;
        const radius = 1.0;
        const logGeo = new THREE.CylinderGeometry(0.15, 0.18, 1.2, 6);
        const log = new THREE.Mesh(logGeo, logMat);
        log.position.set(
            Math.cos(angle) * radius,
            0.35,
            Math.sin(angle) * radius
        );
        log.rotation.z = angle + Math.PI / 2 + (Math.random() - 0.5) * 0.4;
        log.rotation.x = (Math.random() - 0.5) * 0.3;
        log.castShadow = true;
        log.receiveShadow = true;
        logs.add(log);
    }
    // Cross logs
    for (let i = 0; i < 3; i++) {
        const angle = (i / 3) * Math.PI * 2;
        const logGeo = new THREE.CylinderGeometry(0.12, 0.15, 1.5, 6);
        const log = new THREE.Mesh(logGeo, logMat);
        log.position.set(
            Math.cos(angle) * 0.4,
            0.65,
            Math.sin(angle) * 0.4
        );
        log.rotation.z = angle + (Math.random() - 0.5) * 0.3;
        log.rotation.x = (Math.random() - 0.5) * 0.4;
        log.castShadow = true;
        log.receiveShadow = true;
        logs.add(log);
    }
    scene.add(logs);
    
    // Fire light
    const fireLight = new THREE.PointLight(0xff6600, 80, 25, 2);
    fireLight.position.set(0, 1.5, 0);
    fireLight.castShadow = true;
    fireLight.shadow.mapSize.width = 512;
    fireLight.shadow.mapSize.height = 512;
    fireLight.shadow.camera.near = 0.5;
    fireLight.shadow.camera.far = 25;
    fireLight.shadow.bias = -0.0005;
    scene.add(fireLight);
    
    // Ambient moonlight
    const moonLight = new THREE.DirectionalLight(0x6688aa, 0.3);
    moonLight.position.set(10, 20, 10);
    moonLight.castShadow = true;
    moonLight.shadow.mapSize.width = 2048;
    moonLight.shadow.mapSize.height = 2048;
    moonLight.shadow.camera.left = -30;
    moonLight.shadow.camera.right = 30;
    moonLight.shadow.camera.top = 30;
    moonLight.shadow.camera.bottom = -30;
    moonLight.shadow.camera.near = 1;
    moonLight.shadow.camera.far = 60;
    moonLight.shadow.bias = -0.0002;
    scene.add(moonLight);
    
    // Hemisphere light for subtle fill
    const hemiLight = new THREE.HemisphereLight(0x223344, 0x111122, 0.4);
    scene.add(hemiLight);
    
    // Flame geometry - using multiple cone layers with animated vertices
    const flameGroup = new THREE.Group();
    
    // Create flame layers
    const flameGeometries = [];
    const flameMeshes = [];
    const baseFlamePositions = [];
    
    for (let layer = 0; layer < 4; layer++) {
        const height = 0.8 + layer * 0.5;
        const radius = 0.4 - layer * 0.08;
        const geo = new THREE.ConeGeometry(radius, height, 6, 1, true);
        geo.translate(0, height / 2 + 0.3 + layer * 0.15, 0);
        
        // Store base vertex positions for animation
        const positions = geo.attributes.position.array;
        const basePositions = new Float32Array(positions.length);
        basePositions.set(positions);
        baseFlamePositions.push(basePositions);
        flameGeometries.push(geo);
        
        const mat = new THREE.MeshBasicMaterial({
            color: layer < 2 ? 0xff5500 : 0xffaa00,
            transparent: true,
            opacity: 0.7 - layer * 0.1,
            depthWrite: false,
            side: THREE.DoubleSide
        });
        const mesh = new THREE.Mesh(geo, mat);
        flameMeshes.push(mesh);
        flameGroup.add(mesh);
    }
    
    // Inner glow
    const glowGeo = new THREE.SphereGeometry(0.5, 16, 16);
    const glowMat = new THREE.MeshBasicMaterial({
        color: 0xff4400,
        transparent: true,
        opacity: 0.4,
        depthWrite: false
    });
    const glow = new THREE.Mesh(glowGeo, glowMat);
    glow.position.y = 1.2;
    glow.scale.set(1.2, 1.8, 1.2);
    flameGroup.add(glow);
    
    scene.add(flameGroup);
    
    // Sparks particle system
    const sparkCount = 150;
    const sparkGeo = new THREE.BufferGeometry();
    const sparkPositions = new Float32Array(sparkCount * 3);
    const sparkVelocities = new Float32Array(sparkCount * 3);
    const sparkLifetimes = new Float32Array(sparkCount);
    const sparkSizes = new Float32Array(sparkCount);
    const sparkColors = new Float32Array(sparkCount * 3);
    
    for (let i = 0; i < sparkCount; i++) {
        resetSpark(i);
    }
    
    function resetSpark(i) {
        const angle = Math.random() * Math.PI * 2;
        const radius = Math.random() * 0.3;
        sparkPositions[i * 3] = Math.cos(angle) * radius;
        sparkPositions[i * 3 + 1] = 0.5 + Math.random() * 0.5;
        sparkPositions[i * 3 + 2] = Math.sin(angle) * radius;
        
        sparkVelocities[i * 3] = (Math.random() - 0.5) * 0.02;
        sparkVelocities[i * 3 + 1] = 0.05 + Math.random() * 0.1;
        sparkVelocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
        
        sparkLifetimes[i] = 1.0;
        sparkSizes[i] = 0.02 + Math.random() * 0.03;
        
        const colorChoice = Math.random();
        if (colorChoice < 0.5) {
            sparkColors[i * 3] = 1;
            sparkColors[i * 3 + 1] = 0.5 + Math.random() * 0.3;
            sparkColors[i * 3 + 2] = 0.1;
        } else if (colorChoice < 0.8) {
            sparkColors[i * 3] = 1;
            sparkColors[i * 3 + 1] = 0.8 + Math.random() * 0.2;
            sparkColors[i * 3 + 2] = 0.2;
        } else {
            sparkColors[i * 3] = 1;
            sparkColors[i * 3 + 1] = 1;
            sparkColors[i * 3 + 2] = 0.6;
        }
    }
    
    sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
    sparkGeo.setAttribute('size', new THREE.BufferAttribute(sparkSizes, 1));
    sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkColors, 3));
    
    const sparkMat = new THREE.PointsMaterial({
        size: 1,
        vertexColors: true,
        transparent: true,
        opacity: 0.9,
        depthWrite: false,
        blending: THREE.AdditiveBlending,
        sizeAttenuation: true
    });
    
    const sparks = new THREE.Points(sparkGeo, sparkMat);
    scene.add(sparks);
    
    // Trees
    const trees = new THREE.Group();
    const treePositions = [];
    
    // Create tree template
    function createTree() {
        const tree = new THREE.Group();
        
        // Trunk
        const trunkHeight = 2 + Math.random() * 2;
        const trunkGeo = new THREE.CylinderGeometry(0.15, 0.25, trunkHeight, 6);
        const trunk = new THREE.Mesh(trunkGeo, trunkMat);
        trunk.position.y = trunkHeight / 2;
        trunk.castShadow = true;
        trunk.receiveShadow = true;
        tree.add(trunk);
        
        // Foliage layers (low poly)
        const layerCount = 3 + Math.floor(Math.random() * 3);
        for (let i = 0; i < layerCount; i++) {
            const layerHeight = 0.8 + Math.random() * 0.6;
            const layerRadius = 1.0 - i * 0.18 + Math.random() * 0.3;
            const leafGeo = new THREE.ConeGeometry(layerRadius, layerHeight, 6);
            leafGeo.translate(0, layerHeight / 2, 0);
            const leaves = new THREE.Mesh(leafGeo, leafMat);
            leaves.position.y = trunkHeight * 0.5 + i * 0.8;
            leaves.rotation.y = Math.random() * Math.PI * 2;
            leaves.scale.y = 0.8 + Math.random() * 0.4;
            leaves.castShadow = true;
            leaves.receiveShadow = true;
            tree.add(leaves);
        }
        
        // Color variation
        const hueShift = (Math.random() - 0.5) * 0.1;
        tree.traverse(child => {
            if (child.isMesh && child.material.color) {
                const color = child.material.color.clone();
                color.offsetHSL(hueShift, 0, (Math.random() - 0.5) * 0.1);
                child.material = child.material.clone();
                child.material.color = color;
            }
        });
        
        return tree;
    }
    
    // Place trees randomly around clearing
    for (let i = 0; i < 35; i++) {
        let x, z;
        let attempts = 0;
        do {
            const angle = Math.random() * Math.PI * 2;
            const radius = 8 + Math.random() * 25;
            x = Math.cos(angle) * radius;
            z = Math.sin(angle) * radius;
            attempts++;
        } while (x * x + z * z < 4 * 4 && attempts < 50); // Keep away from fire
        
        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);
        trees.add(tree);
        treePositions.push({ x, z, tree });
    }
    scene.add(trees);
    
    // Rocks and logs scattered around
    const debris = new THREE.Group();
    
    // Rocks
    for (let i = 0; i < 20; i++) {
        const angle = Math.random() * Math.PI * 2;
        const radius = 4 + Math.random() * 22;
        const x = Math.cos(angle) * radius;
        const z = Math.sin(angle) * radius;
        
        if (x * x + z * z < 3 * 3) continue;
        
        const rockGeo = new THREE.DodecahedronGeometry(0.3 + Math.random() * 0.5, 0);
        const rock = new THREE.Mesh(rockGeo, rockMat);
        rock.position.set(x, 0.15, z);
        rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
        rock.scale.setScalar(0.5 + Math.random() * 1.2);
        rock.castShadow = true;
        rock.receiveShadow = true;
        debris.add(rock);
    }
    
    // Fallen logs
    for (let i = 0; i < 12; i++) {
        const angle = Math.random() * Math.PI * 2;
        const radius = 5 + Math.random() * 20;
        const x = Math.cos(angle) * radius;
        const z = Math.sin(angle) * radius;
        
        if (x * x + z * z < 3 * 3) continue;
        
        const logGeo = new THREE.CylinderGeometry(0.12, 0.18, 1.5 + Math.random() * 1.5, 6);
        const log = new THREE.Mesh(logGeo, logMat);
        log.position.set(x, 0.15, z);
        log.rotation.set(
            (Math.random() - 0.5) * 0.3,
            Math.random() * Math.PI * 2,
            (Math.random() - 0.5) * 0.3
        );
        log.castShadow = true;
        log.receiveShadow = true;
        debris.add(log);
    }
    scene.add(debris);
    
    // Sky dome with stars
    const skyGeo = new THREE.SphereGeometry(80, 32, 32);
    skyGeo.scale(-1, 1, 1); // Invert normals
    const skyMat = new THREE.MeshBasicMaterial({
        color: 0x050510,
        fog: false
    });
    const sky = new THREE.Mesh(skyGeo, skyMat);
    scene.add(sky);
    
    // Stars as points
    const starCount = 2000;
    const starGeo = new THREE.BufferGeometry();
    const starPositions = new Float32Array(starCount * 3);
    const starSizes = new Float32Array(starCount);
    const starAlphas = new Float32Array(starCount);
    const starPhases = new Float32Array(starCount);
    
    for (let i = 0; i < starCount; i++) {
        const radius = 75;
        const theta = Math.random() * Math.PI * 2;
        const phi = Math.acos(2 * Math.random() - 1);
        starPositions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);
        starPositions[i * 3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
        starPositions[i * 3 + 2] = radius * Math.cos(phi);
        starSizes[i] = 0.5 + Math.random() * 1.5;
        starAlphas[i] = 0.3 + Math.random() * 0.7;
        starPhases[i] = Math.random() * Math.PI * 2;
    }
    
    starGeo.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
    starGeo.setAttribute('size', new THREE.BufferAttribute(starSizes, 1));
    starGeo.setAttribute('alpha', new THREE.BufferAttribute(starAlphas, 1));
    starGeo.setAttribute('phase', new THREE.BufferAttribute(starPhases, 1));
    
    const starMat = new THREE.PointsMaterial({
        color: 0xffffff,
        size: 1,
        vertexColors: false,
        transparent: true,
        opacity: 1,
        depthWrite: false,
        sizeAttenuation: false,
        blending: THREE.AdditiveBlending
    });
    
    // Custom shader for twinkling stars
    starMat.onBeforeCompile = (shader) => {
        shader.uniforms.uTime = { value: 0 };
        shader.vertexShader = `
            attribute float alpha;
            attribute float phase;
            uniform float uTime;
            varying float vAlpha;
            ${shader.vertexShader}
        `.replace(
            'void main() {',
            `void main() {
                vAlpha = alpha * (0.7 + 0.3 * sin(uTime * 2.0 + phase * 10.0));
            `
        );
        shader.fragmentShader = `
            varying float vAlpha;
            ${shader.fragmentShader}
        `.replace(
            'gl_FragColor = vec4( outgoingLight, diffuseColor.a );',
            `gl_FragColor = vec4( outgoingLight, vAlpha );`
        );
    };
    
    const stars = new THREE.Points(starGeo, starMat);
    scene.add(stars);
    
    // Moon
    const moonGeo = new THREE.SphereGeometry(3, 32, 32);
    const moonMat = new THREE.MeshBasicMaterial({
        color: 0xf0e8d8,
        transparent: true,
        opacity: 0.9,
        depthWrite: false
    });
    const moon = new THREE.Mesh(moonGeo, moonMat);
    moon.position.set(-25, 30, -20);
    moon.scale.setScalar(1.5);
    scene.add(moon);
    
    // Moon glow
    const moonGlowGeo = new THREE.SphereGeometry(4, 16, 16);
    const moonGlowMat = new THREE.MeshBasicMaterial({
        color: 0x88aacc,
        transparent: true,
        opacity: 0.08,
        depthWrite: false,
        side: THREE.BackSide
    });
    const moonGlow = new THREE.Mesh(moonGlowGeo, moonGlowMat);
    moonGlow.position.copy(moon.position);
    scene.add(moonGlow);
    
    // Animation
    let time = 0;
    const clock = new THREE.Clock();
    
    function animate() {
        const dt = clock.getDelta();
        time += dt;
        
        // Flame animation
        flameMeshes.forEach((mesh, layer) => {
            const positions = mesh.geometry.attributes.position.array;
            const basePositions = baseFlamePositions[layer];
            
            for (let i = 0; i < positions.length; i += 3) {
                const y = basePositions[i + 1];
                const h = mesh.geometry.parameters.height;
                const factor = Math.pow(y / h, 1.5);
                
                const noiseX = Math.sin(time * 3 + y * 5 + i * 0.1) * 0.03 * factor;
                const noiseZ = Math.cos(time * 2.5 + y * 5 + i * 0.1) * 0.03 * factor;
                const noiseY = Math.sin(time * 4 + y * 3 + i * 0.05) * 0.02 * factor;
                
                positions[i] = basePositions[i] + noiseX;
                positions[i + 1] = basePositions[i + 1] + noiseY;
                positions[i + 2] = basePositions[i + 2] + noiseZ;
            }
            mesh.geometry.attributes.position.needsUpdate = true;
            mesh.geometry.computeVertexNormals();
            
            // Rotate flames slightly
            mesh.rotation.y = time * 0.3 * (layer % 2 === 0 ? 1 : -1);
            
            // Pulse opacity
            mesh.material.opacity = (0.7 - layer * 0.1) * (0.85 + 0.15 * Math.sin(time * 5 + layer));
        });
        
        // Glow pulse
        glow.scale.y = 1.8 + Math.sin(time * 4) * 0.2;
        glow.scale.x = glow.scale.z = 1.2 + Math.sin(time * 3.5) * 0.15;
        glow.material.opacity = 0.35 + 0.1 * Math.sin(time * 3);
        glow.rotation.y += dt * 0.5;
        
        // Fire light flicker
        fireLight.intensity = 60 + Math.sin(time * 4) * 15 + Math.sin(time * 7.3) * 8 + Math.random() * 5;
        fireLight.color.setHSL(0.05, 1, 0.4 + Math.sin(time * 3) * 0.05);
        fireLight.position.x = Math.sin(time * 1.7) * 0.1;
        fireLight.position.z = Math.cos(time * 1.3) * 0.1;
        
        // Sparks update
        for (let i = 0; i < sparkCount; i++) {
            sparkPositions[i * 3] += sparkVelocities[i * 3];
            sparkPositions[i * 3 + 1] += sparkVelocities[i * 3 + 1];
            sparkPositions[i * 3 + 2] += sparkVelocities[i * 3 + 2];
            
            // Gravity and air resistance
            sparkVelocities[i * 3 + 1] -= 0.0015;
            sparkVelocities[i * 3] *= 0.995;
            sparkVelocities[i * 3 + 2] *= 0.995;
            
            sparkLifetimes[i] -= dt * 0.8;
            
            if (sparkLifetimes[i] <= 0 || sparkPositions[i * 3 + 1] < 0.2) {
                resetSpark(i);
            }
        }
        sparkGeo.attributes.position.needsUpdate = true;
        
        // Stars twinkle
        if (starMat.uniforms) {
            starMat.uniforms.uTime.value = time;
        }
        
        // Subtle camera drift
        controls.target.x = Math.sin(time * 0.1) * 0.15;
        controls.target.z = Math.cos(time * 0.1) * 0.15;
        
        controls.update();
        renderer.render(scene, camera);
        requestAnimationFrame(animate);
    }
    
    // Handle resize
    window.addEventListener('resize', () => {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
    });
    
    animate();
    </script>
</body>
</html>