当前位置: 首页 > news >正文

分离轴算法(SAT)的前置步骤:手把手教你用Python实现凹多边形切割

分离轴算法(SAT)的前置步骤:手把手教你用Python实现凹多边形切割

在计算机图形学和游戏物理引擎开发中,碰撞检测是一个基础而关键的问题。分离轴定理(SAT)作为高效的碰撞检测算法,要求输入必须是凸多边形。但现实中的物体形状往往包含凹多边形,这就需要在应用SAT前进行凹多边形切割。本文将带你从零开始,用Python实现这一关键预处理步骤。

1. 理解凹多边形与凸多边形

凹多边形和凸多边形是几何学中的基本概念。简单来说,凸多边形是指所有内角都不超过180度的多边形,而凹多边形则至少有一个内角大于180度。这种结构差异直接影响碰撞检测算法的选择:

  • 凸多边形特性:

    • 任意两点连线都在多边形内部
    • 所有内角均小于180度
    • 适合使用分离轴算法
  • 凹多边形特性:

    • 存在至少一个内角大于180度
    • 可能存在两点连线在多边形外部
    • 需要先切割为凸多边形才能应用SAT
class Polygon: def __init__(self, points): self.points = points # 顶点列表,按逆时针顺序存储 self.is_convex = self._check_convexity() def _check_convexity(self): """检查多边形是否为凸多边形""" n = len(self.points) if n < 3: return False sign = 0 for i in range(n): # 获取连续的三个点 a = self.points[i] b = self.points[(i+1)%n] c = self.points[(i+2)%n] # 计算叉积 cross = (b[0]-a[0])*(c[1]-b[1]) - (b[1]-a[1])*(c[0]-b[0]) if cross == 0: continue # 共线情况 if sign == 0: sign = 1 if cross > 0 else -1 else: if (cross > 0 and sign < 0) or (cross < 0 and sign > 0): return False # 发现凹点 return True

2. 凹多边形切割的核心算法

凹多边形切割的核心思路是递归地消除凹点,直到所有多边形都变为凸多边形。具体步骤如下:

  1. 找到第一个凹点
  2. 延长该凹点的起始边
  3. 计算延长线与多边形其他边的交点
  4. 根据交点将多边形分割为两部分
  5. 对分割后的多边形重复上述过程

2.1 寻找凹点

在逆时针排列的多边形顶点中,凹点的判定标准是内角大于180度。通过向量叉积可以高效判断:

def find_concave_point(polygon): """寻找多边形中的凹点""" n = len(polygon) for i in range(n): a = polygon[i] b = polygon[(i+1)%n] c = polygon[(i+2)%n] # 计算向量ab和bc ab = (b[0]-a[0], b[1]-a[1]) bc = (c[0]-b[0], c[1]-b[1]) # 计算叉积 cross = ab[0]*bc[1] - ab[1]*bc[0] # 在逆时针排列下,叉积为负表示凹点 if cross < 0: return (i+1)%n # 返回凹点索引 return None # 没有凹点,是凸多边形

2.2 延长边与求交点

找到凹点后,我们需要延长其起始边,并计算与多边形其他边的交点:

def line_intersection(line1, line2): """计算两条直线的交点""" (x1, y1), (x2, y2) = line1 (x3, y3), (x4, y4) = line2 # 计算分母 denom = (y4-y3)*(x2-x1) - (x4-x3)*(y2-y1) if denom == 0: # 平行或共线 return None # 计算参数 ua = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / denom ub = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / denom # 检查交点是否在线段上 if ua < 0 or ua > 1 or ub < 0 or ub > 1: return None # 计算交点坐标 x = x1 + ua * (x2 - x1) y = y1 + ua * (y2 - y1) return (x, y)

3. 实现完整的切割流程

将上述步骤组合起来,我们可以实现完整的凹多边形切割算法:

