Intersection between line and triangle in 3D

16,563

Solution 1

1) If you just want to know whether the line intersects the triangle (without needing the actual intersection point):

Let p1,p2,p3 denote your triangle

Pick two points q1,q2 on the line very far away in both directions.

Let SignedVolume(a,b,c,d) denote the signed volume of the tetrahedron a,b,c,d.

If SignedVolume(q1,p1,p2,p3) and SignedVolume(q2,p1,p2,p3) have different signs AND SignedVolume(q1,q2,p1,p2), SignedVolume(q1,q2,p2,p3) and SignedVolume(q1,q2,p3,p1) have the same sign, then there is an intersection.

SignedVolume(a,b,c,d) = (1.0/6.0)*dot(cross(b-a,c-a),d-a)

2) Now if you want the intersection, when the test in 1) passes

write the equation of the line in parametric form: p(t) = q1 + t*(q2-q1)

Write the equation of the plane: dot(p-p1,N) = 0 where

N = cross(p2-p1, p3-p1)

Inject p(t) into the equation of the plane: dot(q1 + t*(q2-q1) - p1, N) = 0

Expand: dot(q1-p1,N) + t dot(q2-q1,N) = 0

Deduce t = -dot(q1-p1,N)/dot(q2-q1,N)

The intersection point is q1 + t*(q2-q1)

3) A more efficient algorithm

We now study the algorithm in:

Möller and Trumbore, « Fast, Minimum Storage Ray-Triangle Intersection », Journal of Graphics Tools, vol. 2,‎ 1997, p. 21–28

(see also:)

https://en.wikipedia.org/wiki/M%C3%B6ller%E2%8%93Trumbore_intersection_algorithm

The algorithm is in the end simpler (less instructions than what we did in 1) and 2)), but sightly more complicated to understand. Let us derive it step by step.

Notation:

  • O = origin of the ray,

  • D = direction vector of the ray,

  • A,B,C = vertices of the triangle

An arbitrary point P on the ray can be written as P = O + tD

An arbitrary point P on the triangle can be written as P = A + uE1 + vE2 where E1 = B-A and E2 = C-A, u>=0, v>=0 and (u+v)<=1

Writing both expressions of P gives:

O + tD = A + uE1 + vE2 

or:

uE1 + vE2 -tD = O-A

in matrix form:

            [u]
 [E1|E2|-D] [v] = O-A
            [t]

(where [E1|E2|-D] is the 3x3 matrix with E1,E2,-D as its columns)

Using Cramer's formula for the solution of:

   [a11 a12 a13][x1]   [b1]
   [a12 a22 a23][x2] = [b2]
   [a31 a32 a33][x3]   [b3]

gives:

       |b1 a12 a13|   |a11 a12 a13|
  x1 = |b2 a22 a23| / |a21 a22 a23|
       |b3 a32 a33|   |a31 a32 a33|

       |a11 b1 a13|   |a11 a12 a13|
  x2 = |a21 b2 a23| / |a21 a22 a23|
       |a31 b3 a33|   |a31 a32 a33|

       |a11 a12 b1|   |a11 a12 a13|
  x3 = |a21 a22 b2| / |a21 a22 a23|
       |a31 a32 b3|   |a31 a32 a33|

Now we get:

  u = (O-A,E2,-D) / (E1,E2,-D)
  v = (E1,O-A,-D) / (E1,E2,-D)
  t = (E1,E2,O-A) / (E1,E2,-D)

where (A,B,C) denotes the determinant of the 3x3 matrix with A,B,C as its column vectors.

Now we use the following identities:

  (A,B,C) = dot(A,cross(B,C))  (develop the determinant w.r.t. first column)

  (B,A,C) = -(A,B,C)           (swapping two vectors changes the sign)

  (B,C,A) =  (A,B,C)           (circular permutation does not change the sign)

Now we get:

u = -(E2,O-A,D)  / (D,E1,E2)
v =  (E1,O-A,D)  / (D,E1,E2)
t = -(O-A,E1,E2) / (D,E1,E2)  

Using:

N=cross(E1,E2);

AO = O-A; 

DAO = cross(D,AO)

We obtain finally the following code (here in GLSL, easy to translate to other languages):

bool intersect_triangle(
    in Ray R, in vec3 A, in vec3 B, in vec3 C, out float t, 
    out float u, out float v, out vec3 N
) { 
   vec3 E1 = B-A;
   vec3 E2 = C-A;
         N = cross(E1,E2);
   float det = -dot(R.Dir, N);
   float invdet = 1.0/det;
   vec3 AO  = R.Origin - A;
   vec3 DAO = cross(AO, R.Dir);
   u =  dot(E2,DAO) * invdet;
   v = -dot(E1,DAO) * invdet;
   t =  dot(AO,N)  * invdet; 
   return (det >= 1e-6 && t >= 0.0 && u >= 0.0 && v >= 0.0 && (u+v) <= 1.0);
}
 

