因为常规帧同步需要使用确定性的物理碰撞,而Unity自带的物理引擎PhysX由于浮点数的平台问题,所以借着实现帧同步的过程中,顺便熟悉了一下基础的物理碰撞及相关实现。
常规来说,碰撞的核心就是检测两物体是否相交,2D情况如此,3D情况亦是如此。如果要实现3D碰撞检测,就需要通过数学方法来求出两者相交的情况。那么这个问题简化一下,其实就是一个物体的边界相对于另外一个物体的边界入侵的深度有多深。
所以基于各种形状的物体,这个物体很快可以有两种解题思路。一种是通过两者最大包围盒的坐标,获取物体边界多个顶点的“平均”边界值,另外一种就是简单粗暴,直接按半径来进行“平均”边界值的获取。基于这两种思路,于是就有了常规的AABB包围盒碰撞和Sphere球形碰撞两种计算方式。
Spheres(球形碰撞器)
在物理模拟和3D图形中,球形碰撞器(Sphere Collider)是一种简单的碰撞形状,用于检测和处理物体之间的碰撞。球形碰撞器通常用于表示那些可以近似为球形的物体,如球、弹珠、某些人物的头部等。
球形碰撞器的实现原理相对简单,主要基于球体的数学属性和物理规则。以下是球形碰撞器的一些关键特性和实现要点:
- 形状定义:
- 球形碰撞器由一个中心点(通常也是物体的质心)和一个半径定义。这个半径代表了球体的半径,决定了球形碰撞器的体积和边界。
- 碰撞检测:
- 当两个球形碰撞器可能发生碰撞时,需要计算它们之间的距离,通常是它们中心点之间的距离。
- 如果这个距离小于两个球体半径之和,那么这两个球形碰撞器就发生了碰撞。
- 相交测试:
- 球形碰撞器的相交测试非常直观。只需比较两个球体中心点之间的距离和它们的半径之和。
- 如果距离小于或等于半径之和,则球体相交;否则,它们不相交。
- 碰撞响应:
- 一旦检测到碰撞,就需要应用物理规则来计算碰撞响应。这通常包括计算碰撞力、改变物体的速度和方向等。
- 球形碰撞器之间的碰撞响应通常相对简单,因为它们只有一个接触点,而且接触点的法线方向很容易确定(即连接两个球心的向量)。
public class Sphere
{
public Vector3 Center { get; set; } // 球体的中心点
public float Radius { get; set; } // 球体的半径
// 构造函数
public Sphere(Vector3 center, float radius)
{
Center = center;
Radius = radius;
}
// 检测与另一个球体是否发生碰撞
public bool CollidesWith(Sphere other)
{
// 计算两个球体中心点之间的距离
float distance = (Center - other.Center).Length();
// 如果距离小于或等于两个球体半径之和,则发生碰撞
return distance <= (Radius + other.Radius);
}
}
AABB(Axis-Aligned Bounding Box,轴对齐包围盒)
AABB包围盒是一种简单、高效和稳定的碰撞算法,其通过一个与坐标轴对齐的长方体,完全包含所要检测碰撞的物体。这个包围盒的边界与坐标轴平行,它拥有六个面,每个面都是矩形,且每个矩形的边都与坐标轴平行。在碰撞检测时,只需要检测两个物体的AABB包围盒是否相交,如果不相交,则两个物体一定没有碰撞。
其算法实现也相当简单
public struct AABB
{
public Vector3 min; // 包围盒的最小坐标(左下角)
public Vector3 max; // 包围盒的最大坐标(右上角)
public AABB(Vector3 min, Vector3 max)
{
this.min = min;
this.max = max;
}
// 检查两个AABB包围盒是否相交
public static bool Intersects(AABB a, AABB b)
{
// 如果一个包围盒在另一个包围盒的左侧、右侧、上方、下方、前面或后面,则它们不相交
if (a.max.x < b.min.x || a.min.x > b.max.x ||
a.max.y < b.min.y || a.min.y > b.max.y ||
a.max.z < b.min.z || a.min.z > b.max.z)
{
return false;
}
// 否则,它们相交
return true;
}
}
从以上代码上可以看到,在进行大规模碰撞运算的时候,其整体运算开销很小
所以这也带来了AABB的优势
- 简单性:AABB包围盒的生成和相交测试都非常简单,计算量小,适合用于大规模的碰撞检测。
- 高效性:由于AABB包围盒与坐标轴对齐,因此其相交测试可以通过简单的比较运算来实现,非常高效。
- 稳定性:AABB包围盒不受物体旋转的影响,即使物体发生旋转,AABB包围盒也不需要更新。
所以在有一定形状刻画要求的物体上,包围盒的制作思路会更好的匹配物体的碰撞
但AABB在实际应用中也有其问题,如斜坡等形状的匹配上。由于AABB包围盒不受旋转影响,使得在斜面情况下,不能很好的匹配物体的碰撞形状。如下图所示
(如果我们想要表现一段上斜坡的动作的话,那显而易见这种碰撞形状是不可接受的)
所以虽然AABB有各种优势,但是在实际的应用中,它并不是主流。
为了解决这种问题,于是之后大家就引入了另外一种碰撞盒计算模式OBB
OBB(Oriented Bounding Box,有向包围盒)
原理:
OBB包围盒是一个与坐标轴不对齐的长方体,它的方向可以任意旋转,以更好地适应物体的形状。与AABB包围盒相比,OBB包围盒可以更加紧密地包围物体,从而减少不必要的碰撞检测。OBB包围盒通常通过物体的顶点信息计算得出,其边界不与坐标轴平行,而是与物体的主要方向对齐。在碰撞检测时,需要检测两个物体的OBB包围盒是否相交,如果相交,则需要进行更精确的碰撞检测。
优势:
- 紧密性好:OBB包围盒可以更加紧密地包围物体,减少了不必要的碰撞检测,提高了碰撞检测的效率。
- 适用于变形体:由于OBB包围盒的方向可以任意旋转,因此当物体发生旋转或变形时,只需要更新OBB包围盒的方向和大小,而不需要像AABB包围盒那样进行频繁的更新。
- 实时性高:由于OBB包围盒的紧密性好,可以大大减少参与相交测试的包围盒数量,从而提高了碰撞检测的实时性。
具体实现原理
- 计算主轴:
- 首先,需要确定物体的主轴。这些轴是物体形状的主要方向,通常通过计算物体的协方差矩阵并找到其特征向量来得到。特征向量代表了物体形状的主要方向,即方差最大的方向。
- 协方差矩阵是一个3x3的矩阵,描述了物体顶点在三个坐标轴方向上的变化程度。通过对协方差矩阵进行特征值分解,可以得到三个特征向量,它们分别代表了物体形状的主轴。
- 定义OBB:
- 一旦得到了主轴,就可以定义一个与物体形状更紧密对齐的长方体,这就是OBB。OBB的中心点通常是物体的质心(所有顶点坐标的平均值),而OBB的半长度(即每个轴上的半径)则是根据主轴和物体顶点计算得出的。
- OBB的方向由主轴确定,这意味着OBB可以随着物体的旋转而旋转,以保持与物体形状的紧密对齐。
- 相交测试:
- 在进行碰撞检测时,需要判断两个OBB是否相交。这通常通过分离轴定理(Separating Axis Theorem, SAT)来实现。SAT的基本思想是将两个OBB投影到它们各自的主轴上,并检查这些投影是否有重叠。如果在所有主轴上的投影都没有重叠,那么两个OBB就不相交。
- 具体来说,对于每个主轴,计算两个OBB在该轴上的投影区间,并检查这些区间是否有交集。如果有任何一个主轴上的投影区间没有交集,那么两个OBB就不相交。否则,它们可能相交,需要进一步检测。
以下是代码实现
[RequireComponent(typeof(Transform), typeof(MeshFilter))]
public class OBB : MonoBehaviour
{
private Vector3 _center = Vector3.zero;
public Vector3 Center
{
get
{
_center = transform.position;
return _center;
}
private set => _center = value;
}
private Vector3[] _axis = null;
public Vector3[] Axis
{
get
{
if (_axis == null)
_axis = new Vector3[3];
_axis[0] = transform.right;
_axis[1] = transform.up;
_axis[2] = transform.forward;
return _axis;
}
}
private FixedVector3 _extents = null;
public FixedVector3 Extents
{
get
{
if (_extents == null)
{
_extents = new FixedVector3((int)(transform.localScale.x * 500), (int)(transform.localScale.y * 500), (int)(transform.localScale.z * 500));
}
return _extents;
}
}
public bool Intersects(OBB other, out Vector3 intersectionDepth, out Vector3 axis)
{
axis = Vector3.zero;
Vector3[] axes = new Vector3[]
{
Axis[0],
Axis[1],
Axis[2],
other.Axis[0],
other.Axis[1],
other.Axis[2],
Vector3.Cross(Axis[0], other.Axis[0]),
Vector3.Cross(Axis[0], other.Axis[1]),
Vector3.Cross(Axis[0], other.Axis[2]),
Vector3.Cross(Axis[1], other.Axis[0]),
Vector3.Cross(Axis[1], other.Axis[1]),
Vector3.Cross(Axis[1], other.Axis[2]),
Vector3.Cross(Axis[2], other.Axis[0]),
Vector3.Cross(Axis[2], other.Axis[1]),
Vector3.Cross(Axis[2], other.Axis[2])
};
intersectionDepth = Vector3.zero;
foreach (Vector3 ax in axes)
{
if (NoOverlapOnAxis(this, other, ax))
{
return false;
}
else
{
Vector3 depth = GetIntersectionDepth(this, other, ax);
if (depth == Vector3.zero) continue; //碰撞深度为0,说明没有碰撞
if (depth.magnitude < intersectionDepth.magnitude || intersectionDepth == Vector3.zero)
{
intersectionDepth = depth;
axis = ax;
}
}
}
return true;
}
private bool NoOverlapOnAxis(OBB obb, OBB other, Vector3 ax)
{
Vector3[] obbVertices = obb.GetVertices();
Vector3[] otherVertices = other.GetVertices();
double obbMin = Vector3.Dot(obbVertices[0], ax.normalized);
double obbMax = obbMin;
foreach (var vertex in obbVertices)
{
var projection = Vector3.Dot(vertex, ax);
obbMin = Math.Min(obbMin, projection);
obbMax = Math.Max(obbMax, projection);
}
double otherMin = Vector3.Dot(otherVertices[0], ax.normalized);
double otherMax = otherMin;
foreach (var vertex in otherVertices)
{
var projection = Vector3.Dot(vertex, ax);
otherMin = Math.Min(otherMin, projection);
otherMax = Math.Max(otherMax, projection);
}
return obbMax < otherMin || obbMin > otherMax;
}
private Vector3 GetIntersectionDepth(OBB obb, OBB other, Vector3 ax)
{
Vector3[] obbVertices = obb.GetVertices();
Vector3[] otherVertices = other.GetVertices();
float obbMin = Vector3.Dot(obbVertices[0], ax);
float obbMax = obbMin;
foreach (var vertex in obbVertices)
{
var projection = Vector3.Dot(vertex, ax);
obbMin = Math.Min(obbMin, projection);
obbMax = Math.Max(obbMax, projection);
}
float otherMin = Vector3.Dot(otherVertices[0], ax);
float otherMax = otherMin;
foreach (var vertex in otherVertices)
{
var projection = Vector3.Dot(vertex, ax);
otherMin = Math.Min(otherMin, projection);
otherMax = Math.Max(otherMax, projection);
}
float overlap = Math.Min(obbMax, otherMax) - Math.Max(obbMin, otherMin);
return overlap > 0 ? ax * overlap : Vector3.zero;
}
private List<Vector3> _boundsVertices = null;
private Vector3[] GetVertices()
{
if (_boundsVertices == null)
{
_boundsVertices = new List<Vector3>();
var vertices = transform.GetComponent<MeshFilter>().mesh.vertices;
for (int i = 0; i < vertices.Length; i++)
{
var point = transform.TransformPoint(vertices[i]);
_boundsVertices.Add(point);
}
}
return _boundsVertices.ToArray();
}
}
最终实现的物理碰撞效果