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
1 min 7 s
Cost
$0.042
Output tokens
11,074
Spent thinking
1,826 tokens
Speed
171.1 tok/s
Console errors
0
First token after
2.2 s
First visible text after
18.8 s
Prompt tokens
592
Code size
25.5 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
model default
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 · 749 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 in Forest at Night</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body, html {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background-color: #050811;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
      user-select: none;
    }
    #webgl-canvas {
      width: 100%;
      height: 100%;
      display: block;
    }
    #ui-hint {
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      color: rgba(230, 240, 255, 0.7);
      background: rgba(8, 12, 24, 0.6);
      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.08);
      transition: opacity 1s ease;
    }
    #sound-toggle {
      position: absolute;
      top: 20px;
      right: 20px;
      background: rgba(8, 12, 24, 0.65);
      border: 1px solid rgba(255, 255, 255, 0.15);
      color: #ffd899;
      padding: 8px 14px;
      border-radius: 20px;
      font-size: 13px;
      cursor: pointer;
      backdrop-filter: blur(8px);
      display: flex;
      align-items: center;
      gap: 6px;
      transition: all 0.2s;
    }
    #sound-toggle:hover {
      background: rgba(20, 28, 50, 0.8);
      border-color: rgba(255, 200, 120, 0.4);
    }
  </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="ui-hint">Click & drag to explore | Scroll to zoom</div>
  <button id="sound-toggle" title="Toggle procedural fire crackle">
    <span id="sound-icon">🔥</span> <span id="sound-text">Sound: Off</span>
  </button>
  <canvas id="webgl-canvas"></canvas>

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

    // --- Renderer & Scene Setup ---
    const canvas = document.getElementById('webgl-canvas');
    const renderer = new THREE.WebGLRenderer({ canvas, 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.05;

    const scene = new THREE.Scene();
    const fogColor = 0x090f1d;
    scene.background = new THREE.Color(fogColor);
    scene.fog = new THREE.FogExp2(fogColor, 0.024);

    const camera = new THREE.PerspectiveCamera(48, window.innerWidth / window.innerHeight, 0.1, 200);
    camera.position.set(0, 4.2, 11);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.maxPolarAngle = Math.PI / 2 - 0.06; // Prevent camera clipping through ground
    controls.minDistance = 3.5;
    controls.maxDistance = 35;
    controls.target.set(0, 0.9, 0);

    // Fade UI hint after camera interaction
    controls.addEventListener('start', () => {
      const hint = document.getElementById('ui-hint');
      if (hint) hint.style.opacity = '0';
    }, { once: true });

    // --- Lighting ---
    // Deep nocturnal ambient & hemisphere light
    const hemiLight = new THREE.HemisphereLight(0x1a2b4c, 0x070b12, 0.4);
    scene.add(hemiLight);

    const dirMoonLight = new THREE.DirectionalLight(0x7ea0d6, 0.35);
    dirMoonLight.position.set(22, 38, -25);
    dirMoonLight.castShadow = true;
    dirMoonLight.shadow.mapSize.width = 1024;
    dirMoonLight.shadow.mapSize.height = 1024;
    dirMoonLight.shadow.camera.near = 10;
    dirMoonLight.shadow.camera.far = 80;
    dirMoonLight.shadow.camera.left = -20;
    dirMoonLight.shadow.camera.right = 20;
    dirMoonLight.shadow.camera.top = 20;
    dirMoonLight.shadow.camera.bottom = -20;
    dirMoonLight.shadow.bias = -0.001;
    scene.add(dirMoonLight);

    // Warm Campfire Point Light
    const fireLight = new THREE.PointLight(0xff7711, 4.5, 26, 2.0);
    fireLight.position.set(0, 0.85, 0);
    fireLight.castShadow = true;
    fireLight.shadow.mapSize.width = 1024;
    fireLight.shadow.mapSize.height = 1024;
    fireLight.shadow.bias = -0.003;
    fireLight.shadow.radius = 2.0;
    scene.add(fireLight);

    // Subtle fill warm light slightly higher for foliage illumination
    const fireFill = new THREE.PointLight(0xff4400, 1.2, 12, 2.0);
    fireFill.position.set(0, 1.8, 0);
    scene.add(fireFill);

    // --- Night Sky: Stars & Moon ---
    // Stylized Moon
    const moonGroup = new THREE.Group();
    moonGroup.position.set(32, 45, -42);

    const moonGeo = new THREE.DodecahedronGeometry(2.4, 1);
    const moonMat = new THREE.MeshBasicMaterial({ color: 0xfff6cf });
    const moonMesh = new THREE.Mesh(moonGeo, moonMat);
    moonGroup.add(moonMesh);

    // Moon Glow Billboard
    const glowCanvas = document.createElement('canvas');
    glowCanvas.width = 128;
    glowCanvas.height = 128;
    const gctx = glowCanvas.getContext('2d');
    const grad = gctx.createRadialGradient(64, 64, 10, 64, 64, 64);
    grad.addColorStop(0, 'rgba(255, 245, 210, 0.45)');
    grad.addColorStop(0.35, 'rgba(180, 210, 255, 0.18)');
    grad.addColorStop(1, 'rgba(0,0,0,0)');
    gctx.fillStyle = grad;
    gctx.fillRect(0, 0, 128, 128);
    const glowTex = new THREE.CanvasTexture(glowCanvas);
    const glowSprite = new THREE.Sprite(new THREE.SpriteMaterial({
      map: glowTex,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    }));
    glowSprite.scale.set(16, 16, 1);
    moonGroup.add(glowSprite);
    scene.add(moonGroup);

    // Starfield
    const starCount = 700;
    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);
      const r = 85 + Math.random() * 20;
      // keep mostly upper hemisphere
      const y = Math.abs(r * Math.cos(phi)) + 5;
      const x = r * Math.sin(phi) * Math.sin(theta);
      const z = r * Math.sin(phi) * Math.cos(theta);
      starPos[i * 3] = x;
      starPos[i * 3 + 1] = y;
      starPos[i * 3 + 2] = z;
    }
    starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
    const starMat = new THREE.PointsMaterial({
      color: 0xe0eaff,
      size: 0.65,
      transparent: true,
      opacity: 0.85
    });
    const stars = new THREE.Points(starGeo, starMat);
    scene.add(stars);

    // --- Low-Poly Ground ---
    const groundGeo = new THREE.PlaneGeometry(80, 80, 36, 36);
    groundGeo.rotateX(-Math.PI / 2);
    const pos = groundGeo.attributes.position;

    // Displace vertices to form subtle gentle hills around clearing
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i);
      const z = pos.getZ(i);
      const dist = Math.sqrt(x * x + z * z);
      let y = 0;
      if (dist > 5.5) {
        // Rolling mounds outside clearing
        const factor = Math.min((dist - 5.5) * 0.12, 1.0);
        y += (Math.sin(x * 0.28) * Math.cos(z * 0.28) * 1.1 + Math.sin(x * 0.15) * 0.8) * factor;
        y += (Math.sin(z * 0.45) * 0.35) * factor;
      } else {
        // Flatten center with slight depression under fire
        y = -0.15 * (1 - dist / 5.5);
      }
      pos.setY(i, y);
    }
    groundGeo.computeVertexNormals();

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

    // Charred dirt circle in the center
    const ashGeo = new THREE.CircleGeometry(1.6, 12);
    ashGeo.rotateX(-Math.PI / 2);
    const ashMat = new THREE.MeshStandardMaterial({
      color: 0x151210,
      roughness: 1.0,
      flatShading: true
    });
    const ash = new THREE.Mesh(ashGeo, ashMat);
    ash.position.y = -0.11;
    ash.receiveShadow = true;
    scene.add(ash);

    // --- Helper function: Low Poly Rock Builder ---
    function createRock(scale = 1, color = 0x484c54) {
      const geo = new THREE.DodecahedronGeometry(0.5 * scale, 1);
      const v = geo.attributes.position;
      for (let i = 0; i < v.count; i++) {
        const x = v.getX(i);
        const y = v.getY(i);
        const z = v.getZ(i);
        const noise = 1 + (Math.sin(x * 5) * Math.cos(y * 5) * Math.sin(z * 5)) * 0.2;
        v.setXYZ(i, x * noise, y * noise, z * noise);
      }
      geo.computeVertexNormals();
      const mat = new THREE.MeshStandardMaterial({
        color: color,
        roughness: 0.88,
        flatShading: true
      });
      const rock = new THREE.Mesh(geo, mat);
      rock.castShadow = true;
      rock.receiveShadow = true;
      return rock;
    }

    // --- Fire Pit: Surrounding Stones ---
    const rockRingCount = 13;
    const ringRadius = 1.25;
    for (let i = 0; i < rockRingCount; i++) {
      const angle = (i / rockRingCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.15;
      const r = ringRadius + (Math.random() - 0.5) * 0.18;
      const s = 0.45 + Math.random() * 0.35;
      const rock = createRock(s, 0x40444c);
      rock.position.set(Math.cos(angle) * r, 0.08 * s, Math.sin(angle) * r);
      rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
      scene.add(rock);
    }

    // Extra scattered rocks in the clearing
    for (let i = 0; i < 9; i++) {
      const angle = Math.random() * Math.PI * 2;
      const dist = 3.2 + Math.random() * 8.0;
      const s = 0.5 + Math.random() * 0.8;
      const rock = createRock(s, 0x3a4046);
      rock.position.set(Math.cos(angle) * dist, 0.12 * s, Math.sin(angle) * dist);
      rock.rotation.set(Math.random(), Math.random(), Math.random());
      scene.add(rock);
    }

    // --- Campfire Logs ---
    const logMat = new THREE.MeshStandardMaterial({
      color: 0x422b18,
      roughness: 0.9,
      flatShading: true
    });
    const logCapMat = new THREE.MeshStandardMaterial({
      color: 0x8a623a,
      roughness: 0.8,
      flatShading: true
    });

    function createFireLog(length, radius) {
      const logGroup = new THREE.Group();
      const bodyGeo = new THREE.CylinderGeometry(radius * 0.85, radius, length, 6, 1, false);
      const body = new THREE.Mesh(bodyGeo, logMat);
      body.castShadow = true;
      body.receiveShadow = true;
      logGroup.add(body);
      return logGroup;
    }

    const logCount = 5;
    for (let i = 0; i < logCount; i++) {
      const log = createFireLog(1.4, 0.12);
      const angle = (i / logCount) * Math.PI * 2;
      log.position.set(Math.cos(angle) * 0.35, 0.22, Math.sin(angle) * 0.35);
      log.rotation.y = angle + Math.PI / 2;
      log.rotation.z = Math.PI / 4.2;
      log.rotation.x = (Math.random() - 0.5) * 0.2;
      scene.add(log);
    }

    // Sitting Logs around campfire
    function createSittingLog(length, radius, angle, dist) {
      const group = new THREE.Group();
      const geo = new THREE.CylinderGeometry(radius, radius, length, 7);
      const mesh = new THREE.Mesh(geo, logMat);
      mesh.rotation.z = Math.PI / 2;
      mesh.castShadow = true;
      mesh.receiveShadow = true;
      group.add(mesh);

      group.position.set(Math.cos(angle) * dist, radius * 0.85, Math.sin(angle) * dist);
      group.rotation.y = angle + Math.PI / 2 + (Math.random() - 0.5) * 0.2;
      scene.add(group);
    }
    createSittingLog(2.8, 0.28, 0.35, 3.4);
    createSittingLog(2.5, 0.26, 2.2, 3.2);
    createSittingLog(2.2, 0.25, -1.9, 3.6);

    // Cozy Low-Poly Tent nearby in clearing
    const tentGroup = new THREE.Group();
    tentGroup.position.set(-4.5, 0.0, -3.8);
    tentGroup.rotation.y = Math.PI * 0.28;

    const tentGeo = new THREE.ConeGeometry(1.6, 2.4, 4, 1, false);
    tentGeo.rotateY(Math.PI / 4);
    tentGeo.scale(1.2, 1.0, 1.7);
    const tentMat = new THREE.MeshStandardMaterial({
      color: 0x9b532b,
      roughness: 0.85,
      flatShading: true
    });
    const tentMesh = new THREE.Mesh(tentGeo, tentMat);
    tentMesh.position.y = 1.15;
    tentMesh.castShadow = true;
    tentMesh.receiveShadow = true;
    tentGroup.add(tentMesh);

    // Tent inner shadow / opening
    const doorGeo = new THREE.BufferGeometry();
    const doorVertices = new Float32Array([
      0, 1.8, 1.37,
      -0.75, 0.02, 1.37,
      0.75, 0.02, 1.37
    ]);
    doorGeo.setAttribute('position', new THREE.BufferAttribute(doorVertices, 3));
    doorGeo.computeVertexNormals();
    const doorMat = new THREE.MeshBasicMaterial({ color: 0x09080c, side: THREE.DoubleSide });
    const doorMesh = new THREE.Mesh(doorGeo, doorMat);
    tentGroup.add(doorMesh);
    scene.add(tentGroup);

    // --- Animated Low-Poly Campfire Flames ---
    const flameGroup = new THREE.Group();
    flameGroup.position.set(0, 0.15, 0);
    scene.add(flameGroup);

    const flameCount = 9;
    const flames = [];

    const flameMaterials = [
      new THREE.MeshBasicMaterial({ color: 0xffaa11, wireframe: false }),
      new THREE.MeshBasicMaterial({ color: 0xff5500, wireframe: false }),
      new THREE.MeshBasicMaterial({ color: 0xff2200, wireframe: false }),
      new THREE.MeshBasicMaterial({ color: 0xffea55, wireframe: false })
    ];

    for (let i = 0; i < flameCount; i++) {
      // 4 to 5 sided cones for faceted low-poly flame look
      const sides = 4 + (i % 2);
      const fGeo = new THREE.ConeGeometry(0.32, 1.2, sides, 2);
      const fMat = flameMaterials[i % flameMaterials.length];
      const mesh = new THREE.Mesh(fGeo, fMat);

      const angle = (i / flameCount) * Math.PI * 2;
      const rad = 0.12 + Math.random() * 0.18;
      mesh.position.set(Math.cos(angle) * rad, 0.4, Math.sin(angle) * rad);
      mesh.scale.set(0.8, 1.0, 0.8);

      flameGroup.add(mesh);
      flames.push({
        mesh,
        baseScale: 0.6 + Math.random() * 0.6,
        basePos: mesh.position.clone(),
        speed: 4.5 + Math.random() * 4.0,
        phase: Math.random() * Math.PI * 2,
        rotSpeed: (Math.random() - 0.5) * 3.0
      });
    }

    // Inner bright hot core
    const coreGeo = new THREE.IcosahedronGeometry(0.35, 0);
    const coreMat = new THREE.MeshBasicMaterial({ color: 0xfff0aa });
    const fireCore = new THREE.Mesh(coreGeo, coreMat);
    fireCore.position.set(0, 0.35, 0);
    flameGroup.add(fireCore);

    // --- Sparks / Embers Particle System ---
    const sparkCount = 130;
    const sparkGeo = new THREE.BufferGeometry();
    const sparkPos = new Float32Array(sparkCount * 3);
    const sparkData = [];

    function resetSpark(i) {
      const angle = Math.random() * Math.PI * 2;
      const r = Math.random() * 0.45;
      sparkPos[i * 3] = Math.cos(angle) * r;
      sparkPos[i * 3 + 1] = 0.2 + Math.random() * 0.4;
      sparkPos[i * 3 + 2] = Math.sin(angle) * r;

      sparkData[i] = {
        vy: 1.8 + Math.random() * 2.2,
        vx: (Math.random() - 0.5) * 0.8,
        vz: (Math.random() - 0.5) * 0.8,
        life: 0,
        maxLife: 1.0 + Math.random() * 1.4,
        turbFreq: 3 + Math.random() * 4
      };
    }

    for (let i = 0; i < sparkCount; i++) {
      resetSpark(i);
      // Stagger initial heights
      sparkPos[i * 3 + 1] = Math.random() * 3.0;
      sparkData[i].life = Math.random() * sparkData[i].maxLife;
    }

    sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));

    // Particle sprite
    const pCanvas = document.createElement('canvas');
    pCanvas.width = 32;
    pCanvas.height = 32;
    const pCtx = pCanvas.getContext('2d');
    const pGrad = pCtx.createRadialGradient(16, 16, 2, 16, 16, 16);
    pGrad.addColorStop(0, '#ffffff');
    pGrad.addColorStop(0.3, '#ffaa22');
    pGrad.addColorStop(0.8, '#ff3300');
    pGrad.addColorStop(1, 'rgba(0,0,0,0)');
    pCtx.fillStyle = pGrad;
    pCtx.fillRect(0, 0, 32, 32);
    const sparkTex = new THREE.CanvasTexture(pCanvas);

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

    // --- Low-Poly Trees Generation (> 30 trees) ---
    const foliageColors = [0x172d1c, 0x132718, 0x1d3623, 0x1b2c20, 0x102115];
    const trunkColor = 0x3d2716;

    const trunkMat = new THREE.MeshStandardMaterial({
      color: trunkColor,
      roughness: 0.95,
      flatShading: true
    });

    function createPineTree(scale = 1.0, color = 0x172d1c) {
      const tree = new THREE.Group();

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

      // Layered conical foliage
      const folMat = new THREE.MeshStandardMaterial({
        color: color,
        roughness: 0.85,
        flatShading: true
      });

      const tiers = 3;
      let bottomY = trunkHeight * 0.75;
      for (let t = 0; t < tiers; t++) {
        const tierScale = 1.0 - t * 0.22;
        const radius = 1.4 * scale * tierScale;
        const height = 1.8 * scale * tierScale;
        const segments = 5 + (t % 2); // asymmetric low poly look
        const coneGeo = new THREE.ConeGeometry(radius, height, segments);
        const coneMesh = new THREE.Mesh(coneGeo, folMat);
        coneMesh.position.y = bottomY + height / 2;
        coneMesh.rotation.y = t * 0.6 + (Math.random() - 0.5) * 0.2;
        coneMesh.castShadow = true;
        coneMesh.receiveShadow = true;
        tree.add(coneMesh);
        bottomY += height * 0.55;
      }

      return tree;
    }

    const treeCount = 55;
    const treeMinRadius = 7.0; // Keep central clearing clean
    const treeMaxRadius = 32.0;

    for (let i = 0; i < treeCount; i++) {
      // Golden ratio / random distribution
      const r = treeMinRadius + Math.sqrt(Math.random()) * (treeMaxRadius - treeMinRadius);
      const theta = Math.random() * Math.PI * 2;
      const x = Math.cos(theta) * r;
      const z = Math.sin(theta) * r;

      // Determine height on ground
      let y = 0;
      if (r > 5.5) {
        const factor = Math.min((r - 5.5) * 0.12, 1.0);
        y = (Math.sin(x * 0.28) * Math.cos(z * 0.28) * 1.1 + Math.sin(x * 0.15) * 0.8) * factor;
        y += (Math.sin(z * 0.45) * 0.35) * factor;
      }

      const s = 0.85 + Math.random() * 0.9;
      const col = foliageColors[i % foliageColors.length];
      const tree = createPineTree(s, col);
      tree.position.set(x, y - 0.1, z);
      tree.rotation.y = Math.random() * Math.PI * 2;
      // Slight natural tilt
      tree.rotation.x = (Math.random() - 0.5) * 0.08;
      tree.rotation.z = (Math.random() - 0.5) * 0.08;
      scene.add(tree);
    }

    // --- Procedural Sound Synthesis (Web Audio API) ---
    // Pure synthesized realistic campfire crackle & soft night wind
    let audioCtx = null;
    let isSoundOn = false;

    function initAudio() {
      if (audioCtx) return;
      try {
        const AudioContext = window.AudioContext || window.webkitAudioContext;
        audioCtx = new AudioContext();

        // White noise node for crackles and soft hiss
        const bufferSize = audioCtx.sampleRate * 2;
        const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
        const output = noiseBuffer.getChannelData(0);
        for (let i = 0; i < bufferSize; i++) {
          output[i] = Math.random() * 2 - 1;
        }

        const whiteNoise = audioCtx.createBufferSource();
        whiteNoise.buffer = noiseBuffer;
        whiteNoise.loop = true;

        // Bandpass filter for campfire hiss/rumble
        const hissFilter = audioCtx.createBiquadFilter();
        hissFilter.type = 'bandpass';
        hissFilter.frequency.value = 1100;
        hissFilter.Q.value = 1.2;

        const hissGain = audioCtx.createGain();
        hissGain.gain.value = 0.05;

        whiteNoise.connect(hissFilter);
        hissFilter.connect(hissGain);
        hissGain.connect(audioCtx.destination);
        whiteNoise.start();

        // Random impulsive crackle generator
        function triggerPop() {
          if (!isSoundOn || audioCtx.state !== 'running') {
            setTimeout(triggerPop, 100);
            return;
          }
          const popOsc = audioCtx.createOscillator();
          const popGain = audioCtx.createGain();
          const filter = audioCtx.createBiquadFilter();

          filter.type = 'highpass';
          filter.frequency.value = 800 + Math.random() * 2200;

          popOsc.type = 'triangle';
          popOsc.frequency.setValueAtTime(120 + Math.random() * 300, audioCtx.currentTime);
          popOsc.frequency.exponentialRampToValueAtTime(30, audioCtx.currentTime + 0.04);

          const popVol = 0.08 + Math.random() * 0.22;
          popGain.gain.setValueAtTime(popVol, audioCtx.currentTime);
          popGain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.04);

          popOsc.connect(filter);
          filter.connect(popGain);
          popGain.connect(audioCtx.destination);

          popOsc.start();
          popOsc.stop(audioCtx.currentTime + 0.05);

          const nextDelay = 40 + Math.random() * 260;
          setTimeout(triggerPop, nextDelay);
        }
        triggerPop();
      } catch (e) {
        console.warn("AudioContext not supported or blocked");
      }
    }

    const soundBtn = document.getElementById('sound-toggle');
    const soundText = document.getElementById('sound-text');
    const soundIcon = document.getElementById('sound-icon');

    soundBtn.addEventListener('click', () => {
      if (!audioCtx) initAudio();
      if (audioCtx.state === 'suspended') {
        audioCtx.resume();
      }
      isSoundOn = !isSoundOn;
      if (isSoundOn) {
        soundText.textContent = "Sound: On";
        soundIcon.textContent = "🔊";
        soundBtn.style.borderColor = "rgba(255, 170, 50, 0.7)";
      } else {
        soundText.textContent = "Sound: Off";
        soundIcon.textContent = "🔥";
        soundBtn.style.borderColor = "rgba(255, 255, 255, 0.15)";
      }
    });

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

    function animate() {
      requestAnimationFrame(animate);

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

      // Fire flicker: multiple sine harmonics + subtle pseudo-random noise
      const flicker1 = Math.sin(time * 14.0) * 0.45;
      const flicker2 = Math.sin(time * 28.3) * 0.3;
      const flicker3 = Math.cos(time * 42.1) * 0.15;
      const noiseFlicker = (Math.random() - 0.5) * 0.35;
      const targetIntensity = 4.2 + flicker1 + flicker2 + flicker3 + noiseFlicker;
      fireLight.intensity = Math.max(2.4, targetIntensity);

      // Light subtly dances horizontally
      fireLight.position.x = Math.sin(time * 5.0) * 0.09 + (Math.random() - 0.5) * 0.03;
      fireLight.position.z = Math.cos(time * 6.2) * 0.09 + (Math.random() - 0.5) * 0.03;

      // Animate low-poly flame meshes
      for (let i = 0; i < flames.length; i++) {
        const f = flames[i];
        const t = time * f.speed + f.phase;
        // Rising and expanding rhythm
        const wave = Math.sin(t);
        const cosWave = Math.cos(t * 0.8);

        f.mesh.scale.y = f.baseScale * (1.1 + wave * 0.35);
        f.mesh.scale.x = f.baseScale * (1.0 - wave * 0.2);
        f.mesh.scale.z = f.baseScale * (1.0 + cosWave * 0.2);

        f.mesh.position.y = f.basePos.y + wave * 0.18 + 0.12;
        f.mesh.position.x = f.basePos.x + Math.sin(time * 3.0 + i) * 0.05;
        f.mesh.position.z = f.basePos.z + Math.cos(time * 3.0 + i) * 0.05;
        f.mesh.rotation.y += f.rotSpeed * 0.02;
      }

      // Fire core pulsing
      const corePulse = 1.0 + Math.sin(time * 12.0) * 0.15;
      fireCore.scale.set(corePulse, corePulse * 1.1, corePulse);

      // Animate rising sparks particle system
      const positions = sparkGeo.attributes.position.array;
      for (let i = 0; i < sparkCount; i++) {
        const data = sparkData[i];
        data.life += 0.016;

        if (data.life >= data.maxLife) {
          resetSpark(i);
        } else {
          // Drifting turbulence & upward rise
          positions[i * 3 + 1] += data.vy * 0.016;
          positions[i * 3] += (data.vx + Math.sin(time * data.turbFreq + i) * 0.6) * 0.016;
          positions[i * 3 + 2] += (data.vz + Math.cos(time * data.turbFreq + i) * 0.6) * 0.016;
        }
      }
      sparkGeo.attributes.position.needsUpdate = true;

      // Gentle camera orbit idle rotation if user is not interacting
      if (!controls.state || controls.state === -1) {
        controls.autoRotate = true;
        controls.autoRotateSpeed = 0.45;
      } else {
        controls.autoRotate = false;
      }

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

    animate();

    // --- Responsive Window Resize ---
    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 (1,826 tokens)
**Drafting Scene Elements**

I'm currently focusing on modeling the core elements for the campfire scene. My current efforts are centered on creating low-poly assets, specifically the ground plane with scattered rocks and logs, ensuring they fit the desired aesthetic.

**Designing Dynamic Elements**

I'm now implementing the dynamic aspects of the scene. This involves developing the animated campfire flames, the flickering point light, and a particle system for sparks. Simultaneously, I'm refining the atmospheric effects, including the night sky, fog, and mood.

**Modeling Core Components**

I'm now meticulously modeling the foundational elements for the campfire scene. My focus is on crafting low-poly assets, including the terrain with scattered rocks and logs, ensuring they align with the envisioned style.

**Refining Lighting**

I'm fine-tuning the lighting for maximum atmosphere. This includes establishing the soft moonlight, the dynamic, flickering point light from the fire, and the subtle glow of the moon itself. I'm also adding star elements.

**Adding Scattered Assets**

I'm now populating the scene with surrounding details. This involves modeling low-poly sitting logs, scattered rocks, and charming elements like mushrooms, grass tufts, and even a rustic A-frame tent for storytelling.

**Flickering Light Intensity**

I'm experimenting with Perlin noise and sine waves, plus random noise, to achieve organic, realistic flicker for both light intensity and its position, aiming for a believable fire effect.

**Simulating Light and Sound**

I'm focusing on refining the lighting's realism by implementing ACES Filmic Tone Mapping and ensuring shadows are correctly rendered without clipping. I'm also exploring procedural audio synthesis for optional campfire crackle, adding an interactive mute/unmute feature.

**Evolving Flame Dynamics**

I'm refining the campfire's core loop, adding subtle turbulence to the flame's upward movement and managing its life cycle. Simultaneously, I'm designing the night sky with a dodecahedron moon, scattering stars, and populating the clearing with logs, a tent, and rocks.