You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何检测三角形与给定立方体是否相交?含三点定义顶点场景

嘿,这个问题在3D碰撞检测里是个经典场景,我来给你梳理一套实用的解决方案,从思路到具体实现都讲清楚:

三角形与立方体相交检测的完整指南

1. 先明确立方体的定义

首先得搞清楚你的立方体是轴对齐包围盒(AABB)还是任意旋转的有向包围盒(OBB)?大部分场景下如果没特别说明,默认都是AABB——也就是用一个最小点min(x₁,y₁,z₁)和最大点max(x₂,y₂,z₂)来定义,每个面都平行于坐标轴。下面我先以AABB为例讲解,最后会提OBB的情况。

2. 核心方法:分离轴定理(SAT)

这是凸体碰撞检测的黄金准则,逻辑直白又高效:

如果存在一条轴,使得两个凸体在这条轴上的投影区间完全不重叠,那么它们不相交;反之,若所有可能的分离轴上的投影都重叠,则它们相交

针对三角形+AABB,需要检查哪些分离轴?

总共三类轴,一个都不能少:

  • AABB的三个坐标轴(x/y/z轴):因为AABB的面是轴对齐的,这是最基础的分离轴。
  • 三角形的法线:如果三角形的法线方向上,两者投影不重叠,直接分离。
  • 三角形三条边与AABB三条边叉乘得到的9条轴:这是为了检测三角形的边穿过AABB棱边的边界情况。

3. 具体检测步骤(按优先级排序,越快短路越好)

步骤1:快速短路判断——顶点包含检查

这是最快的判断方式,先做这个能省很多计算:

  • 如果三角形的任意一个顶点在AABB内部(即min.x ≤ v.x ≤ max.x,y/z方向同理),直接判定相交。
  • 反过来,如果AABB的8个顶点里有任意一个在三角形内部,也直接判定相交。

判断点是否在三角形内部,可以用向量叉乘的符号一致性法:计算点与三角形三条边的叉乘,若所有叉乘结果的符号一致(都正或都负,取决于坐标系左右手性),则点在内部。

步骤2:用分离轴定理遍历所有候选轴

如果上面的短路判断没得出结果,就开始检查所有分离轴:

  1. 对每条轴,分别计算AABB和三角形在该轴上的投影区间:
    • AABB的投影:计算minmax点与轴的点积,取最小和最大值作为区间。
    • 三角形的投影:计算三个顶点与轴的点积,取最小和最大值作为区间。
  2. 检查两个区间是否重叠:如果AABB投影的最大值 < 三角形投影的最小值,或者反过来,说明这条轴是分离轴,直接返回不相交
  3. 所有轴都检查完且没有分离轴,就返回相交

4. 优化技巧

  • 预检测:三角形AABB与目标AABB的快速相交:先给三角形算个AABB,和目标立方体的AABB做简单的区间重叠检测,如果这两个AABB都不相交,那三角形和立方体肯定不相交,直接跳过后续复杂步骤。
  • 短路返回:只要找到一条分离轴,立刻停止检查剩下的轴,节省计算量。
  • 避免零向量:计算叉乘或法线时,要加个小epsilon(比如1e-6)判断是否为零向量,避免退化三角形导致的计算错误。

5. 伪代码实现

# 辅助函数:判断点是否在AABB内
def point_in_aabb(p, aabb_min, aabb_max):
    return (aabb_min[0] <= p[0] <= aabb_max[0] and
            aabb_min[1] <= p[1] <= aabb_max[1] and
            aabb_min[2] <= p[2] <= aabb_max[2])

# 辅助函数:判断点是否在三角形内
def point_in_triangle(p, v0, v1, v2):
    # 计算三条边向量
    edge0 = (v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2])
    edge1 = (v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2])
    edge2 = (p[0]-v0[0], p[1]-v0[1], p[2]-v0[2])

    # 计算点积
    dot00 = edge0[0]*edge0[0] + edge0[1]*edge0[1] + edge0[2]*edge0[2]
    dot01 = edge0[0]*edge1[0] + edge0[1]*edge1[1] + edge0[2]*edge1[2]
    dot02 = edge0[0]*edge2[0] + edge0[1]*edge2[1] + edge0[2]*edge2[2]
    dot11 = edge1[0]*edge1[0] + edge1[1]*edge1[1] + edge1[2]*edge1[2]
    dot12 = edge1[0]*edge2[0] + edge1[1]*edge2[1] + edge1[2]*edge2[2]

    # 计算重心坐标的分母
    denom = dot00 * dot11 - dot01 * dot01
    if abs(denom) < 1e-6:
        return False  # 三角形退化(共线)

    # 计算重心坐标u和v
    u = (dot11 * dot02 - dot01 * dot12) / denom
    v = (dot00 * dot12 - dot01 * dot02) / denom

    # 判断点是否在三角形内部
    return u >= -1e-6 and v >= -1e-6 and (u + v) <= 1 + 1e-6

