说明
前面我们已经说过,三角形重心坐标的含义:
- 代表每个顶点的权重,即受到每个顶点影响的大小;
- 将三顶点看成是局部坐标系统的 x、y、z 轴,重心坐标就是局部坐标系内的坐标;
在本文中,我们还有一种解释:
- 重心坐标的每个分量,代表了该点分割三角形后,形成的小三角形的面积比。
几何
比如,重心的重心坐标为(1/3, 1/3, 1/3)
,含义就是重心将原三角形分割成的 3 个小三角形,每个占总面积的三分之一。这样理解重心坐标,就很容易理解:为什么三角形内的点,重心坐标之和为 1。因为 3 个小三角形面积相加,显然等于大三角形的面积。
那么当点位于三角形外呢?那么面积就会出现负值,所以重心坐标就至少有一个是负数,但总和仍然是 1。
那么为什么重心坐标的和等于 1?其实就是说明该点与三角形各个边形成的新的小三角形,面积之和等于原三角形的面积,本质上来说,就是表示该点与原三角形共面。而只有三维空间,都有共面问题。如果点与三角形不共面,那么重心坐标之和,也就是面积比值,是不等于 1 的。
一般情况下,点与三角形共面时,重心坐标几何意义更明确。所以三维空间中,求任意点在三角形上的重心坐标,首先要保证共面。然后利用叉乘求得各个小三角形面积,除以总面积就是该点的重心坐标。
当然,还有一种方法,就是利用投影后面积比值不变的特性,投影到二维(即丢弃 x, y, z 中的某一个坐标),就可以利用平面几何公式来处理重心坐标问题。至于要丢弃哪个坐标分量,可以根据三角形的法线来判断,比如法线与 x轴点乘结果最大,即最接近平行,那就可以丢弃 x 轴坐标,投影到 yz 平面上处理。
表面上看该方法少了一个维度,计算量更少。但实际上,它多了分支处理,且不能充分发挥 SIMD 指令的效率,实际效果并不好。更推荐在三维空间中,直接用叉乘求面积。
代码
///点在三角形上的重心坐标
static func computeBarycenricCoordinate(of point:simd_float3, in triangle:Triangle) -> simd_float3? {
let e1 = triangle.point3 - triangle.point2
let e2 = triangle.point1 - triangle.point3
let e3 = triangle.point2 - triangle.point1
let d1 = point - triangle.point1
let d2 = point - triangle.point2
let d3 = point - triangle.point3
let n = cross(e1, e2)
if !n.isPerpendicular(to: d1) {
// 点与三角形不共面
return nil
}
let an = dot(cross(e1, e2), n)
if an < Float.leastNormalMagnitude {
// 退化三角形:面积为零的三角形
return nil
}
let b1 = dot(cross(e1, d3), n) / an
let b2 = dot(cross(e2, d1), n) / an
let b3 = dot(cross(e3, d2), n) / an
return simd_float3(b1, b2, b3)
}
///点在三角形上的重心坐标
static func computeBarycenricCoordinate2(of point:simd_float3, in triangle:Triangle) -> simd_float3? {
let d1 = triangle.point2 - triangle.point1
let d2 = triangle.point3 - triangle.point2
let n = cross(d1, d2)
let t = point - triangle.point1
if !n.isPerpendicular(to: t) {
// 点与三角形不共面
return nil
}
var u1,u2,u3,u4:Float
var v1,v2,v3,v4:Float
if (fabsf(n.x) >= fabsf(n.y)) && (fabsf(n.x) >= fabsf(n.z)) {
// 抛弃 x,向 yz 平面投影
u1 = triangle.point1.y - triangle.point3.y
u2 = triangle.point2.y - triangle.point3.y
u3 = point.y - triangle.point1.y
u4 = point.y - triangle.point3.y
v1 = triangle.point1.z - triangle.point3.z
v2 = triangle.point2.z - triangle.point3.z
v3 = point.z - triangle.point1.z
v4 = point.z - triangle.point3.z
} else if fabsf(n.y) >= fabsf(n.z){
// 抛弃 y,向 xz 平面投影
u1 = triangle.point1.z - triangle.point3.z
u2 = triangle.point2.z - triangle.point3.z
u3 = point.z - triangle.point1.z
u4 = point.z - triangle.point3.z
v1 = triangle.point1.x - triangle.point3.x
v2 = triangle.point2.x - triangle.point3.x
v3 = point.x - triangle.point1.x
v4 = point.x - triangle.point3.x
} else {
u1 = triangle.point1.x - triangle.point3.x
u2 = triangle.point2.x - triangle.point3.x
u3 = point.x - triangle.point1.x
u4 = point.x - triangle.point3.x
v1 = triangle.point1.y - triangle.point3.y
v2 = triangle.point2.y - triangle.point3.y
v3 = point.y - triangle.point1.y
v4 = point.y - triangle.point3.y
}
let denom = v1 * u2 - v2 * u1
if abs(denom) < Float.leastNormalMagnitude {
// 退化三角形:面积为零的三角形
return nil
}
// 计算重心坐标
let oneOverDenom = 1.0 / denom
let b0 = (v4*u2 - v2*u4) * oneOverDenom
let b1 = (v1*u3 - v3*u1) * oneOverDenom
let b2 = 1 - b0 - b1
return simd_float3(b0, b1, b2)
}