3.3 二维图形变换:矩阵搬运工


3.3 二维图形变换:矩阵搬运工

本节摘要:变换决定几何"出现在哪、朝向哪、多大"。本节把 1.2 节的齐次坐标机器落到二维:平移、旋转、缩放的 3×3 矩阵,绕任意点旋转的三步分解,以及图形层级(场景树)中变换逐级相乘的传播机制。它是第 4 章三维变换的降维预演,也是所有交互(拖拽、缩放视图、动画)背后的代数。

学习目标

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

  1. 写出二维平移、旋转、缩放的 3×3 齐次矩阵
  2. 推导绕任意点旋转的分解步骤并验证
  3. 用场景树的矩阵级联实现"行星系"式的层级动画
  4. 说出变换顺序从右向左生效在实际代码里如何体现

为什么变换是一切交互的地基

点击拖动一个图形,本质是给它乘一个平移矩阵;滚轮缩放视图,是乘缩放矩阵、再围绕光标平移;旋转 HUD 指针,是绕表盘中心的旋转。所有交互最终都翻译成矩阵运算喂给光栅化器。变换不改变图元本身(顶点相对关系不动),只改变它们落在屏幕哪里——这也是为什么 3.1 的算法只需写一次。

三个基本矩阵

平移 T(tx,ty): 旋转 R(θ): 缩放 S(sx,sy): 1 0 tx cosθ -sinθ 0 sx 0 0 0 1 ty sinθ cosθ 0 0 sy 0 0 0 1 0 0 1 0 0 1

组合的黄金法则只有一条:作用顺序从右往左。"绕点 P 旋转"这类复合目标,用三步分解实现——先把 P 搬到原点,旋转,再搬回去:

import numpy as np, math def T(tx, ty): m = np.eye(3); m[0,2], m[1,2] = tx, ty; return m def R(deg): r = math.radians(deg); c, s = math.cos(r), math.sin(r) m = np.eye(3); m[0,0],m[0,1],m[1,0],m[1,1] = c,-s,s,c; return m def rotate_about(px, py, deg): # 绕任意点旋转 = 平移到原点 → 旋转 → 平移回去(从右往左读) return T(px, py) @ R(deg) @ T(-px, -py) p = np.array([2.0, 0.0, 1.0]) # 待旋转的点 M = rotate_about(2, 2, 90) # 绕 (2,2) 转 90 度 q = M @ p print(np.round(q[:2], 3)) # 输出: [4. 2.] 点 (2,0) 绕 (2,2) 逆时针 90 度后落到 (4,2),手画验证一致

顺序换一下(R 在左 T 在右),算出的落点立刻不同——"先旋转再平移"与"先平移再旋转"的差异在 1.2 节已经实证过,二维同理。

场景树:变换的级联传播

真实应用里图形不是孤立的:机械臂的爪装在小臂上,小臂装在大臂上,大臂装在底座上。子物体的世界变换 = 父级变换 × 自身变换。逐级相乘,改一处、全链更新:

class Node: def __init__(self, local, parent=None): self.local = local # 相对父级的变换 self.parent = parent self.children = [] if parent: parent.children.append(self) def world(self): # 世界变换 = 父级世界变换 乘 自身局部变换 return self.parent.world() @ self.local if self.parent else self.local def circle_marker(t): # 画布上放一个小方块标记点(简化输出) q = (t @ np.array([1.0, 0.0, 1.0]))[:2] return np.round(q, 3) sun = Node(T(4, 4)) # 太阳在画布中心 earth = Node(T(2, 0), sun) # 地球离太阳 2 单位 moon = Node(T(0.8, 0), earth) # 月球离地球 0.8 单位 sun.local = T(4, 4) @ R(0) # 太阳自转 earth.local = T(2, 0) @ R(45) # 地球公转了 45 度 print(circle_marker(earth.world()), circle_marker(moon.world())) # 输出: [5.414 5.414] [5.922 6.165] # 地球绕到 45 度方位,月球跟着走并再叠加相对地球的位置——级联自动生效

这套结构就是游戏引擎场景图的骨架:动画每帧只更新各节点的局部矩阵(转多少、移多少),世界矩阵沿树相乘派生,任何一层的变化自动传导到所有后代。骨骼动画(第 7 章)的关节链在数学上与此完全相同,只是矩阵换成 4×4。

变换对光栅化质量的影响

有一个容易被忽略的事实:缩放与旋转发生在光栅化之前还是之后,画质完全不同。

先变换后光栅化(正确):在连续坐标域搬动几何,光栅化时按最终尺寸采样,边缘干净 先光栅化后变换(错误):像素图被拉伸旋转,每个目标像素反向取源图最近点, 放大出现马赛克块,缩小出现闪烁与摩尔纹

图像缩放本质上也是采样问题:放大用双线性插值平滑取值,缩小得先做低通滤波再降采样,否则高频细节混叠成噪点。1.3 节的采样理论在此兑现第一次工资。

def nearest_scale(src, k): # 最邻近缩放:每个目标像素取源图最近点(演示混叠的最简实现) h, w = src.shape[:2] H, W = round(h*k), round(w*k) out = src[(np.arange(H)/k).astype(int)][:, (np.arange(W)/k).astype(int)] return out src = np.arange(100, dtype=np.uint8).reshape(10, 10) # 一张有渐变纹理的图 big = nearest_scale(src, 3) # 放大 3 倍 print(big.shape, big[0, :6]) # 输出: (30, 30) [0 0 0 1 1 1] —— 每个源像素被复制成 3x3 的硬块

硬块就是马赛克。把取值从"最近点"换成"四邻域加权"(双线性),块状边界变成平滑过渡——一行代码的差别,视觉天壤之别,这也是 GPU 纹理采样器的默认行为(第 5 章纹理过滤会展开)。

本节要点回顾

  • 三大基本变换:平移旋转缩放,齐次矩阵统一表达
  • 绕任意点三步分解:搬回原点、转、搬回去
  • 场景树级联:世界变换沿树相乘,局部改动全链传播
  • 先变换后光栅化:顺序错了画质必坏,缩放本质是采样
  • 骨架已备:本节二维机器加一维,就是第 4 章的三维世界

直线与三角形之外,图形世界还有曲线与填充的面孔,3.4 节扩编图元家族。


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