When the function returns true, the intersection point is given by R.Origin + t * R.Dir. The barycentric coordinates of the intersection in the triangle are u, v, 1-u-v (useful for Gouraud shading or texture mapping). The nice thing is that you get them for free !

Note that the code is branchless. It is used by some of my shaders on ShaderToy

Solution 2

@BrunoLevi: your algorithm does not seem to work, see the following python implementation:

def intersect_line_triangle(q1,q2,p1,p2,p3):
    def signed_tetra_volume(a,b,c,d):
        return np.sign(np.dot(np.cross(b-a,c-a),d-a)/6.0)

    s1 = signed_tetra_volume(q1,p1,p2,p3)
    s2 = signed_tetra_volume(q2,p1,p2,p3)

    if s1 != s2:
        s3 = signed_tetra_volume(q1,q2,p1,p2)
        s4 = signed_tetra_volume(q1,q2,p2,p3)
        s5 = signed_tetra_volume(q1,q2,p3,p1)
        if s3 == s4 and s4 == s5:
            n = np.cross(p2-p1,p3-p1)
            t = -np.dot(q1,n-p1) / np.dot(q1,q2-q1)
            return q1 + t * (q2-q1)
    return None

My test code is:

q0 = np.array([0.0,0.0,1.0])
q1 = np.array([0.0,0.0,-1.0])
p0 = np.array([-1.0,-1.0,0.0])
p1 = np.array([1.0,-1.0,0.0])
p2 = np.array([0.0,1.0,0.0])

print(intersect_line_triangle(q0,q1,p0,p1,p2))

gives:

[ 0.  0. -3.] 

instead of the expected

[ 0.  0. 0.]

looking at the line

t = np.dot(q1,n-p1) / np.dot(q1,q2-q1)

Subtracting p1 from the normal doesn't make sense to me, you want to project from q1 onto the plane of the triangle, so you need to project along the normal, with a distance that is proportional to the ratio of the distance from q1 to the plane and q1-q2 along the normal, right?

The following code fixes this:

n = np.cross(p2-p1,p3-p1)
t = np.dot(p1-q1,n) / np.dot(q2-q1,n)
return q1 + t * (q2-q1)

Solution 3

To find the intersection between a line and a triangle in 3D, follow this approach:

  • Compute the plane supporting the triangle,
  • Intersect the line with the plane supporting the triangle:

    • If there is no intersection, then there is no intersection with the triangle.
    • If there is an intersection, verify that the intersection point indeed lies in the triangle:

      • Each edge of the triangle together with the normal of the plane supporting the triangle determines a half-space bounding the inside of the triangle (the corresponding bounding plane can be derived from the normal and the edge vertices),
      • Verify that the intersection point lies on the inside of all the edge half-spaces.

Here is some sample code with detailed computations that should work:

// Compute the plane supporting the triangle (p1, p2, p3)
//     normal: n
//     offset: d
//
// A point P lies on the supporting plane iff n.dot(P) + d = 0
//
ofVec3f v21 = p2 - p1;
ofVec3f v31 = p3 - p1;

ofVec3f n = v21.getCrossed(v31);
float d = -n.dot(p1);

// A point P belongs to the line from P1 to P2 iff
//     P = P1 + t * (P2 - P1)
//
// Find the intersection point P(t) between the line and
// the plane supporting the triangle:
//     n.dot(P) + d = 0
//                  = n.dot(P1 + t (P2 - P1)) + d
//                  = n.dot(P1) + t n.dot(P2 - P1) + d
//
//     t = -(n.dot(P1) + d) / n.dot(P2 - P1)
//
ofVec3f P21 = P2 - P1;
float nDotP21 = n.dot(P21);

// Ignore line parallel to (or lying in) the plane
if (fabs(nDotP21) < Epsilon)
    return false;

float t = -(n.dot(P1) + d) / nDotP21;
ofVec3f P = P1 + t * P21;

// Plane bounding the inside half-space of edge (p1, p2): 
//     normal: n21 = n x (p2 - p1)
//     offset: d21 = -n21.dot(p1)
//
// A point P is in the inside half-space iff n21.dot(P) + d21 > 0
//

// Edge (p1, p2)
ofVec3f n21 = n.cross(v21);
float d21 = -n21.dot(p1);

if (n21.dot(P) + d21 <= 0)
    return false;

// Edge (p2, p3)
ofVec3f v32 = p3 - p2;
ofVec3f n32 = n.cross(v32);
float d32 = -n32.dot(p2);

if (n32.dot(P) + d32 <= 0)
    return false;

// Edge (p3, p1)
ofVec3f n13 = n.cross(-v31);
float d13 = -n13.dot(p3);

if (n13.dot(P) + d13 <= 0)
    return false;

return true;

