Elytra
Blog数学

游戏开发中的叉积:方向、面积与法线从哪里来?

从二维左右转判断,到三维三角形法线、面积与局部坐标轴,用可拖动的图理解叉积的方向、大小和参数顺序。

一个角色要转向目标时,应该往左还是往右?给三个顶点生成三角形时,哪一面朝外?两条边围出的三角形有多大?

这些问题看起来不同,都可以从同一个运算出发:叉积(cross product)。

如果你读过游戏开发中的点积,已经知道点积擅长回答“两个方向有多一致”。叉积则在两个方向之外,给出一个垂直方向;它的大小还记录了这两个方向张开的面积。

先记住这句话

对三维向量 a\mathbf a 和 b\mathbf b,a×b\mathbf a\times\mathbf b 垂直于两者;交换输入顺序,结果反向。它的长度等于两向量围成的平行四边形面积。

1从一个转向问题开始

先在普通的二维 xyxy 平面上看两个箭头:xx 向右,yy 向上。把 a\mathbf a 看作当前朝向,b\mathbf b 看作目标方向。拖动图中的端点,让 b\mathbf b 分别落在 a\mathbf a 的两侧。

拖动 a、b 的端点,观察叉积的符号、旋转方向和平行四边形面积。

图中填色的是两向量围出的平行四边形。这里显示的“zz 分量”是:

cross2D⁡(a,b)=axby−aybx\operatorname{cross2D}(\mathbf a,\mathbf b)=a_xb_y-a_yb_x

在这个特定的二维坐标约定下,从 a\mathbf a 转到 b\mathbf b:

结果几何意义可以怎样使用
大于 00逆时针转,b\mathbf b 在 a\mathbf a 左侧角色向左转
小于 00顺时针转,b\mathbf b 在 a\mathbf a 右侧角色向右转
等于 00两向量共线再用点积区分同向和反向

例如 a=(1,0)\mathbf a=(1,0)、b=(0,1)\mathbf b=(0,1),则结果为 11:从右方转到上方是逆时针。把顺序换成 b,a\mathbf b,\mathbf a,结果就是 −1-1。

零并不等于已经朝向目标

平行和反平行的叉积都是零。如果角色正好背对目标,叉积无法单独决定往哪边转;这时要用点积判断是否反向,再指定一个固定的转向策略。

2三维叉积究竟返回什么?

二维图上的数值,其实是三维叉积的 zz 分量。把两个二维向量补上 z=0z=0:

a=(ax,ay,0),b=(bx,by,0)\mathbf a=(a_x,a_y,0),\qquad \mathbf b=(b_x,b_y,0)

完整的三维公式是:

a×b=(aybz−azbyazbx−axbzaxby−aybx)\mathbf a\times\mathbf b= \begin{pmatrix} a_yb_z-a_zb_y\\ a_zb_x-a_xb_z\\ a_xb_y-a_yb_x \end{pmatrix}

于是二维情况下只剩下 (0,0,axby−aybx)(0,0,a_xb_y-a_yb_x)。二维玩法常把这个 zz 分量直接当成一个带符号的数来用;严格说,三维叉积的结果仍然是向量。

对于非零且不平行的输入,结果同时满足:

(a×b)⋅a=0,(a×b)⋅b=0(\mathbf a\times\mathbf b)\cdot\mathbf a=0, \qquad (\mathbf a\times\mathbf b)\cdot\mathbf b=0

所以它垂直于 a\mathbf a 和 b\mathbf b。至于它朝垂直方向的哪一侧,由输入顺序决定:

a×b=−(b×a)\mathbf a\times\mathbf b=-(\mathbf b\times\mathbf a)

这也是读叉积代码时最值得先检查的一件事。Unity 的 Vector3.Cross 文档同样明确说明了垂直方向、长度与夹角的关系,以及交换顺序会改变方向。1

下面的三维图把 a\mathbf a、b\mathbf b 和它们的叉积同时画出来。拖动端点改变两个输入向量,绿色箭头会随之更新;拖动空白处可转动观察视角。填色的四边形展示了叉积长度所代表的面积。

拖动 a、b 改变向量,拖动空白处旋转视角;绿色箭头是 a × b。

3为什么叉积的长度等于面积?

设两向量夹角为 θ\theta,取 0≤θ≤π0\leq\theta\leq\pi。叉积的长度是:

∣a×b∣=∣a∣ ∣b∣sin⁡θ|\mathbf a\times\mathbf b| =|\mathbf a|\,|\mathbf b|\sin\theta

把 ∣a∣|\mathbf a| 看作平行四边形的底,∣b∣sin⁡θ|\mathbf b|\sin\theta 就是高。**底乘高,正好是面积。**因此:

A平行四边形=∣a×b∣,A三角形=12∣a×b∣A_{\text{平行四边形}}=|\mathbf a\times\mathbf b|, \qquad A_{\text{三角形}}=\tfrac12|\mathbf a\times\mathbf b|

