线段与AABB求交的算法
2025/3/25大约 7 分钟
线段与AABB求交的算法
如何快速判断任意线段与任意AABB是否相交?首先想到的方法是借助射线与AABB求交的算法,获得射线进入和离开的时机,在射线与AABB相交的前提下保证进入时机在线段终点之前。
下面是搜集到的三种算法即实现,并且通过添加轴的的维度完全可以应用于三维情况。
零、 数据结构
import random
import time
import numpy as np
import matplotlib.pyplot as plt
class Vec2D:
def __init__(self, x, y):
self.v = np.array([x, y])
def __sub__(self, other):
return Vec2D(self.v[0] - other.v[0], self.v[1] - other.v[1])
def __getitem__(self, index):
return self.v[index]
def __mul__(self, scalar):
return Vec2D(self.v[0] * scalar, self.v[1] * scalar)
def __add__(self, other):
return Vec2D(self.v[0] + other.v[0], self.v[1] + other.v[1])
def ToString(self):
return f'({self.v[0]},{self.v[1]})'
class Rect:
def __init__(self, min_x, min_y, max_x, max_y):
self.min = Vec2D(min_x, min_y)
self.max = Vec2D(max_x, max_y)
def ToString(self):
return f'{{ {self.min.ToString()},{self.max.ToString()} }}'一、 射线与AABB求交算法
假设:
- 射线起点为,方向向量为,射线方程为
- AABB包围盒表示为
算法:
- 计算射线进入各个轴的时间,取最大值作为进入AABB的时间
- 计算射线离开各个轴的时间,取最小值作为离开AABB的时间
- 判断 是否成立(先进入、后离开),成立则相交,不成立则不相交
- 如需计算交点,计算 和 即为所求
实现:
# p1决定射线起点,p1、p2共同决定射线方向
def ray_aabb(p1, p2, aabb):
d = np.linalg.norm(p2 - p1)
t_min = 0
t_max = float('inf')
for axis in range(2):
inv_d = 1.0 / d[axis] if d[axis] != 0 else float('inf')
t0 = (aabb.min[axis] - p1[axis]) * inv_d
t1 = (aabb.max[axis] - p1[axis]) * inv_d
if inv_d < 0:
t0, t1 = t1, t0
t_min = max(t_min, t0)
t_max = min(t_max, t1)
if t_min > t_max:
return False
return TrueTricks:
- 根据方向向量 在指定轴上投影 的正负决定进入和离开该轴的时间
- 某个轴上出现 的情况,必不相交
二、 线段与AABB求交算法
1. 射线法
假设:
- 线段起点为,终点为,方向 ,线段方程为
- AABB包围盒表示为
算法:
参考射线与AABB求交的算法,只是方向非单位向量,参数t的上限约束到1
- 计算线段进入各个轴的时间,取最大值作为进入AABB的时间
- 计算线段离开各个轴的时间,取最小值作为离开AABB的时间
- 判断 是否成立(先进入、后离开),成立则相交,不成立则不相交
- 如需计算交点,计算 和 即为所求
# p1决定线段起点,p2决定线段终点
def seg_aabb(p1, p2, aabb):
d = p2 - p1
t_min = 0
t_max = 1
for axis in range(2):
inv_d = 1.0 / d[axis] if d[axis] != 0 else float('inf')
t0 = (aabb.min[axis] - p1[axis]) * inv_d
t1 = (aabb.max[axis] - p1[axis]) * inv_d
if inv_d < 0:
t0, t1 = t1, t0
t_min = max(t_min, t0)
t_max = min(t_max, t1)
if t_min > t_max:
return False
return TrueTricks:
- 将线段视为有上限的射线
2. liang_barsky算法
假设:
- 线段起点为,终点为,方向 ,线段方程为
- AABB包围盒表示为
算法:
- 联立方程
- 获得满足位于AABB内的点
- 计算逐个轴中 的上下限,以x轴为例:
- 预计算不等式中的各项,以x轴为例:
- 根据预计算结果,逐轴计算实际 的上下限并判断该轴内是否可能相交,以x轴为例:
即:
- 如需计算交点,计算 和 即为所求
实现:
def liang_barsky(p1, p2, aabb):
dx = p2[0] - p1[0]
dy = p2[1] - p1[1]
p = [-dx, dx, -dy, dy]
q = [p1[0] - aabb.min[0], aabb.max[0] - p1[0], p1[1] - aabb.min[1], aabb.max[1] - p1[1]]
t0, t1 = 0.0, 1.0
for i in range(4):
if p[i] == 0: # 线段平行于矩形的某一边
if q[i] < 0: # 线段在边界之外
return False
else:
t = q[i] / p[i]
if p[i] < 0:
if t > t1:
return False # 线段完全在边界外
if t > t0:
t0 = t # 向内裁剪
else:
if t < t0:
return False # 线段完全在边界外
if t < t1:
t1 = t # 向内裁剪
return True # 线段与矩形相交Tricks:
- 中间结果预计算
- 根据被除项的正负决定是上限还是下限
3. Cohen-Sutherland算法
原理: 将整个空间用AABB四条边划分为9个区域,并对区域进行编码,根据线段两端点所在区域编码快速决定线段与AABB的关系。无法立即判断的情况下对线段进行裁剪,递归判断
实现:
- 对空间进行编码,左中右分别使用10、00、01,上中下分别使用10、00、01,并加以组合获取9个编码
| 左 | 中 | 右 | |
|---|---|---|---|
| 上 | 10 10 | 10 00 | 10 01 |
| 中 | 00 10 | 00 00 | 00 01 |
| 下 | 01 10 | 01 00 | 01 01 |
获得线段两端点 , 所在区域的编码 ,
快速判断相交情况:
$ c_0 | c_1 = 0 $ , 必相交(线段全在AABB内)
$ c_0 & c_1 \neq 0 $ , 必不相交(线段两端点均在AABB某一侧)
- 其余情况需按照左右上下的顺序,求出虚交点 迭代判断
实现:
def compute_outcode(point, aabb):
# 计算点的区域编码
code = 0
for axis in range(2):
code = code << 2
if point[axis] < aabb.min[axis]:
code |= 0b01
elif point[axis] > aabb.max[axis]:
code |= 0b10
else:
code |= 0b00
return code
def cohen_sutherland_clip(p0, p1, aabb):
outcode0 = compute_outcode(p0, aabb)
outcode1 = compute_outcode(p1, aabb)
accept = False
while True:
# 如果两个端点都在裁剪窗口内,则接受该线段
if not (outcode0 | outcode1):
accept = True
break
# 如果两个端点的区域编码按位与不为零,则线段完全在裁剪窗口外
elif outcode0 & outcode1:
break
else:
# 选择在裁剪窗口外的点
outcode_out = outcode0 if outcode0 else outcode1
# 计算交点
# 需要确定是哪个边界
for axis in range(2):
# 线段与轴的下限相交
if outcode_out & (0b01 << 2 * (2 - 1 - axis)):
t = (aabb.min[axis] - p0[axis]) / (p1[axis] - p0[axis])
p = p0 + (p1 - p0) * t
break
# 线段与轴的上限相交
elif outcode_out & (0b10 << 2 * (2 - 1 - axis)):
t = (aabb.max[axis] - p0[axis]) / (p1[axis] - p0[axis])
p = p0 + (p1 - p0) * t
break
# 更新点和区域编码
if outcode_out == outcode0:
p0 = p
outcode0 = compute_outcode(p0, aabb)
else:
p1 = p
outcode1 = compute_outcode(p1, aabb)
if accept:
return True # (p0, p1)
else:
return False # None
三、 测试代码

