计算机是如何显示3d物体的? 显示器是二维的, 三维的物体要变成二维的, 一定是经过了某种变换.
这个过程跟照相是很相似的, 照相也是将真实世界(三维的)变成一张照片(二维的), 想象一下是如何照相的:
- 找一个好地点, 被照相的人摆好姿势(model transformation)
- 找个好的角度放置相近(view transformation)
- 按下快门(projection transformation)
如何定义一个相机
定义一个相机我们需要三个信息:
- 指定位置(用向量来标记): position
- 看的方向(用单位向量来标记): lookAt
- 向上方向(用单位向量来标记): up
其中和垂直.
如下图所示:
什么是观测变换?
只要相机和被观察的物体的相对位置没有改变, 得到的“相片”就是一样的.
所以我们可以将相机和被观察的物体整体变换, 是相机的的位置在原点, 相机沿着-z看, 向上的方向为y. 这个过程我们称为view transformation.
这么做的原因是相机在原点有很多的好处. 或者说方便透视投影的计算.
如何做观测变换?
我们其实是要得到一个变换矩阵, 相当于原始坐标系发生了变换, 我们最终的目的是要知道的物体在原始坐标系中的坐标经过变换之后的新坐标.
这个变换可以这么来描述:
- 将相机位置平移到原点
- 旋转相机看的方向 与-z轴重合
- 旋转相机向上方向 与y轴重合
其中2,3步相当于将旋转 与x轴重合, 如下图所示:
为图中右边标记为绿色的向量.
我们用变换矩阵来表示观测变换, 表示步骤1的平移变换, 表示步骤2,3的合成的旋转变换, 那么:
的求法
比较简单, 直接给结果:
的求法
代表的是到x轴的变换, 这个不是很好求, 但是x轴到的变换比较好求, 我们可以先求x轴到的变换矩阵, 再求它的逆矩阵, 就得到了.
为什么说x轴到的变换矩阵好求?
投影变换
透视投影 vs 正交投影

正交投影相当于是假设我们的相机离的无限远.
正交投影

定义一个空间中的长方体, 如上图所示, 将其映射成"canonical(正规、规范、标准)"立方体.
具体方法是先平移再缩放, 如下图所示:
可以用如下公式表示:
同样的, 我们需要得到的一个代表正交投影的变换矩阵, 这样我就可以得到我们需要渲染的物体的坐标在标准立方体中的坐标, 直接去掉z坐标, 就可以看作是一个二维的图片了, 如下图所示:
为什么是[-1, 1]? 我觉得是针对不同分辨率的显示器都能方便的进行对应.
透视投影
如何做透视投影:
- 先将锥形压缩成长方体()
- 再做正交投影()
具体公式我看得不是很明白, 这里就不给出了.
上图中的锥形可以叫为视锥, 如何定义视锥:
我们需要指定:
- zNear
- zFar
- field-of-view: 竖直方向视角张开的角度
- aspect ratio: 实际窗口的纵横比
使用webgl绘制一个正方体
准备数据:
// 定义立方体的顶点
const cubePosition = [
[-50, +50, +50], [+50, +50, +50], [+50, -50, +50], [-50, -50, +50], // positive z face.
[+50, +50, +50], [+50, +50, -50], [+50, -50, -50], [+50, -50, +50], // positive x face
[+50, +50, -50], [-50, +50, -50], [-50, -50, -50], [+50, -50, -50], // negative z face
[-50, +50, -50], [-50, +50, +50], [-50, -50, +50], [-50, -50, -50], // negative x face.
[-50, +50, -50], [+50, +50, -50], [+50, +50, +50], [-50, +50, +50], // top face
[-50, -50, -50], [+50, -50, -50], [+50, -50, +50], [-50, -50, +50] // bottom face
];
// 根据顶点划分成三角形
// 因为三角形是webgl基本图元, 复杂图形都是有三角形组成的
const cubeElements = [
[2, 1, 0], [2, 0, 3], // positive z face.
[6, 5, 4], [6, 4, 7], // positive x face.
[10, 9, 8], [10, 8, 11], // negative z face.
[14, 13, 12], [14, 12, 15], // negative x face.
[18, 17, 16], [18, 16, 19], // top face.
[20, 21, 22], [23, 20, 22] // bottom face
]
// 定义相机
const eye = [300, 300, 300]; // 相机位置
const center = [0, 0, 0]; // 焦点, 根据eye和center其实就可以得到相机的朝向
const up = [0, 1, 0]; // up方向
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
// 定义视锥体
const fovy = Math.PI / 4; // 单位是角度
const aspect = viewportWidth/viewportHeight;
const near = 0.1;
const far = 1000;
// 使用了gl-mat4工具库来生成观测变换矩阵和投影变换矩阵
const view = mat4.lookAt([],eye,center,up);
const projection = mat4.perspective([], fovy, aspect, near, far);
顶点着色器:
precision mediump float;
attribute vec3 position;
uniform mat4 projection, view;
void main() {
gl_Position = projection * view * vec4(position, 1);
}
projection, view作为uniform输入, 可以由js随时改变, 动图的控制观测视角.
透视投影会将视锥体压缩成标准立方体, 所以在视锥范围内的点经过变换一定在范围内的. 所以本例中gl_Position一定是在范围内的.
总结
model变换、观测变换(view transformation)、投影变换(projection transformation)就是我们常说的MVP, 它的作用就是将三维空间映射到二维平面. 其中model变换一般来说可以忽略, 因为它其实就是确定好坐标系, 然后在坐标系中确定物体和相机等的坐标. 观测变换是转换原始坐标系得到一个新的坐标系, 在这个坐标系中, 相机的位置在原点, up方向指向x轴, lookat方向指向-z轴. 投影变换是再次转换坐标系, 将视锥体压缩成标准立方体. 所有在这个立方体的物体才会被显示出来.