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
2 min 34 s
Cost
$0.119
Output tokens
31,525
Spent thinking
16,127 tokens
Speed
207.7 tok/s
Console errors
0
First token after
1.8 s
First visible text after
1 min 31 s
Prompt tokens
592
Code size
41.9 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-3.8-flash
Reasoning sent
{"effort":"high"}
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 · 1246 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 Clearing</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      user-select: none;
      -webkit-user-select: none;
    }
    html, body {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background-color: #050813;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      color: #e2e8f0;
    }
    #canvas-container {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      z-index: 1;
    }
    /* Cinematic vignette */
    .vignette {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 2;
      background: radial-gradient(circle at 50% 55%, transparent 45%, rgba(4, 7, 16, 0.45) 80%, rgba(2, 4, 10, 0.8) 100%);
    }
    /* UI Overlay */
    .ui-layer {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      z-index: 3;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding: 24px;
    }
    .header {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
      gap: 16px;
    }
    .card {
      background: rgba(10, 16, 28, 0.6);
      backdrop-filter: blur(12px);
      -webkit-backdrop-filter: blur(12px);
      border: 1px solid rgba(255, 255, 255, 0.1);
      border-radius: 12px;
      padding: 12px 18px;
      box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
      pointer-events: auto;
    }
    .title-group h1 {
      font-size: 16px;
      font-weight: 700;
      letter-spacing: 1.5px;
      text-transform: uppercase;
      color: #ffb347;
      display: flex;
      align-items: center;
      gap: 8px;
    }
    .title-group p {
      font-size: 12px;
      color: #94a3b8;
      margin-top: 4px;
    }
    .controls-btn-group {
      display: flex;
      gap: 8px;
    }
    button.btn {
      background: rgba(18, 25, 42, 0.7);
      color: #cbd5e1;
      border: 1px solid rgba(255, 255, 255, 0.14);
      border-radius: 8px;
      padding: 8px 14px;
      font-size: 12px;
      font-weight: 500;
      cursor: pointer;
      display: flex;
      align-items: center;
      gap: 6px;
      transition: all 0.2s ease;
      pointer-events: auto;
    }
    button.btn:hover {
      background: rgba(30, 41, 68, 0.9);
      color: #ffffff;
      border-color: rgba(255, 179, 71, 0.4);
      transform: translateY(-1px);
    }
    button.btn.active {
      background: rgba(230, 92, 0, 0.25);
      border-color: #ff8c00;
      color: #ffc078;
    }
    .footer {
      display: flex;
      justify-content: space-between;
      align-items: flex-end;
    }
    .hint {
      font-size: 11px;
      color: #64748b;
      letter-spacing: 0.5px;
    }
    @media (max-width: 600px) {
      .ui-layer {
        padding: 14px;
      }
      .title-group p {
        display: none;
      }
      .hint {
        display: none;
      }
    }
  </style>

  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
      }
    }
  </script>
