Three.js — 3D Scene Mode Reference

Overview

Three.js is the standard JavaScript library for 3D graphics in the browser. The 3D template (assets/template-3d.html) uses Three.js with ES module imports via importmap. In "scene" render mode, you build a scene graph of meshes, lights, and cameras — the template handles the render loop, OrbitControls, and sidebar/seed infrastructure.

When to use scene mode vs shader mode:

Template Contract

Implement two functions:

function sceneSetup(THREE, scene, camera, renderer, params, seed) {
  // scene: THREE.Scene — add objects here
  // camera: PerspectiveCamera at (0, 0, 5) — reposition as needed
  // renderer: WebGLRenderer — configure shadows, tone mapping, etc.
  // params: current parameter values
  // seed: current seed number
  // Return: optional state object passed to sceneAnimate
}

function sceneAnimate(THREE, scene, camera, state, params, time, delta) {
  // state: whatever sceneSetup returned
  // time: elapsed seconds (float)
  // delta: seconds since last frame
}

The template provides:

Geometry

Built-in Geometries

// Primitives
new THREE.BoxGeometry(w, h, d, wSegs, hSegs, dSegs)
new THREE.SphereGeometry(radius, widthSegs, heightSegs)
new THREE.CylinderGeometry(radiusTop, radiusBot, height, radialSegs)
new THREE.TorusGeometry(radius, tube, radialSegs, tubularSegs)
new THREE.TorusKnotGeometry(radius, tube, tubularSegs, radialSegs, p, q)
new THREE.PlaneGeometry(w, h, wSegs, hSegs)
new THREE.IcosahedronGeometry(radius, detail)  // great for organic subdivision
new THREE.OctahedronGeometry(radius, detail)
new THREE.TetrahedronGeometry(radius, detail)
new THREE.ConeGeometry(radius, height, radialSegs)
new THREE.RingGeometry(innerR, outerR, thetaSegs)
new THREE.DodecahedronGeometry(radius, detail)

BufferGeometry (custom meshes)

const geo = new THREE.BufferGeometry();
const vertices = new Float32Array([
  -1, -1, 0,   1, -1, 0,   0, 1, 0  // triangle
]);
geo.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geo.computeVertexNormals();

Instanced Meshes (high-performance many-objects)

const geo = new THREE.IcosahedronGeometry(0.1, 1);
const mat = new THREE.MeshStandardMaterial({ color: 0xffffff });
const count = 10000;
const mesh = new THREE.InstancedMesh(geo, mat, count);

const dummy = new THREE.Object3D();
const color = new THREE.Color();

for (let i = 0; i < count; i++) {
  dummy.position.set(
    seededRandom() * 10 - 5,
    seededRandom() * 10 - 5,
    seededRandom() * 10 - 5
  );
  dummy.scale.setScalar(0.5 + seededRandom() * 0.5);
  dummy.updateMatrix();
  mesh.setMatrixAt(i, dummy.matrix);
  color.setHSL(seededRandom(), 0.7, 0.6);
  mesh.setColorAt(i, color);
}
mesh.instanceMatrix.needsUpdate = true;
mesh.instanceColor.needsUpdate = true;
scene.add(mesh);

To animate instanced meshes, update matrices per frame and set mesh.instanceMatrix.needsUpdate = true.

Points (particle systems)

const count = 50000;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);

for (let i = 0; i < count; i++) {
  positions[i * 3]     = (seededRandom() - 0.5) * 20;
  positions[i * 3 + 1] = (seededRandom() - 0.5) * 20;
  positions[i * 3 + 2] = (seededRandom() - 0.5) * 20;

  const c = new THREE.Color().setHSL(seededRandom(), 0.8, 0.6);
  colors[i * 3]     = c.r;
  colors[i * 3 + 1] = c.g;
  colors[i * 3 + 2] = c.b;
}

const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));

const mat = new THREE.PointsMaterial({
  size: 0.05,
  vertexColors: true,
  transparent: true,
  opacity: 0.8,
  blending: THREE.AdditiveBlending,
  depthWrite: false,
});

const points = new THREE.Points(geo, mat);
scene.add(points);

To animate, modify geo.attributes.position.array[...] and set geo.attributes.position.needsUpdate = true.

Materials

// Physically-based (responds to lights)
new THREE.MeshStandardMaterial({
  color: 0x4488ff,
  metalness: 0.5,
  roughness: 0.3,
  emissive: 0x000000,
  emissiveIntensity: 0.0,
  wireframe: false,
  transparent: true,
  opacity: 0.9,
  side: THREE.DoubleSide,
})

