Nemotron 3.5 Super VL (free)
by NVIDIAReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 42 s
- Cost
- $0
- Output tokens
- 11,936
- Spent thinking
- 8,045 tokens
- Speed
- 118.1 tok/s
- Console errors
- 0
- First token after
- 511 ms
- First visible text after
- 58.5 s
- Prompt tokens
- 584
- Code size
- 10.8 KB
- Finish reason
- stop
- Served by
- Nvidia
- Model id
nvidia/nemotron-3.5-super-vl:free- Reasoning sent
model default- Max output
- 32,000 tokens
- Generated
- Nov 9, 2026
What it was asked
The prompt
Build a low-poly 3D scene of a campfire in a forest clearing at night, with Three.js in a single HTML file. Load Three.js only through this import map (do not use any other URL or version): <script type="importmap">{"imports":{"three":"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js","three/addons/":"https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"}}</script> Requirements: - A ground with a few low-poly rocks and logs around a campfire in the center. - At least 30 low-poly trees placed randomly around the clearing (not on the fire). - The campfire has animated flames and a warm point light that flickers and casts shadows. - Sparks rising from the fire as a simple particle system. - A dark night sky with stars and a moon, plus fog that fades the distant trees. - OrbitControls to look around, with limits so the camera stays above the ground. - The renderer fills the window and handles resizing correctly.
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 342 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Low Poly Campfire</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; background: #000; font-family: system-ui, sans-serif; }
canvas { display: block; }
#info {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
text-align: center;
pointer-events: none;
user-select: none;
text-shadow: 0 0 12px rgba(255, 160, 60, 0.6);
font-size: 1.25rem;
mix-blend-mode: screen;
}
</style>
</head>
<body>
<div id="info">Low Poly Campfire Night</div>
<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>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
/* ---------- Scene ---------- */
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020208, 0.04);
const camera = new THREE.PerspectiveCamera(55, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(12, 7, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setClearColor(scene.fog.color);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 4;
controls.maxDistance = 40;
controls.minPolarAngle = 0.25;
controls.maxPolarAngle = 1.45; // keeps camera above ground plane
controls.target.set(0, 0, 0);
controls.update();
/* ---------- Lights ---------- */
const ambient = new THREE.AmbientLight(0x202040, 0.35);
scene.add(ambient);
const hemi = new THREE.HemisphereLight(0x101030, 0x050508, 0.15);
scene.add(hemi);
const moonLight = new THREE.DirectionalLight(0x8090dd, 0.25);
moonLight.position.set(-25, 35, -15);
moonLight.castShadow = true;
scene.add(moonLight);
const fireLight = new THREE.PointLight(0xffaa44, 1.8, 24, 1.6);
fireLight.position.set(0, 2, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(1024, 1024);
fireLight.shadow.bias = -0.0005;
scene.add(fireLight);
/* ---------- Ground ---------- */
const groundGeo = new THREE.PlaneGeometry(90, 90, 20, 20);
const pos = groundGeo.attributes.position;
const v = new THREE.Vector3();
for (let i = 0; i < pos.count; i++) {
v.fromBufferAttribute(pos, i);
const d = Math.hypot(v.x, v.z);
if (d > 7) {
const h = (Math.sin(v.x * 0.6) * Math.cos(v.z * 0.6) + Math.sin(v.x * 2.2) * 0.3) * 0.5;
pos.setY(i, h);
}
}
groundGeo.computeVertexNormals();
groundGeo.rotateX(-Math.PI / 2);
const groundMat = new THREE.MeshStandardMaterial({ color: 0x1c301c, flatShading: true });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.receiveShadow = true;
scene.add(ground);
/* ---------- Sky objects ---------- */
// Moon
const moon = new THREE.Mesh(
new THREE.SphereGeometry(1.4, 12, 12),
new THREE.MeshBasicMaterial({ color: 0xfdf6e3 })
);
moon.position.set(18, 14, -12);
scene.add(moon);
const moonGlow = new THREE.Mesh(
new THREE.SphereGeometry(2.1, 12, 12),
new THREE.MeshBasicMaterial({ color: 0xfdf6e3, transparent: true, opacity: 0.25 })
);
moon.add(moonGlow);
// Stars
const starCount = 500;
const starGeo = new THREE.BufferGeometry();
const starPos = [];
for (let i = 0; i < starCount; i++) {
starPos.push((Math.random() - 0.5) * 140);
starPos.push(15 + Math.random() * 50);
starPos.push((Math.random() - 0.5) * 140);
}
starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.18, sizeAttenuation: true });
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
/* ---------- Trees ---------- */
const treeCount = 40;
const trunkGeo = new THREE.CylinderGeometry(0.14, 0.22, 1.1, 6);
const foliageGeo = new THREE.ConeGeometry(0.65, 1.35, 6);
foliageGeo.translate(0, 0.75, 0);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x443024, flatShading: true });
const leafMat = new THREE.MeshStandardMaterial({ color: 0x2e4d2e, flatShading: true });
const trunkInst = new THREE.InstancedMesh(trunkGeo, trunkMat, treeCount);
const leafInst = new THREE.InstancedMesh(foliageGeo, leafMat, treeCount);
trunkInst.castShadow = true;
trunkInst.receiveShadow = true;
leafInst.castShadow = true;
leafInst.receiveShadow = true;
const dummy = new THREE.Object3D();
for (let i = 0; i < treeCount; i++) {
let r, theta, x, z;
do {
r = 10 + Math.random() * 26;
theta = Math.random() * Math.PI * 2;
x = r * Math.cos(theta);
z = r * Math.sin(theta);
} while (Math.hypot(x, z) < 8); // keep clear of fire
// trunk
dummy.position.set(x, 0.55, z);
dummy.rotation.y = Math.random() * Math.PI;
dummy.scale.set(0.85 + Math.random() * 0.3, 1, 0.85 + Math.random() * 0.3);
dummy.updateMatrix();
trunkInst.setMatrixAt(i, dummy.matrix);
// foliage
dummy.position.set(x, 0.55, z);
dummy.scale.set(0.8 + Math.random() * 0.5, 0.75 + Math.random() * 0.4, 0.8 + Math.random() * 0.5);
dummy.updateMatrix();
leafInst.setMatrixAt(i, dummy.matrix);
}
scene.add(trunkInst);
scene.add(leafInst);
/* ---------- Rocks ---------- */
const rockCount = 14;
const rockGeo = new THREE.IcosahedronGeometry(0.5, 0);
const rockMat = new THREE.MeshStandardMaterial({ color: 0x555555, flatShading: true });
const rockInst = new THREE.InstancedMesh(rockGeo, rockMat, rockCount);
rockInst.castShadow = true;
rockInst.receiveShadow = true;
for (let i = 0; i < rockCount; i++) {
let r, theta, x, z;
do {
r = 5 + Math.random() * 18;
theta = Math.random() * Math.PI * 2;
x = r * Math.cos(theta);
z = r * Math.sin(theta);
} while (Math.hypot(x, z) < 6 || Math.hypot(x, z) > 26);
const sx = 0.4 + Math.random() * 0.7;
const sy = 0.15 + Math.random() * 0.4;
const sz = 0.4 + Math.random() * 0.7;
dummy.position.set(x, sy / 2, z);
dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
dummy.scale.set(sx, sy, sz);
dummy.updateMatrix();
rockInst.setMatrixAt(i, dummy.matrix);
}
scene.add(rockInst);
/* ---------- Campfire ---------- */
const fireGroup = new THREE.Group();
// logs
const logGeo = new THREE.CylinderGeometry(0.16, 0.16, 0.9, 6);
const logMat = new THREE.MeshStandardMaterial({ color: 0x422b1d, flatShading: true });
function addLog(x, z, ry, rx) {
const m = new THREE.Mesh(logGeo, logMat);
m.position.set(x, 0.45, z);
m.rotation.set(rx, ry, 0);
m.castShadow = true;
m.receiveShadow = true;
fireGroup.add(m);
}
addLog(-0.55, -0.4, 0, 0);
addLog(0.55, 0.4, Math.PI / 2, 0);
addLog(0, -0.85, Math.PI / 3, 0);
addLog(0.7, -0.2, -Math.PI / 3, 0);
// flames
const flameBaseGeo = new THREE.ConeGeometry(0.32, 0.85, 5);
const flameCount = 7;
const flames = [];
for (let i = 0; i < flameCount; i++) {
const mat = new THREE.MeshStandardMaterial({
color: 0xffaa00,
emissive: 0xff5500,
emissiveIntensity: 2,
flatShading: true
});
const mesh = new THREE.Mesh(flameBaseGeo, mat);
mesh.position.set((Math.random() - 0.5) * 0.5, 0.55 + Math.random() * 0.7, (Math.random() - 0.5) * 0.5);
mesh.scale.set(0.8 + Math.random() * 0.3, 0.6 + Math.random() * 0.5, 0.8 + Math.random() * 0.3);
mesh.castShadow = true;
fireGroup.add(mesh);
flames.push({
mesh,
baseY: mesh.position.y,
speed: 0.7 + Math.random() * 0.6,
offset: Math.random() * Math.PI * 2
});
}
scene.add(fireGroup);
/* ---------- Sparks ---------- */
const sparkCount = 70;
const sparkGeo = new THREE.BufferGeometry();
const sparkPos = new Float32Array(sparkCount * 3);
const sparkVel = new Float32Array(sparkCount * 3);
const sparkLife = new Float32Array(sparkCount);
function resetSpark(i) {
const a = Math.random() * Math.PI * 2;
const s = 0.25 + Math.random() * 0.35;
sparkPos[i * 3] = Math.cos(a) * s;
sparkPos[i * 3 + 1] = 0.5 + Math.random() * 0.8;
sparkPos[i * 3 + 2] = Math.sin(a) * s;
sparkVel[i * 3] = (Math.random() - 0.5) * 0.015;
sparkVel[i * 3 + 1] = 0.035 + Math.random() * 0.05;
sparkVel[i * 3 + 2] = (Math.random() - 0.5) * 0.015;
sparkLife[i] = 1.0;
}
for (let i = 0; i < sparkCount; i++) resetSpark(i);
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));
const sparkMat = new THREE.PointsMaterial({
color: 0xffaa00,
size: 0.09,
sizeAttenuation: true,
transparent: true,
opacity: 0.9,
blending: THREE.AdditiveBlending
});
const sparks = new THREE.Points(sparkGeo, sparkMat);
scene.add(sparks);
/* ---------- UI hint ---------- */
const info = document.getElementById('info');
setTimeout(() => { info.style.opacity = '0'; }, 3500);
/* ---------- Animation ---------- */
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.025;
controls.update();
// flickering fire light
const flicker = 0.9 + Math.sin(time * 12) * 0.25 + Math.random() * 0.15;
fireLight.intensity = THREE.MathUtils.clamp(flicker, 0.6, 2.4);
fireLight.position.y = 1.9 + Math.sin(time * 8) * 0.08;
// flames
flames.forEach((f) => {
const t = time * f.speed + f.offset;
f.mesh.position.y = f.baseY + Math.sin(t * 2.5) * 0.04;
f.mesh.rotation.y = t * 0.4;
f.mesh.rotation.x = Math.sin(t) * 0.15;
const sx = 1 + Math.sin(t * 1.4) * 0.08;
const sz = 1 + Math.cos(t * 1.4) * 0.08;
const sy = 1 + Math.sin(t * 1.9) * 0.12;
f.mesh.scale.set(f.mesh.scale.x * sx, f.mesh.scale.y * sy, f.mesh.scale.z * sz);
// normalize back roughly to base so scale doesn't drift
f.mesh.scale.x = (0.8 + Math.random() * 0.3) * sx; // slight jitter
f.mesh.scale.z = (0.8 + Math.random() * 0.3) * sz;
f.mesh.scale.y = (0.6 + Math.random() * 0.5) * sy;
const c = f.mesh.material.color;
const e = f.mesh.material.emissive;
c.setHSL(0.11 + Math.sin(t) * 0.02, 1, 0.5 + Math.sin(t * 0.8) * 0.1);
e.copy(c);
});
// sparks
for (let i = 0; i < sparkCount; i++) {
sparkPos[i * 3] += sparkVel[i * 3];
sparkPos[i * 3 + 1] += sparkVel[i * 3 + 1];
sparkPos[i * 3 + 2] += sparkVel[i * 3 + 2];
sparkVel[i * 3 + 1] -= 0.0025; // gravity
sparkLife[i] -= 0.012;
if (sparkLife[i] <= 0 || sparkPos[i * 3 + 1] < 0) {
resetSpark(i);
sparkPos[i * 3 + 1] = 0.5;
sparkLife[i] = 1.0;
}
}
sparkGeo.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
}
animate();
/* ---------- Resize ---------- */
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>Reasoning trace (8,045 tokens)
The user wants a low-poly 3D scene of a campfire in a forest clearing at night using Three.js. Let me break down the requirements:
1. Ground with low-poly rocks and logs around a campfire in center
2. At least 30 low-poly trees randomly around clearing (not on fire)
3. Campfire with animated flames, warm point light that flickers and casts shadows
4. Sparks rising as particle system
5. Dark night sky with stars and moon, fog fading distant trees
6. OrbitControls with limits to keep camera above ground
7. Renderer fills window, handles resizing
Constraints:
- Single HTML file
- Import map with specific Three.js version 0.170.0
- Everything inline
- No external resources except CDN URLs allowed
- Must look good on first paint (3 second screenshot)
- Self-playing demo mode if game-like (this isn't really a game, but scene should animate automatically)
- No localStorage, etc.
- Responsive 375px to 1280x800, no horizontal scrollbar
- Zero console errors
Let me structure the HTML:
- Import map as specified
- Three.js module script
- OrbitControls import
- CSS for full screen, no overflow
- Scene setup:
- Scene with fog (dark blue/night)
- PerspectiveCamera
- WebGLRenderer with shadows
- OrbitControls with min/max polar angle to keep above ground
- Environment:
- Ground plane (low-poly, maybe slightly uneven or just flat with grass color)
- Night sky: starfield (particles or geometry), moon (sphere or circle)
- Fog
- Campfire:
- Logs (center)
- Flames: animated using multiple cone geometries or custom shader-like animation with vertices. Low poly flames using cones with noise displacement.
- PointLight: warm orange, flickering intensity/position, cast shadow
- Sparks: Points/BufferGeometry with vertices moving up and fading
- Trees:
- Low poly tree: cylinder trunk + cone foliage (or icosahedron for foliage)
- 30+ randomly placed in clearing radius, avoiding center fire area
- Rocks:
- Low poly rocks: icosahedron or random scaled box/cone, scattered
- Animation loop:
- Flame animation (rotate, scale, vertex displacement)
- Light flicker
- Sparks update
- Render
Technical details:
- Use `MeshStandardMaterial` or `MeshLambertMaterial` for low poly look. Flat shading: true for low poly aesthetic.
- Trees: `CylinderGeometry` for trunk, `ConeGeometry` for leaves, `MeshStandardMaterial` with flat shading.
- Rocks: `IcosahedronGeometry` with detail 0 or 1, random scale, flat shading.
- Ground: `PlaneGeometry` rotated, maybe subdivided slightly with vertex noise for low-poly terrain, or just flat plane with grass texture (but no external textures, so color only). Could use `PlaneGeometry(100, 100, 8, 8)` and displace vertices randomly.
- Fire flames: Create a group of cone geometries. Animate by rotating, scaling y, and x/z position. Or use a custom shader material? Simpler: multiple `Mesh` with `ConeGeometry`, animate in loop.
- Point light: position slightly above fire, color orange, intensity flicker between 0.8 and 1.5, distance 20, decay 2. Cast shadow true.
- Ambient light: very low blue for night.
- Hemisphere light: dark blue sky, dark ground.
- Stars: `Points` with `BufferGeometry` random positions in a large sphere/cube.
- Moon: small yellow sphere in sky, maybe with a glow (another larger transparent sphere).
- Fog: `scene.fog = new THREE.FogExp2(0x020205, 0.02)` or similar.
OrbitControls limits:
- `controls.minPolarAngle = 0.1` (can't go under ground)
- `controls.maxPolarAngle = Math.PI / 2 - 0.1` (can't go below horizon? Actually polar angle 0 is top, PI/2 is horizon. So max should be less than PI/2 to keep looking somewhat down? Wait, requirement: camera stays above ground. Usually set `minPolarAngle` and `maxPolarAngle`. If camera orbits around center, polar angle from 0 (top) to PI (bottom). To stay above ground, `maxPolarAngle = Math.PI / 2 - 0.1` prevents going below horizon. But user might want to look down at fire. Let's set `minPolarAngle = 0.2` and `maxPolarAngle = 1.4` (about 80 degrees) so it can look down but not under ground. Also `minDistance = 5`, `maxDistance = 30`.
- Target at center (0,0,0). Camera starts at position (10, 8, 10).
Responsive:
- CSS: `body { margin: 0; overflow: hidden; }` and canvas fills window.
- JS: `renderer.setSize(window.innerWidth, window.innerHeight)` on resize.
First paint:
- Scene should be visible immediately. No loading screen needed since assets are procedural.
Performance:
- InstancedMesh for trees and rocks to keep it fast.
- Use `InstancedMesh` for 30+ trees: one trunk geometry, one foliage geometry, or combine into one? Better: separate InstancedMesh for trunks and foliage, or just individual meshes since 30 is small. But InstancedMesh is more efficient. Let's use InstancedMesh for trees (trunk+leaves as separate instanced meshes) and rocks.
Let's code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Low Poly Campfire</title>
<style>
body { margin: 0; overflow: hidden; background: #000; font-family: sans-serif; }
canvas { display: block; }
#info {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
color: #fff; text-align: center; pointer-events: none; user-select: none;
text-shadow: 0 0 10px rgba(255,255,255,0.5);
font-size: 1.2rem;
}
</style>
</head>
<body>
<div id="info">Low Poly Campfire Night</div>
<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>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// Setup
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020208, 0.035);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(12, 8, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.setClearColor(scene.fog.color);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 4;
controls.maxDistance = 40;
controls.minPolarAngle = 0.2;
controls.maxPolarAngle = 1.5; // about 86 deg, keeps camera from going under ground
controls.target.set(0, 0, 0);
controls.update();
// Lights
const ambientLight = new THREE.AmbientLight(0x404060, 0.4);
scene.add(ambientLight);
const moonLight = new THREE.DirectionalLight(0x6070aa, 0.3);
moonLight.position.set(-20, 30, -10);
moonLight.castShadow = true;
scene.add(moonLight);
const fireLight = new THREE.PointLight(0xffaa44, 2, 25, 1.5);
fireLight.position.set(0, 2, 0);
fireLight.castShadow = true;
fireLight.shadow.bias = -0.0001;
scene.add(fireLight);
// Ground
const groundGeo = new THREE.PlaneGeometry(80, 80, 16, 16);
groundGeo.rotateX(-Math.PI / 2);
// low poly bumps
const pos = groundGeo.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
const d = Math.sqrt(x*x + z*z);
if (d > 6) { // keep clearing flat around fire
const h = (Math.sin(x * 0.7) * Math.cos(z * 0.7) + Math.sin(x * 2) * 0.3) * 0.4;
pos.setY(i, h);
}
}
groundGeo.computeVertexNormals();
const groundMat = new THREE.MeshStandardMaterial({ color: 0x1a2e1a, flatShading: true });
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.receiveShadow = true;
scene.add(ground);
// Moon
const moonGroup = new THREE.Group();
const moonGeo = new THREE.SphereGeometry(1.5, 12, 12);
const moonMat = new THREE.MeshBasicMaterial({ color: 0xffffee });
const moon = new THREE.Mesh(moonGeo, moonMat);
moon.position.set(15, 12, -10);
moonGroup.add(moon);
// glow
const glowGeo = new THREE.SphereGeometry(2.2, 12, 12);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xffffee, transparent: true, opacity: 0.3 });
const glow = new THREE.Mesh(glowGeo, glowMat);
moonGroup.add(glow);
scene.add(moonGroup);
// Stars
const starGeo = new THREE.BufferGeometry();
const starPos = [];
for (let i = 0; i < 400; i++) {
starPos.push((Math.random() - 0.5) * 100);
starPos.push(10 + Math.random() * 40);
starPos.push((Math.random() - 0.5) * 100);
}
starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPos, 3));
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 0.15, transparent: true, opacity: 0.8 });
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
// Trees
const treeCount = 36;
const trunkGeo = new THREE.CylinderGeometry(0.15, 0.25, 1.2, 6);
const foliageGeo = new THREE.ConeGeometry(0.7, 1.4, 6);
foliageGeo.translate(0, 0.8, 0);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x3d2b1f, flatShading: true });
const leafMat = new THREE.MeshStandardMaterial({ color: 0x2d4a2d, flatShading: true });
const trunkMesh = new THREE.InstancedMesh(trunkGeo, trunkMat, treeCount);
const leafMesh = new THREE.InstancedMesh(foliageGeo, leafMat, treeCount);
trunkMesh.castShadow = true;
trunkMesh.receiveShadow = true;
leafMesh.castShadow = true;
leafMesh.receiveShadow = true;
const dummy = new THREE.Object3D();
const placed = [];
for (let i = 0; i < treeCount; i++) {
let r, theta, x, z, valid;
do {
r = 14 + Math.random() * 22;
theta = Math.random() * Math.PI * 2;
x = r * Math.cos(theta);
z = r * Math.sin(theta);
valid = Math.sqrt(x*x + z*z) > 8; // away from fire
} while (!valid);
placed.push({x, z, r});
dummy.position.set(x, 0.6, z);
dummy.rotation.y = Math.random() * Math.PI;
dummy.scale.set(0.8 + Math.random() * 0.4, 1, 0.8 + Math.random() * 0.4);
dummy.updateMatrix();
trunkMesh.setMatrixAt(i, dummy.matrix);
dummy.position.set(x, 0.6, z);
dummy.scale.set(0.9 + Math.random() * 0.5, 0.8 + Math.random() * 0.5, 0.9 + Math.random() * 0.5);
dummy.updateMatrix();
leafMesh.setMatrixAt(i, dummy.matrix);
}
scene.add(trunkMesh);
scene.add(leafMesh);
// Rocks
const rockCount = 14;
const rockGeo = new THREE.IcosahedronGeometry(0.5, 0);
const rockMat = new THREE.MeshStandardMaterial({ color: 0x4a4a4a, flatShading: true });
const rockMesh = new THREE.InstancedMesh(rockGeo, rockMat, rockCount);
rockMesh.castShadow = true;
rockMesh.receiveShadow = true;
for (let i = 0; i < rockCount; i++) {
let r, theta, x, z;
do {
r = 5 + Math.random() * 16;
theta = Math.random() * Math.PI * 2;
x = r * Math.cos(theta);
z = r * Math.sin(theta);
} while (Math.sqrt(x*x + z*z) < 6 || Math.sqrt(x*x + z*z) > 24);
const sx = 0.4 + Math.random() * 0.8;
const sy = 0.2 + Math.random() * 0.5;
const sz = 0.4 + Math.random() * 0.8;
dummy.position.set(x, sy/2, z);
dummy.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI);
dummy.scale.set(sx, sy, sz);
dummy.updateMatrix();
rockMesh.setMatrixAt(i, dummy.matrix);
}
scene.add(rockMesh);
// Campfire logs
const logGroup = new THREE.Group();
const logGeo = new THREE.CylinderGeometry(0.18, 0.18, 1.0, 6);
const logMat = new THREE.MeshStandardMaterial({ color: 0x3d2b1f, flatShading: true });
function addLog(x, z, rotY, rotX) {
dummy.position.set(x, 0.5, z);
dummy.rotation.set(rotX, rotY, 0);
dummy.scale.set(1, 1, 1);
dummy.updateMatrix();
const mesh = new THREE.Mesh(logGeo, logMat);
mesh.position.copy(dummy.position);
mesh.rotation.copy(dummy.rotation);
mesh.castShadow = true;
mesh.receiveShadow = true;
logGroup.add(mesh);
}
addLog(-0.6, -0.4, 0, 0);
addLog(0.6, 0.4, Math.PI/2, 0);
addLog(0, -0.8, Math.PI/4, 0);
addLog(0.8, -0.2, -Math.PI/4, 0);
// Flames group
const flameCount = 6;
const flameGeo = new THREE.ConeGeometry(0.35, 0.9, 4);
flameGeo.rotateX(-Math.PI / 2); // point up? Actually cone points up by default (y). Let's keep y up.
// Apply low poly vertex jitter
const flamePos = flameGeo.attributes.position;
const flameOriginal = [];
for (let i = 0; i < flamePos.count; i++) flameOriginal.push(flamePos.getX(i), flamePos.getY(i), flamePos.getZ(i));
const flames = [];
for (let i = 0; i < flameCount; i++) {
const mat = new THREE.MeshStandardMaterial({ color: 0xffaa00, flatShading: true });
const mesh = new THREE.Mesh(flameGeo, mat);
mesh.position.set((Math.random()-0.5)*0.6, 0.5 + Math.random()*0.8, (Math.random()-0.5)*0.6);
mesh.scale.set(0.8 + Math.random()*0.4, 0.6 + Math.random()*0.6, 0.8 + Math.random()*0.4);
mesh.castShadow = true;
logGroup.add(mesh);
flames.push({
mesh,
baseY: mesh.position.y,
baseScale: mesh.scale.clone(),
speed: 0.8 + Math.random() * 0.6,
offset: Math.random() * Math.PI * 2,
vertexData: flameOriginal.slice()
});
}
scene.add(logGroup);
// Embers / Glow under logs? Maybe just fireLight.
// Sparks particle system
const sparkCount = 60;
const sparkGeo = new THREE.BufferGeometry();
const sparkPos = new Float32Array(sparkCount * 3);
const sparkVel = new Float32Array(sparkCount * 3);
const sparkLife = new Float32Array(sparkCount);
for (let i = 0; i < sparkCount; i++) {
resetSpark(i, true);
}
sparkGeo.setAttribute('position', new THREE.BufferAttribute(sparkPos, 3));
const sparkMat = new THREE.PointsMaterial({ color: 0xffaa00, size: 0.08, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending });
const sparks = new THREE.Points(sparkGeo, sparkMat);
scene.add(sparks);
function resetSpark(i, initial) {
const angle = Math.random() * Math.PI * 2;
const spread = 0.3 + Math.random() * 0.3;
sparkPos[i*3] = Math.cos(angle) * spread;
sparkPos[i*3+1] = 0.5 + Math.random() * 1.0;
sparkPos[i*3+2] = Math.sin(angle) * spread;
sparkVel[i*3] = (Math.random()-0.5) * 0.02;
sparkVel[i*3+1] = 0.04 + Math.random() * 0.06;
sparkVel[i*3+2] = (Math.random()-0.5) * 0.02;
sparkLife[i] = initial ? Math.random() : 1.0;
}
// UI hint (only first seconds then fade?)
const info = document.getElementById('info');
setTimeout(() => { info.style.opacity = '0'; }, 4000);
// Animation state
let time = 0;
function animate() {
requestAnimationFrame(animate);
time += 0.03;
controls.update();
// Fire light flicker
const flicker = 0.8 + Math.sin(time * 11) * 0.3 + Math.random() * 0.2;
fireLight.intensity = THREE.MathUtils.clamp(flicker, 0.5, 2.2);
fireLight.position.y = 2 + Math.sin(time * 7) * 0.1;
// Animate flames
flames.forEach((f, i) => {
const t = time * f.speed + f.offset;
f.mesh.position.y = f.baseY + Math.sin(t * 2) * 0.05;
f.mesh.rotation.y = t * 0.5;
f.mesh.rotation.x = Math.sin(t) * 0.2;
f.mesh.scale.x = f.baseScale.x * (1 + Math.sin(t * 1.3) * 0.1);
f.mesh.scale.z = f.baseScale.z * (1 + Math.cos(t * 1.3) * 0.1);
f.mesh.scale.y = f.baseScale.y * (1 + Math.sin(t * 1.7) * 0.15);
// color shift between orange and yellow
const c = f.mesh.material.color;
c.setHSL(0.12 + Math.sin(t)*0.02, 1, 0.55 + Math.sin(t*0.7)*0.1);
});
// Update sparks
for (let i = 0; i < sparkCount; i++) {
sparkPos[i*3] += sparkVel[i*3];
sparkPos[i*3+1] += sparkVel[i*3+1];
sparkPos[i*3+2] += sparkVel[i*3+2];
sparkVel[i*3+1] -= 0.002; // gravity
sparkLife[i] -= 0.01;
if (sparkLife[i] <= 0 || sparkPos[i*3+1] < 0) {
resetSpark(i, false);
sparkPos[i*3+1] = 0.5; // start near fire again
sparkLife[i] = 1.0;
}
}
sparkGeo.attributes.position.needsUpdate = true;
// Spark opacity based on life? PointsMaterial doesn't support per-vertex opacity easily without shader.
// Just keep them additive.
renderer.render(scene, camera);
}
animate();
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
```
Wait, there are some issues:
1. `coneGeometry` by default points along Y axis? In Three.js, ConeGeometry points up along Y by default (from center to top at y=height/2). Actually the cone's apex is at (0, height/2, 0) and base at y=-height/2. So if I want flame pointing up, that's fine. I shouldn't rotate X.
2. For low-poly look, `flatShading: true` is good.
3. InstancedMesh with `dummy.updateMatrix()` then `setMatrixAt`. Correct.
4. Need to ensure `treeCount` is at least 30. I set 36.
5. Moon position and stars should be far away? Stars at y=10-50, but camera can move. Maybe better to put stars on a large sphere or just random in skybox. Since no skybox, Points at y > 0 maybe enough. But if camera looks down, stars might appear under ground? Stars positions y between 10 and 40. If camera looks down at ground from height 8, stars are above. Fine.
6. The ground plane bumps: I used `pos.setY(i, h)` but for a plane rotated X, the vertex Y is actually height. The plane geometry's position attribute has x, y, z where y is up before rotation. So setting Y is correct for height.
7. Shadow map: `renderer.shadowMap.enabled = true`. Lights cast shadow. Ground receives. Trees cast. Good.
8. Fog color matches clear color.
9. The requirement says "The renderer fills the window and handles resizing correctly." Done.
10. "OrbitControls to look around, with limits so the camera stays above the ground." I set minPolarAngle and maxPolarAngle. However, if camera gets too close to ground, it might clip. `minDistance = 4` and target at (0,0,0) means camera stays at least 4 units from center. Since ground is at y=0 and camera min height depends on polar angle. With maxPolarAngle 1.5 rad (~86 deg), camera can look almost horizontally. Its height = distance * sin(polarAngle)? Actually camera position relative to target: y = distance * cos(polarAngle)? Wait, spherical: phi = polar angle from positive Y? In Three.js, polar angle is angle from positive Y axis? No, in OrbitControls, polar angle is angle from the positive y-axis? Actually it's the angle from the positive y axis? Let me recall: In Three.js spherical coordinates, phi is polar angle (from positive Y axis?), theta is azimuth around Y. Wait: `setFromVector3` sets phi = acos(y/radius). So phi=0 is +Y (top), phi=PI/2 is horizon (equator). So minPolarAngle=0.2 means can't go too close to top. maxPolarAngle=1.5 means can't go past ~86 degrees from top, i.e., can't look straight down? Wait, phi=PI/2 is looking at horizon. If maxPolarAngle is 1.5 (< PI/2 ~1.57), camera cannot reach the horizon plane? Actually if phi is small, camera is high above. If phi approaches PI/2, camera is at same height as target (looking horizontally). So maxPolarAngle=1.5 means camera stays slightly above target height. Since target is at y=0, camera height = distance * cos(phi). If phi max=1.5, cos(1.5) ≈ 0.07, so min height ≈ 4 * 0.07 = 0.28. That's okay above ground. But user might want to look down at fire. They can because target is at fire center and camera can orbit around it at height > 0.28. Good enough.
But the requirement "stays above the ground" might mean camera doesn't clip below ground plane. The limits help but if user zooms in close to ground at an angle, they could still go below? With maxPolarAngle < PI/2, camera is always above target y=0 by some amount. minDistance=4 ensures not too close. So fine.
11. First paint: All geometry is procedural, no textures to load. So first paint should show something.
12. Self-playing demo: Not a game, but scene animates automatically. Good.
13. No horizontal scrollbar: CSS body overflow hidden.
14. Responsive: renderer handles resize. On mobile 375px, aspect ratio handled.
15. Click to start? Not required since not a game. But requirement says games must start in self-playing demo mode behind click message. This is not a game, it's a scene. So okay.
Let's refine the flame geometry. ConeGeometry(0.35, 0.9, 4) has radius 0.35, height 0.9, radial segments 4. That's very low poly. Good. But the base is at y=-0.45 and top at y=0.45. I want flames sitting on logs at y around 0.5 to 1.5. So position y should be around 0.8 + random. Let's set `mesh.position.set(..., 0.8 + Math.random()*0.8, ...)`. The cone points up naturally.
Add some variation: scale differently.
Also add an emissive material? Could use `MeshStandardMaterial` with `emissive` to make flames glow. But point light already lights area. The flame mesh itself can be `MeshBasicMaterial` or `MeshStandardMaterial` with emissive. Let's use `MeshStandardMaterial` with `emissive: 0xffaa00`, `emissiveIntensity: 1`, `color: 0xffaa00`. But then they won't be affected by light much, but they will glow. That's okay for low poly.
Actually to make them look like fire, maybe use `MeshBasicMaterial` with bright colors. But `MeshStandardMaterial` with emissive is fine.
Sparks: `PointsMaterial` with `sizeAttenuation: true` maybe. Add `sizeAttenuation: true` so distant sparks smaller.
Trees: Should have flat shading. Trunk cylinder 6 segments. Cone foliage 6 segments.
Rocks: IcosahedronGeometry(0.5, 0) is very low poly (20 faces). Good.
Let's check for console errors:
- Import map correct.
- OrbitControls import path correct.
- `THREE.MathUtils.clamp` exists.
- `dummy.updateMatrix()` before `setMatrixAt`.
- All geometries disposed? Not needed for demo.
- `renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))` fine.
Potential issue: `groundGeo.attributes.position` after `rotateX` - the attribute is still in local space, rotating the geometry doesn't change the buffer attribute values, just applies matrix. So setting Y based on x,z is correct because local Y is up before rotation. Yes.
But wait: `groundGeo.rotateX(-Math.PI / 2)` rotates the geometry vertices. If I then modify position.y, it modifies local Y before rotation. Since the rotation is applied as a matrix, the buffer attribute remains unrotated, and the rotation matrix is applied on top. So modifying position.y after rotateX will displace vertices in local space (which is pre-rotation). Since rotation is around X, local Y becomes world Z? Actually `rotateX` multiplies the vertex positions by a rotation matrix. But `rotateX` on BufferGeometry updates the positions? Let me check: `geometry.rotateX(angle)` rotates the vertices in place? In three.js, `BufferGeometry.rotateX` rotates the geometry by applying the rotation to the position attribute and normals? Actually `BufferGeometry.rotateX` applies a rotation to the position attribute directly (it modifies the buffer). So after `rotateX`, the Y values are already rotated. Then modifying Y is modifying the already-rotated Y, which is not "up" anymore.
Let me verify: In three.js r170, `BufferGeometry.rotateX( angle )`: "Rotates the geometry around the X axis by the given angle." It uses `applyMatrix4` which transforms position, normal, etc. So yes, it modifies the position buffer. So after rotating, Y is no longer vertical. I should create the plane flat, displace Y, then rotate.
Better approach:
```
const groundGeo = new THREE.PlaneGeometry(80, 80, 16, 16);
// displace Y (vertical before rotation)
const pos = groundGeo.attributes.position;
for (...) { pos.setY(i, h); }
groundGeo.computeVertexNormals();
groundGeo.rotateX(-Math.PI / 2);
```
Yes, displace first, then rotate.
Also need to keep clearing flat: `if (d > 6)`. That's fine.
Let's update the code accordingly.
Also add a simple "Click to interact" hint? Not required.
Moon: `moon.position.set(15, 12, -10)` - but with fog, moon might fade. FogExp2 affects everything based on distance from camera. Moon at distance ~20 might be somewhat fogged. That's okay. But moon light directional light doesn't cast visible light if too weak. It's fine.
Stars: `size: 0.15` might be too small or large. Let's use `size: 0.2`.
Let's refine tree placement: ensure they don't overlap too much. Simple random is fine for demo.
Add a low-poly "campfire ring" stones? Already have rocks.
Let's add a gentle animation to the whole scene? Maybe wind blowing trees slightly? Could add slight rotation to leafMesh instances over time, but InstancedMesh matrices would need updating every frame. That's okay but might be overkill. Let's keep it simple: just fire and sparks.
However, to make it more interesting, maybe slowly rotate the moon/stars? Not necessary.
Let's ensure the fire light's shadow camera covers the area. Default shadow camera is okay for small area.
Set `fireLight.shadow.mapSize.width = 1024; height = 1024;` for better shadows.
Also `ground.receiveShadow = true`.
For InstancedMesh, need to set `castShadow` and `receiveShadow` on the mesh itself. `InstancedMesh` has these properties.
Now, the `dummy` object is reused. For leaves, I set dummy.position and scale. Good.
One more thing: The requirement says "at least 30 low-poly trees placed randomly around the clearing (not on the fire)." I have 36.
Let's check if any tree could be inside fire radius (8). I check `Math.sqrt(x*x+z*z) > 8`. Good.
Now, code structure. I will output the complete HTML.
Let's write the final code carefully.
Potential error: `THREE.MathUtils` is available in Three.js. Yes.
`sparkGeo.attributes.position.needsUpdate = true;` inside loop each frame. That's fine for 60 particles.
Use `renderer.setAnimationLoop(animate)` instead of requestAnimationFrame? Either is fine. `requestAnimationFrame` is standard.
Add `window.addEventListener('resize', ...)` correctly.
Info text: Maybe add instructions? "Orbit to look around". But pointer-events none. It's fine.
Let's add a fade out for info after 4 seconds.
Let's ensure CSS doesn't cause scrollbars: `html, body { margin: 0; height: 100%; overflow: hidden; }`.
Canvas `display: block`.
Info div centered.
Now write final code.
Double-check flame geometry: `new THREE.ConeGeometry(0.35, 0.9, 4)`. The cone points up by default (apex at +Y). Good. No rotation needed.
Flame base position: `mesh.position.set(x, 0.8 + Math.random()*0.6, z)`. The cone extends from y=-0.45 to y=0.45 in local space. So if base is at 0.8, the flame spans from 0.35 to 1.25. That sits on logs at y=0.5. Good.
Flame colors: use `MeshStandardMaterial` with `emissive`. Let's set `color: 0xffaa00`, `emissive: 0xff4400`, `emissiveIntensity: 1`. But StandardMaterial with emissive will look bright even without light. Good.
Actually for low poly, maybe `MeshBasicMaterial` is flatter. But StandardMaterial with flatShading looks nicer. I'll use `MeshStandardMaterial` with `flatShading: true` and `emissive` maybe.
Let's make the flame material have `emissive: 0xffaa00`, `emissiveIntensity: 2`, and `color: 0xffaa00`. But `MeshStandardMaterial` with emissive might still be affected by lighting and look dark in shadows. Since fire is bright, emissive ensures visibility.
For sparks: `color: 0xffaa00`, `blending: THREE.AdditiveBlending`, `transparent: true`, `opacity: 0.8`, `size: 0.1`, `sizeAttenuation: true`.
I think this will look good.
Let's add `scene.add(new THREE.HemisphereLight(0x000033, 0x000011, 0.2));` for subtle ground/s sky lighting.
Ambient light: `0x202040, 0.3`.
Alright, let's compose final answer.