</head>
<body>
  <div id="canvas-container"></div>
  <div class="vignette"></div>

  <div class="ui-layer">
    <div class="header">
      <div class="card title-group">
        <h1><span>🔥</span> Campfire Clearing</h1>
        <p>Low-poly forest retreat under the moonlight</p>
      </div>
      <div class="controls-btn-group">
        <button id="audio-btn" class="btn">
          <span id="audio-icon">🔈</span>
          <span id="audio-label">Crackle Sound</span>
        </button>
        <button id="cam-btn" class="btn">
          <span>🔄</span>
          <span id="cam-label">Orbit: On</span>
        </button>
      </div>
    </div>
    <div class="footer">
      <div class="card hint">
        🖱️ Left drag: Orbit • Right drag: Pan • Scroll: Zoom (Clamped above ground)
      </div>
    </div>
  </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();
    scene.background = new THREE.Color(0x060913);
    scene.fog = new THREE.FogExp2(0x080e1a, 0.024);

    const camera = new THREE.PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      0.1,
      120
    );
    camera.position.set(9.5, 4.2, 9.5);

    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.05;
    container.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.target.set(0, 0.9, 0);
    controls.minDistance = 3.0;
    controls.maxDistance = 32.0;
    // Strictly clamp polar angle so camera never goes below the forest floor
    controls.maxPolarAngle = Math.PI / 2 - 0.05;
    controls.minPolarAngle = 0.15;
    controls.autoRotate = true;
    controls.autoRotateSpeed = 0.45;

    // --- Procedural Textures (No external image requests) ---
    function createSparkTexture() {
      const canvas = document.createElement('canvas');
      canvas.width = 64;
      canvas.height = 64;
      const ctx = canvas.getContext('2d');
      const grad = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
      grad.addColorStop(0, 'rgba(255, 255, 255, 1)');
      grad.addColorStop(0.25, 'rgba(255, 210, 80, 0.9)');
      grad.addColorStop(0.6, 'rgba(255, 90, 10, 0.35)');
      grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, 64, 64);
      return new THREE.CanvasTexture(canvas);
    }

    function createSmokeTexture() {
      const canvas = document.createElement('canvas');
      canvas.width = 64;
      canvas.height = 64;
      const ctx = canvas.getContext('2d');
      const grad = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
      grad.addColorStop(0, 'rgba(210, 220, 240, 0.4)');
      grad.addColorStop(0.5, 'rgba(140, 155, 180, 0.15)');
      grad.addColorStop(1, 'rgba(0, 0, 0, 0)');
      ctx.fillStyle = grad;
      ctx.fillRect(0, 0, 64, 64);
      return new THREE.CanvasTexture(canvas);
    }

    // --- Lighting ---
    // Deep midnight ambient
    const ambientLight = new THREE.AmbientLight(0x0e1728, 0.7);
    scene.add(ambientLight);

    // Pale cool moonlight from an angle
    const moonLight = new THREE.DirectionalLight(0x7da4d6, 0.65);
    moonLight.position.set(-24, 34, -22);
    moonLight.castShadow = true;
    moonLight.shadow.mapSize.width = 1024;
    moonLight.shadow.mapSize.height = 1024;
    moonLight.shadow.camera.near = 10;
    moonLight.shadow.camera.far = 70;
    moonLight.shadow.camera.left = -22;
    moonLight.shadow.camera.right = 22;
    moonLight.shadow.camera.top = 22;
    moonLight.shadow.camera.bottom = -22;
    moonLight.shadow.bias = -0.001;
    scene.add(moonLight);

    // Warm flickering campfire point light
    const fireLight = new THREE.PointLight(0xff6e1a, 4.2, 25, 1.25);
    fireLight.position.set(0, 0.85, 0);
    fireLight.castShadow = true;
    fireLight.shadow.mapSize.width = 1024;
    fireLight.shadow.mapSize.height = 1024;
    fireLight.shadow.camera.near = 0.1;
    fireLight.shadow.camera.far = 25;
    fireLight.shadow.bias = -0.003;
    scene.add(fireLight);

    // Secondary deep red ember light at ground level
    const emberLight = new THREE.PointLight(0xff2200, 1.6, 6, 2.0);
    emberLight.position.set(0, 0.25, 0);
    scene.add(emberLight);

    // --- Terrain Analytical Height Function ---
    function getGroundHeight(x, z) {
      const d = Math.sqrt(x * x + z * z);
      if (d < 1.4) {
        return -0.12 * (1.0 - d / 1.4); // slight fire depression
      }
      const rolling = 0.35 * Math.sin(x * 0.14) * Math.cos(z * 0.14) +
                      0.18 * Math.sin(x * 0.32 + 1.1) * Math.cos(z * 0.29);
      if (d < 6.5) {
        // Clearing transition
        const factor = (d - 1.4) / 5.1;
        return rolling * factor * 0.25;
      }
      // Outer bowl rise to frame the forest
      const rim = Math.max(0, d - 18) * 0.12;
      return rolling + rim;
    }

    // --- Low-Poly Ground with Vertex Colors ---
    const terrainGeo = new THREE.PlaneGeometry(86, 86, 54, 54);
    const posAttr = terrainGeo.attributes.position;
    const colors = [];

    const colAsh = new THREE.Color(0x151210);
    const colSoil = new THREE.Color(0x312419);
    const colGrassBase = new THREE.Color(0x22361b);
    const colGrassLush = new THREE.Color(0x2a4422);
    const colGrassDark = new THREE.Color(0x1a2b16);

    for (let i = 0; i < posAttr.count; i++) {
      const x = posAttr.getX(i);
      const y = posAttr.getY(i);
      const h = getGroundHeight(x, -y);
      posAttr.setZ(i, h);

      const d = Math.sqrt(x * x + y * y);
      const c = new THREE.Color();
      if (d < 1.3) {
        c.copy(colAsh);
      } else if (d < 2.5) {
        c.lerpColors(colAsh, colSoil, (d - 1.3) / 1.2);
      } else if (d < 6.0) {
        c.lerpColors(colSoil, colGrassBase, (d - 2.5) / 3.5);
      } else {
        const noise = (Math.sin(x * 0.7) * Math.cos(y * 0.7) + 1) * 0.5;
        c.lerpColors(colGrassDark, noise > 0.5 ? colGrassLush : colGrassBase, noise);
      }
      colors.push(c.r, c.g, c.b);
    }

    terrainGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
    terrainGeo.rotateX(-Math.PI / 2);
    terrainGeo.computeVertexNormals();

    const terrainMat = new THREE.MeshStandardMaterial({
      vertexColors: true,
      flatShading: true,
      roughness: 0.95,
      metalness: 0.05
    });
    const terrainMesh = new THREE.Mesh(terrainGeo, terrainMat);
    terrainMesh.receiveShadow = true;
    scene.add(terrainMesh);

    // --- Moon & Sky Details ---
    const moonGeo = new THREE.IcosahedronGeometry(2.4, 1);
    const moonMat = new THREE.MeshBasicMaterial({ color: 0xfffae8 });
    const moonMesh = new THREE.Mesh(moonGeo, moonMat);
    moonMesh.position.set(-24, 34, -22);
    scene.add(moonMesh);

    // Subtle moon glow sprite
    const moonHaloCanvas = document.createElement('canvas');
    moonHaloCanvas.width = 128;
    moonHaloCanvas.height = 128;
    const mctx = moonHaloCanvas.getContext('2d');
    const mgrad = mctx.createRadialGradient(64, 64, 0, 64, 64, 64);
    mgrad.addColorStop(0, 'rgba(230, 245, 255, 0.45)');
    mgrad.addColorStop(0.3, 'rgba(160, 205, 255, 0.15)');
    mgrad.addColorStop(1, 'rgba(0, 0, 0, 0)');
    mctx.fillStyle = mgrad;
    mctx.fillRect(0, 0, 128, 128);
    const moonHaloTex = new THREE.CanvasTexture(moonHaloCanvas);
    const moonHaloMat = new THREE.SpriteMaterial({
      map: moonHaloTex,
      transparent: true,
      blending: THREE.AdditiveBlending
    });
    const moonHalo = new THREE.Sprite(moonHaloMat);
    moonHalo.scale.set(12, 12, 1);
    moonHalo.position.copy(moonMesh.position);
    scene.add(moonHalo);

    // Star Dome
    const starCount = 650;
    const starPos = new Float32Array(starCount * 3);
    const starCols = 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(1.0 - v * 0.92); // upper dome
      const r = 58;

      starPos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
      starPos[i * 3 + 1] = Math.max(4.0, r * Math.cos(phi));
      starPos[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);

      const colorTint = Math.random();
      if (colorTint < 0.2) {
        starCols[i * 3] = 0.85; starCols[i * 3 + 1] = 0.92; starCols[i * 3 + 2] = 1.0;
      } else if (colorTint < 0.4) {
        starCols[i * 3] = 1.0; starCols[i * 3 + 1] = 0.88; starCols[i * 3 + 2] = 0.75;
      } else {
        starCols[i * 3] = 0.95; starCols[i * 3 + 1] = 0.98; starCols[i * 3 + 2] = 1.0;
      }
    }
    const starGeo = new THREE.BufferGeometry();
    starGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
    starGeo.setAttribute('color', new THREE.BufferAttribute(starCols, 3));
    const starMat = new THREE.PointsMaterial({
      size: 0.45,
      vertexColors: true,
      transparent: true,
      opacity: 0.85
    });
    const starPoints = new THREE.Points(starGeo, starMat);
    scene.add(starPoints);

    // --- Campfire Base: Ash, Fire Stones & Teepee Logs ---
    const campfireGroup = new THREE.Group();
    scene.add(campfireGroup);

    // Ash bed
    const ashGeo = new THREE.CylinderGeometry(0.9, 1.1, 0.12, 10);
    const ashMat = new THREE.MeshStandardMaterial({ color: 0x181412, roughness: 0.95, flatShading: true });
    const ashMesh = new THREE.Mesh(ashGeo, ashMat);
    ashMesh.position.y = -0.02;
    ashMesh.receiveShadow = true;
    campfireGroup.add(ashMesh);

    // Stone ring around fire (14 irregular low-poly rocks)
    const stoneCount = 14;
    const stoneRadius = 1.25;
    for (let i = 0; i < stoneCount; i++) {
      const angle = (i / stoneCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.15;
      const dist = stoneRadius + (Math.random() - 0.5) * 0.12;
      const s = 0.24 + Math.random() * 0.12;
      const sGeo = new THREE.DodecahedronGeometry(s, 0);
      const sp = sGeo.attributes.position;
      for (let j = 0; j < sp.count; j++) {
        sp.setXYZ(
          j,
          sp.getX(j) * (0.8 + Math.random() * 0.4),
          sp.getY(j) * (0.55 + Math.random() * 0.35),
          sp.getZ(j) * (0.8 + Math.random() * 0.4)
        );
      }
      sGeo.computeVertexNormals();
      const stoneMat = new THREE.MeshStandardMaterial({
        color: new THREE.Color().setHSL(0.08, 0.08, 0.32 + Math.random() * 0.15),
        roughness: 0.9,
        flatShading: true
      });
      const stone = new THREE.Mesh(sGeo, stoneMat);
      stone.position.set(Math.cos(angle) * dist, 0.08, Math.sin(angle) * dist);
      stone.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
      stone.castShadow = true;
      stone.receiveShadow = true;
      campfireGroup.add(stone);
    }

    // Criss-crossed logs in fire pit
    const logMat = new THREE.MeshStandardMaterial({ color: 0x332214, roughness: 0.9, flatShading: true });
    const charMat = new THREE.MeshStandardMaterial({ color: 0x14100d, roughness: 0.95, flatShading: true });
    const logCount = 5;
    for (let i = 0; i < logCount; i++) {
      const angle = (i / logCount) * Math.PI * 2 + 0.3;
      const logG = new THREE.Group();
      const wood = new THREE.Mesh(new THREE.CylinderGeometry(0.09, 0.11, 1.3, 5), logMat);
      wood.castShadow = true;
      wood.receiveShadow = true;
      logG.add(wood);

      // Charred end near fire heart
      const charTip = new THREE.Mesh(new THREE.CylinderGeometry(0.091, 0.105, 0.45, 5), charMat);
      charTip.position.y = 0.42;
      logG.add(charTip);

      logG.rotation.z = 0.65;
      logG.rotation.y = angle;
      logG.position.set(Math.cos(angle) * 0.38, 0.28, Math.sin(angle) * 0.38);
      campfireGroup.add(logG);
    }

    // --- Animated Low-Poly Campfire Flames ---
    const flameGroup = new THREE.Group();
    campfireGroup.add(flameGroup);

    // Inner bright hot core
    const coreMat = new THREE.MeshBasicMaterial({ color: 0xfff6cf });
    const coreMesh = new THREE.Mesh(new THREE.ConeGeometry(0.3, 0.9, 5), coreMat);
    coreMesh.position.y = 0.45;
    flameGroup.add(coreMesh);

    // Mid orange flame body
    const midFlameMat = new THREE.MeshBasicMaterial({ color: 0xff6a00 });
    const midFlameMesh = new THREE.Mesh(new THREE.ConeGeometry(0.48, 1.35, 6), midFlameMat);
    midFlameMesh.position.y = 0.6;
    flameGroup.add(midFlameMesh);

    // Dynamic flickering flame tongues
    const tongueMeshes = [];
    const tongueColors = [0xff4500, 0xff7b00, 0xffaa00, 0xe63900, 0xff8c1a, 0xff5500];
    const tongueCount = 6;
    for (let i = 0; i < tongueCount; i++) {
      const tMat = new THREE.MeshBasicMaterial({
        color: tongueColors[i % tongueColors.length],
        transparent: true,
        opacity: 0.9
      });
      const tGeo = new THREE.ConeGeometry(0.18 + Math.random() * 0.08, 0.9 + Math.random() * 0.4, 4);
      // Skew cone tip slightly for organic tongue shape
      const tPos = tGeo.attributes.position;
      tPos.setXYZ(0, (Math.random() - 0.5) * 0.15, 0.5, (Math.random() - 0.5) * 0.15);
      tGeo.computeVertexNormals();

      const tongue = new THREE.Mesh(tGeo, tMat);
      const angle = (i / tongueCount) * Math.PI * 2;
      const rad = 0.22;
      tongue.position.set(Math.cos(angle) * rad, 0.4, Math.sin(angle) * rad);
      tongue.userData = {
        baseY: 0.4,
        angle: angle,
        rad: rad,
        speed: 8.0 + i * 2.1,
        phase: i * 1.3
      };
      flameGroup.add(tongue);
      tongueMeshes.push(tongue);
    }

    // --- Sparks / Embers Particle System ---
    const sparkCount = 130;
    const sparkPositions = new Float32Array(sparkCount * 3);
    const sparkColors = new Float32Array(sparkCount * 3);
    const sparkSizes = new Float32Array(sparkCount);
    const sparkData = [];

    const sparkTex = createSparkTexture();

    function resetSpark(data, i) {
      const angle = Math.random() * Math.PI * 2;
      const r = Math.random() * 0.35;
      data.x = Math.cos(angle) * r;
      data.y = 0.25 + Math.random() * 0.2;
      data.z = Math.sin(angle) * r;

      data.vx = (Math.random() - 0.5) * 0.28;
      data.vy = 1.4 + Math.random() * 1.6;
      data.vz = (Math.random() - 0.5) * 0.28;

      data.life = 0;
      data.maxLife = 1.4 + Math.random() * 1.5;
      data.size = 0.18 + Math.random() * 0.18;
      data.seed = Math.random() * 10.0;

      sparkPositions[i * 3] = data.x;
      sparkPositions[i * 3 + 1] = data.y;
      sparkPositions[i * 3 + 2] = data.z;

      sparkColors[i * 3] = 1.0;
      sparkColors[i * 3 + 1] = 0.9;
      sparkColors[i * 3 + 2] = 0.4;

      sparkSizes[i] = data.size;
    }

    for (let i = 0; i < sparkCount; i++) {
      const d = {};
      resetSpark(d, i);
      // Stagger initial progress
      d.life = Math.random() * d.maxLife;
      sparkData.push(d);
    }

    const sparkGeo = new THREE.BufferGeometry();
    sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
    sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkColors, 3));
    sparkGeo.setAttribute('size', new THREE.BufferAttribute(sparkSizes, 1));

    const sparkMat = new THREE.PointsMaterial({
      size: 0.25,
      map: sparkTex,
      vertexColors: true,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    });
    const sparkPoints = new THREE.Points(sparkGeo, sparkMat);
    scene.add(sparkPoints);

    // --- Campfire Smoke Particle System ---
    const smokeCount = 35;
    const smokePositions = new Float32Array(smokeCount * 3);
    const smokeData = [];
    const smokeTex = createSmokeTexture();

    function resetSmoke(data, i) {
      data.x = (Math.random() - 0.5) * 0.25;
      data.y = 1.2 + Math.random() * 0.3;
      data.z = (Math.random() - 0.5) * 0.25;

      data.vx = 0.25 + (Math.random() - 0.5) * 0.2; // slight wind towards +X
      data.vy = 0.8 + Math.random() * 0.6;
      data.vz = 0.15 + (Math.random() - 0.5) * 0.2;

      data.life = 0;
      data.maxLife = 2.8 + Math.random() * 1.6;
      data.baseSize = 0.6 + Math.random() * 0.4;

      smokePositions[i * 3] = data.x;
      smokePositions[i * 3 + 1] = data.y;
      smokePositions[i * 3 + 2] = data.z;
    }

    for (let i = 0; i < smokeCount; i++) {
      const d = {};
      resetSmoke(d, i);
      d.life = Math.random() * d.maxLife;
      smokeData.push(d);
    }

    const smokeGeo = new THREE.BufferGeometry();
    smokeGeo.setAttribute('position', new THREE.BufferAttribute(smokePositions, 3));
    const smokeMat = new THREE.PointsMaterial({
      size: 1.2,
      map: smokeTex,
      transparent: true,
      opacity: 0.2,
      depthWrite: false,
      blending: THREE.NormalBlending
    });
    const smokePoints = new THREE.Points(smokeGeo, smokeMat);
    scene.add(smokePoints);

    // --- Forest Clearing Props: Seating Logs, Tent, Mug, Rocks, Mushrooms ---
    const propsGroup = new THREE.Group();
    scene.add(propsGroup);

    // Seating Log 1
    const benchMat = new THREE.MeshStandardMaterial({ color: 0x4a3422, roughness: 0.9, flatShading: true });
    const benchCutMat = new THREE.MeshStandardMaterial({ color: 0x96734c, roughness: 0.85, flatShading: true });

    function createSeatingLog(length, radius, angle, dist) {
      const g = new THREE.Group();
      const body = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius * 1.05, length, 6), benchMat);
      body.rotation.z = Math.PI / 2;
      body.castShadow = true;
      body.receiveShadow = true;
      g.add(body);

      // Support stumps under log
      const stumpMat = new THREE.MeshStandardMaterial({ color: 0x3d2817, roughness: 0.95, flatShading: true });
      const stump1 = new THREE.Mesh(new THREE.CylinderGeometry(radius * 0.8, radius * 0.9, radius * 1.5, 5), stumpMat);
      stump1.position.set(-length * 0.35, -radius * 0.5, 0);
      stump1.castShadow = true;
      g.add(stump1);

      const stump2 = stump1.clone();
      stump2.position.x = length * 0.35;
      g.add(stump2);

      const x = Math.cos(angle) * dist;
      const z = Math.sin(angle) * dist;
      const y = getGroundHeight(x, z) + radius * 0.95;
      g.position.set(x, y, z);
      g.rotation.y = angle + Math.PI / 2;
      return g;
    }

    const logBench1 = createSeatingLog(2.4, 0.22, 0.8, 3.2);
    propsGroup.add(logBench1);

    const logBench2 = createSeatingLog(2.1, 0.2, 3.8, 3.5);
    propsGroup.add(logBench2);

    // Camping Mug on Bench 1
    const mugMat = new THREE.MeshStandardMaterial({ color: 0x226d7a, roughness: 0.4, metalness: 0.2, flatShading: true });
    const mug = new THREE.Mesh(new THREE.CylinderGeometry(0.065, 0.055, 0.12, 8), mugMat);
    mug.position.set(0.4, 0.28, 0);
    logBench1.add(mug);

    // Cozy Low-Poly A-Frame Tent
    function createTent() {
      const tentGroup = new THREE.Group();
      const canvasMat = new THREE.MeshStandardMaterial({
        color: 0xcc6e29,
        roughness: 0.85,
        flatShading: true,
        side: THREE.DoubleSide
      });
      const frameMat = new THREE.MeshStandardMaterial({ color: 0x47321e, roughness: 0.9, flatShading: true });

      // Ridge pole
      const ridge = new THREE.Mesh(new THREE.CylinderGeometry(0.04, 0.04, 3.2, 5), frameMat);
      ridge.rotation.x = Math.PI / 2;
      ridge.position.y = 1.7;
      ridge.castShadow = true;
      tentGroup.add(ridge);

      // Support poles front & back (inverted V)
      function addPolePair(zPos) {
        const p1 = new THREE.Mesh(new THREE.CylinderGeometry(0.035, 0.035, 2.1, 5), frameMat);
        p1.position.set(-0.65, 0.85, zPos);
        p1.rotation.z = -0.42;
        p1.castShadow = true;
        tentGroup.add(p1);

        const p2 = p1.clone();
        p2.position.x = 0.65;
        p2.rotation.z = 0.42;
        tentGroup.add(p2);
      }
      addPolePair(1.4);
      addPolePair(-1.4);

      // Canvas roof slopes using BufferGeometry
      const roofGeo = new THREE.BufferGeometry();
      const w = 1.25;
      const h = 1.7;
      const l = 1.4;
      // 2 triangular prisms / 2 slanted rectangles
      const vertices = new Float32Array([
        // Left roof slope
        -w, 0,  l,   0, h,  l,   0, h, -l,
        -w, 0,  l,   0, h, -l,  -w, 0, -l,
        // Right roof slope
         w, 0,  l,   0, h, -l,   0, h,  l,
         w, 0,  l,   w, 0, -l,   0, h, -l,
        // Back triangle
        -w, 0, -l,   0, h, -l,   w, 0, -l
      ]);
      roofGeo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
      roofGeo.computeVertexNormals();
      const roof = new THREE.Mesh(roofGeo, canvasMat);
      roof.castShadow = true;
      roof.receiveShadow = true;
      tentGroup.add(roof);

      // Dark cozy interior ground cloth
      const groundCloth = new THREE.Mesh(
        new THREE.PlaneGeometry(2.3, 2.7),
        new THREE.MeshStandardMaterial({ color: 0x1b2028, roughness: 0.95, flatShading: true })
      );
      groundCloth.rotation.x = -Math.PI / 2;
      groundCloth.position.y = 0.02;
      groundCloth.receiveShadow = true;
      tentGroup.add(groundCloth);

      // Tiny warm lantern light inside tent
      const tentLight = new THREE.PointLight(0xffa233, 0.7, 4, 2.0);
      tentLight.position.set(0, 0.7, 0);
      tentGroup.add(tentLight);

      return tentGroup;
    }

    const tent = createTent();
    const tentX = -4.5;
    const tentZ = 3.6;
    tent.position.set(tentX, getGroundHeight(tentX, tentZ), tentZ);
    tent.rotation.y = 0.55;
    propsGroup.add(tent);

    // Scattered Forest Boulders
    const boulderMat = new THREE.MeshStandardMaterial({ color: 0x4f545c, roughness: 0.95, flatShading: true });
    for (let i = 0; i < 18; i++) {
      const angle = (i / 18) * Math.PI * 2 + Math.random() * 0.3;
      const dist = 5.2 + Math.random() * 12.0;
      const bx = Math.cos(angle) * dist;
      const bz = Math.sin(angle) * dist;
      const scale = 0.35 + Math.random() * 0.55;

      const bGeo = new THREE.DodecahedronGeometry(scale, 0);
      const bp = bGeo.attributes.position;
      for (let j = 0; j < bp.count; j++) {
        bp.setXYZ(
          j,
          bp.getX(j) * (0.8 + Math.random() * 0.4),
          bp.getY(j) * (0.6 + Math.random() * 0.4),
          bp.getZ(j) * (0.8 + Math.random() * 0.4)
        );
      }
      bGeo.computeVertexNormals();

      const boulder = new THREE.Mesh(bGeo, boulderMat);
      boulder.position.set(bx, getGroundHeight(bx, bz) + scale * 0.35, bz);
      boulder.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
      boulder.castShadow = true;
      boulder.receiveShadow = true;
      propsGroup.add(boulder);
    }

    // Little Woodland Mushrooms
    const shroomCapMat = new THREE.MeshStandardMaterial({ color: 0xcc2929, roughness: 0.6, flatShading: true });
    const shroomStemMat = new THREE.MeshStandardMaterial({ color: 0xf0ece1, roughness: 0.8, flatShading: true });
    function createMushroom(s) {
      const g = new THREE.Group();
      const stem = new THREE.Mesh(new THREE.CylinderGeometry(0.02 * s, 0.03 * s, 0.14 * s, 5), shroomStemMat);
      stem.position.y = 0.07 * s;
      g.add(stem);
      const cap = new THREE.Mesh(new THREE.ConeGeometry(0.08 * s, 0.07 * s, 6), shroomCapMat);
      cap.position.y = 0.14 * s;
      g.add(cap);
      return g;
    }

    const shroomClusters = [
      { x: 2.1, z: 2.6 },
      { x: -3.0, z: -2.8 },
      { x: 3.8, z: -1.5 },
      { x: -2.2, z: 4.8 }
    ];
    shroomClusters.forEach(pos => {
      for (let m = 0; m < 3; m++) {
        const shroom = createMushroom(0.8 + Math.random() * 0.5);
        const sx = pos.x + (Math.random() - 0.5) * 0.6;
        const sz = pos.z + (Math.random() - 0.5) * 0.6;
        shroom.position.set(sx, getGroundHeight(sx, sz), sz);
        shroom.rotation.y = Math.random() * Math.PI * 2;
        shroom.rotation.z = (Math.random() - 0.5) * 0.2;
        propsGroup.add(shroom);
      }
    });

    // --- At Least 30 Low-Poly Trees (>50 trees placed around clearing) ---
    const treeGroup = new THREE.Group();
    scene.add(treeGroup);

    const swayingTrees = [];

    // Tree Materials
    const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3d2716, roughness: 0.95, flatShading: true });
    const birchTrunkMat = new THREE.MeshStandardMaterial({ color: 0xd6d1c7, roughness: 0.85, flatShading: true });

    const foliageColors = [
      0x162c1b, // deep forest
      0x1c3822, // pine green
      0x23442a, // emerald shadow
      0x132617, // dark spruce
      0x274d30  // mountain pine
    ];
    const foliageMaterials = foliageColors.map(c =>
      new THREE.MeshStandardMaterial({ color: c, roughness: 0.9, flatShading: true })
    );

    // Tree Archetype 1: Classic Tiered Pine (Conifer)
    function createPineTree(heightScale, matIndex) {
      const group = new THREE.Group();
      const fMat = foliageMaterials[matIndex % foliageMaterials.length];

      // Trunk
      const trunkH = 1.4 * heightScale;
      const trunkRadius = 0.2 * heightScale;
      const trunk = new THREE.Mesh(
        new THREE.CylinderGeometry(trunkRadius * 0.75, trunkRadius, trunkH, 5),
        trunkMat
      );
      trunk.position.y = trunkH * 0.5;
      trunk.castShadow = true;
      trunk.receiveShadow = true;
      group.add(trunk);

      // Foliage Cones
      const foliageGroup = new THREE.Group();
      const tiers = 3;
      for (let t = 0; t < tiers; t++) {
        const tierRadius = (1.8 - t * 0.45) * heightScale;
        const tierH = (1.3 - t * 0.15) * heightScale;
        const cone = new THREE.Mesh(new THREE.ConeGeometry(tierRadius, tierH, 6), fMat);
        cone.position.y = (trunkH * 0.7 + t * (tierH * 0.65));
        cone.rotation.y = t * 0.65;
        cone.castShadow = true;
        cone.receiveShadow = true;
        foliageGroup.add(cone);
      }
      group.add(foliageGroup);
      return { root: group, foliage: foliageGroup };
    }

    // Tree Archetype 2: Slender Alpine Fir (4 tiers, narrow)
    function createFirTree(heightScale, matIndex) {
      const group = new THREE.Group();
      const fMat = foliageMaterials[matIndex % foliageMaterials.length];

      const trunkH = 1.8 * heightScale;
      const trunkRadius = 0.18 * heightScale;
      const trunk = new THREE.Mesh(
        new THREE.CylinderGeometry(trunkRadius * 0.6, trunkRadius, trunkH, 5),
        trunkMat
      );
      trunk.position.y = trunkH * 0.5;
      trunk.castShadow = true;
      trunk.receiveShadow = true;
      group.add(trunk);

      const foliageGroup = new THREE.Group();
      const tiers = 4;
      for (let t = 0; t < tiers; t++) {
        const tierRadius = (1.35 - t * 0.28) * heightScale;
        const tierH = (1.4 - t * 0.12) * heightScale;
        const cone = new THREE.Mesh(new THREE.ConeGeometry(tierRadius, tierH, 5), fMat);
        cone.position.y = (trunkH * 0.6 + t * (tierH * 0.62));
        cone.rotation.y = t * 0.8;
        cone.castShadow = true;
        cone.receiveShadow = true;
        foliageGroup.add(cone);
      }
      group.add(foliageGroup);
      return { root: group, foliage: foliageGroup };
    }

    // Tree Archetype 3: Low-Poly Clumpy Deciduous / Mountain Birch
    function createBirchTree(heightScale, matIndex) {
      const group = new THREE.Group();
      const fMat = foliageMaterials[matIndex % foliageMaterials.length];

      const trunkH = 2.4 * heightScale;
      const trunk = new THREE.Mesh(
        new THREE.CylinderGeometry(0.12 * heightScale, 0.18 * heightScale, trunkH, 5),
        birchTrunkMat
      );
      trunk.position.y = trunkH * 0.5;
      trunk.castShadow = true;
      trunk.receiveShadow = true;
      group.add(trunk);

      const foliageGroup = new THREE.Group();
      const clumps = [
        { x: 0, y: trunkH * 0.95, z: 0, r: 1.05 * heightScale },
        { x: 0.35 * heightScale, y: trunkH * 0.78, z: 0.25 * heightScale, r: 0.75 * heightScale },
        { x: -0.35 * heightScale, y: trunkH * 0.82, z: -0.2 * heightScale, r: 0.78 * heightScale }
      ];
      clumps.forEach(c => {
        const clumpMesh = new THREE.Mesh(new THREE.IcosahedronGeometry(c.r, 1), fMat);
        clumpMesh.position.set(c.x, c.y, c.z);
        clumpMesh.castShadow = true;
        clumpMesh.receiveShadow = true;
        foliageGroup.add(clumpMesh);
      });
      group.add(foliageGroup);
      return { root: group, foliage: foliageGroup };
    }

    // Scatter Trees radially around clearing
    const totalTrees = 58;
    const treePositions = [];

    for (let i = 0; i < totalTrees; i++) {
      let valid = false;
      let tx = 0;
      let tz = 0;
      let attempts = 0;

      while (!valid && attempts < 80) {
        attempts++;
        // Distribute radially outside clearing (clearing radius ~ 6.8m, up to 36m)
        const angle = Math.random() * Math.PI * 2;
        const rad = 7.0 + Math.sqrt(Math.random()) * 27.0;
        tx = Math.cos(angle) * rad;
        tz = Math.sin(angle) * rad;

        // Keep distance from tent and existing trees
        const distTent = Math.hypot(tx - tentX, tz - tentZ);
        if (distTent < 3.2) continue;

        let tooClose = false;
        for (const p of treePositions) {
          if (Math.hypot(tx - p.x, tz - p.z) < 2.3) {
            tooClose = true;
            break;
          }
        }
        if (!tooClose) valid = true;
      }

      treePositions.push({ x: tx, z: tz });

      const scale = 0.75 + Math.random() * 0.65;
      const typeChoice = Math.random();
      let treeObj;

      if (typeChoice < 0.55) {
        treeObj = createPineTree(scale, i);
      } else if (typeChoice < 0.82) {
        treeObj = createFirTree(scale, i + 1);
      } else {
        treeObj = createBirchTree(scale, i + 2);
      }

      const ty = getGroundHeight(tx, tz);
      treeObj.root.position.set(tx, ty, tz);
      treeObj.root.rotation.y = Math.random() * Math.PI * 2;
      // Slight random organic tilt
      treeObj.root.rotation.x = (Math.random() - 0.5) * 0.08;
      treeObj.root.rotation.z = (Math.random() - 0.5) * 0.08;

      treeGroup.add(treeObj.root);

      swayingTrees.push({
        foliage: treeObj.foliage,
        phase: Math.random() * Math.PI * 2,
        speed: 1.0 + Math.random() * 0.8,
        amplitude: 0.02 + Math.random() * 0.025
      });
    }

    // --- Procedural Campfire Crackle Audio (Web Audio API) ---
    let audioCtx = null;
    let isSoundOn = false;
    let brownNoiseSource = null;
    let audioGain = null;
    let crackleTimer = null;

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

      // Continuous brown noise buffer (warm flame air rumble)
      const bufferSize = audioCtx.sampleRate * 2;
      const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
      const data = noiseBuffer.getChannelData(0);
      let lastOut = 0.0;
      for (let i = 0; i < bufferSize; i++) {
        const white = Math.random() * 2 - 1;
        data[i] = (lastOut + 0.02 * white) / 1.02;
        lastOut = data[i];
        data[i] *= 3.0; // gain adjustment
      }

      brownNoiseSource = audioCtx.createBufferSource();
      brownNoiseSource.buffer = noiseBuffer;
      brownNoiseSource.loop = true;

      // Bandpass filter for soft warm wood roar
      const filter = audioCtx.createBiquadFilter();
      filter.type = 'bandpass';
      filter.frequency.value = 380;
      filter.Q.value = 0.85;

      audioGain = audioCtx.createGain();
      audioGain.gain.value = 0.14;

      brownNoiseSource.connect(filter);
      filter.connect(audioGain);
      audioGain.connect(audioCtx.destination);
      brownNoiseSource.start();

      // Randomized pop & crackle triggers
      function triggerPop() {
        if (!isSoundOn || !audioCtx) return;

        const popTime = audioCtx.currentTime;
        const osc = audioCtx.createOscillator();
        const popGain = audioCtx.createGain();
        const popFilter = audioCtx.createBiquadFilter();

        popFilter.type = 'bandpass';
        popFilter.frequency.value = 1400 + Math.random() * 2200;
        popFilter.Q.value = 3.5;

        // Wood snap frequency drop
        osc.frequency.setValueAtTime(450 + Math.random() * 600, popTime);
        osc.frequency.exponentialRampToValueAtTime(80, popTime + 0.025);

        const popVol = 0.08 + Math.random() * 0.22;
        popGain.gain.setValueAtTime(popVol, popTime);
        popGain.gain.exponentialRampToValueAtTime(0.0001, popTime + 0.028);

        osc.connect(popFilter);
        popFilter.connect(popGain);
        popGain.connect(audioCtx.destination);

        osc.start(popTime);
        osc.stop(popTime + 0.03);

        const nextDelay = 70 + Math.random() * 260;
        crackleTimer = setTimeout(triggerPop, nextDelay);
      }
      triggerPop();
    }

    const audioBtn = document.getElementById('audio-btn');
    const audioIcon = document.getElementById('audio-icon');
    const audioLabel = document.getElementById('audio-label');

    audioBtn.addEventListener('click', () => {
      if (!isSoundOn) {
        initAudio();
        if (audioCtx.state === 'suspended') {
          audioCtx.resume();
        }
        if (audioGain) audioGain.gain.value = 0.14;
        isSoundOn = true;
        audioBtn.classList.add('active');
        audioIcon.textContent = '🔊';
        audioLabel.textContent = 'Crackle: ON';
      } else {
        if (audioGain) audioGain.gain.value = 0;
        isSoundOn = false;
        audioBtn.classList.remove('active');
        audioIcon.textContent = '🔈';
        audioLabel.textContent = 'Crackle: OFF';
      }
    });

    // Orbit auto-rotate toggle button
    const camBtn = document.getElementById('cam-btn');
    const camLabel = document.getElementById('cam-label');
    camBtn.addEventListener('click', () => {
      controls.autoRotate = !controls.autoRotate;
      if (controls.autoRotate) {
        camBtn.classList.remove('active');
        camLabel.textContent = 'Orbit: On';
      } else {
        camBtn.classList.add('active');
        camLabel.textContent = 'Orbit: Paused';
      }
    });

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

    function animate() {
      requestAnimationFrame(animate);

      const delta = Math.min(clock.getDelta(), 0.1);
      const time = clock.getElapsedTime();

      // Controls update
      controls.update();

      // Campfire PointLight flicker (organic multi-sine turbulence)
      const flicker1 = Math.sin(time * 14.0) * 0.35;
      const flicker2 = Math.cos(time * 26.5) * 0.22;
      const flicker3 = Math.sin(time * 41.0) * 0.15;
      const jitter = (Math.random() - 0.5) * 0.08;

      fireLight.intensity = 4.1 + flicker1 + flicker2 + flicker3 + jitter;
      fireLight.position.x = Math.sin(time * 6.5) * 0.08;
      fireLight.position.z = Math.cos(time * 8.2) * 0.08;

      emberLight.intensity = 1.5 + Math.sin(time * 12.0) * 0.3;

      // Flame core scale pulsing
      const coreScaleY = 1.0 + Math.sin(time * 16.0) * 0.18 + Math.cos(time * 28.0) * 0.08;
      const coreScaleXZ = 1.0 + Math.cos(time * 14.0) * 0.12;
      coreMesh.scale.set(coreScaleXZ, coreScaleY, coreScaleXZ);

      const midScaleY = 1.0 + Math.sin(time * 12.0 + 1.0) * 0.22;
      const midScaleXZ = 1.0 + Math.cos(time * 10.0 + 0.5) * 0.15;
      midFlameMesh.scale.set(midScaleXZ, midScaleY, midScaleXZ);
      midFlameMesh.rotation.y = time * 1.5;

      // Dancing flame tongue shards
      tongueMeshes.forEach(t => {
        const ud = t.userData;
        const sY = 0.8 + 0.45 * Math.sin(time * ud.speed + ud.phase);
        const sXZ = 0.75 + 0.3 * Math.cos(time * (ud.speed * 0.7) + ud.phase);
        t.scale.set(sXZ, sY, sXZ);
        t.position.y = ud.baseY + 0.12 * Math.sin(time * (ud.speed * 1.2));
        t.rotation.y = time * 2.5 + ud.phase;
        t.rotation.z = Math.sin(time * 6.0 + ud.phase) * 0.14;
      });

      // Sparks particle update
      const posArr = sparkGeo.attributes.position.array;
      const colArr = sparkGeo.attributes.color.array;
      const sizeArr = sparkGeo.attributes.size.array;

      for (let i = 0; i < sparkCount; i++) {
        const d = sparkData[i];
        d.life += delta;

        if (d.life >= d.maxLife) {
          resetSpark(d, i);
        } else {
          const progress = d.life / d.maxLife;

          // Upward physics + spiral breeze turbulence
          d.x += (d.vx + Math.sin(time * 3.5 + d.seed) * 0.2) * delta;
          d.y += d.vy * delta;
          d.z += (d.vz + Math.cos(time * 3.5 + d.seed) * 0.2) * delta;

          // Slightly accelerate upwards from heat convection
          d.vy += 0.25 * delta;

          posArr[i * 3] = d.x;
          posArr[i * 3 + 1] = d.y;
          posArr[i * 3 + 2] = d.z;

          // Color transition: Yellow-White -> Fire Orange -> Dark Crimson
          if (progress < 0.4) {
            colArr[i * 3] = 1.0;
            colArr[i * 3 + 1] = 0.9 - progress * 0.8;
            colArr[i * 3 + 2] = 0.4 - progress * 0.9;
          } else {
            colArr[i * 3] = 1.0 - (progress - 0.4) * 0.6;
            colArr[i * 3 + 1] = Math.max(0, 0.58 - (progress - 0.4) * 1.1);
            colArr[i * 3 + 2] = 0.05;
          }

          // Shrink towards end
          sizeArr[i] = d.size * (1.0 - Math.pow(progress, 1.5));
        }
      }
      sparkGeo.attributes.position.needsUpdate = true;
      sparkGeo.attributes.color.needsUpdate = true;
      sparkGeo.attributes.size.needsUpdate = true;

      // Smoke particles update
      const sPosArr = smokeGeo.attributes.position.array;
      for (let i = 0; i < smokeCount; i++) {
        const sd = smokeData[i];
        sd.life += delta;
        if (sd.life >= sd.maxLife) {
          resetSmoke(sd, i);
        } else {
          sd.x += sd.vx * delta;
          sd.y += sd.vy * delta;
          sd.z += sd.vz * delta;

          sPosArr[i * 3] = sd.x;
          sPosArr[i * 3 + 1] = sd.y;
          sPosArr[i * 3 + 2] = sd.z;
        }
      }
      smokeGeo.attributes.position.needsUpdate = true;

      // Trees gentle nocturnal breeze sway
      for (let i = 0; i < swayingTrees.length; i++) {
        const item = swayingTrees[i];
        const sway = Math.sin(time * item.speed + item.phase) * item.amplitude;
        item.foliage.rotation.z = sway;
        item.foliage.rotation.x = Math.cos(time * (item.speed * 0.8) + item.phase) * (item.amplitude * 0.8);
      }

      renderer.render(scene, camera);
    }

    // --- Responsive Window Resize Handling ---
    window.addEventListener('resize', onWindowResize);
    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    }

    // Start Scene
    animate();
  </script>
