🚀 顶点-面碰撞检测之诗:用牛顿法追寻命运的交点

"一颗顶点,一张面,在四维时空里逐渐靠近,是否会碰撞?还是终将错过?"


🌌 引子: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) 同理

🎯 顶点-面碰撞的数学条件(非公式解释)

顶点会撞上三角形面,等价于:

  1. 点 P(t) 落在 三角形 A(t), B(t), C(t) 所在平面上。
  2. 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 判定逻辑封装成一个模块

相关推荐
小杨同学yx10 分钟前
前端三剑客之Css---day3
前端·css
星月心城1 小时前
Promise之什么是promise?(01)
javascript
二川bro1 小时前
第二篇:Three.js核心三要素:场景、相机、渲染器
开发语言·javascript·数码相机
Mintopia2 小时前
🧱 用三维点亮前端宇宙:构建你自己的 Three.js 组件库
前端·javascript·three.js
故事与九2 小时前
vue3使用vue-pdf-embed实现前端PDF在线预览
前端·vue.js·pdf
小西↬2 小时前
vite+vue3+websocket处理音频流发送到后端
javascript·websocket·音视频
wb1893 小时前
企业WEB应用服务器TOMCAT
运维·前端·笔记·tomcat·云计算
烛阴3 小时前
解锁 Gulp 的潜力:高级技巧与工作流优化
前端·javascript
Entropy-Lee4 小时前
JavaScript 语句和函数
开发语言·前端·javascript