🌌 计算机图形学中的曲面细分求交:一束光与无数三角形的浪漫碰撞
风格:图形学硬核脑洞 + 三维算法浪漫 + 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)
- 与物理引擎结合,做真实形变物体碰撞
📚 延伸阅读
- Real-Time Rendering, 4th Edition
- Möller-Trumbore Algorithm
- three-mesh-bvh (用于加速求交)
- NURBS / Bézier / Catmull-Clark 图形学算法
🚀 愿你用每一束光,照亮三角片的边缘,看见数字世界中,模拟出的每一寸真实。