S02E17:任意点在三角形上的重心坐标

2,766 阅读3分钟

说明

前面我们已经说过,三角形重心坐标的含义:

  • 代表每个顶点的权重,即受到每个顶点影响的大小;
  • 将三顶点看成是局部坐标系统的 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)
}