</body>
</html>
Reasoning trace (16,127 tokens)
**Visualizing Fire's Glow**

I'm working on rendering a low-poly campfire scene at night within a forest clearing. The focus is on utilizing Three.js and integrating everything within a single HTML file. The provided import map is now in place.

**Crafting the Scene's Atmosphere**

I've finalized the import map and am now meticulously detailing the color palette and lighting. My aim is a deep night blue ambient light contrasted with the warm, flickering glow of the campfire, all within the specified low-poly aesthetic.

**Refining Light and Shadow**

I'm now deeply engrossed in the nuanced interplay of light. Specifically, I'm dialing in the ambient, moon, and campfire point lights. The campfire's flicker is becoming more dynamic with Perlin noise and color shifts, and I'm carefully adjusting shadow biases for that soft, ethereal forest night feel.

**Shaping the Forest Floor**

I'm now focusing on the ground plane, developing subtle rolling hills with a slight hollow for the clearing. I'm also starting to place low-poly rocks and logs, carefully considering their texture and color to blend seamlessly with the earthy tones.

**Modeling the Flames**

I'm now intently focused on the campfire itself, experimenting with dynamic flame geometry using simple shapes that flicker and deform upwards. I'm also layering colors to achieve a more believable glow, and considering the addition of small, glowing embers at the fire's base.

