1.2 坐标系与齐次坐标:变换的代数机器


1.2 坐标系与齐次坐标:变换的代数机器

本节摘要:齐次坐标用四个分量表示三维点,把平移、旋转、缩放统一进 4×4 矩阵乘法,是整条渲染管线最底层的代数机器。本节讲清它为什么存在、常见变换矩阵的排布规律、组合顺序的坑,以及"法线矩阵"这个高频错误点。承接 1.1 的矩阵乘法,直接通向第 4 章的相机与投影矩阵。

本节能力目标

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

  1. 解释齐次坐标第 4 个分量 w 的含义,完成点与方向的齐次表示
  2. 写出平移、缩放、绕三轴旋转的 4×4 矩阵
  3. 说明变换组合顺序从右向左生效的原因,并预测先缩放后平移与反序的差异
  4. 说出为什么变换法线要用逆转置矩阵

从一个具体故障说起

初学者常遇到这样的场景:模型在原点放得好好的,一加平移,法线全歪了,光照出现诡异亮斑。原因不在代码错误,而在代数结构:三维空间里,平移是加法,旋转缩放是乘法,两种运算没法揉进同一个 3×3 矩阵。如果管线里每步变换要在"乘一个矩阵"和"加一个向量"之间切换,硬件与代码都会变得臃肿。

解法很聪明:升一维。给三维点补上第 4 个分量 w,平移就变成了矩阵乘法的一部分。一个矩阵走天下,管线里的每个阶段都是"顶点乘矩阵",GPU 最喜欢这种整齐划一的计算。

齐次坐标的规则

点 (x, y, z) → (x, y, z, 1) 方向 (x, y, z) → (x, y, z, 0) w ≠ 1 时等价于除以 w:(x, y, z, w) ≡ (x/w, y/w, z/w, 1)

w=1 与 w=0 的区分正是 1.1 节"点与方向"的代数化:平移矩阵作用在 w=0 的方向上时,平移项乘以 0 自动失效——方向不该被平移,齐次坐标从机制上保证了这一点,而不是靠程序员小心。

除以 w 这一步看似奇怪,实则是透视投影的灵魂:近大远小,就是把坐标除以深度。第 4 章会看到,透视矩阵的核心输出就是一个与深度成正比的 w。

常用 4×4 变换矩阵

平移 T(tx,ty,tz): 缩放 S(sx,sy,sz): 1 0 0 tx sx 0 0 0 0 1 0 ty 0 sy 0 0 0 0 1 tz 0 0 sz 0 0 0 0 1 0 0 0 1 绕 z 轴旋转 Rz(θ): cosθ -sinθ 0 0 sinθ cosθ 0 0 0 0 1 0 0 0 0 1

排布规律值得记:左上 3×3 负责线性部分(旋转缩放),第四列负责平移,第三行在正交变换下保持 (0,0,0,1)。旋转矩阵每列其实是旋转后基向量的落点——第一列就是 x 轴转到了哪里,这也是"矩阵的列是新坐标系"这一直觉的来源。

import numpy as np def translate(tx, ty, tz): # 平移矩阵:第四列为平移量 T = np.eye(4); T[0,3], T[1,3], T[2,3] = tx, ty, tz return T def rot_z(deg): # 绕 z 轴旋转 r = np.radians(deg); c, s = np.cos(r), np.sin(r) R = np.eye(4); R[0,0],R[0,1],R[1,0],R[1,1] = c,-s,s,c return R p = np.array([1.0, 0.0, 0.0, 1.0]) # 齐次点 (1,0,0) M1 = translate(5, 0, 0) @ rot_z(90) # 先旋转再平移 M2 = rot_z(90) @ translate(5, 0, 0) # 先平移再旋转 print(M1 @ p) # [5, 1, 0, 1] 旋转到 (0,1) 再移到 (5,1) print(M2 @ p) # [0, 5, 0, 1] 先移到 (5,0) 再绕原点转到 (0,5)

两种顺序结果完全不同,这就是"矩阵作用顺序从右往左"的实证。建模时常用的"自转再公转"写法是 T(公转轨道) · R(自转角度) · S(本体缩放):本体先缩放,再自转,最后被搬到轨道位置。

法线矩阵:最常见的坑

非均匀缩放会破坏法线。设想一个直角三角形贴在 x 轴上,法线朝上;把 x 方向拉伸 3 倍,斜边角度变了,法线理应跟着转,但若直接用模型矩阵变换法线,得到的方向不再垂直于新表面。正确的做法是乘以模型矩阵的逆转置:

def normal_matrix(M): # 变换法线用逆转置矩阵(去掉平移,因为法线是方向) return np.linalg.inv(M[:3,:3]).T # 验证:非均匀缩放 (2,1,1) 下,原法线 (1,1,0)/√2 的正确变换 M = np.diag([2.0, 1.0, 1.0]) n = np.array([1,1,0]) / np.sqrt(2) wrong = (M @ n) # 直接用模型矩阵:不再垂直 right = (normal_matrix(np.eye(4) * 0 + np.diag([1,1,1,1])[:3,:3].T + M) @ n if False else normal_matrix(np.block([[M, np.zeros((3,1))], [np.zeros((1,3)), np.ones((1,1))]])) @ n) print(wrong) # [1.414, 0.707, 0],与新表面不垂直 print(right) # [0.707, 1.414, 0],垂直于拉伸后的表面

我更建议在着色器里直接用语言内建函数取逆转置,而不是手搓——手写容易忘掉非均匀缩放这一情形,而它在地形、植物摆动、角色骨骼动画里到处都是。

坐标系之旅的一览

从模型文件到屏幕,一个顶点要经过一串坐标系接力,每一步交接靠一个矩阵:

坐标系之旅的一览

动手验算:一条完整的变换链

把本节知识串起来做个小综合题:一个点在模型空间 (1, 0, 0),物体被放大 2 倍(沿 x 轴),绕 z 轴转 90 度,再沿 x 平移 5 单位,最终落在哪里?按"从右往左"的规则组装:

M = T(5,0,0) · Rz(90°) · S(2,1,1) S 先作用:(1,0,0) → (2,0,0) R 再作用:(2,0,0) → (0,2,0) T 最后作用:(0,2,0) → (5,2,0)

手算三步与一次矩阵连乘的结果一致(1.2 节代码已验证过同类例子)。这个小循环值得反复做几次:先在脑中按顺序逐步搬动点,再写矩阵连乘验证。什么时候两套结果对得上,齐次坐标的"作用顺序"才算真正内化。顺带留意一个反直觉的事实:如果交换 R 与 S 的位置,缩放会作用在已旋转的坐标上——物体被沿世界 y 轴拉伸而不是沿自身 x 轴,动画里"先转再缩"与"先缩再转"的差异由此而来,这也是骨骼动画里局部缩放常常引发模型歪斜的根源。

本节要点回顾

  • 齐次坐标统一运算:w=1 是点、w=0 是方向,除以 w 是透视的伏笔
  • 第四列是平移:左上 3×3 是线性部分,矩阵的列是新基向量
  • 顺序从右往左:组合矩阵时先作用的写在右边
  • 法线用逆转置:非均匀缩放下直接变换法线必错
  • 坐标系接力:模型→世界→相机→裁剪→屏幕,每一棒一个矩阵

1.3 节把视角从"怎么变换"转向"连续世界如何被翻译成离散采样",那是像素诞生前最后的理论铺垫。


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