计算机图形学 · 曲面细分求交原理(Surface Subdivision & Ray Intersection)

🌌 计算机图形学中的曲面细分求交:一束光与无数三角形的浪漫碰撞

风格:图形学硬核脑洞 + 三维算法浪漫 + JavaScript 实战指南

技术栈:JavaScript、WebGL(或 Three.js)、数学(但不写公式!)

📜 序章:你看到的不是一个球体,而是无数三角形排成的诗

在图形学的世界里,一切美丽曲线都只能靠无数三角形"假装"出来。

无论是皮克斯的角色,还是 Blender 渲染的龙,它们都是由小小的面片堆叠构成的。

而当一束光(Ray)打向这些曲面时,我们的任务就是:

✨ 找到它穿透哪个三角形,它在哪儿停下,它撞击到了哪一个"伪曲线"上?

这就是**曲面细分求交(subdivision surface intersection)**的故事。


🧠 本质原理:曲面不能直接求交,只能被"片段化"

曲面(如 Bézier 曲面、NURBS、Subdivision Surface)是数学意义上的连续空间,

但 GPU 不喜欢连续曲线,它喜欢的是 离散的三角形网格

于是我们的解决方案是:

把曲面切成足够多的小三角形,然后让光线和这些三角形逐个比划

这种处理过程,就是:

  • 曲面细分(subdivision):将原始曲面分割成三角形网格
  • 逐面求交(ray-triangle intersection):暴力 or 空间优化后的逐个检测
  • 命中判断:谁距离最近,谁就是光的终点

🪓 第一步:曲面细分(Subdivide the Surface)

我们以 Catmull-Clark 或 Loop 等细分算法为代表:

🎯 输入:控制点(通常是一个粗糙的立方体或者面片网格)

🛠️ 过程:根据细分规则不断插值生成新点,形成更密的三角形

🧩 输出:一堆三角形,每个代表原曲面的"近似小片段"

在 JavaScript 中你可以使用 three-subdivide 或者自己写个递归细分函数(以下是简化模拟版):

js 复制代码
function subdivideTriangle(v0, v1, v2, depth) {
  if (depth === 0) return [[v0, v1, v2]]

  const mid = (a, b) => [
    (a[0] + b[0]) / 2,
    (a[1] + b[1]) / 2,
    (a[2] + b[2]) / 2,
  ]

  const a = mid(v0, v1)
  const b = mid(v1, v2)
  const c = mid(v2, v0)

  return [
    ...subdivideTriangle(v0, a, c, depth - 1),
    ...subdivideTriangle(a, v1, b, depth - 1),
    ...subdivideTriangle(b, v2, c, depth - 1),
    ...subdivideTriangle(a, b, c, depth - 1),
  ]
}

🌟 注:真正的 Catmull-Clark 是基于网格拓扑的重构,这里是简化思路。


🎯 第二步:光线与三角形求交(Ray-Triangle Intersection)

曲面被片段化为三角形后,我们就可以让光线大显身手了。

核心任务:

  • 将光线定义为:一个起点 + 一个方向
  • 将每个三角形定义为:三个顶点
  • 使用 Möller--Trumbore 算法(经典,快速,线性代数友好)判断交点
scss 复制代码
function rayIntersectsTriangle(rayOrigin, rayDir, v0, v1, v2) {
  const EPSILON = 0.000001

  const edge1 = subtract(v1, v0)
  const edge2 = subtract(v2, v0)
  const h = cross(rayDir, edge2)
  const a = dot(edge1, h)

  if (Math.abs(a) < EPSILON) return null // 平行,无交点

  const f = 1.0 / a
  const s = subtract(rayOrigin, v0)
  const u = f * dot(s, h)

  if (u < 0.0 || u > 1.0) return null

  const q = cross(s, edge1)
  const v = f * dot(rayDir, q)

  if (v < 0.0 || u + v > 1.0) return null

  const t = f * dot(edge2, q)
  if (t > EPSILON) {
    return {
      point: add(rayOrigin, scale(rayDir, t)),
      distance: t,
      barycentric: { u, v, w: 1 - u - v }
    }
  }

  return null
}

💡 数学避免:这里涉及向量点乘、叉乘等操作,不展示公式,建议封装工具函数。


🧩 第三步:全部检测 + 最近命中

csharp 复制代码
function findClosestIntersection(rayOrigin, rayDir, triangles) {
  let closest = null
  for (const [v0, v1, v2] of triangles) {
    const hit = rayIntersectsTriangle(rayOrigin, rayDir, v0, v1, v2)
    if (hit && (!closest || hit.distance < closest.distance)) {
      closest = hit
    }
  }
  return closest
}

你可以理解成光线穿越了宇宙,它想找到第一个"撞上"的三角形。


🧠 性能优化 tips

✅ 空间加速结构(加快遍历)

  • Bounding Volume Hierarchy(BVH)
  • Uniform Grid / Octree / KD-Tree

这些结构能快速"剔除不可能被光线命中的面片",只检测相关部分。

✅ 降低细分层级(别太密)

  • 如果光线击中远处模型,可以使用低精度曲面
  • 分层细分 + 局部精细,是大模型处理的关键手段

🌄 实用场景举例

场景 应用价值
Path Tracing 渲染器 光线追踪到曲面细节
点击拾取模型(Ray Picking) 精准找到用户点击的位置
物理模拟(碰撞检测) 高精度碰撞判断
NURBS 曲面可视化 将数学曲线可视化为网格
WebGL 游戏/艺术交互 用户操控三维曲面交互元素

💬 总结:光线与三角的对话,构成了 3D 世界的真实感

在三维世界的每一帧图像背后,其实是无数三角形与无数光线的对话

  • 曲面,只是三角形的幻术
  • 光线,只是世界理解你的方式
  • 求交,就是两个世界的碰撞------用户的目光,和模型的灵魂

✨ "当你凝视一个曲面时,那个曲面也在等待你的光线打在它身上。"

------ 某位图形学诗人


🔧 附加:你可以进一步尝试

  • 使用 WebGL 渲染交点位置,并高亮显示
  • 将细分曲面动态加载(LOD)
  • 与物理引擎结合,做真实形变物体碰撞

📚 延伸阅读


🚀 愿你用每一束光,照亮三角片的边缘,看见数字世界中,模拟出的每一寸真实。

相关推荐
_丿丨丨_3 小时前
XSS(跨站脚本攻击)
前端·网络·xss
天天进步20153 小时前
前端安全指南:防御XSS与CSRF攻击
前端·安全·xss
呼啦啦呼啦啦啦啦啦啦4 小时前
利用pdfjs实现的pdf预览简单demo(包含翻页功能)
android·javascript·pdf
拾光拾趣录5 小时前
括号生成算法
前端·算法
拾光拾趣录6 小时前
requestIdleCallback:让你的网页如丝般顺滑
前端·性能优化
前端 贾公子6 小时前
vue-cli 模式下安装 uni-ui
前端·javascript·windows
拾光拾趣录6 小时前
链表合并:双指针与递归
前端·javascript·算法
@大迁世界6 小时前
前端:优秀架构的坟墓
前端·架构
拼图2096 小时前
element-plus——图标推荐
javascript·vue.js·elementui
期待のcode7 小时前
图片上传实现
java·前端·javascript·数据库·servlet·交互