Tutorial: A Spinning 3D Cube

Your first 3D program: a lit cube that spins, then the same cube with a floor, outlined edges, and a camera you can turn by dragging. For the pieces in more depth, see 3D scenes.

1. The cube

// the renderer draws on the canvas
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
renderer.setPixelRatio(pixelRatio)
renderer.setSize(width, height, false)

// the world, and where we look at it from
const scene = new THREE.Scene()
scene.background = new THREE.Color("#14141c")
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100)
camera.position.z = 3

// a cube: a box shape with a material that reacts to light
const cube = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: "dodgerblue" }),
)
scene.add(cube)

// light: a soft fill everywhere, and one bright light from the top right
scene.add(new THREE.AmbientLight("white", 0.5))
const light = new THREE.DirectionalLight("white", 2.5)
light.position.set(2, 3, 4)
scene.add(light)

function draw() {
  // follow the canvas when it changes size
  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height, false)

  cube.rotation.x += 0.01
  cube.rotation.y += 0.015
  renderer.render(scene, camera)
}

Reading it from the top:

  1. The renderer draws the 3D picture on the canvas. These three lines start every 3D program; copy them as they are.
  2. The scene is the world. Its background is a dark grey.
  3. The camera is your eye. 60 is how wide it sees, in degrees; width / height is the shape of the canvas; it sees things between 0.1 and 100 units away. It sits 3 units back from the middle (position.z = 3), looking at the middle.
  4. The cube is a mesh: a shape (BoxGeometry, 1 unit each way) plus a material (MeshStandardMaterial, blue).
  5. The lights: without light, a standard material is black. The ambient light lights everything a little; the directional light shines from one direction like the sun, which is what makes the sides different shades.
  6. draw() first fits the camera and the renderer to the canvas, then turns the cube a little more and draws the scene. The three fitting lines go at the top of every 3D draw(); copy them as they are too. They are what keep the picture the right shape when the preview changes size.

Rotations are in radians, so 0.01 a frame at 60 frames a second is about 34 degrees a second.

2. Turn it with your finger

Add orbit controls and the camera circles the cube as you drag. A floor grid and outlined edges make the shape easier to read.

const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
renderer.setPixelRatio(pixelRatio)
renderer.setSize(width, height, false)

const scene = new THREE.Scene()
scene.background = new THREE.Color("#14141c")
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100)
camera.position.set(2, 2, 3)

// drag to turn, pinch or scroll to zoom
const controls = new addons.OrbitControls(camera, canvas)
controls.enableDamping = true

const geometry = new THREE.BoxGeometry(1, 1, 1)
const cube = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: "dodgerblue", roughness: 0.4 }))
cube.position.y = 0.8
scene.add(cube)

// white lines along the cube's edges, added to the cube so they turn with it
const edges = new THREE.LineSegments(
  new THREE.EdgesGeometry(geometry),
  new THREE.LineBasicMaterial({ color: "white" }),
)
cube.add(edges)

scene.add(new THREE.GridHelper(6, 12, "#555", "#333"))
scene.add(new THREE.AmbientLight("white", 0.5))
const light = new THREE.DirectionalLight("white", 2.5)
light.position.set(2, 3, 4)
scene.add(light)

let spinning = true

function onKeyDown(key) {
  if (key === " ") spinning = !spinning
}

function draw() {
  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height, false)

  if (spinning) cube.rotation.y += 0.015
  controls.update()
  renderer.render(scene, camera)
}
  • new addons.OrbitControls(camera, canvas) moves the camera when you drag on the canvas. enableDamping lets it glide to a stop, and controls.update() in draw() keeps it moving.
  • cube.add(edges) puts the lines inside the cube, so they move and turn with it.
  • Press space to pause and restart the spin. If it does nothing, click the preview once so it has the keyboard.

Try this

  • Swap BoxGeometry(1, 1, 1) for TorusKnotGeometry(0.5, 0.15, 128, 16).
  • Make it metal: new THREE.MeshStandardMaterial({ color: "silver", metalness: 1, roughness: 0.3 }).
  • Make a row of cubes with a for loop, each with its own position.x and colour, and keep them in an array to spin in draw().