Skip to content

Transformation Pipeline

3D renderers can be done in two ways, depending on how they project view space onto the screen, and where they clip geometry outside the view frustum. * Matrix Pipeline - used by graphics hardware, graphics APIs and most modern renderers * Direct Projection - used by simpler 3D renderers

See Implementing a software rasteriser for more specific detail and a link to my Gitlab code.

Common to both methods

Model Space

The coordinate system used to define objects using 3D coordinates that are relative to the object's own origin. Used when defining the object. Also called object space or local space.

World Space

The coordinate system used to position objects relative to each other.

View Space

The world expressed from the camera's point of view, with the camera placed at the origin looking along the Z axis, either -Z (common in OpenGL) or +Z (common in software renderers). Also called camera space or eye space. The camera never moves, the whole world is transformed by the inverse of the camera's transform - both the camera's rotations and translations are inverted.

See World to Screen Transforms

View Frustum

The truncated pyramid that defines the viewing volume. It is created by six planes - near, far, top, bottom, left, right. Anything outside this frustum is clipped. Anything crossing the boundaries is cut into smaller triangles. It is important the near plane is handled correctly. Points behind the camera appear mirrored, points at the camera give a division by zero, and points just in front of the camera give extremely large values.

Matrix Pipeline

What it is

This uses matrices to handle all transforms and projection. Used in modern 3D, 3D hardware and many more serious software renderers.

Do it this way to match how modern graphics hardware and APIs work, or when you want every transform handled the same way.

The matrix pipeline clips before dividing, enabling it to safely remove points at or behind the camera.

Model -> World -> View -> Clip -> NDC -> Screen -> Rasterisation

Projection Transform

The transform that maps view space into clip space. It scales x and y according to the field of view, remaps z into a depth range, and stores the view-space depth in w, so that the perspective divide can create perspective.

Clip Space

The coordinate system produced by the projection transform, where geometry is tested against the view frustum and anything outside it is removed.

Perspective Divide

Dividing x, y and z by w, the view-space depth, to create perspective. The result is Normalised Device Coordinates.

Normalised Device Coordinates

The result of the perspective divide, where visible points lie within a fixed range, usually -1 to 1 on x and y, with depth kept in Z, remapped into a fixed range that is usually -1 to 1 or 0 to 1.

Viewport Transform

Scales and offsets NDC into pixel coordinates, producing screen space.

Direct Projection

What it is

This is the projection done using simple divides of x and y by z. Often done without matrices for rotation and translation on older machines and simple software 3D engines. Matrices can still be used for rotation and translation. Only the projection is done by a direct divide.

Do it this way for very simple 3D views.

Direct projection has no w value, so it must clip against the near plane in view space.

Model -> World -> View -> Near plane clipping -> Projection -> Screen -> Rasterisation

Near Plane Clipping

Done in view space, before projection. Triangles with all vertices closer than the near plane are removed. Triangles with some of them closer are cut at the plane. The other frustum planes are usually ignored. Instead drawing is limited to the edges of the framebuffer during rasterisation.

Projection

x' = x * d / z and y' = y * d / z, where d is the distance from the camera to the projection plane. Choosing d = (screen width / 2) / tan(horizontal field of view / 2) makes x' and y' pixel offsets from the centre of the screen, so this method does not need NDC. The original z, or 1/z, is kept for depth testing. With the -Z convention, divide by -z instead.

static Vec2 project_to_screen(Vec3 point, float focal_length, float aspect_ratio)
{
    /* Perspective divide: x/z and y/z. This assumes point.z > 0 (the point is
     * in front of the camera) - the caller is responsible for making sure of
     * that (see NEAR_PLANE handling in main()). */
    float ndc_x = (point.x / point.z) * focal_length;
    float ndc_y = (point.y / point.z) * focal_length;
    /* Correct horizontal spread for the window's aspect ratio. */
    ndc_x /= aspect_ratio;
    Vec2 screen;
    screen.x = (ndc_x + 1.0f) * 0.5f * (float)WINDOW_WIDTH;
    screen.y = (1.0f - ndc_y) * 0.5f * (float)WINDOW_HEIGHT;
    return screen;
}

Common to both

Screen Space

Projected coordinates mapped to pixel coordinates in the target framebuffer. Y is usually flipped, because screen Y increases downward. Depth is kept for each point. In the matrix pipeline, this is done by the viewport transform. In direct projection: screen x = width / 2 + x', screen y = height / 2 − y'.

Rasterisation

Deciding which pixels in the framebuffer lie within the geometry being drawn. Usually also includes interpolating vertex values (depth, colour, texture coords) across every pixel the triangle covers.

Depth is also not linear in screen space, so software renderers often store 1/z in the depth buffer instead of z.

If texture coordinates use Linear Interpolation (Lerp) in screen space, the result is Affine Texture Mapping, which is not perspective correct. To correct it, interpolate u/z, v/z and 1/z in screen space, then divide the first two by 1/z at each pixel.

Once we have a 2D triangle (three screen-space points), we need to work out which pixels are "inside" it so we can colour them in. This is done using the Edge Function technique.