// Unlit flat color (no lights needed)
new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })

// Normal-colored (debug / aesthetic)
new THREE.MeshNormalMaterial()

// Phong (cheaper shading)
new THREE.MeshPhongMaterial({ color: 0x00ff00, shininess: 100 })

// Line material
new THREE.LineBasicMaterial({ color: 0xffffff, linewidth: 1 })

Lighting

// Ambient (base illumination)
scene.add(new THREE.AmbientLight(0x404040, 0.5));

// Directional (sun-like)
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);

// Point (omni-directional)
const pointLight = new THREE.PointLight(0xff8844, 1.0, 50);
pointLight.position.set(0, 3, 0);
scene.add(pointLight);

// Hemisphere (sky + ground gradient)
scene.add(new THREE.HemisphereLight(0x88ccff, 0x443322, 0.6));

// Spot
const spot = new THREE.SpotLight(0xffffff, 1.0, 30, Math.PI / 6, 0.5, 1);
spot.position.set(0, 10, 0);
scene.add(spot);

Shadows

renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;

mesh.castShadow = true;
groundPlane.receiveShadow = true;

Camera

The template provides a PerspectiveCamera at (0, 0, 5). Adjust in sceneSetup:

camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
camera.fov = 45;
camera.updateProjectionMatrix();

For animated camera (orbiting, fly-through):

// In sceneAnimate:
camera.position.x = Math.cos(time * 0.3) * radius;
camera.position.z = Math.sin(time * 0.3) * radius;
camera.lookAt(0, 0, 0);

Scene Background

scene.background = new THREE.Color(0x000000);

// Fog (depth atmosphere)
scene.fog = new THREE.Fog(0x000000, 5, 30);           // linear
scene.fog = new THREE.FogExp2(0x000000, 0.05);         // exponential

Helpers (useful during development)

scene.add(new THREE.AxesHelper(5));
scene.add(new THREE.GridHelper(10, 10));
scene.add(new THREE.PointLightHelper(pointLight, 0.5));

Procedural Geometry Modifiers

Three.js doesn’t have Cinder’s built-in geometry modifier pipeline, but you can build equivalent transforms on BufferGeometry vertex data. These composable operations turn simple primitives into complex generative sculptures.

Twist

Rotate vertices around an axis proportional to their position along that axis:

function twist(geometry, axis, angle) {
  const pos = geometry.attributes.position;
  const v = new THREE.Vector3();
  for (let i = 0; i < pos.count; i++) {
    v.fromBufferAttribute(pos, i);
    const t = axis === 'y' ? v.y : axis === 'x' ? v.x : v.z;
    const a = t * angle;
    const cos = Math.cos(a), sin = Math.sin(a);
    if (axis === 'y') {
      const x = v.x * cos - v.z * sin;
      const z = v.x * sin + v.z * cos;
      pos.setXYZ(i, x, v.y, z);
    }
    // extend for other axes as needed
  }
  pos.needsUpdate = true;
  geometry.computeVertexNormals();
}

Taper

Scale the cross-section along an axis by a function of position:

