all lessons / computer_graphics / 02 · transforms & spaces lesson 2 / 15

Transforms & spaces — homogeneous coordinates变换与空间——齐次坐标

Lesson 01 kept saying "place a triangle at depth Z, project it through the pinhole." Both loops in that widget were faking a machine we had not built: the one that moves points between object, world, camera, and screen spaces, and performs the nonlinear perspective divide at the end. This lesson builds that machine. The key idea is a single dimension of bookkeeping — the homogeneous coordinate w — that turns translation and perspective, the two operations plain matrices cannot do, into ordinary matrix multiplies. 第 01 课一直在说“把三角形放在深度 Z 处、经针孔投影它”。那个控件里的两个循环,都在假装我们已有一台还没造出来的机器:它在物体、世界、相机、屏幕各空间之间搬运点,并在最后完成非线性的透视除法。这一课就来造这台机器。核心思想只是多记一维账——齐次坐标 w——它把平移与透视这两件普通矩阵做不到的事,变成了寻常的矩阵乘法。

The plan本课计划

Five moves. (1) Separate points from vectors and show why translation is not a linear map, so a 3×3 matrix can never express "move over there." (2) Add the homogeneous w and watch translation fall out as a 4×4 matrix — payoff one. (3) Assemble the affine transforms — translate, scale, rotate — and insist on order, because matrix multiplication does not commute. (4) String them into the transform chain model→world→view→clip→NDC→screen, where the projection matrix stashes depth into w and the perspective divide (payoff two, deferred from lesson 01) happens last. (5) Catch the classic trap: normals do not transform like points. Then lesson 03 hands us the geometry to push through this chain.

五步走。(1) 把点与向量分开,说明为什么平移不是线性映射,因而 3×3 矩阵永远表达不了“挪到那边去”。(2) 加上齐次分量 w,看平移如何以一个 4×4 矩阵自然浮现——收益之一。(3) 装配仿射变换——平移、缩放、旋转——并强调顺序,因为矩阵乘法不可交换。(4) 把它们串成变换链 model→world→view→clip→NDC→screen,其中投影矩阵把深度藏进 w,而透视除法(收益之二,从第 01 课推迟至此)发生在最后。(5) 抓住那个经典陷阱:法线的变换方式与点不同。随后第 03 课把要塞进这条链的几何交到我们手上。

1 · Points, vectors, and why translation isn't linear点、向量,以及为什么平移不是线性变换

Two things live in a scene that look like triples of numbers but behave differently. A point is a location (a vertex of a triangle). A vector is a displacement or direction (a surface normal, a ray direction, a velocity). The test: it makes sense to translate a point ("move the vertex 3 units right"), but translating a direction is meaningless ("move north 3 units to the right" is still north). We need the algebra to respect this difference, and plain 3-vectors do not.

场景里有两种东西,看起来都是三元数组,行为却不同。是位置(三角形的一个顶点)。向量是位移或方向(表面法线、光线方向、速度)。检验办法:平移一个点是有意义的(“把顶点向右移 3 个单位”),但平移一个方向毫无意义(“把‘北’向右移 3 个单位”仍然是北)。我们需要一套代数来尊重这个区别,而普通的三维向量做不到。

Recall what a matrix can do. A linear map M obeys M(a + b) = Ma + Mb and M(0) = 0 — it must fix the origin. Rotation and scale fix the origin, so a 3×3 matrix handles them. But translation moves the origin: T(0) = t ≠ 0. No 3×3 matrix can translate. Yet the whole point of "put the model into the world" is translation. This is the wall.

回想矩阵能做什么。线性映射 M 满足 M(a + b) = Ma + MbM(0) = 0——它必须固定原点。旋转与缩放固定原点,所以 3×3 矩阵能处理它们。但平移会移动原点:T(0) = t ≠ 0。没有任何 3×3 矩阵能做平移。可“把模型摆进世界”本质上就是平移。这就是那堵墙。

