前言
射线与三角形的求交是一道经典面试题,它是模型选择的必备基础。
这道题要是说的话,并不难,但要往深了说,也不太容易。
而你若想脱颖而出,那就得往深了说。
我接下来会整体的说一下这个解题的思路和方法,有些公式我不会说太细,大家哪里不懂可以点击后面的链接学习。
前期准备
- 点积:www.bilibili.com/video/BV13s…
- 叉乘:www.bilibili.com/video/BV1vs…
- 直线和平面的交点公式:wuli.wiki/online/LPin…
- 矩阵行列式:www.bilibili.com/video/BV1Qs…
- 克莱姆法则:www.bilibili.com/video/BV1Pb…
两种解法
射线与三角形求交的常见解法有两种:
- 先求交,后过滤:先求射线与三角形所在平面的交点,然后判断此交点是否在三角形中。
- 先过滤,后求交:逐步过滤射线与三角形相交的条件,最后推导出交点。
第一种方法比较简单,很容易推导和理解,但计算效率会比较低,需要具备的知识是叉乘、点积、射线与平面的求交公式。
第二种方法相对难一些,需要具备扎实的线性变换基础,计算效率会高于前者,需要具备的知识是叉乘、点积、矩阵行列式、克莱姆法则(Cramer’s rule)。
咱们依次说一下这两种方法。
解法一
我们用”先求交,后过滤”的方法求射线与三角形交点。
已知:
- 三角形ABC
- 射线,原点为O,方向为d
求:射线与三角形的交点P
解:
1.通过AB和AC的叉乘,求出三角形ABC所在平面的垂线n。
若n等于0,则三角形的三点共线,甚至共点,需要过滤掉。
注:这里的n 不需要归一化。
2.通过射线与平面的求交公式求出交点。
根据射线与平面的求交公式可得:
- t:射线上的时间或距离标量
- O:射线原点
- A:平面上一点
射线的方程是:
- r(t) :在射线上到射线原点的距离为t的点位。
将之前求出的t值代入r(t)便可求出交点,设此交点为P。
3.判断点P是否在三角形ABC中。
在右手坐标系中,若AP^AC,BP^BA,CP^CB分别点积AB^AC都大于0,则点P在三角形ABC中,此时的点P就是射线与三角形的交点,如下图所示:
否则,点P在三角形ABC之外,如下图所示:
解法二
我们用”先过滤,后求交”的方法求射线与三角形交点。
以具象化的方式把问题和问题的解法画出来,要比数字计算更容易理解。所以我接下来会把这个问题的解法画出来。
这个解法的核心是:基于射线的方向和三角形的两条边把射线的源点投影到一个新的空间中。
已知:
- 三角形ABC
- 射线,原点为O,方向为d
求:射线与三角形的交点P
解:
根据已知条件,我们可以得到一个线性变换矩阵[-d,AB,AC],矩阵也可以理解为坐标系。
在这个矩阵中,A是原点,-d是射线的反方向,AB,AC是三角形的两条边。
向量AO在坐标系[-d,AB,AC] 中的本地坐标位是(t,u,v)。
由上面的条件可知,(t,u,v) 就是向量AO在[-d,AB,AC] 中的本地坐标位。
所以(t,u,v) 就是[-d,AB,AC]的逆矩阵乘以AO。
因为-d是一个单位向量,所以t值就是世界坐标系中射线与三角形ABC相交的距离,由此距离可以得到交点P:
现在我们已经用一个很快的方式把射线与三角形的交点P给算出来了,那说好的过滤呢?
过滤就是要在刚才的计算过程中进行逐步拦截,判断射线有没有可能和三角形存在交点,若不存在,就不用再往后走了。
一边过滤一边求交
1.先判断矩阵[-d,AB,AC]的有效性。
[-d,AB,AC]的有效性可以通过其行列式来判断。
三维矩阵的行列式可以理解为由以矩阵基向量为临边的平行六面体的有向体积。
[-d,AB,AC] 的行列式的计算方式如下:
- det 是determinant 的缩写,即行列式
- AB叉乘AC的结果是垂直于AB、AC的向量,此向量的长度是以AB、AC为临边的平行四边形的面积
- -d点积上面的向量,就是把-d投影到AB和AC的垂线上,从而得到平行六面体的高,然后乘以上面的向量的向量的长度,也就是平行四边形的面积,从而就得到了以-d,AB,AC为临边的平行六面体的有向体积,效果如下:
det([-d,AB,AC]) 的值有三种情况:
- 为正时,射线从三角形所在平面的正面穿过;
- 为负时,射线从三角形所在平面的背面穿过,若三角形背面不可选,则无交点;
- 为零时,矩阵[-d,AB,AC]发生空间降维,射线可能与三角形平行或者出现零向量,射线与三角形所在的平面没有交点或者有无数个交点。此情况一般不考虑,默认无交点。
2.把[-d,AB,AC]的逆矩阵乘以AO的过程分解、分别计算u,v,t,并过滤。
这时候就需要用到克莱姆法则,其规则如下:
上面的结构和咱们之前求(t,u,v) 时的结构是一样的:
顺便回顾一下我们在解法一里说过的,射线与平面的求交公式:
大家有没有觉得这两种解法里的t 有点相似?对,它们不仅相似,而且一样。
对于为什么用克莱姆法则就能算出t,u,v 来,我会在最后简单解释一下。
当然大家也可以看我在前期准备里提供的链接。
接下来咱们先往后走,说一下过滤条件。
当t 满足以下条件时不会有交点:
- t<0
t 是从射线的原点O 向着射线的方向d 推进的距离,当此值小于0时,是向射线反方向推进的,这肯定是不对的。
当u和v 满足以下任一条件都不会有交点:
- u<0
- v<0
- u+v>1
(t,u,v)可以理解为在[-d,AB,AC] 中的本地坐标。
当u或v小于0 时,交点会跑到三角形外面,这个比较好理解,因为点A就是源点,[-d,AB,AC] 中的基向量AB、AC 是三角形的边。
那u+v≤1 是什么概念呢?
这就是一个简单的向量加法。
想象你在直角边为1的等腰直角三角形内跑步,三角形中的任一点都可以理解为你先沿着AB轴跑了一段,然后又沿着AC轴跑了一段,你能跑出的最远距离只能是直角边的长度
3.把t 代入射线方程求出交点P。
代码实现
这是我用three.js写的一个计算过程。
1 | php复制代码/* 三角形ABC */ |
在three.js的Ray对象里有一个intersectTriangle() 方法。
这个方法略有冗余,比如其中的sign 代表了行列式的取向,但这是不需要的,因为我们可以在两个行列式相除的时候,得到行列式取向负负得正的结果。
1 | kotlin复制代码intersectTriangle( a, b, c, backfaceCulling, target ) { |
扩展-克莱姆法则
当我们说向量p 的x 位置是2的时候,我们可以这么理解这个2 的几何概念:
- x 轴上的刻度
- 用来缩放基向量x 的标量
- 以向量p、基向量y 和基向量z为临边的平行六面体的有向体积,即矩阵[p,y,z] 的行列式det([p,y,z] )
最后一种理解就是理解克莱姆法则的关键。
y叉乘z的结果是一条长度为1的垂直于y和z的向量,而p与这个向量的叉乘,就是p在这个向量上的正射影乘以这个向量的长度。
根据当前的情况可知,y叉乘z的结果就是基向量z,z的长度是1。
p 在z 上的正射影就是2。
所以,det([p,y,z] ) 的值就是2*1=2
大家理解完了这个原理后,咱们再做个假设。
假设这个向量p 是通过一个矩阵M乘以向量a(ax,ay,az) 得到的。
矩阵M 的基向量x是原来的3倍,其余的基向量不变:
那a 应该在哪里?
理解矩阵变换关系的同学肯定可以看出,这是一个3*ax=2,ax=2/3 的问题。
因为矩阵M中其它的基向量没变,所以向量a的位置是(2/3,0,0)。
其矩阵关系是这样的:
可是如果我只想知道ax 的值,不想知道其它值,应该怎么办呢?
那我们就得把这个矩阵变换的过程分解开来。
因为我们当前的变换都是线性变换,所以矩阵M的张成空间缩放了多少,就是ax 缩放了多少,即:
det([p,y,z])代表了点P在ax所在的轴向上位置,也就是ax 被矩阵M缩放后的位置。
我基于各个原理,继续往后推。
当矩阵M的其它基向量方式改变的时候,[p,y,z]中的基向量也得做相应改变。
所以,当矩阵M是这样的时候:
那ax 就应该这么算:
其结果还是2/3,这是因为我们没有改变向量p和3x,而矩阵中的4y和5z代表的只是以其为临边的平行四边形的面积,上下一除,就被约掉了,我们就需要它被约掉。
向量a中的其它分量也可以按照同样的原来计算。
注:0的出现是由于向量p与其它的两个向量共面导致的。
这就是克莱姆法则。
总结
在计算机图形学里,数学即代码。
坚实的数学基础,会让你写出的代码简洁优雅。
本文转载自: 掘金