Until now, our drawings lived in a plane. To turn that plane away from us, we need to describe where each point goes outside it. Add one coordinate, keep the same vector operations, and give the screen a separate job.
Give Z a spatial meaning
A spatial point now has three coordinates:
vec3 localPoint = vec3(0.6, 0.3, 0.4);
X and Y measure the familiar horizontal and vertical displacements. Z measures displacement along a third perpendicular axis. In our front view, positive Z points toward us. The system is right-handed: point your right thumb along a positive rotation axis; your curled fingers give the positive turning direction.
This Z is different from the extra component in chapter 16. There, vec3(x,y,w) represented a 2D point or direction in homogeneous coordinates. Here, vec3(x,y,z) contains three spatial components. A mat3 now holds a 3D linear transform. A full 3D affine transform would need a fourth homogeneous component and a mat4.
The type does not decide whether a vector represents a point or a direction. Subtract two positions to get a displacement. Translation affects positions, while a rotation acts on both positions relative to the origin and directions:
vec3 direction = pointB - pointA;
vec3 worldPoint = R * localPoint;
vec3 worldDirection = R * direction;
Length also extends directly: sqrt(x*x+y*y+z*z). Our sample (0.6,0.3,0.4) has length sqrt(0.61), about 0.78102. The old functions still work:
float distanceFromOrigin = length(localPoint);
float agreement = dot(a, b); // a.x*b.x + a.y*b.y + a.z*b.z
vec3 unitDirection = length(direction) > 0.0
? normalize(direction) : vec3(0.0);
The zero fallback avoids normalizing a vector without a direction. In this lab the selected point is never the zero vector, because its local X and Y stay at 0.6 and 0.3.
Build from three axes
The standard basis now contains X=(1,0,0), Y=(0,1,0), and Z=(0,0,1). A point is x copies of X, y copies of Y, and z copies of Z. A matrix records where those three basis vectors go:
mat3 R = mat3(axisX, axisY, axisZ);
vec3 world = R * local;
// world = local.x*axisX + local.y*axisY + local.z*axisZ;
The constructor takes columns, just as before. Each column now has three spatial components. R[2] is the transformed Z axis; it is no longer a translation column. The lab colors X orange, Y green, and Z blue, and lists the complete columns below the picture.
A pure rotation keeps these axes perpendicular and one unit long. This preserves angles and lengths in the world. Their projections onto a flat screen need not remain perpendicular or have equal visible lengths.
Turn around Y
A rotation around Y leaves the Y component unchanged and mixes X with Z. At a positive quarter turn, X goes toward negative Z, while Z goes toward positive X. At an arbitrary angle, those rotated axes become (c,0,−s) and (s,0,c), where c=cos(angle) and s=sin(angle):
mat3 rotationY(float angle) {
float c = cos(angle), s = sin(angle);
return mat3(vec3(c, 0.0, -s),
vec3(0.0, 1.0, 0.0),
vec3(s, 0.0, c));
}
Expand the product once to see what each coordinate does:
world.x = c*local.x + s*local.z;
world.y = local.y;
world.z = -s*local.x + c*local.z;
At 90°, the sample (0.6,0.3,0.4) becomes (0.4,0.3,−0.6). Its squared length is still 0.61. More generally, expanding world.x²+world.z² cancels the mixed terms and leaves (c²+s²)(local.x²+local.z²). Since c²+s²=1 and Y is unchanged, the rotation preserves length.
The unchanged axis is also visible in the matrix: its second column remains (0,1,0). Angles enter GLSL in radians. The interface converts degrees once; 90° becomes pi/2. Tiny residual components near zero are normal floating-point results.
Choose what the screen keeps
Our screen has two coordinates. The simplest front view keeps world X and Y:
vec2 front = world.xy;
This is an orthographic projection. It does not divide by depth, so objects do not get smaller when moved away from the viewer. Points with the same X and Y overlap even if their Z coordinates differ. One projected position cannot uniquely tell us the original 3D position.
The side view looks from positive X toward the origin. Keep Y pointing up and let negative Z point right:
vec2 side = vec2(-world.z, world.y);
The minus sign chooses this screen orientation. Switching views does not change R or worldPoint. It only changes which components we display. At Y=90°, the transformed X axis is (0,0,−1): Front shows (0,0), while Side shows (1,0). Its world length remains 1 in both views.
The wireframe plane behaves the same way. It starts in local XY with z=0. At a Y quarter turn, Front sees it edge-on as a vertical line; Side sees its full rectangle. This is not the singular collapse from chapter 15. The 3D rotation is invertible. Information is discarded afterward by the projection.
Likewise, at Identity, changing point.z does nothing to the gold marker in Front. Switch to Side to see that movement. The cyan segment connects the point to the rotated location of (0.6,0.3,0), making its displacement from the original plane visible when the view allows it.
Rotate around another axis
To rotate around Z, keep Z fixed and apply our familiar 2D rotation to X and Y. Write the images of the basis vectors as columns:
mat3 rotationZ(float angle) {
float c = cos(angle), s = sin(angle);
return mat3(vec3(c, s, 0.0),
vec3(-s, c, 0.0),
vec3(0.0, 0.0, 1.0));
}
At +90°, X becomes Y, Y becomes −X, and Z stays Z. The point becomes (−0.3,0.6,0.4). Rotation around X follows the same construction: X stays fixed, Y becomes (0,c,s), and Z becomes (0,−s,c). At +90°, our point becomes (0.6,−0.4,0.3).
These are rotations about the fixed world origin. We choose one axis at a time here. Composing several 3D rotations follows the same right-to-left rule as chapter 13; their order generally matters too.