手写一个简单图形渲染引擎01

手写简单图形渲染器。第一部分简单介绍引擎的基本结构。项目由C++语言完成。

基本结构设计

一个图形渲染器的任务是将空间的三角形块面正确地栅格化。更明确地说,是将空间内一个受到光照的,拥有纹理(贴图)的三角形块面,投影到摄像机的观察平面上,并计算出各个像素点对应的颜色。还需要考虑深度、平面相交等问题。一个可行的层级结构设计如下,更高级的场景管理、特效等可以在此渲染器之上实现。

  1. 3D空间中的网络(三角形)、光源、纹理、摄像机信息;
  2. 摄像机空间;
  3. 齐次裁剪空间;
  4. 对图形进行栅格化,保留顶点在原空间中的位置信息,法线,贴图坐标等信息,并对栅格化碎片的各种属性插值——空间坐标(深度,坐标)、贴图坐标、法线;
  5. 着色:根据材质、法线、光源、摄像机坐标、空间点坐标,对碎片进行着色。

空间信息

空间信息包含顶点、由顶点构成的三角形块面、光源、摄像机等信息。这些实体都最开始存放在世界坐标系中。关于材质的绘制,我们可以将渲染器编写为一个状态机,材质、贴图等设置可以作为状态设定,设定完毕后绘制出指定的光栅化三角形。

3d scene management

世界坐标系使用右手坐标系系统。右手坐标系是指,张开右手的拇指和食指,分别作为x轴和y轴正方向,中指指向的、垂直于x轴、y轴的方向为z轴正方向。

摄像机空间

空间坐标变换的第一步是将世界坐标系投射到摄像机空间中。为了方便理解,一个典型的摄像机在世界坐标原点、三轴旋转为0时,它是看向z轴负方向的。牢记这一点规定,你会更好地理解后面的说明。摄像机空间简单说是一个摄像机在原点,朝向某个特定方向——在摄像机空间中,是z轴的负方向。旋转操作分为pitch、yaw和roll,这三种旋转分别是俯仰、偏航和横滚,分别对应绕x、y和z轴的旋转。所有的旋转正方向都被定义为朝轴的负方向望去,逆时针的旋转。

首先对世界空间进行平移,平移量为(-camX, -camY, -camZ)。然后摄像机在该空间的原点位置了。现在要做的是旋转空间,使得摄像机看到的内容正确地处在z轴负方向。通常我们的旋转顺序为pitch、yaw和roll,依次进行这三种旋转,将摄像机旋转指定的角度。(思考下为什么?)

camera space transformation

齐次裁剪空间

齐次坐标系统拥有普通的坐标无可比拟的计算特性,它可以方便地使用矩阵运算进行变换。齐次裁剪空间的作用在于,根据顶点所处的距离和摄像机的视角等计算出单点透视投影后的位置。齐次裁剪空间的特点是,被拍摄到的顶点在x和y方向上的取值范围是-1到1,无视摄像机的宽高比。但是它z保留顶点到xoy平面的距离。另一个齐次裁剪空间的特点是,它处在左手坐标系中。离摄像机越远的点拥有更大的深度值,而负的深度值表示点处于摄像机的后方。为了从摄像机空间投影到齐次裁剪空间,我们考虑单点透视的规律——以z轴灭点为中心,越远的顶点越朝该中心收缩。实际上,收缩的比例是深度z的倒数。摄像机的FOV(Field of View)参数表示的是y方向上的视野,再根据aspectRatio即可确定x方向上的视野大小。所以可以得到如下的变换关系。假设齐次裁剪空间为$H$,原空间为$O$。

$$
\begin{align}
x_H & =\frac{1}{z_0 \tan(fov)}\frac{1}{aspectRatio}x_0 \\
y_H & =\frac{1}{z_0 \tan(fov)}y_0 \\
z_H & =-z_0
\end{align}
$$

根据上述变换,可以得到投影变换矩阵:

$$perspectiveProjectionMatrix=\begin{pmatrix}\frac{1}{\tan(fov)} & 0 & 0 & 0 \\0 & \frac{1}{\tan(fov) aspectRatio} & 0 & 0 \\0 & 0 & -1 & 0 \\0 & 0 & -1 & 0 \end{pmatrix}$$

