在计算机图形学中,渲染图片时,会利用图形的顶点把其表面划分成一个个的小三角形,然后再在三角形内部做线性插值,算出图形表面任意点的坐标、颜色等等。
本文主要研究:给定三角形三个顶点 A、B、C 的坐标,以及点 P 的坐标。如何根据它们判断点 P 是否在三角形内部?

关键词:重心坐标,Barycentric coordinate system
参考资料:https://blackpawn.com/texts/pointinpoly
Same Side Technique
最容易想到的方法是:把点 P 和 A、B、C 连起来,然后分别计算
还有一种方法,是判断点 P 在线的哪一边。如果同时满足:
- 点 P 和 C 在直线 AB 的同侧
- 点 P 和 A 在直线 BC 的同侧
- 点 P 和 B 在直线 AC 的同侧
则点 P 显然就在
数学上如何判断两个点在直线同侧呢?以 P 、C 在 AB 同侧为例,只需
转化成伪代码如下所示:
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
上面的伪代码用于判断 p1,p2 两个点,是否在 a 与 b 连线的同侧。
那么,相应的判断点是否在三角形内部的算法,也显而易见了:
function PointInTriangle(p, a, b, c)
PC = SameSide(p, c, a, b) # p 和 c 在 ab 的同侧
PA = SameSide(p, a, b, c) # p 和 a 在 bc 的同侧
PB = SameSide(p, b, a, c) # p 和 b 在 ac 的同侧
return PA and PB and PC
上述算法很简单且易于实现,但效率还不算太高,下面引入本文主角——重心坐标。
Barycentric Technique

为了方便表达,令
因此我们可以令:
要想让 P 在
-
且
-
(反推法,取 BC 上一点 p'= v0+λ(v1-v0),那么 u=(1-λ) v=λ,u+v=1)
下一步是解出 u 和 v。
(1) 两侧同时点乘
(1) 两侧同时点乘
由于四个坐标都是已知的,因此
解得(为了整洁,下面的公式去掉了
发现这俩的分母相同,因此又可以节省很多计算。下面是伪代码:
function PointInTriangle(p, a, b, c)
// 计算向量
v0 = c-a
v1 = b-a
v2 = p-a
// 计算点乘
dot00 = dot(v0, v0)
dot01 = dot(v0, v1)
dot02 = dot(v0, v2)
dot11 = dot(v1, v1)
dot12 = dot(v1, v2)
// 计算重心坐标系下的坐标
invDenom = 1/(dot00 * dot11 - dot01 * dot01)
u = (dot11 * dot02 - dot01 * dot12) * invDenom
v = (dot00 * dot12 - dot01 * dot02) * invDenom
return u>=0 and v>=0 and (u+v)<1
我们把上述算法和第一节中的 Same Side Technique 对比一下。
对于三维向量:
点乘:3 个乘法 + 2 个加法
叉乘:6 个乘法 + 3 个加法
向量相加减:3个加法
因此,对于 SameSide Technique,判断一次 SameSide 需要做 15次乘法和 20 次加法(20次是伪代码的加法数,可以稍微优化一下,在函数外事先求好向量,但能省下的运算也很有限),三次 SameSide 完成判断,所以总共用了 45 个乘法,60 次加法。
对于 Barycentric Technique,数了一下共用了 23 次乘法,1次除法(似乎精度不高时乘法和除法效率差不多), 22 次加法,显然 Barycentric Technique 效率更高。
******************************************************************************************
根据大佬@TM Zhang 的评论,这里用星号分割线更新一下。上文中的 SameSide 算法重复计算了很多次叉乘,原始伪代码需要计算:
而三角形中,任意两条边的叉乘大小为三角形的面积的两倍,方向由右手螺旋定则判断。由此可以发现,
即,只需要计算:
如果上面这四个叉乘方向相同,则 P 在三角形内部。

我们希望用前三个叉乘中的元素,表示最后一个叉乘中的元素:
因此只要前三个叉乘方向相同,它们之和的方向一定与它们相同,最终 SameSide Technique 化简为求三个叉乘:
改进过的 SameSide Technique 伪代码如下:
function PointInTriangle(p, a, b, c)
AP = p-a
AB = b-a
BP = p-b
BC = c-b
CP = p-c
CA = c-a
APxAB = CrossProduct(AP, AB)
BPxBC = CrossProduct(BP, BC)
CPxCA = CrossProduct(CP, CA)
dot1 = DotProduct(APxAB, BPxBC)
dot2 = DotProduct(BPxBC, CPxCA)
return dot1>=0 and dot2>=0
改进后的 SameSide Technique 需要31次加法,24次乘法,还是比不上 Barycentric Technique 的22次加法,24次乘法。虽然这俩看上去也没差多少,但是在计算机图形学中,一个物体表面可能有成千上万乃至几十万个小三角形,而每个小三角形中,又有几十上百个需要判断的点,即使是微弱的优势,也会被放大百万倍,不可轻视。但是评论区另有大佬指出,barycentric technique 需要计算除法,会对精度造成负面影响。
******************************************************************************************
为什么叫 Barycentric Technique
现在再回到 (1):

将向量换回点的坐标相减的形式:
合并同类项得:
这种形式下,就立刻看出为啥这个方法叫 Barycentric Coordinate 了,即:可以把 P 看成 A、B、C 三个顶点的加权平均。P 离哪个点越近,该点的权重就越接近 1;如果三个顶点的权重都是 1/3,P 就是三角形的重心。
它也可以用于计算三角形内任何一点的颜色,例如在做渲染时,已知三角形三个顶点的颜色,那么它内部任何一点的颜色,就是三个顶点颜色的加权平均,权重如公式 (5) 所示。
在判断遮挡关系的 z-buffer 算法中,希望求出图片上每个像素点的深度,此时也需要用到 (5):
我们很容易知道图片上某个像素点的横、纵坐标。那么就可以把它所在的小三角形三个顶点的横、纵坐标,代入公式 (5) 联立求出 u、v 的值:虽然有三个坐标,但公式 (5) 只有两个未知数,因此知道横、纵坐标足以解出 u 和 v。求解的算法还是 Barycentric Technique 的伪代码,只不过此时传入的都是去掉 z 坐标的二维点。
三个顶点的 z 值是知道的,结合刚刚求出的 u、v 值,再代回 (5) 就求出了该像素点的 z 值。
后记
我是图形学的外行,本来这就是一篇学习笔记/备忘录,没想到在和评论区的很多大神的讨论中,学习到那么多新东西,感谢!