function taper(geometry, axis, taperFn) {
  // taperFn: (t) => scale, where t is normalized position along axis (0–1)
  const pos = geometry.attributes.position;
  const bbox = geometry.boundingBox || (geometry.computeBoundingBox(), geometry.boundingBox);
  const min = axis === 'y' ? bbox.min.y : bbox.min.x;
  const max = axis === 'y' ? bbox.max.y : bbox.max.x;
  const v = new THREE.Vector3();

  for (let i = 0; i < pos.count; i++) {
    v.fromBufferAttribute(pos, i);
    const t = (v[axis] - min) / (max - min);
    const s = taperFn(t);
    if (axis === 'y') { v.x *= s; v.z *= s; }
    else { v.y *= s; v.z *= s; }
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  pos.needsUpdate = true;
  geometry.computeVertexNormals();
}
// Usage: taper(geo, 'y', t => 1 - t * 0.8)  // narrows toward top

Noise Displacement

Displace vertices along their normals by a noise field:

function noiseDisplace(geometry, noiseScale, amplitude, noiseFn) {
  geometry.computeVertexNormals();
  const pos = geometry.attributes.position;
  const norm = geometry.attributes.normal;
  const v = new THREE.Vector3();
  const n = new THREE.Vector3();

  for (let i = 0; i < pos.count; i++) {
    v.fromBufferAttribute(pos, i);
    n.fromBufferAttribute(norm, i);
    const d = noiseFn(v.x * noiseScale, v.y * noiseScale, v.z * noiseScale);
    v.addScaledVector(n, d * amplitude);
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  pos.needsUpdate = true;
  geometry.computeVertexNormals();
}

Spherical Projection

Project any geometry onto a sphere (useful for wrapping flat patterns onto globes):

function spherify(geometry, radius, blend = 1.0) {
  const pos = geometry.attributes.position;
  const v = new THREE.Vector3();
  for (let i = 0; i < pos.count; i++) {
    v.fromBufferAttribute(pos, i);
    const spherePos = v.clone().normalize().multiplyScalar(radius);
    v.lerp(spherePos, blend);
    pos.setXYZ(i, v.x, v.y, v.z);
  }
  pos.needsUpdate = true;
  geometry.computeVertexNormals();
}

Extrude Along Curve

Create a mesh by sweeping a 2D cross-section along a 3D curve. Three.js provides ExtrudeGeometry for shapes and TubeGeometry for circular cross-sections, but for arbitrary profiles:

function extrudeAlongCurve(shape2D, curve3D, steps, scaleFunc) {
  // shape2D: array of THREE.Vector2 (cross-section outline)
  // curve3D: THREE.Curve3 (spine)
  // scaleFunc: (t) => float (optional cross-section scaling)
  const frames = curve3D.computeFrenetFrames(steps);
  const vertices = [];
  const indices = [];
  const N = shape2D.length;

  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const pos = curve3D.getPointAt(t);
    const normal = frames.normals[i];
    const binormal = frames.binormals[i];
    const s = scaleFunc ? scaleFunc(t) : 1;

    for (let j = 0; j < N; j++) {
      const pt = shape2D[j];
      const x = pos.x + (pt.x * normal.x + pt.y * binormal.x) * s;
      const y = pos.y + (pt.x * normal.y + pt.y * binormal.y) * s;
      const z = pos.z + (pt.x * normal.z + pt.y * binormal.z) * s;
      vertices.push(x, y, z);
    }
  }

  // Connect rings into triangles
  for (let i = 0; i < steps; i++) {
    for (let j = 0; j < N; j++) {
      const a = i * N + j;
      const b = i * N + (j + 1) % N;
      const c = (i + 1) * N + j;
      const d = (i + 1) * N + (j + 1) % N;
      indices.push(a, c, b, b, c, d);
    }
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
  geo.setIndex(indices);
  geo.computeVertexNormals();
  return geo;
}

Composing Modifiers

Chain modifiers for complex effects. Order matters — twist then taper produces different results than taper then twist:

const geo = new THREE.CylinderGeometry(1, 1, 4, 32, 64);
twist(geo, 'y', 2.0);
taper(geo, 'y', t => 1 - t * 0.6);
noiseDisplace(geo, 2.0, 0.15, noise3D);

Performance note: vertex manipulation runs on CPU. For meshes over ~100K vertices, consider doing the equivalent in a vertex shader for real-time animation. CPU modifiers are best applied once at setup time.

Common 3D Generative Patterns

Generative Sculpture (deformed mesh)

const geo = new THREE.IcosahedronGeometry(2, 5);
const pos = geo.attributes.position;
for (let i = 0; i < pos.count; i++) {
  const v = new THREE.Vector3().fromBufferAttribute(pos, i);
  const noise = perlin3D(v.x * 0.5, v.y * 0.5, v.z * 0.5);  // your noise fn
  v.normalize().multiplyScalar(2 + noise * 0.5);
  pos.setXYZ(i, v.x, v.y, v.z);
}
geo.computeVertexNormals();

3D Lattice / Grid

const size = 10, step = 1;
for (let x = -size; x <= size; x += step) {
  for (let y = -size; y <= size; y += step) {
    for (let z = -size; z <= size; z += step) {
      // Place object at (x, y, z) based on noise or rule
    }
  }
}

Line-Based Structures (attractors, paths)

const points = [];
let x = 0.1, y = 0, z = 0;
for (let i = 0; i < 100000; i++) {
  // Lorenz attractor step
  const dx = 10 * (y - x) * 0.001;
  const dy = (x * (28 - z) - y) * 0.001;
  const dz = (x * y - 2.667 * z) * 0.001;
  x += dx; y += dy; z += dz;
  points.push(new THREE.Vector3(x, y, z));
}
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineBasicMaterial({ color: 0x88ccff });
scene.add(new THREE.Line(geo, mat));

Tubes from Curves

const curvePoints = [/* THREE.Vector3 array */];
const curve = new THREE.CatmullRomCurve3(curvePoints);
const tubeGeo = new THREE.TubeGeometry(curve, 200, 0.05, 8, false);
const tubeMat = new THREE.MeshStandardMaterial({ color: 0xff4444 });
scene.add(new THREE.Mesh(tubeGeo, tubeMat));

Noise in Three.js

Three.js has no built-in noise. Include a noise implementation in your sketch code. A compact 3D Perlin noise (copy into Section 3):

// Compact simplex-style 3D noise — paste this into your sketch
// Returns values in approximately [-1, 1]
function noise3D(x, y, z) {
  // Use a hash-based approach with seededRandom for determinism
  // Or include a full simplex noise implementation
}

For production quality, inline a simplex noise implementation or use the u_time dimension of 2D noise fields. The shader mode (shaders-glsl.md) has built-in GLSL noise functions that are more performant for heavy noise use.

Post-Processing

Three.js supports multi-pass post-processing via the EffectComposer:

import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5,   // strength
  0.4,   // radius
  0.85   // threshold
));

