🪐 Three.js in 3 ideas

Three.js renders real 3D into a <canvas> using WebGL. Unlike CSS 3D (which transforms flat HTML rectangles), Three.js draws actual meshes — geometry covered in material, lit by lights, viewed through a camera.

// The 3-line skeleton every Three.js app starts from:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
const renderer = new THREE.WebGLRenderer({ canvas });

// Then: add meshes, call renderer.render(scene, camera).

Load it from a CDN with <script type="module"> and import * as THREE from '...'. Every demo below is a self-contained function that builds a mini scene and renders into its own canvas.

01 — Basics

Hello, Cube

The "hello world" of Three.js. A rotating cube, a camera, a renderer. Everything else in this lab builds on these three objects.

Scene + PerspectiveCamera + WebGLRenderer BASICS

HTML

<canvas id="c-hello"></canvas>

<script type="module">
  import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
  /* see JS tab */
</script>
02 — Basics

Primitive Geometries

Three.js ships with every basic shape you need out of the box. BoxGeometry, SphereGeometry, TorusGeometry, ConeGeometry, CylinderGeometry, DodecahedronGeometry. Here they are all on one stage.

6 primitives, rotating in sync BASICS

JavaScript

// To run standalone, load THREE first:
// <script type="module">
//   import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
//   /* your code below */
// </script>

// Six different geometries — same material for clarity
const geos = [
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.SphereGeometry(0.6, 32, 32),
  new THREE.TorusGeometry(0.5, 0.2, 16, 48),
  new THREE.ConeGeometry(0.6, 1.2, 32),
  new THREE.CylinderGeometry(0.4, 0.4, 1.2, 32),
  new THREE.DodecahedronGeometry(0.7),
];

const material = new THREE.MeshNormalMaterial();

const meshes = geos.map((g, i) => {
  const mesh = new THREE.Mesh(g, material);
  mesh.position.x = (i - 2.5) * 1.8;  // spread them across x
  scene.add(mesh);
  return mesh;
});

function tick(t) {
  meshes.forEach(m => {
    m.rotation.x = t * 0.0006;
    m.rotation.y = t * 0.001;
  });
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
03 — Basics

Materials

Same sphere, four different materials. MeshBasicMaterial ignores light (always fully lit), MeshLambertMaterial does simple diffuse lighting, MeshPhongMaterial adds specular highlights, MeshStandardMaterial is the modern physically-based material.

Basic · Lambert · Phong · Standard BASICS

JavaScript

// To run standalone, load THREE first:
// <script type="module">
//   import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
//   /* your code below */
// </script>

// The geometry is shared — materials make the difference
const geo = new THREE.SphereGeometry(0.7, 32, 32);

const materials = [
  new THREE.MeshBasicMaterial   ({ color: 0x84cc16 }),
  new THREE.MeshLambertMaterial ({ color: 0x84cc16 }),
  new THREE.MeshPhongMaterial   ({ color: 0x84cc16, shininess: 80 }),
  new THREE.MeshStandardMaterial({ color: 0x84cc16, roughness: 0.3, metalness: 0.4 }),
];

materials.forEach((mat, i) => {
  const sphere = new THREE.Mesh(geo, mat);
  sphere.position.x = (i - 1.5) * 1.8;
  scene.add(sphere);
});

// All except "Basic" need lights
scene.add(new THREE.AmbientLight(0xffffff, 0.5));
const dir = new THREE.DirectionalLight(0xffffff, 1);
dir.position.set(2, 3, 4);
scene.add(dir);
04 — Lighting

Lights

Three.js gives you a handful of light types. This scene uses AmbientLight (fills everything evenly), DirectionalLight (parallel rays — think sun), and a colored PointLight that orbits to show moving illumination.

Ambient + Directional + orbiting Point LIGHTS

JavaScript

// To run standalone, load THREE first:
// <script type="module">
//   import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
//   /* your code below */
// </script>

// A matte object so the lighting really shows
const mesh = new THREE.Mesh(
  new THREE.TorusKnotGeometry(0.8, 0.3, 128, 16),
  new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: .6 })
);
scene.add(mesh);

// Three lights in one scene:
// 1) Ambient — lifts the shadows so nothing is pitch-black
scene.add(new THREE.AmbientLight(0xffffff, 0.3));

// 2) Directional — the "sun", casts parallel rays
const sun = new THREE.DirectionalLight(0xffffff, 0.9);
sun.position.set(-3, 4, 2);
scene.add(sun);

// 3) Colored point light that orbits the mesh
const point = new THREE.PointLight(0xd946ef, 2, 10);
scene.add(point);

