Hello! Welcome back to the next stage of our ray tracer project.
In the last module, you mastered the physics of light transport through dielectric materials, successfully creating realistic glass spheres that reflect and refract their environment. This was a huge step in physical accuracy. However, the worlds you build in Houdini and see in production renderers like Arnold and Karma are rarely made of simple spheres. They are composed of vast, complex triangle meshes.
Today, we make the leap from geometric primitives to arbitrary models. This lesson will focus on the single most important algorithm for this task. Our goal is to implement the Möller-Trumbore algorithm for efficient ray-triangle intersection. This method is a classic in computer graphics—it's fast, memory-efficient, and elegant. Mastering it is a fundamental step toward rendering the complex 3D assets you're familiar with from your work in VFX.
1. The Core Idea: Where the Ray Meets the Triangle
To start, let's define the problem. A ray is defined by its origin and direction , giving any point along it as:
A triangle is defined by three vertices, let's call them , , and . Any point inside this triangle can be described using barycentric coordinates and :
Here, and are weights. For a point to be inside the triangle, we must have , , and .
The genius of the Möller-Trumbore algorithm is to find a point where these two representations are equal. By setting , we can solve for the three unknowns: the ray distance and the barycentric coordinates and .

The first step is to rearrange this equality into a standard system of linear equations. This involves some vector algebra that your linear algebra background will make very familiar.
Ray-Tracing: Rendering a Triangle
The Scratchapixel website offers one of the clearest derivations of the Möller-Trumbore algorithm. This first section walks through the algebraic manipulation that transforms our geometric problem into a solvable system of equations.
Read the section titled 'Möller-Trumbore algorithm'. Focus on how setting the ray equation equal to the barycentric equation leads to the matrix form shown in 'equation 4'. Don't worry about the uv-space explanation for now; focus on the algebraic steps.
After the derivation, you should arrive at a matrix equation that looks like this:
Let's denote the triangle edges as and , and the vector from the ray origin to the first vertex as . The system becomes:
This is a standard 3x3 system of linear equations, which is exactly what we need to solve.
2. Solving the System with Cramer's Rule
To solve for and , the algorithm uses Cramer's Rule, which expresses the solution for each variable as a ratio of determinants. A key insight for performance is that the determinant of a 3x3 matrix whose columns are vectors can be calculated using the scalar triple product.
For a matrix with column vectors , the determinant is:
The video below provides an excellent review of the mathematical machinery, including Cramer's rule and the scalar triple product, before applying them to our problem.
Möller Trumbore Ray Triangle Intersection Explained
This video from enigma tutorials provides a concise visual refresher on the linear algebra concepts that power the Möller-Trumbore algorithm. It's a great way to see the concepts in action.
Watch the segment from 00:46 to 04:05. This covers vectors, barycentric coordinates, cross/dot products, the scalar triple product, determinants, and Cramer's rule. Since you have a strong math foundation, this should be a quick and helpful review.
With that review, let's see how these tools are applied to solve our specific system of equations.
Ray-Tracing: Rendering a Triangle
Now, let's return to Scratchapixel for the formal application of Cramer's rule. This section shows how the determinants are calculated using the scalar triple product, leading to the final formulas for t, u, and v.
Read the section 'Cramer's Rule'. Follow how the rule is used to set up the ratios of determinants and how the scalar triple product identities are used to simplify these into the final expressions for t, u, and v.
The final result gives us explicit formulas for and using only vector operations (cross products and dot products) on known quantities: the ray and the triangle vertices.
3. From Math to Code: Implementation
Now we can translate the mathematical solution into a C++ function. The implementation is essentially a series of calculations and checks.
Here is the logic in pseudocode:
- Compute triangle edge vectors and .
- Compute the vector as . The determinant is then .
- If the determinant is close to zero, the ray is parallel to the triangle plane. No intersection.
- Compute . If or , the hit point is outside the triangle. No intersection.
- Compute . If or , the hit point is outside the triangle. No intersection.
- If all tests pass, an intersection exists! Compute .
- Finally, check if falls within the ray's valid distance range (e.g.,
t > t_minandt < t_max). If it does, we have a valid hit.
Let's watch a detailed walkthrough of how this logic translates into code.
Möller Trumbore Ray Triangle Intersection Explained
The same video from enigma tutorials provides an excellent line-by-line explanation of a C-style implementation. This will clarify how the mathematical formulas map directly to variables and operations in code.
Watch the segment from 05:19 to 12:58. The narrator breaks down a two-sided intersection function, explaining each variable, the calculation of the determinant, the 'u' and 'v' checks, and the final calculation of 't'.
Here's a C++ implementation skeleton based on the Scratchapixel and Scotty3D resources. This is what you will be integrating into your triangle::hit method, which will override the virtual hit method in your hittable abstract base class.
// Within your triangle class, which inherits from hittable
bool triangle::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
// Vertices of the triangle are p0, p1, p2
vec3 e1 = p1 - p0;
vec3 e2 = p2 - p0;
vec3 pvec = cross(r.direction(), e2);
double det = dot(e1, pvec);
// If the determinant is near zero, the ray lies in the plane of the triangle.
// We can use an epsilon value for this check.
const double epsilon = 1e-8;
if (fabs(det) < epsilon)
return false;
double inv_det = 1.0 / det;
vec3 tvec = r.origin() - p0;
double u = dot(tvec, pvec) * inv_det;
if (u < 0.0 || u > 1.0)
return false;
vec3 qvec = cross(tvec, e1);
double v = dot(r.direction(), qvec) * inv_det;
if (v < 0.0 || u + v > 1.0)
return false;
// We have a hit! Calculate t and check if it's within the ray's valid range.
double t = dot(e2, qvec) * inv_det;
if (t > t_min && t < t_max) {
// Yes, it's a valid hit. Populate the hit_record.
rec.t = t;
rec.p = r.at(t);
// We'll compute the normal in the next lesson. For now, a placeholder is fine.
vec3 outward_normal = unit_vector(cross(e1, e2));
rec.set_face_normal(r, outward_normal);
// We will also use u,v later for texture coordinates and smooth shading.
return true;
}
// The intersection is outside the valid t-range.
return false;
}
Notice the check if (t > t_min && t < t_max). This is crucial. In your hittable_list, you need to find the closest intersection. As you test objects, you will update t_max to be the t of the closest hit found so far. This ensures that any subsequent hits further away are ignored, which is a key optimization. This practice is highlighted in the Scotty3D resource (LINK).
Test your understanding!
In the algorithm, we check if det is near zero. What is the geometric meaning of this condition? Why does it mean the ray cannot have a single, clean intersection with the triangle?
Show answer
A determinant of zero means the three vectors used to form the matrix are linearly dependent. In our case, the vectors are the ray direction and the two triangle edges and . If they are linearly dependent, it means they all lie on the same plane.
Geometrically, this means the ray is parallel to the plane of the triangle. The ray will either miss the triangle entirely or, in a rare case, lie within the plane of thetriangle, potentially intersecting with an edge. The Möller-Trumbore algorithm treats these cases as a "miss" to avoid division by zero and handle the most common intersection case cleanly.
Conclusion
Congratulations! You have just dissected one of the most fundamental algorithms in ray tracing. You are now equipped to render far more than just spheres.
Key Takeaways:
- The Möller-Trumbore algorithm elegantly solves for a ray-triangle intersection by finding where the ray's parametric equation and the triangle's barycentric coordinate equation are equal.
- It formulates the problem as a 3x3 system of linear equations, which is efficiently solved using Cramer's Rule and the scalar triple product.
- The implementation is a sequence of fast vector operations and early-out checks (
det,u,v) that make the algorithm highly performant for the common case where rays miss objects. - The output includes not just the intersection distance
t, but also the barycentric coordinatesuandv, which will be invaluable.
Next Steps
We've successfully determined if and where a ray hits a triangle. But what about the appearance? Right now, we can only assign a flat normal to the whole triangle, which would make a mesh look faceted.
In the next lesson, we will leverage the u and v coordinates we just calculated to compute barycentric coordinates of the intersection point. This will allow us to interpolate data—specifically vertex normals—across the triangle's surface, enabling the smooth shading you see on virtually all modern 3D models.