游戏开发中的叉积:方向、面积与法线从哪里来?
从二维左右转判断,到三维三角形法线、面积与局部坐标轴,用可拖动的图理解叉积的方向、大小和参数顺序。
一个角色要转向目标时,应该往左还是往右?给三个顶点生成三角形时,哪一面朝外?两条边围出的三角形有多大?
这些问题看起来不同,都可以从同一个运算出发:叉积(cross product)。
如果你读过游戏开发中的点积,已经知道点积擅长回答“两个方向有多一致”。叉积则在两个方向之外,给出一个垂直方向;它的大小还记录了这两个方向张开的面积。
先记住这句话
对三维向量 和 , 垂直于两者;交换输入顺序,结果反向。它的长度等于两向量围成的平行四边形面积。
1从一个转向问题开始
先在普通的二维 平面上看两个箭头: 向右, 向上。把 看作当前朝向, 看作目标方向。拖动图中的端点,让 分别落在 的两侧。
图中填色的是两向量围出的平行四边形。这里显示的“ 分量”是:
在这个特定的二维坐标约定下,从 转到 :
| 结果 | 几何意义 | 可以怎样使用 |
|---|---|---|
| 大于 | 逆时针转, 在 左侧 | 角色向左转 |
| 小于 | 顺时针转, 在 右侧 | 角色向右转 |
| 等于 | 两向量共线 | 再用点积区分同向和反向 |
例如 、,则结果为 :从右方转到上方是逆时针。把顺序换成 ,结果就是 。
零并不等于已经朝向目标
平行和反平行的叉积都是零。如果角色正好背对目标,叉积无法单独决定往哪边转;这时要用点积判断是否反向,再指定一个固定的转向策略。
2三维叉积究竟返回什么?
二维图上的数值,其实是三维叉积的 分量。把两个二维向量补上 :
完整的三维公式是:
于是二维情况下只剩下 。二维玩法常把这个 分量直接当成一个带符号的数来用;严格说,三维叉积的结果仍然是向量。
对于非零且不平行的输入,结果同时满足:
所以它垂直于 和 。至于它朝垂直方向的哪一侧,由输入顺序决定:
这也是读叉积代码时最值得先检查的一件事。Unity 的 Vector3.Cross 文档同样明确说明了垂直方向、长度与夹角的关系,以及交换顺序会改变方向。1
下面的三维图把 、 和它们的叉积同时画出来。拖动端点改变两个输入向量,绿色箭头会随之更新;拖动空白处可转动观察视角。填色的四边形展示了叉积长度所代表的面积。
3为什么叉积的长度等于面积?
设两向量夹角为 ,取 。叉积的长度是:
把 看作平行四边形的底, 就是高。**底乘高,正好是面积。**因此:
这解释了拖动图形时的读数:两向量越接近同一条直线,高越小,叉积越接近零;两向量互相垂直时,在长度固定的前提下面积最大。
长度和方向要分开看
若只想要垂直方向,通常需要把叉积归一化。若想要面积、转向强度或判断是否退化,就要保留原始长度。先归一化输入或输出,都会改变你手中这个数的含义。
4用三个顶点求三角形法线
程序化生成网格时,给定三角形顶点 ,先从同一个顶点构造两条边:
然后叉积得到与三角形平面垂直的向量:
在 Unity 中,可以写成:
static bool TryGetTriangleNormalAndArea(
Vector3 a, Vector3 b, Vector3 c,
out Vector3 normal, out float area)
{
Vector3 rawNormal = Vector3.Cross(b - a, c - a);
if (rawNormal.sqrMagnitude < 1e-12f)
{
// 三点几乎共线,无法得到可靠的法线。
normal = Vector3.zero;
area = 0f;
return false;
}
normal = rawNormal.normalized;
area = 0.5f * rawNormal.magnitude;
return true;
}这里的 normal 可以用于需要面朝向的计算,area 则保留了这块三角形的大小。Unity 的官方示例也是先取两条边,调用 Vector3.Cross,然后归一化得到法线。1
将 和 对调,法线就会反向。对网格而言,顶点顺序还会影响正面、背面的判定和背面剔除;Unity 使用从可见正面看去顺时针的索引绕序。2 如果一块新生成的面“消失”了,先检查三角形索引顺序和法线方向。
别直接归一化退化三角形
三点重合或共线时,两条边的叉积为零,没有唯一法线。实际项目中应先检查长度是否足够大;阈值要结合模型坐标的尺度选择,示例中的数值不是通用常量。
5在地面上判断目标在左还是右
二维 图中“正值表示左转”很好记,但 Unity 常见的地面玩法是在 平面上: 是上方,角色的 forward 常沿 。此时不能直接照搬二维图的“正值为左”。
例如角色朝 ,目标在 (角色右边):
因此下面这个明确固定了参数顺序的写法中,side > 0 表示右边:
Vector3 toTarget = target.position - transform.position;
Vector3 forwardOnGround = Vector3.ProjectOnPlane(transform.forward, Vector3.up);
Vector3 targetOnGround = Vector3.ProjectOnPlane(toTarget, Vector3.up);
float side = Vector3.Dot(
Vector3.Cross(forwardOnGround, targetOnGround),
Vector3.up
);
if (side > 0.001f) { /* 目标在右侧 */ }
else if (side < -0.001f) { /* 目标在左侧 */ }
else { /* 几乎共线,结合点积判断前后 */ }先投影到地面,是为了让“左右”只描述水平转向,不把目标的高度混进判断。这里 Cross 给出垂直于地面的方向,接着 Dot(..., Vector3.up) 把这个方向压成一个带符号的数。
如果需要的是角度而不只是左右,Unity 已提供 Vector3.SignedAngle(from, to, axis)。它借助指定的轴确定符号,返回 到 之间的最短有符号角;调换 from 和 to 会改变符号。3
6叉积还能构造一根“侧向轴”
知道前方和上方,就能求出与两者垂直的右方。在 Unity 常用的 向右、 向上、 向前的轴约定下:
例如 Vector3.Cross(Vector3.up, Vector3.forward) 得到 (1, 0, 0)。这可以用于镜头侧向移动、角色移动基向量,或沿路径生成带宽度的带状网格。
Vector3 forward = travelDirection.normalized;
Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;但如果 forward 几乎朝上或朝下,它与 up 几乎平行,叉积会趋近零。这时需要换一个不平行的参考轴,或沿用上一帧稳定的侧向轴。叉积能补出第三个方向,前提是前两个方向确实张开了一个平面。
7常见的四个误区
- 忘记顺序。
Cross(a, b)与Cross(b, a)长度相同、方向相反。法线翻面和左右判断写反,往往只是顺序出了问题。 - 把二维图的正负含义直接搬到三维。 三维需要先说明所在平面和参考轴;换了轴、投影方向或参数顺序,左右的符号可能变化。
- 一律归一化。 单位法线适合表达方向;原始叉积还保存了面积信息。根据实际问题选择。
- 忽略坐标空间。 两个向量必须在同一坐标空间内计算。一个是物体局部方向、另一个是世界空间方向时,先转换再叉积。
8最后把点积和叉积放在一起
| 想问什么 | 更直接的工具 |
|---|---|
| 两方向有多一致、目标在前还是在后? | 点积 |
| 两向量张开的面积是多少? | 叉积长度 $ |
| 平面的垂直方向在哪里? | 叉积向量 |
| 在指定平面里朝哪侧转? | 叉积在参考轴上的分量,必要时再配合点积 |
可以把叉积理解为:按顺序放下两个方向,得到它们共同决定的垂直方向与张开程度。 记住“顺序、参考平面、是否保留长度”,大多数游戏开发中的叉积问题就有了清晰的检查路径。
注释
-
Unity Technologies,Vector3.Cross(Unity 6.0)。官方说明结果垂直于输入,长度为两输入长度与夹角正弦的乘积,并展示方向的确定方式。 ↩ ↩2
-
Unity Technologies,网格索引数据(Unity 6.0)。官方说明 Unity 用三角形索引的绕序决定正背面,顺时针为正面。 ↩
-
Unity Technologies,Vector3.SignedAngle(Unity 6.0)。官方说明此 API 的参数、参考轴与返回角度范围。 ↩
CONTINUE EXPLORING