**Populating the Forest Edge**

I'm now populating the forest's edge with varied low-poly trees, focusing on spruce, oak, and stylized pines. Foliage colors are deep greens, and placement is randomized from a minimum of 7 to a maximum of 35 units from the center, ensuring the clearing remains open. I'm also beginning to implement subtle swaying for the trees.

**Adding Campfire Ambiance**

I'm now integrating more immersive campsite elements. A strategically placed log creates a sense of a lived-in space. I'm also developing subtle smoke particles that drift realistically and exploring synthesized crackling fire audio using Web Audio API for a completely self-contained, delightful interaction.

**Optimizing Flame Dynamics**

I'm now enhancing the campfire's visual fidelity by refining the animated flame shards and ember generation. I'm also implementing `requestAnimationFrame`, soft shadows, and ACES Filmic tone mapping for optimal performance and a more realistic look.

**Generating Fire and Smoke Textures**

I'm now creating specific canvas textures for the fire and smoke. This involves defining radial gradients with carefully chosen color stops to achieve the desired glow and transparency for both elements.

**Shaping the Terrain's Form**

I'm now focusing on the ground plane, developing subtle rolling hills with a slight hollow for the clearing. I'm also starting to place low-poly rocks and logs, carefully considering their texture and color to blend seamlessly with the earthy tones. The terrain now features vertex colors for a rich, handcrafted look.