2 · Homogeneous coordinates — one extra number, two payoffs齐次坐标——多一个数,两份收益

The fix is disarmingly small: append a fourth coordinate w. Write a point as (x, y, z, 1) and a vector as (x, y, z, 0). The w = 1 makes translation reachable by a matrix; the w = 0 makes a direction immune to translation automatically (multiply it out and the translation column, scaled by 0, vanishes). One convention encodes the point/vector distinction from section 1 for free.

修复的办法小得让人意外:追加第四个坐标 w。把点写成 (x, y, z, 1),把向量写成 (x, y, z, 0)w = 1 让平移能被矩阵触及;w = 0 让方向自动对平移免疫(乘开后,平移那一列被 0 缩放而消失)。一个约定,免费地把第 1 节的点/向量区别编码了进去。

Now translation is a matrix. Multiply and read off the bottom-row-preserving result:

现在平移就是一个矩阵。乘出来,看这个保持底行的结果:

T(t) =
⎡ 1 0 0 tx ⎤   ⎡x⎤     ⎡ x + tx
⎢ 0 1 0 ty ⎥ · ⎢y⎥ = ⎢ y + ty
⎢ 0 0 1 tz ⎥   ⎢z⎥     ⎢ z + tz
⎣ 0 0 0 1  ⎦   ⎣1⎦     ⎣   1   ⎦

That is payoff one: every affine transform — rotate, scale, translate, and any product of them — is now a single 4×4 matrix, so a whole chain collapses to one matrix we can precompute. Payoff two is subtler and is the reason we chose this specific machinery over just "add a translation vector afterwards": if we allow the bottom row to be nonzero, the matrix can write into w, and a later divide by w produces exactly the nonlinear 1/Z of perspective. Homogeneous coordinates are the only representation that makes both translation and perspective linear matrix steps. Hold that thought for section 4.

这就是收益之一:每一个仿射变换——旋转、缩放、平移,以及它们的任意乘积——现在都是单个 4×4 矩阵,于是一整条链坍缩成一个可以预计算的矩阵。收益之二更微妙,也正是我们为何选这套特定机制、而不只是“事后加个平移向量”的原因:如果允许底行非零,矩阵就能写入 w,而随后除以 w 恰好产生透视那个非线性的 1/Z。齐次坐标是唯一能让平移与透视都成为线性矩阵步骤的表示。把这个念头留到第 4 节。

3 · The affine kit, and why order is everything仿射工具箱,以及为什么顺序就是一切

Three building blocks (upper-left 3×3 shown; the 4×4 pads with the homogeneous row):

三个基本构件(此处显示左上 3×3;4×4 用齐次行补齐):

Scale · 缩放
diag(sx, sy, sz)
Rotate · 旋转 (about z)
[[cosθ, −sinθ],[sinθ, cosθ]]
Translate · 平移
+ (tx, ty, tz) via the w column经由 w

To place an object you compose them, and matrix multiplication does not commute: A·B ≠ B·A. With column vectors, the matrix nearest the vector is applied first — you read a chain right-to-left. "Rotate then translate" T·R orbits the object into place facing a new heading; "translate then rotate" R·T swings it around the origin like a planet. Same two operations, wildly different result.

要摆放一个物体,你把它们复合,而矩阵乘法不可交换A·B ≠ B·A。对列向量而言,离向量最近的矩阵最先作用——一条链要从右往左读。“先旋转再平移”T·R 把物体转好朝向、再送到位;“先平移再旋转”R·T 则像行星绕原点公转一圈。同样两个操作,结果天差地别。

p′ = T · R · S · p   ⟹   scale first, then rotate, then translate · 先缩放,再旋转,最后平移
The order foot-gun顺序这把“走火枪”

The single most common transform bug: building S·R·T when you meant T·R·S, or multiplying in row-vector convention (which reverses the reading order) while thinking in column-vector convention. The symptom is an object that spirals away from the origin as you rotate it, or scales its translation. Fix the convention once — this course uses column vectors, matrix on the left, read right-to-left — and never mix it.

