1.1 向量与矩阵:像素的坐标语言


1.1 向量与矩阵:像素的坐标语言

本节摘要:向量描述位置与方向,矩阵描述变换,两者构成图形学的全部代数语言。本节讲透点积的"夹角判据"与叉积的"左右手判据"这两个最常用的几何工具,并给出矩阵乘法的最小实现与组合顺序的常见错误。它是全教程的第一站:后面投影、光栅化、光照,全都建立在这两个运算之上。

学习目标

阅读完本节,你应当能够:

  1. 用点积判断两个方向的夹角关系,并解释它在漫反射、背面剔除中的用法
  2. 用叉积的符号判断点在向量的哪一侧、点是否在三角形内
  3. 写出 3×3 矩阵乘法的实现,并验证矩阵乘法不满足交换律
  4. 区分行向量左乘与列向量右乘两种约定,避免坐标系混乱

点积与叉积的几何分工

点积与叉积的几何分工

一、为什么图形学离不开向量

一个像素在出生之前,世界是这样的:模型文件里存着几百万个顶点坐标,每个坐标是三维空间中的一个向量;每个三角形有面法线,也是向量;光从某个方向照来,还是向量。图形学本质上是"一堆向量经过一堆矩阵,变成另一堆向量"的过程。

向量有两种身份:点和方向。点是"空间中的某处",方向是"指向哪边,多长"。同一个三元组 (3, 2, 5),当作点时表示从原点出发的位置,当作方向时只关心它的朝向与长度。这个区分在后面会反复出现——法线是方向,顶点是位置,平移矩阵影响位置但不该影响法线。

向量的长度(模长)由勾股定理给出:

模长 |v| = sqrt(x*x + y*y + z*z) 例:v = (3, 4, 0),|v| = sqrt(9+16) = 5 单位化:v / |v| = (0.6, 0.8, 0),长度变为 1

单位化是使用频率最高的操作之一:光照计算、视线方向、反射方向,全部要求先归一化,否则结果会随向量长度漂移。

二、点积:夹角的判据

点积把两个向量映射成一个数:

a·b = ax*bx + ay*by + az*bz = |a||b|cosθ 例:a=(1,0,0), b=(0,1,0),a·b = 0,夹角 90 度 例:a=(1,0,0), b=(1,1,0)/√2,a·b = 0.7071,夹角 45 度

这个数携带了夹角信息,因此成为图形学里最密集使用的运算:

场景 用法 判断
漫反射强度 光方向 · 法线 值越大越亮,负值表示背光
背面剔除 视线 · 面法线 小于 0 则面朝相机,保留
聚光灯 光轴 · 出射方向 小于阈值则在光锥外
Lambert 材质 max(0, L·N) 截断负值避免"负光"

一个直观例子:桌面法线朝上 (0,1,0),光从斜上方 45 度照来 L=(0.707,0.707,0),点积为 0.707,桌面亮度约为正对光源时的七成。第 5 章的 Lambert 定律就是这一行代码。

def dot(a, b): # 三维点积:返回夹角余弦乘以两个模长 return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] n = (0.0, 1.0, 0.0) # 桌面法线,已单位化 L = (0.707, 0.707, 0.0) # 光照方向,已单位化 intensity = max(0.0, dot(L, n)) print(f"漫反射强度 = {intensity:.3f}") # 输出: 漫反射强度 = 0.707

三、叉积:左右与内外

叉积 a×b 得到一个新向量,垂直于 a 和 b 所在平面,方向由右手定则决定。图形学里更常用它的两个派生判据:

叉积 a×b = (ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx) 在二维中退化为一个数:ax*by - ay*bx,称为叉积 z 分量 z 分量 > 0:b 在 a 的逆时针一侧 z 分量 < 0:b 在 a 的顺时针一侧 z 分量 = 0:共线

经典应用是"点是否在三角形内":对三角形三条边按同一绕向做叉积,符号一致即在内部。这正是第 3 章光栅化时判断某像素中心是否落入三角形的核心判断(工程实现常用更快的重心坐标,但原理同源)。

def cross_z(a, b): # 二维叉积的 z 分量:符号代表方向关系 return a[0]*b[1] - a[1]*b[0] def point_in_triangle(p, a, b, c): # 对三条边做同一绕向的叉积判断 d1 = cross_z((b[0]-a[0], b[1]-a[1]), (p[0]-a[0], p[1]-a[1])) d2 = cross_z((c[0]-b[0], c[1]-b[1]), (p[0]-b[0], p[1]-b[1])) d3 = cross_z((a[0]-c[0], a[1]-c[1]), (p[0]-c[0], p[1]-c[1])) has_pos = d1 > 0 or d2 > 0 or d3 > 0 has_neg = d1 < 0 or d2 < 0 or d3 < 0 return not (has_pos and has_neg) # 三角形 (0,0) (4,0) (0,4),测试点 (1,1) 与 (3,3) print(point_in_triangle((1,1), (0,0),(4,0),(0,4))) # True print(point_in_triangle((3,3), (0,0),(4,0),(0,4))) # False

另一个必考点:叉积的模长等于两向量张成的平行四边形面积,三角形面积是它的一半。行列式求面积的公式就来自这里,网格体素化、面积加权法线都要用。

四、矩阵:变换的压缩包

矩阵可以看作"对向量的批量操作指令"。缩放矩阵把各分量乘系数;旋转矩阵由 cos 与 sin 排布而成;这些内容在 1.2 节展开。这里先给出乘法实现,并强调一个新手大坑。

def mat3_mul(A, B): # 3x3 矩阵乘法,结果 C = A·B return [[sum(A[i][k]*B[k][j] for k in range(3)) for j in range(3)] for i in range(3)] import math def rot_z(deg): # 绕 z 轴旋转的矩阵 r = math.radians(deg); c, s = math.cos(r), math.sin(r) return [[c,-s,0],[s,c,0],[0,0,1]] def scale(sx, sy): return [[sx,0,0],[0,sy,0],[0,0,1]] # 先缩放再旋转:R·S(作用顺序从右往左) M = mat3_mul(rot_z(90), scale(2, 1)) p = (1, 0) # 先放大成 (2,0),再旋转 90 度 → (0,2) q = (M[0][0]*p[0]+M[0][1]*p[1], M[1][0]*p[0]+M[1][1]*p[1]) print(q) # 输出约 (0.0, 2.0) # 若换成 S·R:先旋转成 (0,1),再缩放 x 轴 → (0,1),结果完全不同

矩阵乘法不满足交换律,所以"先旋转再平移"和"先平移再旋转"是两件事。约定上,图形学界多用列向量、矩阵左乘(新矩阵乘在左边,作用顺序从右向左);某些教材用行向量右乘,两种约定的矩阵互为转置。读不同资料时先看清约定,是避免坐标系精神错乱的第一课。

本节要点回顾

  • 向量双身份:点表位置、方向表朝向,平移只影响前者
  • 点积是夹角探针:单位化后即余弦,负值常被截断为 0
  • 叉积是方向判据:二维看 z 分量符号判左右,三维判点在三角形内
  • 矩阵是压缩的变换:乘法不可交换,组合顺序从右往左生效
  • 先归一化再运算:光照与视线计算里最常见的前置动作

1.2 节把这些零件组装成完整的变换机器:齐次坐标如何让平移也走进矩阵大家庭。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U