# 辅助函数:计算AABB在轴上的投影区间
def project_aabb(aabb_min, aabb_max, axis):
    proj_min = aabb_min[0]*axis[0] + aabb_min[1]*axis[1] + aabb_min[2]*axis[2]
    proj_max = aabb_max[0]*axis[0] + aabb_max[1]*axis[1] + aabb_max[2]*axis[2]
    if proj_min > proj_max:
        proj_min, proj_max = proj_max, proj_min
    return (proj_min, proj_max)

# 辅助函数:计算三角形在轴上的投影区间
def project_triangle(v0, v1, v2, axis):
    p0 = v0[0]*axis[0] + v0[1]*axis[1] + v0[2]*axis[2]
    p1 = v1[0]*axis[0] + v1[1]*axis[1] + v1[2]*axis[2]
    p2 = v2[0]*axis[0] + v2[1]*axis[1] + v2[2]*axis[2]
    return (min(p0, p1, p2), max(p0, p1, p2))

# 辅助函数:判断两个区间是否重叠
def intervals_overlap(a_min, a_max, b_min, b_max):
    return not (a_max < b_min - 1e-6 or b_max < a_min - 1e-6)

# 主函数:三角形与AABB相交检测
def triangle_aabb_intersection(v0, v1, v2, aabb_min, aabb_max):
    # 短路判断:三角形顶点在AABB内
    if point_in_aabb(v0, aabb_min, aabb_max) or \
       point_in_aabb(v1, aabb_min, aabb_max) or \
       point_in_aabb(v2, aabb_min, aabb_max):
        return True
    
    # 短路判断:AABB顶点在三角形内
    aabb_vertices = [
        (aabb_min[0], aabb_min[1], aabb_min[2]),
        (aabb_max[0], aabb_min[1], aabb_min[2]),
        (aabb_min[0], aabb_max[1], aabb_min[2]),
        (aabb_max[0], aabb_max[1], aabb_min[2]),
        (aabb_min[0], aabb_min[1], aabb_max[2]),
        (aabb_max[0], aabb_min[1], aabb_max[2]),
        (aabb_min[0], aabb_max[1], aabb_max[2]),
        (aabb_max[0], aabb_max[1], aabb_max[2])
    ]
    for vert in aabb_vertices:
        if point_in_triangle(vert, v0, v1, v2):
            return True
    
    # 准备所有分离轴
    axes = []
    # AABB的三个坐标轴
    axes.append((1.0, 0.0, 0.0))
    axes.append((0.0, 1.0, 0.0))
    axes.append((0.0, 0.0, 1.0))
    # 三角形的法线
    edge0 = (v1[0]-v0[0], v1[1]-v0[1], v1[2]-v0[2])
    edge1 = (v2[0]-v0[0], v2[1]-v0[1], v2[2]-v0[2])
    tri_normal = (
        edge0[1]*edge1[2] - edge0[2]*edge1[1],
        edge0[2]*edge1[0] - edge0[0]*edge1[2],
        edge0[0]*edge1[1] - edge0[1]*edge1[0]
    )
    norm_len = (tri_normal[0]**2 + tri_normal[1]**2 + tri_normal[2]**2)**0.5
    if norm_len > 1e-6:
        # 归一化法线
        tri_normal = (tri_normal[0]/norm_len, tri_normal[1]/norm_len, tri_normal[2]/norm_len)
        axes.append(tri_normal)
    # 三角形边与AABB边的叉乘轴
    tri_edges = [edge0, (v2[0]-v1[0], v2[1]-v1[1], v2[2]-v1[2]), (v0[0]-v2[0], v0[1]-v2[1], v0[2]-v2[2])]
    aabb_edges = [(1.0,0.0,0.0), (0.0,1.0,0.0), (0.0,0.0,1.0)]
    for te in tri_edges:
        for ae in aabb_edges:
            cross = (
                te[1]*ae[2] - te[2]*ae[1],
                te[2]*ae[0] - te[0]*ae[2],
                te[0]*ae[1] - te[1]*ae[0]
            )
            cross_len = (cross[0]**2 + cross[1]**2 + cross[2]**2)**0.5
            if cross_len > 1e-6:
                cross = (cross[0]/cross_len, cross[1]/cross_len, cross[2]/cross_len)
                axes.append(cross)
    
    # 检查每个分离轴
    for axis in axes:
        a_min, a_max = project_aabb(aabb_min, aabb_max, axis)
        t_min, t_max = project_triangle(v0, v1, v2, axis)
        if not intervals_overlap(a_min, a_max, t_min, t_max):
            return False
    
    # 所有轴都重叠,判定相交
    return True

6. 扩展:任意旋转立方体(OBB)的情况

如果立方体是任意旋转的OBB,分离轴的集合会变成:

  • OBB的三个面法线
  • 三角形的法线
  • 三角形三条边与OBB三条边叉乘的9条轴

检测逻辑和AABB一致,只是AABB的投影计算要换成OBB的投影:需要先将OBB的顶点变换到局部空间,或者直接用OBB的中心、轴向和半长来计算投影区间。

注意事项

  • 浮点数精度:所有比较都要引入一个小epsilon(比如1e-6),避免因为计算误差导致的误判。
  • 退化三角形:如果三个顶点共线,直接降级为线段与立方体的相交检测即可。

内容的提问来源于stack exchange,提问作者v.slobodzian

火山引擎 最新活动