第2章:向量与线性代数基础
2.1 向量基础
2.1.1 什么是向量
在数学中,向量(Vector)是一个既有大小(magnitude)又有方向(direction)的量。与之相对的是标量(Scalar),标量只有大小没有方向。
标量 vs 向量
标量(Scalar):
- 温度:25°C
- 质量:5kg
- 时间:3秒
特点:只有大小
向量(Vector):
- 位移:向东走5米
- 速度:每秒向北3米
- 力:向下100牛顿
特点:有大小和方向
向量的几何表示:
▲
│ 方向
│
○───┼───────►
起点 终点(箭头)
向量可以自由移动(只要方向和大小不变)2.1.2 向量的表示方法
在计算机图形学中,我们使用坐标来表示向量:
2D 向量表示
y
▲
│
│ • v = (3, 2)
│ ╱│
│ ╱ │ 2
│ ╱ │
│╱ │
────────────────┼────┴────► x
│ 3
│
向量 v 可以表示为:
- 坐标形式:v = (3, 2)
- 列向量形式:
┌ ┐
v = │ 3 │
│ 2 │
└ ┘
3D 向量表示
y
▲
│
│ • v = (2, 3, 4)
│ ╱
│ ╱
│ ╱
│╱
────────────────┼──────────► x
╱│
╱ │
╱ │
▼
z
向量 v = (2, 3, 4)
- x 分量:2
- y 分量:3
- z 分量:42.1.3 JavaScript 中的向量表示
javascript
// 简单对象表示
const vector2D = { x: 3, y: 2 };
const vector3D = { x: 2, y: 3, z: 4 };
// 数组表示
const vec2 = [3, 2];
const vec3 = [2, 3, 4];
// 类表示
class Vector2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
}
class Vector3 {
constructor(x = 0, y = 0, z = 0) {
this.x = x;
this.y = y;
this.z = z;
}
}
const v2 = new Vector2(3, 2);
const v3 = new Vector3(2, 3, 4);2.2 向量运算
2.2.1 向量加法
向量加法遵循平行四边形法则或首尾相接法则:
向量加法的几何意义
方法1:首尾相接法
a + b 的结果:
b
○─────────►
│ ╲
│ ╲
a │ ╲ a + b
│ ╲
│ ╲
▼ ▼
────────────────►
方法2:平行四边形法则
╱╲
╱ ╲
╱ ╲ b
a ╱ ╲
╱ a+b ╲
○─────────►
b
数学表达式:
如果 a = (a₁, a₂) 且 b = (b₁, b₂),则:
a + b = (a₁ + b₁, a₂ + b₂)
例如:
(3, 2) + (1, 4) = (3+1, 2+4) = (4, 6)2.2.2 向量减法
向量减法可以理解为加上一个反向向量:
向量减法的几何意义
a - b 等于 a + (-b)
从 b 指向 a 的向量
▲ a
│
│
│ a - b
│ ╱
│ ╱
│ ╱
○─────────► b
数学表达式:
a - b = (a₁ - b₁, a₂ - b₂)
例如:
(5, 4) - (2, 1) = (5-2, 4-1) = (3, 3)
应用:计算两点之间的向量
点 A = (1, 2)
点 B = (4, 6)
从 A 到 B 的向量 = B - A = (4-1, 6-2) = (3, 4)2.2.3 标量乘法
向量乘以标量会改变向量的长度,如果是负数还会改变方向:
标量乘法
设向量 v = (2, 3)
2v = (4, 6) ← 长度变为2倍
0.5v = (1, 1.5) ← 长度变为一半
-v = (-2, -3) ← 方向相反
几何示意:
原向量 v:
▲
│ v
│
○
2v(两倍长度):
▲
│
│ 2v
│
│
○
-v(反向):
○
│
│ -v
▼
数学表达式:
如果 v = (v₁, v₂) 且 k 是标量,则:
kv = (k·v₁, k·v₂)2.2.4 代码实现
javascript
class Vector2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
// 向量加法
add(v) {
return new Vector2(this.x + v.x, this.y + v.y);
}
// 向量减法
subtract(v) {
return new Vector2(this.x - v.x, this.y - v.y);
}
// 标量乘法
scale(scalar) {
return new Vector2(this.x * scalar, this.y * scalar);
}
// 取反
negate() {
return new Vector2(-this.x, -this.y);
}
}
// 使用示例
const a = new Vector2(3, 4);
const b = new Vector2(1, 2);
console.log(a.add(b)); // Vector2 { x: 4, y: 6 }
console.log(a.subtract(b)); // Vector2 { x: 2, y: 2 }
console.log(a.scale(2)); // Vector2 { x: 6, y: 8 }
console.log(a.negate()); // Vector2 { x: -3, y: -4 }2.3 点积(内积)
2.3.1 点积的定义
点积(Dot Product)是两个向量的一种乘法运算,结果是一个标量:
点积的两种定义
代数定义:
a · b = a₁b₁ + a₂b₂ + a₃b₃
几何定义:
a · b = |a| |b| cos(θ)
其中 θ 是两个向量之间的夹角
例子:
a = (1, 2, 3)
b = (4, 5, 6)
a · b = 1×4 + 2×5 + 3×6
= 4 + 10 + 18
= 322.3.2 点积的几何意义
点积与夹角的关系
b
╱
╱
╱ θ
╱
○─────────► a
a · b = |a| |b| cos(θ)
因此:
cos(θ) = (a · b) / (|a| |b|)
点积结果的含义:
┌──────────────────────────────────────────────────────┐
│ a · b > 0 → θ < 90° → 两向量夹角为锐角 │
│ a · b = 0 → θ = 90° → 两向量垂直 │
│ a · b < 0 → θ > 90° → 两向量夹角为钝角 │
└──────────────────────────────────────────────────────┘
特别地:
- a · a = |a|² (向量点积自己等于长度的平方)2.3.3 点积的应用
应用1:计算两向量夹角
function angleBetween(a, b) {
const dot = a.x * b.x + a.y * b.y;
const lenA = Math.sqrt(a.x * a.x + a.y * a.y);
const lenB = Math.sqrt(b.x * b.x + b.y * b.y);
const cosTheta = dot / (lenA * lenB);
return Math.acos(cosTheta); // 弧度
}
应用2:判断向量方向关系
function checkDirection(a, b) {
const dot = a.x * b.x + a.y * b.y;
if (dot > 0) return "同向(夹角 < 90°)";
if (dot < 0) return "反向(夹角 > 90°)";
return "垂直(夹角 = 90°)";
}
应用3:投影
向量 a 在向量 b 上的投影长度:
b
╱│
╱ │ a
╱ │
╱ θ │
○─────●────┴─────► b
↑
投影点
投影长度 = |a| cos(θ) = (a · b) / |b|
投影向量 = ((a · b) / |b|²) × b2.3.4 代码实现
javascript
class Vector2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
// 点积
dot(v) {
return this.x * v.x + this.y * v.y;
}
// 向量长度
length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
// 长度的平方(避免开方运算)
lengthSquared() {
return this.x * this.x + this.y * this.y;
}
// 计算与另一向量的夹角(弧度)
angleTo(v) {
const dot = this.dot(v);
const len = this.length() * v.length();
return Math.acos(Math.max(-1, Math.min(1, dot / len)));
}
// 投影到另一向量
projectOnto(v) {
const scalar = this.dot(v) / v.lengthSquared();
return new Vector2(v.x * scalar, v.y * scalar);
}
// 判断是否与另一向量垂直
isPerpendicularTo(v, epsilon = 0.0001) {
return Math.abs(this.dot(v)) < epsilon;
}
}
// 使用示例
const a = new Vector2(3, 4);
const b = new Vector2(4, 3);
console.log(a.dot(b)); // 24
console.log(a.length()); // 5
console.log(a.angleTo(b)); // 约 0.284 弧度 ≈ 16.26°
// 垂直测试
const v1 = new Vector2(1, 0);
const v2 = new Vector2(0, 1);
console.log(v1.isPerpendicularTo(v2)); // true2.4 叉积(外积)
2.4.1 叉积的定义
叉积(Cross Product)是两个向量的另一种乘法运算,结果是一个向量(仅限于3D向量):
叉积的定义(3D)
a × b = (a₂b₃ - a₃b₂, a₃b₁ - a₁b₃, a₁b₂ - a₂b₁)
使用行列式表示:
| i j k |
a × b = | a₁ a₂ a₃ |
| b₁ b₂ b₃ |
展开:
= i(a₂b₃ - a₃b₂) - j(a₁b₃ - a₃b₁) + k(a₁b₂ - a₂b₁)
例子:
a = (1, 0, 0)
b = (0, 1, 0)
a × b = (0×0 - 0×1, 0×0 - 1×0, 1×1 - 0×0)
= (0, 0, 1)
结果是 z 轴正方向的单位向量2.4.2 叉积的几何意义
叉积的结果
1. 方向:垂直于 a 和 b 组成的平面
2. 大小:|a × b| = |a| |b| sin(θ)(等于平行四边形面积)
右手定则:
四指从 a 转向 b,大拇指指向 a × b 的方向
a × b
▲
│
│
○────────┤
╱ │
a └──────► b
注意:叉积不满足交换律
a × b ≠ b × a
实际上:a × b = -(b × a)2.4.3 2D 中的"叉积"
虽然严格意义上2D向量没有叉积,但我们可以定义一个类似的运算:
2D 叉积(返回标量)
a × b = a₁b₂ - a₂b₁
这个值的几何意义:
1. 结果的绝对值 = 平行四边形面积
2. 结果的符号 = 确定旋转方向
b
╱
╱
╱
○──────●────► a
如果 a × b > 0:b 在 a 的逆时针方向(左侧)
如果 a × b < 0:b 在 a 的顺时针方向(右侧)
如果 a × b = 0:a 和 b 平行2.4.4 叉积的应用
应用1:计算法向量(3D)
给定平面上的两个向量,叉积得到平面的法向量
function surfaceNormal(v1, v2) {
return v1.cross(v2).normalize();
}
应用2:判断点在线段的哪一侧(2D)
点 P 在 AB 的哪一侧?
P •
╲
╲
A●──────●B
向量 AB = B - A
向量 AP = P - A
如果 AB × AP > 0,P 在左侧
如果 AB × AP < 0,P 在右侧
应用3:计算三角形面积
三角形 ABC 的面积 = |AB × AC| / 22.4.5 代码实现
javascript
// 3D 向量
class Vector3 {
constructor(x = 0, y = 0, z = 0) {
this.x = x;
this.y = y;
this.z = z;
}
// 叉积
cross(v) {
return new Vector3(
this.y * v.z - this.z * v.y,
this.z * v.x - this.x * v.z,
this.x * v.y - this.y * v.x
);
}
// 长度
length() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
}
// 归一化
normalize() {
const len = this.length();
if (len === 0) return new Vector3();
return new Vector3(this.x / len, this.y / len, this.z / len);
}
}
// 2D 向量
class Vector2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
// 2D "叉积"(返回标量)
cross(v) {
return this.x * v.y - this.y * v.x;
}
// 判断点在线段的哪一侧
static pointSide(lineStart, lineEnd, point) {
const line = new Vector2(
lineEnd.x - lineStart.x,
lineEnd.y - lineStart.y
);
const toPoint = new Vector2(
point.x - lineStart.x,
point.y - lineStart.y
);
const cross = line.cross(toPoint);
if (cross > 0) return 'left';
if (cross < 0) return 'right';
return 'on_line';
}
// 计算三角形面积
static triangleArea(a, b, c) {
const ab = new Vector2(b.x - a.x, b.y - a.y);
const ac = new Vector2(c.x - a.x, c.y - a.y);
return Math.abs(ab.cross(ac)) / 2;
}
}
// 使用示例
const v1 = new Vector3(1, 0, 0);
const v2 = new Vector3(0, 1, 0);
const v3 = v1.cross(v2);
console.log(v3); // Vector3 { x: 0, y: 0, z: 1 }
// 判断点的位置
const a = { x: 0, y: 0 };
const b = { x: 10, y: 0 };
const p = { x: 5, y: 3 };
console.log(Vector2.pointSide(a, b, p)); // 'left'
// 计算三角形面积
const t1 = { x: 0, y: 0 };
const t2 = { x: 4, y: 0 };
const t3 = { x: 0, y: 3 };
console.log(Vector2.triangleArea(t1, t2, t3)); // 62.5 向量规范化
2.5.1 单位向量
单位向量(Unit Vector)是长度为1的向量,它只表示方向:
单位向量
任意非零向量 v 可以被规范化为单位向量:
v
v̂ = ─────
|v|
视觉表示:
原向量(长度为5):
────────────────────► v
单位向量(长度为1):
────► v̂
例子:
v = (3, 4)
|v| = √(3² + 4²) = √25 = 5
v̂ = (3/5, 4/5) = (0.6, 0.8)
验证:|v̂| = √(0.6² + 0.8²) = √(0.36 + 0.64) = √1 = 1 ✓2.5.2 为什么需要单位向量
单位向量的用途
1. 表示纯方向
- 光线方向
- 表面法向量
- 运动方向
2. 简化计算
- |v̂| = 1,很多公式可以简化
- 例如:a · v̂ 直接得到投影长度
3. 着色器中的常见需求
- 法向量必须是单位向量
- 光照计算需要单位方向向量2.5.3 代码实现
javascript
class Vector2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
// 向量长度
length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
// 规范化(返回新向量)
normalize() {
const len = this.length();
if (len === 0) {
console.warn('Cannot normalize zero vector');
return new Vector2();
}
return new Vector2(this.x / len, this.y / len);
}
// 就地规范化(修改自身)
normalizeSelf() {
const len = this.length();
if (len === 0) {
console.warn('Cannot normalize zero vector');
return this;
}
this.x /= len;
this.y /= len;
return this;
}
// 检查是否为单位向量
isUnit(epsilon = 0.0001) {
return Math.abs(this.length() - 1) < epsilon;
}
// 设置长度(保持方向)
setLength(newLength) {
return this.normalize().scale(newLength);
}
scale(scalar) {
return new Vector2(this.x * scalar, this.y * scalar);
}
}
// 使用示例
const v = new Vector2(3, 4);
console.log(v.length()); // 5
console.log(v.normalize()); // Vector2 { x: 0.6, y: 0.8 }
console.log(v.normalize().length()); // 1 (或非常接近1)
console.log(v.normalize().isUnit()); // true2.6 向量空间
2.6.1 基本概念
向量空间的概念
向量空间是由向量组成的集合,满足加法和标量乘法的封闭性。
2D 向量空间:
- 所有形如 (x, y) 的向量
- 任意两个向量相加仍在空间中
- 任意向量乘标量仍在空间中
基向量(Basis Vectors):
在 2D 中,标准基向量为:
- e₁ = (1, 0) ─ x 轴方向的单位向量
- e₂ = (0, 1) ─ y 轴方向的单位向量
任何 2D 向量都可以表示为基向量的线性组合:
v = (3, 4) = 3·e₁ + 4·e₂
坐标系示意:
e₂
▲
│
│ (0,1)
│
────────┼────────► e₁
│ (1,0)
│2.6.2 线性组合与生成空间
线性组合
给定向量 v₁, v₂, ..., vₙ 和标量 c₁, c₂, ..., cₙ
线性组合:c₁v₁ + c₂v₂ + ... + cₙvₙ
生成空间(Span)
向量组的所有可能线性组合构成的集合
例如:
span{(1,0), (0,1)} = 整个 2D 平面
span{(1,0)} = x 轴(一条直线)
span{(1,2), (2,4)} = 过原点的一条直线(因为 (2,4) = 2·(1,2))2.6.3 线性无关
线性无关
如果向量组中没有任何一个向量可以用其他向量的线性组合表示,
则称这组向量线性无关。
判断方法:
2D 中两个向量 a, b 线性无关 ⟺ a × b ≠ 0(不平行)
例子:
v₁ = (1, 2)
v₂ = (3, 4)
v₁ × v₂ = 1×4 - 2×3 = 4 - 6 = -2 ≠ 0
所以 v₁, v₂ 线性无关,它们可以作为 2D 空间的基。2.6.4 坐标变换基础
不同基下的坐标
同一个向量在不同的基下有不同的坐标表示。
例子:
标准基 {e₁=(1,0), e₂=(0,1)} 下:
v = (3, 4)
新基 {b₁=(1,1), b₂=(1,-1)} 下:
v = (3, 4) = ?·b₁ + ?·b₂
解方程:
3 = 1·c₁ + 1·c₂
4 = 1·c₁ + (-1)·c₂
解得:c₁ = 3.5, c₂ = -0.5
验证:
3.5·(1,1) + (-0.5)·(1,-1) = (3.5, 3.5) + (-0.5, 0.5) = (3, 4) ✓2.7 完整向量类实现
javascript
/**
* 完整的 2D 向量类
*/
class Vec2 {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
// ========== 静态工厂方法 ==========
static zero() {
return new Vec2(0, 0);
}
static one() {
return new Vec2(1, 1);
}
static unitX() {
return new Vec2(1, 0);
}
static unitY() {
return new Vec2(0, 1);
}
static fromAngle(angle) {
return new Vec2(Math.cos(angle), Math.sin(angle));
}
static fromArray(arr) {
return new Vec2(arr[0] || 0, arr[1] || 0);
}
static random(min = 0, max = 1) {
const range = max - min;
return new Vec2(
Math.random() * range + min,
Math.random() * range + min
);
}
// ========== 基本运算 ==========
clone() {
return new Vec2(this.x, this.y);
}
copy(v) {
this.x = v.x;
this.y = v.y;
return this;
}
set(x, y) {
this.x = x;
this.y = y;
return this;
}
add(v) {
return new Vec2(this.x + v.x, this.y + v.y);
}
addSelf(v) {
this.x += v.x;
this.y += v.y;
return this;
}
subtract(v) {
return new Vec2(this.x - v.x, this.y - v.y);
}
subtractSelf(v) {
this.x -= v.x;
this.y -= v.y;
return this;
}
scale(scalar) {
return new Vec2(this.x * scalar, this.y * scalar);
}
scaleSelf(scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
}
divide(scalar) {
if (scalar === 0) {
console.warn('Division by zero');
return new Vec2();
}
return new Vec2(this.x / scalar, this.y / scalar);
}
negate() {
return new Vec2(-this.x, -this.y);
}
negateSelf() {
this.x = -this.x;
this.y = -this.y;
return this;
}
// ========== 向量积 ==========
dot(v) {
return this.x * v.x + this.y * v.y;
}
cross(v) {
return this.x * v.y - this.y * v.x;
}
// ========== 长度与规范化 ==========
length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
lengthSquared() {
return this.x * this.x + this.y * this.y;
}
normalize() {
const len = this.length();
if (len === 0) return new Vec2();
return new Vec2(this.x / len, this.y / len);
}
normalizeSelf() {
const len = this.length();
if (len > 0) {
this.x /= len;
this.y /= len;
}
return this;
}
setLength(length) {
return this.normalize().scaleSelf(length);
}
limit(maxLength) {
const lenSq = this.lengthSquared();
if (lenSq > maxLength * maxLength) {
return this.normalize().scale(maxLength);
}
return this.clone();
}
// ========== 角度与旋转 ==========
angle() {
return Math.atan2(this.y, this.x);
}
angleTo(v) {
const dot = this.dot(v);
const len = this.length() * v.length();
if (len === 0) return 0;
return Math.acos(Math.max(-1, Math.min(1, dot / len)));
}
signedAngleTo(v) {
return Math.atan2(this.cross(v), this.dot(v));
}
rotate(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
return new Vec2(
this.x * cos - this.y * sin,
this.x * sin + this.y * cos
);
}
rotateSelf(angle) {
const cos = Math.cos(angle);
const sin = Math.sin(angle);
const x = this.x * cos - this.y * sin;
const y = this.x * sin + this.y * cos;
this.x = x;
this.y = y;
return this;
}
// ========== 投影与反射 ==========
projectOnto(v) {
const lenSq = v.lengthSquared();
if (lenSq === 0) return new Vec2();
const scalar = this.dot(v) / lenSq;
return v.scale(scalar);
}
reject(v) {
return this.subtract(this.projectOnto(v));
}
reflect(normal) {
const d = 2 * this.dot(normal);
return new Vec2(
this.x - d * normal.x,
this.y - d * normal.y
);
}
// ========== 距离计算 ==========
distanceTo(v) {
const dx = this.x - v.x;
const dy = this.y - v.y;
return Math.sqrt(dx * dx + dy * dy);
}
distanceSquaredTo(v) {
const dx = this.x - v.x;
const dy = this.y - v.y;
return dx * dx + dy * dy;
}
manhattanDistanceTo(v) {
return Math.abs(this.x - v.x) + Math.abs(this.y - v.y);
}
// ========== 插值 ==========
lerp(v, t) {
return new Vec2(
this.x + (v.x - this.x) * t,
this.y + (v.y - this.y) * t
);
}
// ========== 比较 ==========
equals(v, epsilon = 0.0001) {
return Math.abs(this.x - v.x) < epsilon &&
Math.abs(this.y - v.y) < epsilon;
}
isZero(epsilon = 0.0001) {
return this.lengthSquared() < epsilon * epsilon;
}
isUnit(epsilon = 0.0001) {
return Math.abs(this.lengthSquared() - 1) < epsilon;
}
// ========== 垂直向量 ==========
perpendicular() {
return new Vec2(-this.y, this.x);
}
perpendicularCW() {
return new Vec2(this.y, -this.x);
}
// ========== 工具方法 ==========
toArray() {
return [this.x, this.y];
}
toString() {
return `Vec2(${this.x.toFixed(4)}, ${this.y.toFixed(4)})`;
}
toFixed(digits = 2) {
return `(${this.x.toFixed(digits)}, ${this.y.toFixed(digits)})`;
}
}
// 导出
export default Vec2;2.8 本章小结
核心概念
| 概念 | 定义 | 公式 |
|---|---|---|
| 向量 | 有大小和方向的量 | v = (x, y) |
| 向量加法 | 首尾相接或平行四边形法则 | a + b = (a₁+b₁, a₂+b₂) |
| 标量乘法 | 改变向量长度 | kv = (kv₁, kv₂) |
| 点积 | 两向量乘法,结果为标量 | a·b = a₁b₁ + a₂b₂ |
| 叉积 | 两向量乘法,结果为向量 | a×b = (a₂b₃-a₃b₂, ...) |
| 单位向量 | 长度为1的向量 | v̂ = v/ |
| 向量长度 | 向量的大小 |
关键公式
向量长度: |v| = √(v₁² + v₂² + v₃²)
点积: a · b = |a||b|cos(θ) = a₁b₁ + a₂b₂ + a₃b₃
叉积大小: |a × b| = |a||b|sin(θ)
夹角: cos(θ) = (a · b) / (|a||b|)
投影长度: proj = (a · b) / |b|
2D叉积: a × b = a₁b₂ - a₂b₁图形学中的应用
| 操作 | 应用场景 |
|---|---|
| 向量加法 | 位移、力的合成、速度叠加 |
| 向量减法 | 计算方向向量、相对位置 |
| 点积 | 光照计算、判断方向、投影 |
| 叉积 | 法向量、判断左右、面积计算 |
| 规范化 | 方向向量、法向量 |
| 投影 | 阴影、碰撞检测 |
下一章预告:在第3章中,我们将学习矩阵与变换,理解如何用矩阵表示旋转、缩放、平移等几何变换。
文档版本:v1.0
字数统计:约 10,000 字
