gamedev-skills/awesome-gamedev-agent-skillsCheck passed
SKILL DETAIL
threejs-scene-setup
gamedev-skills/awesome-gamedev-agent-skills/threejs-scene-setup
Stand up a three.js scene: import maps and the three/addons path, the Scene/PerspectiveCamera/WebGLRenderer trio, the setAnimationLoop render loop, responsive resize, OrbitControls, and opting into WebGPURenderer. Use when starting or debugging a three.js app — when the user mentions three.js, THREE.Scene, WebGLRenderer, WebGPURenderer, PerspectiveCamera, the render loop, resizing, or OrbitControls. For models use threejs-gltf-loading; for materials/lights use threejs-materials-lighting.
Installs · 256View source
Installation
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill threejs-scene-setup
Skill files
SKILL.md
Last synced · Oct 25, 2026
references/scene-graph.md›
# three.js scene graph, cameras & cleanup (r150+)
Depth behind the scene-setup skill: coordinate conventions, the `Object3D`
hierarchy, the orthographic camera, and resource disposal.
## Coordinates & conventions
- Right-handed coordinate system: **+X right, +Y up, +Z toward the viewer**. A fresh
`PerspectiveCamera` looks down **-Z**.
- Rotations are in **radians** (except `PerspectiveCamera`'s `fov`, which is degrees).
Use `THREE.MathUtils.degToRad(deg)` when you think in degrees.
- Units are arbitrary but be consistent. glTF models are authored in metres; pick a
scale and stick to it across the project.
## The Object3D hierarchy
Every visual thing (`Mesh`, `Group`, `Camera`, `Light`) extends `Object3D` and has
`position`, `rotation`, `quaternion`, and `scale`. Children inherit their parent's
transform.
```js
import * as THREE from 'three';
const turret = new THREE.Group(); // empty transform node
turret.add(barrelMesh); // child, positioned relative to turret
scene.add(turret);
turret.rotation.y = Math.PI / 4; // rotates the whole group
barrelMesh.removeFromParent(); // detach (r129+)
scene.add(barrelMesh); // re-parent to the scene root
```
Useful traversal/lookup helpers:
```js
scene.getObjectByName('Player'); // first descendant with that .name
root.traverse((obj) => { /* visit every descendant */ });
obj.getWorldPosition(new THREE.Vector3()); // world-space position
```
Avoid scaling parents of physics/gameplay objects: non-unit parent scale compounds
through children and makes world-space math (raycasts, distances) error-prone.
## PerspectiveCamera vs OrthographicCamera
- **PerspectiveCamera(fov, aspect, near, far)** — objects shrink with distance.
Default for 3D. Keep `near`/`far` as tight as the scene allows; a huge far/near
ratio wrecks depth precision (z-fighting).
- **OrthographicCamera(left, right, top, bottom, near, far)** — no perspective;
ideal for 2.5D, isometric, or CAD-like views. Size the frustum to the aspect:
```js
const aspect = window.innerWidth / window.innerHeight;
const d = 5;
const cam = new THREE.OrthographicCamera(-d * aspect, d * aspect, d, -d, 0.1, 100);
// On resize, recompute left/right from the new aspect, then updateProjectionMatrix().
```
After changing any projection property (`aspect`, `fov`, frustum extents, `zoom`)
call `camera.updateProjectionMatrix()`.
## Disposing of resources (avoiding leaks)
three.js cannot garbage-collect GPU memory for you. Removing an object from the
scene frees nothing on the GPU; you must dispose geometries, materials, and
textures explicitly.
```js
// Disposes everything under obj. Skip geometries/materials/textures that other
// objects still share, or they will be re-uploaded (or break) on the next frame.
function disposeObject(obj) {
obj.traverse((node) => {
if (node.geometry) node.geometry.dispose();
const materials = Array.isArray(node.material) ? node.material : [node.material];
for (const mat of materials) {
if (!mat) continue;
for (const key of Object.keys(mat)) {
const value = mat[key];
if (value && value.isTexture) value.dispose(); // map, normalMap, etc.
}
mat.dispose();
}
if (node.isSkinnedMesh) node.skeleton.dispose(); // bone texture
node.dispose(); // Object3D.dispose() (r186+): shadow maps, instancing buffers
});
obj.removeFromParent();
}
```
`Object3D.dispose()` is on every object since r186 (before that only lights,
`InstancedMesh`, and `BatchedMesh` had it — guard with `node.dispose?.()` on older
releases). The base method frees no geometry, materials, or textures; those stay
manual (`BatchedMesh` is the exception: it owns and disposes its own geometry).
Also dispose render targets (`renderTarget.dispose()`) and, on full teardown, the
renderer (`renderer.dispose()`). When swapping levels, dispose the old level's
subtree before loading the next, or memory grows every transition.
## Stopping and restarting the loop
`renderer.setAnimationLoop(fn)` starts the loop; `setAnimationLoop(null)` stops it
(e.g. when the tab is hidden via the Page Visibility API, or a menu is open). This
is also the loop required by WebXR — `requestAnimationFrame` does not drive XR
frames.
## Render-on-demand
For static scenes or editors, you don't need a continuous loop. Render only when
something changes (input, controls' `change` event, a tween step):
```js
let needsRender = true;
controls.addEventListener('change', () => { needsRender = true; });
renderer.setAnimationLoop(() => {
controls.update(); // keeps damping alive; fires 'change' while moving
if (!needsRender) return;
needsRender = false;
renderer.render(scene, camera);
});
```
This cuts GPU/battery use dramatically for non-animated content.
SKILL.md›
---
name: threejs-scene-setup
description: >
Stand up a three.js scene: import maps and the three/addons path, the
Scene/PerspectiveCamera/WebGLRenderer trio, the setAnimationLoop render loop,
responsive resize, OrbitControls, and opting into WebGPURenderer. Use when
starting or debugging a three.js app — when the user mentions three.js,
THREE.Scene, WebGLRenderer, WebGPURenderer, PerspectiveCamera, the render loop,
resizing, or OrbitControls. For models use
threejs-gltf-loading; for materials/lights use threejs-materials-lighting.
---
# three.js Scene Setup
Create the foundation of a three.js app: module loading, the
scene/camera/renderer trio, the render loop, responsive resizing, and camera
controls. Patterns target **r186**. Read the installed `three` version before
changing an existing project because examples and addons move across releases.
## When to use
- Use when bootstrapping a three.js scene, fixing a blank/black canvas, making the
canvas responsive, setting up the animation loop, or adding `OrbitControls`.
- Use when `package.json` depends on `three` and code does `import * as THREE from
'three'`.
**When *not* to use:** loading `.gltf`/`.glb` models or skinned animation →
`threejs-gltf-loading`. Materials, lights, shadows, environment maps →
`threejs-materials-lighting`. 2D rendering → `pixijs-rendering`.
## Core workflow
1. **Load three.js as an ES module with an import map.** Since r137 the bare
specifier `'three'` must be resolved by an import map (in HTML) or a bundler;
map `'three/addons/'` too. Addons (controls, loaders) live under
`three/addons/...`.
2. **Create the trio.** A `Scene` (root of the graph), a `PerspectiveCamera(fov,
aspect, near, far)` moved back from the origin, and a `WebGLRenderer` whose
`domElement` is in the DOM. Set size and `pixelRatio`.
3. **Add a mesh.** `new Mesh(geometry, material)` and `scene.add(mesh)`. With a
lit material you also need a light (see `threejs-materials-lighting`).
4. **Drive a render loop with `renderer.setAnimationLoop(fn)`.** It's the modern,
WebXR-/WebGPU-safe replacement for hand-rolled `requestAnimationFrame`. Use a
`Timer` for delta time (`Clock` is deprecated since r183).
5. **Handle resize** so the camera aspect and renderer match the canvas; update
`camera.aspect`, call `updateProjectionMatrix()`, and `renderer.setSize(...)`.
6. **Add `OrbitControls`** for orbit/pan/zoom while developing. Confirm something
actually renders (a lit cube, the controls responding) before assuming success.
## Patterns
### 1. HTML import map + module entry (no bundler)
```html
<canvas id="c"></canvas>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
}
}
</script>
<script type="module" src="./main.js"></script>
```
With a bundler (Vite/webpack), skip the import map and just
`npm i three`; the same `import` statements resolve.
### 2. Scene + camera + renderer
```js
// main.js
import * as THREE from 'three';
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // cap for perf
renderer.setSize(window.innerWidth, window.innerHeight);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x101018);
const camera = new THREE.PerspectiveCamera(
60, // vertical field of view (degrees)
window.innerWidth / window.innerHeight, // aspect
0.1, // near
100 // far
);
camera.position.set(3, 2, 5);
camera.lookAt(0, 0, 0);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial() // unlit; shows orientation without a light
);
scene.add(cube);
```
### 3. The render loop (setAnimationLoop + Timer)
```js
const timer = new THREE.Timer(); // replaces the deprecated THREE.Clock (r183+)
timer.connect(document); // optional: no huge delta after a hidden tab
renderer.setAnimationLoop((time) => {
timer.update(time); // call once per frame, before getDelta()
const dt = timer.getDelta(); // seconds since last frame
cube.rotation.x += dt; // frame-rate independent
cube.rotation.y += dt * 0.7;
renderer.render(scene, camera);
});
// renderer.setAnimationLoop(null); // stop the loop
```
### 4. Responsive resize
```js
function onResize() {
const w = window.innerWidth, h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix(); // required after changing aspect
renderer.setSize(w, h);
}
window.addEventListener('resize', onResize);
```
### 5. OrbitControls (orbit / pan / zoom)
```js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // inertial feel
controls.target.set(0, 0, 0);
renderer.setAnimationLoop(() => {
controls.update(); // needed every frame when damping is on
renderer.render(scene, camera);
});
```
### 6. Opting into WebGPURenderer (only when you need it)
`WebGLRenderer` stays the default choice for compatibility. Switch to
`WebGPURenderer` when the project needs TSL node materials or compute shaders; it
falls back to a WebGL 2 backend where WebGPU is unavailable.
```js
// Official WebGPU examples map both "three" and "three/webgpu" to
// build/three.webgpu.js, plus "three/tsl" to build/three.tsl.js.
import * as THREE from 'three/webgpu';
const renderer = new THREE.WebGPURenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
await renderer.init(); // required before direct render()/compute()
renderer.setAnimationLoop(() => renderer.render(scene, camera));
```
Scenes, cameras, built-in `Mesh*Material`s, and most addons carry over; custom
GLSL (`ShaderMaterial`, `onBeforeCompile`) does not.
## Pitfalls
- **`Failed to resolve module specifier "three"`** → missing import map (or bundler
config). Map both `"three"` and `"three/addons/"`; addon paths must end with `/`.
- **Black canvas, no errors** → camera is at the origin (inside/behind the object),
or you used a lit material (`MeshStandardMaterial`) with no light. Move the camera
back; use `MeshNormalMaterial`/`MeshBasicMaterial` to verify geometry first.
- **Nothing animates** → you never called `renderer.render` inside the loop, or you
call `setAnimationLoop` but render outside it.
- **Stretched / squashed view on resize** → you resized the renderer but didn't
update `camera.aspect` + `updateProjectionMatrix()`.
- **Blurry or jagged on HiDPI** → set `renderer.setPixelRatio(...)`; cap it (≈2) so
4K/retina screens don't tank performance.
- **OrbitControls feel dead** → with `enableDamping = true` you must call
`controls.update()` every frame.
- **Old tutorials use `<script src="three.min.js">` or `examples/js/...`** →
`examples/js` was removed in r148 and `build/three.min.js` in r161; use
`type="module"` + import maps.
- **`.render() called before the backend is initialized`** (WebGPURenderer) → call
`await renderer.init()` before any direct `render()`/`compute()`, or render only
from `setAnimationLoop`, which initializes first.
- **`Material "ShaderMaterial" is not compatible`, or an `onBeforeCompile` patch is
ignored** (WebGPURenderer) → it renders node materials only; port the effect to
TSL, or stay on `WebGLRenderer`.
## References
- For coordinate conventions, the scene-graph (`Group`, parent/child transforms,
`Object3D` add/remove), `OrthographicCamera` for 2.5D, and disposing of
geometries/materials/textures to avoid leaks, read `references/scene-graph.md`.
## Related skills
- `threejs-materials-lighting` — give surfaces a lit look (lights, shadows, PBR).
- `threejs-gltf-loading` — load 3D models and play their animations.
- `pixijs-rendering` — 2D rendering in the browser.
- `fps-shooter` — a 3D genre template that composes three.js skills.