Graphics glossary 18 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
A ray is often drawn as an arrow. The picture is useful, but an arrow on a page does not tell a program where the ray begins, how its direction is represented or how to obtain a point three units along it. We need a form that can be calculated. That form is small: an origin point, a direction vector and a scalar parameter. Nearly every later operation depends on using those three values without confusing their roles.
Points and vectors are commonly stored using the same three numbers, and this can tempt us to treat them as interchangeable. They are not. A point is a location. A vector is a displacement. Adding two locations has no necessary geometric meaning, while subtracting them produces the displacement from one to the other. JavaScript will not enforce this distinction for us, so our functions and names must make it visible.
What you should be able to account for
- Use vector addition, subtraction, scaling, length, normalisation, dot product and cross product.
- Evaluate
P(t) = O + tDfor known values and explain what changes astchanges. - State which operations have a geometric meaning when their inputs are points or directions.
Build only the operations we can explain
We shall store a three-component value as a JavaScript array. This is not the most elaborate vector class we could write. It is deliberately plain, because the arithmetic should remain visible while we are learning what the renderer asks of it.
function vec3(x, y, z) {
return [x, y, z];
}
function add(a, b) {
return vec3(a[0] + b[0], a[1] + b[1], a[2] + b[2]);
}
function subtract(a, b) {
return vec3(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
}
function scale(a, amount) {
return vec3(a[0] * amount, a[1] * amount, a[2] * amount);
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
The dot product produces a scalar. For unit vectors it measures directional agreement: 1 means the same direction, 0 means perpendicular and -1 means opposite. Later we shall use this one operation to test whether a ray approaches a plane, how directly a surface faces a light and whether a camera ray meets the front or back of a surface.
The vector length follows from the vector dotted with itself:
|v| = √(v · v)
function length(a) {
return Math.sqrt(dot(a, a));
}
function normalise(a) {
const magnitude = length(a);
if (magnitude === 0) {
throw new Error("A zero vector has no direction");
}
return scale(a, 1 / magnitude);
}
Normalising retains direction and changes length to one. A zero vector cannot be normalised because division by zero cannot create a meaningful direction. Silently returning another zero vector may keep a program running, but it turns a missing direction into a later and less obvious error. The laboratory uses a defensive zero result so the page remains interactive; production teaching code should choose and document a failure policy.
The ray equation
Let O be an origin point and D a direction vector. The position at parameter t is:
P(t) = O + tD
At t = 0, the position is the origin. Positive values move along the direction. Negative values lie behind the ray origin and are normally rejected by a camera visibility query. If D has unit length, t is also distance in the scene's coordinate units. This is convenient, but it follows from normalisation; it is not automatically true of every stored direction.
function pointOnRay(ray, t) {
return add(ray.origin, scale(ray.direction, t));
}
const ray = {
origin: vec3(0, 0, 0),
direction: normalise(vec3(2, 1, -4))
};
Follow a simple direction
Take O = (1, 2, 0) and the already normalised direction D = (0, 0, -1). At t = 0, P = (1, 2, 0). At t = 2.5, scale the direction to (0, 0, -2.5) and add the origin, giving (1, 2, -2.5). At t = -1, the point is (1, 2, 1), behind the chosen forward direction. The formula calculates all three; the query interval decides which are acceptable.
The laboratory uses the two-dimensional slice z = 0 and fixes the origin at O = (0, 0). This removes one coordinate without changing either calculation. The first panel normalises V to obtain D. The second substitutes that D and one selected value of t into the ray equation. No image or camera is involved yet.
Vector and ray laboratory
Turn a Vector into a Ray Direction
First turn a raw vector V into a unit direction D. Then use one value of t to calculate the position P(t) on a ray whose origin remains fixed.
Change this, then watch this: Change V to change the unit direction D. Change t to move P(t); the origin O remains fixed.
- Raw vector V
- Raw length |V|
- Unit direction D
- Position P(t)
D = V ÷ |V| = (3.000, 2.000) ÷ 3.606 = (0.832, 0.555)
P(2.000) = (0, 0) + 2.000 × D = (1.664, 1.109)
What this establishes:
Direction is not position
Translations should affect points and leave directions unchanged. If a camera moves three units to the right, its origin point changes. A direction described as forward does not become three units more right merely because the camera moved. Rotations affect both the camera's orientation vectors and the points attached to it, but through different geometric roles.
Our compact arrays cannot prevent an expression such as add(pointA, pointB). Names such as origin, direction, position and displacement therefore carry real responsibility. A later Vulkan implementation may use the same vec3 representation in a shader. The type still will not tell us whether the value is a point. Clear semantics do not become optional because the arithmetic is fast.
The cross product prepares a coordinate frame
The cross product takes two three-dimensional vectors and produces a perpendicular vector. Its operand order matters: reversing the inputs reverses the result. Lesson 3 will use it to construct the camera's right and corrected up directions from a desired forward direction.
function cross(a, b) {
return vec3(
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
);
}
const right = normalise(cross(forward, worldUp));
const up = cross(right, forward);
If forward is parallel to worldUp, their cross product is zero and no right direction can be obtained this way. That is not a mysterious camera failure. It is a direct consequence of asking two parallel directions to define a plane. A robust camera must reject that configuration or choose a different reference up vector.
Calculate before you draw
A ray begins at O = (2, -1, 4) and has unit direction D = (-1, 0, 0). Give P(0), P(3) and P(-2). Which result is normally excluded from a primary-ray hit search?
Reveal the trace
P(0) = (2, -1, 4). P(3) = (2, -1, 4) + (-3, 0, 0) = (-1, -1, 4). P(-2) = (2, -1, 4) + (2, 0, 0) = (4, -1, 4). The last point is behind the origin relative to D, so a primary visibility interval beginning at a small positive value excludes it.
Test the invariants
Write a small set of console checks for the vector functions. Verify that normalising (3, 4, 0) produces length one, that perpendicular unit axes have dot product zero and that cross((1,0,0), (0,1,0)) points along positive Z. Then reverse the cross-product operands and explain the sign change. A rendered picture is not a replacement for these numerical checks.
Keep this model for Vulkan
Shader languages provide vector types and built-in operations, which will remove much of this JavaScript. They do not remove the geometry. normalize, dot, cross and the ray equation will reappear almost unchanged. Learning the operations by hand means the later shader is a more compact expression of known reasoning rather than an unfamiliar collection of intrinsic functions.
We can now calculate a position along a ray. We still have only one manually chosen ray. Lesson 3 constructs a camera that gives every sample in the raster its own origin and direction.