想象你是一位动画电影导演,要拍摄一部叫做《小美奇遇记》的3D动画。主演小美是个圆滚滚的立方体机器人,她得先在自己的化妆间里摆好姿势(这间化妆间以她为中心),然后走到舞台上站到指定位置,导演老张扛起摄像机从某个角度对准她,镜头取景框决定哪些画面能进电影,最后信号录进录像带变成观众看到的二维画面。这整个过程,就是 3D 图形中一个顶点从“模型空间”一路变身成“屏幕像素”的坐标之旅。 本节课我们要拆解的,正是这条 3D 坐标变换流水线:模型空间 → 世界空间 → 视图空间 → 裁剪空间 → 屏幕空间。每个空间都有自己的坐标系,靠一个个 4×4 矩阵把顶点接力传递。掌握了这条管线,你就理解了 3D 引擎(如 Unity、Unreal)乃至游戏显卡最核心的数学运作逻辑。哪怕你未来做 AI 渲染、三维重建或自动驾驶仿真,都和这一套变换密不可分。

岗位关联

这一课是走进计算机图形学大门的“数学钥匙”。无论做游戏开发、三维建模工具还是 AI 视觉仿真,理解坐标变换都能让你看懂引擎源码、定位渲染 Bug,并亲手搭建自己的 3D 管线。

目录
  1. 可视化演示
  2. 模型空间变换
  3. 世界空间变换
  4. 视图空间变换
  5. 齐次坐标原理
  6. 裁剪空间变换
  7. 透视除法与NDC
  8. 视口变换映射
  9. 变换管线串联
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态演示3D坐标变换的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

3D坐标变换:模型空间 模型空间与世界空间的模型变换 世界空间到视图空间的视图变换 核心机制可视化 3D坐标变换整体架构与关键组件

模型空间变换

清晨的化妆间里,小美站在一个标着原点的地板上。造型师用量尺记录她每个部位的位置:眼睛在 (0, 0.8, 0.5),左脚在 (-0.3, 0, 0),右脚在 (0.3, 0, 0)。这些坐标全都以小美自己为参考点——她往左转,坐标跟着转;化妆间搬走了,坐标依然不变。这个以物体自身为中心的坐标系,就叫模型空间,也叫本地空间。

3D 建模软件(如 Blender、Maya)中,美术师雕刻模型时看到的网格坐标,就是模型空间坐标。一个模型可能包含成千上万个顶点,每个顶点在模型空间里都有一个固定的三维位置。为什么要多此一举建一个“以自己为中心”的空间?因为这样小美的模型文件才能被反复复用——今天她演主角,明天当群演,模型本身不用改,只需要在场景里给她安排一个位置就行。

在数学上,我们把模型空间里的一个顶点记作列向量。为了后面统一做矩阵乘法,这里引入齐次坐标,多写一个 1:

化妆间里的坐标,就是模型空间。 它记录的是“小美自己身体上每个点长在哪”,跟舞台在哪、摄像机在哪完全无关。如果顶点位置定义在建模时出错——比如把左脚坐标写成了右脚——那模型一进引擎就会“天生残疾”。

Pmodel=(xyz1)\displaystyle P_{\text{model}} = \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}
  1. 三维坐标$x$、$y$、$z$ 分别是顶点相对模型原点的左右、上下、前后距离,单位通常是米或引擎自定义的单位。
    Pxyz=(xyz)\displaystyle P_{xyz} = \begin{pmatrix} x \\ y \\ z \end{pmatrix}
  2. 补上齐次分量在末尾添加一个值为 1 的第四分量,它让平移、旋转、缩放都能统一成矩阵乘法,是后续透视投影的关键伏笔。
    Pmodel=(xyz1)\displaystyle P_{\text{model}} = \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix}

世界空间变换

导演老张拿着剧本说:“小美,你站到舞台正中央偏右的位置,面向观众席。”场务在舞台地面上铺了一张大坐标纸,以舞台中心为原点,标好了东南西北。小美走出化妆间,走到位置 (3, 0, 2),再转了 30° 面向观众。此时,她身体上的每个点都获得了一个全新的、以舞台为中心的坐标——这就是世界空间。

从模型空间到世界空间,靠的是模型矩阵(Model Matrix)。它把“小美身上各点相对她自己”的坐标,变成“相对整个舞台”的坐标。这个矩阵由三个基础变换组合而成:缩放 S(小美今天要求把自己放大 1.2 倍,显得更有气势)、旋转 R(面向观众转 30°)、平移 T(走到舞台上的指定位置)。

