自己写向量和矩阵运算
目标
亲自实现三维向量运算和 4x4 矩阵乘法,并用它们平移、旋转和缩放图形。完成后,你将能够读写变换矩阵,并通过亲手计算理解乘法顺序为何重要。
为什么重要
三维计算却使用 4x4 矩阵,是因为平移并非乘法。将坐标扩展为 (x, y, z, 1),最后一列便会与这个 1 相乘并加到结果中,从而把平移纳入乘法。这称为齐次坐标,也由此区分 w 为 1 的点和 w 为 0 的方向。
用矩阵表示后,可以预先将多个变换相乘并合成一个矩阵。这就是 GPU 对每个顶点只乘一个 MVP 矩阵的原因。但矩阵乘法不满足交换律;改变顺序后,物体可能不再原地旋转,而是绕轨道运行。亲眼看到一次数值差异,就不会忘记。
步骤
- 放置
/root/linalg/gfxlib.py工具箱。 - 在
/root/linalg/vec.py中实现七个向量运算。 - 在
/root/linalg/mat.py中实现 4x4 矩阵乘法。 - 在
/root/linalg/xform.py中实现平移、缩放和旋转矩阵。 - 将交换乘法顺序后的结果写入
/root/linalg/out/05-order.txt。 - 将三组变换后的正方形绘制到
/root/linalg/out/square.png。 - 使用叉积求法线和面积,并写入
/root/linalg/out/07-normal.txt。
参考
- 所有文件都放在
/root/linalg中。评分器会将该目录加入 import 路径并直接调用函数。 - 要查看图像,请运行
nohup python3 -m http.server 8080 -d /root/linalg/out &,然后在网页预览中打开http://localhost:8080/square.png。 - 常见错误 1:混淆
mul的索引,得到转置矩阵。单位矩阵检测不出此问题,请使用平移矩阵测试。 - 常见错误 2:让
rotate_y的符号与其他轴保持一致。由于轴的循环顺序,只有 y 轴符号相反。
放置绘图工具箱
按示例原样保存 /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 的元组,仅 dot 和 length 返回实数。
必须连同符号准确写出叉积定义。
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] = sin、m[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,z 和 RT=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*x、sy = 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.Canvas 的 line(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