In chapter 07, projection found the part of a displacement along a line. We can use that same operation to separate a ray into two perpendicular parts. A mirror leaves one part alone and reverses the other.
Which way does I point?
Put the contact point at the origin. Our mirror is the horizontal line through it. The incident vector I describes the direction of travel toward the contact:
vec2 incident = vec2(0.6, -0.8);
vec2 normal = vec2(0.0, 1.0);
The ray travels right and down. We draw its tail above and left of the contact, then point the arrow toward the origin. Drawing an arrow from the origin toward incident would show the same vector translated to a different starting point, but it would look like a ray continuing through the mirror.
The surface normal is a unit vector perpendicular to the mirror. Here it points up. Both vectors use the same two-dimensional coordinate space. The normal has length one; the incident vector does not have to. Its length might represent the size of a step or a speed that we want to preserve.
Split the direction
Project the incident vector onto the unit normal. The dot product gives one signed number. Multiplying by the normal turns it into the perpendicular part of the vector:
float towardNormal = dot(incident, normal); // -0.8
vec2 normalPart = towardNormal * normal; // (0.0, -0.8)
vec2 tangentPart = incident - normalPart; // (0.6, 0.0)
The negative dot product matters. Our normal points up, but the ray travels down toward the mirror. Its normal component therefore points opposite to the normal.
Subtracting that component leaves the part along the mirror. This is the tangent component. Add the two parts and we recover the original incident vector. For a tilted mirror, the components need not match the X and Y axes, but their geometric roles stay the same.
Why is the remainder tangent? Dot it with the unit normal: dot(I, N) - dot(I, N) * dot(N, N). Since dot(N, N) = 1, the result is zero. The two components are perpendicular.
Reverse the normal part
The reflected ray keeps traveling along the surface in the same direction. Its motion through the surface must change sign:
vec2 reflected = tangentPart - normalPart; // (0.6, 0.8)
We already have tangentPart = incident - normalPart. Substitute it into the expression:
vec2 reflected = incident
- 2.0 * dot(incident, normal) * normal;
The factor of two removes the original normal component and adds its opposite. Subtracting it only once would leave the tangent component, a ray sliding along the mirror.
GLSL provides the same operation:
vec2 sameReflected = reflect(incident, normal);
This function uses the supplied vectors directly. It does not normalize either argument for us. With a unit normal, the manual formula and reflect describe the same result, up to floating-point rounding.
Keep the length and angle
Write the incident vector as the sum of its tangent and normal components. The reflected vector contains the same tangent component and the negative normal component. Because those components are perpendicular, the squared length is the sum of their squared lengths. Changing one sign does not change that sum.
Our example has length sqrt(0.6 * 0.6 + 0.8 * 0.8) = 1 before and after reflection. Scaling the incident vector scales the reflected vector by the same amount. We do not need to normalize a velocity just to reflect it.
The normal component has the same magnitude on both sides, so the incoming and outgoing paths make equal angles with the normal line. For the example, both angles are about 36.87 degrees. Compare the incoming path from the contact back toward its source, rather than measuring from the forward incident arrow to the upward normal.
The lab reports the acute angle to the normal line. It accepts a ray arriving from either face of the mirror. Reversing the normal selects the other face but does not change the reflected vector: both dot(I, N) and N change sign, so their product stays the same.
At normal incidence, the whole vector reverses, so the incoming and outgoing paths overlap. At exact tangency, the dot product is zero and the direction stays unchanged. That tangent case is the algebraic limit of a grazing ray; the demo does not claim that a separate collision occurred.
Use a unit normal
Suppose we replace N = (0,1) with (0,0.5) without normalizing. The direction looks right, but the projection is wrong:
dot(incident, vec2(0.0, 0.5)) // -0.4
// Wrong reflected result: (0.6, -0.4)
// Wrong length: sqrt(0.52), about 0.72111
The normal appears twice in the formula. Halving it reduces the correction to a quarter of its proper size. The resulting ray still points into the other side of the mirror, and its length is no longer preserved.
Normalize a valid normal before using it. As in chapter 04, avoid dividing a zero or tiny vector by its length:
float normalLength = length(uNormal);
vec2 normal = normalLength > 0.0001 ? uNormal / normalLength : vec2(0.0, 1.0);
The fallback chooses an upward normal, hence a horizontal mirror. This is an explicit choice for our small scene. A zero normal cannot describe a surface orientation. A zero incident vector is different: its reflected vector is also zero, but it has no direction and no angle to report.