《游戏编程算法与技巧》渲染篇

闲话

近日读完了《游戏编程算法与技巧》这本书。更感觉是《游戏引擎架构》的缩略图。先阅读此书会对游戏开发有一个整体的认识与理解。适合需要了解游戏开发的初学者阅读

此书对于我也是梳理游戏开发知识点的概括好书。对于此书会在重点部分做提炼。

渲染基础

此部分只做简单的总结,网上已经有很多的好文章了。而此书作者也没有很详细的写这部分。毕竟这块要是讲细了够学一辈子了

推荐一些好文(有兴趣可以看看我写的软渲染

双缓冲技术解决渲染撕裂

CRT显示器时代,所谓场消隐期(VBLANK):喷枪从右下角移动到左上角所花费的时间

渲染撕裂:显示器在绘制像素缓冲区中的内容时,游戏输出了新的渲染结果到像素缓冲区,导致渲染撕裂

双缓冲技术:解决屏幕撕裂,用两块像素缓冲区,游戏交替的在这两块缓冲区中绘制

垂直同步:让交换缓冲区的时机在场消隐期进行

三缓冲技术:利用3个缓冲区,让画面更加平滑,但增加输入延迟

画家算法

所有物体按照从后往前的顺序绘制。优点:绘制绝对正确(包括透明物体),缺点:overdraw高,效率慢

3D渲染

为什么用三角形表示面片?

  • 仅用3顶点表示的最简单的多边形
  • 三角形总在一个面上
  • 任何3D对象都可以简单地用细分三角面表示

网格:多个三角面组成

软件光栅化

将3D模型正确渲染到2D颜色缓冲的算法

3D模型经过4个主要坐标系空间转到最终的2D颜色缓冲中

  • 模型(局部)坐标系:相对于模型自身的坐标系(角色模型一般为两脚中间)
  • 世界坐标系:所有对象都相对于世界原点偏移
  • 视角(摄像机)坐标系:将世界坐标系的模型变换到相对于摄像机的位置上
  • 投影坐标系:将3D场景平铺到2D平面上得到的坐标系

除了上面的坐标系外,还有一个特殊的坐标系,就是齐次坐标系

将4D坐标系应用在3D空间中,就被称为齐次坐标系。而第四个分量为w分量。如果w=0,则此齐次坐标是3D向量。而w=1,则表示此齐次坐标是3D的点

矩阵变换

将模型在各个坐标系中转换所用的矩阵,就是矩阵变换。

此处和之后所用的都为行向量。(OpenGL为列向量)

模型转世界坐标系

平移

将顶点移动一段距离,只作用在点上

$$ T(t_x,t_y,t_z) = \begin{bmatrix} 1 & 0 & 0 & 0 \newline 0 & 1 & 0 & 0 \newline 0 & 0 & 1 & 0 \newline t_x & t_y & t_z & 1 \end{bmatrix} $$

旋转

将顶点或向量相对于某个轴旋转(欧拉角旋转)

旋转矩阵是正交的,就是说转置矩阵就是逆矩阵

$$ RotateX(\theta) = \begin{bmatrix} 1 & 0 & 0 & 0 \newline 0 & \cos \theta & - \sin \theta & 0 \newline 0 & \sin \theta & \cos \theta & 0 \newline 0 & 0 & 0 & 1 \end{bmatrix} $$

$$ RotateY(\theta) = \begin{bmatrix} \cos \theta & 0 & \sin \theta & 0 \newline 0 & 1 & 0 & 0 \newline - \sin \theta & 0 & \cos \theta & 0 \newline 0 & 0 & 0 & 1 \end{bmatrix} $$

$$ RotateZ(\theta) = \begin{bmatrix} \cos \theta & - \sin \theta & 0 & 0 \newline \sin \theta & \cos \theta & 0 & 0 \newline 0 & 0 & 1 & 0 \newline 0 & 0 & 0 & 1 \end{bmatrix} $$

缩放

$$ S(s_x,s_y,s_z) = \begin{bmatrix} s_x & 0 & 0 & 0 \newline 0 & s_y & 0 & 0 \newline 0 & 0 & s_z & 0 \newline 0 & 0 & 0 & 1 \end{bmatrix} $$

按行矩阵应用矩阵变换,乘积的顺序应为:$ model2world = scale * rotation * translation $

应先旋转在平移,因为旋转是相对于原点的,先旋转对象可自转。而后旋转则对象就是相对于世界坐标系原点旋转了。

观察矩阵(Look-At)

$L$表示左边或x轴,$U$表示上方或y轴,$F$表示前方或z轴,$T$则是摄像机的平移

$$ LookAt = \begin{bmatrix} L_x & U_x & F_x & 0 \newline L_y & U_y & F_y & 0 \newline L_z & U_z & F_z & 0 \newline T_x & T_y & T_z & 1 \end{bmatrix} $$

 1Matrix4x4 LookAt(Vector3 eye, Vector3 target, Vector3 Up) {
 2    Vector3 F = normalize(target - eye);
 3    Vector3 L = normalize(cross(Up, F));
 4    Vector3 U = cross(F, L);
 5    Vector3 T;
 6    T.x = -dot(L, eye);
 7    T.y = -dot(U, eye);
 8    T.z = -dot(F, eye);
 9
10    //创建并返回观察矩阵
11    //...
12}

投影坐标系

正交投影

$$ Orthographic = \begin{bmatrix} \frac{2}{width} & 0 & 0 & 0 \newline 0 & \frac{2}{height} & 0 & 0 \newline 0 & 0 & \frac{1}{far - near} & 0 \newline 0 & 0 & \frac{near}{far - near} & 1 \end{bmatrix} $$

透视投影

$$ Perspective=\begin{bmatrix} \cot \frac{fov}{2} & 0 & 0 & 0 \newline 0 & \frac{height}{width} & 0 & 0 \newline 0 & 0 & \frac{far}{far - near} & 1 \newline 0 & 0 & \frac{-near * far}{far - near} & 0 \end{bmatrix} $$

光照与着色

颜色

RGB颜色空间:将颜色分为红色,绿色和蓝色分量。

色深:每个像素用多少位来存储(大部分每分量都为8位来存储,就是说每位有256种可能,共大约1600万种不同颜色)

不透明度(用A表示,从而组成RGBA,一共32位)

顶点属性

模型顶点上存储的额外信息

  • 纹理映射:将2D图片映射到3D的三角形中
    • UV坐标:纹理的x坐标为u,y坐标为v
  • 顶点法线:产生凹凸感
    • 将拥有该顶点的三角形的法线取平均值,用于平滑模型(如圆形)
    • 每个顶点存储自己的法线方向,用于棱角清晰的模型(如四边形)

顶点序:顶点的顺序,用于决定叉乘的值向量的朝向,保持全程一致就可。还可用于背面剔除

光照
  • 环境光:添加到场景中每一个物体上固定的光
  • 方向光:没有位置,只指定光照方向的光
  • 点光源:从某个点向四面八方射出的光照
  • 聚光灯:将点光源限制在锥体内有光

Phong光照模型公式:

$$ Phong 光照 = 环境光 + 漫反射 + 高光 $$
  • 漫反射:光源作用于物体表面的主要反射。被所有方向光,点光源和聚光灯影响。
  • 高光:物体表面的闪光点
 1// N = 物体表面法线
 2// eye = 摄像机位置
 3// pos = 物体表面位置
 4// a = 高光量
 5Vector3 V = normalize(eye - pos);
 6Vector3 Phong = AmbientColor;   // 环境光
 7foreach Light light in scene    // 循环场景中所有的灯光
 8    Vector3 L = normalize(light.pos - pos); // 从物体表面到光源
 9    Phong += DiffuseColor * dot(N, L);  // 漫反射光照强度
10    Vector3 R = normalize(reflect(-L, N)); // 计算-L关于N的反射
11    Phong += SpecularColor * pow(dot(R, V), a); // 高光强度
着色方式
  • 平面着色:每个三角面只用一种颜色
  • Gouraud着色:通过顶点颜色插值填充(多边形数量越多越好)
  • Phong着色:逐像素光照,针对每个像素单独计算

深度缓冲区

为场景的每个像素存储数据,与颜色缓冲一样,不过存储的是像素到摄像机的距离(深度)

在每一帧渲染前会清空深度缓冲区(让所有像素无限远)。渲染过程中,深度会在像素着色器渲染前计算出来。如果比当前深度小,则绘制并写入新的深度

透明对象的绘制

透明对象不适用于深度缓冲,如果透明物体比不透明物体深度浅,如果先画透明物体,则不透明物体就不会被绘制了

为了解决此问题,应用深度测试先画所有的不透明物体,然后关闭深度测试,渲染所有透明物体。确保不透明物体背后的对象不进行渲染仍需进行深度检查

大多数会采用24位或32位的深度缓冲区

怎么解决像素重绘问题? 先绘制深度pass,然后按照所得的深度缓冲结果计算光照pass

利用剔除或者遮挡算法可消除在某些帧完全看不到的对象。类似的算法有二叉树分区算法(BSP),人口算法和遮挡体积等。

(完)