Möller–Trumbore算法

Möller–Trumbore 算法( Möller–Trumbore ray-triangle intersection algorithm )用于三维三维中射线和三角形的快速求交。Möller–Trumbore 算法计算量小,适用于实时渲染。

算法引入

判断空间中射线是否穿过一个三角形,常规的方法大概是这样:

空间平面上任意一点 p 的表达式:

\[(P - P') \cdot \vec{n} = 0\]

空间射线上任意一点的表达式:

\[r(t) = O + t \vec{d}\]

联立可解得:

\[t = \frac{(P'-O)\cdot\vec{n}}{\vec{d}\cdot\vec{n}}\]

带入射线表达式后解出交点坐标,然后检查点与三角形每个顶点的向量叉乘的符号,如果符号都相同即在三角形内部。

这么做是最符合正常人已有的思维模式的,但效率不够高。因此有Möller–Trumbore 算法。

算法推导

首先三角形平面上任意一点可以写成重心坐标的形式:

\(P = b_0 P_0 + b_1 P_1 + b_2 P_2 = (1 - b_1 - b_2) P_0 + b_1 P_1 + b_2 P_2\) ​

把空间射线上任意一点的表达式带入得:

\[O + t \vec{d} = ( 1 - b_1 - b_2) P_0 + b_1 P_1 + b_2 P_2\]

单纯看这个式子,从直观上可以说我们一定有解决的办法。因为一共有 t、b1 和 b2 三个未知量,而三维空间让我们可以有三个方程。

我们将上面的式子整理一下:

\[O - P_0 = - t\vec{d} + b_1(P_1-P_0) + b_2(P_2-P_0)\]

将其中的已知量进行替换:

\[S = O - P\] \[E_1 = P_1 - P_0\] \[E_2 = P_2 - P_0\]

得到:

\[S = b_1E_1 + b_2E_2 - t\vec{d} =\begin{bmatrix} -\vec{d} & E_1 & E_2 \end{bmatrix} \begin{bmatrix} t \\ b_1 \\ b_2 \end{bmatrix}\]

根据 Cramer 法则和向量混合积的性质:

\[t = \frac{\det\begin{bmatrix} S & E_1 & E_2 \end{bmatrix}}{\det\begin{bmatrix} -\vec{d} & E_1 & E_2 \end{bmatrix}} = \frac{(S\times E_1)\cdot E_2}{ ( \vec{d}\times E_2)\cdot E_1 }\]

同理分别解出 b1 和 b2 :

\[b_1 = \frac{(\vec{d}\times E_2)\cdot S}{ ( \vec{d}\times E_2)\cdot E_1 }\] \[b_2= \frac{(S\times E_1)\cdot \vec{d}}{ ( \vec{d}\times E_2)\cdot E_1 }\]

同样进行已知量的替换:

\[S_1 = d\times E_2\] \[S_2 = S\times E_1\]

最后得到:

\[\begin{bmatrix} t \\ b_1 \\ b_2 \end{bmatrix} = \frac{1}{S_1\cdot E_1} \begin{bmatrix} S_2\cdot E_2 \\ S_1 \cdot S \\ S_2 \cdot \vec{d} \end{bmatrix}\]

其中:

\[S = O - P\] \[E_1 = P_1 - P_0\] \[E_2 = P_2 - P_0\] \[S_1 = d\times E_2\] \[S_2 = S\times E_1\]

代码实现

bool ray_triangle_intersection(const Point3f& P0, const Point3& P1, const Point3& P2, 
                               const Vector3f& orig, const Vector3f& dir, 
                               float& t, float& b1, float& b2) {
    auto S = orig - P0;
    auto E1 = P1 - P0;
    auto E2 = P2 - P0;
    auto S1 = cross(dir, E2);
    auto S2 = cross(S, E1);

    auto divisor = dot(S1, E1);
    if (divisor == 0) 
        return false;
    t = dot(S2, E2) / divisor;
    b1 = dot(S1, S) / divisor;
    b2 = dot(S2, dir) / divisor;

    if (t < 0 || b1 < 0 || b2 < 0 || b1 + b2 > 1) 
        return false;
        
    return true;
}