"一颗顶点,一张面,在四维时空里逐渐靠近,是否会碰撞?还是终将错过?"
🌌 引子:CCD 是什么?
连续碰撞检测(Continuous Collision Detection,简称 CCD) ,不是《漫威宇宙》的新组织,而是一个避免"穿模"现象的高级物理机制。
- 普通的碰撞检测通常每帧检测一次位置,这叫 离散碰撞检测(DCD) ,容易错过高速运动中的碰撞。
- CCD 则细致到每一帧之间的连续路径,确保即便对象高速飞驰,也不会偷偷"穿过"墙壁,毁了游戏体验。
今天我们聊的是 CCD 中的经典问题:
在一个时间区间
[t0, t1]
内,一个移动的点 是否会撞上一张移动的三角面?
🧩 问题建模
想象你是一个顶点粒子,正在动感单车般穿越三维空间。与此同时,远处有一张缓缓移动的三角形大网,像三只冷酷无情的手指等待你落入陷阱。
我们需要求的是:有没有一个时刻 t
(0 到 1 之间),你刚好撞在这张三角网格上?
📚 线性插值(Lerp)建模运动轨迹
我们假设顶点 P(t) 和三角形三个顶点 A(t), B(t), C(t) 都是线性运动的:
javascript
function lerp(v0, v1, t) {
return v0.map((v, i) => v + (v1[i] - v) * t);
}
在任意时间 t
:
P(t) = lerp(P0, P1, t)
A(t), B(t), C(t)
同理
🎯 顶点-面碰撞的数学条件(非公式解释)
顶点会撞上三角形面,等价于:
- 点 P(t) 落在 三角形 A(t), B(t), C(t) 所在平面上。
- P(t) 也位于该三角形内部(使用重心坐标检测)。
因此我们等价转化为:
求解一个函数 f(t),它表示 P(t) 和三角形面在时间 t 是否共面,当 f(t) = 0 时,即发生碰撞。
🔍 解方程:用牛顿法抓住那一瞬的命运
什么是牛顿法?
牛顿法是数值分析界的"福尔摩斯",专门用来寻找某个函数为零的点。给定一个初始猜测 t0
,不断更新:
scss
t_next = t - f(t) / f'(t)
只要选得好,几轮就能收敛!
✂️ + 区间裁剪(Interval Clipping)
为了避免牛顿法跳出 [0,1]
区间,我们在每轮迭代中做如下处理:
- 如果
t
超出了[t0, t1]
,我们就把t
强行剪回来。 - 并在迭代过程中维护一个收缩区间,像套娃一样越来越小,直到逼近根。
🧪 JavaScript 实现
ini
function dot(a, b) {
return a.reduce((sum, ai, i) => sum + ai * b[i], 0);
}
function cross(a, b) {
return [
a[1]*b[2] - a[2]*b[1],
a[2]*b[0] - a[0]*b[2],
a[0]*b[1] - a[1]*b[0]
];
}
function subtract(a, b) {
return a.map((v, i) => v - b[i]);
}
function scalarTriple(a, b, c) {
return dot(a, cross(b, c));
}
// f(t): 点与面之间的有符号体积(应为0)
function f(t, P0, P1, A0, A1, B0, B1, C0, C1) {
const P = lerp(P0, P1, t);
const A = lerp(A0, A1, t);
const B = lerp(B0, B1, t);
const C = lerp(C0, C1, t);
return scalarTriple(subtract(P, A), subtract(B, A), subtract(C, A));
}
function df(t, eps, ...args) {
return (f(t + eps, ...args) - f(t - eps, ...args)) / (2 * eps);
}
function findRoot(P0, P1, A0, A1, B0, B1, C0, C1, maxIter = 20, eps = 1e-6) {
let t0 = 0.0;
let t1 = 1.0;
let t = 0.5;
for (let i = 0; i < maxIter; i++) {
const ft = f(t, P0, P1, A0, A1, B0, B1, C0, C1);
const dft = df(t, 1e-5, P0, P1, A0, A1, B0, B1, C0, C1);
if (Math.abs(ft) < eps) return t;
if (dft === 0) {
// 牛顿法无法推进,改为二分
t = (t0 + t1) / 2;
} else {
const t_next = t - ft / dft;
if (t_next < t0 || t_next > t1) {
t = (t0 + t1) / 2;
} else {
t = t_next;
}
}
// 更新搜索区间
if (f(t, P0, P1, A0, A1, B0, B1, C0, C1) > 0) {
t1 = t;
} else {
t0 = t;
}
}
return null; // 未找到碰撞点
}
🧠 思维总结
- CCD 就像是在"时间"这条轴上进行几何运算。
- 用线性插值来表示时间参数化运动。
- 牛顿法 + 区间裁剪,就像戴着安全带开快车,快速又稳当。
f(t)
是你和命运的距离,f'(t)
是你的速度方向,组合起来,找到你是否真的"撞墙"。
🏁 彩蛋:什么是"穿模"的哲学本质?
"穿模"其实是一个程序世界里的因果悖论:你本应该撞上去,但因为时间间隔太大,你直接"跳过"了那一帧。CCD,就是在帮你重新定义命运,让每一次物理接触都无所遁形。
✨ 下一步
- 加入重心坐标检测,判断点是否落在三角形内(非仅共面)。
- 支持面-面、边-边 CCD 检测。
- 使用双精度 + 自适应精度(Shewchuk)避免数值不稳定。
如果你觉得这是一段严肃的数值计算,不妨换个角度想:
"在数字世界里,每一个点的移动,都是一场寻找归属感的旅程。"
如需更复杂版本(含边界检测、鲁棒数值包),欢迎继续追问!
是否需要我也把 重心坐标检测 部分和完整 CCD 判定逻辑封装成一个模块?