Unity物理系统由浅入深第一节:Unity 物理系统基础与应用
Unity物理系统由浅入深第二节:物理系统高级特性与优化
Unity物理系统由浅入深第三节:物理引擎底层原理剖析
Unity物理系统由浅入深第四节:物理约束求解与稳定性
Unity 物理系统底层使用的是 NVIDIA PhysX 引擎。理解 PhysX 或任何其他实时物理引擎(如 Havok, Bullet)的工作方式,对于一名进阶的 Unity 开发者来说,是极其宝贵的知识。本篇我们将从宏观到微观,逐步了解物理引擎的核心概念和主要算法。
1. 物理引擎概述:一个模拟真实世界的机器
物理引擎本质上是一个复杂的软件系统,它接收场景中所有具有物理属性(刚体、碰撞体等)的物体信息,然后在一个固定时间步(Fixed Timestep)内,迭代计算这些物体在各种力(重力、推力、摩擦力等)和约束(碰撞、关节)作用下的位置、速度和旋转,从而模拟出真实的物理行为。
一个典型的物理引擎循环(在每个 FixedUpdate
中执行)大致遵循以下步骤:
- 积分器 (Integrators): 根据当前的速度和受到的力,预测物体在下一个时间步的位置和速度。
- 宽相碰撞检测 (Broad-Phase Collision Detection): 快速找出场景中所有可能发生碰撞的物体对。
- 窄相碰撞检测 (Narrow-Phase Collision Detection): 对上一步筛选出的可能碰撞对,进行精确的碰撞检测,计算出详细的碰撞信息(接触点、法线、穿透深度等)。
- 接触生成 (Contact Generation): 根据窄相检测的结果,生成接触点(Contact Points)和接触法线(Contact Normals)。
- 约束求解器 (Constraint Solver): 处理所有碰撞(以及关节)产生的约束。这是物理引擎最复杂的部分,它通过迭代计算来解决物体之间的相互作用,确保它们不会相互穿透,并且能正确反弹、滑动或连接。
- 更新状态: 根据求解器的结果,更新所有刚体的位置和速度。
2. 刚体动力学基础:物理引擎的语言
在深入算法之前,我们需要回顾一下物理学中的一些基本概念,它们是物理引擎的“语言”。
- 力 (Force):改变物体运动状态的原因。单位牛顿 (N)。
- 质量 (Mass):物体惯性的量度。单位千克 (kg)。
- 惯性 (Inertia):物体保持其运动状态的特性。质量越大,惯性越大。
- 惯性张量 (Inertia Tensor):描述刚体旋转惯性的一个 3x3 矩阵。它表示了物体在不同轴上抵抗旋转的能力。对于非球对称的物体,其旋转惯性在不同方向上是不同的。在 Unity 中,对于常见的原始碰撞体,PhysX 会自动计算好。对于 Mesh Collider,可能需要外部工具或手动计算。
- 线速度 (Linear Velocity):物体直线运动的速度向量。
- 角速度 (Angular Velocity):物体旋转的速度向量。
- 动量 (Momentum):物体的质量与其线速度的乘积。
- P=mvP = mvP=mv
- 角动量 (Angular Momentum):描述物体旋转运动的量。
- L=IωL = I\omegaL=Iω (其中 III 是惯性张量,ω\omegaω 是角速度)
- 冲量 (Impulse):在短时间内施加的巨大力。它直接导致动量的瞬间变化。
- ΔP=FΔt\Delta P = F \Delta tΔP=FΔt (力乘以时间,或直接视为动量变化量)
- 扭矩 (Torque):使物体产生旋转的力矩。它直接导致角动量的瞬间变化。
- τ=r×F\tau = r \times Fτ=r×F (力臂与力的叉乘)
物理引擎通过这些概念来计算和更新物体的运动状态。例如,当施加一个力时,引擎会根据牛顿第二定律 F=maF = maF=ma 来计算加速度,然后用积分器更新速度和位置。当发生碰撞时,引擎会计算一个冲量,施加到碰撞的物体上,使其反弹或滑动。
3. 积分器(Integrators):连接现在与未来
积分器是物理引擎的“时间旅行者”,它们负责根据当前的速度和力,预测物体在下一个微小时间步长后的位置和速度。这是物理模拟的核心,因为我们需要连续地更新物体的状态。
-
欧拉积分 (Euler Integration):
- 原理: 最简单也是最直观的积分方法。它假设在整个时间步长内,速度或力是恒定的,并直接通过当前的速度和加速度来更新位置和速度。
- 公式:
- vnew=vcurrent+a⋅Δtv_{new} = v_{current} + a \cdot \Delta tvnew=vcurrent+a⋅Δt
- xnew=xcurrent+vcurrent⋅Δtx_{new} = x_{current} + v_{current} \cdot \Delta txnew=xcurrent+vcurrent⋅Δt
- 优点: 实现简单,计算速度快。
- 缺点: 精度最低,容易产生累积误差,导致能量损失或增加(物体可能会越弹越低或越弹越高),特别是在较大的时间步长下,可能导致数值不稳定。
- 示例: 在简单场景中可以接受,但在需要高精度模拟(如车辆、关节链)时则不够理想。
-
Verlet 积分 (Verlet Integration):
- 原理: 一种更稳定且能更好地保持能量的积分方法。它不直接使用速度,而是利用当前位置和上一帧位置来计算新位置。速度是隐式包含的。
- 公式(简化版):
- xnew=2xcurrent−xprevious+a⋅Δt2x_{new} = 2x_{current} - x_{previous} + a \cdot \Delta t^2xnew=2xcurrent−xprevious+a⋅Δt2
- 优点: 能量守恒性好,数值稳定性强,即使在较大时间步长下也不太容易发散。
- 缺点: 无法直接访问速度,如果需要速度信息(例如用于碰撞响应),需要额外计算。
- 示例: 常用于粒子系统、布料模拟等对稳定性要求高的场景。
-
龙格-库塔积分 (Runge-Kutta Integration - RK4):
- 原理: 一系列更高阶的积分方法,通过在时间步长内采样多个点来计算加权平均值,从而提高精度。RK4 是最常用的一种。
- 优点: 精度高,稳定性好,能够更准确地模拟复杂的物理系统。
- 缺点: 计算量远大于欧拉积分,因为需要在每个时间步内进行多次求值。
- 示例: 理论上最接近真实物理的积分方法,但在实时游戏中因性能开销大而较少直接用于大量刚体,更常见于需要高精度模拟的特定场景或离线物理模拟。
Unity PhysX 采用的积分器: 尽管 PhysX 的具体实现是专有的,但它通常使用经过优化的隐式欧拉积分(Implicit Euler)或类似的半隐式积分。这种方法结合了欧拉的简洁和一些稳定性优势,通过在迭代求解器中同时考虑当前和下一帧的状态来提高稳定性。它并不是单纯的显式欧拉积分。
4. 碰撞检测(Collision Detection):寻找接触点
碰撞检测是物理引擎最核心、最耗性能的部分之一。它分为两个阶段:宽相检测和窄相检测。
4.1. 宽相检测(Broad-Phase Collision Detection)
- 目的: 快速排除场景中绝大多数不可能发生碰撞的物体对。这一步不需要精确的计算,只需要找出“可能”碰撞的候选对。
- 核心思想: 使用粗略的包围体(Bounding Volume),如 AABB (Axis-Aligned Bounding Box),来近似表示物体的空间范围。如果两个 AABB 不重叠,那么它们内部的物体肯定不碰撞。
- 常用算法:
- AABB 树 (AABB Tree):一种层次化的数据结构。将场景中的所有 AABB 组织成一棵树。检测时,只需遍历树,如果父节点的 AABB 不重叠,则其子节点也不可能重叠,从而快速排除大量物体。这是 PhysX 广泛使用的技术。
- SAP (Sweep and Prune / Sort and Sweep):对所有物体在每个轴上的 AABB 投影进行排序。只有当两个物体的 AABB 在所有轴上都重叠时,才可能发生碰撞。通过维护排序列表,可以高效地找出重叠对。适用于物体数量不太多且运动方向较固定的场景。
- 网格/格子(Grids/Spatial Hashing):将空间划分为规则的网格单元。每个单元存储其中包含的物体。检测时只检查相邻或同一单元格内的物体。适用于物体均匀分布的场景。
4.2. 窄相检测(Narrow-Phase Collision Detection)
- 目的: 对宽相检测筛选出的“可能”碰撞对,进行精确的几何测试,判断它们是否真的发生了碰撞,并计算出详细的碰撞信息(接触点、法线、穿透深度等)。
- 核心思想: 使用更精确的几何算法来处理复杂形状。
- 常用算法:
- GJK (Gilbert-Johnson-Keerthi Distance Algorithm):
- 原理: GJK 算法并不是直接找到碰撞点,而是用来判断两个凸包(Convex Hull)是否相交。它通过迭代寻找两个物体之间的最短距离向量。如果这个最短距离为零或指向内,则表示它们相交。
- 优点: 效率高,适用于任意凸包形状,是现代物理引擎的核心。
- 缺点: 只能判断是否相交,不能直接得到碰撞点和法线。
- EPA (Expanding Polytope Algorithm):
- 原理: EPA 通常紧接着 GJK 之后使用。当 GJK 判断两个物体相交后,EPA 会从 GJK 找到的“穿透方向”开始,在闵可夫斯基差集(Minkowski Difference)中扩展一个多面体,直到找到最接近原点的那个面。这个面就定义了最小穿透向量(法线和深度)。
- 优点: 与 GJK 结合,可以准确地计算穿透深度和碰撞法线。
- SAT (Separating Axis Theorem - 分离轴定理):
- 原理: 对于两个凸体,如果它们不相交,那么一定存在一个轴,使得它们在这条轴上的投影互不重叠。反之,如果找不到这样的分离轴,则它们相交。对于多边形,需要测试所有边以及垂直于这些边的轴。
- 优点: 简单易懂,对于多边形(2D)和多面体(3D)非常有效。
- 缺点: 对于复杂形状,需要测试的轴可能很多,效率不如 GJK/EPA 组合。
- GJK (Gilbert-Johnson-Keerthi Distance Algorithm):
4.3. 接触生成 (Contact Generation)
- 一旦窄相检测确定了碰撞,下一步就是生成一个或多个接触点 (Contact Point)。每个接触点包含了:
- 位置 (Position):碰撞发生在世界空间中的坐标。
- 法线 (Normal):指向从一个物体到另一个物体的方向。这决定了碰撞后的反弹方向。
- 深度 (Separation/Penetration Depth):物体之间相互穿透的距离。这个信息对于约束求解器至关重要,因为它需要将物体推开。
- 对于复杂的碰撞,可能会生成多个接触点(例如,一个盒子平躺在地面上)。这些接触点会被收集起来,传递给下一步的约束求解器。
总结
至此,我们已经基本了解了 Unity 物理引擎的底层工作原理:从宏观的物理循环,到驱动物体运动的刚体动力学基础,再到连接过去与未来的积分器,以及寻找和确定碰撞点的宽相和窄相碰撞检测算法。
我们现在应该对物理引擎如何“看”到和“理解”我们的游戏世界有了更清晰的认识。下一篇,我们将继续深入物理引擎最复杂也是最精妙的部分:物理约束求解与稳定性,了解它是如何解决碰撞穿透、摩擦和关节限制等问题的。
Unity物理系统由浅入深第一节:Unity 物理系统基础与应用
Unity物理系统由浅入深第二节:物理系统高级特性与优化
Unity物理系统由浅入深第三节:物理引擎底层原理剖析
Unity物理系统由浅入深第四节:物理约束求解与稳定性