**Placing Fire Pit Elements**

I'm now meticulously placing the campfire's components. This involves arranging irregular, jittered dodecahedrons for rocks in a circle, and positioning low-poly cylinders as logs within the fire pit, accented by a dark, ashy base with tiny ember specks.

**Animating Flame's Dance**

I'm now focusing on the dynamic movement of the flame geometry. Each flame segment's scale, position, rotation, and skew are being animated rapidly using multiple sine waves to simulate energetic flickering. The campfire's point lights are also being intensified and jittered procedurally in the render loop.

**Positioning Tree Elements**

I'm now strategically placing varied low-poly trees around the clearing. I'm using polar coordinates for placement, ensuring they avoid the immediate campfire area and are planted firmly into the terrain. I'm also applying random scale, rotation, and slight tilts to achieve a natural forest look and enabling shadows.

**Designing Camp Elements**

I'm now carefully placing campsite props, including logs with supports, a charming camping mug with steam, and a cozy A-frame tent with warm canvas. I'm also scattering mossy boulders and clusters of small, whimsical mushrooms around the scene to enrich the environment.

**Adding Celestial Elements**

I'm now introducing a subtle moon and stars to the scene. The moon provides a cool ambient light, casting soft shadows that contrast nicely with the fire's warmth, while the stars add depth and a touch of magic with a gentle twinkle.

