# 从零开始写软渲染器05

## 1. 概览

这次我们来学习剩下的两种变换，视图变换和投影变换。图形学的渲染和现实中的拍照原理相似，在经过上次分享我们学到的模型变换将 POSE 摆好之后，我们再利用视图变换把摄像机的位置和角度调整好，最后再通过投影变换把镜头拍到的图像洗成相片，就完成了一次简单的渲染。

## 2. 视图变换

View Transformation 视图变换，也可以叫做观测变换或者相机变换，这是因为这个变化做的事情与给相机找个“好角度”有关。思考一下，当我们得到一个三维的场景时，我们首先想的应该是从哪个位置哪个角度来看这个场景，也就是我们需要调整好摄像机的位置和角度。

那么从数学上如何确定好相机的位置和角度呢？我们这样定义相机：

![image](/files/-MXSI-sb17mgOW8o4rmV)

* 相机（或眼睛）的位置 E
* 相机（或眼睛）看向的前方 Look At Direction，记作 G
* 相机（或眼睛）的正上方向 Top Diretion，记作 T

> 读者思考下，为什么需要正上方向？

这样一来我们定义好了相机，接下来，我们还需要观察一个现象，就是“相对论”。想象一下，在确定好相机和物体的位置之后，如果我把相机和物体同时平移一段距离，那么相机和物体的相对位置是保持不变的，拍摄出来的景象也是一样的（可以假设背景是全白的幕布）。

![image](/files/-MXSI-sffplAxkEJfpBT)

既然如此，那么为了方便计算，我把相机移动到原点，让相机的 G 朝向 -Z ，T 朝向 +Y ，这就涉及以下几个仿射变换：

1. 把 E 平移到原点
2. 旋转 G 到 -Z
3. 旋转 T 到 +Y
4. 旋转 (G X T) 到 +X

平移很容易写出变换矩阵：

![image](/files/-MXSI-shUmcGyAz7-Hw9)

旋转怎么写出来呢？读者可以暂停思考下。

这里我们利用上次学到的一个性质，那就是逆矩阵的性质。我们不妨来思考，怎么把 +X 旋转到 (G X T)，+Y 旋转到 T，+Z 旋转到 -G：

![image](/files/-MXSI-sk5i-vMKGTVGFo)

我们需要求的其实就是上面矩阵的逆矩阵。好消息是，这里的矩阵是旋转矩阵，通过定义可以证明，旋转矩阵的逆矩阵是其转置，也就是说，旋转矩阵是一个正交矩阵，那么问题就很简单了，我们只需要把上面的矩阵转置就能得到答案：

![image](/files/-MXSI-smN0u7U1xiAlag)

所以，我们相机需要进行得变换就是：

![image](/files/-MXSI-soS_ArnQqM7oit)

其实，也就是（相对论）物体在做完第一步 Model 变换之后，还需要做上面推出的 View 变换，做完这两个变换之后，相机就在原点，并且看向 -Z ，正上方指向 +Y 。至此就完成了相机的摆放问题。

> 读者发现了没有，视图变换其实也还是仿射变换，跟 Model 变换做的事情其实差不多。这两个变换常常也被人合起来称作 ModelView 变换。

## 3. 投影变换

我们常见的两种投影：正交投影和透视投影，他们的作用就是把三维的视图转化为二维的图像。在这里，我们需要把他们用数学来表示出来。

![image](/files/-MXSI-srYDYE2wjy5gvN)

### 3.1 正交投影

大学学习的工程制图中，基本就是在学怎么画正交投影，那么一个简单的正视图我们怎么画呢？其实就是分以下几步：

![image](/files/-MXSI-stHMovDoZTKlRs)

1. 摆好相机（回想以下我们约定的相机摆放位置）
2. 把 Z 轴干掉
3. 为了方便计算，把物体平移和缩放到 \[-1, 1] 的二维平面上

拓展到三维的数学表示上，其实就是先找到一个长方体的包围盒，长方体分上下左右远近(t,b,l,r,f,n)六个面。

