------ 无分支的二次方程光线求交法
"所有的相遇,都是宇宙精心编排的抛物线。"
📜 引子:光线追踪的第一课
在计算机图形学这个瑰丽的世界里,**光线追踪(Ray Tracing)**堪称魔法。它让每一道光都带着目的,从眼睛出发,穿越像素宇宙,寻找命中注定的物体。
而最经典、最具象征意义的求交对象是什么?不是复杂的网格,不是布娃娃物理,而是------一个球。
在这节课里,我们不需要 if
、没有 branch
、更不靠 try-catch,全靠数学的优雅和你我的浪漫。
🧠 背后原理:球是什么?
从几何学角度讲,一个球体就是一个三维空间中的完美之物。它有:
- 中心点
C
- 半径
r
而一条光线,也不过是这样一个表达:
- 起点
O
- 方向
D
(已归一化)
我们要做的事情很简单却不平凡:判断这条光线是否会命中这个球,如果命中,在哪个"时刻" t
发生了。
🔢 数学转译:二次方程
别担心,我们不会给你写出血淋淋的公式。但你要知道:
- 把光线代入球的隐式方程。
- 整理一下,就能得到一个关于 t 的二次方程。
这就像你把某人定位到了地图上,然后问:"我照着这方向走,会不会撞上他?"
🛠️ 实战代码:无分支的 JS 实现
ini
function intersectRaySphere(rayOrigin, rayDir, sphereCenter, sphereRadius) {
const L = {
x: rayOrigin.x - sphereCenter.x,
y: rayOrigin.y - sphereCenter.y,
z: rayOrigin.z - sphereCenter.z,
};
const a = 1; // 因为 rayDir 是单位向量
const b = 2 * (L.x * rayDir.x + L.y * rayDir.y + L.z * rayDir.z);
const c = L.x * L.x + L.y * L.y + L.z * L.z - sphereRadius * sphereRadius;
const discriminant = b * b - 4 * a * c;
const noHit = discriminant < 0;
const sqrtD = Math.sqrt(Math.max(discriminant, 0)); // 关键:避免分支
// 两个可能的解
const t0 = (-b - sqrtD) / 2;
const t1 = (-b + sqrtD) / 2;
// 使用最小的正解(无条件选最大0和两个t中的最小值)
const t = Math.min(t0, t1);
return {
hit: !noHit && t >= 0,
t: t >= 0 ? t : Math.max(t0, t1),
};
}
🚫 无 if
的美学
传统实现中,我们可能会这样写:
kotlin
if (discriminant < 0) return null;
但分支是GPU性能杀手。特别是并行处理大量像素时,每一个分支判断都可能让着色器慢如蜗牛。我们用 Math.max(discriminant, 0)
来避免 if
,并自然让 sqrtD
为 0
时代表"miss"。
GPU 上这种 无分支逻辑 可以极大提升性能,尤其是在现代 WebGL 或 WebGPU 管线中。
💡 彩蛋:为什么这样求就够了?
你可能想问:"不判断 t 是负的怎么办?"
答案是我们始终返回"最近的正 t",它对应了可视的交点。如果都为负,说明射线从球内向外或者根本背向球,我们依然可以无分支地处理它。
这就是代数的优雅,用极简的数学避开程序上的繁杂岔路。
🌈 小结:一发入魂
- 光线和球的求交,本质就是解一个二次方程。
- 没有
if
,只有代数。 - 使用
Math.max
、Math.min
替代逻辑判断。 - 无分支计算不仅美观,更能提高 GPU 性能。
✨ 尾声:诗意收场
"光线行走于虚空之中,带着目的而生。"
"当它与球体邂逅,不是奇迹,而是数学。"
这是计算机图形学的魅力。下一次你再点亮一个像素,记得:它背后可能是一行优雅的代码、一次无声的相遇。