**Controlling the Camera's View**

I'm now implementing user controls, specifically `OrbitControls` with damping and polar angle limits. I'm also configuring auto-rotation for a gentle, immersive start and planning for its graceful pause on user interaction.

**Incorporating User Interface**

I'm now adding the finishing touches to the user interface. This includes implementing subtle letter spacing, a translucent dark glass badge with interaction hints, and an audio toggle button for the procedural fire crackle.

**Refining Flame Animations**

I'm now focused on the intricate animation of the fire geometry. By employing multiple sine waves to manipulate the scale, position, rotation, and skew of each flame segment, I'm achieving a highly dynamic and energetic flickering effect. The campfire's point lights are also being intensified and jittered in real-time to further enhance this realism.

**Animating Fire's Core**

I'm intensely focused on the heart of the fire, animating individual flame geometry. Using a combination of sine waves for scale and position, I'm creating a dynamic, flickering effect for each flame segment, driven by the real-time render loop.

**Animating Fire's Core**

I'm intensely focused on the heart of the fire, animating individual flame geometry. Using a combination of sine waves for scale and position, I'm creating a dynamic, flickering effect for each flame segment, driven by the real-time render loop. I'm also synchronizing emissive intensity and point light flickers for ultimate realism.

**Synthesizing Campfire Audio**