小美走进舞台的那一刻,她的每个顶点都完成了从“化妆间坐标”到“舞台坐标”的换算。 世界空间里,所有演员、道具、灯光都共享同一套坐标,这样小美站在 (3, 0, 2) 时,灯光师才能准确地把光束打在她头顶。

Pworld=T×R×S×Pmodel\displaystyle P_{\text{world}} = T \times R \times S \times P_{\text{model}}
  1. 先缩放$S$ 沿三个轴方向分别缩放模型,缩放不改变顶点相对模型原点的方向,只改变“身体比例”。
    S=(sx0000sy0000sz00001)\displaystyle S = \begin{pmatrix} s_x & 0 & 0 & 0 \\ 0 & s_y & 0 & 0 \\ 0 & 0 & s_z & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
  2. 再旋转$R$ 让模型绕某个轴转动。旋转也不会改变顶点到原点的距离,只是改变它相对模型的朝向。
    R=(cos⁡θ0sin⁡θ00100−sin⁡θ0cos⁡θ00001)\displaystyle R = \begin{pmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
  3. 最后平移$T$ 把整个模型搬移到舞台的指定位置。平移是“整体搬家”,矩阵里放在左上角第四列。
    T=(100tx010ty001tz0001)\displaystyle T = \begin{pmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{pmatrix}
  4. 从右往左乘矩阵乘法从右往左生效。顶点先经过 $S$,再经过 $R$,最后经过 $T$。顺序不能调换——先旋转再平移,和先平移再旋转,小美在舞台上的位置和朝向会完全不同。

视图空间变换

现在轮到导演老张了。他没有站上舞台,而是扛着摄像机退到观众席后方,半蹲下来,以 30° 的俯角对准小美。世界空间是“上帝视角”的全局坐标,但摄像机拍到的画面,取决于摄像机的位置和朝向。视图空间,就是以摄像机为原点的坐标系:摄像机所在位置是原点,它看的方向是前方,它的头顶方向是上方。

世界坐标变到视图坐标,靠的是视图矩阵(View Matrix)$V$。这个矩阵通常由 LookAt 函数构造:你告诉它摄像机在哪(eye)、看点在哪(center)、哪个方向是上(up),它就能生成一个矩阵,把整个世界重新摆放到“摄像机面前”。

导演把摄像机架好,整个舞台就活在了他的镜头坐标系里。 视图空间里,小美站在摄像机前方负 Z 方向,左右上下都相对摄像机而言。此时无论世界坐标多么复杂,后续的投影都只需关心“摄像机看到了什么”。

Pview=V×Pworld,V=LookAt(eye,center,up)\displaystyle P_{\text{view}} = V \times P_{\text{world}}, \quad V = \text{LookAt}(\text{eye}, \text{center}, \text{up})
  1. 确定三个基向量由视线方向 $f$(center − eye 归一化)、右方向 $s$($f$ 与 up 叉积归一化)、上方向 $u$($s$ 与 $f$ 叉积)构成摄像机坐标系的三根轴。
    f=center−eye∣center−eye∣,s=f×up∣f×up∣,u=s×f\displaystyle f = \frac{\text{center} - \text{eye}}{|\text{center} - \text{eye}|}, \quad s = \frac{f \times up}{|f \times up|}, \quad u = s \times f
  2. 构造视图矩阵把三根轴写成矩阵的行,并用它们与摄像机位置的点积完成平移部分,实现“把摄像机搬回原点”。
    V=(sxsysz−s⋅eyeuxuyuz−u⋅eye−fx−fy−fzf⋅eye0001)\displaystyle V = \begin{pmatrix} s_x & s_y & s_z & -s \cdot eye \\ u_x & u_y & u_z & -u \cdot eye \\ -f_x & -f_y & -f_z & f \cdot eye \\ 0 & 0 & 0 & 1 \end{pmatrix}

齐次坐标原理

细心的你可能会问:为什么每个顶点都要写 4 个数?三维坐标 (x, y, z) 不是挺好吗?让我们回到剧组的工牌制度。每个演职人员都挂一张工牌:正常演员工牌号是 1,意味着他是“具体的点”,有实际位置;而另一种工牌号是 0,代表“方向”——比如灯光师说的“光束朝正前方打”,这只是个方向,不代表“舞台上某个位置有个人”。

这个多出来的维度,就是齐次坐标中的 $w$ 分量。引入它的核心原因有两个:第一,把平移变成矩阵乘法。三维空间中平移 $x + t$ 没法写成 3×3 矩阵乘法(因为矩阵乘法无法表达加法),但升到四维后,平移也能用 4×4 矩阵统一表达。第二,为透视除法埋下伏笔——后面做透视投影时,$w$ 会携带深度信息。

用分块矩阵的写法,能一眼看清齐次坐标如何优雅地统一旋转、缩放与平移:

齐次坐标就像剧组工牌,用第四维的小小标记把“点”和“方向”区分开。 它让所有变换统一成 4×4 矩阵乘法,GPU 也因此能用同一套硬件电路快速处理所有顶点变换——这是 3D 图形学最优雅的数学设计之一。

(Rt0T1)×(xyz1)=(R⋅(xyz)+t1)\displaystyle \begin{pmatrix} R & t \\ 0^T & 1 \end{pmatrix} \times \begin{pmatrix} x \\ y \\ z \\ 1 \end{pmatrix} = \begin{pmatrix} R \cdot \begin{pmatrix} x \\ y \\ z \end{pmatrix} + t \\ 1 \end{pmatrix}
  1. 分块表示左上角的 $R$ 是 3×3 旋转/缩放矩阵,右上角的 $t$ 是三维平移向量,底行是 $(0,0,0,1)$。
    M=(R3×3t3×101×31)\displaystyle M = \begin{pmatrix} R_{3 \times 3} & t_{3 \times 1} \\ 0_{1 \times 3} & 1 \end{pmatrix}
  2. 对点作用计算过程是“先做旋转缩放,再加平移”,这正是仿射变换的数学本质:线性变换 + 平移。
    P′=M×P=(R⋅p+t1)\displaystyle P' = M \times P = \begin{pmatrix} R \cdot p + t \\ 1 \end{pmatrix}
  3. $w$ 分量的工程含义点(工牌号为 1)平移会改变位置;方向向量(工牌号为 0)平移后不变——所以灯光的朝向不受摄像机位置影响。
    w={1,点的位置0,方向向量\displaystyle w = \begin{cases} 1, & \text{点的位置} \\ 0, & \text{方向向量} \end{cases}

裁剪空间变换

摄像机架好了,但导演老张不可能把所有东西都拍进画面——太远的山、太近的镜头贴脸、左右超出镜头边缘的布景,都得裁掉。视锥体(Frustum)就是导演的“取景范围”:一个近平面、一个远平面、上下左右四个斜面围成的平截头体。只有落在视锥体内的顶点才需要渲染。

把视图空间变换到裁剪空间,靠的是投影矩阵 $P$。投影矩阵一方面把视锥体“压扁”成一个标准盒子,方便后续裁剪;另一方面在 $w$ 分量里写入深度,为近大远小做准备。这是整个管线里最有“魔法感”的一步:

视锥体就是导演的取景框,投影矩阵则负责把三维世界“压”进这个框。 注意这里的裁剪空间坐标还带着 $w$ 这个附加维度,真正的“压扁”要等下一步透视除法来完成。

Mproj=(maspect0000m0000f+nn−f2fnn−f00−10)\displaystyle M_{\text{proj}} = \begin{pmatrix} \dfrac{m}{aspect} & 0 & 0 & 0 \\ 0 & m & 0 & 0 \\ 0 & 0 & \dfrac{f + n}{n - f} & \dfrac{2fn}{n - f} \\ 0 & 0 & -1 & 0 \end{pmatrix}
  1. 焦距缩放 $m$$m = \dfrac{1}{\tan(\text{fov}/2)}$,fov 是摄像机纵向视场角。视场角越大,看到的范围越广,物体显得越小。
    m=1tan⁡(fov/2)\displaystyle m = \frac{1}{\tan(\text{fov} / 2)}
  2. 横向修正 $aspect$屏幕宽高比(如 16:9)修正横向缩放,保证画面不被拉伸变形。
    maspect=1aspect⋅tan⁡(fov/2)\displaystyle \frac{m}{\text{aspect}} = \frac{1}{\text{aspect} \cdot \tan(\text{fov} / 2)}
  3. 深度映射第 3 行第 3、4 列把视图空间的深度映射到裁剪空间的 $z$ 范围,并引入近平面 $n$ 与远平面 $f$。
    zclip=f+nn−f⋅zview+2fnn−f\displaystyle z_{\text{clip}} = \frac{f + n}{n - f} \cdot z_{\text{view}} + \frac{2fn}{n - f}
  4. $w$ 分量写入 $-z$第 4 行第 3 列为 -1,把视图空间深度以 $-z$ 的形式存进 $w$——这是透视除法的关键输入。
    wclip=−zview\displaystyle w_{\text{clip}} = -z_{\text{view}}

透视除法与NDC

现在到了最激动人心的时刻。投影矩阵已经计算出了 $P_{\text{clip}} = (x, y, z, w)$,但我们真正想要的,是把这个带 $w$ 的四维坐标变成标准化的三维坐标。方法出奇简单:用 $w$ 去除以 $x$、$y$、$z$。这就是透视除法。

除法听起来简单,效果却神奇:因为离摄像机越远的顶点,它的 $-z$ 越大,$w$ 也就越大,除以 $w$ 后 $x$、$y$ 被压缩得越厉害——于是远处的物体变小了,近大远小就这样诞生了。除法后的坐标称为 NDC(Normalized Device Coordinates,规范设备坐标),它的 $x$、$y$、$z$ 都被限制在 [-1, 1] 范围内,这个标准盒子让后续所有工作变得极其简单。

透视除法相当于导演把取景框里的画面“打印”成一张标准底片。 谁离摄像机近,谁在底片上占比大;谁离得远,谁就缩成一个小点。深度值也在这时被规整成统一尺码,为后续的深度测试(判断谁遮挡谁)铺好了路。

Pndc=(xclip/wclipyclip/wclipzclip/wclip)\displaystyle P_{\text{ndc}} = \begin{pmatrix} x_{\text{clip}} / w_{\text{clip}} \\ y_{\text{clip}} / w_{\text{clip}} \\ z_{\text{clip}} / w_{\text{clip}} \end{pmatrix}
  1. x 分量除法裁剪空间的 $x$ 除以 $w$,得到水平方向的规范化坐标,范围约在 [-1, 1] 之间。
    xndc=xclipwclip\displaystyle x_{\text{ndc}} = \frac{x_{\text{clip}}}{w_{\text{clip}}}
  2. y 分量除法同理处理纵向坐标,$y=0$ 表示屏幕中线,$y=1$ 是屏幕顶端(不同图形 API 的上下方向略有差异)。
    yndc=yclipwclip\displaystyle y_{\text{ndc}} = \frac{y_{\text{clip}}}{w_{\text{clip}}}
  3. z 分量除法$z$ 也除以 $w$,得到线性化的深度值:近平面处为 -1,远平面处为 1(以 OpenGL 为例)。
    zndc=zclipwclip,zndc∈[−1,1]\displaystyle z_{\text{ndc}} = \frac{z_{\text{clip}}}{w_{\text{clip}}}, \quad z_{\text{ndc}} \in [-1, 1]

视口变换映射

底片洗出来了,但观众要看的是电影院的银幕——可能是 1920×1080 的横屏,也可能是手机 1080×2400 的竖屏。视口变换就是把 NDC 标准盒子里的坐标(范围 [-1, 1]),映射到屏幕的像素坐标系(范围 [0, width] × [0, height])。

这一步没有矩阵,只是简单的线性映射。把 NDC 的 -1 映射到屏幕 0,+1 映射到屏幕宽/高,中间按比例均匀分布。视口矩形(viewport rectangle)决定映射到屏幕的哪个区域——比如游戏分屏时,左半边玩家、右半边玩家,各自映射到不同的视口区域。

从底片到银幕,只需要一个比例尺。 同样的 NDC 坐标,在 4K 银幕上映射出 3840 个像素,在手机屏幕上映射出 1080 个像素——这就是分辨率适配的底层逻辑。

sx=xndc+12×width+viewportx,sy=1−yndc2×height+viewporty\displaystyle s_x = \frac{x_{\text{ndc}} + 1}{2} \times \text{width} + \text{viewport}_x, \quad s_y = \frac{1 - y_{\text{ndc}}}{2} \times \text{height} + \text{viewport}_y
  1. 把 [-1, 1] 归一化到 [0, 1]$(x_{\text{ndc}} + 1) / 2$ 把 -1 变成 0,把 1 变成 1,中间值线性映射。
    u=xndc+12,v=1−yndc2\displaystyle u = \frac{x_{\text{ndc}} + 1}{2}, \quad v = \frac{1 - y_{\text{ndc}}}{2}
  2. 乘屏幕尺寸$u$、$v$ 乘以像素宽高,得到像素坐标。注意 $y$ 方向取反,因为 NDC 的 $y$ 朝上,而屏幕像素的 $y$ 朝下。
    sx=u×width,sy=v×height\displaystyle s_x = u \times \text{width}, \quad s_y = v \times \text{height}
  3. 加上视口偏移如果画面不是铺满全屏(比如分屏),要加上视口矩形左上角的像素偏移。
    sx=u×width+viewportx,sy=v×height+viewporty\displaystyle s_x = u \times \text{width} + \text{viewport}_x, \quad s_y = v \times \text{height} + \text{viewport}_y

变换管线串联

现在把全部旅程串起来。小美从化妆间出发(模型空间),走到舞台指定位置(世界空间,模型矩阵 M),导演从摄像机角度观看(视图空间,视图矩阵 V),经过取景框压缩和透视除法(裁剪空间与 NDC,投影矩阵 P),最后被印上银幕(屏幕空间,视口变换)。完整管线如下:

实际操作中,CPU 会把三个矩阵预先乘成一个 MVP 矩阵($M_{\text{proj}} \times V_{\text{view}} \times M_{\text{model}}$),每个顶点只需做一次矩阵乘法。 小美从化妆间到银幕的旅程,最终被压缩成一行矩阵乘法——这就是 3D 引擎渲染千万个顶点依然能保持流畅的秘密。

Pscreen=Vviewport×Mproj×Vview×Mmodel×Pmodel\displaystyle P_{\text{screen}} = V_{\text{viewport}} \times M_{\text{proj}} \times V_{\text{view}} \times M_{\text{model}} \times P_{\text{model}}
  1. 模型矩阵 M模型空间 → 世界空间,包含缩放、旋转、平移。
    Pworld=Mmodel×Pmodel\displaystyle P_{\text{world}} = M_{\text{model}} \times P_{\text{model}}
  2. 视图矩阵 V世界空间 → 视图空间,把场景摆到摄像机面前。
    Pview=Vview×Pworld\displaystyle P_{\text{view}} = V_{\text{view}} \times P_{\text{world}}
  3. 投影矩阵 P视图空间 → 裁剪空间,用视锥体压缩场景,并把深度写入 $w$。
    Pclip=Mproj×Pview\displaystyle P_{\text{clip}} = M_{\text{proj}} \times P_{\text{view}}
  4. 透视除法与视口变换裁剪空间 → NDC → 屏幕像素坐标,最终得到画面上该顶点的二维位置。
    Pndc=Pclip.xyzPclip.w,Pscreen=Vviewport×Pndc\displaystyle P_{\text{ndc}} = \frac{P_{\text{clip}.xyz}}{P_{\text{clip}.w}}, \quad P_{\text{screen}} = V_{\text{viewport}} \times P_{\text{ndc}}

AI 应用场景

游戏引擎中的场景对象管理

阶段:游戏 AI 与场景理解

解决的问题:在 Unity 或 Unreal 中,策划需要在场景里放置成百上千个 NPC、道具和建筑。如果每个物体都手工维护各自的顶点坐标,场景编辑将是一场灾难。借助模型→世界变换,每个预制体(Prefab)只需保存本地的模型资源,场景中指定位置、旋转、缩放即可复用。

具体案例:在 Unity 中拖拽一个小美角色到场景中,Inspector 面板显示 Transform 组件的 Position (3, 0, 2)、Rotation (0, 30, 0)、Scale (1.2, 1.2, 1.2)。引擎后台自动构造模型矩阵,把预制体的所有顶点变换到世界空间。AI 寻路系统、碰撞检测和 NavMesh 都在世界空间里工作,大家共享同一套坐标。

三维建模软件的视图导航

阶段:交互式三维可视化

解决的问题:用户在 Blender 中按住鼠标中键旋转视角时,模型本身没动,动的是摄像机。传统做法需要手动计算每个顶点相对新视角的位置,而视图矩阵让这个过程变成一次矩阵更新。

具体案例:Blender 的 3D 视口中,用户旋转视角时,软件实时更新视图矩阵(LookAt 的 eye 点在球面上移动,center 始终指向模型中心),GPU 对每个顶点重新做 view 变换,画面便以每秒钟 60 帧的速度平滑旋转。AI 辅助建模工具(如自动生成模型后预览)也依赖这套视图管线。

AR/VR 中虚拟物体的空间锚定

阶段:空间计算与位姿估计

解决的问题:AR 眼镜要在真实世界的桌面上放一个虚拟咖啡杯,必须知道设备的精确位置和朝向(六自由度位姿)。位姿数据被编码成视图矩阵和投影矩阵,虚拟物体才能稳定地“粘”在桌面上,不随头部晃动而漂移。

具体案例:HoloLens 通过 SLAM 算法估计设备位姿,每帧把位姿转换成视图矩阵,配合透视投影矩阵渲染虚拟内容。当 AI 识别出桌面平面后,虚拟咖啡杯的模型空间坐标通过 M→V→P 变换,叠加到真实场景的对应像素位置上,实现逼真的混合现实效果。

Python 演示

python80 行
import numpy as np  def mat_model(tx, ty, tz, ry, sx, sy, sz):    # 模型矩阵 = 平移 x 旋转 x 缩放    S = np.array([        [sx, 0, 0, 0],        [0, sy, 0, 0],        [0, 0, sz, 0],        [0, 0, 0, 1]], dtype=float)    c, s = np.cos(ry), np.sin(ry)    R = np.array([        [c, 0, s, 0],        [0, 1, 0, 0],        [-s, 0, c, 0],        [0, 0, 0, 1]], dtype=float)    T = np.array([        [1, 0, 0, tx],        [0, 1, 0, ty],        [0, 0, 1, tz],        [0, 0, 0, 1]], dtype=float)    return T.dot(R.dot(S))  def mat_view(eye, target, up):    # LookAt 视图矩阵    f = target - eye    f = f / np.linalg.norm(f)    s = np.cross(f, up)    s = s / np.linalg.norm(s)    u = np.cross(s, f)    return np.array([        [s[0], s[1], s[2], -np.dot(s, eye)],        [u[0], u[1], u[2], -np.dot(u, eye)],        [-f[0], -f[1], -f[2], np.dot(f, eye)],        [0, 0, 0, 1]], dtype=float)  def mat_proj(fov, aspect, near, far):    # 透视投影矩阵    m = 1.0 / np.tan(fov / 2.0)    return np.array([        [m / aspect, 0, 0, 0],        [0, m, 0, 0],        [0, 0, (far + near) / (near - far), 2 * far * near / (near - far)],        [0, 0, -1, 0]], dtype=float)  def main():    # 小美:中心在原点的单位立方体,8 个顶点    vertices = np.array([        [-0.5, -0.5, -0.5, 1], [0.5, -0.5, -0.5, 1],        [0.5, 0.5, -0.5, 1], [-0.5, 0.5, -0.5, 1],        [-0.5, -0.5, 0.5, 1], [0.5, -0.5, 0.5, 1],        [0.5, 0.5, 0.5, 1], [-0.5, 0.5, 0.5, 1]], dtype=float)     # 三步:搬运到世界 -> 摄像机取景 -> 透视压缩    M = mat_model(0, 0, -6, 0.5, 1, 1, 1)    V = mat_view(np.array([0, 0, 0]), np.array([0, 0, -1]), np.array([0, 1, 0]))    P = mat_proj(np.deg2rad(60), 16.0 / 9.0, 0.1, 100.0)    MVP = P.dot(V.dot(M))     # 变换到裁剪空间    clip_pts = [MVP.dot(p) for p in vertices]     # 透视除法 + 视口映射    width, height = 800, 450    screen_pts = []    for c in clip_pts:        ndc = c[:3] / c[3]        sx = (ndc[0] + 1) / 2.0 * width        sy = (1 - ndc[1]) / 2.0 * height        screen_pts.append((round(sx, 1), round(sy, 1), round(ndc[2], 4)))     for row in screen_pts:        print(row)  if __name__ == "__main__":    main()
目的:将一个立方体的 8 个顶点完整走一遍「模型 → 世界 → 视图 → 裁剪 → NDC → 屏幕」的坐标变换链路,输出每个顶点最终落在 800×450 屏幕上的像素坐标与深度值。
  1. MVP = P.dot(V.dot(M))把投影、视图、模型三个矩阵预乘成一个 MVP 矩阵,每个顶点只需一次矩阵乘法,这是 GPU 渲染管线的高效核心。
  2. clip_pts = [MVP.dot(p) for p in vertices]对 8 个顶点分别做矩阵乘法,得到裁剪空间坐标,此时坐标仍是四维的(x, y, z, w)。
  3. ndc = c[:3] / c[3]透视除法:用 w 分量同时除 x、y、z,实现近大远小,得到 NDC 坐标。
  4. sx = (ndc[0] + 1) / 2.0 * width视口变换的 x 映射:从 [-1, 1] 线性映射到 [0, width] 像素范围。

常见误区

误区我一开始以为模型空间变换完,还要再乘一个“世界矩阵”才能到世界空间,所以写了两个矩阵,结果场景里物体位置全乱了。

正解模型矩阵就是“从模型空间变换到世界空间”的那个矩阵,它是一个整体,内部由缩放、旋转、平移组合而成。没有单独的“世界矩阵”这回事。就像小美从化妆间到舞台只有“走一段路”这一个动作,而不是“先走到门口再走到舞台”两个独立动作。

为何常见:很多教程把 M、V、P 三个矩阵并列介绍,初学者容易误以为 M 内部还有多个子矩阵要对顶点依次作用,实际它们已经合并成一个矩阵了。

误区我写代码时先做了透视除法,再把结果拿去裁剪,结果屏幕边缘出现了奇怪的重影。

正解正确顺序是:先做投影矩阵变换得到裁剪空间坐标,在裁剪空间里判断顶点是否在视锥体内(硬件裁剪),然后才做透视除法得到 NDC。如果把除法提前,w 分量就丢了,裁剪判断失去了正确的深度依据。就像导演得先看取景框决定谁进画面,再洗照片,不能先洗照片再决定裁剪。

为何常见:透视除法公式简单(除以 w),看起来像是一个“收尾步骤”,容易被随手提前;但裁剪阶段依赖的是 w 未除之前的齐次坐标范围。

误区第四维 w 就是数学上为了让矩阵变成方阵硬凑出来的,反正永远是 1,没什么实际意义。

正解齐次坐标有两个重要工程作用:一是让平移也能写成矩阵乘法,否则必须单独做加法;二是 w 在透视除法中携带深度信息,是实现近大远小的关键。此外 w=0 可以表示方向向量(如灯光方向),这在线性代数层面解决了“方向不受平移影响”的问题。

为何常见:在模型和世界变换阶段 w 确实恒为 1,看起来“没用”;直到透视投影阶段 w 才开始承载深度,初学者没走到那一步自然感受不到它的价值。

误区矩阵乘法反正都是乘,我习惯先乘平移再乘旋转,结果物体绕了奇怪的点转圈。

正解矩阵乘法不满足交换律。正确习惯是先缩放、再旋转、最后平移(S→R→T),这样旋转是绕模型自身原点转的,符合“先摆姿势、再转身、最后走到指定位置”的直觉。顺序反过来,旋转会变成绕世界原点转,物体就像被甩出去一样。

为何常见:小学数学里乘法交换律深入人心,矩阵乘法打破了这一直觉;而且初学时只验证过一个轴的简单情况,看不出顺序差异。

面试题

Q1. 在齐次坐标中,一个“点”的 w 分量通常取什么值?

单选题 · easy

  • A.0,表示方向向量
  • B.1,表示具体位置← 正确答案
  • C.任意实数都可
  • D.等于 x、y、z 的平均值

答案:B。齐次坐标中 w=1 表示具体的点(工牌号为 1 的演员),w=0 表示方向向量(只指示方向的灯光)。本题考察对 w 分量语义的基本理解,其余选项都不符合齐次坐标的定义。

Q2. 模型空间(本地空间)的坐标系原点位于哪里?

单选题 · easy

  • A.世界舞台的中心
  • B.摄像机镜头的位置
  • C.物体自身设定的基准点← 正确答案
  • D.屏幕的左上角像素

答案:C。模型空间以物体自身为原点,美术师建模时所有顶点都相对这个基准点定义。世界中心属于世界空间,镜头位置属于视图空间,屏幕左上角属于屏幕空间。这正是“化妆间坐标”的比喻含义。

Q3. 模型矩阵组合 M = T × R × S 中,顶点实际经历的变换顺序是?

单选题 · mid

  • A.先平移、再旋转、最后缩放
  • B.先缩放、再旋转、最后平移← 正确答案
  • C.先旋转、再平移、最后缩放
  • D.三个变换同时作用于顶点

答案:B。矩阵乘法从右往左生效,所以顶点先乘 S 做缩放,再乘 R 旋转,最后乘 T 平移。顺序不能调换,否则旋转会绕错误中心进行——这正是“先摆姿势再转身最后走位”的剧组流程。

Q4. NDC(规范设备坐标)中 x、y、z 分量的取值区间是?

单选题 · mid

  • A.[0, 1]
  • B.[0, width] 与 [0, height]
  • C.[-1, 1]← 正确答案
  • D.[-∞, +∞]

答案:C。透视除法后的 NDC 坐标三个分量都被规范化到 [-1, 1] 区间,这个标准盒子让裁剪、深度比较和视口映射都变得简单。[0,1] 是深度缓冲常用的映射区间,[0,width] 是屏幕像素区间。

Q5. 透视除法在整个管线中的正确位置是?

单选题 · mid

  • A.模型变换之前
  • B.视图变换之前
  • C.裁剪判断之后、视口变换之前← 正确答案
  • D.屏幕像素输出之后

答案:C。管线顺序是投影矩阵得到裁剪坐标 → 裁剪判断 → 透视除法得到 NDC → 视口变换得到屏幕坐标。透视除法必须在裁剪之后进行,因为裁剪依赖 w 分量未除之前的齐次坐标做范围判断。

Q6. 以下哪些操作可以统一用 4×4 变换矩阵表达?

多选题 · mid

  • A.模型的平移← 正确答案
  • B.模型的旋转← 正确答案
  • C.模型的缩放← 正确答案
  • D.顶点颜色的混合

答案:ABC。平移、旋转、缩放都是仿射变换,借助齐次坐标可统一写成 4×4 矩阵。颜色混合属于像素着色阶段,与几何坐标变换无关,不能由变换矩阵表达。

Q7. 构造 LookAt 视图矩阵时,必须提供哪三个输入?

单选题 · hard

  • A.物体位置、物体旋转、物体缩放
  • B.摄像机位置、观察目标点、上方向← 正确答案
  • C.视场角、宽高比、近裁剪面
  • D.世界原点、世界三轴、世界缩放

答案:B。LookAt 函数的三个参数是 eye(摄像机位置)、center(观察目标点)、up(上方向),由它们推导出视图矩阵的三根基向量。视场角和宽高比属于投影矩阵参数,与视图矩阵无关。

Q8. 关于透视投影和正交投影,下列说法正确的是?

多选题 · hard

  • A.透视投影会产生近大远小效果← 正确答案
  • B.正交投影适合工程制图类应用← 正确答案
  • C.正交投影也需要透视除法← 正确答案
  • D.透视投影的 w 分量恒为 1

答案:ABC。透视投影的 w 分量包含深度信息,除法后产生近大远小;正交投影的 w 恒为 1,不做透视除法(相当于除 1),因此平行线保持平行,常用于 CAD 制图。C 项说“正交投影也需要透视除法”有一定迷惑性——它形式上也会除以 w,只是 w 恒为 1,结果不变。

Q9. 视口变换的主要作用是什么?

单选题 · easy

  • A.把顶点变换到世界空间
  • B.把 NDC 坐标映射为屏幕像素坐标← 正确答案
  • C.对顶点执行透视除法
  • D.判断顶点是否在视锥体内

答案:B。视口变换是管线最后一步,把 [-1,1] 的 NDC 坐标按屏幕宽高线性映射为像素坐标。世界空间变换、透视除法、裁剪分别对应其他管线阶段。

Q10. 关于 MVP 矩阵 P_clip = M_proj × V_view × M_model × P_model,以下说法正确的是?

多选题 · extreme

  • A.矩阵从右往左依次应用,模型变换最先执行← 正确答案
  • B.三个矩阵可以在 CPU 端预先合并成 MVP 矩阵← 正确答案
  • C.改变摄像机位置时,只需更新 V 矩阵← 正确答案
  • D.模型缩放不会影响最终屏幕上的像素范围

答案:ABC。矩阵右乘先生效,模型最先变换;CPU 可预乘 MVP 减少 GPU 计算量;摄像机移动只影响视图矩阵 V。而模型缩放会影响顶点在裁剪空间的坐标范围,进而影响屏幕上的像素大小,因此 D 错误。