Command Palette

Search for a command to run...

Hectal
Course outline
Topic 2.4Beginner ~21 min · step 4 of 7

Coordinate Spaces

In one line

A point has different numbers depending on what you measure it from. The same wheel is (1, 0, 0) from its frame and (2, 0, −1) from the world, and the GPU carries it through camera, clip and screen space before a pixel is lit.

0/7 · 0%

Think of it like this

Ask for directions to a cafe. "Second table on the left" is measured from the door. "12 Mill Street" is measured from the city. "Fifty metres ahead" is measured from where you stand. It is the same cafe with three different sets of numbers, because three different things are the starting point. Each starting point and its way of counting is a coordinate space.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

coordinate space
A starting point and a set of axes that numbers are measured from. Local, world, view, clip and screen are the five you meet.
local space
Numbers measured from an object's parent (for position) or from the object's own centre (for its vertices).
world space
Numbers measured from the scene's origin. Where things really are.
view space
Numbers measured from the camera, which looks down −z. Also called camera or eye space.

Step by step

01The real numbers

In the first example the frame stands at x = 2 and is turned 90° around y. The wheel is at local (1, 0, 0). Its world position is (2, 0, −1): turning 90° around y sends the frame's x axis to world −z, so "1 along the frame" is 1 along −z, starting from x = 2. localToWorld gives the same answer, and worldToLocal returns (1, 0, 0).

02Through the camera

The same point continues. In camera space it is (0.207, −0.088, −7.155): the camera is the origin, the point is slightly right and slightly below, and 7.155 units ahead (negative z). After the projection and division by w it is (0.031, −0.026, 0.974) in NDC, close to the centre of the picture. On a 300 × 150 picture that is pixel (155, 77).

03A scaled parent

In the second example the parent has scale 2. A child at local (1, 0, 0) is at world (2, 0, 0), and its world scale is 2, so its radius of 0.2 shows as 0.4. The child's own scale still says (1, 1, 1): the doubling comes from the parent.

04add or attach

The blue ball sits at world (2, 0, 0). other.attach(twin) puts it into a group at x = 4 and keeps it at (2, 0, 0): its local position becomes (−2, 0, 0). The orange ball is moved with other.add(child): its local numbers stay (1, 0, 0), so it jumps to world x = 5, which is 4 + 1.

Try it yourself

  1. 1

    Scale the frame

    In the first example, add frame.scale.setScalar(2); after frame.rotation.y = Math.PI / 2;. The wheel's local position stays (1, 0, 0), but its world position becomes (2, 0, −2) and every later number changes: the camera line prints (0.621, 0.283, -7.987) and the pixel is (163, 69).

  2. 2

    Swap attach for add

    In the second example, change other.attach(twin); to other.add(twin);. The first output line of the pair becomes attach(): world (2, 0, 0) -> (5, 0, 0), local is now (1, 0, 0): the label still says attach, but the code now behaves like add, and the twin jumps to x = 5.

Code & diagrams

Try it: interactive

Coordinate-space playground

Drag the frame's position, turn and scale. The wheel's local numbers never change, but its world position does. Then change the wheel's local x.

One point through every space New tab

The frame (with its own axes) stands at x = 2 and is turned 90° around y; the orange wheel is its child. The camera numbers are for a picture twice as wide as tall.

idle

Console

Run the scene to see its console.

Expected console output

1. local position  (1, 0, 0)
2. world position  (2, 0, -1)
   same, with localToWorld: (2, 0, -1)
   and back, with worldToLocal: (1, 0, 0)
3. camera (view) space  (0.207, -0.088, -7.155)   (the camera is the origin and looks down -z)
4. clip, after dividing by w (NDC)  (0.031, -0.026, 0.974)   (-1 to 1 is inside the picture)
5. screen pixels of a 300 x 150 picture  (155, 77)
A scaled parent, add() and attach() New tab

The orange ball is a child of a parent with scale 2; the blue ball is moved with attach. The grid is 10 × 10 units.

idle

Console

Run the scene to see its console.

Expected console output

