Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

接触生成与流形

当窄相碰撞检测算法(如SAT或GJK/EPA)报告两个物体发生碰撞,并给出了穿透深度和法线后,我们还需要一个至关重要的信息才能进行真实的物理响应:接触点 (Contact Points)。一个单一的穿透向量不足以描述复杂的接触情况,例如一个箱子平放在地面上。在这种情况下,接触实际上发生在整个底面上,而不仅仅是一个点。为了稳定地模拟这种平面、线或多点接触,现代物理引擎引入了接触流形 (Contact Manifold) 的概念。

接触流形是一个数据结构,它封装了两个物体之间一次完整接触的所有几何信息。它不仅包含了一到多个接触点,还负责在连续的几个时间步内追踪和更新这些接触点,以实现稳定、平滑的碰撞响应和摩擦效果。

为什么单点接触不够?

想象一个长方形箱子落在平坦的地面上。在理想情况下,它们应该在箱子底部的四个角都产生接触。如果我们只使用由EPA等算法计算出的单一最深穿透点作为接触点,会发生什么?

  1. 不稳定和抖动: 引擎会在该点施加一个分离冲量。由于冲量作用点可能不在质心正下方,这会产生一个力矩,导致箱子轻微旋转。在下一帧,箱子的另一个角可能会成为最深穿透点,引擎又在该点施加冲量,导致箱子向反方向旋转。如此往复,箱子就会在地面上不停地抖动或摇摆,永远无法稳定地静止下来。
  2. 不真实的摩擦: 摩擦力的计算与法向力(正压力)密切相关。如果只有一个接触点,我们就无法模拟出分布在整个接触面上的、能抵抗旋转的摩擦力矩。

接触流形 (Contact Manifold)

为了解决这些问题,我们引入接触流形。对于每一对发生碰撞的物体,我们都创建一个接触流形来描述它们之间的交互。这个数据结构通常包含:

  • 接触点数组: 通常是一个包含1到4个接触点的小数组。4个点足以稳定地表示任何两个凸体之间的平面接触。
  • 接触法线 (Normal): 描述接触面的方向。
  • 参与的物体: 指向两个碰撞物体的指针。
  • 摩擦与恢复系数: 用于该次接触的综合物理参数。

每个接触点自身也是一个结构体,包含:

  • 世界空间位置 (Position): 接触点在世界坐标系中的位置。
  • 局部空间位置 (Local Position): 接触点在两个物体各自局部坐标系中的位置。
  • 穿透深度 (Penetration Depth): 该点的穿透深度。
  • 法向冲量累积 (Normal Impulse): 在求解器中累积的用于分离的冲量。
  • 切向冲量累积 (Tangent Impulse): 用于摩擦的冲量。
  • 生命周期 (Lifetime): 用于追踪该接触点持续了多少个时间步。

流形的生命周期管理

接触流形的强大之处在于它的时间相干性。在一个时间步结束时,我们不清空所有接触信息,而是尝试将它们“缓存”到下一帧。

  1. 更新: 在新的一帧,对于上一帧就存在的物体对,我们不创建新的流形,而是更新旧的流形。我们根据物体的运动变换旧的接触点位置,并检查它们是否仍然有效(例如,是否还在物体表面附近)。
  2. 添加: 计算新的接触点,并与流形中已有的点进行比较。如果是一个真正的新接触点,就将其加入流形。如果流形已满(例如,已有4个点),则需要根据某种策略(如保留最深的点,或保留能形成最大接触面积的点)来替换掉一个旧点。
  3. 移除: 如果一个旧的接触点在新的一帧中已经分离,或者距离新的接触位置太远,就将其从流形中移除。

通过这种方式,一个稳定的接触(如箱子在地面上)其接触流形中的点会保持相对稳定,这使得求解器可以累积冲量,从而让物体平稳地静止下来。这就是所谓的**“暖启动” (Warm Starting)**,它是实现高性能、高稳定性物理引擎的关键技术。


接触点的生成算法

如何从两个重叠的凸体中计算出合理的多个接触点呢?

1. 特殊形状对

对于球体、胶囊体等,接触点可以直接解析计算,通常只有一个或两个点。

2. 多边形-多边形接触:裁剪 (Clipping)

对于两个凸多面体(例如,两个方块)的接触,最经典和鲁棒的方法是Sutherland-Hodgman 裁剪算法

思想: 当一个物体(称为“入射体”,Incident Body)的一个面与另一个物体(称为“参考体”,Reference Body)的一个面发生碰撞时,真实的接触区域是这两个面在空间中重叠的部分。我们可以通过用参考体的侧面(由参考面的边和接触法线构成的平面)去“裁剪”入射体的面来得到这个接触区域。

算法流程 (面-面接触):

  1. 确定参考面和入射面: 首先,需要确定哪个是参考面,哪个是入射面。一个常见的启发式规则是:选择在接触法线方向上“最不陡峭”或“最垂直”于法线的那个面作为参考面。
  2. 获取入射面的顶点: 得到入射面的所有顶点,构成一个多边形。
  3. 构建裁剪平面: 参考面的每一条边,都与接触法线一起定义了一个裁剪平面。这些平面构成了参考面所在棱柱的侧面。
  4. 迭代裁剪: 依次使用每个裁剪平面去裁剪入射面多边形。Sutherland-Hodgman 算法一次处理一个裁剪平面,输入一个顶点列表,输出一个新的、被裁剪后的顶点列表。
  5. 输出接触点: 经过所有裁剪平面裁剪后,最终剩下的顶点就是位于两个物体内部的、代表真实接触区域的多边形顶点。我们还需要将这些点投影到接触平面上,并剔除掉那些穿透深度为正(即已经分离)的点。

代码示例 (裁剪伪代码):

std::vector<Point> clip(const std::vector<Point>& subjectPolygon, const Plane& clipPlane) {
    std::vector<Point> outputList;
    for (int i = 0; i < subjectPolygon.size(); ++i) {
        Point currentPoint = subjectPolygon[i];
        Point prevPoint = subjectPolygon[(i + subjectPolygon.size() - 1) % subjectPolygon.size()];

        // 测试当前点和前一个点相对于裁剪平面的位置
        float dist_curr = clipPlane.distanceTo(currentPoint);
        float dist_prev = clipPlane.distanceTo(prevPoint);

        if (dist_curr <= 0) { // 当前点在平面内侧
            if (dist_prev > 0) { // 前一个点在外侧,说明边与平面相交
                // 计算交点并加入输出
                outputList.add(intersection(prevPoint, currentPoint, clipPlane));
            }
            outputList.add(currentPoint); // 加入当前点
        } else if (dist_prev <= 0) { // 当前点在外侧,但前一个点在内侧
            // 计算交点并加入输出
            outputList.add(intersection(prevPoint, currentPoint, clipPlane));
        }
    }
    return outputList;
}

总结

接触生成与流形管理是连接碰撞检测和碰撞响应的关键桥梁,是实现稳定物理模拟的“幕后英雄”。它解决了单点接触带来的不稳定性问题,通过生成和追踪多个接触点,使得引擎能够稳健地处理平面接触(如堆叠)和复杂的摩擦现象。

虽然 GJK/EPA 等算法能告诉我们“是否”以及“多深”地碰撞了,但只有通过诸如 Sutherland-Hodgman 裁剪这样的接触生成算法,我们才能得到描述“如何”接触的丰富几何信息。对接触流形进行细致的生命周期管理,并利用“暖启动”技术,是现代高性能物理引擎实现稳定、高效和可信物理行为的核心秘诀。