Fast ray tri (with precomputation) November 9, 2002 by Charles Bloom, cbloom@cbloom.com Moller/Haines and others have good ray-tri intersection code for the case of "no precomputation", but sometimes you want to do precomputation. For example, if you have only a few tris and you want to shoot lots of rays at them, you should precompute some things on your triangle to speed up the test. Here's how. I actually want segment-tri intersection, that is, my ray has two end points. We want to write a routine like bool RayTriIntersect(const Triangle & tri,const Vec3 & from, const Vec3 & to, ); Compute the plane of the triangle. The plane is a normal and offset (4 floats). Now you can do your first tests on your segment : dFm = tri.plane.Distance(from); dTo = tri.plane.Distance(to); Where plane.Distance is just (plane.normal * vec - plane.offset) Now you need to check the signs of dFm and dTo to see if the segment passes through the plane. You have some options here depending on where you want to treat your triangle as one-sided or two-sided. I'll assume that you want a one-sided triangle, so only rays going "at" the triangle should count as hits, so : if ( dTo <= 0 ) { // segment doesn't reach back side of triangle return false; } if ( dFm > 0 ) { // segment starts already on the back side return false; } assert( dTo > 0 && dFm <= 0 ) Now we know that the plane is hit. We can now make the point on the plane (we don't actually want to do this, but we'll write it out) : float t = dFm / (dTo - dFm) onPlane = lerp(from,to,t); Now we want to check if this point is on the triangle. We do that using barycentric coordinates. To compute the barycentric coords, you can use scaled planes. You do : edge1 = tri.vert1 - tri.vert0; edge2 = tri.vert2 - tri.vert1; edgePerp1 = tri.normal CROSS edge1 edgePerp2 = tri.normal CROSS edge2 edgePlane1 = { edgePerp1, edgePerp1*tri.vert0 } edgePlane2 = { edgePerp2, edgePerp2*tri.vert1 } Now a plane is just a vector and a constant; we can scale this normal to change the distances we want, so we make baryPlane1 = edgePlane1 / (edgePlane1.Distance(tri.vert2)) baryPlane2 = edgePlane2 / (edgePlane2.Distance(tri.vert0)) The result are these two "baryPlanes". These give you the barycentric coords just by doing dots : u = baryPlane1.Distance(onPlane); v = baryPlane2.Distance(onPlane); These baryPlanes are pretty neat; they are the planes that lie along the triangle edges and normal; their vectors are scaled so that the opposite point on the triangle returns a distance of 1. Once we have these barycentric coordinates, we can check collision easily : if ( u < 0 || v < 0 || (u+v) > 1 ) { // no collision return false; } // collision ! And we also have the time of hit and the bary-coords, which is good info to have. Now lets put it all together and delay the divide : struct Triangle { Plane plane; Plane bary1; Plane bary2; }; bool RayTriIntersect(const Triangle & tri,const Vec3 & from, const Vec3 & to) { float dTo = tri.plane.Distance(to); if ( dTo <= 0 ) { // segment doesn't reach back side of triangle return false; } float dFm = tri.plane.Distance(from); if ( dFm > 0 ) { // segment starts already on the back side return false; } assert( dTo > 0 && dFm <= 0 ); float denom = dTo - dFm; assert( denom > 0 ); Vec3 temp = dTo * from + dFm * to; float uTimesDenom = (tri.bary1.vector DOT temp) - tri.bary1.offset * denom; if ( uTimesDenom < 0 || uTimesDenom > denom ) { // off triangle return false; } float vTimesDenom = (tri.bary2.vector DOT temp) - tri.bary2.offset * denom; if ( vTimesDenom < 0 || (uTimesDenom+vTimesDenom) > denom ) { // off triangle return false; } // it's on the triangle, compute hit info : float inv = 1 / denom; *pT = dFm * inv; *pU = uTimesDenom * inv; *pV = vTimesDenom * inv; return true; } On my machine (1 GHz P3), I measure about 165 clocks for Moller's optimized ray-tri #1 (late division). See : http://www.ce.chalmers.se/staff/tomasm/raytri/ The code here takes about 120 clocks. The exact numbers depend on how much my extra early-outs help. The 120 clock number is with no help from the early-outs ; in practice, the number is much lower, more like 50-60 clocks average in typical use. Most of all, I like the mental construct of the "barycentric planes". The size of the pre-computed "Triangle" structure is very small. It's only 12 floats, compared to 9 floats to store the triangle vertices. Thus, the memory-fetch cost of this routine is not significantly higher. Note that I don't use the triangle's actual vertices at all in the ray-intersection routine. Finally, note that in the real world there should be higher-level acceleration structures used for ray intersection of all kinds. ==================================================================================== == Addendum : some notes on Moller-Haines ray-triangle test : I've been thinking about the classic Moller-Haines ray-triangle test. The basic ray-tri test looks like this : bool intersect_triangle(const Vec3& orig, const Vec3& dir, const Vec3& vert0, const Vec3& vert1, const Vec3& vert2) { const Vec3 edge1 = v1 - v0; const Vec3 edge2 = v2 - v0; /* begin calculating determinant - also used to calculate U parameter */ // ^ = cross product Vec3 pvec = dir ^ edge2; /* if determinant is near zero, ray lies in plane of triangle */ const float det = fabsf( edge1 * pvec ); // = Vec3U::TripleProduct(dir,edge1,edge2); /* calculate distance from vert0 to ray origin */ const Vec3 tvec = orig - vert0; /* calculate U parameter and test bounds */ const float u = tvec * pvec; // = Vec3U::TripleProduct(dir,edge2,tvec); if ( u < 0 || u > det ) return false; /* prepare to test V parameter */ /* calculate V parameter and test bounds */ const float v = Vec3U::TripleProduct(dir,tvec,edge1); // = dir * (tvec ^ edge1) if ( v < 0 || u + v > det ) return false; return true; } It's interesting what's going on here geometrically. First let's back up and think about how you do a ray-triangle test. The obvious way is to take your ray, find the point where it hits the plane of the triangle, and make the point on that plane. Now you have to do a 2d point-in-triangle on that plane. You do this with Barycentric coordinates. They good way to do that is using areas. Let's say your verts are (v0,v1,v2), and your point on the plane is (P). You compute Barycentric coordinates like this : u = Area(v0,v1,P) / Area(v0,v1,v2) v = Area(v0,P,v2) / Area(v0,v1,v2) w = Area(P,v1,v2) / Area(v0,v1,v2) = 1 - u - v and the areas are signed and the winding matters. Now, computing these Area's in 3d is actually a little bit of a pain, because you would have to project down to 3d and such. In fact, the easier thing to do is to compute the *volume* of the parallelopiped defined by the triangle extruded along its normal direction. Let me switch to coordinates where v0 = 0 by doing : e1 = v1 - v0 e2 = v2 - v0 Q = P - v0 u = Area(0,e1,Q) / Area(0,e1,e2) v = Area(0,Q,e2) / Area(0,e1,e2) now if we have N = e1 ^ e2 , which is proportional to the triangle normal, we can do : u = Volume(N,e1,Q) / Volume(N,e1,e2) v = Volume(N,Q,e2) / Volume(N,e1,e2) note that the sign and magnitude of N actually don't matter since we divide through by them. Now the Volume of 3 vectors is just a "triple product" : Volume(a,b,c) = a * (b ^ c) = Epsilon_ijk * a_i b_j c_k which is the determinant of the matrix with a,b,c as columns. Now, you can notice that what direction we use for N actually doesn't matter, it just introduces a scaling factor, since the ratio of Volumes is all that matters. That is, we can shear our parallelopiped so it's not at right angles. u = Volume(D,e1,Q) / Volume(D,e1,e2) v = Volume(D,Q,e2) / Volume(D,e1,e2) This works for any D. In particular, we can use the original ray direction. The only time this breaks down is if D is in the plane of the triangle, but in that case you ray-tri is falling apart anyway. Next, note that if we add anything to Q in the direction of D, it doesn't affect the answer. That's because we cross product it out - Volume(D,e1,Q + tD) = Volume(e1,Q + tD,D) = e1 * (Q+tD) X D = e1 * Q X D = Volume(e1,Q,D) = Volume(D,e1,Q) That means we can use the original ray source point, we don't have to use the point Q that's on the triangle surface. This gives us our final equations : u = Volume(dir,e1,orig-v0) / Volume(dir,e1,e2) v = Volume(dir,orig-v0,e2) / Volume(dir,e1,e2) We can understand this geometrically as the volumes of the parallelopides from the triangle verts, in the direction of the ray normal, through the ray origin. If you look along the ray direction, you see the triangle in 2d; the ray origin must lie inside that triangle to have an intersection, which is exactly this test. This trivially leads us to the Moller-Haines tests : bool intersect() { det = Volume(dir,e1,e2); tvec = orig - v0; u = Volume(dir,e1,tvec); if ( u < 0 || u > det ) return false; v = Volume(dir,tvec,e2); if ( v < 0 || v > det ) return false; return true; }