这解释了拖动图形时的读数:两向量越接近同一条直线,高越小,叉积越接近零;两向量互相垂直时,在长度固定的前提下面积最大。

长度和方向要分开看

若只想要垂直方向,通常需要把叉积归一化。若想要面积、转向强度或判断是否退化,就要保留原始长度。先归一化输入或输出,都会改变你手中这个数的含义。

4用三个顶点求三角形法线

程序化生成网格时,给定三角形顶点 A,B,CA,B,C,先从同一个顶点构造两条边:

e1=B−A,e2=C−A\mathbf e_1=B-A,\qquad \mathbf e_2=C-A

然后叉积得到与三角形平面垂直的向量:

nraw=e1×e2,n=nraw∣nraw∣\mathbf n_{\text{raw}}=\mathbf e_1\times\mathbf e_2, \qquad \mathbf n=\frac{\mathbf n_{\text{raw}}}{|\mathbf n_{\text{raw}}|}

在 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

将 BB 和 CC 对调,法线就会反向。对网格而言,顶点顺序还会影响正面、背面的判定和背面剔除;Unity 使用从可见正面看去顺时针的索引绕序。2 如果一块新生成的面“消失”了,先检查三角形索引顺序和法线方向。

别直接归一化退化三角形

三点重合或共线时,两条边的叉积为零,没有唯一法线。实际项目中应先检查长度是否足够大;阈值要结合模型坐标的尺度选择,示例中的数值不是通用常量。

5在地面上判断目标在左还是右

二维 xyxy 图中“正值表示左转”很好记,但 Unity 常见的地面玩法是在 xzxz 平面上:yy 是上方,角色的 forward 常沿 +z+z。此时不能直接照搬二维图的“正值为左”。

例如角色朝 +z+z,目标在 +x+x(角色右边):

(0,0,1)×(1,0,0)=(0,1,0)(0,0,1)\times(1,0,0)=(0,1,0)

因此下面这个明确固定了参数顺序的写法中,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)。它借助指定的轴确定符号,返回 −180∘-180^\circ 到 180∘180^\circ 之间的最短有符号角;调换 from 和 to 会改变符号。3

6叉积还能构造一根“侧向轴”

知道前方和上方,就能求出与两者垂直的右方。在 Unity 常用的 xx 向右、yy 向上、zz 向前的轴约定下:

right=up×forward\mathbf{right}=\mathbf{up}\times\mathbf{forward}

例如 Vector3.Cross(Vector3.up, Vector3.forward) 得到 (1, 0, 0)。这可以用于镜头侧向移动、角色移动基向量,或沿路径生成带宽度的带状网格。

Vector3 forward = travelDirection.normalized;
Vector3 right = Vector3.Cross(Vector3.up, forward).normalized;

但如果 forward 几乎朝上或朝下,它与 up 几乎平行,叉积会趋近零。这时需要换一个不平行的参考轴,或沿用上一帧稳定的侧向轴。叉积能补出第三个方向,前提是前两个方向确实张开了一个平面。

7常见的四个误区

  1. 忘记顺序。 Cross(a, b) 与 Cross(b, a) 长度相同、方向相反。法线翻面和左右判断写反,往往只是顺序出了问题。
  2. 把二维图的正负含义直接搬到三维。 三维需要先说明所在平面和参考轴;换了轴、投影方向或参数顺序,左右的符号可能变化。
  3. 一律归一化。 单位法线适合表达方向;原始叉积还保存了面积信息。根据实际问题选择。
  4. 忽略坐标空间。 两个向量必须在同一坐标空间内计算。一个是物体局部方向、另一个是世界空间方向时,先转换再叉积。

8最后把点积和叉积放在一起

想问什么更直接的工具
两方向有多一致、目标在前还是在后?点积 a⋅b\mathbf a\cdot\mathbf b
两向量张开的面积是多少?叉积长度 $
平面的垂直方向在哪里?叉积向量 a×b\mathbf a\times\mathbf b
在指定平面里朝哪侧转?叉积在参考轴上的分量,必要时再配合点积

可以把叉积理解为:按顺序放下两个方向,得到它们共同决定的垂直方向与张开程度。 记住“顺序、参考平面、是否保留长度”,大多数游戏开发中的叉积问题就有了清晰的检查路径。

注释

  1. Unity Technologies,Vector3.Cross(Unity 6.0)。官方说明结果垂直于输入,长度为两输入长度与夹角正弦的乘积,并展示方向的确定方式。 ↩ ↩2

  2. Unity Technologies,网格索引数据(Unity 6.0)。官方说明 Unity 用三角形索引的绕序决定正背面,顺时针为正面。 ↩

  3. Unity Technologies,Vector3.SignedAngle(Unity 6.0)。官方说明此 API 的参数、参考轴与返回角度范围。 ↩

CONTINUE EXPLORING

沿着这篇文章继续

在地图中查看

On this page