Models 44

Gemini 4 Argon

by Googlereasoning defaultClean
Open in new window

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

How it went

Total time
1 min 53 s
Cost
$0.088
Output tokens
8,636
Spent thinking
0 tokens
Speed
80.3 tok/s
Console errors
0
First token after
5.5 s
First visible text after
5.5 s
Prompt tokens
630
Code size
22.8 KB
Finish reason
stop
Served by
Google
Model id
google/gemini-4-argon
Reasoning sent
model default
Max output
943,718 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 · 660 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 Night Campfire</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    html, body {
      width: 100%;
      height: 100%;
      overflow: hidden;
      background-color: #070b19;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      user-select: none;
    }
    canvas {
      display: block;
      width: 100%;
      height: 100%;
    }
    #ui {
      position: absolute;
      bottom: 20px;
      left: 50%;
      transform: translateX(-50%);
      color: rgba(255, 235, 205, 0.75);
      background: rgba(10, 15, 30, 0.55);
      backdrop-filter: blur(6px);
      padding: 8px 18px;
      border-radius: 999px;
      font-size: 13px;
      letter-spacing: 0.4px;
      border: 1px solid rgba(255, 180, 100, 0.2);
      pointer-events: none;
      white-space: nowrap;
      box-shadow: 0 4px 20px rgba(0,0,0,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">Drag to orbit &bull; Scroll to zoom</div>
  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    // --- Scene & Renderer Setup ---
    const scene = new THREE.Scene();
    const skyColor = 0x070b17;
    scene.background = new THREE.Color(skyColor);
    scene.fog = new THREE.FogExp2(skyColor, 0.022);

    const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 200);
    camera.position.set(0, 5.5, 13.5);

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

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 1.3, 0);
    controls.enableDamping = true;
    controls.dampingFactor = 0.05;
    controls.maxPolarAngle = Math.PI / 2 - 0.04; // Keep above ground
    controls.minPolarAngle = 0.15;
    controls.minDistance = 4;
    controls.maxDistance = 32;
    controls.autoRotate = true;
    controls.autoRotateSpeed = 0.45;

    // Stop auto-rotate when user interacts
    controls.addEventListener('start', () => {
      controls.autoRotate = false;
    });

    // --- Lighting ---
    const hemiLight = new THREE.HemisphereLight(0x2a3c66, 0x0a101d, 0.55);
    scene.add(hemiLight);

    // Moon directional light
    const moonDirLight = new THREE.DirectionalLight(0x8aa8ff, 0.45);
    moonDirLight.position.set(-22, 30, -28);
    moonDirLight.castShadow = true;
    moonDirLight.shadow.mapSize.width = 1024;
    moonDirLight.shadow.mapSize.height = 1024;
    moonDirLight.shadow.camera.near = 5;
    moonDirLight.shadow.camera.far = 90;
    const d = 32;
    moonDirLight.shadow.camera.left = -d;
    moonDirLight.shadow.camera.right = d;
    moonDirLight.shadow.camera.top = d;
    moonDirLight.shadow.camera.bottom = -d;
    moonDirLight.shadow.bias = -0.0005;
    scene.add(moonDirLight);

    // Warm campfire point light
    const fireLight = new THREE.PointLight(0xff7711, 28, 30, 1.4);
    fireLight.position.set(0, 1.4, 0);
    fireLight.castShadow = true;
    fireLight.shadow.mapSize.width = 1024;
    fireLight.shadow.mapSize.height = 1024;
    fireLight.shadow.camera.near = 0.3;
    fireLight.shadow.camera.far = 28;
    fireLight.shadow.bias = -0.002;
    scene.add(fireLight);

    // Subtle secondary fill glow near ground
    const fireGroundGlow = new THREE.PointLight(0xff3300, 8, 10, 1.6);
    fireGroundGlow.position.set(0, 0.35, 0);
    scene.add(fireGroundGlow);

    // --- Ground ---
    const groundRadius = 65;
    const groundGeo = new THREE.PlaneGeometry(groundRadius * 2, groundRadius * 2, 70, 70);
    groundGeo.rotateX(-Math.PI / 2);

    const posAttr = groundGeo.attributes.position;
    const colors = [];
    const cCenter = new THREE.Color(0x241d17); // Warm dirt near fire
    const cGrass = new THREE.Color(0x16241b);  // Dark forest grass
    const cOuter = new THREE.Color(0x0d1714);  // Deep forest floor
    const tmpColor = new THREE.Color();

    for (let i = 0; i < posAttr.count; i++) {
      const vx = posAttr.getX(i);
      const vz = posAttr.getZ(i);
      const dist = Math.hypot(vx, vz);

      // Keep clearing relatively flat, add gentle low-poly hills outside
      let height = 0;
      if (dist > 4.5) {
        const factor = Math.min(1, (dist - 4.5) / 15);
        const wave = Math.sin(vx * 0.18) * Math.cos(vz * 0.18) * 1.1
                   + Math.sin(vx * 0.45 + vz * 0.35) * 0.35;
        height = Math.max(-0.2, wave * factor);
      } else {
        // Slight depression for fire pit right at center
        if (dist < 1.3) {
          height = -0.08 * (1 - dist / 1.3);
        }
      }
      posAttr.setY(i, height);

      // Vertex coloring
      if (dist < 3.2) {
        tmpColor.copy(cCenter).lerp(cGrass, dist / 3.2);
      } else {
        tmpColor.copy(cGrass).lerp(cOuter, Math.min(1, (dist - 3.2) / 30));
      }
      // Subtle random tint per vertex
      const jitter = (Math.sin(i * 12.9898) * 43758.5453 % 1) * 0.03;
      tmpColor.offsetHSL(0, 0, jitter);
      colors.push(tmpColor.r, tmpColor.g, tmpColor.b);
    }

    groundGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
    groundGeo.computeVertexNormals();

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

    // Helper to sample approximate ground height
    function getTerrainHeight(x, z) {
      const dist = Math.hypot(x, z);
      if (dist <= 4.5) return 0;
      const factor = Math.min(1, (dist - 4.5) / 15);
      const wave = Math.sin(x * 0.18) * Math.cos(z * 0.18) * 1.1
                 + Math.sin(x * 0.45 + z * 0.35) * 0.35;
      return Math.max(-0.2, wave * factor);
    }

    // --- Campfire Centerpiece ---
    const campfireGroup = new THREE.Group();
    scene.add(campfireGroup);

    // Dark ash bed
    const ashGeo = new THREE.CylinderGeometry(1.15, 1.3, 0.08, 9);
    const ashMat = new THREE.MeshStandardMaterial({ color: 0x181515, roughness: 1, flatShading: true });
    const ashMesh = new THREE.Mesh(ashGeo, ashMat);
    ashMesh.position.y = 0.02;
    ashMesh.receiveShadow = true;
    campfireGroup.add(ashMesh);

    // Glowing embers bed
    const emberGeo = new THREE.DodecahedronGeometry(0.65, 1);
    emberGeo.scale(1, 0.25, 1);
    const emberMat = new THREE.MeshStandardMaterial({
      color: 0xff3300,
      emissive: 0xff2200,
      emissiveIntensity: 1.8,
      roughness: 0.8,
      flatShading: true
    });
    const emberBed = new THREE.Mesh(emberGeo, emberMat);
    emberBed.position.y = 0.08;
    campfireGroup.add(emberBed);

    // Ring of stones around fire pit
    const stoneMat = new THREE.MeshStandardMaterial({
      color: 0x565a63,
      roughness: 0.88,
      flatShading: true
    });
    const numRingStones = 13;
    for (let i = 0; i < numRingStones; i++) {
      const angle = (i / numRingStones) * Math.PI * 2 + (Math.random() * 0.15);
      const radius = 1.18 + (Math.random() - 0.5) * 0.14;
      const sGeo = new THREE.DodecahedronGeometry(0.24 + Math.random() * 0.08, 0);
      const stone = new THREE.Mesh(sGeo, stoneMat);
      stone.position.set(Math.cos(angle) * radius, 0.14, Math.sin(angle) * radius);
      stone.scale.set(1 + Math.random() * 0.35, 0.65 + Math.random() * 0.3, 1 + Math.random() * 0.35);
      stone.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
      stone.castShadow = true;
      stone.receiveShadow = true;
      campfireGroup.add(stone);
    }

    // Fuel logs inside the fire pit
    const fuelLogMat = new THREE.MeshStandardMaterial({
      color: 0x3b2313,
      roughness: 0.9,
      flatShading: true
    });
    for (let i = 0; i < 6; i++) {
      const angle = (i / 6) * Math.PI * 2 + 0.3;
      const fLogGeo = new THREE.CylinderGeometry(0.09, 0.11, 1.35, 6);
      const fLog = new THREE.Mesh(fLogGeo, fuelLogMat);
      fLog.position.set(Math.cos(angle) * 0.28, 0.26, Math.sin(angle) * 0.28);
      fLog.rotation.z = Math.cos(angle) * 0.55;
      fLog.rotation.x = -Math.sin(angle) * 0.55;
      fLog.rotation.y = Math.random() * Math.PI;
      fLog.castShadow = true;
      fLog.receiveShadow = true;
      campfireGroup.add(fLog);
    }

    // --- Animated Low-Poly Flames ---
    const flames = [];
    const flameColors = [0xff2a00, 0xff5500, 0xff8800, 0xffbb11, 0xffee55];

    for (let i = 0; i < 11; i++) {
      const isCore = i >= 7;
      const radius = isCore ? 0.22 + Math.random() * 0.1 : 0.28 + Math.random() * 0.18;
      const height = isCore ? 0.75 + Math.random() * 0.4 : 1.1 + Math.random() * 0.65;
      const coneGeo = new THREE.ConeGeometry(radius, height, 5);
      // Shift pivot to bottom of cone
      coneGeo.translate(0, height / 2, 0);

      const colorHex = flameColors[Math.min(flameColors.length - 1, Math.floor((i / 11) * flameColors.length))];
      const flameMat = new THREE.MeshBasicMaterial({
        color: colorHex,
        transparent: true,
        opacity: isCore ? 0.92 : 0.82,
        blending: THREE.AdditiveBlending,
        depthWrite: false
      });

      const flameMesh = new THREE.Mesh(coneGeo, flameMat);
      const dist = isCore ? Math.random() * 0.16 : 0.12 + Math.random() * 0.32;
      const angle = (i / 11) * Math.PI * 2 + Math.random() * 0.5;

      flameMesh.position.set(
        Math.cos(angle) * dist,
        0.15 + Math.random() * 0.08,
        Math.sin(angle) * dist
      );
      flameMesh.rotation.y = Math.random() * Math.PI * 2;

      campfireGroup.add(flameMesh);
      flames.push({
        mesh: flameMesh,
        baseX: flameMesh.position.x,
        baseZ: flameMesh.position.z,
        speed: 4.5 + Math.random() * 4.0,
        phase: Math.random() * Math.PI * 2,
        swayAmp: 0.12 + Math.random() * 0.12
      });
    }

    // --- Sparks Particle System ---
    const sparkCount = 85;
    const sparkGeo = new THREE.BufferGeometry();
    const sparkPositions = new Float32Array(sparkCount * 3);
    const sparkColors = new Float32Array(sparkCount * 3);
    const sparkData = [];

    const cSparkWarm = new THREE.Color(0xffaa22);
    const cSparkHot = new THREE.Color(0xffee77);

    function resetSpark(i, initialRandomY = false) {
      const r = Math.random() * 0.55;
      const theta = Math.random() * Math.PI * 2;
      sparkPositions[i * 3] = Math.cos(theta) * r;
      sparkPositions[i * 3 + 1] = initialRandomY ? 0.3 + Math.random() * 4.8 : 0.25 + Math.random() * 0.3;
      sparkPositions[i * 3 + 2] = Math.sin(theta) * r;

      const col = Math.random() > 0.4 ? cSparkWarm : cSparkHot;
      sparkColors[i * 3] = col.r;
      sparkColors[i * 3 + 1] = col.g;
      sparkColors[i * 3 + 2] = col.b;

      sparkData[i] = {
        vy: 0.9 + Math.random() * 1.4,
        vx: (Math.random() - 0.5) * 0.35,
        vz: (Math.random() - 0.5) * 0.35,
        turbulence: 1.5 + Math.random() * 2.5,
        phase: Math.random() * Math.PI * 2,
        maxY: 3.2 + Math.random() * 3.0
      };
    }

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

    sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
    sparkGeo.setAttribute('color', new THREE.BufferAttribute(sparkColors, 3));

    const sparkMat = new THREE.PointsMaterial({
      size: 0.14,
      vertexColors: true,
      transparent: true,
      opacity: 0.95,
      blending: THREE.AdditiveBlending,
      depthWrite: false
    });

    const sparkSystem = new THREE.Points(sparkGeo, sparkMat);
    scene.add(sparkSystem);

    // --- Seating Logs & Rocks around the Clearing ---
    const logBarkMat = new THREE.MeshStandardMaterial({
      color: 0x4a2e1b,
      roughness: 0.9,
      flatShading: true
    });
    const logEndMat = new THREE.MeshStandardMaterial({
      color: 0x9c7a54,
      roughness: 0.85,
      flatShading: true
    });

    function createSeatingLog(x, z, rotY, length = 2.4, radius = 0.28) {
      const logGroup = new THREE.Group();
      const bodyGeo = new THREE.CylinderGeometry(radius, radius * 1.04, length, 7);
      bodyGeo.rotateZ(Math.PI / 2);
      const body = new THREE.Mesh(bodyGeo, logBarkMat);
      body.castShadow = true;
      body.receiveShadow = true;
      logGroup.add(body);

      const capGeo = new THREE.CylinderGeometry(radius * 0.85, radius * 0.85, length + 0.02, 7);
      capGeo.rotateZ(Math.PI / 2);
      const cap = new THREE.Mesh(capGeo, logEndMat);
      logGroup.add(cap);

      // Small branch stub
      const stubGeo = new THREE.CylinderGeometry(radius * 0.25, radius * 0.32, 0.35, 5);
      const stub = new THREE.Mesh(stubGeo, logBarkMat);
      stub.position.set(length * 0.2, radius * 0.85, 0);
      stub.rotation.z = -0.4;
      stub.castShadow = true;
      logGroup.add(stub);

      const y = getTerrainHeight(x, z) + radius * 0.85;
      logGroup.position.set(x, y, z);
      logGroup.rotation.y = rotY;
      // Slight natural tilt
      logGroup.rotation.z = (Math.random() - 0.5) * 0.06;
      scene.add(logGroup);
    }

    createSeatingLog(2.9, 1.2, -Math.PI * 0.33, 2.5, 0.3);
    createSeatingLog(-2.6, 2.0, Math.PI * 0.28, 2.3, 0.27);
    createSeatingLog(-1.8, -2.7, -Math.PI * 0.14, 2.6, 0.31);
    createSeatingLog(2.5, -2.4, Math.PI * 0.72, 2.0, 0.25);

    // Low-poly Boulders / Rocks scattered around clearing
    const rockPalette = [0x464b56, 0x383d47, 0x525864];
    const rockMaterials = rockPalette.map(c => new THREE.MeshStandardMaterial({
      color: c,
      roughness: 0.88,
      flatShading: true
    }));

    function createRockCluster(x, z, baseScale) {
      const count = 1 + Math.floor(Math.random() * 3);
      for (let i = 0; i < count; i++) {
        const s = baseScale * (i === 0 ? 1 : 0.45 + Math.random() * 0.4);
        const geo = new THREE.DodecahedronGeometry(s, 0);
        const mat = rockMaterials[Math.floor(Math.random() * rockMaterials.length)];
        const rock = new THREE.Mesh(geo, mat);

        const ox = i === 0 ? 0 : (Math.random() - 0.5) * baseScale * 1.4;
        const oz = i === 0 ? 0 : (Math.random() - 0.5) * baseScale * 1.4;
        const rx = x + ox;
        const rz = z + oz;
        const ry = getTerrainHeight(rx, rz);

        rock.position.set(rx, ry + s * 0.35, rz);
        rock.scale.set(1 + Math.random() * 0.4, 0.65 + Math.random() * 0.4, 1 + Math.random() * 0.4);
        rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
        rock.castShadow = true;
        rock.receiveShadow = true;
        scene.add(rock);
      }
    }

    const rockLocations = [
      [4.2, -0.8, 0.65],
      [-4.3, -0.5, 0.75],
      [0.8, -4.4, 0.55],
      [-0.9, 4.3, 0.5],
      [5.5, 3.8, 0.85],
      [-6.0, 4.5, 0.9],
      [-5.2, -5.0, 0.7],
      [6.4, -4.8, 0.8],
      [3.3, 5.8, 0.6]
    ];
    for (const [rx, rz, rScale] of rockLocations) {
      createRockCluster(rx, rz, rScale);
    }

    // --- Forest of Low-Poly Pine Trees (60+ trees) ---
    const trunkMat = new THREE.MeshStandardMaterial({
      color: 0x362214,
      roughness: 0.92,
      flatShading: true
    });

    const foliageColors = [0x142e24, 0x1b3b2b, 0x10261e, 0x204230];
    const foliageMats = foliageColors.map(col => new THREE.MeshStandardMaterial({
      color: col,
      roughness: 0.85,
      flatShading: true
    }));

    function createPineTree(x, z, scale) {
      const tree = new THREE.Group();

      // Trunk
      const trunkHeight = 1.6 * scale;
      const trunkGeo = new THREE.CylinderGeometry(0.16 * scale, 0.28 * scale, trunkHeight, 6);
      const trunk = new THREE.Mesh(trunkGeo, trunkMat);
      trunk.position.y = trunkHeight / 2;
      trunk.castShadow = true;
      trunk.receiveShadow = true;
      tree.add(trunk);

      // 3 to 4 stacked cone tiers
      const tiers = 3 + (scale > 1.05 ? 1 : 0);
      const leafMat = foliageMats[Math.floor(Math.random() * foliageMats.length)];

      for (let t = 0; t < tiers; t++) {
        const progress = t / tiers;
        const coneRadius = (1.45 - progress * 0.78) * scale;
        const coneHeight = (1.9 - progress * 0.55) * scale;
        const coneGeo = new THREE.ConeGeometry(coneRadius, coneHeight, 6 + (t % 2));
        const cone = new THREE.Mesh(coneGeo, leafMat);

        cone.position.y = trunkHeight * 0.85 + t * (1.05 * scale) + coneHeight * 0.35;
        cone.rotation.y = t * 0.6 + Math.random() * 0.5;
        cone.castShadow = true;
        cone.receiveShadow = true;
        tree.add(cone);
      }

      const groundY = getTerrainHeight(x, z);
      tree.position.set(x, groundY - 0.05, z);
      // Slight organic lean
      tree.rotation.x = (Math.random() - 0.5) * 0.06;
      tree.rotation.z = (Math.random() - 0.5) * 0.06;
      tree.rotation.y = Math.random() * Math.PI * 2;

      scene.add(tree);
    }

    // Place 65 trees around the clearing (guaranteeing none near the fire)
    const placedTrees = [];
    const totalTrees = 65;

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

      while (!valid && attempts < 50) {
        attempts++;
        // Distribute from edge of clearing (r=6.8) out into the fog (r=38)
        const angle = (i / totalTrees) * Math.PI * 2 + (Math.random() - 0.5) * 0.5;
        const radius = 6.8 + Math.pow(Math.random(), 0.85) * 31;
        tx = Math.cos(angle) * radius;
        tz = Math.sin(angle) * radius;

        // Keep viewing corridor slightly clearer right in front of initial camera
        if (tz > 7 && tz < 15 && Math.abs(tx) < 2.8) continue;

        valid = true;
        for (const pt of placedTrees) {
          if (Math.hypot(tx - pt.x, tz - pt.z) < 2.2) {
            valid = false;
            break;
          }
        }
      }

      placedTrees.push({ x: tx, z: tz });
      const distFromCenter = Math.hypot(tx, tz);
      const baseScale = 0.75 + Math.random() * 0.75 + (distFromCenter > 16 ? 0.25 : 0);
      createPineTree(tx, tz, baseScale);
    }

    // --- Night Sky: Stars & Low-Poly Moon ---
    const starCount = 700;
    const starGeo = new THREE.BufferGeometry();
    const starPositions = new Float32Array(starCount * 3);
    const starColors = new Float32Array(starCount * 3);

    for (let i = 0; i < starCount; i++) {
      // Hemisphere above the horizon
      const theta = Math.random() * Math.PI * 2;
      const phi = Math.acos(0.12 + Math.random() * 0.88); // Stay above horizon
      const r = 85 + Math.random() * 15;

      starPositions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
      starPositions[i * 3 + 1] = r * Math.cos(phi);
      starPositions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);

      const brightness = 0.55 + Math.random() * 0.45;
      const tint = Math.random();
      starColors[i * 3] = brightness * (tint > 0.7 ? 0.85 : 1.0);
      starColors[i * 3 + 1] = brightness * 0.95;
      starColors[i * 3 + 2] = brightness * (tint < 0.3 ? 0.85 : 1.0);
    }

    starGeo.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
    starGeo.setAttribute('color', new THREE.BufferAttribute(starColors, 3));

    const starMat = new THREE.PointsMaterial({
      size: 0.45,
      vertexColors: true,
      transparent: true,
      opacity: 0.9,
      fog: false
    });
    const stars = new THREE.Points(starGeo, starMat);
    scene.add(stars);

    // Low-poly glowing Moon + Halo
    const moonGroup = new THREE.Group();
    moonGroup.position.set(-26, 24, -48);
    scene.add(moonGroup);

    const moonGeo = new THREE.IcosahedronGeometry(3.2, 1);
    const moonMat = new THREE.MeshBasicMaterial({
      color: 0xf4f6ff,
      fog: false
    });
    const moonMesh = new THREE.Mesh(moonGeo, moonMat);
    moonGroup.add(moonMesh);

    const haloGeo = new THREE.IcosahedronGeometry(4.4, 1);
    const haloMat = new THREE.MeshBasicMaterial({
      color: 0x7799ff,
      transparent: true,
      opacity: 0.16,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      fog: false
    });
    const haloMesh = new THREE.Mesh(haloGeo, haloMat);
    moonGroup.add(haloMesh);

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

    function animate() {
      requestAnimationFrame(animate);

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

      // 1. Animate low-poly flames
      for (let i = 0; i < flames.length; i++) {
        const f = flames[i];
        const t = elapsed * f.speed + f.phase;
        const scaleY = 0.82 + Math.sin(t) * 0.24 + Math.cos(t * 2.1) * 0.1;
        const scaleXZ = 0.88 + Math.cos(t * 1.3) * 0.15;

        f.mesh.scale.set(scaleXZ, scaleY, scaleXZ);
        f.mesh.position.x = f.baseX + Math.sin(t * 0.8) * 0.04;
        f.mesh.position.z = f.baseZ + Math.cos(t * 0.9) * 0.04;
        f.mesh.rotation.z = Math.sin(t * 0.7) * f.swayAmp;
        f.mesh.rotation.x = Math.cos(t * 0.6) * f.swayAmp;
      }

      // 2. Flicker campfire light & ember bed
      const flicker = Math.sin(elapsed * 11.0) * 0.35
                    + Math.sin(elapsed * 23.0) * 0.25
                    + Math.cos(elapsed * 5.3) * 0.4;

      fireLight.intensity = 27 + flicker * 6.5;
      fireLight.position.x = Math.sin(elapsed * 8.0) * 0.08;
      fireLight.position.z = Math.cos(elapsed * 7.2) * 0.08;
      fireLight.position.y = 1.35 + Math.sin(elapsed * 14.0) * 0.08;

      fireGroundGlow.intensity = 7.5 + flicker * 2.0;
      emberMat.emissiveIntensity = 1.6 + flicker * 0.45;

      // 3. Update rising sparks
      const posArr = sparkGeo.attributes.position.array;
      for (let i = 0; i < sparkCount; i++) {
        const d = sparkData[i];
        const idx = i * 3;

        posArr[idx + 1] += d.vy * delta;
        posArr[idx] += (d.vx + Math.sin(elapsed * d.turbulence + d.phase) * 0.35) * delta;
        posArr[idx + 2] += (d.vz + Math.cos(elapsed * d.turbulence + d.phase) * 0.35) * delta;

        if (posArr[idx + 1] > d.maxY) {
          resetSpark(i, false);
        }
      }
      sparkGeo.attributes.position.needsUpdate = true;

      // Subtle slow rotation of stars
      stars.rotation.y = elapsed * 0.004;

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

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

    animate();
  </script>
</body>
</html>