// In sceneAnimate, replace renderer.render with:
// composer.render();  — but since the template calls renderer.render,
// store the composer on state and override in animate:

Note: When using EffectComposer, the template’s automatic renderer.render(scene, camera) still runs. To use the composer instead, set renderer.autoClear = false in sceneSetup and call composer.render() in sceneAnimate, then set a flag to skip the template’s render. Or simply render your scene entirely through the composer by storing it on the returned state and checking for it.

Available post-processing passes (via importmap three/addons/):

Performance Tips

Coordinate System

Notable 3D Generative Art Practitioners

Demos in the gallery

Aizawa Attractor (3D)
A 3D strange attractor with a slowly forming shell — discovered by exploration of low-parameter polynomial systems. Rendered as a continuously growing line trace orbiting in space; orbit-drag to spin the attractor and inspect its filaments.
Boids 3D
Reynolds's flocking algorithm extended to three dimensions. Agents wrap around a cubic volume; orbit-drag the camera to watch a swarm form, turn, and split from any angle.
Crease & Outline
Non-photorealistic 3D rendering — silhouette via inverted-hull outline, creases via geometry-edge extraction at a threshold angle. The drawing shows the form's boundary as if inked.
3D Flow Field
A 3D vector field built from three offset Perlin noise samples (one per axis). Thousands of particles drift through the field, leaving short trails — the structure of the field emerges from their collective motion. Orbit-drag the camera to inspect from any angle.
Gyroid TPMS
The Gyroid is a triply periodic minimal surface — discovered by Alan Schoen in 1970 as a soap-film geometry that infinitely tiles space without intersecting itself. Found in butterfly wings and block copolymers.
Metaballs
Multiple moving energy fields — each a smooth blob — sum to a scalar field whose iso-surface fuses, splits, and flows. The classical algorithmic CGI shape from the 1980s, isosurface-extracted into a continuous mesh.
Schwarz P-Surface
Hermann Schwarz's primitive P-surface — the simplest triply periodic minimal surface, defined by cos(x)+cos(y)+cos(z)=0. Its 'cubic' topology bisects space into two interpenetrating but unconnected halves.
Sierpinski Tetrahedron
Wacław Sierpiński's 3D fractal — a tetrahedron recursively replaced by four smaller tetrahedra at its corners. The limit shape has self-similar structure at every scale; each visible 'gap' is its own missing tetrahedron.
Algorithmic Splats from Lorenz
The Lorenz attractor's trajectory rendered as Gaussian splats — each point of the orbit becomes a soft luminous billboard sized and colored by local velocity. The splat representation makes the attractor's filaments glow continuously rather than appearing as discrete dots.
Splat Nebula
A volumetric cloud of Gaussian splats sampled from 3D Perlin noise. Splats accumulate where the noise field exceeds a threshold; their colors come from a second offset noise field. The layered transparency builds a soft, nebula-like volumetric effect.
3D Supershape
The 3D Gielis supershape — two superformulas combined as a parametric surface. With twelve parameters total, the shape ranges from squashed spheres to spiky asteroids to lobed organic forms. Orbit-drag to inspect.
Toon Shading
Cel-shaded 3D — diffuse lighting posterized into discrete tone bands, plus an inverted-hull silhouette outline. The aesthetic of animated film and graphic novels, applied to procedurally rotated geometry.