def visualize():
center = np.array([random.uniform(-10, 10), random.uniform(-10, 10)])
extend = np.array([random.uniform(0.1, 6.5), random.uniform(0.1, 6.5)])
aabb = Rect(center[0] - extend[0], center[1] - extend[1], center[0] + extend[0], center[1] + extend[1])
# aabb = Rect(2,1,3,4)
# 绘制 AABB
rectangle = plt.Rectangle((aabb.min[0], aabb.min[1]),
aabb.max[0] - aabb.min[0],
aabb.max[1] - aabb.min[1],
linewidth=1, edgecolor='r', facecolor='none', linestyle='--', label='AABB')
plt.gca().add_patch(rectangle)
for t in range(1, 20):
p1 = Vec2D(random.uniform(-10, 10), random.uniform(-10, 10))
p2 = Vec2D(random.uniform(-10, 10), random.uniform(-10, 10))
# p1 = Vec2D(-1.40,-1.79)
# p2 = Vec2D(4.54,1.39)
# 检查并显示检测结果
# is_collision = seg_aabb(p1, p2, aabb)
# is_collision = liang_barsky(p1, p2, aabb)
is_collision = cohen_sutherland_clip(p1, p2, aabb)
print(f'{p1.ToString()}->{p2.ToString()},{aabb.ToString()},{is_collision}')
if is_collision:
line_color = 'r'
else:
line_color = 'g'
# 绘制线段
plt.plot([p1[0], p2[0]], [p1[1], p2[1]], color=line_color)
# 设置坐标轴范围
plt.xlim(-10, 10)
plt.ylim(-10, 10)
plt.axhline(0, color='black', linewidth=0.5, ls='--')
plt.axvline(0, color='black', linewidth=0.5, ls='--')
plt.grid(color='gray', linestyle='--', linewidth=0.5)
plt.title('Line Segment vs AABB')
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
plt.legend()
plt.show()
def test(t):
start = time.perf_counter()
for i in range(t):
p1 = Vec2D(random.uniform(-10, 10), random.uniform(-10, 10))
p2 = Vec2D(random.uniform(-10, 10), random.uniform(-10, 10))
center = np.array([random.uniform(-10, 10), random.uniform(-10, 10)])
extend = np.array([random.uniform(0.1, 6.5), random.uniform(0.1, 6.5)])
aabb = Rect(center[0] - extend[0], center[1] - extend[1], center[0] + extend[0], center[1] + extend[1])
is_collision = liang_barsky(p1, p2, aabb)
end = time.perf_counter()
runTime = end - start
print("运行时间:", runTime, "秒")
start = time.perf_counter()
for i in range(t):
p1 = Vec2D(random.uniform(-10, 10), random.uniform(-10, 10))
p2 = Vec2D(random.uniform(-10, 10), random.uniform(-10, 10))
center = np.array([random.uniform(-10, 10), random.uniform(-10, 10)])
extend = np.array([random.uniform(0.1, 6.5), random.uniform(0.1, 6.5)])
aabb = Rect(center[0] - extend[0], center[1] - extend[1], center[0] + extend[0], center[1] + extend[1])
is_collision = liang_barsky(p1, p2, aabb)
end = time.perf_counter()
runTime = end - start
print("运行时间:", runTime, "秒")
visualize()
#test(1000000)