child local position (1, 0, 0), world position (2, 0, 0)
child scale (1, 1, 1), world scale (2, 2, 2)  (radius 0.2 now looks like 0.4)
attach(): world (2, 0, 0) -> (2, 0, 0), local is now (-2, 0, 0)
add():    world (2, 0, 0) -> (5, 0, 0), local is still (1, 0, 0)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Put the wheel in the scene, not the frame

In the first example, change frame.add(wheel); to scene.add(wheel);.

terminal
$ Run
── what you'll see ──
1. local position (1, 0, 0)
2. world position (1, 0, 0)
same, with localToWorld: (2, 0, -1)
and back, with worldToLocal: (0, 0, -1)
3. camera (view) space (-1.117, -0.291, -6.702) (the camera is the origin and looks down -z)
4. clip, after dividing by w (NDC) (-0.179, -0.093, 0.972) (-1 to 1 is inside the picture)
5. screen pixels of a 300 x 150 picture (123, 82)

Myth vs fact

Myth

object.position is where the object is.

Fact

It is where the object is relative to its parent. Only for a direct child of the scene is it also the world position. For anything inside a group, use getWorldPosition().

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Clip space exists for a reason: the GPU clips triangles against the −w..w box in clip space before dividing by w, because dividing a triangle that passes through the camera plane would flip it. Division happens afterwards, once clipping has made it safe. This is also why the fourth number, w, is the distance in front of the camera for a perspective camera.

  • ▸

    getWorldPosition needs up-to-date matrices. It calls updateWorldMatrix(true, false) on its way, so it is safe in your own code. localToWorld and worldToLocal do not do that update: they use the matrices as they were the last time three.js computed them, so call scene.updateMatrixWorld() first if you have just moved things, as the first example does.

Remember this

  1. 1

    The problem this solves: the wheel of a bike is placed relative to its frame, and the frame is placed relative to the stand. When the frame turns, the wheel must go with it, without you recalculating the wheel's position. Coordinate spaces are how three.js does that, and how it gets your point to a pixel.

  2. 2

    Local space. A mesh's own space. Its position, rotation and scale are measured from its parent (the object it was added to, or the scene). Its vertices are measured from its own origin: a BoxGeometry(1, 1, 1) has corners at ±0.5. This is the space you write in.

  3. 3

    World space. The space of the whole scene: the scene's origin and axes. object.getWorldPosition(target) gives the object's position in it. parent.localToWorld(v) converts a point from a parent's local numbers into world numbers, and worldToLocal(v) goes back. Both are done with the world matrix, which three.js builds by multiplying the matrices of all ancestors (Topic 2.5).

  4. 4

    Clip space, NDC and screen space. The camera's projection matrix turns view space into clip space, where the GPU decides what is inside the picture. Dividing x, y, z by a fourth number called w gives NDC (normalised device coordinates): −1 to 1 on each axis, with −1 on the left, bottom and near. NDC is mapped to screen space: pixel columns and rows. vector.project(camera) does world → NDC in one call.

  5. 5

    Parent and child. Add an object to another, and it joins its space. When the parent moves, turns or scales, the child follows, and the child's own numbers stay the same. A parent scaled by 2 makes every child twice as big and twice as far from it: with scale = 2, a child at local x = 1 sits at world x = 2.

  6. 6

    **add() and attach().** parent.add(child) keeps the child's local numbers, so its world position jumps when the new parent is somewhere else. parent.attach(child) keeps the child's world position, and changes its local numbers to match. Use attach when you pick something up and want it to stay where it was.

Explain it without notes

01

A wheel is a child of a frame, and the frame is turned 90° and moved to x = 2. How do you get the wheel's position in world space, and what do you expect for a local position of (1, 0, 0)?

02

Explain the difference between parent.add(child) and parent.attach(child), with a case where you need attach.

Practice

01

A group named pivot sits at (3, 0, 0). Add a ball to it at local (0, 1, 0). Print the ball's world position, then turn pivot 90° around z and print it again.

02

Write a function worldPositionOf(object) that returns a new Vector3, and explain why it is better than reading object.position.

Done when you can

  • Name the five spaces and say what each one is measured from

  • Get a world position with getWorldPosition and convert with localToWorld and worldToLocal

  • Say what a scaled parent does to a child's position and size

  • Choose between add and attach