计算机图形学 · 曲面细分求交原理(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)
  • 与物理引擎结合,做真实形变物体碰撞

📚 延伸阅读


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

相关推荐
鬼手点金12 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
柳杉12 小时前
借助 Codex 和可视化工作流,从调研到完整实现,3D 大屏竟然可以这么简单
前端·ai编程·数据可视化
IMPYLH12 小时前
HTML 的 <wbr> 元素
前端·html
Dovis(誓平步青云)12 小时前
常用命令藏得太深?怎么做一个快捷命令坞
运维·前端·自动化·音视频·prometheus
IT_陈寒13 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信13 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端
蜗牛互联网13 小时前
Python Agent工具调用的白名单、参数校验与审批门禁
java·服务器·前端·人工智能·后端·python
计算机魔术师14 小时前
Perplexity:每个 Project 都是一个 Git 仓库
前端
Andrew55114 小时前
让 AI 写页面不再“一眼假”:我把设计品味做成了一个 Claude Code Skill 流水线
前端
乐享qicheng14 小时前
14-AI智能管家JSBridge深度问答
前端