function tick(t) {
  mesh.rotation.x = t * 0.0004;
  mesh.rotation.y = t * 0.0006;
  // move the point light in a circle
  point.position.set(Math.sin(t*0.001)*2, 1, Math.cos(t*0.001)*2);
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
05 — Lighting

Wireframe

Any material can be rendered as a wireframe — showing the underlying triangle mesh. Great for debugging geometry or for a stylized, digital look.

material.wireframe = true STYLE

JavaScript

// To run standalone, load THREE first:
// <script type="module">
//   import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
//   /* your code below */
// </script>

// Higher segment counts reveal the wireframe structure better
const ico = new THREE.Mesh(
  new THREE.IcosahedronGeometry(1.2, 1),  // detail level 1
  new THREE.MeshBasicMaterial({
    color: 0x84cc16,
    wireframe: true
  })
);
scene.add(ico);

// Everything outside the wireframe mesh can still use regular
// lighting / materials — wireframe is a per-material flag.

function tick(t) {
  ico.rotation.x = t * 0.0004;
  ico.rotation.y = t * 0.0007;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
06 — Lighting

Textures

Wrap an image around geometry with TextureLoader. Here a sphere is covered in an earth-style map, with a point light giving it shape. The same technique wraps labels around bottles, billboards, or cube faces.

TextureLoader + material.map TEXTURES

JavaScript

// To run standalone, load THREE first:
// <script type="module">
//   import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
//   /* your code below */
// </script>

const loader = new THREE.TextureLoader();
const earthTex = loader.load(
  'https://unpkg.com/three-globe@2.27.2/example/img/earth-blue-marble.jpg'
);

const planet = new THREE.Mesh(
  new THREE.SphereGeometry(1.2, 64, 64),
  new THREE.MeshStandardMaterial({
    map: earthTex,              // ← the color ("diffuse") texture
    roughness: .8
  })
);
scene.add(planet);

scene.add(new THREE.AmbientLight(0xffffff, 0.25));
const sun = new THREE.DirectionalLight(0xffffff, 1.3);
sun.position.set(3, 1, 2);
scene.add(sun);

function tick() {
  planet.rotation.y += 0.003;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();
07 — Motion

Animated Torus Knot

A torus knot is just a fancier torus — twisted through itself p times while going around q times. It makes great hero-section decoration. Here it spins on two axes and changes color with a gradient material.

TorusKnotGeometry + MeshNormalMaterial MOTION

JavaScript

// To run standalone, load THREE first:
// <script type="module">
//   import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
//   /* your code below */
// </script>

const knot = new THREE.Mesh(
  new THREE.TorusKnotGeometry(
    0.9,    // radius of the overall ring
    0.28,   // radius of the tube
    128,    // tubular segments
    16,     // radial segments
    2,      // p — how many times around the axis
    3       // q — how many times through the hole
  ),
  new THREE.MeshNormalMaterial()
);
scene.add(knot);

function tick(t) {
  knot.rotation.x = t * 0.0004;
  knot.rotation.y = t * 0.0006;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);

// Try changing p and q to different integers for wildly
// different knot shapes. p=2 q=3 is the classic trefoil.
08 — Motion

Particle Field

Thousands of dots in 3D space, rendered by a single Points object instead of thousands of meshes. Great for starfields, snow, dust, or any ambient background.

BufferGeometry + Points + PointsMaterial MOTION

JavaScript

// To run standalone, load THREE first:
// <script type="module">
//   import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js';
//   /* your code below */
// </script>

// Build an array of xyz positions for 3,000 points
const count = 3000;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
  positions[i * 3 + 0] = (Math.random() - .5) * 10;  // x
  positions[i * 3 + 1] = (Math.random() - .5) * 10;  // y
  positions[i * 3 + 2] = (Math.random() - .5) * 10;  // z
}

// BufferGeometry holds raw position data
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));

const material = new THREE.PointsMaterial({
  color: 0x84cc16,
  size: 0.03,
  sizeAttenuation: true,        // far particles look smaller
});

const cloud = new THREE.Points(geo, material);
scene.add(cloud);

function tick(t) {
  cloud.rotation.y = t * 0.0001;
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
09 — Input

Orbit Controls

Let users drag to rotate and scroll to zoom. OrbitControls is an optional addon — not in the main Three.js core, so you import it separately. Try dragging the object below.

OrbitControls addon + controls.update() INPUT
drag · scroll to zoom

JavaScript

// OrbitControls lives in the "addons" / "examples/jsm" path
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three@0.164.0/examples/jsm/controls/OrbitControls.js';

// ...scene, camera, renderer setup as usual...

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;        // smoothed pan/zoom
controls.dampingFactor = 0.08;
controls.minDistance   = 2;           // don't zoom in past this
controls.maxDistance   = 12;

function tick() {
  controls.update();                  // required each frame for damping
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
}
tick();
10 — Input

Loaded GLTF Model

Real projects use real 3D models, typically in .gltf or .glb format (like JPG for 3D). You load them with GLTFLoader and add the resulting scene to your own. This demo fetches a free model — you can swap the URL for any GLB on the web.

GLTFLoader + loader.load() MODEL
drag to inspect

JavaScript

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader    } from 'three/addons/loaders/GLTFLoader.js';

const scene    = new THREE.Scene();
const camera   = new THREE.PerspectiveCamera(35, aspect, 0.1, 100);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });

// Bright, simple lighting so the model reads
scene.add(new THREE.AmbientLight(0xffffff, 0.8));
const dir = new THREE.DirectionalLight(0xffffff, 1);
dir.position.set(2, 3, 4);
scene.add(dir);

