Rendering Engines - 3D Transformations

Position, rotation and scale, why they are relative to a parent, and the journey from local space to the screen.

three-js webgl

A transformation moves an object without changing its geometry. The vertex data stays exactly as it was; what changes is where those vertices are interpreted as being.

Where This Fits

The rendering engine layer, with one foot in GLSL — the matrices at the end of this note are what these properties become by the time a shader sees them.

Assumes The Scene Graph. Connects to Vertex and Fragment Shaders.

The Three Transforms

TransformWhat it doesUnits
PositionTranslates along X, Y and ZScene units
RotationTurns about the X, Y and Z axesRadians
ScaleStretches along X, Y and ZMultipliers, 1 is unchanged

Position and scale are triples read one value per axis. Scale is multiplicative, so 1 leaves the object alone and 0 collapses it.

Rotation has the trap. Values are in radians, not degrees — a full turn is 2π2\pi, a right angle π/2\pi/2. Setting a rotation to 90 expecting a right angle gives roughly fourteen revolutions.

Rotations also do not commute: X then Y lands somewhere different from Y then X. The order is part of the rotation’s definition, and engines expose it as a setting.

Everything Is Relative to a Parent

Transforms are measured against whatever the object is attached to, not against the world. This is what makes the scene graph a tree.

Scene
└── Car          position (10, 0, 0), rotation (0, π/4, 0)
    ├── Body     position (0, 0, 0)
    ├── Wheel A  position (1, 0, 1)     ← relative to Car, not the scene
    └── Wheel B  position (-1, 0, 1)

Grouping is the practical form. Four cubes positioned inside a group, then one rotation applied to the group itself:

const group = new THREE.Group();

[[-0.6, 0.6], [0.6, 0.6], [-0.6, -0.6], [0.6, -0.6]].forEach(([x, y]) => {
  const cube = new THREE.Mesh(geometry, material);
  cube.position.set(x, y, 0);
  group.add(cube);
});

group.rotation.set(0.6, 0.8, 0.4);   // one rotation, on the parent

group-transform.png

Left is the group untouched, right the same group after that single rotation. No cube’s own transform changed — each still sits at its own corner, relative to the parent that moved. The alternative is recalculating every child’s world position by hand each time the assembly moves.

Local Space to Screen Space

A vertex starts in local space, relative to its own geometry’s origin. Getting to a pixel is three steps.

Model applies the object’s own transform chain, walking up through every parent, putting the vertex in world space. View re-expresses that world position relative to the camera, so the camera becomes the origin. Projection flattens the result onto a 2D plane, applying perspective if the camera has it.

Each step is a matrix, and applying one is a multiplication. This is why the standard closing line of a vertex shader reads as it does — the three stages composed right to left:

gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

position is the local-space vertex. modelViewMatrix combines the model and view stages, since they are always applied together. The engine computed both matrices before the shader ran; the line is a journey from the object’s own frame of reference out to the screen’s.

See Also

Resources

  • Object3D — position, rotation and scale on every three.js object
  • Euler — how rotation order is expressed

Source: WebGL & GLSL — A Primer by Matt DesLauriers

-
-