教程

记录

基础

Shader

Shader

纹理环绕方式和过滤

Transform and Z-Buffer

Cube

Cubes

Camera

Phong Model and Material Texture

仔细看有微弱的镜面反射。

Light

用了一组点光源打光。

Blinn-Phong

Blinn-Phong

当视线正好与(现在不需要的)反射向量对齐时,半程向量就会与法线完美契合。

所以当观察者视线越接近于原本反射光线的方向时,镜面高光就会越强。

现在,不论观察者向哪个方向看,半程向量与表面法线之间的夹角都不会超过90度(除非光源在表面以下)。

Gamma 校正

由于人眼中的两倍光强与显示器的有所区别,所以需要矫正。

阴影

阴影映射背后的思路非常简单:以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。

Shadow Mapping

这里的所有蓝线代表光源可以看到的fragment。黑线代表被遮挡的fragment:它们应该渲染为带阴影的。

法线贴图

让每个 fragment 都有自己的法线。

Blinn-Phong

视差贴图

视差贴图 (Parallax Mapping) 技术和法线贴图差不多,但它有着不同的原则。

和法线贴图一样视差贴图能够极大提升表面细节,使之具有深度感。它也是利用了视错觉,然而对深度有着更好的表达,与法线贴图一起用能够产生难以置信的效果。

陡峭视差映射

陡峭视差映射

通过分层(增加采样次数)以提高精度。

HDR

增强明亮区域的亮度细节。

色调映射

色调映射 (Tone Mapping) 是一个损失很小的转换浮点颜色值至我们所需的 LDR[0.0, 1.0] 范围内的过程。

最简单的色调映射算法是 Reinhard 色调映射,它涉及到分散整个 HDR 颜色值到 LDR 颜色值上,所有的值都有对应。

Reinhard色调映射算法平均地将所有亮度值分散到LDR上。

泛光

通过高斯模糊模糊亮色后与原纹理混合。

Bloom

延迟着色法

延迟着色法基于我们延迟大部分计算量非常大的渲染(如光照)到后期进行处理的想法。它包含两个处理阶段:在第一个几何处理阶段中,我们先渲染场景一次,之后获取对象的各种几何信息,并储存在一系列叫做G缓冲的纹理中,如位置向量、颜色向量、法向量和镜面值。

场景中这些储存在G缓冲中的几何信息将会在之后用来做(更复杂的)光照计算。下面是一帧中G缓冲的内容:

G-buffer

我们会在第二个光照处理阶段中使用 G 缓冲内的纹理数据。在光照处理阶段中,我们渲染一个屏幕大小的方形,并使用 G 缓冲中的几何数据对每一个片段计算场景的光照;在每个像素中我们都会对 G 缓冲进行迭代。

对于渲染过程进行解耦,将它高级的片段处理挪到后期进行,而不是直接将每个对象从顶点着色器带到片段着色器。

光照计算过程还是和我们以前一样,但是现在我们需要从对应的 G 缓冲而不是顶点着色器那里获取输入变量了。

延迟着色法

这种渲染方法一个很大的好处就是能保证在 G 缓冲中的片段和在屏幕上呈现的像素所包含的片段信息是一样的,因为深度测试已经最终将这里的片段信息作为最顶层的片段。

这样保证了对于在光照处理阶段中处理的每一个像素都只处理一次,所以我们能够省下很多无用的渲染调用。

结合延迟渲染与正向渲染

需要先把深度信息传给延迟渲染,不然延迟渲染出的东西会覆盖在正向渲染上。

光体积

SSAO

环境光遮蔽可以让一些拐角和褶皱看起来更暗。

SSAO背后的原理很简单:对于铺屏四边形上的每一个片段,我们都会根据周边深度值计算一个遮蔽因子。这个遮蔽因子之后会被用来减少或者抵消片段的环境光照分量。

遮蔽因子是通过采集片段周围球型核心的多个深度样本,并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。

SSAO

上图中在几何体内灰色的深度样本都是高于片段深度值的,他们会增加遮蔽因子;几何体内样本个数越多,片段获得的环境光照也就越少。

也可以通过半向法球体采样。

半向法球体

载入模型

