Dots3-Note Preview (free)
by Dots StudioReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 1 min 51 s
- Cost
- $0
- Output tokens
- 10,380
- Spent thinking
- 6,549 tokens
- Speed
- 93.7 tok/s
- Console errors
- 0
- First token after
- 712 ms
- First visible text after
- 1 min 22 s
- Prompt tokens
- 563
- Code size
- 10.0 KB
- Finish reason
- stop
- Served by
- AtlasCloud
- Model id
dots-studio/dots-3-note-preview: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 · 284 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Campfire Scene</title>
<style type="text/css">
html, body { margin: 0; padding: 0; overflow: hidden; height: 100%; background: #000; }
canvas { display: block; }
</style>
</head>
<body>
<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, camera, renderer
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000011, 0.02);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// OrbitControls with limits to keep camera above ground
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minPolarAngle = 0.2;
controls.maxPolarAngle = Math.PI / 2 - 0.05;
controls.minDistance = 5;
controls.maxDistance = 50;
controls.enableKeys = false; // disable keyboard to avoid needing a click-to-start message
// Lights
scene.add(new THREE.AmbientLight(0x333344, 1));
const moonLight = new THREE.DirectionalLight(0x445566, 0.5);
moonLight.position.set(10, 20, 5);
moonLight.castShadow = true;
moonLight.shadow.mapSize.set(1024, 1024);
moonLight.shadow.camera.near = 0.5;
moonLight.shadow.camera.far = 50;
scene.add(moonLight);
// Campfire point light (warm, flickering, casts shadows)
const fireLight = new THREE.PointLight(0xff6622, 2, 0, 1.5);
fireLight.position.set(0, 1.2, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.set(256, 256);
scene.add(fireLight);
// Ground (low-poly plane)
const groundGeometry = new THREE.PlaneGeometry(200, 200);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x224411, roughness: 1, flatShading: true });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Low-poly rocks
const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 1, flatShading: true });
for (let i = 0; i < 10; i++) {
const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.5, 0), rockMaterial);
rock.position.set((Math.random() - 0.5) * 10, 0.5, (Math.random() - 0.5) * 10);
rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
rock.castShadow = true;
rock.receiveShadow = true;
scene.add(rock);
}
// Low-poly logs
const logMaterial = new THREE.MeshStandardMaterial({ color: 0x4a2f1c, roughness: 1, flatShading: true });
for (let i = 0; i < 6; i++) {
const log = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.3, 2, 6), logMaterial);
log.position.set((Math.random() - 0.5) * 8, 0.5, (Math.random() - 0.5) * 8);
log.rotation.set(0, Math.random() * Math.PI, 0);
log.castShadow = true;
log.receiveShadow = true;
scene.add(log);
}
// Campfire base stones
const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 1, flatShading: true });
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const stone = new THREE.Mesh(new THREE.IcosahedronGeometry(0.3, 0), stoneMaterial);
stone.position.set(Math.cos(angle) * 0.8, 0.3, Math.sin(angle) * 0.8);
stone.castShadow = true;
stone.receiveShadow = true;
scene.add(stone);
}
// Animated flames (canvas sprite)
function createFlameTexture() {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
const gradient = ctx.createRadialGradient(128, 128, 0, 128, 128, 128);
gradient.addColorStop(0, 'rgba(255, 200, 50, 1)');
gradient.addColorStop(0.5, 'rgba(255, 100, 20, 0.8)');
gradient.addColorStop(1, 'rgba(255, 50, 0, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 256, 256);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
const flameTexture = createFlameTexture();
const flameMaterial = new THREE.SpriteMaterial({
map: flameTexture,
color: 0xff6622,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});
const flames = [];
for (let i = 0; i < 5; i++) {
const flame = new THREE.Sprite(flameMaterial);
flame.position.set((Math.random() - 0.5) * 0.4, 1.0 + Math.random() * 0.5, (Math.random() - 0.5) * 0.4);
flame.scale.setScalar(0.5 + Math.random() * 0.5);
flame.material.opacity = 0.7 + Math.random() * 0.3;
scene.add(flame);
flames.push(flame);
}
// Sparks particle system
const sparkCount = 60;
const sparkGeometry = new THREE.BufferGeometry();
const sparkPositions = new Float32Array(sparkCount * 3);
const sparkVelocities = new Float32Array(sparkCount * 3);
const sparkLifetimes = new Float32Array(sparkCount);
for (let i = 0; i < sparkCount; i++) {
sparkPositions[i * 3] = (Math.random() - 0.5) * 0.5;
sparkPositions[i * 3 + 1] = 1.0 + Math.random() * 0.5;
sparkPositions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
sparkVelocities[i * 3] = (Math.random() - 0.5) * 0.02;
sparkVelocities[i * 3 + 1] = 0.02 + Math.random() * 0.03;
sparkVelocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
sparkLifetimes[i] = Math.random() * 2;
}
sparkGeometry.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
const sparkMaterial = new THREE.PointsMaterial({
color: 0xffaa33,
size: 0.1,
transparent: true,
opacity: 0.8,
blending: THREE.AdditiveBlending
});
const sparks = new THREE.Points(sparkGeometry, sparkMaterial);
scene.add(sparks);
// Low-poly trees
function createTree() {
const tree = new THREE.Group();
const trunk = new THREE.Mesh(
new THREE.CylinderGeometry(0.1, 0.15, 1.5, 6),
new THREE.MeshStandardMaterial({ color: 0x3a2612, roughness: 1, flatShading: true })
);
trunk.position.y = 0.75;
trunk.castShadow = true;
trunk.receiveShadow = true;
tree.add(trunk);
const leaves = new THREE.Mesh(
new THREE.ConeGeometry(0.8, 1.5, 6),
new THREE.MeshStandardMaterial({ color: 0x226622, roughness: 1, flatShading: true })
);
leaves.position.y = 1.5;
leaves.castShadow = true;
leaves.receiveShadow = true;
tree.add(leaves);
return tree;
}
for (let i = 0; i < 35; i++) {
const tree = createTree();
let angle, radius;
do {
angle = Math.random() * Math.PI * 2;
radius = 5 + Math.random() * 15;
} while (radius < 4); // keep away from the fire
tree.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
tree.rotation.y = Math.random() * Math.PI;
const scale = 0.5 + Math.random() * 0.8;
tree.scale.set(scale, scale, scale);
scene.add(tree);
}
// Night sky (dark sphere)
const skyGeometry = new THREE.SphereGeometry(500, 32, 32);
const skyMaterial = new THREE.MeshBasicMaterial({ color: 0x000011, side: THREE.BackSide });
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
scene.add(sky);
// Stars
const starCount = 200;
const starGeometry = new THREE.BufferGeometry();
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 400;
starPositions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
starPositions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
starPositions[i * 3 + 2] = r * Math.cos(phi);
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, sizeAttenuation: true });
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
// Moon
const moon = new THREE.Mesh(
new THREE.SphereGeometry(1, 32, 32),
new THREE.MeshBasicMaterial({ color: 0xcccccc })
);
moon.position.set(20, 30, 10);
scene.add(moon);
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Animate flames
flames.forEach((flame, i) => {
const pulse = 0.8 + Math.sin(performance.now() * 0.01 + i) * 0.2;
flame.scale.setScalar((0.5 + Math.random() * 0.5) * pulse);
flame.material.opacity = 0.6 + Math.random() * 0.4;
flame.position.y = 1.0 + Math.random() * 0.5;
});
// Animate sparks
for (let i = 0; i < sparkCount; i++) {
sparkPositions[i * 3] += sparkVelocities[i * 3];
sparkPositions[i * 3 + 1] += sparkVelocities[i * 3 + 1];
sparkPositions[i * 3 + 2] += sparkVelocities[i * 3 + 2];
sparkVelocities[i * 3 + 1] -= 0.0005; // gravity
sparkLifetimes[i] -= 0.01;
if (sparkLifetimes[i] <= 0 || sparkPositions[i * 3 + 1] < 0.5) {
sparkPositions[i * 3] = (Math.random() - 0.5) * 0.5;
sparkPositions[i * 3 + 1] = 1.0;
sparkPositions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
sparkVelocities[i * 3] = (Math.random() - 0.5) * 0.02;
sparkVelocities[i * 3 + 1] = 0.02 + Math.random() * 0.03;
sparkVelocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
sparkLifetimes[i] = 2.0;
}
}
sparkGeometry.attributes.position.needsUpdate = true;
// Flicker fire light
fireLight.intensity = 1.5 + Math.random() * 0.5 + Math.sin(performance.now() * 0.01) * 0.5;
controls.update();
renderer.render(scene, camera);
}
animate();
// Handle window resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>Reasoning trace (6,549 tokens)
The user wants a single HTML file with Three.js, using the import map specified. I need to build a low-poly 3D campfire scene. Let's break down the requirements:
- Ground with rocks and logs around campfire.
- At least 30 low-poly trees randomly placed around clearing (not on fire).
- Campfire with animated flames and warm point light that flickers and casts shadows.
- Sparks rising as particle system.
- Dark night sky with stars and moon, plus fog.
- OrbitControls with limits so camera stays above ground.
- Renderer fills window and handles resize.
I need to use Three.js r0.170.0 from the import map. I'll use ES modules with import. I can import OrbitControls from the addons path.
I'll create a scene, camera, renderer. Add ambient light, maybe a directional moon light. Add a point light at the fire with warm color, flickering intensity. Enable shadows.
Ground: a plane with a low-poly texture or just a mesh with a dark green/brown color. Maybe use a MeshStandardMaterial with a simple color. For low-poly, I can use a plane with many segments but flat shading, or just a plane with a wireframe? Actually low-poly style: I can use flat shading on all materials and simple geometry. I can create a ground plane with a few subdivisions and then add some noise? Or just a flat plane with a color. To make it low-poly, I can use a plane with a low segment count but then it won't have detail. Alternatively, I can use a plane with a displacement map, but that's not low-poly. For a low-poly look, I can use a plane with a few vertices and then use a MeshStandardMaterial with flat shading. But a simple plane is just a single quad, which is low-poly. I can add some small rocks and logs as simple boxes or cylinders.
Trees: I need at least 30 low-poly trees. I can create a simple tree using a cylinder for trunk and a cone for leaves, or a low-poly pyramid. I'll create a function that generates a tree with random position, rotation, scale. Place them around the clearing, ensuring they are not on the fire (the fire is at center). I'll place them in a ring or randomly within a certain radius, but not too close to center.
Campfire: I can create a small fire using a few particle systems or a flame mesh. For animated flames, I can use a plane with a flame texture and animate its vertices, or use a shader. Simpler: use a Mesh with a flame sprite texture and animate its scale and rotation. Or use a few point light and a flame object that flickers. I'll create a group of flame planes with additive blending and animated opacity. Or use a custom particle system for flames. Since they want a particle system for sparks, I can also use a particle system for flames. But requirement says "animated flames" and "sparks rising as a simple particle system". So I can make flames with a few meshes that scale and flicker, and sparks with a Points object.
I'll create a flame using a plane with a radial gradient texture (orange to transparent) and animate its scale and opacity. Or use a Sprite. For low-poly, maybe a simple flame mesh with a flame texture. I'll create a flame sprite using canvas texture, then use several with different scales and speeds.
Sparks: use BufferGeometry with positions and velocities, update in animation loop.
Fog: use FogExp2 with a dark color and density.
Sky: not a texture, but a gradient background? Or a sphere with a star texture. I can create a sky dome with a shader or a simple gradient. For stars, I can use a Points system with random points on a sphere. Moon: a sphere with a moon texture, or just a white sphere with emission.
OrbitControls: import from three/addons/controls/OrbitControls.js. Set limits: I can limit the camera's polar angle (theta) so it can't go below the ground. I'll set minPolarAngle to something like Math.PI/2 - 0.1 (just above horizon) or maybe a bit higher to keep camera above ground. Actually, to keep camera above ground, I need to limit the polar angle so it doesn't go below the ground plane. Since the ground is at y=0, camera should have y > 0. I can set minPolarAngle to 0.1 (radians) and maxPolarAngle to Math.PI/2 - 0.1? Wait, polar angle is measured from positive y-axis. So if polar angle is 0, camera is directly above; if PI/2, camera is on the horizon; if > PI/2, camera is below horizon. So to keep camera above ground, I should set maxPolarAngle to something less than PI/2, e.g., PI/2 - 0.1. And minPolarAngle to something small, maybe 0.1. Also I can limit azimuth.
I'll also add a small ambient light and a directional moon light to cast shadows.
Let's plan the code structure.
First, import Three.js and OrbitControls.
Set up scene, camera, renderer.
Create ground: a plane with a low-poly green/brown material. I'll use a MeshStandardMaterial with a dark green color and roughness. I'll add some rocks: simple boxes or icosahedrons with a gray material. Logs: cylinders with a brown material.
Create campfire: a group of small stones or logs in a circle, then flames on top. I'll create a fire texture using canvas.
Create trees: a function that returns a tree group. I'll use a cylinder for trunk and a cone for leaves. I'll randomize position within a radius, ensuring not too close to center. I'll also randomize scale and rotation.
Create sparks: a particle system with BufferGeometry. I'll set positions randomly in a small volume above fire, then animate them upward with gravity and fade.
Create sky: a sphere with a shader or a gradient background. I'll use a large sphere with a MeshBasicMaterial with a sky texture or a vertex shader. Simpler: use a clear color with a gradient? Actually, I can set the renderer's background to a dark blue, but that's not a sky with stars. I'll create a sky dome using a sphere with a custom shader that colors the top lighter and bottom darker, and then add stars as points on the sphere. Or I can just use a Points system for stars and a moon sphere, and set the background to a dark color. But the sky should be visible. I'll create a large sphere with a MeshBasicMaterial with a color that changes with vertex normal? Or just use a cube with a gradient texture. For simplicity, I'll create a sky sphere with a shader material that outputs a dark blue color, and then add stars as a separate points system. Or I can use a single Points system for stars and a moon mesh, and set the scene background to a dark blue. That works.
Fog: scene.fog = new THREE.FogExp2(0x000011, 0.02);
Animation loop: update flames (scale, opacity), update sparks (positions, velocities), update point light flicker, update OrbitControls.
Handle resize.
I'll also add a "Click or press a key to start" message? The requirement says "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." OrbitControls uses mouse/touch, not keyboard. But OrbitControls does have keyboard shortcuts (arrow keys, etc.)? Actually, OrbitControls has optional keyboard controls. By default, it includes keyboard controls. So I should either disable keyboard or show the message. I'll disable keyboard to avoid the need for the message. I can set controls.enableKeys = false. Or I can just leave it and show the message. But the requirement says "if the demo uses the keyboard", so if I disable keyboard, I don't need the message. I'll disable keyboard to keep it simple.
Now, let's write the code.
I'll use a single HTML file with the import map.
I need to be careful with the import map: it maps "three" to the module, and "three/addons/" to the examples/jsm path. So I can import OrbitControls like: import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
I'll also need to import GLTFLoader if needed, but I'm not using it.
Let's start coding.
First, the HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Campfire Scene</title>
<style type="text/css">
html, body { margin: 0; padding: 0; overflow: hidden; height: 100%; }
canvas { display: block; }
</style>
</head>
<body>
<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">
// code here
</script>
</body>
</html>
Now the JS code.
I'll create a main function or just top-level code.
Let's define scene, camera, renderer.
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000011, 0.02);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// OrbitControls
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minPolarAngle = 0.2; // camera can't go too low
controls.maxPolarAngle = Math.PI / 2 - 0.05; // can't go below horizon
controls.minDistance = 5;
controls.maxDistance = 50;
controls.enableKeys = false; // disable keyboard to avoid needing click message
// Lights
scene.add(new THREE.AmbientLight(0x333344, 1));
const moonLight = new THREE.DirectionalLight(0x445566, 0.5);
moonLight.position.set(10, 20, 5);
moonLight.castShadow = true;
moonLight.shadow.mapSize.width = 1024;
moonLight.shadow.mapSize.height = 1024;
moonLight.shadow.camera.near = 0.5;
moonLight.shadow.camera.far = 50;
scene.add(moonLight);
// Campfire point light
const fireLight = new THREE.PointLight(0xff6622, 2, 0, 1.5);
fireLight.position.set(0, 1, 0);
fireLight.castShadow = true;
fireLight.shadow.mapSize.width = 256;
fireLight.shadow.mapSize.height = 256;
scene.add(fireLight);
// Ground
const groundGeometry = new THREE.PlaneGeometry(200, 200);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x224411, roughness: 1, flatShading: true });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// Rocks
const rockMaterial = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 1, flatShading: true });
for (let i = 0; i < 10; i++) {
const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(0.5, 0), rockMaterial);
rock.position.set((Math.random() - 0.5) * 10, 0.5, (Math.random() - 0.5) * 10);
rock.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
rock.castShadow = true;
rock.receiveShadow = true;
scene.add(rock);
}
// Logs
const logMaterial = new THREE.MeshStandardMaterial({ color: 0x4a2f1c, roughness: 1, flatShading: true });
for (let i = 0; i < 6; i++) {
const log = new THREE.Mesh(new THREE.CylinderGeometry(0.2, 0.3, 2, 6), logMaterial);
log.position.set((Math.random() - 0.5) * 8, 0.5, (Math.random() - 0.5) * 8);
log.rotation.set(0, Math.random() * Math.PI, 0);
log.castShadow = true;
log.receiveShadow = true;
scene.add(log);
}
// Campfire base: small stones
const stoneMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 1, flatShading: true });
for (let i = 0; i < 8; i++) {
const angle = (i / 8) * Math.PI * 2;
const stone = new THREE.Mesh(new THREE.IcosahedronGeometry(0.3, 0), stoneMaterial);
stone.position.set(Math.cos(angle) * 0.8, 0.3, Math.sin(angle) * 0.8);
stone.castShadow = true;
stone.receiveShadow = true;
scene.add(stone);
}
// Flames: create a flame sprite using canvas
function createFlameTexture() {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const context = canvas.getContext('2d');
const gradient = context.createRadialGradient(128, 128, 0, 128, 128, 128);
gradient.addColorStop(0, 'rgba(255, 200, 50, 1)');
gradient.addColorStop(0.5, 'rgba(255, 100, 20, 0.8)');
gradient.addColorStop(1, 'rgba(255, 50, 0, 0)');
context.fillStyle = gradient;
context.fillRect(0, 0, 256, 256);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
const flameTexture = createFlameTexture();
const flameMaterial = new THREE.SpriteMaterial({ map: flameTexture, color: 0xff6622, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false });
const flames = [];
for (let i = 0; i < 5; i++) {
const flame = new THREE.Sprite(flameMaterial);
flame.position.set((Math.random() - 0.5) * 0.4, 1.0 + Math.random() * 0.5, (Math.random() - 0.5) * 0.4);
flame.scale.set(1, 1, 1).multiplyScalar(0.5 + Math.random() * 0.5);
flame.material.opacity = 0.7 + Math.random() * 0.3;
scene.add(flame);
flames.push(flame);
}
// Sparks particle system
const sparkCount = 50;
const sparkGeometry = new THREE.BufferGeometry();
const sparkPositions = new Float32Array(sparkCount * 3);
const sparkVelocities = new Float32Array(sparkCount * 3);
const sparkLifetimes = new Float32Array(sparkCount);
for (let i = 0; i < sparkCount; i++) {
sparkPositions[i * 3] = (Math.random() - 0.5) * 0.5;
sparkPositions[i * 3 + 1] = 1.0 + Math.random() * 0.5;
sparkPositions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
sparkVelocities[i * 3] = (Math.random() - 0.5) * 0.02;
sparkVelocities[i * 3 + 1] = 0.02 + Math.random() * 0.03;
sparkVelocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
sparkLifetimes[i] = Math.random() * 2;
}
sparkGeometry.setAttribute('position', new THREE.BufferAttribute(sparkPositions, 3));
const sparkMaterial = new THREE.PointsMaterial({ color: 0xffaa33, size: 0.1, transparent: true, opacity: 0.8, blending: THREE.AdditiveBlending });
const sparks = new THREE.Points(sparkGeometry, sparkMaterial);
scene.add(sparks);
// Trees
function createTree() {
const tree = new THREE.Group();
const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.15, 1.5, 6), new THREE.MeshStandardMaterial({ color: 0x3a2612, roughness: 1, flatShading: true }));
trunk.position.y = 0.75;
trunk.castShadow = true;
trunk.receiveShadow = true;
tree.add(trunk);
const leaves = new THREE.Mesh(new THREE.ConeGeometry(0.8, 1.5, 6), new THREE.MeshStandardMaterial({ color: 0x226622, roughness: 1, flatShading: true }));
leaves.position.y = 1.5 + 0.75;
leaves.castShadow = true;
leaves.receiveShadow = true;
tree.add(leaves);
return tree;
}
for (let i = 0; i < 35; i++) {
const tree = createTree();
// Place trees in a ring around the clearing, not on the fire
let angle, radius;
do {
angle = Math.random() * Math.PI * 2;
radius = 5 + Math.random() * 15;
} while (radius < 4); // ensure not on fire
tree.position.set(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
tree.rotation.y = Math.random() * Math.PI;
const scale = 0.5 + Math.random() * 0.8;
tree.scale.set(scale, scale, scale);
scene.add(tree);
}
// Sky: a dark sphere with stars
const skyGeometry = new THREE.SphereGeometry(500, 32, 32);
const skyMaterial = new THREE.MeshBasicMaterial({ color: 0x000011, side: THREE.BackSide });
const sky = new THREE.Mesh(skyGeometry, skyMaterial);
scene.add(sky);
// Stars
const starCount = 200;
const starGeometry = new THREE.BufferGeometry();
const starPositions = new Float32Array(starCount * 3);
for (let i = 0; i < starCount; i++) {
// Random point on sphere
const theta = Math.random() * Math.PI * 2;
const phi = Math.acos(2 * Math.random() - 1);
const r = 400;
starPositions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
starPositions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
starPositions[i * 3 + 2] = r * Math.cos(phi);
}
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 1, sizeAttenuation: true });
const stars = new THREE.Points(starGeometry, starMaterial);
scene.add(stars);
// Moon
const moon = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 32), new THREE.MeshBasicMaterial({ color: 0xcccccc }));
moon.position.set(20, 30, 10);
scene.add(moon);
// Animation loop
function animate() {
requestAnimationFrame(animate);
// Animate flames
flames.forEach((flame, i) => {
flame.scale.setScalar((0.5 + Math.random() * 0.5) * (0.8 + Math.sin(performance.now() * 0.01 + i) * 0.2));
flame.material.opacity = 0.6 + Math.random() * 0.4;
flame.position.y = 1.0 + Math.random() * 0.5;
});
// Animate sparks
for (let i = 0; i < sparkCount; i++) {
sparkPositions[i * 3] += sparkVelocities[i * 3];
sparkPositions[i * 3 + 1] += sparkVelocities[i * 3 + 1];
sparkPositions[i * 3 + 2] += sparkVelocities[i * 3 + 2];
sparkVelocities[i * 3 + 1] -= 0.0005; // gravity
sparkLifetimes[i] -= 0.01;
if (sparkLifetimes[i] <= 0 || sparkPositions[i * 3 + 1] < 0.5) {
// Reset spark
sparkPositions[i * 3] = (Math.random() - 0.5) * 0.5;
sparkPositions[i * 3 + 1] = 1.0;
sparkPositions[i * 3 + 2] = (Math.random() - 0.5) * 0.5;
sparkVelocities[i * 3] = (Math.random() - 0.5) * 0.02;
sparkVelocities[i * 3 + 1] = 0.02 + Math.random() * 0.03;
sparkVelocities[i * 3 + 2] = (Math.random() - 0.5) * 0.02;
sparkLifetimes[i] = 2.0;
}
}
sparkGeometry.attributes.position.needsUpdate = true;
// Flicker fire light
fireLight.intensity = 1.5 + Math.random() * 0.5 + Math.sin(performance.now() * 0.01) * 0.5;
controls.update();
renderer.render(scene, camera);
}
animate();
// Handle resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
That should cover most requirements. Let's check:
- Ground: plane with green color, flat shading for low-poly.
- Rocks: icosahedrons with flat shading.
- Logs: cylinders with flat shading.
- Campfire: stones base, flames as sprites with canvas texture, animated.
- Sparks: particle system with gravity and reset.
- Trees: 35 trees, each with cylinder trunk and cone leaves, flat shading, placed randomly around clearing, not on fire (radius > 4).
- Sky: dark sphere, stars points, moon sphere.
- Fog: FogExp2.
- OrbitControls with limits (minPolarAngle, maxPolarAngle, minDistance, maxDistance). I set maxPolarAngle to PI/2 - 0.05 so camera stays above ground. Actually, polar angle is from positive y-axis. So if maxPolarAngle is PI/2 - 0.05, the camera can't go below the horizon. That should keep it above ground.
- Renderer fills window, handles resize.
- Shadows enabled for point light and directional light.
- No external dependencies besides the import map.
Potential issues:
- The flame texture might not be low-poly, but it's a sprite. The requirement says "low-poly 3D scene", but the flames are sprites, which is fine for a campfire. The trees and rocks are low-poly.
- The sparks might need a bit more size variation.
- The fog might need adjustment.
- The sky sphere might clip if camera moves far. I set radius 500, which should be fine.
- The moon might be too bright or not visible. I'll set its material to a light color.
- The point light shadow might need adjustment. I set shadow map size 256, which is low but okay for low-poly.
- The fire light flicker: I used intensity with random and sin. That's fine.
- The flames: I used sprites with additive blending. They might need to be sorted? But it's fine.
- The trees: I used a cone for leaves, which is low-poly. The trunk is a cylinder with 6 segments, which is low-poly.
- The ground is a single plane, which is low-poly. But it's flat. I could add some terrain, but not required.
- The requirement says "a ground with a few low-poly rocks and logs around a campfire in the center." I have that.
- "At least 30 low-poly trees placed randomly around the clearing (not on the fire)." I have 35, placed with radius > 4.
- "The campfire has animated flames and a warm point light that flickers and casts shadows." Yes.
- "Sparks rising from the fire as a simple particle system." Yes.
- "A dark night sky with stars and a moon, plus fog that fades the distant trees." Yes.
- "OrbitControls to look around, with limits so the camera stays above the ground." Yes.
- "The renderer fills the window and handles resizing correctly." Yes.
I should also consider that the camera might start below the ground if not set properly. I set camera position (0, 5, 10), which is above ground. The controls minPolarAngle is 0.2, maxPolarAngle is PI/2 - 0.05. That should keep it above.
One thing: the OrbitControls maxPolarAngle might need to be set to something like Math.PI/2 - 0.1 to avoid clipping into the ground. But if the camera goes exactly to the horizon, it might see the edge of the ground plane. That's okay.
I'll also add a small ambient light to make the scene not too dark.
Let's refine the code a bit:
- I should use `THREE.NoBlending` for the ground? No, standard.
- For the flames, I might want to use a shader for more effect, but canvas texture is fine.
- The sparks: I should set `sizeAttenuation: true` so they get smaller with distance. Already done.
- The stars: I used size 1, but with sizeAttenuation they might be too small. I can increase size to 2.
- The fog: I used color 0x000011 (very dark blue). Density 0.02. Might need to adjust.
- The moon: I used a sphere with MeshBasicMaterial, so it's always visible. I can add a slight glow.
I'll also add a small glow sprite for the moon? Not necessary.
Now, I need to output the code as a single fenced code block with HTML.
Let's write the final code.
I'll include the import map exactly as specified.
I'll also add a small CSS to ensure no scrollbar.
One more thing: the requirement says "Load Three.js only through this import map (do not use any other URL or version)." So I must not import anything else from CDN. I'm only importing OrbitControls from the addons path, which is allowed because it's under the "three/addons/" mapping.
I should also ensure that the code runs without errors. I'll test mentally.
Potential error: The OrbitControls import path: I used 'three/addons/controls/OrbitControls.js'. The import map maps "three/addons/" to the examples/jsm path. So the full URL becomes https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/controls/OrbitControls.js. That should be correct.
Another potential error: The CanvasTexture might need to be updated after creation. I set `texture.needsUpdate = true;` which is good.
The flame sprites: I used `SpriteMaterial` with `map: flameTexture`. That should work.
The sparks: I used `BufferAttribute` correctly.
The trees: I used `CylinderGeometry` and `ConeGeometry`. The cone's radius and height: I set radius 0.8, height 1.5. The trunk is cylinder with radius 0.1, height 1.5. The tree group's position is at ground level (y=0). The trunk is positioned at y=0.75 (so its base is at 0? Actually, cylinder geometry is centered at origin, so if I set position.y = 0.75, the cylinder extends from 0 to 1.5? Wait, a cylinder with height 1.5, when centered, goes from -0.75 to 0.75. So if I set position.y = 0.75, the bottom is at 0 and top at 1.5. That's correct. The cone is positioned at y = 1.5 + 0.75 = 2.25? Actually, the cone height is 1.5, so if centered, it goes from -0.75 to 0.75. If I set its position.y = 1.5 + 0.75 = 2.25, then the bottom of the cone is at 1.5 and top at 3.0. That seems too high. I want the cone to sit on top of the trunk. The trunk top is at 1.5 (since trunk goes from 0 to 1.5). So the cone should be positioned such that its bottom is at 1.5. If the cone is centered, its bottom is at position.y - 0.75. So I need position.y - 0.75 = 1.5 => position.y = 2.25. That's what I did. But then the cone extends from 1.5 to 3.0, which is fine. However, the tree might look a bit tall. I can adjust. But it's okay.
Alternatively, I can set the cone's position.y = 1.5 (so its center is at 1.5, bottom at 0.75, overlapping with trunk). That might look better. I'll change to `leaves.position.y = 1.5;` so the cone sits on the trunk with some overlap. Actually, if I set leaves.position.y = 1.5, the cone's bottom is at 0.75, which is inside the trunk. That's fine. I'll do that.
Also, the tree scale: I set scale to 0.5-1.3. That's fine.
Now, the ground: I used a PlaneGeometry(200, 200). That's a single quad, which is low-poly. But it's flat. I could add some vertices to make it slightly uneven, but not required.
The rocks: I used IcosahedronGeometry with detail 0, which gives a simple icosahedron. That's low-poly.
The logs: CylinderGeometry with 6 radial segments. That's low-poly.
The campfire stones: Icosahedrons.
Everything seems fine.
I'll also add a small ambient light to make the scene not completely dark. I already have AmbientLight(0x333344, 1). That's good.
Now, I'll write the final code.
I need to make sure the code is self-contained and runs. I'll put everything in a single HTML file.
One more thing: The requirement says "The renderer fills the window and handles resizing correctly." I have the resize listener.
Also, I should set the pixel ratio? Not necessary, but I can set `renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));` to avoid performance issues. I'll add that.
I'll also add a small CSS to hide any loading issues.
Let's produce the final answer.