![image](/files/-MXSI-su056MTrof_uyD)

然后，先把长方体中心平移到原点，再把长宽高缩放成一个 \[-1, 1] 的正方体。这样的过程，就是正交投影的规范化，或者说标准化（这么做都是为了方便计算）：

![image](/files/-MXSI-svwnIivTA2R_tn)

到这里，我们就找到了一种三维转二维的方式，只不过正交投影并不是我们常用的，他生成的二维图像也不是我们日常常见的，我们见的最多的是另外一种投影，透视投影。

> 一个物体被正交投影标准化后，其实会被拉伸，但是这并不是三维转二维的最后一步！在做完正交投影之后，我们还需要做一个视口变换(Viewport Transformation)，才把二维图像显示到屏幕上。

### 3.2 透视投影

透视投影是我们平常看到最多的投影，这种投影有近大远小的特点。那么在数学中，我们怎么表示呢？

> 提醒一下，这里的基本逻辑就是，把透视投影转化为我们刚学的正交投影，然后再由正交投影转为二维图像。

首先抽象地想象一下，其实透视投影就是从相机所在的原点向-Z方向形成的一个四棱锥。

![image](/files/-MXSI-swO_Uraosw3wlM)

离相机比较近的叫做近平面n，离相机比较远的叫做远平面f，这两个平面是我们定死的，然后两个平面之间夹起来的形状，我们叫做视锥(Frustum)，我们要干的就是想办法把这个视锥转为一个长方体，然后转为正交投影就完事了。

![image](/files/-MXSI-sx4L0RkjdidZ5u)

透视投影转正交投影的矩阵如下：

![image](/files/-MXSI-sycGV4maDHJ65A)

下面给出这个矩阵的推导过程（by 闫令琪大神），首先观察， 1. 对于近平面，在变换之后，该平面上的点其实都保持原样 2. 对于远平面，上面的点的Z在变换之后保持不变 3. 对于远平面，平面上的中心点变换之后保持不变

先看Y，由相似三角形可得：

![image](/files/-MXSI-sz5FfhXpYBK7vg)

对于X，同理可得：

![image](/files/-MXSI-t-aHJaZYjSSjy8)

那么可以推出任一点 (x, y, z) 推出 (x', y', z')：

![image](/files/-MXSI-t0nfeyPJmc__zx)

即：

![image](/files/-MXSI-t1vTFimTf9AjbV)

从而可以求出透视矩阵的三行：

![image](/files/-MXSI-t2IMz-iDb9i7Qv)

现在求第三行，根据观察2得到：

![image](/files/-MXSI-t38WdsZu8WFJMy)

![image](/files/-MXSI-t4ABfrBr9ErxtV)

根据观察3得到：

![image](/files/-MXSI-t5OMVKemyzf15q)

最后即可解出第三行：

![image](/files/-MXSI-t6NqhCh0QbcTeJ)

至此，我们已知如何从透视投影转到正交投影，那么要得到最后的透视投影，只需要乘上之前的正交投影矩阵即可：

![image](/files/-MXSI-t7RZWHSaB9XEO-)

## 4. 小结

这次加上上次的分享，我们终于把 MVP 变换讲完了，这意味着我们有能力把三维场景转二维图像了，总结下来，其实就是先把物体的位置摆好(Model Transform)，再把相机的位置摆好(View Transform)，最后把三维的物体投影到二维的平面上(Projection Transform)，过程中用到的都是矩阵乘积的知识。

最近工作太忙，都没什么时间写代码，原本想着清明假期可以抽时间写一写，然而父母过来视察，就陪他们逛逛街、吃吃东西。下周我们就抽抽空改一改代码框架，把 MVP 变换写完吧！

## 5. 参考

Peter Shirley, Steve Marschner. Fundamentals of Computer Graphics, Third Edition.

GAMES101, <https://sites.cs.ucsb.edu/~lingqi/teaching/games101.html>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://xiaobaiha.gitbook.io/tech-share/gamedev/cong-ling-kai-shi-xie-ruan-xuan-ran-qi-05.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
