MiniMax M3.1 Flash
by MiniMaxReasoning1 console error
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 9 min 34 s
- Cost
- $0.023
- Output tokens
- 37,962
- Spent thinking
- 26,581 tokens
- Speed
- 66.2 tok/s
- Console errors
- 1
- First token after
- 404 ms
- First visible text after
- 8 min 16 s
- Prompt tokens
- 730
- Code size
- 36.9 KB
- Finish reason
- stop
- Served by
- Together
- Model id
minimax/minimax-m3.1-flash- Reasoning sent
{"effort":"max"}- Max output
- 64,000 tokens
- Generated
- Nov 11, 2026
Console errors
Caught in the first 3 seconds after load, in a sandboxed iframe like the one above.
Cannot read properties of undefined (reading 'set')
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 · 1179 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#03070d" />
<title>Emberlight — Midnight Clearing</title>
<style>
:root {
color-scheme: dark;
--ink: #e9f0f2;
--muted: #92a6ad;
--ember: #ff9b50;
}
* { box-sizing: border-box; }
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #03070d;
}
body {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
color: var(--ink);
}
#scene {
position: fixed;
inset: 0;
}
#scene canvas {
display: block;
width: 100%;
height: 100%;
cursor: grab;
outline: none;
}
#scene canvas:active { cursor: grabbing; }
.vignette,
.edge-shade {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 2;
}
.vignette {
background:
radial-gradient(ellipse at 50% 48%, transparent 35%, rgba(0, 3, 8, 0.18) 67%, rgba(0, 2, 6, 0.72) 115%),
linear-gradient(to bottom, rgba(2, 7, 14, 0.28), transparent 23%, transparent 74%, rgba(2, 6, 11, 0.44));
}
.hud {
position: fixed;
inset: 0;
z-index: 3;
pointer-events: none;
}
.title-block {
position: absolute;
top: clamp(22px, 4.5vw, 48px);
left: clamp(22px, 4.5vw, 54px);
max-width: 340px;
text-shadow: 0 2px 24px rgba(0, 0, 0, 0.65);
}
.kicker {
display: flex;
align-items: center;
gap: 9px;
margin-bottom: 9px;
color: #b7c7cb;
font-size: 10px;
font-weight: 650;
letter-spacing: 0.24em;
text-transform: uppercase;
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--ember);
box-shadow: 0 0 12px rgba(255, 128, 47, 0.95);
animation: pulse 2.4s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.55; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.15); }
}
h1 {
margin: 0;
font-size: clamp(29px, 4vw, 48px);
font-weight: 300;
line-height: 0.98;
letter-spacing: -0.045em;
}
.title-block p {
margin: 12px 0 0;
color: var(--muted);
font-size: 12px;
font-weight: 450;
letter-spacing: 0.055em;
}
.meta {
position: absolute;
top: clamp(27px, 4.8vw, 52px);
right: clamp(22px, 4.5vw, 54px);
display: flex;
align-items: center;
gap: 13px;
color: #91a6ae;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 9px;
letter-spacing: 0.16em;
}
.meta b {
color: #dce7e9;
font-weight: 650;
}
.meta .divider {
width: 1px;
height: 14px;
background: rgba(190, 211, 216, 0.25);
}
.meta .live {
color: #efa16a;
}
.hint {
position: absolute;
right: clamp(22px, 4.5vw, 54px);
bottom: clamp(22px, 4vw, 42px);
display: flex;
align-items: center;
gap: 10px;
padding: 9px 13px;
border: 1px solid rgba(190, 214, 220, 0.13);
border-radius: 999px;
background: rgba(4, 10, 16, 0.48);
color: #9db0b6;
font-size: 10px;
letter-spacing: 0.06em;
backdrop-filter: blur(9px);
}
.hint i {
color: #52676f;
font-style: normal;
}
.mouse-icon {
position: relative;
width: 12px;
height: 17px;
border: 1px solid #8299a0;
border-radius: 7px;
}
.mouse-icon::after {
content: "";
position: absolute;
top: 3px;
left: 4px;
width: 2px;
height: 4px;
border-radius: 2px;
background: #a9bbc0;
}
@media (max-width: 650px) {
.meta { display: none; }
.title-block { max-width: 245px; }
.title-block p { font-size: 10px; }
.hint {
left: 50%;
right: auto;
bottom: 18px;
transform: translateX(-50%);
white-space: nowrap;
}
}
</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>
<main id="scene" aria-label="Low-poly forest clearing with an animated campfire"></main>
<div class="vignette"></div>
<div class="hud">
<section class="title-block">
<div class="kicker"><span class="live-dot"></span>Procedural night</div>
<h1>Emberlight</h1>
<p>Midnight in the pine clearing</p>
</section>
<div class="meta">
<span><b id="tree-count">48</b> LOW-POLY PINES</span>
<span class="divider"></span>
<span class="live">LIVE FIRE</span>
</div>
<div class="hint">
<span class="mouse-icon"></span>
Drag to orbit <i>·</i> Scroll to zoom
</div>
</div>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const TAU = Math.PI * 2;
const container = document.getElementById('scene');
// Seeded randomness keeps the forest composition consistent.
let seed = 0x73a4c19;
function random() {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
return seed / 4294967296;
}
const renderer = new THREE.WebGLRenderer({
antialias: true,
powerPreference: 'high-performance'
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.7));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.12;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.domElement.setAttribute('aria-label', 'Interactive low-poly campfire forest scene');
container.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x03070d);
scene.fog = new THREE.FogExp2(0x0a1622, 0.0215);
const camera = new THREE.PerspectiveCamera(
49,
window.innerWidth / window.innerHeight,
0.1,
220
);
camera.position.set(7.8, 4.8, 10.0);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1.2, 0);
controls.enableDamping = true;
controls.dampingFactor = 0.055;
controls.enablePan = false;
controls.minDistance = 6.2;
controls.maxDistance = 20;
controls.minPolarAngle = 0.28;
controls.maxPolarAngle = Math.PI * 0.47;
controls.rotateSpeed = 0.52;
controls.zoomSpeed = 0.72;
controls.autoRotate = true;
controls.autoRotateSpeed = 0.2;
controls.addEventListener('start', () => {
controls.autoRotate = false;
});
controls.update();
function makeRadialTexture(size = 128) {
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
const context = canvas.getContext('2d');
const gradient = context.createRadialGradient(
size / 2, size / 2, 0,
size / 2, size / 2, size / 2
);
gradient.addColorStop(0, 'rgba(255,255,255,1)');
gradient.addColorStop(0.16, 'rgba(255,255,255,0.92)');
gradient.addColorStop(0.48, 'rgba(255,255,255,0.28)');
gradient.addColorStop(1, 'rgba(255,255,255,0)');
context.fillStyle = gradient;
context.fillRect(0, 0, size, size);
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
}
const glowTexture = makeRadialTexture();
// Lighting: cool moon ambience plus the warm, shadow-casting campfire.
const hemisphere = new THREE.HemisphereLight(0x58789c, 0x131a12, 0.95);
scene.add(hemisphere);
const moonLight = new THREE.DirectionalLight(0xa5c5f2, 2.05);
moonLight.position.set(-20, 29, -25);
scene.add(moonLight);
scene.add(moonLight.target);
const fillLight = new THREE.AmbientLight(0x213344, 0.18);
scene.add(fillLight);
// Faceted gradient sky.
const skyGeometry = new THREE.SphereGeometry(120, 32, 18);
const skyPositions = skyGeometry.attributes.position;
const skyColors = new Float32Array(skyPositions.count * 3);
const horizonColor = new THREE.Color(0x101e2b);
const middleColor = new THREE.Color(0x07111d);
const zenithColor = new THREE.Color(0x02050c);
const skyColor = new THREE.Color();
for (let i = 0; i < skyPositions.count; i++) {
const h = THREE.MathUtils.clamp(skyPositions.getY(i) / 120 * 0.5 + 0.5, 0, 1);
if (h < 0.5) {
skyColor.copy(horizonColor).lerp(middleColor, h * 2);
} else {
skyColor.copy(middleColor).lerp(zenithColor, (h - 0.5) * 2);
}
skyColors[i * 3] = skyColor.r;
skyColors[i * 3 + 1] = skyColor.g;
skyColors[i * 3 + 2] = skyColor.b;
}
skyGeometry.setAttribute('color', new THREE.BufferAttribute(skyColors, 3));
const sky = new THREE.Mesh(
skyGeometry,
new THREE.MeshBasicMaterial({
vertexColors: true,
side: THREE.BackSide,
depthWrite: false,
fog: false,
toneMapped: false
})
);
sky.renderOrder = -100;
sky.frustumCulled = false;
scene.add(sky);
// Stars in two brightness layers.
function createStars(count, radius, size, opacity) {
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
const color = new THREE.Color();
for (let i = 0; i < count; i++) {
const y = 0.055 + Math.pow(random(), 0.68) * 0.945;
const angle = random() * TAU;
const planar = Math.sqrt(1 - y * y);
positions[i * 3] = Math.cos(angle) * planar * radius;
positions[i * 3 + 1] = y * radius;
positions[i * 3 + 2] = Math.sin(angle) * planar * radius;
color.setHSL(
0.54 + random() * 0.09,
0.12 + random() * 0.34,
0.68 + random() * 0.30
);
colors[i * 3] = color.r;
colors[i * 3 + 1] = color.g;
colors[i * 3 + 2] = color.b;
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size,
sizeAttenuation: false,
map: glowTexture,
transparent: true,
opacity,
alphaTest: 0.01,
vertexColors: true,
depthWrite: false,
fog: false,
toneMapped: false
});
const points = new THREE.Points(geometry, material);
points.renderOrder = -20;
scene.add(points);
return points;
}
createStars(850, 90, 1.45, 0.88);
createStars(95, 89, 2.8, 0.95);
// Low-poly moon and its soft halo.
const moonPosition = new THREE.Vector3(-18, 18, -30);
const moon = new THREE.Mesh(
new THREE.SphereGeometry(2.05, 10, 7),
new THREE.MeshBasicMaterial({
color: 0xe6efff,
fog: false,
toneMapped: false
})
);
moon.position.copy(moonPosition);
scene.add(moon);
const moonHalo = new THREE.Sprite(
new THREE.SpriteMaterial({
map: glowTexture,
color: 0x9bc4ff,
transparent: true,
opacity: 0.28,
blending: THREE.AdditiveBlending,
depthWrite: false,
fog: false,
toneMapped: false
})
);
moonHalo.position.copy(moonPosition);
moonHalo.scale.set(9.5, 9.5, 1);
scene.add(moonHalo);
// Faceted ground with individually colored triangles.
const groundGeometry = new THREE.PlaneGeometry(120, 120, 36, 36);
groundGeometry.rotateX(-Math.PI / 2);
const facetedGround = groundGeometry.toNonIndexed();
groundGeometry.dispose();
const groundPosition = facetedGround.attributes.position;
const groundColorAttribute = new Float32Array(groundPosition.count * 3);
const clearingSoil = new THREE.Color(0x26372a);
const forestSoil = new THREE.Color(0x101d19);
for (let i = 0; i < groundPosition.count; i += 3) {
const x = (
groundPosition.getX(i) +
groundPosition.getX(i + 1) +
groundPosition.getX(i + 2)
) / 3;
const z = (
groundPosition.getZ(i) +
groundPosition.getZ(i + 1) +
groundPosition.getZ(i + 2)
) / 3;
const radius = Math.hypot(x, z);
const blend = THREE.MathUtils.clamp((radius - 6) / 44, 0, 1);
const color = clearingSoil.clone().lerp(forestSoil, blend);
color.multiplyScalar(0.76 + random() * 0.43);
for (let j = 0; j < 3; j++) {
const index = (i + j) * 3;
groundColorAttribute[index] = color.r;
groundColorAttribute[index + 1] = color.g;
groundColorAttribute[index + 2] = color.b;
}
}
facetedGround.setAttribute('color', new THREE.BufferAttribute(groundColorAttribute, 3));
const ground = new THREE.Mesh(
facetedGround,
new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 1,
metalness: 0,
flatShading: true,
dithering: true
})
);
ground.receiveShadow = true;
scene.add(ground);
// Irregular, slightly raised clearing.
const clearingShape = new THREE.Shape();
for (let i = 0; i < 32; i++) {
const angle = i / 32 * TAU;
const radius = 7.45 + Math.sin(angle * 3 + 0.4) * 0.34 + Math.sin(angle * 7) * 0.17;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
if (i === 0) clearingShape.moveTo(x, y);
else clearingShape.lineTo(x, y);
}
clearingShape.closePath();
const clearingGeometry = new THREE.ShapeGeometry(clearingShape);
clearingGeometry.rotateX(-Math.PI / 2);
const clearing = new THREE.Mesh(
clearingGeometry,
new THREE.MeshStandardMaterial({
color: 0x2b382c,
roughness: 1,
flatShading: true,
polygonOffset: true,
polygonOffsetFactor: -1
})
);
clearing.position.y = 0.025;
clearing.receiveShadow = true;
scene.add(clearing);
const ashBed = new THREE.Mesh(
new THREE.CircleGeometry(1.75, 20),
new THREE.MeshStandardMaterial({
color: 0x211b16,
roughness: 1,
flatShading: true
})
);
ashBed.rotation.x = -Math.PI / 2;
ashBed.position.y = 0.042;
ashBed.receiveShadow = true;
scene.add(ashBed);
// Instanced low-poly forest: 48 random trees, all outside the fire.
const treeCount = 48;
document.getElementById('tree-count').textContent = treeCount;
const trunkGeometry = new THREE.CylinderGeometry(0.13, 0.24, 1, 6, 1);
const foliageGeometry = new THREE.ConeGeometry(1, 1, 6, 1);
const trunkMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 1,
flatShading: true
});
const foliageMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 0.96,
flatShading: true
});
const trunks = new THREE.InstancedMesh(trunkGeometry, trunkMaterial, treeCount);
const lowerFoliage = new THREE.InstancedMesh(foliageGeometry, foliageMaterial, treeCount);
const middleFoliage = new THREE.InstancedMesh(foliageGeometry, foliageMaterial, treeCount);
const upperFoliage = new THREE.InstancedMesh(foliageGeometry, foliageMaterial, treeCount);
for (const mesh of [trunks, lowerFoliage, middleFoliage, upperFoliage]) {
mesh.castShadow = true;
mesh.receiveShadow = true;
scene.add(mesh);
}
const matrixDummy = new THREE.Object3D();
const trunkPalette = [0x4a3428, 0x3c2d25, 0x513a2b];
const foliagePalette = [0x193d31, 0x214a37, 0x15342e, 0x28503b];
for (let i = 0; i < treeCount; i++) {
let x;
let z;
let radius;
do {
const angle = random() * TAU;
radius = 12.7 + Math.pow(random(), 0.62) * 30;
x = Math.cos(angle) * radius;
z = Math.sin(angle) * radius;
} while (
Math.hypot(x, z) < 12.2 ||
Math.hypot(x - camera.position.x, z - camera.position.z) < 3.1
);
const scale = 0.72 + Math.pow(random(), 0.72) * 0.68;
const rotation = random() * TAU;
const trunkHeight = 3.8 * scale;
matrixDummy.position.set(x, trunkHeight * 0.5, z);
matrixDummy.rotation.set(0, rotation, 0);
matrixDummy.scale.set(scale * 0.95, trunkHeight, scale * 0.95);
matrixDummy.updateMatrix();
trunks.setMatrixAt(i, matrixDummy.matrix);
matrixDummy.position.set(x, 4.22 * scale, z);
matrixDummy.rotation.set(0, rotation + 0.16, 0);
matrixDummy.scale.set(2.12 * scale, 3.58 * scale, 2.12 * scale);
matrixDummy.updateMatrix();
lowerFoliage.setMatrixAt(i, matrixDummy.matrix);
matrixDummy.position.set(x, 5.82 * scale, z);
matrixDummy.rotation.set(0, rotation + 0.48, 0);
matrixDummy.scale.set(1.61 * scale, 3.02 * scale, 1.61 * scale);
matrixDummy.updateMatrix();
middleFoliage.setMatrixAt(i, matrixDummy.matrix);
matrixDummy.position.set(x, 7.20 * scale, z);
matrixDummy.rotation.set(0, rotation + 0.82, 0);
matrixDummy.scale.set(1.03 * scale, 2.42 * scale, 1.03 * scale);
matrixDummy.updateMatrix();
upperFoliage.setMatrixAt(i, matrixDummy.matrix);
trunks.setColorAt(i, new THREE.Color(trunkPalette[i % trunkPalette.length]));
const leafColor = new THREE.Color(foliagePalette[i % foliagePalette.length]);
lowerFoliage.setColorAt(i, leafColor);
middleFoliage.setColorAt(i, leafColor.clone().multiplyScalar(1.08));
upperFoliage.setColorAt(i, leafColor.clone().multiplyScalar(0.91));
}
for (const mesh of [trunks, lowerFoliage, middleFoliage, upperFoliage]) {
mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
mesh.computeBoundingSphere();
}
// Small crossed grass blades around the clearing.
const bladePositions = [];
for (let i = 0; i < 3; i++) {
const angle = i / 3 * TAU + 0.22;
const dx = Math.cos(angle);
const dz = Math.sin(angle);
bladePositions.push(
-dx * 0.17, 0, -dz * 0.17,
dx * 0.17, 0, dz * 0.17,
dx * 0.12, 0.72, dz * 0.12
);
}
const bladeGeometry = new THREE.BufferGeometry();
bladeGeometry.setAttribute('position', new THREE.Float32BufferAttribute(bladePositions, 3));
bladeGeometry.computeVertexNormals();
const grassCount = 300;
const grass = new THREE.InstancedMesh(
bladeGeometry,
new THREE.MeshStandardMaterial({
color: 0xffffff,
roughness: 1,
flatShading: true,
side: THREE.DoubleSide
}),
grassCount
);
grass.receiveShadow = true;
for (let i = 0; i < grassCount; i++) {
const angle = random() * TAU;
const radius = 3.4 + Math.pow(random(), 0.7) * 11.5;
const scale = 0.48 + random() * 0.88;
matrixDummy.position.set(
Math.cos(angle) * radius,
0.045,
Math.sin(angle) * radius
);
matrixDummy.rotation.set(0, random() * TAU, 0);
matrixDummy.scale.set(scale * 0.72, scale, scale * 0.72);
matrixDummy.updateMatrix();
grass.setMatrixAt(i, matrixDummy.matrix);
grass.setColorAt(
i,
new THREE.Color().setHSL(
0.31 + random() * 0.055,
0.24 + random() * 0.18,
0.15 + random() * 0.10
)
);
}
grass.instanceMatrix.needsUpdate = true;
if (grass.instanceColor) grass.instanceColor.needsUpdate = true;
grass.computeBoundingSphere();
scene.add(grass);
// Distant, fog-softened polygonal hills.
const hillCount = 17;
const hills = new THREE.InstancedMesh(
new THREE.ConeGeometry(1, 1, 5, 1),
new THREE.MeshStandardMaterial({
color: 0x101c25,
roughness: 1,
flatShading: true
}),
hillCount
);
for (let i = 0; i < hillCount; i++) {
const angle = i / hillCount * TAU + random() * 0.24;
const radius = 54 + random() * 9;
const height = 7 + random() * 12;
const width = 7 + random() * 10;
matrixDummy.position.set(
Math.cos(angle) * radius,
height * 0.5 - 0.8,
Math.sin(angle) * radius
);
matrixDummy.rotation.set(0, random() * TAU, 0);
matrixDummy.scale.set(width, height, width * 0.8);
matrixDummy.updateMatrix();
hills.setMatrixAt(i, matrixDummy.matrix);
}
hills.instanceMatrix.needsUpdate = true;
hills.computeBoundingSphere();
scene.add(hills);
// Rocks around the fire and scattered through the clearing.
const rockGeometryA = new THREE.IcosahedronGeometry(1, 0);
const rockGeometryB = new THREE.DodecahedronGeometry(1, 0);
const rockMaterials = [
new THREE.MeshStandardMaterial({ color: 0x4c5357, roughness: 1, flatShading: true }),
new THREE.MeshStandardMaterial({ color: 0x3f484c, roughness: 1, flatShading: true }),
new THREE.MeshStandardMaterial({ color: 0x59595a, roughness: 1, flatShading: true })
];
function addRock(geometry, material, x, z, sx, sy, sz) {
const rock = new THREE.Mesh(geometry, material);
rock.position.set(x, sy * 0.38, z);
rock.scale.set(sx, sy, sz);
rock.rotation.set(random() * 0.6, random() * TAU, random() * 0.6);
rock.castShadow = true;
rock.receiveShadow = true;
scene.add(rock);
return rock;
}
for (let i = 0; i < 15; i++) {
const angle = i / 15 * TAU + random() * 0.16;
const radius = 2.03 + random() * 0.22;
const size = 0.33 + random() * 0.19;
addRock(
i % 2 ? rockGeometryA : rockGeometryB,
rockMaterials[i % rockMaterials.length],
Math.cos(angle) * radius,
Math.sin(angle) * radius,
size * (0.9 + random() * 0.25),
size * (0.62 + random() * 0.2),
size * (0.9 + random() * 0.25)
);
}
for (let i = 0; i < 17; i++) {
const angle = random() * TAU;
const radius = 3.8 + random() * 7.4;
const size = 0.33 + random() * 0.86;
addRock(
i % 3 ? rockGeometryA : rockGeometryB,
rockMaterials[(i + 1) % rockMaterials.length],
Math.cos(angle) * radius,
Math.sin(angle) * radius,
size * (0.8 + random() * 0.6),
size * (0.48 + random() * 0.5),
size * (0.8 + random() * 0.6)
);
}
// Cylindrical logs, including three charred logs over the hearth.
const barkMaterial = new THREE.MeshStandardMaterial({
color: 0x493125,
roughness: 1,
flatShading: true
});
const charredBarkMaterial = new THREE.MeshStandardMaterial({
color: 0x241b16,
roughness: 1,
flatShading: true
});
const burnedEndMaterial = new THREE.MeshStandardMaterial({
color: 0x100c09,
roughness: 1,
flatShading: true
});
const upAxis = new THREE.Vector3(0, 1, 0);
function createLog(startArray, endArray, radius, material, endMaterial) {
const start = new THREE.Vector3(...startArray);
const end = new THREE.Vector3(...endArray);
const direction = end.clone().sub(start);
const length = direction.length();
const group = new THREE.Group();
const body = new THREE.Mesh(
new THREE.CylinderGeometry(radius * 0.91, radius, length, 8, 1, false),
material
);
body.castShadow = true;
body.receiveShadow = true;
group.add(body);
const bottomCap = new THREE.Mesh(
new THREE.CircleGeometry(radius, 8),
endMaterial
);
bottomCap.rotation.x = Math.PI / 2;
bottomCap.position.y = -length * 0.5 - 0.003;
const topCap = new THREE.Mesh(
new THREE.CircleGeometry(radius * 0.91, 8),
endMaterial
);
topCap.rotation.x = -Math.PI / 2;
topCap.position.y = length * 0.5 + 0.003;
group.add(bottomCap, topCap);
group.position.copy(start).add(end).multiplyScalar(0.5);
group.quaternion.setFromUnitVectors(upAxis, direction.normalize());
scene.add(group);
return group;
}
createLog([-1.28, 0.29, -0.88], [1.12, 0.38, 0.78], 0.25, charredBarkMaterial, burnedEndMaterial);
createLog([1.20, 0.27, -0.92], [-1.18, 0.38, 0.90], 0.25, charredBarkMaterial, burnedEndMaterial);
createLog([-0.76, 0.55, -1.25], [0.78, 0.58, 1.28], 0.21, charredBarkMaterial, burnedEndMaterial);
createLog([-6.2, 0.36, 2.8], [-2.7, 0.49, 3.8], 0.38, barkMaterial, burnedEndMaterial);
createLog([5.7, 0.34, -4.9], [8.4, 0.48, -3.7], 0.34, barkMaterial, burnedEndMaterial);
createLog([-1.7, 0.32, -7.1], [2.6, 0.4, -7.8], 0.31, barkMaterial, burnedEndMaterial);
// Glowing coals.
const coalMaterial = new THREE.MeshStandardMaterial({
color: 0x3b160b,
emissive: 0xff3108,
emissiveIntensity: 1.8,
roughness: 1,
flatShading: true
});
for (let i = 0; i < 12; i++) {
const angle = random() * TAU;
const radius = random() * 0.92;
const coal = new THREE.Mesh(
i % 2 ? rockGeometryA : rockGeometryB,
coalMaterial
);
coal.position.set(
Math.cos(angle) * radius,
0.24 + random() * 0.13,
Math.sin(angle) * radius
);
coal.scale.setScalar(0.13 + random() * 0.12);
coal.rotation.set(random() * TAU, random() * TAU, random() * TAU);
scene.add(coal);
}
// Warm pool of light painted over the ground.
const groundGlowMaterial = new THREE.MeshBasicMaterial({
map: glowTexture,
color: 0xff7828,
transparent: true,
opacity: 0.34,
blending: THREE.AdditiveBlending,
depthWrite: false,
side: THREE.DoubleSide
});
const groundGlow = new THREE.Mesh(new THREE.CircleGeometry(4.7, 32), groundGlowMaterial);
groundGlow.rotation.x = -Math.PI / 2;
groundGlow.position.y = 0.068;
groundGlow.renderOrder = 1;
scene.add(groundGlow);
const fireLight = new THREE.PointLight(0xff8b3d, 105, 21, 2);
fireLight.position.set(0, 1.48, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.camera.near = 0.2;
fireLight.shadow.camera.far = 21;
fireLight.shadow.camera.updateProjectionMatrix();
fireLight.shadow.bias = -0.0015;
fireLight.shadow.normalBias = 0.035;
fireLight.shadow.radius = 2;
scene.add(fireLight);
// Procedural faceted flame geometry.
const flameBottom = new THREE.Color(0xff2600);
const flameOrange = new THREE.Color(0xff6500);
const flameGold = new THREE.Color(0xffad12);
const flameYellow = new THREE.Color(0xffe06a);
function flameColor(t, target) {
if (t < 0.28) return target.copy(flameBottom).lerp(flameOrange, t / 0.28);
if (t < 0.68) return target.copy(flameOrange).lerp(flameGold, (t - 0.28) / 0.4);
return target.copy(flameGold).lerp(flameYellow, (t - 0.68) / 0.32);
}
function createFlameGeometry(radius, height, leanX, leanZ, phase) {
const sides = 6;
const levels = [
{ t: 0.00, r: 0.73, x: 0, z: 0 },
{ t: 0.17, r: 1.00, x: leanX * 0.08, z: leanZ * 0.08 },
{ t: 0.48, r: 0.66, x: leanX * 0.34, z: leanZ * 0.34 },
{ t: 0.76, r: 0.29, x: leanX * 0.72, z: leanZ * 0.72 },
{ t: 1.00, r: 0.00, x: leanX, z: leanZ }
];
const rings = levels.map(level => {
const ring = [];
for (let side = 0; side < sides; side++) {
const angle = side / sides * TAU;
const variation = 1 + Math.sin(side * 2.73 + phase * 1.7) * 0.11;
const r = level.r * radius * variation;
ring.push(new THREE.Vector3(
level.x + Math.cos(angle) * r,
level.t * height,
level.z + Math.sin(angle) * r
));
}
return ring;
});
const positions = [];
const colors = [];
const color = new THREE.Color();
function pushVertex(point) {
positions.push(point.x, point.y, point.z);
flameColor(THREE.MathUtils.clamp(point.y / height, 0, 1), color);
colors.push(color.r, color.g, color.b);
}
for (let level = 0; level < levels.length - 1; level++) {
for (let side = 0; side < sides; side++) {
const nextSide = (side + 1) % sides;
const a = rings[level][side];
const b = rings[level][nextSide];
const c = rings[level + 1][nextSide];
const d = rings[level + 1][side];
pushVertex(a); pushVertex(d); pushVertex(c);
pushVertex(a); pushVertex(c); pushVertex(b);
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geometry.computeVertexNormals();
return geometry;
}
const outerFlameMaterial = new THREE.MeshStandardMaterial({
vertexColors: true,
emissive: 0xff4b00,
emissiveIntensity: 1.1,
roughness: 1,
transparent: true,
opacity: 0.94,
depthWrite: false,
side: THREE.DoubleSide,
flatShading: true
});
const innerFlameMaterial = new THREE.MeshBasicMaterial({
vertexColors: true,
transparent: true,
opacity: 0.95,
depthWrite: false,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
toneMapped: false
});
const flames = [];
function addFlame(radius, height, x, z, inner = false, scale = 1) {
const phase = random() * TAU;
const leanX = (random() - 0.5) * radius * 0.9;
const leanZ = (random() - 0.5) * radius * 0.9;
const geometry = createFlameGeometry(radius, height, leanX, leanZ, phase);
const mesh = new THREE.Mesh(
geometry,
inner ? innerFlameMaterial : outerFlameMaterial
);
mesh.position.set(x, 0.43, z);
mesh.rotation.y = random() * TAU;
mesh.renderOrder = inner ? 3 : 2;
scene.add(mesh);
flames.push({
mesh,
phase,
baseX: x,
baseY: 0.43,
baseZ: z,
baseRotationZ: 0,
scale,
inner
});
}
addFlame(0.65, 2.95, 0, 0, false, 1);
const outerSpecs = [
[0.46, 2.35, 0.48, 0.08],
[0.43, 2.08, -0.45, 0.19],
[0.48, 2.46, 0.10, -0.53],
[0.40, 1.92, -0.20, -0.45],
[0.38, 2.15, 0.28, 0.39],
[0.39, 1.88, -0.38, -0.18]
];
for (const spec of outerSpecs) addFlame(...spec, false, 0.94);
addFlame(0.29, 1.82, 0.08, 0.03, true, 0.9);
addFlame(0.25, 1.52, -0.21, 0.10, true, 0.82);
addFlame(0.24, 1.44, 0.20, -0.15, true, 0.8);
addFlame(0.22, 1.25, 0.02, 0.25, true, 0.76);
const fireCore = new THREE.Mesh(
new THREE.IcosahedronGeometry(0.42, 1),
new THREE.MeshBasicMaterial({
color: 0xffd34b,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending,
depthWrite: false,
toneMapped: false
})
);
fireCore.position.y = 0.77;
fireCore.renderOrder = 4;
scene.add(fireCore);
// Rising ember particle system.
const sparkCount = 100;
const sparkPositions = new Float32Array(sparkCount * 3);
const sparkColors = new Float32Array(sparkCount * 3);
const sparkGeometry = new THREE.BufferGeometry();
sparkGeometry.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
sparkGeometry.setAttribute('color', new THREE.BufferAttribute(sparkColors, 3));
const sparkMaterial = new THREE.PointsMaterial({
size: 0.17,
sizeAttenuation: true,
map: glowTexture,
transparent: true,
alphaTest: 0.01,
vertexColors: true,
blending: THREE.AdditiveBlending,
depthWrite: false,
toneMapped: false
});
const sparkPoints = new THREE.Points(sparkGeometry, sparkMaterial);
sparkPoints.renderOrder = 5;
scene.add(sparkPoints);
const sparks = [];
function resetSpark(spark, initial = false) {
const angle = random() * TAU;
const radius = random() * 0.72;
spark.age = 0;
spark.life = 1.45 + random() * 2.25;
spark.phase = random() * TAU;
spark.position.set(
Math.cos(angle) * radius,
0.72 + random() * 0.28,
Math.sin(angle) * radius
);
spark.velocity.set(
(random() - 0.5) * 0.42,
0.72 + random() * 0.85,
(random() - 0.5) * 0.42
);
if (initial) {
spark.age = random() * spark.life;
spark.position.addScaledVector(spark.velocity, spark.age);
}
}
for (let i = 0; i < sparkCount; i++) {
const spark = {};
resetSpark(spark, true);
sparks.push(spark);
}
// Sparse low-poly smoke puffs.
const smoke = [];
for (let i = 0; i < 7; i++) {
const material = new THREE.MeshStandardMaterial({
color: 0x66727a,
transparent: true,
opacity: 0,
depthWrite: false,
roughness: 1,
flatShading: true
});
const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(0.58, 0), material);
mesh.renderOrder = 1;
scene.add(mesh);
smoke.push({
mesh,
phase: random(),
angle: random() * TAU,
drift: 0.18 + random() * 0.24
});
}
const clock = new THREE.Clock();
let elapsed = 0;
const sparkColor = new THREE.Color();
function animate() {
requestAnimationFrame(animate);
const delta = Math.min(clock.getDelta(), 0.05);
elapsed += delta;
const flicker =
0.82 +
Math.sin(elapsed * 10.7) * 0.095 +
Math.sin(elapsed * 18.3 + 1.4) * 0.055 +
Math.sin(elapsed * 29.1 + 0.3) * 0.035;
fireLight.intensity = 105 * flicker;
fireLight.position.y = 1.48 + Math.sin(elapsed * 13.4) * 0.035;
fireLight.position.x = Math.sin(elapsed * 8.1) * 0.025;
coalMaterial.emissiveIntensity = 1.15 + flicker * 1.55;
for (const flame of flames) {
const speed = flame.inner ? 10.5 : 7.1;
const pulse =
0.88 +
Math.sin(elapsed * speed + flame.phase) * 0.115 +
Math.sin(elapsed * speed * 1.73 + flame.phase * 2.1) * 0.045;
flame.mesh.scale.set(
flame.scale * (0.94 + Math.sin(elapsed * 6.2 + flame.phase) * 0.075),
flame.scale * pulse,
flame.scale * (0.96 + Math.cos(elapsed * 7.1 + flame.phase) * 0.065)
);
flame.mesh.rotation.z = Math.sin(elapsed * 3.1 + flame.phase) * 0.105;
flame.mesh.rotation.x = Math.cos(elapsed * 2.7 + flame.phase) * 0.065;
flame.mesh.position.x = flame.baseX + Math.sin(elapsed * 4.1 + flame.phase) * 0.028;
flame.mesh.position.y = flame.baseY + (pulse - 0.9) * 0.13;
}
fireCore.scale.setScalar(0.86 + flicker * 0.22);
fireCore.rotation.y = elapsed * 1.7;
const glowScale = 0.93 + flicker * 0.13;
groundGlow.scale.set(glowScale, glowScale, 1);
groundGlowMaterial.opacity = 0.27 + flicker * 0.12;
for (let i = 0; i < sparkCount; i++) {
const spark = sparks[i];
spark.age += delta;
if (spark.age > spark.life || spark.position.y > 7.3) {
resetSpark(spark);
}
spark.position.addScaledVector(spark.velocity, delta);
spark.velocity.y += 0.035 * delta;
spark.position.x += Math.sin(elapsed * 2.1 + spark.phase) * 0.17 * delta;
spark.position.z += Math.cos(elapsed * 1.8 + spark.phase) * 0.14 * delta;
const fade = 1 - spark.age / spark.life;
sparkColor.setRGB(1, 0.76 - spark.age / spark.life * 0.38, 0.25 - spark.age / spark.life * 0.2);
sparkColor.multiplyScalar(Math.min(1, fade * 1.8));
sparkPositions[i * 3] = spark.position.x;
sparkPositions[i * 3 + 1] = spark.position.y;
sparkPositions[i * 3 + 2] = spark.position.z;
sparkColors[i * 3] = sparkColor.r;
sparkColors[i * 3 + 1] = sparkColor.g;
sparkColors[i * 3 + 2] = sparkColor.b;
}
sparkGeometry.attributes.position.needsUpdate = true;
sparkGeometry.attributes.color.needsUpdate = true;
for (const puff of smoke) {
const life = (elapsed * 0.105 + puff.phase) % 1;
puff.mesh.position.set(
Math.sin(puff.angle + life * 2.2) * (0.18 + life * puff.drift * 4),
2.0 + life * 6.1,
Math.cos(puff.angle + life * 1.8) * (0.15 + life * puff.drift * 3.5)
);
const size = 0.35 + life * 1.55;
puff.mesh.scale.set(size * 1.2, size * 0.88, size);
puff.mesh.rotation.set(life * 1.4, puff.angle + life, life * 0.8);
puff.mesh.material.opacity = Math.sin(life * Math.PI) * 0.075;
}
controls.update(delta);
renderer.render(scene, camera);
}
function resize() {
const width = window.innerWidth;
const height = window.innerHeight;
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, width < 700 ? 1.5 : 1.7));
renderer.setSize(width, height);
}
window.addEventListener('resize', resize);
resize();
animate();
</script>
</body>
</html>Reasoning trace (26,581 tokens)
We need produce exactly one fenced code block html complete file. Need implement Three.js via specified import map only. Need meet requirements robustly in sandbox 1280x800 responsive 375. Low-poly 3D forest clearing at night campfire center animated flames warm point light flicker shadows. Sparks particles. Dark night sky stars moon fog distant trees. 30+ trees randomly around not fire. Ground few low-poly rocks/logs. OrbitControls limits above ground. Renderer fills window resize.
We need design visually compelling. Need decide content and technical details. Single HTML inline css/js module imports via import map. We can import three and OrbitControls from addons as permitted. No other URLs. Build procedural geometry all inline. Need first paint visible meaningful, after 3 sec. Self-playing not game; no need click start (unless keyboard no). We can maybe show subtle title/instructions overlay, no interaction required. Need avoid performance heavy shadow maps etc. Aim polished.
Let's plan scene.
Canvas full window. Deep navy background/fog. Low poly ground clearing maybe circular ground disk with faceted terrain. Could use a large PlaneGeometry with flatShading and vertex displacement, low poly triangles. We can make radial disk? PlaneGeometry 90,90, 40,40 rotated, vertex colors varying dark teal/green based radial distance/noise, with low poly effect. Flat shading MeshStandardMaterial vertexColors. Fog exponential? Use THREE.FogExp2(0x071019, 0.035) or Fog linear near 14 far 62. Distant trees fade. Background 0x02060d. Sky stars as Points in sphere upper hemisphere; moon as low poly circle/sphere at back left, with glow sprite maybe canvas texture but inline canvas allowed. No external images. Can use custom radial gradient CanvasTexture for moon glow and ground vignette? Fine. Stars Points geometry randomized on sphere radius 85 or 100; size attenuation false; colors, varying sizes? PointsMaterial uniform size maybe use custom shader? Simpler two layers: small Points with size 0.12 and a few larger stars; use BufferGeometry positions/colors. In 3D sky sphere.
Trees: at least 36 around clearing randomly with radial distribution from radius 10 to 38, not within 7.5 of fire. Each tree low-poly: trunk CylinderGeometry radius top/bottom 5-7 sides height, brown flat shading. Foliage 2-3 cones (ConeGeometry radius, height, 5/6 sides) stacked in dark greens. Maybe variation. 40 trees, each group. Individual meshes with shadows. To keep draw calls okay ~160. Could merge? No BufferGeometryUtils unless allowed (three/addons/utils/BufferGeometryUtils.js) URL allowed because prefix. But simpler no need. At least 30 trees. Use InstancedMesh for tree parts for performance and variety: trunks instanced cylinder, foliage instanced cones 2 layers each. But want individual tree casts shadows. InstancedMesh supports castShadow. Could create 42 trees with 3 layer cone instanced mesh. Use tree count random. Tree positions in ring excluding clearing; radius 11-43, angle random; perhaps some clusters. Need ensure not obscure central campfire from camera initial. Camera at (10, 6.5, 14) looking at (0,1.4,0). Some trees in foreground could block. We can place trees mostly in arc ring radius 13-42; camera line direction ~+z, +x. Avoid sectors near initial camera perhaps leave clearing visual, but requirement at least 30 around clearing, random. We can keep trees radius 13+; camera z 14 outside clearing, trees at z>16 might obscure if in direct view. We can place radius >= 16, and avoid near camera corridor? Camera at radius ~17.5 so initial might among trees. Better camera at (9.5,5.8,12.5), radius 15.7. Trees start at radius 12 could be nearby. View from above to fire and trees frame. We can deliberately clear foreground corridor: skip positions in angle near camera azimuth but still random ring. 44 attempts maybe. Trees all at radius 12-38; camera can be at (10,7,13) radius 16.4 so potential. Could put camera (9,5.8,11) radius14.2. Exclude radius <13 around fire. Camera at r14.2, min tree r14 could intersect. Better camera radius 12.8 at (8.5,5.5,9.5), r12.76; min tree r14, camera outside? Actually inside tree ring, okay. Trees can be behind camera too but no obstruction because camera inside ring radius 12.7 and trees at >=13.5. View inward center, surrounding forest. Good.
Ground: large plane 100x100 segments 42x42 flat shaded with vertex colors. Flat ground around center maybe light dark moss. Could add clearing disk flattened? Mesh circle radius 8.5 color slightly lighter, but ground displacement. Use plane vertices and radial color. Add scattered small grass tufts/triangles around not fire (instanced cone maybe 180 small shards). Adds low-poly. Fern? maybe.
Campfire: stone ring around fire (12-16 dodecahedron/icosahedron rocks with random rotations, gray flat shaded) plus 3 logs arranged crossed. Logs cylinder geometry rotated horizontal and charred ends. Embers dark base. Flames animated low-poly mesh(es) using custom geometry tapered curved flame shape, e.g. ConeGeometry with 5 radial segments and open? Use ShaderMaterial for flicker or animate scale/position. Low poly flames should look like multiple tapered flame tongues with orange/yellow inner. Could create group of 7-9 flame "spikes" as ConeGeometry(radius .5, height 2.8, radial 5, heightSegments 4), each flat shading MeshBasicMaterial with vertex colors gradient? MeshBasic won't respond. Use MeshStandardMaterial emissive, flatShading. But flame shape could be custom lathe-like geometry with jagged profile, additive transparent? Need visually bright.
Option custom flame geometry via BufferGeometry rings with vertices, radius profile per y [0.0, 0.45, 0.8, 0.5, 0] and 6 sides, random offsets, then animate group scale and sway. Multiple flame meshes colored orange/red/amber. Could create THREE.LatheGeometry with profile points and segments 7, then distort vertices. But flat shading. Simpler cone with vertex colors gradient. Cylinder/Cone color gradient via vertex colors: bottom deep orange, mid orange, top yellow. ConeGeometry radius .45 height 3 segments 6, heightSegments 4; assign per vertex color based y. MeshStandardMaterial emissive 0xff4a00, emissiveIntensity 1.7, vertexColors, transparent maybe. But using vertex colors plus emissive. Inner flame small yellow cone. Add 6 outer flames (orange/red), 4 inner (yellow). Animate each scale.y and rotation z/x and position; opacity maybe.
Can make procedural flame shader using time uniforms? MeshBasicMaterial onBeforeCompile? Custom ShaderMaterial with low poly faceted colors and sway, but needs lighting? We can make ShaderMaterial with world normal fake. More effort. The requirement only animated flames; group scaling works.
Warm point light at (0,1.8,0), color 0xff8a2b, intensity 70? Three r170 physical lighting default. PointLight intensity in candela, distance 18, decay 2. Need visible warm. intensity flicker 45-70. castShadow true, shadow.mapSize 1024, bias -0.002, normalBias 0.04. Only point light shadows are 6 faces expensive but manageable at 1024. Flame meshes maybe don't cast shadow (avoid artifacts). Light and shadows from logs/rocks/trees near fire. Renderer shadowMap PCFSoftShadowMap. Other lights: hemisphere ambient 0x30445d sky and 0x060807 ground intensity ~0.6; directional moon light 0x829dcc intensity 0.9 from (-15,25,-12) casts maybe no shadows? Requirement warm point light casts shadows, need ensure visible shadow. Can have directional shadow too but heavier. Use moon directional castShadow with map 1024 maybe around scene; but point light shadow enough, ambient moon. Directional castShadow perhaps false to keep performance and not flatten. Yet tree silhouettes under point light. Point light castShadow.
Sparks particle system: Points geometry with 70 particles attributes positions; update each frame upward, sine, fade via color/alpha? PointsMaterial with map radial glow CanvasTexture, transparent, blending AdditiveBlending, depthWrite false, vertexColors. Per particle colors fade; update colors based life. Need ensure texture generated inline canvas. Points material size 0.16, sizeAttenuation true. Each spark has velocity, life. On reset at fire origin. positions around y1.4-2.8, rise to 5-9. Color from yellow to orange dark. Could add short trails as line segments? "simple particle system" points enough. Maybe two point systems: embers and smoke. Smoke low poly translucent gray cones/dodecahedrons drifting up. Not required, but adds atmosphere. Need no external.
Moon: Mesh low-poly sphere (SphereGeometry radius 2.2 segments 8,6) positioned (-18,24,-30) maybe direction visible camera at (8,5,10) looking center means forward direction (-8,-...,-10), so visible sky is negative x negative z, so moon at (-14,18,-24) visible. Use MeshBasicMaterial 0xe2eaff. Add glow Sprite with radial gradient canvas, opacity 0.35, scale 9. Maybe stars around. Moon could be circle/sphere. "dark night sky with stars and a moon". Use sphere with slight crescent? Can simulate crescent using overlay dark sphere offset slightly? Moon should clear. We can make low-poly pale sphere plus subtle crater spots maybe small circles? Not necessary. Full moon with glow.
Fog: scene.fog = new THREE.FogExp2(0x050b14, 0.026) or linear 12-60. Ground large. Sky background same but fog might affect moon if mesh material fog false. stars fog false. Need sky stars visible; PointsMaterial fog false.
Sky dome maybe ShaderMaterial gradient based direction to horizon; background flat dark plus a large sphere with shader? Could use scene.background color and fog. Add gradient sky using giant SphereGeometry with ShaderMaterial side BackSide depthWrite false, colors based normalized world y. But fog applies only if shader not using fog; no issue. Since background dark, can set scene.background 0x02050c and sky gradient mesh with top 0x020611, horizon 0x112033. Need stars visible over sky sphere: sky depthWrite false but render order? Sky rendered maybe first. Stars positions radius 70, sphere radius 95 so inside. transparent? sky opaque, renderOrder -10. Good.
We can use a simple ShaderMaterial:
vertex varying vec3 vWorldPosition; modelMatrix position. fragment h = normalize(vWorldPosition).y; mix horizon and zenith smoothstep(-.05,.65,h); gl_FragColor.
Since sphere centered maybe camera moves; center sky at origin, radius 120, camera inside. Works. Fog perhaps doesn't affect.
Ground: plane segments maybe 50 => 2500 triangles, okay. vertex colors. Vertex color choose based radial and noise. Need ensure flatShading and color. MeshStandardMaterial({vertexColors:true, roughness:1, flatShading:true}). PlaneGeometry(100,100,35,35) rotated -PI/2, but to set vertices color after rotate? geometry.rotateX(-PI/2), position attributes. for each x,z random. Height slight y = noise. We want central clearing flat, outer slight uneven. Use deterministic pseudo-random. Could define hash. For each vertex radial, height = (noise) * smoothstep(4,25,r) maybe 0.5. Colors: center moss 0x253426; edges 0x101c18. Lighting at night dark but visible. Material color white and vertex color. Add central flat circle patch Mesh CircleGeometry(7.5, 24) rotated, material 0x29342b roughness, maybe y 0.02. But plane heights around center 0; central patch polygon gives a clearing. Use flatShading true? Circle triangles all coplanar. color 0x26352a. Might zfight if y .02. Good. Add subtle ring of earth around fire darker.
Rocks around: 15-18 random lowpoly using IcosahedronGeometry(1,0) scaled, color gray/blue, material flat. Place near fire radius 2.3-3.2 (stones) and 12 larger rocks clearing r4-11. Each random nonuniform scale, rotation, y half etc. Cast receive shadows. Use shared geometry but material variations maybe 3.
Logs around campfire: 3 logs crossed over fire with cylinder geometry 8 sides, length 3.4, radius .25, rotated horizontal; color bark brown with char ends. Need logs around fire center (not fire maybe arranged over fire). We can make cylinders with geometry along Y, rotate z=PI/2 and y angles. Position y .45. Add end caps perhaps CylinderGeometry includes caps same material; dark burnt end via small Cylinder circles at ends maybe. Create log group function with CylinderGeometry radius .24 length 3.4 segments 8, standard brown rough. To align between points use quaternion from y axis to vector. create logs with endpoints around center: (-1.4,0.35,-0.8) to (1.1,0.55,1.0); etc. This uses lookAt? quaternion.setFromUnitVectors(new Vector3(0,1,0), dir). Add two black bands maybe. Logs around campfire beyond fire maybe two fallen logs on ground (requirement ground with logs around campfire). Crossed logs over fire count; also 3 fallen logs at radius 5-8. Low-poly cylinders.
Trees: create instanced meshes maybe 46 trees. Need at least 30 "placed randomly". We can code deterministic positions. For each tree, trunk and 2/3 conical layers. InstancedMesh for trunk and 3 foliage layers, cast shadows, receive? Geometry low-poly. But individual varying scale and rotation. Need include placement positions computed and avoid central radius. use random function seeded. To maintain visual variation and not hide moon, maybe ring distribution. At least 44.
Instanced tree trunk CylinderGeometry(0.16,0.32,3.2,6) maybe y at height/2. Scale each tree height random 0.85-1.55, foliage radius etc. Foliage layers cone geometry(1,1,6,1) oriented y, scaled (r,h,r) each layer at y. Actually base ConeGeometry radius 1, height 1. Tree total heights 6-10. Need 42 trees around. Low-poly broadleaf with stacked cones. Colors per instance: setColorAt variations green, teal. Trunk per instance color brown variations. We can use MeshStandardMaterial({color white, vertexColors? InstancedMesh color multiplies base automatically with instanceColor even without vertexColors? In three, instanceColor supported by shader if present; material color multiplies. yes}). MeshStandard flatShading true.
For tree parts: trunk InstancedMesh count N, foliage lower count N, middle count N, top count N. Geometry unit cone radius1 height1. Lower: scale radius 2.2*scale, height 3.6*scale, y=3.2*scale; middle radius1.7 height3 y=5.1; top radius1.15 height2.6 y=6.7. Need trunk top around 3.6. Tree total ~8. At scale 1.2 => 9.6. Fog distant.
CastShadow true for tree meshes. Point shadows include 42 trees but only near fire within distance 18 of light, maybe 8. Rendering instanced mesh point shadows 6* but okay. Directional no cast. Renderer shadows enabled. Pixel ratio min(devicePixelRatio,1.75) performance.
Could also add tree branches? no.
Add some bushes low-poly around clearing radius 5-10 maybe Icosahedron clusters.
Grass tufts: InstancedMesh of ConeGeometry(0.16,0.8,3) maybe 260, placed random r 3.5-14 except fire, colors dark green. A cone as grass blade isn't ideal; use flat triangle geometry custom crossed blades. Could create BufferGeometry of 3 triangles around center, each blade; InstancedMesh 300, random rotations/scales. This adds visual detail. But first paint visible at night with moonlight. Grass color 0x314632. We can create a simple blade geometry with vertices for 3 vertical triangles oriented 120. Use MeshStandardMaterial side DoubleSide flatShading. Instanced. Nice.
Fire glow ground: PointLight plus a Sprite glow at fire maybe canvas radial texture, plane? Sprite always facing, could appear as a blob in 3D, not ideal. Add horizontal CircleGeometry at y=.05 with MeshBasicMaterial transparent map radial gradient blending Additive, color orange, opacity .4, depthWrite false. This simulates warm pool. Rotate -PI/2, scale maybe 5. Add two concentric circles. But actual point light already illuminates ground if material roughness and light. The warm point should visibly flicker. Need likely enough intensity. Since physically correct lighting in Three r170: PointLight intensity 100 and decay 2 yields irradiance at 3m ~11 lux, quite bright. Use 85.
Ambient: HemisphereLight intensity 0.45 maybe enough.
Campfire logs lit. warm light positioned y1.7.
Animated flames: We can use several custom geometry "tongues". Let's design custom flame geometry:
- ConeGeometry(0.5, 2.6, 5, 4, true) with openEnded false.
- vertex colors: y normalized: bottom 0: orange 0xff4d0a; .4: 0xff8a16; .75: 0xffd43b; 1: 0xfff3a0. But cone tip color. MeshStandardMaterial({vertexColors:true, emissive:0xff5a00, emissiveIntensity:1.4, transparent:true, opacity:.92, roughness:1, flatShading:true, depthWrite:false?}) If depthWrite false can layering weird but nice. If opacity .95, depthWrite true perhaps better. Outer flame red-orange. Inner smaller cone with bright yellow. Point light flicker drives.
Could instead build flame sheet using BufferGeometry with 7 sides and 4 rings, distorted:
rings t [0,.25,.55,.8,1], radii [0.45,0.5,0.35,0.18,0], center offset [0,0,0.1,-.1,.3] per flame. This yields angular low poly. For each ring sides 6, triangles. Normals compute. Vertex color by y. Outer each with additive? MeshStandard.
Need animate shape. Store base positions for each flame mesh; each frame update position attribute y or x based sin? Could update geometry vertices each frame for ~8 flames x ~36 vertices =288, fine. But scale and rotation simpler. Requirement animated flames, obvious if flickering tongues scale. We'll animate scale x/y and rotation each frame, plus light intensity flicker. Some shapes leaning.
Outer flames: 7 custom low-poly geometry variants (radius,height) each. material different? If using vertexColors, create per flame but share materials orange and yellow. Each geometry with positions. We can update base positions maybe. To make inner bright, add 3 small cones.
Also glowing coal bed: 8 small dodecahedrons red-orange emissive near ground, randomly pulse.
Sparks:
- positions 90; color; sizes cannot vary with PointsMaterial. Maybe custom ShaderMaterial points to size attribute and alpha per life. But PointsMaterial with vertexColors and a canvas radial sprite map. We can update colors intensity based age; points stay same size. Fine. size .18.
- Use BufferGeometry with position/color attributes, dynamic draw.
- Spawn from random ring at y1, velocity upward .7-1.6, drift.
- Update life, pos x += vx dt + sin; y += vy; z += vz; if age > life or y>8 reset. Color set HSL? Set from pale yellow to orange to red with brightness fade. vertex colors only RGB no alpha, but blending + map + transparent. Opacity uniform global. Old particles shrink? PointsMaterial fixed. Could set color to black as fade, additive blending so black invisible. Yes fade color scale by (1-t). Need update each frame.
- Add embers occasionally.
Smoke: group of 7 low-poly translucent icosahedron meshes, rising slowly and fading; shared material opacity .08, gray. Animate positions and scales. Fog plus smoke. Not required but atmospheric. Could add.
Additional UI: Minimal top-left title "NIGHTFALL / FOREST CLEARING", bottom-left hint "Drag to orbit · Scroll to zoom", maybe a small badge "LIVE • 42 trees". Need visually polished but not obscure. Overlay CSS with pointer-events none. Subtle vignette via radial gradient pseudo overlay. Maybe top-right "NIGHT 01" but no need. User just asks scene. Add title tasteful. Ensure in 375 responsive, smaller.
Need no console errors. Must handle WebGL context? Should. Import map exactly as given. Use `<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>` exactly. Script module import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; allowed URL via prefix. No other CDN.
We need output exactly one fenced code block. No explanation.
Let's deeply reason all geometry, lighting, rendering to avoid bugs.
Scene setup:
```
const renderer = new THREE.WebGLRenderer({antialias:true, alpha:false, powerPreference:'high-performance'});
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.75));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.05;
document.getElementById('scene').appendChild(renderer.domElement);
```
Maybe three v0.170 outputColorSpace default SRGB, setting fine.
Scene background color 0x020712 fog new FogExp2(0x07121c,0.024). But with sky shader sphere, background irrelevant. Fog color should match horizon ~0x08131f. `scene.fog = new THREE.Fog(0x07111d, 14, 66);` linear fog clearer at distance. Distant trees fade. At 40 radius fade significant. Use FogExp2 density .028 means at 40 factor exp(-(1.12)^2)=.285 visible? Fog factor 1-exp(-density^2 distance^2)=.71. Good. At moon distance 45 gets fog but set fog false. Trees fade.
Camera fov 48 maybe. Position (9.5,6.3,11.5), target (0,1.2,0). Distance 15.8. minDistance 6, max 30. OrbitControls maxPolarAngle = Math.PI/2.12 (~85deg) or 1.45 (<90) so camera stays above ground. minPolarAngle .35. enableDamping .06. autoRotate true? Requirement OrbitControls look around. We can enable slow autoRotate maybe nice but user control; `controls.autoRotate = true; autoRotateSpeed=0.25;` Then it moves continuously. User can drag; on pointerdown set autoRotate false maybe. But judges screenshot after 3 sec; static composition fine. Auto rotate could alter but slowly. Could add subtle autoRotate initially and stop on interaction to showcase. But no need; if camera doesn't move scene still animated. Maybe `controls.autoRotate = true` and on start set false. This is accepted. It might rotate the moon out? 3 sec angle .4 deg no. Add.
Ground:
Create `PlaneGeometry(100,100,40,40); geometry.rotateX(-Math.PI/2);`
position attr count 1681. For each:
x = getX, z = getZ. radial = hypot. h = (rand-.5)*1.1*smoothstep(3,24,r) + maybe sin... Central r<8 h=0. Need all plane y0. At tree ring perhaps hills. We need not create hills that float logs etc. Tree base ground height function should correspond. To simplify all ground flat with subtle vertex color only; low-poly triangles visible via color variation and flat shading. But flat normals all same if flat plane, so color variations create facets. Vertex colors can vary independently. We can avoid displacement entirely. `vertexColors` and random per vertex yield patchwork triangular lowpoly. But adjacent shared vertices with indexed geometry and flatShading true should compute face normal, color interpolates; good. Ground plane huge, triangles 2.5m. center patch.
Could use non-indexed geometry and assign same color per triangle to stronger low poly. `geometry.toNonIndexed()` then for each triangle assign one color random based center. But ground material flat shading and face color. We can make `PlaneGeometry(100,100,32,32); rotate` then `toNonIndexed`; get pos count 6144. For each triangle choose color based radial center and random. vertex colors per triangle. That's ~6k triangles, fine. Center clear. However central circle covers r<7, outside triangles.
Colors: base around clearing `0x203027`; at edges `0x101d1c`; patch variation.
Need ensure color visible with lighting. Vertex color sRGB? THREE.Color(hex) converts to linear automatically? `new THREE.Color(0x...)` stores linear if ColorManagement enabled. setXYZ. yes.
Ground material: `MeshStandardMaterial({vertexColors:true, roughness:1, metalness:0, flatShading:true})`; color 0xffffff. `receiveShadow=true`.
Maybe add dirt patch around fire circle radius 3.2 in warm dark brown, but point light makes.
Clearing disk CircleGeometry(7.8, 28), material color 0x2b3428, roughness1, flatShading true. MeshStandard. rotate -PI/2 at y .015. It will receive point shadow. Circle triangles fan share center; no polygon offset maybe z-fighting only where plane y=0, y .015 enough. Slight polygonOffset true.
Fallen logs outside: Need orient cylinders. helper:
```
function makeLog(a,b,r,color,charred=false) {
const start=new Vector3(...a), end...
const len=...
const geom = new THREE.CylinderGeometry(r*.9,r,len,8,1,false);
const mesh = new THREE.Mesh(geom, mat);
mesh.position.copy(start).add(end).multiplyScalar(.5);
mesh.quaternion.setFromUnitVectors(new Vector3(0,1,0), end.clone().sub(start).normalize());
...
}
```
For charred ends, add end circles using CircleGeometry(r*.9,8), material dark 0x221710, position at endpoint + direction*0.006, quaternion align z axis to direction. CircleGeometry normal +z; setFromUnitVectors(z, dir). One at each end. But group transform complexity. Could create group with cylinder along y and end circles at ±len/2; set group position/quaternion. Ends local y. Circle geometry rotateX? Circle normal +z; to face +y, rotateX(PI/2) gives normal? +z rotated x +90 -> -y; -90 -> +y. For top y=+len/2 want +y use rotateX(-PI/2). bottom -y use rotateX(PI/2). Add. group quaternion from y to direction. Good. Material charred near fire.
Campfire logs positions:
1 from (-1.25,.35,-.85) to (1.15,.45,.75)
2 from (1.15,.30,-.95) to (-1.25,.42,.85)
3 from (-.65,.62,-1.3) to (.65,.6,1.3) maybe.
At y .3- .6, flame base y .5. Point light y1.4. Stones ring radius 2.15. logs cross center but fire could visually obscure. Good.
Fire base coals: dodeca/icosa small, emissive. 10.
Stone ring: 14 rocks `IcosahedronGeometry(1,0)` scaled .35-.55, position radius 2.15, y .25, rotation. dark gray with color variation. Need shape partially embedded. cast/receive.
Outer rocks: 18 positions random radius 3.8-11, avoid camera direct perhaps okay; scales .3-1.2. Use 3 shared geometry variants Icosahedron detail0, Dodeca detail0. Could use flatShading. Materials 0x45505a etc. Some low rocks.
Fallen logs around fire: 3 at radius 5-8, lengths 2.5-4, endpoints oriented. dark bark. Include a broken stump maybe cylinders.
Trees:
We can use `InstancedMesh` with shared unit geometries:
- trunk geom `new CylinderGeometry(0.13,0.25,1,6,1)` maybe top .13 bottom .25, height1. transform scaling x,z by scale, y trunk height. Instanced color.
- cone geometry `new ConeGeometry(1,1,6,1)`, translate? Its origin centered, spans -0.5 to +0.5. Position accordingly.
- foliage lower radius 2.0*s, height 3.5*s, position y=3.6*s? trunk height=3.6*s, from ground; cone center at trunk height? Need cone bottom at 2.5s and apex6s. Lower cone height3.6 center4.3. Middle cone radius1.55s h3.0 center5.8, bottom4.3 top7.3. Top radius1.05s h2.4 center7.1, bottom5.9 top8.3. total 8.3s.
Trunk visible from 0 to3.6.
Maybe tree scale random .75-1.35, positions r14-42. Height 6-11. Fine.
Count 48. Random radial distribution with maybe cluster: angle random, radius 14 + pow(rand,.7)*30. Avoid direct foreground? camera inside ring r15, so trees at r14 could be close around camera. Camera r15.3, so some trees between camera and center if positions opposite? If tree at angle same as camera radius14, camera radius15 behind it outside looking inward - no obstruction because tree behind camera. Tree at angle opposite is beyond center, forms background. Tree at angle 90deg side. Fine. If near camera but offset can appear edge. Good.
But initial camera position r15.3 and tree at r14 could be behind camera mostly. okay.
To make clearing clearly read, min radius 13.5 and max 42. At camera r15, inner trees surround camera. The fire appears central. Moon at (-15,21,-25) behind fire. Trees can frame.
Tree placement random with "not on fire" radius > 12. Maybe some at r9 to provide near forest? But camera inside? Requirement around clearing. Clearing radius 8. Trees start 11.5. At camera r15, no issue. Use min 12.5. Then 46.
Need compute matrices:
```
dummy.position.set(x,0,z);
dummy.rotation.y=rot;
dummy.scale.set(s,1,s); // trunk geom radius base .25 * s etc
dummy.updateMatrix();
trunks.setMatrixAt(i,dummy.matrix)
```
For trunk geometry built with radius fixed, scaling x=s, y=trunkHeight, z=s. Cylinder unit height1, position y=trunkHeight/2.
Foliage scaling radius values times s, height values times s. position y center* s.
Use dummy.scale.set(r*s,h*s,r*s), dummy.rotation.y=random.
setColorAt with Colors. Need `instanceMatrix.needsUpdate=true`; `instanceColor.needsUpdate=true`. `castShadow=true; receiveShadow=true; frustumCulled=false` perhaps instanced bounding sphere default computed from matrices? `computeBoundingSphere()` after update to include instances; in r170 InstancedMesh computeBoundingSphere available. call.
Material `MeshStandardMaterial({color:0xffffff,roughness:1,metalness:0,flatShading:true})`. For foliage maybe emissive slight 0x061008? no.
Tree colors dark greens `0x16352c`, `0x1a4032`, `0x12302a`, `0x214334`; moon light should catch facets.
Trunk `0x3a2d25`, etc.
Could add branches not needed. At night low poly.
Moonlight directional:
```
const moonLight = new THREE.DirectionalLight(0x9db8e0, 1.2);
moonLight.position.set(-16,28,-20);
scene.add(moonLight);
```
Not cast shadows to preserve. Hemisphere 0x536f8c,0x10150f,0.55. Ambient maybe .12. Since materials dark and physically correct, directional 1.2 enough. ACES exposure 1.15.
Warm point intensity maybe 90, distance 22, decay2. At y1.6, ground r2 irradiance 90/4=22.5, good. At 8m 1.4 subtle.
Moon sphere:
`SphereGeometry(2.1,10,7)` at (-18,22,-30). Since camera orientation center, let's ensure on screen. Camera (9.5,6.2,11.5) looking at (0,1.3,0). Forward normalized (-.596,-.31,-.72). Moon vector from camera (-27.5,15.8,-41.5), dot forward =16.4-4.9+29.9=41.4; magnitude52.3 => cos .79, angle37.7°. Vertical FOV 48 half24; horizontal FOV ~66 half33. Moon angle combined may be outside vertical: direction vertical component 15.8; camera up orientation? Up roughly (0.16,.95,-.19); dot moon normalized with up ~ -? vector norm (. -0.526,.302,-.793) dot up= -0.084+.287+.151=.354, vertical angle20.7°, within24 but near top. Horizontal right vector roughly (.77,0,-.636), dot= -.405+.504=.099 angle5.7. So moon appears near top 20.7°, screen y ~14% from top. Nice. Maybe y18 => 16°. Use moon (-18,20,-30). Sphere 2 radius angular 2.2°, visible.
Glow sprite behind at same pos with scale 9. `SpriteMaterial({map:glowTex,color:0xbcd8ff,transparent:true,opacity:.35,depthWrite:false,blending:THREE.AdditiveBlending,fog:false})`. Sprite faces camera.
Moon sphere MeshBasicMaterial color 0xe7efff, fog false. Maybe full. Add crater patches as 5 small flattened circle meshes attached on facing camera? Hard orientation. Could use a canvas texture moon with craters generated; but user forbids external images only, canvas fine. Make CanvasTexture 256x256 with radial gradient and random dark circles, then use on Sprite? Then moon appears always face camera, no 3D but okay. However requirement low-poly scene, full sphere better. Can add a few small darker sphere spots positioned on surface relative to visible face. Not necessary.
Stars: Generate 900 points on sphere radius 80, y > 3. But sky sphere radius120, camera inside; fog false. PointsMaterial size 0.22, sizeAttenuation true? If true, size scales with distance/height? PointsMaterial size is world units, so at radius80 size .22 pixels ~1 maybe. `sizeAttenuation:false` size .8 pixels? With perspective, sizeAttenuation false yields size in pixels? PointsMaterial vertex shader `gl_PointSize = size;` when false, yes size pixels maybe 1.2. Use `size:1.4, sizeAttenuation:false, transparent:true, opacity:.85, vertexColors:true, depthWrite:false, fog:false`. 900 points. Color varied blue/white with brightness. But all same size. Could have two star sets: 650 size1.0 and 110 size2.2 for brighter. Positions random sphere upper half. Need sky sphere rendered before and not depth occlude: sky material depthWrite false, side back, renderOrder -100. Stars depthTest true; sky doesn't write depth so visible. Ground occludes lower stars.
Position distribution:
```
theta = rand*2PI, phi = acos? y positive.
r=90; x=sqrt(1-y^2)*cos; y=0.1+0.9*rand?
```
Use `u=rand`, `y=0.08+0.92*Math.pow(rand, .7)`, angle. At horizon y .08. But camera inside and points all around. stars behind ground if below line.
Colors buffer with Color white/blue random multiply.
Also add a few cross-shaped? no.
Sky shader: custom, no external.
```
const skyMat = new THREE.ShaderMaterial({
side:BackSide, depthWrite:false,
uniforms:{top:{value:new Color(0x020611)}, bottom:{value:new Color(0x142334)}},
vertexShader:`varying vec3 vPos; void main(){vPos=position; gl_Position=...;}`,
fragmentShader:`... float h=clamp(normalize(vPos).y*.5+.5,...); vec3 c=mix(bottom,top,smoothstep(.0,.7,h)); ...`
});
```
Sphere at y maybe0. Since camera can move but within120. vPos local. Fine.
Maybe moon should be behind sky? Sky sphere radius120, moon radius distance42 inside, stars radius90. Depth order: sky depthWrite false but depthTest true. Sky drawn maybe opaque render first; since depthWrite false, moon/stars later show. Rendering order opaque by material/program maybe sky first? opaque sorted front-to-back; sky at far but depthWrite false. Moon also opaque; if moon drawn before sky, sky could cover it even depthWrite false because sky is opaque and renders after, depth test sees moon depth? Moon writes depth, sky fragment at farther depth fails depth test -> moon stays. Actually moon writes depth, sky behind fails. Good. Stars Points transparent after opaque, depthTest with moon; fine.
Sky fog false because ShaderMaterial doesn't include fog.
Ground extent 120? Plane 100, sphere 120. Horizon visible due fog. camera max 30, plane extends 50 all directions. Good.
Fire glow Circle with CanvasTexture:
```
const glowTex = makeRadialTexture(['rgba(255,120,20,.6)',...])
new MeshBasicMaterial({map:glowTex, color:0xff7a22, transparent:true, opacity:.5, blending:AdditiveBlending, depthWrite:false, fog:true})
```
CircleGeometry radius? Texture radial. Mesh at y=.025 rotate. With additive glow on ground visible warm pool, but map itself alpha edges. Circle receives? Basic.
Also maybe sprite at fire center for glow bloom but may occlude; use small sprite scale3 opacity .15.
Fire light shadows. Need pointLight.shadow.camera.near=.1, far=22; mapSize 1024. `shadow.bias=-0.001; normalBias=.04`.
PointLight castShadow on. In three, PointLight shadow uses cube map; each frame 6 renders. Scene moderate. On mobile 375 may still okay, but maybe pixel ratio. 46 instanced meshes only 4 draw calls in each shadow pass. fine.
Animated fire implementation:
Create `flameGroup`. Use custom geometry function:
```
function createFlameGeometry(radius,height,seed) {
const sides=6; const rings=[{t:0,r:.8},{t:.18,r:1},{t:.5,r:.68},{t:.78,r:.3},{t:1,r:0}];
positions = [];
colors=[];
indices ...
for ring ... each side angle + random jitter fixed
center x = sin(t*...)*.1*radius? centerZ...
r=radius*ring.r*(1+ jitter)
y=t*height
...
```
Need tips: ring r=0 but if six duplicate vertices at tip, degenerate triangles but okay. We can just ConeGeometry easier. Custom gives nicer shape. Use nonindexed triangles to compute flat normals; assign color per vertex by vertical gradient. Generate triangle vertices manually with each ring pair, but rings with sides and wrap. Colors each vertex based y. For flat facets, `geometry.computeVertexNormals()` on nonindexed gives per-face normals. Build positions array tri by tri, colors. For each quad, two tris. At tip ring duplicate six, degenerate one triangle but mostly.
Geometry local base y=0. For each flame offset and rotate.
Gradient color function: bottom deep red, middle orange, top gold. Outer flame use bottom #ff3a05, mid #ff6a0a, top #ffb51b. Material vertexColors true, emissive white? `emissive:0xff4d00, emissiveIntensity:1.2`. Since emissive also uniform color not vertex gradient; base color vertex. Maybe set emissive 0xffffff intensity .5 to brighten all.
`MeshStandardMaterial({vertexColors:true, emissive:0xff5a08, emissiveIntensity:1.35, transparent:true, opacity:.92, depthWrite:true, roughness:.9, flatShading:true, side:DoubleSide})`.
Emissive tint makes top not yellow though vertex base yellow multiplies? emissive is added independent. Could use `emissive:0xff6a00`; okay.
Inner flame material `MeshBasicMaterial({vertexColors:true,transparent:true,opacity:.95,blending:AdditiveBlending,depthWrite:false})` with bright yellow; add 3. Basic appears very bright under tone mapping? toneMapped false maybe `toneMapped:false` can overbright. Keep.
Outer flames maybe MeshBasicMaterial too for stylized glow, but requirement point light handles lighting. Use MeshStandard.
Store flame objects with phase, baseScale, basePos, baseRot. Animate:
```
const pulse = .86 + sin(t*8+phase)*.13 + sin(t*13+phase*2)*.05;
mesh.scale.set((1+sin*.1)*baseSX, pulse*baseSY, ...)
mesh.rotation.z = baseRot + sin(t*3+phase)*.12
mesh.rotation.x = ...
mesh.position.y = baseY + (pulse-.9)*.1
```
Need flames vary in width; initial scale x1. For outer 7, random radial offsets radius .55; base heights 1.6-3.0. center larger 3.0. At center overlap, bright.
Add inner 4 height 1-1.8 radius .25. Material additive.
Maybe flames should not look solid cones. Custom shape handles.
Fire glow scale animate.
Coal emissive pulse: store coal meshes, animate material emissiveIntensity? Shared material means all same. Could use individual materials or one shared and animate global. Shared material intensity 1.4 + flicker. Good.
Sparks system:
Generate arrays length 90. Each spark initial life random; update.
Store `sparks` objects {pos Vector3, vel Vector3, age, life, maxLife, phase}. Buffer positions and colors.
Initialize resetSparks(s, initial):
```
s.age = initial ? rand*life :0;
s.pos.set(cos a*rand*.7, .8+rand*.5, sin ...);
s.vel.set((rand-.5)*.35,.7+rand*1.2,(rand-.5)*.35);
s.life=1.4+rand*2.6;
```
If initial, pos should be distributed along path maybe; easier reset all at base, screenshot after3 sec shows rising stream.
Update dt capped .05:
age += dt; if > life or y>8 reset; pos.addScaledVector(vel,dt); vel.y += .08dt (slows? maybe buoyancy; keep), pos.x += sin(t*2+phase)*.08dt; colors:
f = age/life; color = new Color().setHSL(lerp .12 to .03?), brightness (1-f). We can pre-store Color and multiplyScalar. `color.setRGB(1*(1-f*.4), .65*(1-f), .12*(1-f))` then multiply fade? Use `lerpColors(bright, dark, f) * (1-f*0.7)`. Additive black at end.
PointsMaterial map radial texture, alphaTest .01, size .18, vertexColors, transparent, blending Additive, depthWrite false.
Need `map.colorSpace = THREE.SRGBColorSpace` for texture maybe.
Smoke:
7 `Mesh` IcosahedronGeometry(1,0) shared; material `MeshLambertMaterial`? transparent opacity .07, color 0x71808a, depthWrite false. But each needs varying opacity for fade, individual material or scale only. Shared opacity .08 and scale fade in/out, position. As it rises, scale grows. 7 objects. Could use Sprite but low poly. Store phase. Update y = 2.4 + ((t*0.28 + phase)%1)*6; angle drift; scale .3+f*1.1; rotation. Since shared material, opacity constant, looks ghostly. Point light won't affect MeshBasic if using standard. Use MeshStandard transparent; lighting.
Could skip if performance. Add 6.
Fire glow ground radial CanvasTexture. Need make textures:
```
function radialTexture(stops) { const c=document.createElement('canvas'); c.width=c.height=128; ctx... gradient...; new CanvasTexture(c); }
```
For glow, center rgba(255,200,80,.9), .2 rgba(...), 1 transparent. For spark center white/yellow.
Stars might get washed by sky. Fine.
Additional atmospheric fireflies? Not required; maybe 18 tiny green points around clearing drifting, could distract. Keep scene focused.
Overlay:
```
<div class="hud">
<div class="brand"><span class="eyebrow">PROCEDURAL NIGHT</span><h1>Emberlight</h1><p>Forest clearing · midnight</p></div>
<div class="hint"><svg...> drag etc</div>
</div>
<div class="vignette"></div>
```
No external fonts. Use system. CSS inline. Title top-left. bottom-right small coordinates? Could show "Drag to explore / Scroll to zoom". On small screen hide subtitle maybe.
Need canvas aria label.
Could add loading veil? Not needed; import network may take 1 sec. First paint screenshot 3 sec. Could have a CSS background so before load appears dark. `#scene` canvas. Overlay immediately visible. But meaningful scene after loads. Fine.
Potential importmap handling in sandbox? CDN allowed. Use exact script. No other URLs. Good.
Important: requirement "Load Three.js only through this import map (do not use any other URL or version)" We obey. Use jsdelivr exactly. Could OrbitControls module internally imports 'three', resolved. yes.
No use of external images.
Ground shadows: point light casts dynamic shadows of flame? flame castShadow false. Logs/stones/trees cast. Need light position and point shadows. `fireLight.castShadow = true`. Renderer shadowMap autoUpdate true. At least obvious. At night, directional not casting; warm point shadow under logs.
OrbitControls camera stays above ground: `maxPolarAngle = Math.PI/2 - 0.08` ensures target polar less than ~85°, camera y > target + positive. `minPolarAngle=0.2`. target y=1.1, at max polar, camera y=1.1+distance*cos(1.49)>1.45, above ground. Good. `enablePan=false` maybe target fixed, camera doesn't go below. requirement look around. Orbit limits minDistance 6 maxDistance 27. `maxPolarAngle=Math.PI*0.47` (84.6). `minPolarAngle=0.35`. target fixed (0,1.2,0). enablePan false.
Camera initial: (8.8,5.5,11.3), fov50. Fire center lower? target y1.2, good. Moon visible? recalc camera forward (-8.8,-4.3,-11.3) norm .129? mag15.2 =>(-.58,-.283,-.744). Moon vector (-26.8,14.5,-41.3) norm51.6 =>(-.519,.281,-.8). dot=.301-.08+.595=.816 angle35.3. vertical up roughly(.175,.959,-.225), dot=-.091+.269+.18=.358 angle21, within vertical half25. top 8%. Could lower moon y17 gives angle19. Moon at (-17,17,-28). Use y19? Let's target 18.5. vertical vector (-25.8,13,-39.3) norm49.2 =>(-.524,.264,-.799); up dot -.092+.253+.18=.341 angle19.9, screen y around10%. Glow scale 9 can fit top maybe. Good. Title top-left might overlap moon? moon near center top, okay.
Foliage rings can block moon, but that's atmospheric. Ensure some gap around moon azimuth maybe skip tree positions projected near moon. Could place tree ring dense but moon behind trees might partially hide, nice but maybe want visible. Position moon (-18,18,-30), background trees likely there. At distance30, trees height 8; line of sight to moon above tree, so visible.
Fog with sky sphere. Distant trees fade.
Low poly aesthetic: flatShading on all. Add wire? no.
Fire ring rocks around center. Logs.
Maybe create a central ash disk `CircleGeometry(1.9,16)` with dark material and stones. Flames at y .45.
Camera near fire enough to show. At 1280x800 aspect 1.6, fire perhaps central 30% height. FOV50 distance15 gives fire 3 high ~11% height. Good. Ground clearing fills lower half. Moon top.
Maybe initial camera should be closer (10.5,5.6,12.5 distance16.5) to showcase. Current 15.2. Fine.
Additional foreground trees: min r13.5, camera r14.35, inside. At sides and back. clearing radius 8. Maybe tree line seems distant but ring close. Camera can orbit within clearing, not through trees. maxDistance27 would move camera outside tree ring and through trees; OrbitControls maxDistance 24 maybe. Camera can go into trees at r24. Set maxDistance 21, tree min14, potential but user can zoom. Could set maxDistance 18 to stay near clearing (camera max r18, some trees). Requirement limits camera above ground only. Maybe maxDistance 24. Fine.
We can keep tree positions r12.8-40 and camera r14.3. Some trunks very near viewport edges, framing. Ensure no tree at exact camera location and collision: while generating, if distance to camera <3, shift angle/r. Also avoid central. Place random.
Pseudo random deterministic:
```
let seed=92841;
function rand(){seed = (seed*1664525+1013904223)>>>0; return seed/4294967296;}
```
Use for scene.
Outer trees count 52 perhaps requirement at least 30. 52 instanced. Nice dense forest. At 52, shadows manageable. Need only 30 visible? likely. ring 12.5-42 with area huge; angular density near inner ring high. Some.
Could create two rings: 38 random. count48.
Potential shadow performance on point light with InstancedMesh. Point shadow rendering scene 6 times * (48 trees instanced 4 draws + ground + logs + rocks) ~50 draw per pass =>300 draw, plus main ~50, okay. 60fps maybe. At 3 sec no issue. Pixel ratio 1.5. Use shadow map 768? mapSize must power of 2 ideally 1024. Mobile 375 still renders 1280? viewport actual. Fine.
Could restrict castShadow to trees near clearing? InstancedMesh all instances cast, cannot per instance. But all 48. point shadow camera far22 includes radius22, ~30 trees. Each instanced draw anyway. fine.
Rocks and logs individual 30 draw calls. Total ~100. okay.
Let's think exact code for custom flame geometry and coloring.
```
function makeFlameGeometry(radius,height,leanX,leanZ){
const sides=6;
const levels=[
{t:0,r:.75,cx:0,cz:0},
{t:.16,r:1,cx:leanX*.08,cz:leanZ*.08},
{t:.48,r:.66,cx:leanX*.35,cz:leanZ*.35},
{t:.76,r:.30,cx:leanX*.72,cz:leanZ*.72},
{t:1,r:0,cx:leanX,cz:leanZ}
];
const vertices=[];
for i<levels.length-1:
for s<sides:
const a0=s/sides*2PI, a1=(s+1)/...
// radius jitter based s precomputed? deterministic local rand
p00 = level i at s with radius * (0.85+0.15*rand pattern)
...
```
We need consistent ring vertex radius for quad edges; define arrays point[level][side] before. At level last all same center.
Then triangles [p00,p10,p11], [p00,p11,p01] where next level.
Positions push. Colors based average y perhaps per vertex using colorForT. For flat faces, color per triangle can be uniform based avg t, giving faceted bands. But low-poly triangles with same color might look polygon. Use per vertex gradient.
`geometry.setAttribute('position', Float32BufferAttribute(vertices,3)); setAttribute('color'...)`; computeVertexNormals. Since nonindexed, normals flat.
Color function:
```
const c = new THREE.Color();
function flameColor(t) {
if(t<.3) return new Color(0xff3c06).lerp(new Color(0xff7b08),t/.3)
...
}
```
Use hex converted linear.
Bottom maybe 0xff2800; at .35 0xff6a00; .7 0xffb817; 1 0xffe36a.
Material Standard with vertex colors and emissive. But base color in linear; under ACES.
Animate flame mesh scale. geometry built actual height, so scale y around 1. `mesh.scale.set(1,1,1)`. Base positions.
Outer material transparent .94 depthWrite true, blending Normal. Flame intersections with logs. good.
Inner geometry use same function smaller and material `MeshBasicMaterial({vertexColors:true,transparent:true,opacity:.95,depthWrite:false,blending:THREE.AdditiveBlending})`. toneMapped true default; additive.
Outer `MeshStandardMaterial({vertexColors:true,emissive:0xff3a00,emissiveIntensity:1.1,roughness:1,transparent:true,opacity:.93,side:DoubleSide,depthWrite:true,flatShading:true})`.
If transparent depthWrite true may occlude inner incorrectly. Outer transparent render after opaque, but inner transparent also. Rendering transparent sorted by distance. Could simplify outer opacity1 opaque emissive; low poly fire crisp. Inner additive will render after and visible if inside? Depth test: outer writes depth; inner inside outer might fail depth test because outer surface closer to camera, so hidden. So inner not visible except above. We want inner visible. Set outer depthWrite=false and inner depthWrite=false; render order outer1 inner2 maybe inner visible through outer. But transparency layering can look glowy. Set outer material transparent .9, depthWrite:false, `renderOrder=2`; inner order3. Both depthTest true. Inner can render over outer because no depth write from outer; other scene depth can occlude. Good. Outer faces blend. Fine.
Alternatively use only outer + small core sphere emissive at base. But inner yellow at center can show with depthWrite false.
Flame castShadow false receive false.
Light flicker:
```
const baseIntensity=82;
fireLight.intensity = base*(.84+.11*sin(t*11.3)+.07*sin(t*17.7)+.05*sin(t*29.1));
fireLight.distance=20;
```
Also point light position jitter y .05.
`glowMat.opacity = .34 + .08*...` and scale.
Coal material emissiveIntensity.
Time from `clock.getElapsedTime()` but dt `clock.getDelta()` then elapsed accum. Use `const clock=new THREE.Clock(); function animate(){const dt=Math.min(clock.getDelta(),.05); elapsed+=dt;... requestAnimationFrame(animate);}`. In sandbox first screenshot 3sec.
Responsive:
```
function resize(){ const w=innerWidth,h=innerHeight; camera.aspect=w/h; camera.updateProjectionMatrix(); renderer.setSize(w,h); renderer.setPixelRatio(Math.min(devicePixelRatio, w<700?1.5:1.8));}
addEventListener('resize',resize)
```
Order set pixel ratio before setSize maybe. fine.
CSS canvas display block width100 height100.
No horizontal scrollbar: html body overflow hidden.
Could use CSS overlay film grain? Inline SVG data URI maybe external? Data URI okay but unnecessary. Use pseudo gradient.
Maybe add tiny "WEBGL • REALTIME" tag. Keep classy.
Let's assess color/lighting likely. Ground vertex color 0x1e2b22 under hemi .6 directional1.0, appears dark green. Fire pool via point. Sky horizon 0x0b1623. Trees foliage 0x143328, subtle. Moon light blue. This meets night.
Could add moonlit ground patches via directional. Directional 1.25 and hemi .7. Renderer exposure1.1. Should see. Need not too dark. Use colors a bit brighter: foliage 0x1d4133, 0x254b38; trunks 0x49352a. Ground 0x29372a. Night with clear fire.
Fog color 0x091420. Distant trees fade into sky.
Stars: 1000 points, some too many. size 1.2 px. The sky around top has ~400 visible.
Moon material MeshBasic 0xdbe8ff, no fog. Glow.
Need ensure point light shadow camera and renderer shadows includes `renderer.shadowMap.autoUpdate` default. yes.
Potential point light shadow artifacts from transparent flames not cast. logs etc.
Use ambient occlusion? no.
Let's think of tree positioning and moon view. Generate trees using deterministic rand, but we also use rand for geometry etc. Position radius:
```
const angle=rand()*TAU;
const radius=12.5+Math.pow(rand(),0.55)*30;
let x=Math.cos(angle)*radius,z=Math.sin(angle)*radius;
```
At radius12.5 exactly clearing boundary. Camera r14.3. Could have tree at camera-ish. Avoid `dist to camera <3.5`. Also no tree within 11.5 of fire. requirement not on fire obviously.
Maybe create near-ring inner trees at radius11.8 some. Initial camera at r14.3, trees inner to camera may appear around. Good.
But camera line to fire, trees opposite side (angle camera+PI) at r>12.5 will be directly behind fire at distance27. Good backdrop. Trees at other angles side.
At initial camera, outer ring trees at angle around camera but behind camera (radius>camera) won't show. Others visible.
46 trees enough.
Add clearing boundary bushes and fallen logs.
Code organization.
HTML:
```
<!doctype html>
<html lang="en">
<head>
<meta charset...
<meta name viewport...
<title>Emberlight — Midnight Clearing</title>
<style>...</style>
<script type="importmap">...</script>
</head>
<body>
<div id="scene"></div>
<div class="vignette"></div>
<header...>
...
<script type="module">
...
</script>
</body>
</html>
```
Importmap in head exactly. CSS uses no external fonts.
Maybe overlay initial loader? We can put:
```
<div id="loading"><span>Building the clearing…</span></div>
```
then hide after init. But screenshot at 3 sec hidden. If errors, loader remains; no console errors expected. No need.
Could add accessible description.
Let's detail seeded random:
```
let seed=0xC0FFEE;
function random(){ seed |= 0; seed = seed + 0x6D2B79F5 | 0; let t=Math.imul(seed ^ seed >>>15, 1|seed); t = t + Math.imul(t ^ t >>>7, 61|t)^t; return ((t ^ t>>>14)>>>0)/4294967296; }
```
Simpler LCG:
```
seed=(seed*1664525+1013904223)>>>0; return seed/4294967296;
```
JS multiplication exact under 2^53? seed*1.6m up to 7e15 >9e15? max 4.29e9*1.66e6=7.15e15 below 9e15 safe integer perhaps yes, addition safe. use Math.imul.
```
seed=(Math.imul(seed,1664525)+1013904223)>>>0
```
fine.
Hash for ground triangle color uses random sequential. Fine.
Ground toNonIndexed:
`const g = new THREE.PlaneGeometry(100,100,32,32); g.rotateX(-Math.PI/2); const geo=g.toNonIndexed(); g.dispose();`
For each triangle positions length positions.count =32*32*6=6144. For i +=3:
center x=(p[i]+p[i+3]+p[i+6])/3 etc. radius. base color:
```
const dark = new Color(0x10201c), mid = new Color(0x26382b);
const t=clamp((r-6)/38,0,1);
const col=mix(mid,dark,t).multiplyScalar(.8+rand*.35)
```
But vertex color multiply scalar can exceed. colors in linear. `col.multiplyScalar(0.78+rand*.35)`.
For r<8, base 0x2c392b maybe. Disk covers.
Set each of 3 vertices same col to hard triangle color. Flat low-poly.
Plane material with vertexColors. With indexed -> nonindexed and hard colors. Great.
Ground disk:
`CircleGeometry(7.6,32)` but center vertex triangles might smooth color no issue. Material flatShading. `polygonOffset:true, polygonOffsetFactor:-1`. y=.02. color 0x283529.
Maybe create irregular clearing edge rather than circle. Could use a `ShapeGeometry` with 32 irregular points radius 7.5-8.2, triangulated. MeshStandard, y .025. This looks natural. Build `THREE.Shape` with x,z? Shape uses x,y then rotate.
```
const shape = new THREE.Shape();
for i 0..31 angle, r=7.6+sin...; shape.lineTo(cos*r,sin*r);
shape.closePath();
const geo=new ShapeGeometry(shape); geo.rotateX(-PI/2);
```
ShapeGeometry normals after rotate? yes +z rotated -90 => +y? Rotation x -90 maps z to y? vector (0,0,1) -> (0,1,0) yes.
Material.
Add central ash circle radius2 at y .035 color0x231b16.
Rocks:
Shared geoms:
`rockGeo1 = new THREE.IcosahedronGeometry(1,0)`, `rockGeo2 = new THREE.DodecahedronGeometry(1,0)`.
Materials array MeshStandard color 0x4a5055 etc.
For each ring stone:
```
r=2.05+random*.22; angle; scale .32-.52 with y .55; pos y=.26*scale? geometry radius1, y scale .4, bottom if position y=.2*scale?
mesh.scale.set(s*(.8+...), s*.55, s*(...)); mesh.position.y=s*.28; (bottom -s*.28 + pos .28s =0)
```
rotation random.
Add shadows.
Outer rocks 16, scale .35-1.1, y=s*.45 maybe embedded. dark.
Could add 2 rock clusters near foreground.
Logs:
Bark material 0x3d2b20 roughness1 flat.
Camp logs charred: outer mesh material bark dark0x231b16, end material0x100c09. Add two thin band cylinders? not needed.
Place.
Fallen logs: helper `createLog(start,end,r,mat,burned)` creates group. CylinderGeometry unit length? each unique geometry maybe 6. Use CylinderGeometry(r*.85,r,len,8,1). Mesh. end circles. End cap color. Add to scene. cast.
Need ground logs at radius. Choose:
- (-6,0.4,2.8) to (-2.5,.55,3.8), r .4
- (5.5,.35,-4.8) to (8.3,.5,-3.6), r .35
- (-1.5,.35,-7.0) to (2.4,.4,-7.8), r .32.
Potential in camera view. good.
Stumps maybe not.
Trees instanced:
Trunk geometry unit cylinder radius? `new CylinderGeometry(.12,.24,1,6,1)` (top radius .12 bottom .24), translate? position at h/2. Flat.
Cone layers geometry `new ConeGeometry(1,1,6,1)`, translate? center 0. positions.
Set matrix:
```
const dummy=new THREE.Object3D();
function setInstance(mesh,i,pos,scale,rot) { dummy.position...; dummy.rotation.set(0,rot,0); dummy.scale...; dummy.updateMatrix(); mesh.setMatrixAt(i,dummy.matrix);}
```
Trunk scale (s,s*trunkH,s) since geometry x radius .24, height1. scale x=s; so actual trunk radius .24*s (reasonable). y=s*3.5. position y=s*1.75.
Leaves cone radius1 scale x=2.0*s, y=3.4*s, z same. actual.
Trees colors.
Trunk mesh `count=treeCount`, foliage `count=treeCount*layers`.
Maybe instance matrix rotation y affects cone symmetry little; add rotation z slight random? Cone facets orientation via rotation y. `dummy.rotation.set(0,rot, tilt?)`; trunk vertical.
For leaves, random y rotation and slight x/z tilt perhaps trees stylized. Use only y.
Set colors for foliage per layer. Need `mesh.instanceColor` exists after setColorAt. `mesh.instanceColor.needsUpdate=true`.
`mesh.computeBoundingSphere()` method in r170 yes. Could call if function exists to avoid error.
Material side default FrontSide.
To add variety, some tree scale and color. 48.
Maybe small pines instead of conical broadleaf? Cones stacked works.
Grass:
Custom geometry:
```
const verts=[];
for k=0..2:
angle=k*PI/3
const dx=Math.cos(angle)*.18,dz=...
verts.push(-dx,0,-dz, dx,0,dz, dx*.35,.75,dz*.35);
```
Actually a blade as triangle pointing up and leaning.
Need normals compute.
InstancedMesh 280 count.
Place r from 3.3 to 14.5, random. Avoid fire r<2.8. y=0.02. scale .5-1.3, rotation y random. Colors variations. `MeshStandardMaterial({vertexColors? instanceColor, color:0xffffff, side:DoubleSide, flatShading:true})`.
If using same material and instanceColor, okay.
Maybe grass could clutter; 240.
Fire stones/ground shadows.
Fire glow texture:
Canvas gradient:
```
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace=THREE.SRGBColorSpace;
```
CanvasTexture default flip. PointsMaterial map uses alpha from image; radial gradient with white alpha. PointsMaterial color multiplied by vertexColors. texture black? If radial gradient fill white with alpha; addColorSpace.
For ground glow use same but gradient warm? Make separate.
`glowTexture` white radial alpha.
Ground circle material map radial white and color orange.
`MeshBasicMaterial({map:glowTexture,color:0xff7624,transparent:true,opacity:.38,blending:AdditiveBlending,depthWrite:false,side:DoubleSide})`.
At y .04, size 8x? Circle radius4.8. Texture radial. Fog true default.
A point light already.
Moon glow uses glowTexture.
Sparks PointsMaterial map glowTexture, `size:.22`, alphaTest:.01, transparent, vertexColors, blending Additive. `depthWrite:false`. `sizeAttenuation:true`.
At distance15, size .22 world -> pixels maybe .22 * height/(2*tan25)=.22*800/0.93=189? Wait point size formula in perspective: scale = height/2; gl_PointSize = size * (scale / -mvPosition.z). So size .22 world *400/15=5.9 pixels. Good. Maybe size .13 ->3.5 px. Use .16.
Moon glow sprite size8 world -> pixels huge 8*400/45=71px. good.
Fire light intensity and shadows. Maybe add secondary subtle point no shadows? no.
Sky stars positions radius85. With fog false. Camera max maybe 21; stars far. Fine.
Star color:
```
const c=new Color();
c.setHSL(.55+rand*.08,.2+rand*.3,.65+rand*.35)
```
Points material vertex colors multiplies white. bright. star bright set.
Potential star points behind moon. okay.
Add "constellation" not.
Controls:
```
controls.target.set(0,1.35,0);
controls.enableDamping=true;
controls.dampingFactor=.055;
controls.minDistance=6.5;
controls.maxDistance=24;
controls.minPolarAngle=.28;
controls.maxPolarAngle=Math.PI*.47;
controls.enablePan=false;
controls.rotateSpeed=.55;
controls.zoomSpeed=.7;
controls.autoRotate=true;
controls.autoRotateSpeed=.18;
controls.addEventListener('start',()=>controls.autoRotate=false);
```
When autoRotate enabled, update each frame with dt? OrbitControls.update(deltaTime) supports autoRotate framerate-independent in newer version? r170 update(deltaTime) optional. Call `controls.update(dt)`.
We might want camera remains above ground even max polar. good.
Keyboard not used so no "Click or press key" needed (game rule only).
Overlay CSS:
```
:root color-scheme:dark;
body background:#03070d;
.hud position fixed inset:0 pointer-events:none;
.title position absolute top:32 left:38; ...
.eyebrow letter-spacing .24em...
h1 font-size clamp(22px,3vw,38px) font-weight 300...
.line maybe.
.hint bottom 24 right 30 ...
.vignette background radial-gradient(ellipse at center,transparent 45%,rgba(0,0,0,.55) 100%); mix.
```
Avoid overlay darkening fire. Vignette edges only.
Maybe top-left title could overlap moon? title width, moon top center. okay.
Add bottom-left ambient reading "MIDNIGHT • 00:47" in monospace. Fine.
Instruction pill bottom right with mouse icon via CSS circles no SVG maybe.
No external fonts.
No use of localStorage.
Let's ensure no `window.open`.
Now check Three APIs:
- `renderer.outputColorSpace=THREE.SRGBColorSpace` yes.
- `THREE.PCFSoftShadowMap`.
- `MeshStandardMaterial.flatShading`.
- `InstancedMesh.setColorAt`.
- `PointLight.shadow.camera` for point is Orthographic? PointLightShadow camera is Perspective, near/far properties. Set `light.shadow.camera.near=.1; far=20;` yes.
- `light.shadow.mapSize.set(1024,1024)`.
- `light.shadow.bias`.
- `SpriteMaterial` fog property? Material has fog bool default true; set fog:false accepted.
- `PointsMaterial.fog` bool accepted.
- ShaderMaterial no tone mapping? It outputs linear but renderer tone maps only if shader includes chunks; custom shader won't be tone mapped unless `toneMapped`? ShaderMaterial default toneMapped true but custom shader doesn't include output fragment. Since sky very dark, okay. Color uniforms `Color` linear, output to sRGB? Without `#include <tonemapping_fragment>` and `colorspace_fragment`, renderer won't transform? ShaderMaterial may automatically inject? No, custom shader needs include. But the canvas is sRGB, values from uniforms linear appear dark if not converted. We can choose colors directly. Better use `THREE.Color(0x...)` which converts sRGB to linear; without output conversion, canvas gets linear darker. Could add fragment chunks:
```
#include <tonemapping_fragment>
#include <colorspace_fragment>
```
at end after gl_FragColor. ShaderMaterial supports includes. Use `gl_FragColor=vec4(color,1.0); #include...`.
In three r170 chunk name `colorspace_fragment` yes (since r154 `encodings_fragment` renamed colorspace_fragment). likely.
Could instead use a huge sphere MeshBasicMaterial with vertex colors gradient and fog false. Set geometry sphere radius120, color per vertex based y. MeshBasicMaterial({vertexColors:true,side:BackSide,depthWrite:false,fog:false}); no custom shader, correct output color/tone. Simpler and robust. Let's do that! Create SphereGeometry(120,32,16); set color attribute per vertex based normalized y:
- h = clamp(y/120 * .5+.5)
- horizon = Color(0x0b1826), zenith=Color(0x020611)
- blend smoothstep(0.35,.9,h)
- add slight blue.
Set vertex colors. Since indexed geometry shares vertices, gradient smooth. material Basic vertexColors side BackSide depthWrite false fog false. Render order -10.
Sphere receives no tone? MeshBasic participates tone mapping/colorspace, good. Set `toneMapped:false` maybe avoid ACES dim; but colors are specified display? If toneMapped false, color conversion still? Material fragment includes colorspace regardless. Tone mapping optional. We can use toneMapped:false for sky and stars/moon to retain brightness. MeshBasicMaterial has toneMapped property inherited. Great.
Stars PointsMaterial toneMapped false maybe bright. Moon MeshBasic toneMapped false.
Ground standard tone mapped.
Sky sphere at origin, camera inside. BackSide and depthWrite false. Material depthTest true. `frustumCulled=false`.
Sky color gradient:
```
const horizon=new Color(0x0a1724), zenith=new Color(0x02040b), mid=new Color(0x06101d);
h=clamp((pos.y/120+1)/2)
c= h<.5 ? horizon.clone().lerp(mid,h*2): mid.clone().lerp(zenith,(h-.5)*2)
```
At horizon y=0 h=.5, mid. Need lower hemisphere same. Fine.
Maybe add very faint aurora? no.
Stars are outside? radius90 < sky radius120. good.
Moon glow texture.
Far mountains? Requirement forest clearing only. Could add low-poly distant hills to improve horizon, but fog and trees enough. Maybe a ring of low-poly hill mounds? Could be nice: 14 large cones/icosahedron at radius55, dark. But not needed; might block star/horizon. Add a subtle distant hill ring with `ConeGeometry(7,10,6)` at radius55 and y0, dark, fog. 12. This adds depth. But scene already forest. Could add one polygon ridge using cone shapes. Nice low-poly. Yet more geometry. Let's add 14 distant mounds instanced? `ConeGeometry(1,1,5)` scale random 8-16 radius, 8-18 height, at r52-64. Because fog density .027 at 55 almost fully fog; silhouette faint. This enriches horizon. Use InstancedMesh count18. cast false. color 0x0b1822. Since sky sphere and fog, visible faint. Fine.
FogExp2 density 0.028 at55 factor 1-exp(-(1.54)^2)=.905 so only 9.5% visible. subtle.
Need ensure tree count visible beyond fog. at 40 factor .72.
Now "at least 30 low-poly trees placed randomly around the clearing (not on the fire)." Our count 48 instanced. Could mention in UI "48 pines" maybe. Use `48 TREES` badge. Since actual 48. Good.
Maybe set 52. Let's pick 48 exactly. Requirement at least30. UI "48 TREES · LIVE FLAME" maybe cheesy. Better title "Emberlight" subtitle "A midnight clearing". Bottom chip "48 low-poly pines • live flame". Fine.
Should we make tree forest "low-poly" obvious in daylight? Night colors and facets.
Maybe moon casts cool light; name moonLight. Directional from moon direction to origin: position(-18,24,-25), target origin. Add target to scene. Intensity 1.35, color 0x8faed2. No shadow.
Could enable directional shadow at low 1024 for dramatic tree shadows but point already. Not necessary. Requirement warm light casts shadows specifically; our point does.
Maybe point shadow not obvious if no ground objects between light and ground? Logs/stones cast. Fire light inside logs, shadows radial. good.
Renderer shadow map PCFSoft for point maybe not supported? yes.
We can include a small warm fill `PointLight(0xffa040,15,8)` no shadow at ground? Fire light enough.
Fire point light y=1.6. Stone ring around r2, light shines outward.
Ember material `MeshStandardMaterial({color:0x371008, emissive:0xff3000, emissiveIntensity:2, roughness:1,flatShading:true})`; animate shared intensity.
Sparks rise from y1.1, visible against dark background. Color bright.
Smoke might be too visible due fog? Material transparent, standard and fog. okay.
Maybe add ambient "dust" small points, but stars/sparks enough.
Need include pointer hint. no interaction needed.
Let's write full code carefully.
Pseudo code:
```
import * as THREE ...
import {OrbitControls}...
const container = ...
const renderer...
const scene...
const camera...
const controls...
const rng...
// sky
...
// stars
...
// moon
...
// ground
...
// trees
...
// grass
...
// rocks/logs
...
// fire
...
// sparks/smoke
...
const clock...
function animate(){...}
animate()
```
Order no issue.
Detailed functions:
`makeRadialTexture(size=128)`:
```
const canvas=document.createElement('canvas');
canvas.width=canvas.height=size;
const ctx=canvas.getContext('2d');
const g=ctx.createRadialGradient(size/2,size/2,0,size/2,size/2,size/2);
g.addColorStop(0,'rgba(255,255,255,1)');
g.addColorStop(.18,'rgba(255,255,255,.95)');
g.addColorStop(.5,'rgba(255,255,255,.28)');
g.addColorStop(1,'rgba(255,255,255,0)');
ctx.fillStyle=g; ctx.fillRect...
const tex=new THREE.CanvasTexture(canvas);
tex.colorSpace=THREE.SRGBColorSpace;
return tex;
```
Points map alpha uses RGB and alpha; CanvasTexture premultiply? fine.
`makeFlameGeometry` as above. Need color function return new Color each call expensive only init. use:
```
const cBottom=new THREE.Color(...);...
function flameColor(t,out) {...}
```
At each vertex use out temp.
Construct points:
```
const ringPts=levels.map...
for each s:
angle = s/sides*TAU + (level>0 && level<last ? .08*Math.sin(s*2.7+seed) :0)
rad = level.r*radius*(1 + .12*Math.sin(s*3.1+seed*0.1))
x=level.cx+cos*rad ...
```
At tip r0.
Then quad:
```
a=ringPts[l][s], b=ringPts[l][s1], c=ringPts[l+1][s1], d=ringPts[l+1][s];
push(a,b,c); push(a,c,d)
```
Colors each vertex based y/height. `colorAt(y/height,out)`. Since nonindexed compute normals.
Maybe triangle winding: For rings ascending y and angle CCW viewed top. a lower s, b lower s+1, c upper s+1. Cross (b-a) x (c-a) should outward? Let's check at angle0, a=(r,0,0), b=(r cosd,0,r sind), c=(r2 cosd,h,r2 sind). (b-a) approx (0,0,+), (c-a)=(-,h,+); cross = (-z*h, ... negative?) cross z*x: (b-a)=(0,0,z), (c-a)=(-dr,h,z2). cross = (0*z2-z*h=-zh, z*(-dr)-0 = -zdr, 0*h-0=0): x negative at angle0, inward. Need order a,d,c? For outward x positive, use (a,c,b) maybe. Cross (c-a)x(b-a) = positive. So triangles [a,c,b] and [a,d,c]? Let's derive second. We want face at s angle0 outward +x. vertices a lower s, b lower next, c upper next, d upper s. Standard cylinder outward triangles lower a -> upper d -> upper c? vectors d-a=(dr,h,0), c-a=(dr,h,z). cross d x c = (h*z, -dr*z, dr*h-dr*h) => +x. So [a,d,c] good. Second [a,c,b]: c-a=(dr,h,z), b-a=(0,0,z); cross = (h*z, -dr*z,0) +x. Thus triangles [a,d,c], [a,c,b]. Use that.
At cone tip degeneracy okay.
Set `computeVertexNormals`; normals outward.
Outer flame positions and scales:
```
const flameSpecs=[
{r:.62,h:2.9,x:0,z:0},
{r:.48,h:2.25,x:.48,z:.12}, ...
]
```
Need center flames overlap but custom mesh base starts at y .55. Position y .5. offset.
For each:
```
const geo=makeFlameGeometry(spec.r,spec.h,...)
const mesh=...
mesh.position.set(spec.x,.48,spec.z)
mesh.rotation.y=...
```
Outer material same.
Store baseX etc. We can update `mesh.rotation.z` and x.
Since geometry itself leans based spec lean, rotate subtle.
Inner flames 4:
radius .26, h1.6, positions around center, meshBasic additive, order3. Animate faster.
Fire light and coals:
coal geometry Dodecahedron . maybe scale .2. positions circle r .75 y .3. shared material. coals cast false, receive.
Add log embers glowing? maybe small orange spheres.
Rocks fire ring created before logs perhaps.
Fire glow circle:
`CircleGeometry(4.6,32)` rotate x. `glowMat`. position y .055. Because disk patch y .03, ash disk y .04, glow y .07.
Set renderOrder=1.
When camera below? never.
Maybe add a subtle volumetric cone of light? MeshBasic transparent cone above fire, could look fake. no.
Smoke:
`smokeMaterial = new THREE.MeshStandardMaterial({color:0x5d6570, transparent:true, opacity:.065, roughness:1, depthWrite:false, flatShading:true});`
Fog applies. Each `IcosahedronGeometry(.55,0)` same. store `offset`. update:
```
const life=(elapsed*.11+phase)%1;
mesh.position.set(sin(phase*10)*(.25+life*.5)+..., 2.3+life*6, ...)
const s=.35+life*1.4;
mesh.scale.set(s*1.2,s*.85,s);
mesh.rotation...
```
Shared opacity no fade, but as it appears from fire always, can pop at wrap. Could vary opacity per material (each clone) and update material.opacity = sin(pi*life)*.09; 6 materials, okay. create each with own material and store. `depthWrite=false`.
Smoke may intersect stars but near fire.
Trees and point light: moonDirectional no shadow, hemi.
Ground point shadow receiving.
Resize.
Let's consider UI overlay not blocking canvas pointer: pointer-events none, so controls get pointer. good.
Potential mobile height: 375x? CSS title smaller, bottom hint. Renderer aspect. no scroll.
Let's include `aria-label` on container.
Now could implement loading error fallback? No console errors; if CDN unavailable, scene blank. Not necessary.
Let's reason about import map and OrbitControls: `OrbitControls.js` imports several classes from 'three', all available in single module. yes.
No BufferGeometryUtils needed.
Maybe use `MathUtils.damp`? not.
Let's test mentally geometry:
Tree foliage ConeGeometry y spans -.5 to .5. dummy position y specified. `scale.set(radius,height,radius)` with cone geometry radius1, height1. Good.
Trunk geometry Cylinder height1. dummy pos y=h/2, scale y=h. Good.
Instance color material. `MeshStandardMaterial` with `vertexColors` false but InstancedMesh color handling: In shader, `#ifdef USE_INSTANCING_COLOR vColor.xyz *= instanceColor.xyz;` and `vColor` varying is only declared if `USE_COLOR_ALPHA` or `USE_COLOR`? Let's check Three shader chunks: `color_pars_vertex`:
```
#if defined( USE_COLOR_ALPHA )
varying vec4 vColor;
#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR )
varying vec3 vColor;
#endif
```
color_vertex initializes if USE_COLOR etc and instance. Material `vertexColors` not needed for instanceColor? `USE_INSTANCING_COLOR` based object.instanceColor not null, so yes. Fragment uses vColor if defined. Good.
`setColorAt` creates instanceColor. Need `mesh.instanceColor.needsUpdate=true`.
Material color white.
Ground geometry colors with `vertexColors:true`.
Grass custom geometry normals: for triangle vertices order maybe. `computeVertexNormals`, double side anyway.
Grass placement can be behind ground disk. At r3.4 etc. Ground patch y.02 and grass position y .04. fine.
Trees base y=0 but ground disk only to r8; trees beyond. plane y0. Good.
Outer rocks positions y=scale*.38. For icosa radius1 bottom -scale, center at .38scale -> embedded .62scale. okay.
Camp ring stone scale x .45, y .33, z .45; center y .2, bottom -.13, good.
Logs helper cylinder cross-section radius; positions y .35 means bottom .1, floating slightly. endpoints y .35, cylinder radius .26, bottom .09. Ground y0, slight gap .09. Could lower y to .22 for radius .26, contact. Use endpoints y=.22-.38 to angle. For fallen log r .35, y=.32. Fine. Camp logs r .26, y .27.
Fire base y .3.
Flame base y .38, logs top maybe .5, good.
Point light y1.45.
Sparks spawn y .75.
The warm light casts shadows from logs onto coals/ground; stones.
Stars positions:
```
const starCount=900;
for i:
let y=0.04+Math.pow(rng(),0.65)*.96;
let a=rng()*TAU;
let radial=Math.sqrt(1-y*y);
positions x=cos a*radial*90, y=y*90, z=sin a*radial*90;
```
This only upper hemisphere from y 3.6. visible.
Could include few below but ground. no.
Star colors and sizes. Two geometries. For large stars, 90 positions, size2.2.
PointsMaterial `sizeAttenuation:false` means size in pixels? Actually PointsMaterial shader:
```
gl_PointSize = size;
#ifdef USE_SIZEATTENUATION
bool isPerspective = isPerspectiveMatrix( projectionMatrix );
if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z );
#endif
```
scale = height/2 when not attenuation? uniform size maybe multiplied by pixel ratio. I recall renderer sets uniforms.size = material.size * pixelRatio. Thus 1.3 pixels. good.
`map` on stars gives round soft points, but small. Could use no map to square; PointsMaterial renders square points. Better map radial with alpha. Use glowTexture map, alphaTest .01, transparent. size1.6. Large 3.0.
But same glow texture has center white and alpha fade. okay.
Star `depthWrite:false, fog:false, toneMapped:false`.
Moon glow sprite map radial. Sprite material color0xaecfff opacity. Moon sphere with `MeshBasicMaterial({color:0xe5efff, fog:false, toneMapped:false})`.
Maybe add crater geometry: small MeshBasic dark circles on moon surface. Could use sprites/dots? Not necessary.
Distant hills instanced cone geometry; material fog true. Since sky sphere depthWrite false, hills write depth and fog blend. Fine.
Hills positions radius58, cone bottom at ground. Geometry unit cone y -0.5..0.5; scale height10 position y5. flat. color0x0a151f. At fog they blend.
Fog density maybe .024 not .028 to show distant trees. Set `FogExp2(0x08131e,0.0225)`. at40 factor.56? formula: density*distance=.9, exp(-.81)=.445, fog 55%. Good. at55 1.24, exp(-1.53)=.216, fog78%. Distant visible. Use .0215. Nice.
Moon light color and intensity. Hemi:
`new HemisphereLight(0x587898,0x10170f,0.85)`.
Directional `1.25`.
Maybe night too bright; ACES and dark colors. Let's estimate ground color linear from sRGB 0x283629 -> linear .021,.037,.022. Direct light diffuse Lambert ~1.25/π? Three physically based direct irradiance color*intensity, BRDF 1/π. So outgoing ~.021*1.25/π=.008, plus hemi .85/π=.005; display sRGB ~.1 (25/255). Dark but visible. Fire at r2 with intensity90 /r²=22.5 irradiance /π ~7*albedo=.15, display .42 warm. Good.
Directional intensity might be interpreted lux and no pi? Three directional direct diffuse `irradiance = dotNL * directLight.color`, BRDF diffuse /pi; intensity1.25. similar. Maybe intensity2.2 to see facets. Use 1.6. Hemisphere .75. Exposure1.15. okay.
Trees foliage sRGB 0x224c39 linear ~.016,.072,.041; moonlight creates blue facets. good.
Point light color `0xff8a3c`, intensity 95. At distance 2, irradiance ~24*color, bright. At8,1.5. Flicker.
Maybe point shadow map at 1024. `fireLight.shadow.radius=2` only for PCF? PointLightShadow radius maybe supported. set 2.
Fire light cast shadow false? No true.
Directional target default origin, add.
Ambient occlusion under fire logs via shadows.
Need maybe add contact shadow ellipse under logs? Point handles.
Potential transparent flame's `side:DoubleSide` and depthWrite false. Good.
Let's think about orbit camera and autoRotate. After 3 seconds, scene angle changed negligible. The hint "Drag to orbit • Scroll to zoom". At bottom. No key requirement.
Could include a small pulsing red dot "LIVE" but no.
Now exact output rules: exactly one fenced code block with complete file. We must not include any text before/after. Final code block ```html. Great.
Let's compose code fully, avoiding syntax errors. We'll write JS carefully.
Potential issue `new THREE.Color().lerpColors` modifies and returns. fine.
Ground color helper:
```
const c = centerColor.clone().lerp(edgeColor,t).multiplyScalar(...)
```
At each triangle clone alloc many but init only.
Need `THREE.MathUtils.clamp`.
Shape clearing:
```
const clearingShape = new THREE.Shape();
for i=0..31 { const a=i/32*TAU; const r=7.3+...; if(i===0) clearingShape.moveTo... else lineTo...}
closing automatically? call closePath.
const clearingGeo=new THREE.ShapeGeometry(clearingShape);
clearingGeo.rotateX(-Math.PI/2);
```
The Shape lies in XY, after rotate x -90, original y maps z negative? shape x->x, shape y->? rotation -90: y' = y cos -90 - z sin -90 = z? For vector(0,1,0): (0,0,-1). So z = -shape y. Orientation flips but symmetric. Normals +z -> (0,1,0), good.
Set mesh y .025.
Shadow receive.
Ground ash circle: CircleGeometry normal +z, rotate -PI/2 => +y. fine.
Glow circle same.
Moon position maybe update with controls? Moon stays world.
Trees and moon.
`createTreeInstances`:
```
const treeCount=48;
const trunkGeo=...
const foliageGeo=...
const trunkMat...
...
const trunkMesh=new InstancedMesh(...);
...
for (let i=0; i<treeCount; i++) {
let x,z; do generate until radius>12.2 and distance from camera>3
...
const s=.78+Math.pow(rng(),.7)*.62;
...
dummy.position...
dummy.scale...
dummy.rotation...
...
trunkMesh.setColorAt(i,new Color(...))
lowerMesh.setColorAt(i,...)
}
```
Using shared dummy and setMatrixAt immediate okay.
Cone meshes one per layer all count treeCount. Colors per layer.
`lower` position y = s*4.25 (bottom 2.45 top6.05 if h3.6); middle pos s*5.85 (bottom4.3 top7.4); top pos s*7.25 (bottom6.05 top8.45). Trunk h3.8 s, top3.8s, hidden mostly.
Tree overall height 8.45*s max11.5.
Scale x base2.15s lower, 1.62s middle,1.02s top. Foliage diam4.3s max6.1. Good.
Tree min scale .72 => height6.1, radius1.55.
Set trunk scale x s*.95, z s*.95 (geometry bottom radius .22) actual .22s. Good.
`dummy.rotation.set(0, rot, 0)`.
Tree color:
```
const foliagePalette=[0x183b30,0x204936,0x16362f,0x244c39];
```
Set lower color base, middle brighter? `new Color(base).multiplyScalar(1.05)` but Color values linear, multiply. top maybe .9.
Material leaves roughness .95.
Trunk palette.
Trees receiveShadow true? Foliage receiving moonlight shadow? only fire point shadows; yes. `receiveShadow=true`.
Call `mesh.instanceMatrix.needsUpdate = true`, instanceColor. `mesh.computeBoundingSphere()`.
Mounds count 16, ConeGeometry(1,1,5), matrix positions. base at r, scale width8-16, h6-13, position y=h/2. fog. No shadow.
Grass placement after trees uses random. 320. Custom geometry:
```
const blade = new BufferGeometry();
const p=[];
for k=0..2:
a=k*PI/3 + .2;
const bx=Math.cos(a), bz=Math.sin(a);
const w=.16;
p.push(-bx*w,0,-bz*w, bx*w,0,bz*w, bx*.15,.72,bz*.15);
```
This triangle lies along horizontal width and leans. Actually vertices first two form base perpendicular to direction, third top shifted. Good. DoubleSide.
Instanced 300. scale values .5-1.4. Position y .03. rotate y random. SetColorAt. Material color white. castShadow false, receive true.
At 3m radius, fine.
Rocks:
function addRock(geometry, material, x,z, sx,sy,sz) returns mesh.
Need avoid trees? no.
Logs helper:
```
function createLog(a,b,r,barkMaterial,endMaterial){
const start=new Vector3(...a), end...
const length=start.distanceTo(end);
const group=new THREE.Group();
const body=new Mesh(new CylinderGeometry(r*.88,r,length,8,1,false),barkMaterial);
group.add(body);
const capGeo=new CircleGeometry(r*.88,8);
const capA=new Mesh(capGeo,endMat); capA.rotation.x=Math.PI/2; capA.position.y=-length/2-.006;
const capB ... rotation.x=-Math.PI/2; position y=length/2+.006;
group.add...
group.position.copy(start).add(end).multiplyScalar(.5);
group.quaternion.setFromUnitVectors(UP,end.clone().sub(start).normalize());
...
return group;
}
```
Wait CircleGeometry normal +z. capA rotation x=PI/2 maps +z to -y (correct bottom), capB rotation x=-PI/2 maps +z to +y. good.
Group quaternion maps local y to dir. So capB at local +y points toward end. good.
End circle radius r*.88 matches cylinder top radius. Bottom cylinder radius r (geometry top? `CylinderGeometry(radiusTop,radiusBottom,height)` first top=.88r, second bottom=r). At local +y top radius .88r; local -y bottom r, but capA radius .88 mismatch bottom slightly. Use two cap geos: bottom radius r, top r*.88. Or cylinder radius same r both ends for simplicity. `CylinderGeometry(r*.92,r,length)` bottom r. cap bottom r, top .92r. Good.
Bark material cast. End cap offset along local y: bottom -length/2-.002; top +length/2+.002.
Group position midpoint. Group quaternion.
Logs near ground should have y endpoints correspond. Body extends horizontally. `group.rotation` overwritten by quaternion; fine.
Fallen logs y .35.
Camp logs all use charred bark material.
Add small glowing inner log maybe not.
Fire stones ring radius2.15. But logs extend to radius1.5, stones outside.
Fire glow maybe covers stones but additive.
Flames specs 8:
center r .66 h2.9
surround 6 r .42-.52 h1.8-2.5 offsets .48.
One offset back. Use lean random.
Positions at y .43.
`flameObjects.push({mesh,baseX:...,baseZ...,baseY:.43,phase:rng()*TAU,baseScaleY:1,...})`.
For outer, mesh scale base maybe x1. animate.
`mesh.rotation.y=rng*TAU` but geometry sides orientation; set.
Inner specs 5 radius .24-.3 heights1.2-1.9 positions radius .25. MeshBasic. Animate.
Could create fire core sphere `IcosahedronGeometry(.5,1)` yellow at y.8 emissive. Nice.
Flame material color vertex. Need no point light reflection issues.
`createFlameGeometry` uses global rng, which advances positions; okay.
Color helper:
```
const FIRE_COLORS=[new Color(0xff2500),...]
function colorForFlame(t,target){
if(t<.28) target.copy(c0).lerp(c1,t/.28)
else...
return target;
}
```
`Color.lerp` in linear.
Outer material `emissive:0xff3b00` adds red to yellow, but still.
Maybe `emissive:0xff6a00, emissiveIntensity:1.0`.
MeshStandard with `transparent:true,opacity:.92`. `side:THREE.DoubleSide`.
No cast.
Point light and glow.
Coal material intensity animate.
Maybe `coalMesh.material` shared and all pulse.
Sparks initialization:
```
const sparkCount=96;
const sparkPositions = new Float32Array(...);
const sparkColors=...
const sparkGeo...
const sparkMat=...
const sparks=[];
function spawnSpark(s, spread=false){...}
for each spawn and s.age=rng()*s.life; but if age advanced without pos trail, positions at base. Better calculate p = age/life; pos y=.8+p*4; etc. Use spawn then if initial:
s.age=rng()*s.life;
s.pos.y += s.age*s.vel.y;
s.pos.x += ...;
```
Fine.
Reset function sets age0.
Update `s.pos.addScaledVector(s.vel,dt); s.vel.y += .08*dt; s.pos.x += Math.sin(elapsed*2.2+s.phase)*.18*dt; s.pos.z += ...`.
Life 1.6-3.2, vy .8-1.5, height up to5.
Fade color:
```
const f=s.age/s.life;
const bright=1-f;
scratch.setRGB(1, .68-.25*f, .18-.1*f).multiplyScalar(Math.min(1,(1-f)*1.8));
```
At age0 1,.68,.18; at end0. Additive yellow/orange.
Maybe sparks should pale at spawn: r1,g.8,b.3. fine.
`positionAttr.needsUpdate`, `colorAttr.needsUpdate`.
Spark points may be occluded by logs but okay.
Smoke objects own materials. `life=(elapsed*.105+phase)%1`, position starts y2.1. Color warm at base? material color0x64717a, opacity sin(pi life)*.08. At life0 opacity0, grows. Scale .35+life*1.6. Base x initial random .2 plus drift; use sin.
This makes smoke visible maybe.
Fire glow pulse update:
```
const flicker = ...
fireLight.intensity=90*flicker;
glowMaterial.opacity=.3 + ...
glow.scale.setScalar? mesh scale on Circle geometry radius4.5, setScalar 1+...
```
Use `glow.scale.set(1+...,1+...,1)` but geometry circle rotated; scale x/y local (local x/y correspond world x/z after rotation) so set x/y. `glow.scale.set(s,s,1)`.
Core scale.
Coal emissiveIntensity=1.4+flicker.
Flames animate.
AutoRotate means moon can move slowly. Fine.
Ambient maybe adjust after first frame? no.
Could add a subtle pulsing `fireLight.position.x=Math.sin(elapsed*7)*.04`.
Potential `renderer.info` no.
No console logs maybe fine. Could log tree count? no.
CSS overlay title might have text-shadow.
Let's make body cursor grab; canvas cursor grab and active grabbing. CSS:
```
canvas { cursor: grab; }
canvas:active{cursor:grabbing;}
```
Now verify all references defined before animate.
Let's sketch final code in order.
1 imports.
2 DOM refs.
3 renderer scene camera controls.
4 textures helper, rng.
5 sky/stars/moon.
6 ground.
7 trees.
8 grass, rocks, logs.
9 fire.
10 particles.
11 loop.
Maybe directional light target created:
```
const moonTarget = new THREE.Object3D();
moonTarget.position.set(0,0,0); scene.add(moonTarget);
moonLight.target=moonTarget;
```
Directional default target not added? target must be in scene if changed. add.
Sky sphere sphere geometry color attr. `skyGeo.setAttribute('color',...)`. For each vertex pos. `MeshBasicMaterial({vertexColors:true, side:BackSide, depthWrite:false, fog:false, toneMapped:false})`. `sky.renderOrder=-100`. `sky.frustumCulled=false`.
Because material depthWrite false and depthTest default true, opaque sky render order -100 ensures first. Opaque objects sorted but renderOrder overrides. good.
Stars renderOrder -90 maybe transparent after all opaque; their depth test may be blocked by sky? sky doesn't write depth, so fine. But transparent render happens after opaque moon and ground; depth test ground blocks lower. Set renderOrder -50 doesn't force before opaque? Render list separates opaque and transparent regardless renderOrder. transparent stars after opaque, good.
Moon sphere opaque at distance45 writes depth, stars behind fail at moon pixels. good.
Moon glow Sprite transparent after stars maybe depth. Set depthTest true, sprite at moon location and stars behind; sprite depth equals moon center, front? Depth test with moon depth may cause glow sprite center fail if moon wrote closer depth, hiding central glow but outer glow no moon depth. That's good (glow around).
Set glow sprite position moon, scale8.
Ground material receives shadows.
Shadow settings:
```
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
```
For point shadow in r170, PCF soft. Fine.
Renderer clear color scene background maybe `0x03070d`.
Maybe `scene.background = new Color(0x03070d)` behind sky.
Now tree count and UI:
`document.querySelector('#treeCount').textContent = treeCount;` We can hardcode 48 in HTML or set. Use `<span id="tree-count">48</span>`.
At init set.
Overlay:
```
<div class="hud">
<div class="title-block">
<div class="kicker"><span class="dot"></span> PROCEDURAL NIGHT</div>
<h1>Emberlight</h1>
<p>Midnight in the pine clearing</p>
</div>
<div class="meta">
<span><b id=tree-count>48</b> LOW-POLY PINES</span>
<span class=divider></span>
<span>LIVE FIRE</span>
</div>
<div class=hint><span class=mouse></span> Drag to orbit <i>·</i> Scroll to zoom</div>
</div>
```
At mobile hide meta perhaps.
Maybe dot animation pulse CSS; no issue. `@keyframes pulse`.
No hidden important things.
Vignette pseudo plus top gradient.
Potential requirement page adapt 375. Title 26px, meta hidden, hint centered bottom. Fine.
Let's think if need "Click or press key to start" because developer says games must; this is not a game. No.
Now let's scrutinize Three rendering of sky with fog. MeshBasicMaterial has fog false. sphere radius120 and camera far plane maybe 160? Camera far set 180. Moon at distance50, hills60. Sky sphere radius120 from origin but camera at up to21 from origin, far edge141 > far180 fine. Set camera far=220. Fog doesn't affect sky. Stars radius90, from camera offset up to111, within far. Good.
Camera far 180 enough sky at 120+21=141. set220.
Stars on upper hemisphere radius90. If camera moves21, some stars behind at111. fine.
Fog exp2 applies to tree material built-in. yes.
Ground plane size110 perhaps horizon; 100 gives edge at50 from origin, camera at21 can see edge at71 away; fog hides. Set 120 plane with 40 segments; camera max21 edge distance81; fog almost fully. good. Plane 120, segments36 -> triangle ~3.3. color facets. Use 110. Let's use 120.
Ground nonindexed triangles 36*36*2=2592, pos15552, okay.
At r60 edge. camera max21 ->81. fog density .0215 factor exp(-(1.74)^2)=.048, edge95% fog. hidden. good.
Trees up to42.
Clearing shape radius7.7.
Maybe add undergrowth patches around trees no.
Let's tune flame geometry. Outer flame top at height2.9, point light y1.5. Looks big. center flame width1.3. Fire ring radius2.1, good.
Fire logs visible behind flames.
Sparks around y1-6.
Smoke to y8.
Moon at (-18,18,-30), visible.
Initial camera perhaps target (0,1.15,0), camera (8.9,5.8,11.7). Distance15.7. Fire appears small-ish. Let's use camera (7.6,4.6,9.6), distance13.4. FOV50. Fire height3.3 => 24% viewport? angular13.8°, vertical FOV50 so 28%. Good. Ground circle visible. Tree inner radius12.5, camera r12.25 inside tree ring. Great. Position (7.8,4.8,10.0) r12.67. min tree radius12.8, camera just inside. Avoid tree near camera. Fire appears robust. Controls target y1.2. Direction forward (-.6,-.27,-.76).
Moon vector(-25.8,13.2,-40) norm49.2. angle with forward .302-.072+.581=.811 angle35.8. vertical screen near top. okay. Moon radius2 at49 ->4.7° diameter. visible.
Maybe camera is low enough to see campfire stones, good.
Orbit max polar 84.6 from target, at max dist13 camera y=1.2+1.3=2.5 above ground. min target distance6.5. good.
maxDistance=24 camera may leave clearing and tree collisions but no requirement. Could set maxDistance=19 to remain just inside/around tree ring. Camera r12.7 initial; max19 near trees, could clip tree but okay. Use maxDistance=20. User can see wider. min6.
AutoRotate slow.
DPR cap 1.6 to performance.
Point light shadows on trees near. `fireLight.distance=20`.
Need moon directional position correspond moon direction: (-18,25,-30) normalized; set (-18,30,-28) maybe.
Let's consider adding a cold rim light from behind to give trees depth. Directional light does that. `moonLight.position.set(-20,28,-24)`. Since moon visual at(-18,18,-30), light direction from similar. Intensity1.45 color0x8fb2dd.
Maybe hemisphere ground color warm? no.
One concern: scene overall too dark for screenshot. We can make sky brighter horizon and moonlit trees. Set exposure1.25. Dark colors. Use ambient0.35? Hemisphere.75. Directional1.8. ACES compresses. Let's set:
- hemi 0x58789c sky,0x141a11 ground,intensity0.95
- moon directional color0xa5c5f2,intensity2.1
- tone exposure1.1
- fire 110.
This will show blue-lit facets while still night.
Ground base colors . maybe.
Fog color0x0a1622.
Renderer output with toneMapped fire basic inner. okay.
Maybe add small `AmbientLight(0x203040,.2)`.
Let's formulate final.
Let's ensure use `THREE.MathUtils.lerp` where needed.
Detailed ground code:
```
const groundBase = new THREE.Color(0x1b2b22);
const groundEdge = new THREE.Color(0x0c1918);
for (let i=0;i<pos.count;i+=3) {
const cx...
const radius...
const t=clamp((radius-6)/45,0,1);
const color=groundBase.clone().lerp(groundEdge,t);
const warm = ...? random.
color.multiplyScalar(.78+rng()*.42);
for j set same color.
}
```
Some triangles brighter. Great.
Material `roughness:1, metalness:0`. Could use `dithering:true`.
Clearing shape material color0x29362b. Fire point will warm.
Rocks materials:
```
new MeshStandardMaterial({color:0x4a5154,roughness:.96,flatShading:true})
...
```
Night blue light.
Stone ring maybe color0x555456.
Trees point light can light lower foliage.
Instance colors multiplied material. okay.
Hills `MeshStandard` or Basic? With fog. Use Standard dark; directional. at distance.
Smoke standard transparent may sort weird but fine.
Text hint uses `•` characters.
Let's consider if importmap must be exactly as user gave including no whitespace? We use exact. Good.
No external fonts. CSS `font-family: ui-sans-serif...`.
Let's write final code with all details. Need keep code reasonable maybe ~800 lines. Desired oververbosity irrelevant; code complete.
Let's now draft the actual code in final. But before, let's mentally execute key operations for errors.
- `new THREE.ShapeGeometry(clearingShape)` triangulates shape; no issue.
- `geometry.toNonIndexed()`: PlaneGeometry is indexed? Yes likely index. If not, toNonIndexed warns and returns same? Three BufferGeometry.toNonIndexed checks index null -> console.warn and return this. PlaneGeometry has index, so fine.
- `groundBaseGeo.dispose()` after toNonIndexed; no references. good.
- `Float32BufferAttribute` usage.
- `instanceMesh.computeBoundingSphere()` exists. To be safe:
```
if (mesh.boundingSphere===null) mesh.computeBoundingSphere();
```
But after setMatrixAt, boundingSphere initially null; compute. In r170 InstancedMesh.computeBoundingSphere method. call direct. no error.
- `mesh.setColorAt` on InstancedMesh: `this.instanceColor` null, method creates. yes.
- `new THREE.CircleGeometry` and rotate.
- `new THREE.DodecahedronGeometry(1,0)` okay.
- `new THREE.ConeGeometry(1,1,6,1)` okay.
- `new THREE.CylinderGeometry(.13,.24,1,6,1)` okay.
- `new THREE.IcosahedronGeometry(1,0)` okay.
- `THREE.AdditiveBlending`.
- `PointsMaterial` `map` and `vertexColors`.
- `colorAttr.setXYZ`.
- `CanvasTexture` colorSpace.
- `SpriteMaterial` map, blending.
- `MeshBasicMaterial` `fog:false` property recognized.
- `toneMapped:false` property inherited.
- `DirectionalLight.target` add.
- `PointLight` constructor `(color,intensity,distance,decay)`.
- `fireLight.shadow.camera` is Camera; setting near/far after creation should update projection automatically? For point shadow camera far property and updateProjectionMatrix maybe renderer updates shadow camera each frame? Three's LightShadow updateMatrices calls camera.updateProjectionMatrix only if auto? For PointLightShadow, camera is PerspectiveCamera and `updateMatrices` uses camera.near/far maybe projection matrix initialized in constructor with far500. Setting far20 might not update projection? PointLightShadow.updateMatrices sets camera.far = light.distance || 500 and updateProjectionMatrix? Looking at three source: LightShadow.updateMatrices sets shadow camera position, etc. PointLightShadow updateMatrices likely `camera.far = light.distance || 500; camera.updateProjectionMatrix();`. We set distance20. near .5 maybe default .5. Set `shadow.camera.near=.25; shadow.camera.far=20; shadow.camera.updateProjectionMatrix();`. Works.
- `shadow.mapSize.set`.
- `shadow.bias` accepted.
- `normalBias`.
Point light shadow map allocates on first frame.
- `Clock.getDelta()` first call small.
- `controls.update(dt)` r170 supports optional deltaTime; if not, extra ignored. fine.
- `controls.addEventListener('start', ()=> {controls.autoRotate=false;})`.
- Resize listener.
Potential autoRotate after user stops? no.
UI tree count set.
CSS `backdrop-filter` maybe okay, no error. Use simple.
Let's optimize flame geometry color: `geometry.computeVertexNormals()` on nonindexed. positions arrays.
Fire colors in material with vertex colors and emissive. Since `MeshStandardMaterial` vertexColors true, color attributes interpreted linear. `Color(hex)` converts sRGB to linear, correct.
Inner MeshBasic vertex colors and toneMapped false? If toneMapped false, vertex colors linear to output conversion still; bright.
Set inner `toneMapped:false` might be too white, but okay. Outer tone mapped.
Fire `glowMat.opacity` updated, material transparent.
`glow.scale.set(s,s,1)` after circle rotated? Object scale local x,y; yes circle in XY then rotate object x -90, local y maps world z, so scale x,y expands world x,z.
Smoke materials opacity own. `mesh.material.opacity` updated.
Sparks spawn:
```
function resetSpark(s,initial=false){
s.age=0; s.life=1.5+rng()*2.2; ...
s.pos.set(...);...
if(initial){s.age=rng()*s.life; s.pos.y += s.age*s.vel.y; s.pos.x += ...;}
}
```
If age advanced and color initial uses age/life. fine.
Update age > life reset. `s.vel.y` maybe slight plus .05. At age life, position may be y4. Good.
Flame object `baseScale` maybe store. Animate:
```
const sy = baseScale*(.86+.11*sin+...);
mesh.scale.set(baseScale*(.94+...),sy,baseScale*(...));
```
For outer baseScale1. Inner maybe .9.
Rotation z = baseRotZ + sin* .12.
Position x base + sin*.035.
Point light flicker uses sum potentially >1.2; clamp .65-1.2.
`const flicker=0.82+0.11*sin...+0.07*sin...+0.04*sin...`; range .6-1.04. Use intensity 105*flicker. at min63. good.
Also random-ish sine.
Coal emissive intensity1.2+1.2*flicker.
Core scale.
Smoke phase as fraction.
Tree instanced colors:
```
const lowerColor = new THREE.Color(palette[idx]);
trunks.setColorAt(i,trunkColor)
...
```
`new Color(0x...)`.
Could set `mesh.instanceColor.needsUpdate` only if instanceColor. after setColorAt yes.
Mounds color maybe one.
Grass setColorAt, palette.
Some grass may be placed on fire ring if radius random range3.1-14, okay outside fire r2.1.
Fire ring stones and outer rocks could overlap grass fine.
Distant mounds position y=height/2-1 maybe.
No audio.
Let's maybe add `renderer.domElement.setAttribute('aria-label','A low-poly forest clearing...')`.
Need loading no external.
Let's decide overlay and CSS final:
```
#scene {position:fixed; inset:0;}
#scene canvas {width:100%;height:100%;display:block;}
.vignette {position:fixed;inset:0;pointer-events:none;background:radial-gradient(...);}
.hud...
.title top clamp(20px,4vw,44px) left clamp...
h1 letter spacing -.04em.
.kicker...
.meta top right.
.hint bottom.
@media(max-width:640px){ .meta{display:none} .hint{left:50%;transform:translateX(-50%);...} .title p{max-width...}}
```
`transform` on hint pointer events none.
Could add top/bottom gradients overlay.
No scroll.
Now potential issue in final code due code block size but okay.
Let's perhaps add a subtle ray of moonlight? no.
Let's evaluate visual hierarchy: fire at center lower maybe. Title top left. moon top center. trees around. Fire glow. Good.
Maybe initial camera target is fire but target y1.25 means center screen at flame base/top? Fire center around1.5, yes. The clearing extends lower. Great.
Could set `camera.setViewOffset` no.
Let's include `controls.zoomToCursor = true`? OrbitControls property available; could cause target shift on zoom though panning. not needed.
Now exact final code. Let's write with attention to quote styles and syntax. Use `const TAU = Math.PI * 2;`.
One concern: in `createFlameGeometry`, color per vertex uses y/height but levels center offsets don't change y. fine.
```
const points = levels.map((level,l)=> {
const arr=[];
for s ...
const rr = level.r*radius*(1+0.10*Math.sin(s*2.7+seed*0.01));
...
return arr;
});
```
When level.r=0 tip all same; triangles around tip have some degenerate but acceptable.
For quad s last, `s1=(s+1)%sides`.
push functions.
`colors.push(c.r,c.g,c.b)`.
Use temp color clone each call? `const cc = flameColor(...);` returns new. fine.
Inner flame material uses `vertexColors:true`.
`side:DoubleSide` perhaps.
Outer flame mesh `renderOrder=2`, inner=3, glow1.
Fire light placed and added before coals.
Maybe add `fireLight.shadow.autoUpdate=true` default.
PointLight shadow with transparent objects no cast.
Let's think `scene.fog` and sky material `fog:false`; ground fades. good.
Let's include a `scene.environment`? no external.
Everything self-contained.
Let's consider canvas texture and CORS: local canvas no issue.
No use `window.open`.
Okay.
Could use `requestAnimationFrame` function. yes.
Let's now produce final.