Gemini 4 Argon
by Googlereasoning defaultClean
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
- 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 • 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>