☰
向量点乘与叉乘详解:从几何意义到图形学与游戏开发实战
2026/10/1 5:53:52 网站建设 项目流程

聊到线性代数里最容易被忽略、却在实际工程里天天用的两个运算,一个是向量点乘(点积),一个是向量叉乘(叉积)。很多人本科时背过公式、考完试就忘了,结果一进图形学、游戏开发、机器人、物理仿真这些领域,马上又得捡回来。原因很简单:点乘和叉乘不只是数学题里的符号,它们是判断“方向、角度、朝向、法线、投影”这套空间直觉的基础工具,用得好能省掉大量三角函数运算,用得不好就会出现模型渲染黑半边、物体旋转反向、物理碰撞判定错误这类诡异问题。

这篇文章我从“这两东西到底在算什么”讲起,把代数公式、几何含义、典型应用、常见误区一次说透。不管你是正准备期末考试的在校生,还是工作中需要用向量做功能开发的工程师,都能从里面拿到直接能用的结论和实操经验。

1. 点乘(内积)到底在算什么

1.1 从数值计算出发:两种公式先记住

点乘的代数定义极其简单。两个同维向量,对应分量相乘后求和:

[ \mathbf{a} \cdot \mathbf{b} = a_1 b_1 + a_2 b_2 + \cdots + a_n b_n ]

比如向量 (\mathbf{a}=(2, 3)),向量 (\mathbf{b}=(4, -1)),那么:

[ \mathbf{a} \cdot \mathbf{b} = 2 \times 4 + 3 \times (-1) = 8 - 3 = 5 ]

就这么算,没有任何弯弯绕。这也是为什么在编程里,只要你会写一个循环,就能实现点乘。实际开发中更常用的是numpy的np.dot,或者各类图形引擎自带的Vector3.Dot。

几何定义长这样:

[ \mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| |\mathbf{b}| \cos\theta ]

其中 (\theta) 是两个向量之间的夹角(取0到π之间的小角)。这个公式才是点乘的灵魂。它告诉我们:点乘结果不只看两个向量的长短,还看它们方向上的“协同程度”。

很多初学者会误以为这两个公式是两个不同的定义,其实它们是等价的,可以用余弦定理推出来。你可以把几何定义当作“结果解释器”,代数定义当作“计算器”。需要理解含义时用几何定义,需要实际算数值时用代数定义。

1.2 几何直觉:投影是点乘的灵魂

先回答一个问题:点乘算出来的那个数,到底是什么?

几何公式里 (\cos\theta) 帮我们拆开看:(|\mathbf{b}|\cos\theta) 表示向量 (\mathbf{b}) 在向量 (\mathbf{a}) 方向上的投影长度。再乘上 (|\mathbf{a}|),就得到了一个“带方向的长度乘积”。

可以这样理解:把 (\mathbf{a}) 想象成地面上的一条水平线,把 (\mathbf{b}) 想象成一根斜杆。正午时分支杆在地面上的影子长度,就是 (|\mathbf{b}|\cos\theta)。点乘的结果,就是用这根斜杆的长度和这根水平线的长度一起决定的“有效贡献量”。

所以我在实际教学中经常说:点乘的本质是衡量“一个向量在另一个向量方向上有多少分量”,顺带把另一个向量本身的长度也考虑进来。

如果你想看的是纯粹的投影长度,那就得把点乘结果再除以那个向量的模长:

[ \text{proj}_{\mathbf{a}} \mathbf{b} = \frac{\mathbf{a} \cdot \mathbf{b}}{|\mathbf{a}|} ]

如果在代码里两个向量都做了归一化处理,那点乘就直接等于两个单位向量夹角的余弦值,不需要再除模长。这也是图形学里最常见的优化手段。

1.3 为什么“同向最大、垂直为零、反向为负”

我把点乘的几种极端情况摆出来:

  • 两个向量同向,夹角0°,(\cos 0 = 1),结果为正,且数值最大。
  • 两个向量夹角90°,(\cos 90° = 0),结果为零。
  • 两个向量夹角180°,(\cos 180° = -1),结果为负,且绝对值最大。
  • 夹角在0°到90°之间,结果为正;90°到180°之间,结果为负。

这个特性在工程里极其有用。举个例子,你想判断一个物体是“面向”还是“背对”某个方向,只需要做一次点乘,看符号就够了,根本不需要真的去算夹角。游戏里很多“敌人是否在视野范围内”的判定,就是用这个思路实现:计算视线方向和目标方向向量的点乘,大于阈值则说明在视野内,反之在视野外。

很多同学会问:那夹角在90°附近时要不要担心误差?要的。在浮点运算里,(\cos\theta)在接近90°时变化率很大,稍微一点误差就会被放大。这也是为什么用点乘判断“是否垂直”时,不要直接判断等于0,而是判断绝对值小于一个极小量(比如1e-6)。

1.4 常用性质速览:交换律、分配律、与模长的关系

点乘有几个性质,平时写推导公式时会频繁用:

  • 交换律:(\mathbf{a} \cdot \mathbf{b} = \mathbf{b} \cdot \mathbf{a}),左右顺序不影响结果。
  • 分配律:(\mathbf{a} \cdot (\mathbf{b} + \mathbf{c}) = \mathbf{a} \cdot \mathbf{b} + \mathbf{a} \cdot \mathbf{c}),这个在拆解复杂几何关系时很有用。
  • 数乘结合:((k\mathbf{a}) \cdot \mathbf{b} = k(\mathbf{a} \cdot \mathbf{b})),常数可以提出来。
  • 自身点乘:(\mathbf{a} \cdot \mathbf{a} = |\mathbf{a}|^2),这就是向量模长的平方。

最后一条特别重要。在很多几何算法里,为了不引入开方运算,会直接使用 (|\mathbf{a}|^2) 进行比较。比如判断两点距离是否小于某个半径,可以直接比较距离平方与半径平方,省掉一次开方,性价比很高。做移动端渲染优化时,这种“能省则省”的习惯能让性能提升非常明显。

2. 叉乘(外积)到底在算什么

2.1 二维情形的“有向面积”

叉乘在二维空间里可以看作一种“有向面积”的计算。给出两个二维向量 (\mathbf{a}=(x_1, y_1)) 和 (\mathbf{b}=(x_2, y_2)),定义:

[ \text{cross}(\mathbf{a}, \mathbf{b}) = x_1 y_2 - y_1 x_2 ]

这个数等于以这两个向量为邻边构成的平行四边形的“有向面积”。加引号是因为它的符号有讲究:如果 (\mathbf{b}) 在 (\mathbf{a}) 的逆时针方向,结果为正;顺时针方向,结果为负。在实际判定时,这比计算三角函数快得多,也因此被广泛应用在计算几何里判断点的左右侧关系。

需要注意的是,二维向量的叉乘结果是一个标量,而不是一个向量。很多刚接触线性代数的朋友会在这一步犯糊涂:明明说叉乘结果是向量,怎么二维里变成了数?原因在于二维叉乘本质上是三维叉乘中 (z) 轴方向的那个分量。二维空间没有第三个维度,所以我们只保留了大小和正负号。

2.2 三维叉乘:一个向量,同时垂直两个向量

三维叉乘的定义是:

[ \mathbf{a} \times \mathbf{b} = |\mathbf{a}| |\mathbf{b}| \sin\theta , \mathbf{n} ]

其中 (\mathbf{n}) 是垂直于 (\mathbf{a}) 和 (\mathbf{b}) 所在平面的单位法向量。这个式子的关键信息有三点:

第一,叉乘结果是一个新向量,方向垂直于原来两个向量所在的平面。第二,它的长度等于以 (\mathbf{a})、(\mathbf{b}) 为邻边的平行四边形面积,也就是 (|\mathbf{a}| |\mathbf{b}| \sin\theta)。第三,它的方向由右手定则决定,下文会细说。

这个“垂直”特性在几何计算中极其重要。已知一个平面内两条不共线的边向量,直接做叉乘就能得到该平面的法向量。图形学中的表面法线就是这么来的,机器人运动学里的旋转轴也常常这样求解。

我建议把二维叉乘看成三维叉乘的一个特例:当两个向量都在 (xy) 平面内时,它们叉乘的方向必然沿 (z) 轴,所以二维中可以只存一个标量来表示方向和大小。

2.3 右手定则与坐标系

叉乘的方向判断要记住右手定则:张开右手,四指从第一个向量 (\mathbf{a}) 弯向第二个向量 (\mathbf{b}),拇指所指的方向就是 (\mathbf{a} \times \mathbf{b}) 的方向。

这里特别容易出错的是两点:

第一,叉乘不满足交换律。(\mathbf{a} \times \mathbf{b} = -(\mathbf{b} \times \mathbf{a})),顺序一换,方向就反了。这个错误在我调程序时踩过不止一次:明明公式写对了,结果因为两个向量的传入顺序写反,法线方向始终朝里,导致光照结果漆黑一片。

第二,右手定则依赖坐标系。在右手坐标系(x轴向右、y轴向上、z轴朝外)里,x轴叉乘y轴得到z轴。但在左手坐标系(很多游戏引擎用这种)里,判断方式会变成“左手定则”。所以写代码前一定要确认项目使用的是哪种坐标系,否则很容易把旋转方向搞反。

作为快速验证,记住这个口诀:右手坐标系中,(x \times y = z),(y \times z = x),(z \times x = y)。反过来都是负号。左手坐标系则正好相反。

2.4 行列式形式的计算技巧

三维叉乘的代数计算方法常写成“伪行列式”的形式:

[ \mathbf{a} \times \mathbf{b} = \begin{vmatrix} \mathbf{i} & \mathbf{j} & \mathbf{k} \ x_1 & y_1 & z_1 \ x_2 & y_2 & z_2 \end{vmatrix} ]

按第一行展开,得到:

[ \mathbf{a} \times \mathbf{b} = (y_1 z_2 - z_1 y_2,; z_1 x_2 - x_1 z_2,; x_1 y_2 - y_1 x_2) ]

很多教程会让你直接背这个公式,但我更推荐理解推导过程:把叉乘看成一种“对偶向量”的计算,每个分量本质上是在算另外两个方向上坐标组成的二维有向面积。这样即使公式忘了,也能自己推出来。

实际手算时有一个小技巧:把两个向量写成一个矩阵形状,然后每次“划掉”要计算的轴那一列,对剩余两列做交叉相乘相减。比如算 (z) 方向分量时,只看 (x_1y_2 - y_1x_2)。多算几遍,熟练后一眼就能看出来。

注意:用行列式展开时,中间那个分量是 (z_1 x_2 - x_1 z_2),不是 (x_1 z_2 - z_1 x_2)。这里最容易抄错符号,我在批改作业时见过太多次了。

3. 点乘与叉乘的核心应用场景

3.1 图形学与游戏开发中的点乘:光照、朝向判定

点乘在图形学里几乎无处不在。最经典的例子是漫反射光照计算:表面法线向量 (\mathbf{n}) 和光线方向向量 (\mathbf{l}) 都归一化后,它们之间的点乘就是光线在表面的“有效强度”。结果越接近1,说明光线越正对表面,亮度越高;结果接近0,说明光线擦着表面掠过,亮度接近于0。

这个计算非常廉价,因为点乘只需要三次乘法和两次加法。如果不用点乘而直接算夹角,那就要调用反余弦函数,性能开销会高出一个数量级。实时光照需要每帧对大量顶点做计算,这种性能差距在性能优化时是致命的。

图形学里还有一个常见操作是“背面剔除”。思路是:拿视线向量和表面法线做点乘,如果结果小于0,说明当前看到的是背面,可以跳过不渲染。这个判定不需要知道具体角度,只看符号,效率极高。

3.2 判断点在三角形/多边形内:叉乘的符号

二维叉乘最有名的应用之一就是判断点与多边形的位置关系。核心思想是:对于多边形每条边,判断点是在边的左侧还是右侧。如果点对于所有边都在同一侧,那么点就在多边形内部。

具体做法是:对于三角形三个顶点 (A)、(B)、(C),以及待判断点 (P),依次计算:

[ \text{cross}(B-A, P-A), \quad \text{cross}(C-B, P-B), \quad \text{cross}(A-C, P-C) ]

如果三个结果符号相同,则点在三角形内部。这个方法在计算机图形学的“鼠标拾取”“点击碰撞检测”中非常常用,因为不需要开方、三角函数等复杂运算,只需几次乘减法。

网上有不少代码用这种方法实现屏幕点击是否选中的判断,运行起来非常快。需要注意的地方是,这个方法的正确性依赖于顶点顺序是逆时针还是顺时针。如果整个三角形顶点顺序反了,判断结果里的“内部”和“外部”也会互换。所以工程上一般会先约定好顶点绕序,比如统一用逆时针。

3.3 叉乘求法线、旋转轴

三维叉乘“垂直”的特性让它成了求法线最直接的工具。已知平面上两个不共线的向量 (\mathbf{u}) 和 (\mathbf{v}),那么 (\mathbf{u} \times \mathbf{v}) 就是该平面的法向量。

实际项目中,比如用三角形网格表示地形起伏时,要计算每个顶点的法线,就是先取相邻边的向量,再做叉乘。这里有一个细节:如果三角形面积非常小,叉乘结果的长度也会非常小,很容易出现浮点精度问题。所以正规做法是先把叉乘结果归一化,甚至在做叉乘前先对边向量做一次归一化,以增强数值稳定性。

叉乘还有一个应用是求旋转轴。在刚体旋转中,如果两个向量分别代表旋转前后的方向,那么它们的叉乘方向就是旋转轴的方向,夹角大小还能通过点乘求出来。这正好是四元数或旋转矩阵构造过程中经常做的事:先用叉乘确定轴,再用点乘求角度,组合起来就得到了旋转信息。

3.4 物理中的功(点乘)与力矩(叉乘)

物理里点乘和叉乘的意义更加直观。功的定义是力与位移的点乘:

[ W = \mathbf{F} \cdot \mathbf{s} ]

只有沿着位移方向的力分量才对做功有贡献,垂直于位移的力分量只改变方向不改变能量。这恰好对应了点乘的“投影”本质。

力矩则是典型的叉乘:

[ \boldsymbol{\tau} = \mathbf{r} \times \mathbf{F} ]

力矩的大小等于力臂乘以力在垂直于力臂方向上的分量,方向则是旋转轴的方向。用叉乘而非普通乘法,是因为力矩天然带着“平面旋转方向”这个属性,只有借助法向量方向才能完整表达旋转的方向。

在我自己写物理仿真小工具时,这两条公式帮了大忙。遇到需要判断“这个力是在加速物体旋转还是让它平移”的问题时,分别计算点乘和叉乘就能把力的贡献拆解得干干净净。

4. 常见误区与避坑实操

4.1 点乘结果是一个数,不是向量

初学者最容易犯的一个错误是:搞不清点乘结果到底是什么类型。点乘的结果是标量,可以理解为“两个向量协同程度的度量”。你不能拿一个标量去直接表示方向,也不能把它当向量去和另一个向量做加法。

这个误区在写代码时会造成类型问题。像numpy里np.dot返回的是标量(一维情况),你如果继续拿它去做叉乘运算,会直接报错。我在带项目时,会让新人先打印结果的shape或者type,确认维度没问题再往下走。

4.2 叉乘顺序不能随意换,右手定则

叉乘不满足交换律,这个性质太重要了,重要到值得单独拿出来强调一遍:

[ \mathbf{a} \times \mathbf{b} = -\mathbf{b} \times \mathbf{a} ]

也就是说,两个向量的顺序一旦调换,结果方向就会取反。很多bug的根源就在这。比如你要用法线向量判断光照方向,结果因为顶点顺序不对导致法线朝内,模型就会“黑脸”或者出现背面剔除异常。

提供一个自查技巧:算完后可以用点乘验证一下。叉乘结果 (\mathbf{c} = \mathbf{a} \times \mathbf{b}) 必然同时满足 (\mathbf{c} \cdot \mathbf{a} = 0) 和 (\mathbf{c} \cdot \mathbf{b} = 0)。如果这两个条件不满足,说明计算过程有问题。这是我在代码里最常用的自检手段。

4.3 单位向量与归一化的重要性

很多几何公式是定义在单位向量基础上的。最典型的就是用点乘算夹角:只有两个向量都是单位向量时,点乘结果才直接等于 (\cos\theta)。如果直接用原始向量算,结果还会被模长缩放,就不再是角度的余弦值。

工程上处理这个问题有两种常用做法:

  • 先归一化再计算:对点和叉乘都安全。
  • 用点乘结果除以两个向量模长的乘积:适合不想改动原始数据、只需要角度值的场景。

实际中还会碰到一种情况:向量的模长可能为0,比如两个坐标完全相同的点构成的边向量。此时直接做归一化会出现“除零”错误,程序里要提前判断模长是否接近0,返回默认值或者跳过计算。

4.4 0向量、平行与垂直的特殊情况

0向量的点乘、叉乘结果都为0,这倒是没问题。但要注意,0向量的“方向”是没有定义的,如果你对一个零向量做归一化,程序会崩溃或产生NaN值。

两个向量平行时,夹角为0°或180°,叉乘结果为零向量。这一点经常被人忽略:以为叉乘结果不为零就能说明不平行,结果碰巧遇到平行向量时,算出来全是0,方向信息完全丢失。

两个向量垂直时,点乘结果为零。但浮点数计算中几乎不会出现精确的0,而是很小的近似值。所以判断垂直时,不要用“是否等于0”作为条件,应该用“绝对值是否小于阈值”。

我梳理了一个高频错误速查表,方便以后排查问题:

错误类型具体表现解决思路
点乘结果当向量用程序报维度错误或结果诡异明确返回类型,先打印验证
叉乘顺序颠倒法线方向相反、旋转方向反统一顶点绕序,用右手定则复核
未归一化直接算角度角度偏差严重归一化或除以模长乘积
平行向量做叉乘结果接近零向量,方向丢失提前检测平行,单独处理
垂直判断不设阈值因浮点误差误判用绝对值小于1e-6判断

5. 实操:手算一遍加代码验证

5.1 手算完整示例

拿两个三维向量来完整算一遍。设:

[ \mathbf{a} = (1, 2, 3), \quad \mathbf{b} = (4, 5, 6) ]

点乘:

[ \mathbf{a} \cdot \mathbf{b} = 1\times4 + 2\times5 + 3\times6 = 4 + 10 + 18 = 32 ]

叉乘,按公式展开:

[ \mathbf{a} \times \mathbf{b} = (2\times6 - 3\times5,; 3\times4 - 1\times6,; 1\times5 - 2\times4) ]

[ = (12 - 15,; 12 - 6,; 5 - 8) = (-3,; 6,; -3) ]

验证一下垂直性:

[ \mathbf{a} \cdot (\mathbf{a} \times \mathbf{b}) = 1\times(-3) + 2\times6 + 3\times(-3) = -3 + 12 - 9 = 0 ]

[ \mathbf{b} \cdot (\mathbf{a} \times \mathbf{b}) = 4\times(-3) + 5\times6 + 6\times(-3) = -12 + 30 - 18 = 0 ]

两次点乘都是0,说明叉乘结果确实垂直于两个原始向量。这个手算过程建议你也亲自走一遍,手感很重要。尤其是中间那个分量的符号,多算几遍就能形成肌肉记忆。

5.2 Python/NumPy代码验证

用numpy验证就非常快了:

import numpy as np a = np.array([1.0, 2.0, 3.0]) b = np.array([4.0, 5.0, 6.0]) dot_result = np.dot(a, b) cross_result = np.cross(a, b) print("点乘结果:", dot_result) print("叉乘结果:", cross_result) # 验证垂直性 print("a 与叉乘结果点乘:", np.dot(a, cross_result)) print("b 与叉乘结果点乘:", np.dot(b, cross_result))

运行结果应该和手算一致。如果要用点乘算夹角,可以这样:

def angle_between_degrees(a, b): cos_theta = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) cos_theta = np.clip(cos_theta, -1.0, 1.0) # 防御浮点误差 return np.degrees(np.arccos(cos_theta)) print("夹角(度):", angle_between_degrees(a, b))

np.clip这步不是可有可无的。浮点运算中 (\cos\theta) 有可能出现1.0000000002这种值,直接传给arccos会导致nan。这个坑我在实际项目里遇到过,别问我是怎么知道的。

5.3 游戏引擎中的直接调用

如果你在Unity里做开发,直接调用现成API就行:

Vector3 a = new Vector3(1f, 2f, 3f); Vector3 b = new Vector3(4f, 5f, 6f); float dot = Vector3.Dot(a, b); Vector3 cross = Vector3.Cross(a, b);

在虚幻引擎里也有类似的接口,蓝图节点一个叫Dot Product,一个叫Cross Product。这些接口底层实现都是上面讲的公式。理解原理后,即使换一个引擎、换一种语言,你也能很快适应,因为计算的数学本质完全不变。

注意:不同引擎的坐标系习惯不一样,Unity是左手坐标系,OpenGL风格是右手坐标系。同样写一句Vector3.Cross,在左手坐标系里得到的法线方向判断标准会有所不同。跨平台做功能移植时,一定要把坐标系这个变量纳入考量。

6. 踩坑经历与最后的小建议

前面讲了大量理论,最后分享一个我真实遇到的bug。在某次做地形渲染时,我用法线向量计算光照,结果山脊和山谷的光照完全反了,阳面变暗、阴面变亮。排查了很久才发现,问题出在建索引时三角形顶点顺序不统一,有的三角形按逆时针建,有的按顺时针建,导致法线一会儿朝上一会儿朝下。点乘光照公式本身没写错,错在给公式喂数据的顺序不统一。

后来我养成了一个习惯:在所有需要叉乘求法线的地方,先统一顶点绕序,再加一个断言,确保法线方向的 (y) 分量始终大于0。如果出现负数,就交换顶点顺序重新计算。这些小检查看起来繁琐,但能帮助你在数据量大的时候快速定位问题。

如果你正在学习线性代数,我建议不要只背公式,而是把点乘、叉乘的几何意义反复在脑子里过几遍。遇到实际问题时先问自己:这是要算投影、算角度、算垂直关系,还是要求法线、求面积、判方向?想清楚用哪种运算后,再去套公式,正确率会高很多。

最后分享一个小技巧:在你需要判断两个向量方向关系时,优先用点乘的符号;在需要构造垂直向量或计算有向面积时,优先用叉乘。这两个工具配合起来,能解决绝大部分空间几何问题。多动手画图、多写代码验证,比单纯刷题记得牢得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询