def split_concave_polygon(polygon): """将凹多边形分割为凸多边形列表""" from collections import deque convex_polygons = [] queue = deque([polygon]) while queue: current = queue.popleft() concave_idx = find_concave_point(current) if concave_idx is None: # 已经是凸多边形 convex_polygons.append(current) continue # 获取凹点和其相邻点 n = len(current) prev_idx = (concave_idx - 1) % n next_idx = (concave_idx + 1) % n prev_point = current[prev_idx] concave_point = current[concave_idx] next_point = current[next_idx] # 延长prev_point到concave_point的边 extended_line = (prev_point, concave_point) # 寻找与延长线相交的边 intersection = None intersect_idx = None for i in range(n): if i == prev_idx or i == concave_idx: continue line = (current[i], current[(i+1)%n]) intersect = line_intersection(extended_line, line) if intersect: intersection = intersect intersect_idx = i break if not intersection: # 没有找到交点,可能是特殊情况 # 处理延长线与顶点相交的情况 for i in range(n): if i == prev_idx or i == concave_idx: continue # 检查点是否在延长线上 point = current[i] if point_on_line(point, extended_line): intersection = point intersect_idx = i break if not intersection: continue # 无法分割,保留原多边形 # 分割多边形 if prev_idx < intersect_idx: part1 = current[prev_idx+1:intersect_idx+1] + [intersection] part2 = [intersection] + current[intersect_idx+1:] + current[:prev_idx+1] else: part1 = current[prev_idx+1:] + current[:intersect_idx+1] + [intersection] part2 = [intersection] + current[intersect_idx+1:prev_idx+1] queue.append(part1) queue.append(part2) return convex_polygons

4. 可视化与性能优化

为了更好理解算法过程,我们可以使用matplotlib进行可视化:

import matplotlib.pyplot as plt from matplotlib.patches import Polygon as MplPolygon def plot_polygons(polygons, title=""): """绘制多边形列表""" fig, ax = plt.subplots(figsize=(8, 8)) for i, poly in enumerate(polygons): color = (random.random(), random.random(), random.random()) patch = MplPolygon(poly, closed=True, facecolor=color, edgecolor='black', alpha=0.5, label=f'Polygon {i+1}') ax.add_patch(patch) ax.set_xlim(min(p[0] for poly in polygons for p in poly)-1, max(p[0] for poly in polygons for p in poly)+1) ax.set_ylim(min(p[1] for poly in polygons for p in poly)-1, max(p[1] for poly in polygons for p in poly)+1) ax.set_aspect('equal') ax.set_title(title) ax.legend() plt.grid(True) plt.show()

4.1 性能优化技巧

  1. 提前终止检查:在分割过程中,一旦发现多边形已经是凸多边形,立即停止进一步分割
  2. 缓存计算结果:对于大型多边形,可以缓存已经计算过的边交点
  3. 并行处理:对于多个独立的多边形,可以采用并行处理方式
def optimized_split(polygon, max_depth=10): """带优化的凹多边形分割""" if max_depth <= 0: return [polygon] if is_convex(polygon): return [polygon] # ... 分割逻辑 ... part1_convex = is_convex(part1) part2_convex = is_convex(part2) result = [] if not part1_convex: result += optimized_split(part1, max_depth-1) else: result.append(part1) if not part2_convex: result += optimized_split(part2, max_depth-1) else: result.append(part2) return result

5. 与分离轴算法(SAT)的集成

完成凹多边形切割后,我们可以将结果直接用于SAT碰撞检测:

