3D Scenes
The preview can draw 3D scenes as well as 2D. 3D is built with THREE, which is the three.js 3D toolkit driven from Python, so the names here are three.js's own names. Everything three.js can do is there, and its documentation at threejs.org/docs describes every part in full. This page covers the parts you need most, in Python; 3D functions lists every geometry, material, light and more, with pictures.
The three things every scene needs
A 3D picture needs a renderer (it draws onto the preview), a scene (the world you put things in) and a camera (where you look from). Then, every frame, the renderer draws the scene as the camera sees it.
# 1. The renderer draws on the preview's canvas
renderer = THREE.WebGLRenderer(canvas=canvas, antialias=True)
renderer.setPixelRatio(pixel_ratio)
renderer.setSize(width, height, False)
# 2. The scene holds everything
scene = THREE.Scene()
scene.background = THREE.Color("#101820")
# 3. The camera: field of view in degrees, shape, nearest and farthest distance it sees
camera = THREE.PerspectiveCamera(60, width / height, 0.1, 100)
camera.position.set(0, 1.5, 4)
camera.lookAt(0, 0, 0)
# Something to look at, and light to see it by
cube = THREE.Mesh(THREE.BoxGeometry(1, 1, 1), THREE.MeshStandardMaterial(color="tomato"))
scene.add(cube)
scene.add(THREE.AmbientLight("white", 0.4))
sun = THREE.DirectionalLight("white", 2)
sun.position.set(3, 4, 5)
scene.add(sun)
def draw():
cube.rotation.y += 0.01
renderer.render(scene, camera)
Start every 3D program with those first lines. What they do:
canvas=canvastells the renderer to draw on the preview.antialias=Truesmooths the edges.setPixelRatio(pixel_ratio)keeps the picture sharp on high-resolution screens.setSize(width, height, False)sizes the drawing to the preview.- Resizing is automatic. When the preview changes size, the renderer and every perspective camera are refitted for you, so the scene never looks squashed.
Distances are in whatever unit you like; most scenes treat 1 as one metre. The y axis points up, x to the right, and z towards you.
Calling three.js from Python
The rules from How a program is shaped apply:
- A capitalised name makes something new:
THREE.Mesh(...),THREE.Vector3(1, 2, 3). You never writenew. - Keyword arguments become an options object:
THREE.MeshStandardMaterial(color="gold", metalness=0.8). - Colours can be a name or a hex string (
"tomato","#ff6347") or a number (0xff6347). - Changing values in place works:
mesh.rotation.y += 0.01. - snake_case works here too (
set_pixel_ratio), but this page uses three.js's own spelling so that it matches its documentation.
Shapes: geometries
A geometry is the shape of an object. These are the common ones; the numbers are sizes, and the optional last numbers say how smooth the surface is.
| Geometry | Shape |
|---|---|
THREE.BoxGeometry(w, h, d) | A box |
THREE.SphereGeometry(radius, 32, 16) | A ball |
THREE.CylinderGeometry(top, bottom, height, 32) | A cylinder, or a cone if one radius is 0 |
THREE.ConeGeometry(radius, height, 32) | A cone |
THREE.TorusGeometry(radius, tube, 16, 64) | A ring doughnut |
THREE.TorusKnotGeometry(radius, tube, 128, 16) | A knotted tube |
THREE.PlaneGeometry(w, h) | A flat rectangle, good for floors |
THREE.CircleGeometry(radius, 32) | A flat disc |
THREE.IcosahedronGeometry(radius, 0) | A 20-sided gem (raise the 0 to make it rounder) |
THREE.CapsuleGeometry(radius, length, 8, 16) | A pill |
Looks: materials
A material is how the surface looks. A mesh is a geometry plus a material: THREE.Mesh(geometry, material).
| Material | Looks |
|---|---|
THREE.MeshBasicMaterial(color=…) | Flat colour. Ignores lights, so it always shows. Good for glowing things. |
THREE.MeshLambertMaterial(color=…) | Matte, like paper or chalk. Needs light. |
THREE.MeshPhongMaterial(color=…, shininess=60) | Shiny highlights, like plastic. Needs light. |
THREE.MeshStandardMaterial(color=…, roughness=0.5, metalness=0) | Realistic. roughness 0 is a mirror finish, 1 is rough; metalness 1 is metal. Needs light. |
THREE.MeshNormalMaterial() | Rainbow colours by direction. Needs no light; handy for testing. |
Useful options on any material: wireframe=True (show the edges only), transparent=True, opacity=0.5 (see-through), side=THREE.DoubleSide (show the back of flat shapes), flatShading=True (show the facets).
renderer = THREE.WebGLRenderer(canvas=canvas, antialias=True)
renderer.setPixelRatio(pixel_ratio)
renderer.setSize(width, height, False)
scene = THREE.Scene()
scene.background = THREE.Color("#1b1b24")
camera = THREE.PerspectiveCamera(50, width / height, 0.1, 100)
camera.position.set(0, 0, 8)
camera.lookAt(0, 0, 0)
scene.add(THREE.HemisphereLight("white", "#334", 1.5))
light = THREE.DirectionalLight("white", 2)
light.position.set(2, 5, 4)
scene.add(light)
shapes = [
THREE.Mesh(THREE.BoxGeometry(1, 1, 1), THREE.MeshStandardMaterial(color="tomato")),
THREE.Mesh(THREE.SphereGeometry(0.6, 32, 16), THREE.MeshStandardMaterial(color="gold", metalness=0.4, roughness=0.3)),
THREE.Mesh(THREE.ConeGeometry(0.6, 1.2, 32), THREE.MeshLambertMaterial(color="mediumseagreen")),
THREE.Mesh(THREE.TorusGeometry(0.5, 0.2, 16, 64), THREE.MeshPhongMaterial(color="deepskyblue", shininess=80)),
THREE.Mesh(THREE.IcosahedronGeometry(0.6, 0), THREE.MeshNormalMaterial(flatShading=True)),
THREE.Mesh(THREE.TorusKnotGeometry(0.4, 0.12, 128, 16), THREE.MeshBasicMaterial(color="hotpink", wireframe=True)),
]
for i, mesh in enumerate(shapes):
mesh.position.x = (i % 3 - 1) * 2 # three across
mesh.position.y = 1 - (i // 3) * 2 # two rows
scene.add(mesh)
def draw():
for mesh in shapes:
mesh.rotation.x += 0.01
mesh.rotation.y += 0.015
renderer.render(scene, camera)
Placing things: position, rotation and scale
Every object has a position, a rotation and a scale, each with x, y and z:
| Code | What it does |
|---|---|
mesh.position.set(1, 0, -2) | Moves it to that spot |
mesh.position.y = 2 | Moves it up to height 2 |
mesh.rotation.y = math.pi / 4 | Turns it 45 degrees around the up axis (radians) |
mesh.rotation.x += 0.01 | Keeps turning it, a little each frame |
mesh.scale.set(2, 1, 1) | Stretches it to twice as wide |
mesh.lookAt(0, 0, 0) | Turns it to face a point |
mesh.visible = False | Hides it |
scene.remove(mesh) | Takes it out of the scene |
Groups
A THREE.Group() holds other objects so they move together. Turn the group and everything in it turns around the group's centre. Groups inside groups are how you build things like a planet with a moon going round it (solar system tutorial).
import math
renderer = THREE.WebGLRenderer(canvas=canvas, antialias=True)
renderer.setPixelRatio(pixel_ratio)
renderer.setSize(width, height, False)
scene = THREE.Scene()
camera = THREE.PerspectiveCamera(50, width / height, 0.1, 100)
camera.position.set(0, 3, 7)
camera.lookAt(0, 0, 0)
scene.add(THREE.HemisphereLight("white", "#223", 2))
# a little windmill: a tower, and a group of blades that turns
tower = THREE.Mesh(THREE.CylinderGeometry(0.2, 0.4, 3, 16), THREE.MeshStandardMaterial(color="beige"))
scene.add(tower)
blades = THREE.Group()
blades.position.set(0, 1.5, 0.45)
scene.add(blades)
for i in range(4):
blade = THREE.Mesh(THREE.BoxGeometry(0.25, 1.6, 0.05), THREE.MeshStandardMaterial(color="firebrick"))
blade.position.y = 0.8 # sticks out from the centre
holder = THREE.Group() # turned to space the blades out
holder.rotation.z = i * math.pi / 2
holder.add(blade)
blades.add(holder)
def draw():
blades.rotation.z += 0.03
renderer.render(scene, camera)
Clouds of points
THREE.Points draws one dot per position, which suits stars, particles and scatter plots. Give it a list of numbers, three for each point (x, y, z).
import random
renderer = THREE.WebGLRenderer(canvas=canvas, antialias=True)
renderer.setPixelRatio(pixel_ratio)
renderer.setSize(width, height, False)
scene = THREE.Scene()
camera = THREE.PerspectiveCamera(60, width / height, 0.1, 100)
camera.position.z = 6
positions = []
for _ in range(3000):
positions += [random.uniform(-5, 5), random.uniform(-5, 5), random.uniform(-5, 5)]
geometry = THREE.BufferGeometry()
geometry.setAttribute("position", THREE.Float32BufferAttribute(positions, 3))
stars = THREE.Points(geometry, THREE.PointsMaterial(color="white", size=0.04))
scene.add(stars)
def draw():
stars.rotation.y += 0.002
renderer.render(scene, camera)
Numbers from numpy work too: pass array.ravel() where the list goes.
Looking around: orbit controls
addons.OrbitControls(camera, canvas) lets you turn the camera around the scene by dragging: drag to orbit, pinch or scroll to zoom, and drag with two fingers (or right-drag) to pan. Call controls.update() every frame.
renderer = THREE.WebGLRenderer(canvas=canvas, antialias=True)
renderer.setPixelRatio(pixel_ratio)
renderer.setSize(width, height, False)
scene = THREE.Scene()
scene.background = THREE.Color("#0e1116")
camera = THREE.PerspectiveCamera(50, width / height, 0.1, 100)
camera.position.set(3, 3, 5)
controls = addons.OrbitControls(camera, canvas)
controls.enableDamping = True # glides to a stop when you let go
controls.target.set(0, 0.5, 0) # the point it orbits around
scene.add(THREE.GridHelper(10, 10))
scene.add(THREE.AxesHelper(2))
knot = THREE.Mesh(THREE.TorusKnotGeometry(0.6, 0.2, 128, 16), THREE.MeshNormalMaterial())
knot.position.y = 1
scene.add(knot)
def draw():
controls.update()
renderer.render(scene, camera)
GridHelper and AxesHelper draw a floor grid and the three axes (red x, green y, blue z), which help you find your way while building a scene.
Picking: what did I click?
A THREE.Raycaster finds which objects are under a point on the screen. Finding out needs an answer back from the preview, so the click handler is async and uses await (reading a value back).
import random
renderer = THREE.WebGLRenderer(canvas=canvas, antialias=True)
renderer.setPixelRatio(pixel_ratio)
renderer.setSize(width, height, False)
scene = THREE.Scene()
scene.background = THREE.Color("#202020")
camera = THREE.PerspectiveCamera(50, width / height, 0.1, 100)
camera.position.set(0, 0, 8)
scene.add(THREE.HemisphereLight("white", "#444", 2.5))
boxes = []
for i in range(5):
box = THREE.Mesh(THREE.BoxGeometry(1, 1, 1), THREE.MeshStandardMaterial(color="lightgray"))
box.position.x = (i - 2) * 1.6
box.name = f"box {i + 1}"
scene.add(box)
boxes.append(box)
ray = THREE.Raycaster()
async def clicked(event):
# the click as -1..1 across and up the preview
point = THREE.Vector2(event.x / width * 2 - 1, -(event.y / height) * 2 + 1)
ray.setFromCamera(point, camera)
hits = ray.intersectObjects(boxes)
if await hits.length > 0:
hit = hits[0].object
hit.material.color.setHSL(random.random(), 0.8, 0.55)
print("You clicked", await hit.name)
canvas.addEventListener("pointerdown", clicked)
def draw():
for box in boxes:
box.rotation.y += 0.01
renderer.render(scene, camera)
Lights and shadows
Most materials need light to be seen. Lights, and how to turn on shadows, have a page of their own: lighting a scene.
Related
- 3D functions: every geometry, material, light and more, with pictures
- Colours
- Tutorial: a spinning cube
- Tutorial: lighting a scene
- Tutorial: a solar system
- three.js documentation