上面的矩阵用于左乘列向量坐标。观察到实际上所有的点都被投影到了$z=1$的所在平面上。实际上的坐标,根据齐次坐标的定义,为$(x/w, y/w, z/w)$,而深度信息被保存在了结果的$z$中。

在投影到其次裁剪空间之后,可以进行块面裁剪操作。首先剔除所有顶点均在摄像机的视野之外,而且在nearZ和farZ之外的三角形。然后对于相交的三角形,可以对三角形进行拆分和裁剪操作。

栅格化和插值

栅格化将齐次裁剪空间中能被摄像机观察到的(z小)的点,渲染到指定尺寸的屏幕空间中。栅格化的意义在于,栅格碎片与屏幕中的像素点一一对应。为了进行栅格化,我们需要遍历屏幕空间中所有的位置,计算出它在空间中是否在某个三角形块面上。这可以使用向量叉积进行判断。随后,计算该点在三角形块面的位置进行插值。假设三角形三点是$A$,$B$和$C$。我们用$\overrightarrow{AB}=\vec u$和$\overrightarrow{AC}=\vec v$来表示目标点$P$:$P=A+s\vec u+t \vec v$。解二元一次方程组即可求出$s$和$t$。首先对于三点的某个属性$a$,可以使用双线性插值对其进行插值,插值针对三点都在平面中的情况可以正确进行:$P_a=A_a\times (1-s-t)+B_a\times s + C_a\times t$。双线性插值的问题是——对于三点z不同的形况下,投影会发生错误。

比如如下的一维透视投影:屏幕坐标的等距点并非原平面(线)上的等距点。

linear interpolation

可以观察到,线性插值的插值点并非实际位置的插值点位置。实际操作中,需要使用透视矫正插值——系数中加入顶点z坐标的倒数,对不同深度插值进行修正。插值分为2步:

  1. 首先需要插值计算出目标点$P$的深度(倒数)
  2. 再使用深度坐标和透视不变插值公式对其它参量进行插值

$$
\begin{align}
\frac{1}{z_P} &= (1-s-t)\times \frac{1}{z_A} + s\times \frac{1}{z_B} + t\times \frac{1}{z_C} &\text{(1),} \\
attr_P &= \left(\frac{1-s-t}{z_A}\times attr_A + \frac{s}{z_B}\times attr_B + \frac{t}{z_C}\times attr_C\right) \times z_P &\text{(2).}
\end{align}
$$

等式1表示了在单点透视过程中,深度倒数符合线性关系。等式2在等式1的基础上提供了透视矫正插值。通常,需要插值的属性有空间坐标、法线、贴图坐标等。这些信息,连同场景中的光照、材质、贴图资源信息都将被传入到下一个阶段——着色阶段。

深度缓冲

构建一个和光栅场一样的深度记录区,用于记录已经光栅化的像素距离摄像机平面xoy的最近距离。判断新的点是否被遮挡,从而对被遮挡部分进行剔除。

着色和渲染

着色阶段根据光栅的各种属性,计算出该光栅的颜色。计算机图形中,有若干种光照/色彩模型,这里我们只用简单的RGB线性叠加即可。我们首先实现一个ADS(Ambient, Diffuse, Specular)光照模型,分别对应环境光、漫反射和高光。环境光被定义为在环境中反射足够多的次数,以至于到达物体表面时是从各个方向照射来的、均匀的同波长电磁波;漫反射是粗糙物体表面的光线反射行为,它的宏观简单模型描述的光线反射行为是——表面的反射强度只和入射光的大小有关,入射的光线在宏观上,向表面的各个方向上均匀地反射出去;高光类似于镜面反射的行为,在该点对光源镜面反射的光线和相机接近时,光最强。

根据上面阶段提供的场景和栅格信息,可以计算出各种光源和摄像机向量信息,从而计算光照和颜色。

demo

扩展阅读

  1. 多边形网络,法线和贴图坐标
  2. 光,颜色和色彩空间
  3. 摄像机-小孔成像模型
  4. 光栅化:一种实现