def sat_collision(poly1, poly2): """分离轴算法实现""" polygons1 = split_concave_polygon(poly1) if not is_convex(poly1) else [poly1] polygons2 = split_concave_polygon(poly2) if not is_convex(poly2) else [poly2] # 检查所有凸多边形组合 for p1 in polygons1: for p2 in polygons2: if convex_sat(p1, p2): return True return False def convex_sat(poly1, poly2): """凸多边形SAT检测""" axes = [] # 获取所有边的法线作为投影轴 for i in range(len(poly1)): a = poly1[i] b = poly1[(i+1)%len(poly1)] edge = (b[0]-a[0], b[1]-a[1]) normal = (-edge[1], edge[0]) # 左手法则得到的法线 axes.append(normal) for i in range(len(poly2)): a = poly2[i] b = poly2[(i+1)%len(poly2)] edge = (b[0]-a[0], b[1]-a[1]) normal = (-edge[1], edge[0]) axes.append(normal) # 归一化所有轴 axes = [normalize(axis) for axis in axes] # 在所有轴上检查投影重叠 for axis in axes: if not overlap_on_axis(poly1, poly2, axis): return False return True

在实际项目中,这种凹多边形预处理配合SAT算法能够高效处理复杂形状的碰撞检测,是游戏引擎和物理模拟中的常用技术组合。

http://www.jsqmd.com/news/874656/

相关文章:

  • 线性化多噪声训练:提升混沌系统长期预测稳定性的正则化技术
  • JWT签名机制与常见攻击实战:从PortSwigger靶场12关学透算法混淆、密钥混淆与JWKS劫持
  • Rust异步编程实战:构建高性能并发应用
  • 边缘计算与多车协同如何提升自动驾驶目标检测
  • Ubuntu 22.04双网卡配置踩坑记:netplan apply报错‘默认路由冲突’的三种解法
  • 2026四川导轨油代理商品牌推荐榜:壳牌润滑油代理商推荐、导轨油代理商推荐、昆仑润滑油代理商推荐、福斯润滑油代理商推荐选择指南 - 优质品牌商家
  • Keil µVision项目文件路径批量修改实战指南
  • NVIDIA Geforce RTX 5060 Ti显卡能本地部署的哪些AI应用?
  • 玛氏北京怀柔巧克力工厂迎来在华发展三十周年里程碑
  • 别再只懂ls -l了!手把手教你用getfattr/setfattr玩转Linux文件隐藏属性
  • AI企业参与国防采购的挑战、机遇与实操路线图
  • 从原理到实战:深入理解ArUco码如何算出相机在三维空间中的位置和朝向(Python/OpenCV)
  • 如何用Nvidia Geforce RTX 5060 Ti显卡进行本地Whisper语音转文字任务?
  • 2026年5月更新:专业模具温控系统定制,如何选择值得信赖的合作伙伴? - 2026年企业推荐榜
  • 别再让auditd拖慢你的麒麟系统!手把手教你排查并关闭这个审计服务
  • C51开发中VPRINTF与VSPRINTF的内存陷阱与解决方案
  • 从‘进程打架’到‘内存搬家’:用大白话图解操作系统核心概念(附避坑指南)
  • 量子机器学习中的ROC曲线分析与优化实践
  • BL51链接器段名通配符使用技巧与工程实践
  • 别再只跑模型了!用FAD、NDB、JSD给你的AI生成声音打个分(Python实战避坑)
  • 2026 年 YAML“挪威难题”仍未解决,流行库为何还停留在旧版本?
  • Unity动画中断控制:Interruption Source与Ordered Interruption详解
  • 别再一股脑儿塞特征了!用sklearn的VarianceThreshold和SelectKBest给你的模型减减肥
  • GPU计算优化:MPK架构提升深度学习推理效率
  • OpenPLC Editor:如何用免费开源工具解决工业自动化编程难题
  • CVE-2025-1974深度解析:Exchange身份透传漏洞与NTLM信任链崩塌
  • 卸载360/火绒后Win11安全中心打不开?亲测有效的完整修复流程记录
  • OpenSSH信号竞态漏洞CVE-2024-6387深度解析与实战修复
  • 低资源环境下BERT领域适应与混合精度训练优化
  • 避坑指南:用CloudCompare修改点云标签时,为什么总会多出一列NaN?我的修复脚本分享