const loader = new GLTFLoader();
loader.load(
  'https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/Duck/glTF-Binary/Duck.glb',
  (gltf) => {                         // SUCCESS
    scene.add(gltf.scene);
  },
  undefined,
  (err) => console.error(err)         // ERROR
);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
11 — Full Page

The whole scene as one file

Everything from this lab — scene, camera, renderer, lights, a spinning torus knot, a starfield, and drag-to-orbit controls — combined into one complete HTML file. Save it as scene.html, open it in a browser, and it just works. The live preview below is rendered from the exact code in the box.

Live preview

Complete HTML — copy this whole file

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Three.js Scene</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    html, body { height: 100%; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #0a0a12;
      color: #e2e8f0;
      overflow: hidden;
    }
    /* The canvas fills the whole window */
    #scene {
      display: block;
      width: 100%;
      height: 100%;
      cursor: grab;
    }
    #scene:active { cursor: grabbing; }
    /* A little caption floating over the 3D scene */
    .overlay {
      position: fixed;
      left: 24px;
      bottom: 24px;
      max-width: 320px;
      pointer-events: none;
    }
    .overlay h1 {
      font-size: 1.6rem;
      font-weight: 700;
      color: #fff;
      margin-bottom: 6px;
    }
    .overlay h1 span { color: #84cc16; }
    .overlay p {
      font-size: .85rem;
      line-height: 1.5;
      color: rgba(255,255,255,.6);
    }
    .hint {
      position: fixed;
      right: 24px;
      top: 24px;
      font-size: .72rem;
      letter-spacing: .06em;
      text-transform: uppercase;
      color: rgba(255,255,255,.45);
      background: rgba(0,0,0,.4);
      border: 1px solid rgba(255,255,255,.1);
      border-radius: 6px;
      padding: 6px 10px;
      pointer-events: none;
    }
  </style>
</head>
<body>

  <!-- Three.js renders into this single canvas -->
  <canvas id="scene"></canvas>

  <div class="overlay">
    <h1>Three<span>.js</span> Scene</h1>
    <p>A torus knot, a starfield, and three lights — drag to orbit, scroll to zoom.</p>
  </div>
  <div class="hint">drag · scroll to zoom</div>

  <!-- Import map lets the addons resolve the bare "three" import -->
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.164.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.164.0/examples/jsm/"
    }
  }
  </script>

  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const canvas = document.getElementById('scene');

    // 1. SCENE — the container for everything 3D
    const scene = new THREE.Scene();
    scene.background = new THREE.Color('#0a0a12');

    // 2. CAMERA — the point of view
    const camera = new THREE.PerspectiveCamera(
      45, window.innerWidth / window.innerHeight, 0.1, 100
    );
    camera.position.set(0, 1, 6);

    // 3. RENDERER — draws the scene into the canvas
    const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(window.innerWidth, window.innerHeight);

    // 4. MESH — a torus knot with a shiny standard material
    const knot = new THREE.Mesh(
      new THREE.TorusKnotGeometry(0.9, 0.28, 160, 24, 2, 3),
      new THREE.MeshStandardMaterial({ color: 0x84cc16, roughness: 0.25, metalness: 0.5 })
    );
    scene.add(knot);

    // 5. LIGHTS — ambient fill, a "sun", and an orbiting point light
    scene.add(new THREE.AmbientLight(0xffffff, 0.35));
    const sun = new THREE.DirectionalLight(0xffffff, 1);
    sun.position.set(-3, 4, 2);
    scene.add(sun);
    const point = new THREE.PointLight(0xd946ef, 2.5, 12);
    scene.add(point);

    // 6. PARTICLE FIELD — 1,500 dots as a single Points object
    const count = 1500;
    const positions = new Float32Array(count * 3);
    for (let i = 0; i < count; i++) {
      positions[i*3+0] = (Math.random() - 0.5) * 14;
      positions[i*3+1] = (Math.random() - 0.5) * 14;
      positions[i*3+2] = (Math.random() - 0.5) * 14;
    }
    const starGeo = new THREE.BufferGeometry();
    starGeo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    const stars = new THREE.Points(
      starGeo,
      new THREE.PointsMaterial({ color: 0xaaaaaa, size: 0.04, sizeAttenuation: true })
    );
    scene.add(stars);

    // 7. ORBIT CONTROLS — drag to rotate, scroll to zoom
    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.minDistance = 3;
    controls.maxDistance = 12;

    // 8. ANIMATION LOOP — runs once per browser paint (~60 fps)
    function tick(t) {
      knot.rotation.x = t * 0.0003;
      knot.rotation.y = t * 0.0005;
      point.position.set(Math.sin(t*0.001)*3, 1, Math.cos(t*0.001)*3);
      stars.rotation.y = t * 0.00008;
      controls.update();
      renderer.render(scene, camera);
      requestAnimationFrame(tick);
    }
    requestAnimationFrame(tick);

    // Keep the scene sized to the window
    window.addEventListener('resize', () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>
</html>