Model

  • 和材质和网格 (Mesh) 一样,所有的场景/模型数据都包含在 Scene 对象中。 Scene 对象也包含了场景根节点的引用。

  • 场景的 Root node (根节点)可能包含子节点(和其它的节点一样),它会有一系列指向场景对象中mMeshes数组中储存的网格数据的索引。 Scene 下的 mMeshes 数组储存了真正的 Mesh 对象,节点中的 mMeshes 数组保存的只是场景中网格数组的索引。

  • 一个 Mesh 对象本身包含了渲染所需要的所有相关数据,像是顶点位置、法向量、纹理坐标、面 (Face) 和物体的材质。

  • 一个网格包含了多个面。Face代表的是物体的渲染图元 (Primitive) (三角形、方形、点)。一个面包含了组成图元的顶点的索引。由于顶点和索引是分开的,使用一个索引缓冲来渲染是非常简单的。

  • 最后,一个网格也包含了一个 Material 对象,它包含了一些函数能让我们获取物体的材质属性,比如说颜色和纹理贴图(比如漫反射和镜面光贴图)。

载入模型时通过递归遍历子节点,获取节点的网格和材质进行渲染。

渲染结果

进阶

深度测试

深度值计算

深度函数图像

可以看到,深度值很大一部分是由很小的z值所决定的,这给了近处的物体很大的深度精度。

模板测试

模板测试可以用来制作高亮的模型边框。

  • 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
  • 渲染物体。
  • 禁用模板写入以及深度测试。
  • 将每个物体缩放一点点。
  • 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
  • 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
  • 再次启用模板写入和深度测试。

混合

混合后的颜色是由该物体颜色与物体背后的颜色混合而成。

混合方程

面剔除

根据观察者的视角去剔除看不到的面以节约性能开销。

卷积处理

因为是 fragment 处理,所以不用像图像处理一样遍历每个像素,只要对当前传入的片段进行处理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const float offset = 1.0 / 300.0;  

void main()
{
vec2 offsets[9] = vec2[](
vec2(-offset, offset), // 左上
vec2( 0.0f, offset), // 正上
vec2( offset, offset), // 右上
vec2(-offset, 0.0f), // 左
vec2( 0.0f, 0.0f), // 中
vec2( offset, 0.0f), // 右
vec2(-offset, -offset), // 左下
vec2( 0.0f, -offset), // 正下
vec2( offset, -offset) // 右下
);

float kernel[9] = float[](
-1, -1, -1,
-1, 9, -1,
-1, -1, -1
);

vec3 sampleTex[9];
for(int i = 0; i < 9; i++)
{
sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
}
vec3 col = vec3(0.0);
for(int i = 0; i < 9; i++)
col += sampleTex[i] * kernel[i];

FragColor = vec4(col, 1.0);
}

立方体贴图

skybox

通过透视除法可以将 skybox 的深度保持为 1.0

环境映射

可以对片段着色器进行修改从而渲染出物体对环境的反射以及折射。

Reflect

实例化

可以帮助减少 CPUGPU 间的通信次数。

多重采样

无论三角形遮盖了多少个子采样点,(每个图元中)每个像素只运行一次片段着色器。片段着色器所使用的顶点数据会插值到每个像素的中心,所得到的结果颜色会被储存在每个被遮盖住的子采样点中。当颜色缓冲的子样本被图元的所有颜色填满时,所有的这些颜色将会在每个像素内部平均化。

MSAA

PBR

理论

基于微平面的表面模型

一个平面越是粗糙,这个平面上的微平面的排列就越混乱。这些微小镜面这样无序取向排列的影响就是,当我们特指镜面光/镜面反射时,入射光线更趋向于向完全不同的方向发散开来,进而产生出分布范围更广泛的镜面反射。而与之相反的是,对于一个光滑的平面,光线大体上会更趋向于向同一个方向反射,造成更小更锐利的反射。

然而由于这些微平面已经微小到无法逐像素的继续对其进行区分,因此我们只有假设一个粗糙度参数,然后用统计学的方法来概略的估算微平面的粗糙程度。我们可以基于一个平面的粗糙度来计算出某个向量的方向与微平面平均取向方向一致的概率。这个向量便是位于光线向量l和视线向量v之间的中间向量。

能量守恒

反射光与折射光它们二者之间是互斥的关系。无论何种光线,其被材质表面所反射的能量将无法再被材质吸收。因此,诸如折射光这样的余下的进入表面之中的能量正好就是我们计算完反射之后余下的能量。

应用基于物理的BRDF

跟电磁波场模型比较像。

实战

代码结构

UML

碰撞

碰撞检测

AABB - AABB 碰撞

矩形碰撞检测,只要判断是否在 x 轴和 y 轴方向均重叠。

AABB - AABB 碰撞

AABB - 圆碰撞检测

需要求出矩形上离圆最近的点再判断。

AABB - AABB 碰撞

碰撞处理

可以通过 std::make_tuple 返回碰撞信息包,再在游戏函数里解析包。

粒子

可以设计粒子的纹理,生命值等参数,随着生命值减小越来越暗。