最常见的变换 bug:本想要 T·R·S 却搭成了 S·R·T,或者在行向量约定下相乘(阅读顺序相反)却按列向量约定思考。症状是:物体一旋转就螺旋着飞离原点,或者连平移量也被缩放了。把约定一次定死——本课用列向量、矩阵在左、从右往左读——然后绝不混用。

4 · The transform chain and the perspective divide变换链与透视除法

A vertex takes a fixed pipeline of spaces to reach the screen. Each arrow is one matrix multiply; only the last step is nonlinear.

一个顶点要经过一条固定的空间流水线才能到达屏幕。每一支箭头是一次矩阵乘法;只有最后一步是非线性的。

object ──[ M model ]──▶ world ──[ V view ]──▶ camera ──[ P projection ]──▶ CLIP (mesh's own (scene, shared (eye at origin, (x,y,z,w), coordinates) by all objects) looking down −z) w now holds depth │ ÷ w (PERSPECTIVE DIVIDE) ▼ screen ◀──[ viewport ]── NDC ◀────────────────────────────────────── x/w, y/w, z/w (pixels) (cube [-1,1]³, resolution-independent)

Why divide at the end and not immediately? Because clip space — before the divide — is where the GPU clips triangles against the view frustum and where lesson 05's perspective-correct interpolation gets its 1/w. The linear stretch of the chain is deliberately kept linear as long as possible; the one nonlinearity is quarantined to a single, well-understood divide. The widget makes that divide visible: watch a receding row of equal-height posts foreshorten under perspective, then flatten to equal heights under orthographic projection, where there is no divide at all.

为什么要在最后才除、而不是立刻除?因为裁剪空间——在除法之前——正是 GPU 拿三角形与视锥做裁剪之处,也是第 05 课透视校正插值取得其 1/w 的地方。这条链的线性段被刻意尽量保持线性;唯一的非线性被隔离到单独一次、被透彻理解的除法里。下面的控件把这次除法可视化:看一排等高的柱子在透视下随距离缩短,再在正交投影下——那里根本没有除法——被压平成等高。

Where the divide-by-Z lives — perspective vs orthographic除以 Z 究竟发生在哪 —— 透视 vs 正交
Side view. The eye is on the left; the vertical dashed line is the image plane at focal length f; the bars are five posts of equal world height at increasing depth. Under perspective, projection rays converge to the eye and each post's image height is f·H/Z — farther posts shrink (foreshortening), the visible signature of the ÷w divide. Under orthographic, projectors are parallel, there is no divide, and every post projects to the same height. Drag f.侧视图。眼睛在左;竖直虚线是位于焦距 f 处的成像平面;条形是五根等世界高度、深度递增的柱子。在透视下,投影光线汇聚于眼睛,每根柱子的成像高度为 f·H/Z——越远越矮(近大远小),正是 ÷w 除法的可见标志。在正交下,投影线彼此平行,没有除法,每根柱子都投影成相同高度。拖动 f
Divide by Z?
yes
Nearest img height
Farthest img height
Foreshortening (far/near)
Show the core JS查看核心 JS
// Equal-world-height posts H at depths Z; image plane at focal length f.
// PERSPECTIVE: similar triangles → image height = f * H / Z  (this IS the ÷w divide)
// ORTHOGRAPHIC: parallel projectors → image height = H       (no divide, no depth cue)
function project(Z, H, f, perspective){
  return perspective ? (f * H / Z) : H;
}
var near = project(Zmin, H, f, true);   // tallest
var far  = project(Zmax, H, f, true);   // shortest
var foreshorten = far / near;           // < 1 under perspective, = 1 under ortho

5 · The normal trap — directions transform by the inverse-transpose法线陷阱——方向要用逆转置来变换

One last hazard, because it silently corrupts every later lighting lesson. A surface normal is a direction, but it does not transform by the same matrix M as the points on the surface. If you scale a shape non-uniformly — say, stretch it along x — and rotate its normals by that same stretch, the "normals" tilt the wrong way and no longer stay perpendicular to the surface. The correct rule: transform normals by the inverse-transpose of the model matrix's upper 3×3:

最后一个隐患,因为它会悄无声息地污染后面每一课的光照。表面法线是一个方向,但它按点所用的同一个矩阵 M 变换。如果你对形状做非均匀缩放——比如沿 x 拉长——再用同样的拉伸去变换法线,那些“法线”就会朝错误方向倾斜,不再垂直于表面。正确规则:法线用模型矩阵左上 3×3 的逆转置来变换:

n′ = (M3×3−1)T · n    (for rotation-only M, M−1ᵀ = M, so nothing changes · 若 M 只含旋转,则 M−1ᵀ = M,不变)

The intuition: normals are perpendicularity constraints, and perpendicularity is preserved only if you undo the shape distortion in the "opposite" way — which is exactly what the inverse-transpose does. This matters the instant lesson 08 dots a normal with a light direction; a wrongly-transformed normal makes physically-based shading quietly wrong.

直觉是:法线是“垂直性”约束,而垂直性只有在你以“相反”的方式抵消形状畸变时才被保持——逆转置做的正是这件事。到第 08 课把法线与光照方向做点积的那一刻,它立刻要紧;一个被错误变换的法线,会让基于物理的着色悄悄出错。

Where this points next接下来指向何处

We can now take any point — a triangle vertex — from its object space all the way to a pixel, and we know directions (normals, ray dirs) ride along with their own rule. But we have been assuming the objects are bags of triangle vertices. Where do those triangles come from, why triangles and not squares or spheres, how do we store a normal per vertex, and what about smooth curved surfaces and the implicit shapes a ray tracer prefers? Lesson 03 defines the geometry — meshes, curves, subdivision, and signed distance fields — that this transform chain exists to move. After that, lesson 04 finally runs the rasterization loop from lesson 01 for real.

现在我们能把任意一个点——一个三角形顶点——从它的物体空间一路送到某个像素,也知道方向(法线、光线方向)随行时自有其规则。但我们一直假设物体就是一袋三角形顶点。那些三角形从哪来?为什么是三角形而不是正方形或球?每个顶点的法线怎么存?还有光滑曲面、以及光线追踪偏爱的隐式形状呢?第 03 课定义这条变换链存在的意义所在——要搬运的几何:网格、曲线、细分与有向距离场。之后第 04 课才真正把第 01 课的光栅化循环跑起来。

Takeaway要点

Points (x,y,z,1) and vectors (x,y,z,0) differ, and translation is not linear — a 3×3 matrix cannot do it because it must fix the origin. The homogeneous w fixes this twice over: it makes translation a 4×4 matrix (payoff one) and, by letting the matrix write depth into w, makes perspective a matrix followed by a single divide by w (payoff two). Compose affine transforms as T·R·S and respect that matrix multiply does not commute. The full transform chain is model→world→view→clip→(÷w)→NDC→screen, keeping everything linear until the one quarantined perspective divide. Finally, normals transform by the inverse-transpose (M^{-1})^T, not by M — or your lighting is wrong.

(x,y,z,1)向量 (x,y,z,0) 不同,而平移不是线性的——3×3 矩阵做不到,因为它必须固定原点。齐次分量 w 一举两得地解决了它:让平移成为 4×4 矩阵(收益一),又通过允许矩阵把深度写入 w,让透视变成“一个矩阵 + 一次除以 w”(收益二)。仿射变换按 T·R·S 复合,并牢记矩阵乘法不可交换。完整的变换链是 model→world→view→clip→(÷w)→NDC→screen,在唯一被隔离的透视除法之前一切保持线性。最后,法线用逆转置 (M^{-1})^T 变换、而非 M——否则光照就是错的。

Interview prompts面试题