Models 44

Gemini 3.8 Flash

by GoogleReasoningClean
Open in new window

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

How it went

Total time
47.0 s
Cost
$0.033
Output tokens
8,633
Spent thinking
801 tokens
Speed
199.1 tok/s
Console errors
0
First token after
3.7 s
First visible text after
9.7 s
Prompt tokens
592
Code size
21.2 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"medium"}
Max output
65,536 tokens
Generated
Nov 12, 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 · 620 lines

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Cozy Campfire in Forest Clearing</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
    }
    body {
      overflow: hidden;
      background-color: #050811;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    }
    #canvas-container {
      width: 100vw;
      height: 100vh;
      position: absolute;
      top: 0;
      left: 0;
    }
    #ui-overlay {
      position: absolute;
      bottom: 24px;
      left: 50%;
      transform: translateX(-50%);
      color: rgba(220, 235, 255, 0.75);
      background: rgba(10, 16, 28, 0.55);
      backdrop-filter: blur(8px);
      padding: 8px 18px;
      border-radius: 20px;
      font-size: 13px;
      letter-spacing: 0.5px;
      pointer-events: none;
      border: 1px solid rgba(255, 255, 255, 0.1);
      box-shadow: 0 4px 18px rgba(0, 0, 0, 0.4);
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .dot {
      width: 8px;
      height: 8px;
      background: #ff7b24;
      border-radius: 50%;
      box-shadow: 0 0 8px #ff7b24;
      animation: pulse 2s infinite ease-in-out;
    }
    @keyframes pulse {
      0%, 100% { opacity: 0.6; transform: scale(0.9); }
      50% { opacity: 1; transform: scale(1.2); }
    }
  </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="canvas-container"></div>
  <div id="ui-overlay">
    <span class="dot"></span>
    <span>Drag to explore • Scroll to zoom</span>
  </div>

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

    // --- Scene Setup ---
    const container = document.getElementById('canvas-container');
    const scene = new THREE.Scene();
    const bgColor = 0x070b14;
    scene.background = new THREE.Color(bgColor);
    scene.fog = new THREE.FogExp2(0x090f1a, 0.024);

    const camera = new THREE.PerspectiveCamera(48, window.innerWidth / window.innerHeight, 0.1, 200);
    camera.position.set(13, 7.5, 15);

    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
    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.1;
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.target.set(0, 1.2, 0);
    controls.maxPolarAngle = Math.PI * 0.485; // Stay above ground
    controls.minDistance = 3.5;
    controls.maxDistance = 38;

    // --- Lighting ---
    // Ambient moonlight
    const ambientLight = new THREE.AmbientLight(0x132238, 0.7);
    scene.add(ambientLight);

    // Moonlight (Directional)
    const moonLight = new THREE.DirectionalLight(0x82b4ea, 0.85);
    moonLight.position.set(-25, 32, -22);
    moonLight.castShadow = true;
    moonLight.shadow.mapSize.width = 1024;
    moonLight.shadow.mapSize.height = 1024;
    moonLight.shadow.camera.near = 10;
    moonLight.shadow.camera.far = 70;
    const shadowDist = 28;
    moonLight.shadow.camera.left = -shadowDist;
    moonLight.shadow.camera.right = shadowDist;
    moonLight.shadow.camera.top = shadowDist;
    moonLight.shadow.camera.bottom = -shadowDist;
    moonLight.shadow.bias = -0.001;
    scene.add(moonLight);

    // Campfire PointLight (warm, flickering, casting shadows)
    const fireLight = new THREE.PointLight(0xff7722, 22, 28, 1.3);
    fireLight.position.set(0, 1.2, 0);
    fireLight.castShadow = true;
    fireLight.shadow.mapSize.width = 1024;
    fireLight.shadow.mapSize.height = 1024;
    fireLight.shadow.bias = -0.003;
    fireLight.shadow.radius = 2.5;
    scene.add(fireLight);

    // Secondary subtle fill for ground radiance
    const fireFill = new THREE.PointLight(0xff3300, 4, 12, 1.5);
    fireFill.position.set(0, 0.4, 0);
    scene.add(fireFill);

    // --- Night Sky & Moon ---
    // Low-poly Moon
    const moonGeo = new THREE.IcosahedronGeometry(2.4, 1);
    const moonMat = new THREE.MeshBasicMaterial({ color: 0xeaf2ff });
    const moon = new THREE.Mesh(moonGeo, moonMat);
    moon.position.set(-38, 48, -34);
    scene.add(moon);

    // Subtle moon glow sprite
    const moonGlowCanvas = document.createElement('canvas');
    moonGlowCanvas.width = 128;
    moonGlowCanvas.height = 128;
    const ctx = moonGlowCanvas.getContext('2d');
    const grad = ctx.createRadialGradient(64, 64, 10, 64, 64, 64);
    grad.addColorStop(0, 'rgba(180, 215, 255, 0.45)');
    grad.addColorStop(0.4, 'rgba(130, 180, 245, 0.15)');
    grad.addColorStop(1, 'rgba(10, 18, 32, 0)');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, 128, 128);
    const glowTex = new THREE.CanvasTexture(moonGlowCanvas);
    const moonGlow = new THREE.Sprite(new THREE.SpriteMaterial({
      map: glowTex,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    }));
    moonGlow.scale.set(16, 16, 1);
    moon.add(moonGlow);

    // Stars
    const starCount = 350;
    const starGeo = new THREE.BufferGeometry();
    const starPos = new Float32Array(starCount * 3);
    for (let i = 0; i < starCount; i++) {
      const u = Math.random();
      const v = Math.random();
      const theta = u * 2.0 * Math.PI;
      const phi = Math.acos(2.0 * v - 1.0) * 0.5; // Upper dome
      const r = 85 + Math.random() * 20;
      starPos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
      starPos[i * 3 + 1] = Math.abs(r * Math.cos(phi)) + 12;
      starPos[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
    }
    starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
    const starMat = new THREE.PointsMaterial({
      color: 0xbed6f8,
      size: 0.8,
      sizeAttenuation: true,
      transparent: true,
      opacity: 0.85
    });
    scene.add(new THREE.Points(starGeo, starMat));

    // --- Low-Poly Ground with slight variation ---
    const groundRadius = 38;
    const groundSegments = 32;
    const groundGeo = new THREE.CircleGeometry(groundRadius, groundSegments);
    groundGeo.rotateX(-Math.PI / 2);

    // Randomize vertex heights slightly for rustic low-poly terrain
    const posAttr = groundGeo.attributes.position;
    for (let i = 0; i < posAttr.count; i++) {
      const x = posAttr.getX(i);
      const z = posAttr.getZ(i);
      const distFromCenter = Math.sqrt(x * x + z * z);
      let y = 0;
      if (distFromCenter > 3.5) {
        y = (Math.sin(x * 0.25) * Math.cos(z * 0.25) * 0.6) +
            (Math.sin(x * 0.6 + z * 0.4) * 0.25);
        if (distFromCenter > 22) {
          y += (distFromCenter - 22) * 0.15; // Gentle outer hills
        }
      } else {
        // Campfire bowl: slight depression
        y = -0.1 + (distFromCenter / 3.5) * 0.1;
      }
      posAttr.setY(i, y);
    }
    groundGeo.computeVertexNormals();

    const groundMat = new THREE.MeshStandardMaterial({
      color: 0x1f2e21,
      roughness: 0.95,
      metalness: 0.05,
      flatShading: true
    });
    const ground = new THREE.Mesh(groundGeo, groundMat);
    ground.receiveShadow = true;
    scene.add(ground);

    // Dirt mound under fire pit
    const dirtGeo = new THREE.CylinderGeometry(2.3, 2.7, 0.2, 10);
    const dirtMat = new THREE.MeshStandardMaterial({
      color: 0x221a14,
      roughness: 1.0,
      flatShading: true
    });
    const dirt = new THREE.Mesh(dirtGeo, dirtMat);
    dirt.position.y = 0.03;
    dirt.receiveShadow = true;
    scene.add(dirt);

    // --- Campfire Base: Fire Pit Rocks & Burning Logs ---
    const rockMat = new THREE.MeshStandardMaterial({
      color: 0x484c52,
      roughness: 0.88,
      flatShading: true
    });

    const firePitGroup = new THREE.Group();
    const numPitRocks = 12;
    for (let i = 0; i < numPitRocks; i++) {
      const angle = (i / numPitRocks) * Math.PI * 2;
      const r = 1.6 + (Math.sin(i * 3.7) * 0.15);
      const rockGeo = new THREE.DodecahedronGeometry(0.32 + Math.random() * 0.12, 0);
      const rock = new THREE.Mesh(rockGeo, rockMat);
      rock.position.set(Math.cos(angle) * r, 0.15, Math.sin(angle) * r);
      rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
      rock.scale.set(1 + Math.random() * 0.4, 0.75 + Math.random() * 0.3, 1 + Math.random() * 0.4);
      rock.castShadow = true;
      rock.receiveShadow = true;
      firePitGroup.add(rock);
    }

    // Firewood logs stacked in teepee style
    const logMat = new THREE.MeshStandardMaterial({
      color: 0x3d2719,
      roughness: 0.9,
      flatShading: true
    });
    const charcoalMat = new THREE.MeshStandardMaterial({
      color: 0x181211,
      roughness: 0.95,
      flatShading: true
    });

    const numLogs = 6;
    for (let i = 0; i < numLogs; i++) {
      const angle = (i / numLogs) * Math.PI * 2;
      const logGeo = new THREE.CylinderGeometry(0.12, 0.15, 1.7, 5);
      const log = new THREE.Mesh(logGeo, (i % 2 === 0) ? logMat : charcoalMat);
      const dist = 0.65;
      log.position.set(Math.cos(angle) * dist, 0.45, Math.sin(angle) * dist);
      log.rotation.x = 0.55;
      log.rotation.y = angle + Math.PI / 2;
      log.rotation.z = (Math.random() - 0.5) * 0.2;
      log.castShadow = true;
      log.receiveShadow = true;
      firePitGroup.add(log);
    }

    // Embers/ash in the center
    const emberBedGeo = new THREE.ConeGeometry(0.8, 0.4, 7);
    const emberBedMat = new THREE.MeshStandardMaterial({
      color: 0x220c04,
      emissive: 0xbf2c03,
      emissiveIntensity: 0.8,
      roughness: 0.9,
      flatShading: true
    });
    const emberBed = new THREE.Mesh(emberBedGeo, emberBedMat);
    emberBed.position.set(0, 0.15, 0);
    firePitGroup.add(emberBed);
    scene.add(firePitGroup);

    // --- Animated Low-Poly Flame Meshes ---
    const flameGroup = new THREE.Group();
    const flameElements = [];

    // Outer flame (orange-red)
    const flameGeoOuter = new THREE.ConeGeometry(0.8, 1.9, 5, 2);
    const flameMatOuter = new THREE.MeshStandardMaterial({
      color: 0xff3705,
      emissive: 0xff3b00,
      emissiveIntensity: 1.2,
      roughness: 0.2,
      flatShading: true,
      transparent: true,
      opacity: 0.92
    });
    const flameOuter = new THREE.Mesh(flameGeoOuter, flameMatOuter);
    flameOuter.position.y = 0.95;
    flameGroup.add(flameOuter);
    flameElements.push({ mesh: flameOuter, baseScale: new THREE.Vector3(1, 1, 1), speed: 7.2, rotSpeed: 1.5 });

    // Inner flame (bright yellow)
    const flameGeoInner = new THREE.ConeGeometry(0.48, 1.4, 4, 2);
    const flameMatInner = new THREE.MeshStandardMaterial({
      color: 0xffeb3b,
      emissive: 0xffbb00,
      emissiveIntensity: 2.0,
      roughness: 0.1,
      flatShading: true
    });
    const flameInner = new THREE.Mesh(flameGeoInner, flameMatInner);
    flameInner.position.y = 0.75;
    flameGroup.add(flameInner);
    flameElements.push({ mesh: flameInner, baseScale: new THREE.Vector3(0.7, 0.8, 0.7), speed: 9.5, rotSpeed: -2.2 });

    // Little licking tongue flame
    const flameGeoTip = new THREE.TetrahedronGeometry(0.35, 0);
    const flameMatTip = new THREE.MeshStandardMaterial({
      color: 0xff9100,
      emissive: 0xff5500,
      emissiveIntensity: 1.5,
      flatShading: true
    });
    const flameTip = new THREE.Mesh(flameGeoTip, flameMatTip);
    flameTip.position.set(0.15, 1.6, 0.1);
    flameGroup.add(flameTip);
    flameElements.push({ mesh: flameTip, baseScale: new THREE.Vector3(0.6, 1.2, 0.6), speed: 11.0, rotSpeed: 3.1 });

    scene.add(flameGroup);

    // --- Sparks Particle System ---
    const sparkCount = 45;
    const sparkPositions = new Float32Array(sparkCount * 3);
    const sparkVelocities = [];

    for (let i = 0; i < sparkCount; i++) {
      resetSpark(i, true);
    }

    function resetSpark(i, initialSpread = false) {
      const angle = Math.random() * Math.PI * 2;
      const radius = Math.random() * 0.45;
      sparkPositions[i * 3] = Math.cos(angle) * radius;
      sparkPositions[i * 3 + 1] = initialSpread ? 0.3 + Math.random() * 3.5 : 0.4 + Math.random() * 0.2;
      sparkPositions[i * 3 + 2] = Math.sin(angle) * radius;

      sparkVelocities[i] = {
        vx: (Math.random() - 0.5) * 0.4,
        vy: 1.8 + Math.random() * 1.8,
        vz: (Math.random() - 0.5) * 0.4,
        drift: Math.random() * Math.PI * 2,
        life: initialSpread ? Math.random() : 1.0,
        decay: 0.35 + Math.random() * 0.3
      };
    }

    const sparkGeo = new THREE.BufferGeometry();
    sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));

    // Canvas particle texture for bright glowing embers
    const sparkCanvas = document.createElement('canvas');
    sparkCanvas.width = 32;
    sparkCanvas.height = 32;
    const sCtx = sparkCanvas.getContext('2d');
    const sGrad = sCtx.createRadialGradient(16, 16, 2, 16, 16, 16);
    sGrad.addColorStop(0, '#ffffff');
    sGrad.addColorStop(0.3, '#ffaa33');
    sGrad.addColorStop(0.7, '#ff3300');
    sGrad.addColorStop(1, 'rgba(0,0,0,0)');
    sCtx.fillStyle = sGrad;
    sCtx.fillRect(0, 0, 32, 32);
    const sparkTex = new THREE.CanvasTexture(sparkCanvas);

    const sparkMat = new THREE.PointsMaterial({
      color: 0xffaa44,
      size: 0.35,
      map: sparkTex,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    });
    const sparks = new THREE.Points(sparkGeo, sparkMat);
    scene.add(sparks);

    // --- Seating Logs & Scatter Rocks in Clearing ---
    const seatLogGeo = new THREE.CylinderGeometry(0.3, 0.32, 3.8, 6);
    const seatLogMat = new THREE.MeshStandardMaterial({
      color: 0x4a3222,
      roughness: 0.92,
      flatShading: true
    });

    const benchPositions = [
      { x: -3.8, z: 0.5, rotY: 0.25 },
      { x: 3.2, z: 2.2, rotY: -1.1 },
      { x: 0.2, z: -3.6, rotY: 2.1 }
    ];

    benchPositions.forEach((bench) => {
      const logMesh = new THREE.Mesh(seatLogGeo, seatLogMat);
      logMesh.position.set(bench.x, 0.28, bench.z);
      logMesh.rotation.x = Math.PI / 2;
      logMesh.rotation.z = bench.rotY;
      logMesh.castShadow = true;
      logMesh.receiveShadow = true;
      scene.add(logMesh);

      // Support stones under ends of bench log
      for (let s of [-1.5, 1.5]) {
        const supportStoneGeo = new THREE.DodecahedronGeometry(0.22, 0);
        const stone = new THREE.Mesh(supportStoneGeo, rockMat);
        stone.position.set(
          bench.x + Math.sin(bench.rotY) * s,
          0.12,
          bench.z + Math.cos(bench.rotY) * s
        );
        stone.castShadow = true;
        stone.receiveShadow = true;
        scene.add(stone);
      }
    });

    // Scattering loose rocks around clearing
    const scatterRockGroup = new THREE.Group();
    for (let i = 0; i < 18; i++) {
      const angle = Math.random() * Math.PI * 2;
      const r = 2.4 + Math.random() * 10;
      const rock = new THREE.Mesh(
        new THREE.DodecahedronGeometry(0.18 + Math.random() * 0.25, 0),
        rockMat
      );
      rock.position.set(Math.cos(angle) * r, 0.12, Math.sin(angle) * r);
      rock.rotation.set(Math.random(), Math.random(), Math.random());
      rock.scale.set(1 + Math.random() * 0.4, 0.7 + Math.random() * 0.3, 1 + Math.random() * 0.4);
      rock.castShadow = true;
      rock.receiveShadow = true;
      scatterRockGroup.add(rock);
    }
    scene.add(scatterRockGroup);

    // --- Low-Poly Trees Generation (Pine & Fir Trees) ---
    // Palette variations for pine foliage & bark
    const foliageColors = [0x162c1c, 0x1b3522, 0x1e3f28, 0x14281a];
    const trunkMat = new THREE.MeshStandardMaterial({
      color: 0x2e1c12,
      roughness: 0.95,
      flatShading: true
    });

    function createPineTree(scale = 1.0) {
      const tree = new THREE.Group();
      const col = foliageColors[Math.floor(Math.random() * foliageColors.length)];
      const foliageMat = new THREE.MeshStandardMaterial({
        color: col,
        roughness: 0.88,
        flatShading: true
      });

      // Trunk
      const trunkHeight = 1.4 * scale;
      const trunkGeo = new THREE.CylinderGeometry(0.2 * scale, 0.35 * scale, trunkHeight, 5);
      const trunk = new THREE.Mesh(trunkGeo, trunkMat);
      trunk.position.y = trunkHeight / 2;
      trunk.castShadow = true;
      trunk.receiveShadow = true;
      tree.add(trunk);

      // Stacked foliage cones (3 or 4 layers)
      const layers = 3 + (Math.random() > 0.5 ? 1 : 0);
      let bottomY = trunkHeight * 0.7;
      let baseRadius = 1.6 * scale;
      let layerHeight = 1.8 * scale;

      for (let i = 0; i < layers; i++) {
        const coneGeo = new THREE.ConeGeometry(baseRadius, layerHeight, 5 + Math.floor(Math.random() * 2));
        const cone = new THREE.Mesh(coneGeo, foliageMat);
        cone.position.y = bottomY + (layerHeight * 0.55);
        cone.rotation.y = Math.random() * Math.PI;
        cone.castShadow = true;
        cone.receiveShadow = true;
        tree.add(cone);

        bottomY += layerHeight * 0.48;
        baseRadius *= 0.73;
        layerHeight *= 0.88;
      }

      return tree;
    }

    // Place at least 45 trees safely outside the clearing (clearing radius ~ 5.5 to 7.0)
    const treeGroup = new THREE.Group();
    const treeCount = 52;
    const clearingRadius = 5.8;

    for (let i = 0; i < treeCount; i++) {
      let x, z, dist;
      let attempts = 0;
      do {
        const angle = Math.random() * Math.PI * 2;
        // Distribute from clearing edge to forest edge with bias toward denser edge
        const r = clearingRadius + Math.pow(Math.random(), 0.7) * (groundRadius - clearingRadius - 2.5);
        x = Math.cos(angle) * r;
        z = Math.sin(angle) * r;
        dist = Math.sqrt(x * x + z * z);
        attempts++;
      } while (dist < clearingRadius && attempts < 50);

      const treeScale = 0.8 + Math.random() * 0.8;
      const tree = createPineTree(treeScale);
      tree.position.set(x, 0, z);
      tree.rotation.y = Math.random() * Math.PI * 2;
      tree.rotation.x = (Math.random() - 0.5) * 0.08;
      tree.rotation.z = (Math.random() - 0.5) * 0.08;
      treeGroup.add(tree);
    }
    scene.add(treeGroup);

    // --- Animation Loop ---
    const clock = new THREE.Clock();

    function animate() {
      requestAnimationFrame(animate);

      const elapsed = clock.getElapsedTime();
      const delta = clock.getDelta();

      // 1. Flicker Campfire PointLights with organic multi-frequency noise
      const n1 = Math.sin(elapsed * 12.0) * 1.8;
      const n2 = Math.sin(elapsed * 24.3 + 1.2) * 1.1;
      const n3 = Math.cos(elapsed * 41.7 + 2.5) * 0.7;
      const jitter = (Math.random() - 0.5) * 1.4;
      const currentIntensity = 21 + n1 + n2 + n3 + jitter;
      fireLight.intensity = Math.max(12, currentIntensity);

      // Micro-shift light position to mimic moving flame source
      fireLight.position.x = (Math.sin(elapsed * 8.0) * 0.08) + (Math.random() - 0.5) * 0.04;
      fireLight.position.z = (Math.cos(elapsed * 7.0) * 0.08) + (Math.random() - 0.5) * 0.04;
      fireLight.position.y = 1.15 + (Math.sin(elapsed * 14.0) * 0.06);

      fireFill.intensity = 3.5 + Math.sin(elapsed * 10) * 0.8;

      // 2. Animate Flame Meshes (scale pulsing, twisting & shifting)
      flameElements.forEach((f, idx) => {
        const timeFactor = elapsed * f.speed;
        const scaleX = f.baseScale.x * (1.0 + Math.sin(timeFactor) * 0.16 + (Math.random() - 0.5) * 0.04);
        const scaleY = f.baseScale.y * (1.0 + Math.cos(timeFactor * 0.8) * 0.22);
        const scaleZ = f.baseScale.z * (1.0 + Math.sin(timeFactor + 1.5) * 0.16 + (Math.random() - 0.5) * 0.04);

        f.mesh.scale.set(scaleX, scaleY, scaleZ);
        f.mesh.rotation.y = elapsed * f.rotSpeed;
        f.mesh.rotation.z = Math.sin(elapsed * 5 + idx) * 0.08;
      });

      // 3. Animate Rising Sparks / Embers
      const positions = sparkGeo.attributes.position.array;
      for (let i = 0; i < sparkCount; i++) {
        const v = sparkVelocities[i];
        v.drift += 0.04;

        positions[i * 3] += v.vx * 0.02 + Math.sin(v.drift) * 0.012;
        positions[i * 3 + 1] += v.vy * 0.018;
        positions[i * 3 + 2] += v.vz * 0.02 + Math.cos(v.drift) * 0.012;

        v.life -= v.decay * 0.016;

        // Reset spark when it burns out or rises high
        if (v.life <= 0 || positions[i * 3 + 1] > 4.5) {
          resetSpark(i, false);
        }
      }
      sparkGeo.attributes.position.needsUpdate = true;

      // 4. Subtle camera idle breathing if user isn't interacting
      if (!controls.state || controls.state === -1) {
        controls.update();
      } else {
        controls.update();
      }

      renderer.render(scene, camera);
    }

    animate();

    // --- Window Resize Handling ---
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    });
  </script>
</body>
</html>
Reasoning trace (801 tokens)
**Generating Scenic Campfire**

I'm currently building a low-poly 3D campfire scene within a forest clearing at night. My focus is on assembling a single HTML file, integrating inline CSS and JavaScript, and utilizing the specified Three.js r170.0 import map. I'm also designing the low-poly ground with surrounding rocks and logs.

**Populating Forest Clearing**

I'm now focusing on populating the scene, placing over thirty randomly distributed low-poly trees and crafting a detailed campfire with animated flames and flickering light. I'm also implementing particle effects for sparks.

**Enhancing Ambient Effects**

I'm refining the campfire's visual impact by layering warm colors for the flames, from inner yellow to outer red and smoke. I'm also experimenting with particle systems for sparks, aiming for turbulent drift and fading effects. Further development includes adding a low-poly moon, starfield, and subtle fog to the scene.