LabHub
学习 学习路径 课程

3D 数学与软件光栅化器

自己写向量和矩阵运算

在 LabHub 中继续学习

目标

亲自实现三维向量运算和 4x4 矩阵乘法,并用它们平移、旋转和缩放图形。完成后,你将能够读写变换矩阵,并通过亲手计算理解乘法顺序为何重要。

为什么重要

三维计算却使用 4x4 矩阵,是因为平移并非乘法。将坐标扩展为 (x, y, z, 1),最后一列便会与这个 1 相乘并加到结果中,从而把平移纳入乘法。这称为齐次坐标,也由此区分 w 为 1 的点和 w 为 0 的方向。

用矩阵表示后,可以预先将多个变换相乘并合成一个矩阵。这就是 GPU 对每个顶点只乘一个 MVP 矩阵的原因。但矩阵乘法不满足交换律;改变顺序后,物体可能不再原地旋转,而是绕轨道运行。亲眼看到一次数值差异,就不会忘记。

步骤

  1. 放置 /root/linalg/gfxlib.py 工具箱。
  2. /root/linalg/vec.py 中实现七个向量运算。
  3. /root/linalg/mat.py 中实现 4x4 矩阵乘法。
  4. /root/linalg/xform.py 中实现平移、缩放和旋转矩阵。
  5. 将交换乘法顺序后的结果写入 /root/linalg/out/05-order.txt
  6. 将三组变换后的正方形绘制到 /root/linalg/out/square.png
  7. 使用叉积求法线和面积,并写入 /root/linalg/out/07-normal.txt

参考

放置绘图工具箱

按示例原样保存 /root/linalg/gfxlib.py,并用 /root/linalg/check.py 绘制测试图案,生成 /root/linalg/out/00-check.png。图案是在 64x64 黑色背景上,从 (0,0) 到 (63,63) 绘制白色(255,255,255) 对角线,再叠加从 (0,32) 到 (63,32) 的红色(255,0,0) 水平线。

从本实验开始,不再重复实现 PNG 编码器。我们把第一个实验中手写的同类代码作为工具提供,因为这里要学习的不是文件格式。

实验 Pod 没有持久卷,因此上一个实验创建的文件不会保留。每个实验都从重新放置工具箱开始。

创建 Canvas(w, h, bg),用 line(x0, y0, x1, y1, rgb) 绘制两条线,再用 write_png(path) 保存。必须最后绘制水平线,交点 (32,32) 才会是红色。

三维向量运算

/root/linalg/vec.py 中实现 add(a,b)sub(a,b)scale(a,s)dot(a,b)cross(a,b)length(a)normalize(a) 七个函数。向量是长度为 3 的元组,仅 dotlength 返回实数。

必须连同符号准确写出叉积定义。

cross(a, b) = (a1*b2 - a2*b1, a2*b0 - a0*b2, a0*b1 - a1*b0)

cross((1,0,0), (0,1,0)) 得到 (0,0,1),符号就是正确的。若结果相反,右手坐标系被翻转,之后光照会看起来像从物体背后照来。

normalize 可能收到长度为 0 的向量。此时应原样返回,避免除以 0。

4x4 矩阵乘法

/root/linalg/mat.py 中实现 identity()mul(a,b)apply(m,v) 三个函数。矩阵按行优先存储(由四个长度为 4 的列表构成),apply 将点 (x,y,z) 视为列向量 (x,y,z,1) 相乘,并返回 (x,y,z,w) 四个分量。

乘法定义为 out[r][c] = sum(a[r][k] * b[k][c] for k in range(4))。一旦弄错索引顺序便会得到转置矩阵,而单位矩阵测试无法发现,因为单位矩阵转置后仍是自身。

因此请使用非对称矩阵测试,例如只有最后一列有值的平移矩阵。

apply 对 r=0..3 计算 m[r][0]*x + m[r][1]*y + m[r][2]*z + m[r][3]*1

平移、缩放和旋转矩阵

/root/linalg/xform.py 中实现 translate(tx,ty,tz)scale(sx,sy,sz)rotate_x(deg)rotate_y(deg)rotate_z(deg) 五个函数。角度以**度(degree)**接收,并使用右手坐标系。

rotate_z 左上角的 2x2 为 [[cos, -sin], [sin, cos]]。这样 rotate_z(90) 会把 x 轴 (1,0,0) 变到 y 轴 (0,1,0)

按同一规则,rotate_x 将 y 变到 z,rotate_y 将 z 变到 x。只有 rotate_y 的符号方向相反——m[0][2] = sinm[2][0] = -sin。这是因为轴的循环顺序为 x→y→z→x;若强行统一符号,只有 y 轴旋转会反向。

使用 math.radians 将度转换为弧度。

乘法顺序会改变结果

令 T = translate(2,0,0)、R = rotate_z(90),分别将 mul(T,R)mul(R,T) 应用于点 (1,0,0),把结果写入 /root/linalg/out/05-order.txt,格式为 TR=x,y,zRT=x,y,z 两行,并保留小数点后六位。

在列向量右乘的约定中,右侧矩阵先应用。 mul(T, R) 表示先旋转、后平移。

请手动推导:先求 R 把 (1,0,0) 变到哪里,再加上 T。相反顺序则先应用 T,再应用 R。

一个结果是物体原地旋转后平移,另一个则先远离原点,再绕原点公转。结果差异很大。

绘制变换后的正方形

使用 /root/linalg/plot.py 将 256x256 PNG 写入 /root/linalg/out/square.png。模型坐标中的正方形为 (-1,-1,0)、(1,-1,0)、(1,1,0)、(-1,1,0),屏幕坐标转换为 sx = 128 + 60*xsy = 128 - 60*y。在黑色背景上绘制三组正方形:未变换者为白色(255,255,255),应用 rotate_z(30) 者为红色(255,60,60),应用 mul(translate(1.2,-0.8,0), scale(0.5,0.5,1)) 者为绿色(60,255,60)。

每个正方形由四条线段组成:依次连接四个顶点,最后从末点回到首点。使用 gfxlib.Canvasline(x0, y0, x1, y1, rgb)

转换到屏幕坐标时注意 y 使用减法。数学坐标的 y 向上,图像坐标的 y 向下。若遗漏,绿色正方形会画到上方。

绿色正方形先缩小、后平移——mul(translate, scale) 的右侧先应用。

用叉积求法线和面积

使用 /root/linalg/normal.py 求两个三角形的单位法线和面积,并在 /root/linalg/out/07-normal.txt 中写成 t1_normal=t1_area=t2_normal=t2_area= 四行。三角形 1 为 A(0,0,0) B(2,0,0) C(0,3,0),三角形 2 为 A(1,1,1) B(2,1,1) C(1,1,3),法线是归一化后的 cross(B-A, C-A)

叉积长度是两条边构成的平行四边形面积,因此三角形面积是其一半,即 area = length(cross(B-A, C-A)) / 2

法线的符号由顶点环绕顺序决定。 cross(B-A, C-A)cross(C-A, B-A) 方向完全相反。该顺序之后将成为区分正面和背面的标准,请严格按要求执行。

数值用 %.6f 格式,向量以逗号连接。例如:t1_normal=0.000000,0.000000,1.000000