Some comments on the code posted with the question:

  • Predefined operations of ofVec3f (.dot() and .cross() for geometric products, etc...) should be preferred when available (more readable, avoids implementation mistakes, etc...),
  • The code initially follows the approach above but then only checks that the intersection point is in the 3D axis-aligned bounding box of the line segment [P1, P2]. This combined with possible other errorscould explain why the results are incorrect.
  • One can verify that the intersection point is in the 3D axis-aligned bounding box of the (whole) triangle. While this is not enough to guarantee intersection, it can however be used to cull points clearly not intersecting and avoid further complex computations.
Share:
16,563
Kaito Kid
Author by

Kaito Kid

Updated on July 21, 2022

Comments

  • Kaito Kid
    Kaito Kid almost 2 years

    I have a line and a triangle somewhere in 3D space. In other words, I have 3 points ([x,y,z] each) for the triangle, and two points (also [x,y,z]) for the line.

    I need to figure out a way, hopefully using C++, to figure out if the line ever crosses the triangle. A line parallel to the triangle, and with more than one point in common, should be counted as "does not intersect".

    I already made some code, but it doesn't work, and I always get false even when a visual representation clearly shows an intersection.

    ofVec3f P1, P2;
    P1 = ray.s;
    P2 = ray.s + ray.t;
    
    ofVec3f p1, p2, p3;
    p1 = face.getVertex(0);
    p2 = face.getVertex(1);
    p3 = face.getVertex(2);
    
    ofVec3f v1 = p1 - p2;
    ofVec3f v2 = p3 - p2;
    
    float a, b, c, d;
    
    a = v1.y * v2.z - v1.z * v2.y;
    b = -(v1.x * v2.z - v1.z * v2.x);
    c = v1.x * v2.y - v1.y * v2.x;
    d = -(a * p1.x + b * p1.y + c * p1.z);
    
    ofVec3f O = P1;
    ofVec3f V = P2 - P1;
    
    float t;
    
    t = -(a * O.x + b * O.y + c * O.z + d) / (a * V.x + b * V.y + c * V.z);
    
    ofVec3f p = O + V * t;
    
    float xmin = std::min(P1.x, P2.x);
    float ymin = std::min(P1.y, P2.y);
    float zmin = std::min(P1.z, P2.z);
    
    float xmax = std::max(P1.x, P2.x);
    float ymax = std::max(P1.y, P2.y);
    float zmax = std::max(P1.z, P2.z);
    
    
    if (inside(p, xmin, xmax, ymin, ymax, zmin, zmax)) {
        *result = p.length();
        return true;
    }
    return false;
    

    And here is the definition of inside()

    bool primitive3d::inside(ofVec3f p, float xmin, float xmax, float ymin, float ymax, float zmin, float zmax) const {
        if (p.x >= xmin && p.x <= xmax && p.y >= ymin && p.y <= ymax && p.z >= zmin && p.z <= zmax)
            return true;
    
        return false;
    }
    
  • Dahn
    Dahn about 6 years
    By "If SignedVolume(q1,p1,p2,p3) differs from SignedVolume(q2,p1,p2,p3)" you mean if their signs differ?
  • Garrett
    Garrett about 5 years
    Shouldn't the equation of the plane be: dot(p,N) - dot(p1,N) = 0?
  • Jesse Pepper
    Jesse Pepper over 4 years
    Isn't the 1/6 * at the start of SignedVolume a redundant multiply?
  • BrunoLevy
    BrunoLevy over 4 years
    Yes, you are right. I just kept it fo have the SigmedVolume explanation of what's going on, but it can be removed in the end, does not change the sign.
  • TiberiumFusion
    TiberiumFusion over 4 years
    +1 to this answer. The corrected maths in this answer ought to be incorporated into the accepted answer.
  • BrunoLevy
    BrunoLevy over 4 years
    Thank you very much for noticing, I've fixed the other answer.
  • BrunoLevy
    BrunoLevy over 4 years
    Fixed the computation of the intersection point that was completely wrong (thank you @user3250705 for noticing)
  • Chet
    Chet almost 4 years
    Would be nice to see what the intersection point is in terms of u, v, and t more clearly for the final algorithm.
  • Chet
    Chet almost 4 years
    intersection_point = (R.Origin + t * R.Direction). Furthermore, if you're using this for an intersection test for collision detection and not ray tracing (the difference being that the ray is not infinite in length), you can simply add t <= 1 to the list of conditions. You'll probably want to rename things a bit since at that point R.dir should be the difference vector between q1 and q2 and not a unit-norm direction.
  • Aldo
    Aldo over 3 years
    Very late to the game, but not yet mentioned in other comments: shouldn't the det >= 1e-6 condition in the return statement be fabs(det) >= 1e-6 ? I suppose it is a check on the dependency of the matrix columns, so the sign should be irrelevant. I checked your algorithm (in C) and this was the only thing keeping me from passing tests.
  • Bryce Wagner
    Bryce Wagner over 2 years
    If you want the segment version instead of the ray version, just add a check at the end that t is less than the length of the segment and convert the segment to a ray where Dir is a unit vector. And if you want the line version, get rid of the t >= 0 check altogether.