We have moved points through a fixed view. Now let the scene stay where it is and move the place from which we describe it. The card does not rotate when the camera goes around it. Its coordinates in the camera's frame change.
A different origin and three new axes
Keep a small card in the world XY plane, centered at the origin. Put a separate sample point at (0.6,0.3,0.4), slightly in front of that plane. We need two positions to aim the camera:
vec3 eye = vec3(2.0, 1.0, 2.0);
vec3 target = vec3(0.0);
eye is the camera's position. target is a point it looks toward. Neither value moves the card. Moving eye around target changes which side of the card faces the camera. In the world pane, the card and sample remain fixed; in the view pane, their projected arrangement changes.
This is the change of basis from chapter 9, with three axes and a translated origin. We want each point's displacement along the camera's right, up, and back directions.
Choose the direction of positive Z
We use a right-handed view frame whose camera looks along negative Z. Positive Z points backward, toward the space behind the camera. Therefore back points from target toward eye:
vec3 offset = eye - target;
float distanceToTarget = length(offset);
// Continue only when distanceToTarget > 0.0001.
vec3 back = offset / distanceToTarget;
The subtraction order matters. target-eye points forward, which would give a different sign convention. With eye=(0,0,3) and target=(0,0,0), back=(0,0,1). A point at the world origin is three units in front of the camera, so we expect its view Z to be −3.
That negative coordinate does not mean a negative distance. It says which side of the camera's origin contains the point. Later, perspective will use this sign to turn forward depth into a positive divisor.
Find right, then correct up
An aiming direction still permits the camera to roll around it. Supply an up hint to choose the remaining orientation. Usually the hint is world Y:
vec3 upHint = vec3(0.0, 1.0, 0.0);
vec3 right = normalize(cross(upHint, back));
vec3 up = cross(back, right);
The first cross product supplies a direction perpendicular to the hint and back. For the front-facing camera, cross(Y,Z)=X: right really points right. The second cross constructs the perpendicular up direction. We used the same construction to complete a frame in chapter 18.
The hint is not generally the final up axis. If the camera looks downward, world Y leans toward its back direction. The corrected up removes that component. In exact arithmetic, right, up, and back have unit length, all three pairwise dot products are zero, and cross(right,up)=back.
For eye=(2,1,2) and target=0, the distance is 3. The axes are back=(2,1,2)/3, right=(1,0,−1)/sqrt(2), and up=(−1,4,−1)/(3*sqrt(2)). The camera tilts downward while keeping a consistent orientation. Reversing the first cross order would reverse both right and the corrected up, rolling the picture by 180 degrees.
Measure from the camera
First express the world point as a displacement from eye. Then ask how much of that displacement follows each unit axis:
vec3 relative = world - eye;
vec3 view = vec3(
dot(right, relative),
dot(up, relative),
dot(back, relative)
);
Each dot product is the signed coordinate along one camera axis. The view origin is eye itself: subtracting eye gives zero. A direction, such as a surface normal, has no position to translate; converting a direction uses the rotational part alone.
Try the calculation with target. Its displacement is −distanceToTarget*back. Perpendicularity makes its right and up coordinates zero. Its back coordinate is −distanceToTarget, because dot(back,back)=1. So target should become approximately (0,0,−distanceToTarget), up to floating-point rounding.
For our default camera, the sample at (0.6,0.3,0.4) becomes approximately (0.14142,0.04714,−2.23333). It sits slightly right of and above the center, in front of the camera. Its world coordinates have not changed.
Turn the three measurements into a matrix
Put the axes into GLSL's column constructor:
mat3 cameraToWorld = mat3(right, up, back);
This matrix builds a world direction from camera coordinates: it scales the three columns and adds them. We need the reverse operation to describe a world displacement in camera coordinates.
For an orthonormal matrix, its transpose is its inverse. Why? Each entry of transpose(C)*C is a dot product between two columns of C. The diagonal entries are 1; the other entries are 0. That produces the identity matrix.
vec3 view = transpose(cameraToWorld) * (world - eye);
// The reverse mapping:
vec3 worldAgain = eye + cameraToWorld * view;
Transposition exchanges rows and columns. The rows of transpose(cameraToWorld) therefore perform exactly the three dot products above. This shortcut relies on an orthonormal basis; it is not the inverse of an arbitrary scale or shear.
Notice the order: move the origin first, then change the axes. Forgetting the subtraction describes positions relative to the world's origin. Forgetting transpose applies the conversion in the wrong direction. At the Front preset the matrix is identity, so that second mistake is invisible. Oblique exposes it immediately.
Give the construction defined boundaries
If eye equals target, there is no aiming direction. The lab rejects a distance at or below 0.0001 before dividing. This cutoff belongs to the lab's coordinate scale; a rejected tiny separation may still be mathematically nonzero.
There is another boundary. Looking straight up or down makes world Y parallel to back, so their cross product vanishes. Near those directions, use world Z as the hint:
vec3 upHint = abs(back.y) > 0.99
? vec3(0.0, 0.0, 1.0)
: vec3(0.0, 1.0, 0.0);
With the usual hint, the first cross has length sqrt(1-back.y*back.y), at least about 0.141 within the chosen branch. With the alternate hint, it is comfortably nonzero because back is close to Y. We can safely normalize it after the distance check.
The alternate hint gives a deterministic frame at both poles, but switching hints can change the camera's roll abruptly. This is a small teaching camera, not a continuous free-flight controller. Its hint readout makes the choice visible. A more advanced controller would need a policy for carrying orientation through such motion.
Draw view coordinates without perspective
The camera calculation returns three coordinates. For this chapter, draw view.xy directly. This is an orthographic projection: moving a point only along the camera's Z axis does not make it smaller. We are separating the camera's coordinate system from the projection applied afterward.
Negative view Z means in front of the eye plane; positive Z means behind it. The lab deliberately draws both as a coordinate diagnostic. It has no depth clipping or hidden-surface calculation, so a projected marker is not a promise that a physical camera would see that point.
The world pane uses a fixed oblique projection and a wider scale to include the camera. The view pane uses the camera's XY coordinates. Compare positions within each pane; their different scales mean that equal pixel lengths across the panes are not equal world lengths.