早上来公司调试一个交互项目,同事逮住我就问:为什么从手柄发射出去的那条射线,打在左侧按钮上一打一个准,打到右边UI上却像穿过了空气?我扫了一眼代码,用的都是同一个函数,射线起点、方向也看不出毛病。这种“时而命中、时而miss”的现象,在“发射射线,判断是否存在交点”这类几何计算里特别常见。简单说,射线求交就是从某个起点出发,沿一个方向找有没有物体挡在前进路径上;它是三维交互、物理碰撞、工业测量里最基础也最容易被低估的一类计算。
这篇文章我会从那次排错说起,把射线求交的数学原理、工程实现里的坑、性能优化思路,以及同一套逻辑在不同行业里的变体串起来聊一遍。适合刚接触游戏物理、在做手势或手柄交互、以及写工业视觉和CAD辅助功能的朋友参考。后续你会看到:有些问题看起来像玄学,最后都是坐标系、浮点精度或“你拿错了几何对象”在捣乱。
1. 一次“射线没打中”的经典排查现场
1.1 现象还原:为什么左边命中、右边漏掉
同事的项目是虚拟现实手柄射线点选UI。手柄发射射线,射到面板按钮上,就触发点击事件。左侧按钮一切正常,右侧按钮却偶尔失灵。更奇怪的是,失灵并不固定,稍微转一下头又好了。由于左右按钮用的是同一套逻辑,第一反应是怀疑按钮的碰撞体挂错了位置。
我让他做了一件事:把射线的可视化打开,同时在按钮的Box Collider上画一个线框。结果发现,射线明明穿过了按钮的可视范围,却没有命中Collider。再仔细一看,问题根本不在射线计算,而是按钮面板被手柄拖拽或旋转后,碰撞体的世界坐标更新滞后了一帧。Physics.Raycast读的是物理引擎当前帧的数据,而UI的World坐标在LateUpdate里才同步,于是射线打中了“旧位置”的空气。
这个案例给了我一个很重要的提醒:射线求交本身是纯数学判断,不会错;错的是你喂给它的数据。
1.2 我画了一条调试辅助线,问题立刻缩小了一半
排查这类问题,我有个习惯,先用一条可视化的线把射线画出来,再和实际场景对照。在Unity里写这几行就够了:
csharp复制Vector3 origin = handTransform.position;
Vector3 direction = handTransform.forward;
Debug.DrawLine(origin, origin + direction * 5f, Color.red, 2f);
如果用的是鼠标点击屏幕后发射的摄像机射线,还需要确认屏幕坐标到世界射线这一步有没有换算错。例如Canvas的Render Mode不是Overlay时,屏幕上的点击点不能直接当作UI坐标,要先转成世界平面上的点。射线“看起来穿过去了,实际上没碰到”的最常见原因,就是这里的二维三维转换出了问题。
画线只是帮我们把“想象中的射线”和“真实的射线”对齐。一旦对齐,很多问题就藏不住了。比如射线被某个不需要的物体挡住,或者射线长度不够,根本到不了目标位置。
1.3 最小复现:把几何测试从引擎里剥离
那次排查的最后,我们确认不是射线函数的锅。但为了还原整个过程,我还做了一件更彻底的事:把检测逻辑从工程里抽出来,写一个只输入起点、方向、目标点三个参数的纯函数,用它单独算一遍。
csharp复制public static bool RayHitsSphere(
Vector3 origin, Vector3 dir, Vector3 sphereCenter, float radius)
{
Vector3 oc = origin - sphereCenter;
float b = Vector3.Dot(dir, oc);
float c = Vector3.Dot(oc, oc) - radius * radius;
float discriminant = b * b - c;
return discriminant >= 0f;
}
这段代码无论有没有引擎都能跑,所有输入都清清楚楚。跑完的结果和Unity的Physics.Raycast对比,就能区分“是我的数据不对”还是“引擎计算有问题”。绝大多数时候,答案都是前者。这个“最小复现”的思路,后来成了我排查几何相关Bug的第一步。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 交点检测背后的数学,其实只需要这一套参数化模型
2.1 从“射线”到“线段”:先认清你在和什么做相交
射线最简单的写法是参数方程:
P(t) = O + D * t
O是起点,D是方向向量,t是一个非负实数。当t为0时,P就在起点;t越大,点沿方向越远。如果进一步限制t的取值范围,就能得到不同类型:
