1.前言
在学习 C++ 三维可视化、OpenGL 以及仿真软件开发的过程中,向量是一个非常基础且重要的数学概念。
例如,在三维场景中,我们需要计算物体的朝向、判断两个方向是否垂直、计算三角形的法向量、实现光照效果,甚至进行模型拾取和碰撞检测。这些功能的背后,都离不开向量运算。
向量常见的两种运算是点乘(Dot Product)和叉乘(Cross Product)。
二、向量的基本概念
向量既有大小,也有方向。
在二维空间中,一个向量可以表示为:
a = (ax, ay)
在三维空间中,一个向量可以表示为:
a = (ax, ay, az)
例如:
a = (1, 2, 3)
表示这个向量在 X、Y、Z 三个坐标轴方向上的分量分别为 1、2、3。
向量在三维图形学中有很多用途,例如:
- 表示物体的移动方向。
- 表示摄像机的观察方向。
- 表示光线的传播方向。
- 表示三角形表面的法向量。
- 表示两个空间点之间的方向关系。
理解点乘和叉乘之前,首先需要明确一个概念:
点乘的结果是标量,叉乘的结果是向量。
这也是两种运算最直观的区别。
三、点乘(Dot Product)
3.1 点乘的定义
点乘也叫内积。两个向量经过点乘运算后,得到一个标量。
对于两个三维向量:
a = (ax, ay, az)
b = (bx, by, bz)
它们的点乘公式为:
a · b = ax * bx + ay * by + az * bz
简单来说,就是对应分量相乘,再把结果相加。
例如:
a = (1, 2, 3)
b = (4, 5, 6)
代入公式:
a · b = 1 * 4 + 2 * 5 + 3 * 6
= 4 + 10 + 18
= 32
最终结果为 32,是一个标量,而不是向量。
3.2 点乘的几何意义
点乘还可以通过向量的长度和夹角表示:
a · b = |a| * |b| * cos(θ)
其中:
|a|表示向量 a 的长度。|b|表示向量 b 的长度。θ表示两个向量之间的夹角。
这个公式揭示了点乘的重要意义:
点乘能够反映两个向量方向的一致程度。
当两个向量都不是零向量时,可以根据点乘结果判断夹角:
| 点乘结果 | 夹角关系 | 几何意义 |
|---|---|---|
| 大于 0 | 小于 90° | 两个向量方向大致一致 |
| 等于 0 | 等于 90° | 两个向量相互垂直 |
| 小于 0 | 大于 90° | 两个向量方向相对 |
注意:如果其中一个向量是零向量,点乘同样为零,但不能据此判断两个向量垂直。
3.3 点乘的实际应用
应用一:判断两个方向是否相近
假设三维场景中有一个摄像机和一个目标物体。
我们可以使用向量表示摄像机的观察方向,以及摄像机指向目标的方向。将两个方向向量归一化后进行点乘:
d = a · b
由于两个向量都是单位向量,因此:
d = cos(θ)
点乘结果越接近 1,两个方向越接近;越接近 -1,两个方向越相反。
在摄像机视野判断、角色朝向判断等场景中,可以利用这个性质进行方向比较。
应用二:计算光照强度
在三维图形学中,点乘是基础光照计算的重要组成部分。
假设:
n表示物体表面的单位法向量。l表示从表面指向光源的单位方向向量。
在简单的漫反射模型中,光照强度可以写成:
I = max(0, n · l)
当法向量与光源方向一致时,点乘较大,表面接收到的漫反射光照较强。
当两个方向垂直时,点乘为零,漫反射分量为零。
当点乘为负数时,说明表面朝向背离光源的一侧,通常将该分量截断为零。
因此,点乘可以帮助我们根据表面方向计算光照效果,使三维模型呈现不同的明暗变化。
应用三:计算向量投影
点乘还可以用于计算向量在指定方向上的投影。
如果 b 是单位向量,那么向量 a 在 b 方向上的有符号投影长度为:
s = a · b
例如,在分析物体运动时,可以利用点乘计算速度在某个方向上的分量,从而判断物体沿指定方向运动的快慢。
四、叉乘(Cross Product)
4.1 叉乘的定义
叉乘也叫向量积。与点乘不同,三维空间中两个向量叉乘的结果仍然是一个向量。
对于两个三维向量:
a = (ax, ay, az)
b = (bx, by, bz)
它们的叉乘公式为:
a × b = (
ay * bz - az * by,
az * bx - ax * bz,
ax * by - ay * bx
)
将三个分量分别写出来:
cx = ay * bz - az * by
cy = az * bx - ax * bz
cz = ax * by - ay * bx
计算时需要特别注意 Y 分量的顺序,避免将减法顺序写反。
4.2 叉乘的计算示例
假设:
a = (1, 2, 3)
b = (4, 5, 6)
根据叉乘公式:
cx = 2 * 6 - 3 * 5 = -3
cy = 3 * 4 - 1 * 6 = 6
cz = 1 * 5 - 2 * 4 = -3
因此:
a × b = (-3, 6, -3)
可以看到,叉乘的结果是一个新的三维向量。
4.3 叉乘的几何意义
叉乘最重要的几何性质是:
两个向量的叉乘结果,同时垂直于这两个向量。
如果:
c = a × b
那么:
c · a = 0
c · b = 0
为什么会这样?
可以将两个不平行的向量看成位于同一个平面内的两条方向线。它们张成了一个平面,而叉乘得到的向量同时垂直于这两条方向线,因此垂直于它们所在的平面。
所以,当我们已知一个平面内的两个不平行向量时,就可以通过叉乘计算这个平面的法向量。
叉乘的长度还满足:
|a × b| = |a| * |b| * sin(θ)
这个长度等于两个向量所张成的平行四边形的面积。
当两个向量平行或反平行时,sin(θ) = 0,叉乘结果为零向量。此时,无法仅通过这个零向量确定唯一的法向方向。
4.4 二维叉积:判断点在直线的左侧还是右侧
在二维几何中,我们经常需要判断一个点位于有向直线的左侧、右侧,还是直线上。
给定三个点:
A = (Ax, Ay)
B = (Bx, By)
P = (Px, Py)
先构造两个向量:
u = B - A
v = P - A
然后计算二维叉积:
cross = (Bx - Ax) * (Py - Ay)
- (By - Ay) * (Px - Ax)
在通常的数学坐标系中,X 轴向右、Y 轴向上:
cross > 0:点 P 位于有向直线 AB 的左侧。cross < 0:点 P 位于有向直线 AB 的右侧。cross = 0:三个点共线。
例如:
A = (0, 0)
B = (1, 0)
P = (0, 1)
代入公式:
cross = (1 - 0) * (1 - 0)
- (0 - 0) * (0 - 0)
= 1
结果大于零,因此点 P 位于有向直线 AB 的左侧。
这个方法常用于凸包算法、线段相交判断和多边形几何计算。
需要注意,屏幕坐标系通常以左上角为原点,Y 轴向下,因此视觉上的左右判断可能与数学坐标系相反。在实际程序中,如果涉及浮点数,还需要考虑误差容限。
4.5 叉乘的实际应用
应用一:计算三角形法向量
在三维模型中,一个三角形由三个顶点组成:
P0、P1、P2
首先构造三角形的两条边向量:
u = P1 - P0
v = P2 - P0
然后计算:
n = u × v
得到的向量垂直于这两条边,也就是垂直于三角形所在的平面,因此它是三角形的法向量。
如果需要单位法向量,可以继续归一化:
n_normalized = n / |n|
计算法向量时,需要注意顶点顺序。交换两条边的顺序,法向量就会反向。如果三个顶点共线,则无法得到有效的单位法向量。
应用二:计算三角形面积
由于叉乘的长度等于平行四边形面积,因此三角形面积可以表示为:
S = 0.5 * |u × v|
在三维网格处理中,可以利用这个公式计算三角形面积,进而进行网格面积统计和几何分析。
应用三:构建三维坐标系
在三维图形学中,我们经常需要构建摄像机的右方向、上方向和观察方向。
已知两个不平行的方向向量时,可以通过叉乘求出同时垂直于它们的第三个方向,再结合归一化操作,构建一组互相垂直的方向向量。
这类操作在摄像机控制、模型姿态计算和空间变换中十分常见。
应用四:使用叉乘判断点是否在三角形内
在三维图形学中,经常需要判断一个点是否位于某个三角形内部,例如鼠标点击模型时判断射线是否击中了三角形、进行碰撞检测等。叉乘可以帮助我们判断点相对于三角形三条边的位置,从而确定点是否在三角形内。
假设三角形的三个顶点为 A、B、C,需要判断的点为 P。首先计算三角形的法向量:
N = (B - A) × (C - A)
然后,分别计算点 P 相对于三条边的叉乘结果,并与法向量进行点乘:
d1 = N · ((B - A) × (P - A))
d2 = N · ((C - B) × (P - B))
d3 = N · ((A - C) × (P - C))
通过判断 d1、d2 和 d3 的符号,可以确定点 P 位于每条边的哪一侧。如果三个结果都非负,或者都非正,说明点 P 位于三角形内部或边界上;如果结果中同时存在正数和负数,则说明点 P 位于三角形外部。
4.6、使用 C++ 实现点乘与叉乘
理解数学公式之后,可以尝试自己实现一个简单的三维向量类。
#include <iostream>
struct Vec3 {
double x, y, z;
// 点乘:返回标量
double dot(const Vec3& other) const {
return x * other.x
+ y * other.y
+ z * other.z;
}
// 叉乘:返回向量
Vec3 cross(const Vec3& other) const {
return {
y * other.z - z * other.y,
z * other.x - x * other.z,
x * other.y - y * other.x
};
}
};
int main() {
Vec3 a{1, 2, 3};
Vec3 b{4, 5, 6};
std::cout << "Dot: " << a.dot(b) << '\n';
Vec3 c = a.cross(b);
std::cout << "Cross: "
<< c.x << ", "
<< c.y << ", "
<< c.z << '\n';
return 0;
}
运行结果:
Dot: 32
Cross: -3, 6, -3
通过这个例子可以看出,点乘和叉乘在代码层面上的区别也很明显:
- 点乘返回一个数值。
- 叉乘返回一个新的向量。
在实际的三维项目中,还可以在 Vec3 类中继续实现向量长度、归一化、加减法等功能,逐步构建自己的数学工具库。
五、点乘与叉乘的区别总结
| 对比项目 | 点乘 | 叉乘 |
|---|---|---|
| 英文名称 | Dot Product | Cross Product |
| 计算结果 | 标量 | 三维向量 |
| 核心公式 | ax*bx + ay*by + az*bz |
各分量交叉相乘后相减 |
| 几何意义 | 反映方向关系,与夹角相关 | 得到垂直于两个向量的方向 |
| 常见应用 | 方向判断、投影、漫反射光照 | 法向量、左右判断、面积计算 |
六、二维矩阵
6.1、二维几何变换的基本形式
假设二维平面上有一个点 P,其坐标为:
p = [ x ]
[ y ]
我们希望通过某种变换,将它变成新的点 P':
p' = A * p
其中:
- p 表示原始坐标。
- p' 表示变换后的坐标。
- A 表示变换矩阵。
矩阵 A 的不同形式,对应不同的几何操作。
需要注意:普通的 2×2 矩阵可以表示缩放、镜像、切变和绕原点的旋转,但不能直接通过矩阵乘法表示任意平移。
6.2、缩放矩阵
缩放用于改变物体在不同方向上的大小。

假设水平方向的缩放系数为 sx,垂直方向的缩放系数为 sy,则:
x' = sx * x
y' = sy * y
写成矩阵形式:
S = [ sx 0 ]
[ 0 sy ]
因此:
p' = S * p
展开后:
[ x' ] [ sx 0 ] [ x ]
[ y' ] = [ 0 sy ] [ y ]
1. 等比例缩放
如果 sx = sy = s,则:
S = [ s 0 ]
[ 0 s ]
物体在两个方向上按照相同的比例缩放,形状不会因为缩放而产生额外的拉伸。
2. 非等比例缩放
如果 sx 和 sy 不相等,则物体会在不同方向上产生不同程度的缩放。
例如:
sx = 2
sy = 3
原来的点:
p = [ 1 ]
[ 2 ]
缩放后:
p' = [ 2 ]
[ 6 ]
3. 缩放的特点
以原点为中心的缩放,会使点相对于原点的距离发生变化。
如果希望绕其他中心缩放,则需要先将缩放中心平移到原点,完成缩放后再平移回去。
6.3、镜像矩阵
镜像变换用于将点关于某条直线进行翻转。

1. 关于 x 轴镜像
关于 x 轴镜像时,x 坐标不变,y 坐标取反:
x' = x
y' = -y
因此:
Mx = [ 1 0 ]
[ 0 -1 ]
2. 关于 y 轴镜像
关于 y 轴镜像时,y 坐标不变,x 坐标取反:
x' = -x
y' = y
因此:
My = [ -1 0 ]
[ 0 1 ]
3. 关于原点镜像
关于原点镜像时,两个坐标都取反:
x' = -x
y' = -y
对应矩阵:
M = [ -1 0 ]
[ 0 -1 ]
4. 镜像的特点
镜像会改变物体的朝向,并使变换矩阵的行列式为 -1。
这里介绍的是关于坐标轴或原点的镜像。如果需要关于任意直线镜像,也可以利用平移、旋转和镜像组合实现。
五、切变矩阵
切变,也称为错切,用于使物体产生倾斜。

1. 水平切变
水平切变表示 x 坐标会受到 y 坐标的影响,而 y 坐标保持不变:
x' = x + kx * y
y' = y
其中 kx 为水平切变系数。
对应矩阵:
Hx = [ 1 kx ]
[ 0 1 ]
2. 垂直切变
垂直切变表示 y 坐标会受到 x 坐标的影响,而 x 坐标保持不变:
x' = x
y' = y + ky * x
对应矩阵:
Hy = [ 1 0 ]
[ ky 1 ]
3. 切变的特点
切变会改变物体的形状,但在上述二维切变中,矩阵的行列式仍然为 1,因此有向面积保持不变。
需要注意,切变通常会让原本与坐标轴平行的线段产生倾斜。
六、旋转矩阵
旋转用于让点绕原点旋转指定角度。
假设点 P 到原点的距离为 r,与 x 轴正方向的夹角为 alpha。

这里我们使用特殊点,设置正方形的边长就为1,来简单的推导一下旋转矩阵即可。
七、为什么需要齐次坐标?
前面介绍的缩放、镜像、切变和旋转,都可以通过 2×2 矩阵表示。
例如:
p' = A * p
但平移不同。
假设将点向右移动 tx,向上移动 ty,则:
x' = x + tx
y' = y + ty
写成向量形式:
p' = p + t
其中:
t = [ tx ]
[ ty ]
这里存在一个问题:平移需要在原始坐标上增加一个固定向量,而普通的 2×2 矩阵乘法无法直接产生这样的固定加法项。
为了将平移也统一表示为矩阵乘法,我们引入齐次坐标。

1. 扩展二维坐标
原来的二维点为:
p = [ x ]
[ y ]
将其扩展为:
ph = [ x ]
[ y ]
[ 1 ]
这就是二维点的齐次坐标。
增加的 1 并不意味着点真的进入了三维空间,而是增加了一个用于统一表达几何变换的坐标分量。
2. 使用齐次坐标表示平移
构造平移矩阵:
T = [ 1 0 tx ]
[ 0 1 ty ]
[ 0 0 1 ]
然后执行矩阵乘法:
[ x' ] [ 1 0 tx ] [ x ]
[ y' ] = [ 0 1 ty ] [ y ]
[ 1 ] [ 0 0 1 ] [ 1 ]
展开后:
x' = x + tx
y' = y + ty
这样,平移也可以通过矩阵乘法表示。
3. 将其他变换扩展到齐次坐标
只需要在原来的 2×2 矩阵右侧和下方补上相应的元素,就可以得到 3×3 齐次变换矩阵。
缩放矩阵:
S = [ sx 0 0 ]
[ 0 sy 0 ]
[ 0 0 1 ]
镜像矩阵(关于 x 轴):
Mx = [ 1 0 0 ]
[ 0 -1 0 ]
[ 0 0 1 ]
水平切变矩阵:
Hx = [ 1 kx 0 ]
[ 0 1 0 ]
[ 0 0 1 ]
旋转矩阵:
R = [ cos(theta) -sin(theta) 0 ]
[ sin(theta) cos(theta) 0 ]
[ 0 0 1 ]
平移矩阵:
T = [ 1 0 tx ]
[ 0 1 ty ]
[ 0 0 1 ]
由此可见,二维空间中的缩放、镜像、切变、旋转和平移,都可以统一表示为 3×3 矩阵。
这就是齐次坐标的一个重要作用。
八、多个矩阵如何组合?
当一个点需要依次经过多个变换时,可以将这些变换矩阵相乘,合并成一个总变换矩阵。
假设点 p 先缩放,再旋转,最后平移。
使用列向量时:
p' = T * R * S * p
其执行顺序是:
p
-> S * p
-> R * (S * p)
-> T * (R * (S * p))
因此,矩阵的执行顺序是从右向左。
也就是说:
最终矩阵 = 平移矩阵 * 旋转矩阵 * 缩放矩阵
为什么顺序不能随意改变?
矩阵乘法通常不满足交换律:
A * B != B * A
例如,先缩放再平移,与先平移再缩放,通常会得到不同的结果。
所以,在组合变换时,不仅要确定有哪些变换,还必须明确它们的执行顺序。
组合变换的优势
如果有多个点需要执行相同的变换,可以先计算总变换矩阵:
A = T * R * S
之后,每个点只需要计算:
p' = A * p
这样可以减少重复的矩阵运算,也让变换过程更容易管理。
九、如何绕任意中心进行变换?
前面介绍的缩放和旋转矩阵,默认都是以原点为中心进行操作。
但实际应用中,我们可能希望:
- 绕某个指定点旋转。
- 以物体中心为基准缩放。
- 关于不经过原点的直线进行镜像。
此时,可以使用一个通用的思路:
先将目标中心平移到原点,再执行变换,最后平移回原来的位置。
假设目标中心为:
C = (cx, cy)
第一步:平移到原点
构造平移矩阵:
T1 = [ 1 0 -cx ]
[ 0 1 -cy ]
[ 0 0 1 ]
执行:
p1 = T1 * p
此时,原来的中心点 C 会移动到原点。
第二步:执行目标变换
假设要绕该中心旋转 theta,则:
p2 = R * p1
其中 R 是二维旋转矩阵的齐次形式。
第三步:平移回原位置
构造反向平移矩阵:
T2 = [ 1 0 cx ]
[ 0 1 cy ]
[ 0 0 1 ]
执行:
p' = T2 * p2
将三个步骤合并:
p' = T2 * R * T1 * p
因此,绕任意中心旋转的总变换矩阵为:
A = T2 * R * T1
也就是:
A = T(cx, cy) * R(theta) * T(-cx, -cy)
这里的顺序非常重要:先将中心移到原点,再旋转,最后移回去。
推广到缩放和镜像
这个方法同样适用于其他变换。
绕指定中心缩放:
A = T(cx, cy) * S * T(-cx, -cy)
关于不经过原点的直线进行镜像,也可以通过平移、旋转、镜像和逆向变换组合实现。
对于任意方向的镜像轴,通常还需要先将镜像轴旋转到与坐标轴对齐的位置,再进行镜像,最后撤销旋转和平移。
十、三维矩阵
在二维空间中,我们使用 2×2 矩阵表示缩放、旋转、镜像和切变等几何变换,并通过齐次坐标将平移也统一到矩阵运算中。
当几何对象从二维平面扩展到三维空间时,情况也类似,只不过坐标从 (x, y) 变成了 (x, y, z)。我们需要考虑三个坐标轴方向上的变化,以及物体绕不同轴旋转的情况。
三维几何变换矩阵广泛应用于计算机图形学、游戏开发、CAD 建模、机器人运动控制和三维动画等领域。
所以三维矩阵其实和二维矩阵差别也不大,我们这边主要提一下三维矩阵的旋转矩阵。
旋转矩阵
在二维空间中,一个点可以表示为:
p = (x, y)
旋转角度为 θ 时,旋转矩阵为:
R(θ) = [ cosθ -sinθ ]
[ sinθ cosθ ]
因此:
x' = x*cosθ - y*sinθ
y' = x*sinθ + y*cosθ
二维旋转只有一个旋转平面,即 XY 平面。可以将它理解为绕垂直于该平面的 Z 轴旋转。
到了三维空间,点的坐标变为:
p = (x, y, z)
空间中有三个基本坐标轴,因此我们可以分别考虑绕 X、Y、Z 轴旋转。
这三个旋转矩阵是构建一般三维旋转的基础。
二、绕 X 轴旋转
绕 X 轴旋转时,点在 X 轴方向上的位置保持不变,变化发生在 YZ 平面内。
因此:
x' = x
而 Y、Z 坐标的变化,与二维平面中的旋转形式相同:
y' = y*cosθ - z*sinθ
z' = y*sinθ + z*cosθ
将这三个坐标关系写成矩阵形式:
Rx(θ) = [ 1 0 0 ]
[ 0 cosθ -sinθ ]
[ 0 sinθ cosθ ]
如果使用齐次坐标,则可以扩展为 4×4 矩阵:
Rx(θ) = [ 1 0 0 0 ]
[ 0 cosθ -sinθ 0 ]
[ 0 sinθ cosθ 0 ]
[ 0 0 0 1 ]
这里需要注意一个关键点:绕 X 轴旋转,本质上是在 YZ 平面内执行二维旋转。
X 坐标不变,只有 Y、Z 坐标参与旋转。
三、绕 Y 轴旋转
绕 Y 轴旋转时,Y 坐标保持不变,变化发生在 XZ 平面内。
对应的坐标关系为:
x' = x*cosθ + z*sinθ
y' = y
z' = -x*sinθ + z*cosθ
因此,旋转矩阵为:
Ry(θ) = [ cosθ 0 sinθ ]
[ 0 1 0 ]
[ -sinθ 0 cosθ ]
齐次形式为:
Ry(θ) = [ cosθ 0 sinθ 0 ]
[ 0 1 0 0 ]
[ -sinθ 0 cosθ 0 ]
[ 0 0 0 1 ]
与绕 X 轴旋转类似,绕 Y 轴旋转时,只有垂直于旋转轴的两个坐标分量发生变化。
这里容易产生一个疑问:为什么矩阵中的正弦项符号与绕 X 轴旋转时不同?
我们都知道xyz,xy向量进行叉乘可以得出z,yz向量叉乘可以得出x,y向量则不是xz叉乘得出的,而是zx叉乘得出来的,所以这里的sinθ的正负是反过来的。
四、绕 Z 轴旋转
绕 Z 轴旋转时,Z 坐标保持不变,变化发生在 XY 平面内。
其坐标关系为:
x' = x*cosθ - y*sinθ
y' = x*sinθ + y*cosθ
z' = z
因此:
Rz(θ) = [ cosθ -sinθ 0 ]
[ sinθ cosθ 0 ]
[ 0 0 1 ]
齐次形式为:
Rz(θ) = [ cosθ -sinθ 0 0 ]
[ sinθ cosθ 0 0 ]
[ 0 0 1 0 ]
[ 0 0 0 1 ]
可以发现,绕 Z 轴旋转的左上角 2×2 子矩阵,正好就是二维旋转矩阵。
这并不是巧合,而是因为 XY 平面垂直于 Z 轴。绕 Z 轴旋转时,三维旋转实际上退化成了 XY 平面内的二维旋转。