这是一个三维空间中的平面问题(三角形确定一个平面),假设三角形的三个顶点为A(xa, ya, za)、B(xb, yb, zb)、C(xc, yc, zc),另外一个顶点为P(xp, yp, zp)。
问题:判断顶点 P 是否位于ABC组成的三角形上(内部和边界)。
方法1:效率较低的方法。
(1) 利用面积判断。如果顶点落在三角形上,那么顶点P分别和ABC三点连接后组成的三个小三角形的面积之和一定等于三角形ABC的面积,否则P位于三角形ABC之外。三角形面积的求法有海伦公式,叉积法等。
(2) 利用角度判断。连接顶点P和三角形的三个顶点ABC,每两条边的夹角之和如果等于 2*PI,则 P 位于三角形上,否则位于三角形外。两个向量之间的夹角可以利用向量的点积来求,不过要求反三角函数。
方法2:同侧检测:判定顶点是否位于三角形三条边的同侧。
如果顶点P位于三角形的内部,那么按顺时针或者逆时针将三角形的三条边形成的向量首尾相接(AB, BC, CA),顶点P一定是位于三个向量的同侧。如果顶点相对于三向量的位置发生了改变,比如逆时针时由左侧变为了右侧,则顶点一定位于三角形的外部。
如何判定顶点P位于向量的哪侧呢?我们知道向量的叉积是一个新的向量,具有方向。所以,通过向量叉积即可确定一个顶点位于一个向量的哪一侧。
那么又如何确定顺时针还是逆时针呢?其实,不需考虑顺时针还是逆时针。只要顶点 P 与三角形除却判断的向量(比如AB)外余下的那个顶点(比如C)位于同一侧,就可以判定P的相对位置。
接下来就是如何确定两点位于一个向量的同侧?因为向量的点积可以确定两个向量夹角的大小,当两个向量的点积大于0时,夹角小于 PI/2,可认为同向,否则两向量反向。于是,确定两个顶点P、C是否位于线段AB同侧的方法为:
构造三个向量:v1 = B - A,v2 = P - A,v3 = C - A;
判断P、C相对于AB的方向,做叉积:vp = v1 x v2, vc = v3 x v1;
判断vp、vc是否同向,做点积:d = vp ・ vc;
如果 d >= 0,说明叉积向量同向,进而推出P、C位于AB的同侧。
最后,如果针对三角形的三条边,P都和另外的一个顶点同向的话,那么P一定位于三角形上。
所以整个算法的流程如下:
function SameSide(p1,p2, a,b)
cp1 = CrossProduct(b-a, p1-a)
cp2 = CrossProduct(b-a, p2-a)
if DotProduct(cp1, cp2) >= 0 then return true
else return false
function PointInTriangle(p, a,b,c)
if SameSide(p,a, b,c) and SameSide(p,b, a,c) and SameSide(p,c, a,b) then
return true
else
return false
算法没有开根号和反三角函数的求解,所以效率相对较高。
方法3:重心法(barycentric technique)
在三角形确定的平面上的任意一点P,都可以使用一个重心坐标的形式来表示P的坐标:
P = uA + vB + wC,u + v + w = 1;
消去w:
P - C = u(A - C) + v(B - C)
上式可以理解为,平面上的任意一个向量都能表示成两个不共线的向量的线性形式。如果P位于三角形上,那么u、v需要满足:
0 <= u, v <= 1 && u + v <= 1
为简化书写,我们做以下标记:
v2 = P - C, v0 = A - C, v1 = B - C
进而
v2 = u*v0 + v*v1
接下来就是求得u、v。因为v0、v1和v2是已知向量,可以直接利用坐标来求解。但是三维空间中,可以得到三个(xyz)关于u、v的方程,而我们只需要两个方程即可,如何选择需要进一步的做些判断,如果选择方程组不合理可能无法解出u、v。
这里有个避免上述选择的做法,就是利用向量的点积,将向量转化为实数,方程两边同时点积一个向量:
(v2) ・ v0 = (u * v0 + v * v1) ・ v0
(v2) ・ v1 = (u * v0 + v * v1) ・ v1
进而
v2 ・ v0 = u * (v0 ・ v0) + v * (v1 ・ v0)
v2 ・ v1 = u * (v0 ・ v1) + v * (v1 ・ v1)
利用线性方程组的求解方法可以得到u、v:
u = ((v1・v1)(v2・v0)-(v1・v0)(v2・v1)) / ((v0・v0)(v1・v1) - (v0・v1)(v1・v0))
v = ((v0・v0)(v2・v1)-(v0・v1)(v2・v0)) / ((v0・v0)(v1・v1) - (v0・v1)(v1・v0))
此处,看到出现了除法,不能保证除数不等于0。可以分析什么情况下除数为0,预先判断即可。
(v0・v0)(v1・v1) - (v0・v1)(v1・v0) = 0
上式中如果 v0 或 v1 等于 0,则三角形便会退化成线段或者点的情况,这变成了点位于线段上的问题。
当v0、v1都不为0时,由内积公式,上式转变为:
(v0・v1)(v0・v1) / ((v0・v0)(v1・v1))
= |v0|^2 * |v1|^2 * (cos<v0,v1> )^2 / (|v0|^2 * |v1|^2)
= (cos<v0,v1>)^2
= 1
进而可以知道:
<v0, v1> = 0 或者 <v0, v1> = PI
v0和v1的夹角无论是哪一种情况,都表示v0和v1是共线的。剩下的又变成了顶点是否位于线段上的问题。
当 (v0・v0)(v1・v1) - (v0・v1)(v1・v0) != 0 时,便可求解u、v,进而确定P相对于三角形的位置。
算法的描述如下:
function PointInTriangle(p, a,b,c)
v0 = a - c; v1 = b -c; v2 = p - c;
dot00 = dot(v0, v0); dot01 = dot(v0, v1); dot11 = dot(v1, v1);
invDemon = dot00 * dot11 - dot01 * dot01;
if (invDemon < epsilon && invDemon > -epsilon)
return PointInLineSegment(p, a, b) || PointInLineSegment(p, a, c) || PointInLineSegment(p, b, c)
invDemon = 1 / invDemon;
dot02 = dot(v0, v2); dot12 = dot(v1, v2);
u = (dot11*dot02 - dot01*dot12) * invDemon;
v = (dot00*dot12 - dot01*dot02) * invDemon;
return 0<= u、v <= 1 && u + v <= 1
顶点是否位于线段上的判定算法PointInLineSegment,参看前篇博文。
重心法相较方法2中的同侧比较运算要少,所以更高效些。详细讨论见[1].
参考:
[1] http://www.blackpawn.com/texts/pointinpoly/default.html