如何检测三角形与给定立方体是否相交?含三点定义顶点场景
嘿,这个问题在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:用分离轴定理遍历所有候选轴
如果上面的短路判断没得出结果,就开始检查所有分离轴:
- 对每条轴,分别计算AABB和三角形在该轴上的投影区间:
- AABB的投影:计算
min和max点与轴的点积,取最小和最大值作为区间。 - 三角形的投影:计算三个顶点与轴的点积,取最小和最大值作为区间。
- AABB的投影:计算
- 检查两个区间是否重叠:如果
AABB投影的最大值 < 三角形投影的最小值,或者反过来,说明这条轴是分离轴,直接返回不相交。 - 所有轴都检查完且没有分离轴,就返回相交。
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