I'm now crafting the campfire's audio. I'm generating brown noise for the underlying rush and scheduling random, high-frequency pops through filters for a realistic crackle. This creates an immersive, self-contained soundscape, initially muted and activated by a clear UI toggle.

**Modifying Ground Geometry**

I'm now refining the ground plane's shape and vertex colors. The new geometry incorporates specific dips for the fire pit, gentle slopes for the clearing, and rolling hills further out, all with distinct vertex colors to represent ash, dirt, grass, and forest floor.

**Modeling Diverse Flora**

I'm refining the creation of unique tree geometries, implementing functions to generate stylized pine and birch variants with randomized organic cone shapes for pine tiers. I'm also continuing to scatter these, aiming for approximately 55 trees placed around the clearing using polar coordinates.

**Scattering More Flora**

I'm now implementing a more robust tree scattering system, using polar coordinates with adjusted radii for a natural distribution around the clearing, ensuring they avoid camp props. I'm also enabling shadow casting and receiving for all foliage and trunks, and refining their placement on the terrain.

**Crafting Cozy Camp Details**

I'm now meticulously adding charming camp props: a mug, an A-frame tent with wooden poles, a rolled sleeping mat, scattered boulders, whimsical mushrooms, and grass tufts. I'm also developing a particle system for campfire sparks using `BufferGeometry`, with plans for position, color, and size attributes.

**Simulating Spark Particles**

I'm now implementing the particle system logic for campfire sparks. This involves tracking position, velocity, and life cycle, with updates in the render loop. I'm also defining color gradients from yellow-white to deep red, and setting up `THREE.PointsMaterial` with additive blending.

**Animating Smoke Dynamics**

I'm now focusing on the subtle rising smoke puffs. I'm managing 35 particles with defined start and end Y positions, and a soft grey-blueish alpha. These will expand in size as they ascend from the campfire.

**Implementing Lighting and Atmosphere**

I'm now fine-tuning the scene's lighting, balancing the cool ambient blue with the campfire's warm glow. The moon and directional lights are set, creating subtle shadows, while fog is applied to deepen the atmosphere. I've also added a basic moon and twinkling stars.

**Refining Camera and UI**

I'm finalizing camera positioning and `OrbitControls` constraints for optimal viewing angles, ensuring a consistent, immersive experience. The user interface is also taking shape, with clear hints for interaction and a functional sound toggle, all within a single HTML file.

**Verifying Audio and Shading**

I'm confirming the Web Audio implementation for the campfire crackle, ensuring it's fully offline and authentic, triggered by user interaction. I'm also verifying that flat shading is applied correctly to all geometries, especially tree foliage, and that shadows are enabled with soft settings for a realistic look.

**Animating Flames with Sine Waves**

I'm currently refining the flame animations, employing multiple sine waves to dynamically adjust each flame segment's scale, rotation, and position. This creates an energetic, flickering effect synchronized with real-time point light intensity changes. I'm also experimenting with custom cone geometries for the flame's core and outer shards, layering colors and emissive properties for enhanced realism.

**Building the Campfire Core**

I'm now integrating the visual and physical elements of the campfire. Stylized, dynamic flames are animating beautifully, complemented by low-poly logs arranged in a teepee formation with charred ends. I'm also placing stones around the pit, ensuring a natural, somewhat randomized arrangement.

**Positioning Fire Pit Stones**

I'm refining the placement of the campfire stones, using jittered dodecahedrons to create an irregular, natural ring. Each stone is slightly deformed for an organic, faceted look and assigned a randomized, earthy HSL color. They're positioned at specific angles around the fire pit, with random rotations and tilt for added realism.

**Defining Flora and Terrain**

I'm solidifying the tree generation with distinct pine, fir, and oak/birch types, planning for 60 scattered around the clearing. Crucially, I've established an analytical ground height function that ensures all placed assets, like trees and rocks, will sit perfectly flush with the terrain.

**Shaping the Terrain's Curves**

I'm now implementing a precise analytical function to define the ground's undulating topography. This ensures all elements, from the fire pit's dip to the surrounding rolling hills, will integrate seamlessly with the terrain's geometry.

**Adjusting Terrain and Colors**

I'm now fine-tuning the ground geometry and its coloring. The terrain's Y-axis now correctly aligns with height after rotation, ensuring world coordinates are accurate. I'm also implementing vertex colors based on height and distance from the center, blending between ash, dirt, and various grass shades to give the ground a rich, natural appearance.

**Crafting Cozy Camp Details**

**Designing the Tent**

I'm meticulously designing the A-frame tent, detailing its dimensions and vertex structure for a stylized low-poly look. This includes planning for a slight separation at the front for a peek at the interior, along with a simple ridge pole.

**Animating Spark Particles**

I'm currently focused on animating the campfire sparks. They'll drift upwards with a helical motion and turbulence, their size and color fading realistically over their lifespan, transitioning from bright gold to ember red, with a glow texture and additive blending for a vibrant effect.

**Animating Light and Sound**

I'm intensely focused on the real-time rendering loop. The campfire's light intensity and position are dynamically animated using multiple sine waves and subtle noise, creating a believable, dancing glow that casts shifting shadows. Concurrently, I'm refining the Web Audio campfire crackle, ensuring it's responsive to user interaction and authentically complements the visual.

**Animating Light and Sound**

**Refining Camera and UI**

**Refining Camera and UI**

I'm adjusting the camera's polar angle limits and focus target for an optimal, immersive view of the campfire, ensuring it remains grounded and avoids clipping. Performance is looking excellent; shared geometries and low-poly counts will maintain a smooth frame rate, and shadow maps are well within typical GPU capabilities.

**Generating Diverse Trees**

I'm now implementing distinct generation functions for pine, fir, and oak-like trees, each with unique geometry and color variations. I'm also developing a system to animate their subtle swaying in response to wind, adding a touch of organic life to the scene.

**Animating Forest Sway**

I've successfully integrated subtle swaying animations for the trees and branches, bringing a gentle, organic breath to the forest. The night sky and moon are also in place, with a dark, foggy atmosphere and a directional light simulating moonlight, casting soft shadows.

**Refining Celestial and Tent Details**

I'm adjusting the moon's shadow camera settings and have generated 700 stars with subtle opacity variations on a hemisphere above the scene. The A-frame tent's geometry is now finalized, featuring a cute, low-poly design with tied-back flaps revealing a cozy interior complete with a mattress and a warm internal lantern glow.

**Animating Flame Dynamics**

I'm now meticulously animating the fire's core, blending bright yellow, orange, and red hues. The flames are dynamically sculpted upwards with vertex and scale modulation, creating a realistic dance. I'm also generating 140 sparks with turbulent upward motion and a soft glow, alongside 25 smoke puffs that expand and fade.

**Simulating Spark Particles**

I'm animating the campfire sparks with a helical upward motion and turbulence. Their size and color will fade realistically, transitioning from gold to red, with a glow texture and additive blending for vibrancy.

**Finalizing Scene Elements**

I've refined the moon's shadow settings and populated the sky with 700 twinkling stars. The A-frame tent is complete with a cozy interior, and I'm now animating the fire's core with dynamic color and vertex modulation, alongside turbulent sparks and expanding smoke puffs.

**Refining Camera and UI**

**Generating Diverse Trees**

**Animating Forest Sway**

**Refining Celestial and Tent Details**

**Animating Flame Dynamics**

**Finalizing Code Structure**

I'm consolidating all code into a single HTML file, ensuring the import map is correctly configured and that Three.js is loaded solely through it. I'm also performing final checks for console errors and deprecated Three.js calls, ensuring adherence to the specified Three.js version.

**Validating Scene Integration**

I'm reviewing the scene's integration, confirming that the moon, fog, and stars create the desired atmosphere, and that `OrbitControls` are properly limited. Renderer resizing and window filling are confirmed, and all code is properly fenced and inline.

**Verifying Three.js Compatibility**

I've thoroughly checked for deprecated Three.js calls in version 0.170.0, including `Color.setHSL`, `FogExp2`, `renderer.shadowMap.enabled`, `renderer.shadowMap.type`, `renderer.toneMapping`, `renderer.toneMappingExposure`, and `OrbitControls` import path. All are confirmed valid. I've also ensured the use of `BufferGeometry` and its attributes.

**Finalizing Rendering Settings**

I'm confirming the renderer's configuration, ensuring it fills the window and handles resizing correctly. The ACES Filmic tone mapping and exposure levels are set, and soft shadows are enabled, all contributing to the final visual output.

**Ensuring Code Structure**

I'm verifying that all code is contained within a single fenced HTML block, with no preceding or trailing text. The Three.js library is loaded exclusively through the provided import map, and I am diligently checking for any console errors in the latest Chrome browser.