Learn OpenGL:高级OpenGL

深度测试

深度缓冲Depth Buffer

由窗口系统自动创建,和颜色缓冲有着一样的宽度和高度。深度值以16、24或32位float的形式储存(大部分都是24位的),范围[0.0~1.0]

执行过程

深度测试是在片段着色器运行之后(以及模板测试(Stencil Testing)运行之后)在屏幕空间中运行。在片段着色器中gl_FragCoord的x和y保存了片段的屏幕空间坐标,z是深度值

深度测试就是将片段的深度值与深度缓冲的内容进行对比(执行一个测试函数),如果测试通过,深度缓冲将会更新为新的深度值,否则片段被丢弃

深度测试函数

可以使用glDepthFunc设置测试函数,默认是GL_LESS,深度值更小时通过测试。还可以设置其它的比较运算符,甚至还有GL_ALWAYSGL_NEVER

精度

经过模型变换和观测变换得到摄像机视角下的观测坐标\(pos_{view}\),然后经过透视投影、透视除法、视口变换得到屏幕空间坐标\(pos_{screen}\),坐标的z就是深度值 \[ F_{depth} = \frac{1/z-1/near}{1/far-1/near} \] 因为是反比例函数,所以当z比较大时,深度变化幅度小;当z比较小时,深度变换比较快

如果采用的是正交投影,则最后得到的深度值与z之间是线性关系

防止深度冲突

因为精度问题,当两个面非常接近时,深度缓冲无法决定哪个在前面。结果就是在两者之间不断切换前后顺序,从而导致很奇怪的花纹。因为采用了非线性的变换,在远处的深度精度比较差,更容易发生深度冲突。

解决办法

  1. 不要把物体放的太近
  2. 把摄像机的近平面near设置得远一些,从而在稍微远点的距离也能获得高精度
  3. 使用更高精度的深度缓冲,把24位换成32位

模板测试

模板缓冲Stencil Buffer

模板缓冲也是由 GLFW自动配置。模板值一般是8位的,一共可以取256种数值

执行过程

在片段着色器处理完一个片段之后开始执行,执行模板测试函数并根据设置的操作更新缓冲。通过模板测试的片段会进入之后的深度测试

模板函数

模板测试也可以像深度测试一样通过glStencilFunc指定测试函数,不同的是可以在参数里提供一个参考值ref,而且还可以通过glStencilOp设置测试成功和失败时如何更新模板值,比深度测试更灵活。

实现物体描边

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // 模板测试和深度测试都通过时,将模板值设置为参考值

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

glStencilMask(0x00); // 关闭写入
/* 绘制不需要描边的物体 */

glStencilFunc(GL_ALWAYS, 1, 0xFF); // 全部通过测试,参考值为1
glStencilMask(0xFF); // 启用写入
/* 绘制要描边的物体 */ // 对应位置的模板值会更新为参考值1

glStencilFunc(GL_NOTEQUAL, 1, 0xFF); // 只通过模板值不是1的
glStencilMask(0x00); // 关闭写入
//glDisable(GL_DEPTH_TEST);
/* 用边缘颜色绘制放大一点的物体 */ // 只有放大的边缘部分模板值不是1,会被绘制
//glEnable(GL_DEPTH_TEST);
glStencilMask(0xFF); // 恢复写入,不然glClear都清除不了
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 恢复通过,不然后面绘制不出来

实现透视

在绘制放大一点的物体之前关闭深度测试,绘制之后开启,也就是上面代码注释掉的部分

混合

添加植被

植被的贴图中透明度要么是0(完全透明)要么是1(完全不透明)。要添加植被到场景中,只需要在片段着色器里将alpha小于阈值的片段丢弃

1
2
3
4
vec4 texColor = texture(texture1, TexCoords);
if(texColor.a < 0.1)
discard; // 丢弃此片段
FragColor = texColor;

对于有透明度的贴图,如果设置为GL_REPEAT,边缘部分会与另一头进行混合,如果边缘是透明的,而另一头不透明,结果就会产生一个边框。所以需要设置为GL_CLAMP_TOEDGE

混合方程

窗户的玻璃是半透明的,需要开启混合,按照混合方程对窗户玻璃的颜色\(Color\_{source}\)和后面物体的颜色\(Color\_{destination}\)进行加权求和 \[ Color_{result} = Color_{source}\times Frac_{source} + Color_{destination}\times Frac_{destination} \] 具体的混合系数可以通过glBlendFunc设置

通常用的glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)就是指\(Frac_{source} = Alpha_{source}\)\(Frac_{destination} = 1-Alpha_{source}\),按照要写入的颜色的alpha值进行混合,如果是alpha=0完全透明,就直接使用原来的颜色;如果是alpha=1完全不透明,就直接使用新的颜色。

glBlendFunc(GL_ONE, GL_ZERO)则表示直接覆盖原有颜色

绘制顺序

应该按照“不透明物体-比较远的透明物体-比较近的透明物体”的顺序绘制。

  1. 按照混合方程,混合用的alpha是透明物体的,需要不透明物体先被绘制出来才能混合,否则后绘制不透明物体会直接覆盖透明物体的颜色,不管哪个在前哪个在后。

  2. 如果同样是透明物体,如果先绘制了近距离的,那么在绘制远距离的时候会因为深度测试失败而导致后面的根本绘制不出来,也就不会混合。

所以透明物体需要按照与摄像机的距离进行排序,为了方便动态添加删除,采用红黑树map,以距离为键,物体索引为值。

如果存在旋转、形变等变换的话,还是会露馅。

面剔除

在顶点着色器运行之后

默认逆时针顺序(从观察者的角度来看)的顶点组成的面是正向的,默认剔除背面。可以用glCullFace设置要剔除的面,glFrontFace(GL_CW)设置顺时针为正面(默认是GL_CCW

对于非封闭的物体,不能开启面剔除,比如双面的植被,开启之后就只能看到一面

帧缓冲

包括颜色缓冲、深度缓冲和模板缓冲。对应有帧缓冲对象,可以添加纹理附件(data参数传递空指针只分配内存而没有数据)作为颜色缓冲,将渲染结果写入到纹理中;可以添加渲染缓冲对象附件(只写),适合保存深度值和模板值(共32位)

渲染到纹理上

先创建帧缓冲对象,生成并附加纹理,在渲染循环中将屏幕渲染到纹理上,然后恢复默认的缓冲,把纹理绘制到屏幕上(使用的一个屏幕四边形顶点数据,shader里不需要进行坐标变换,直接采样纹理并输出),看起来和之前效果一样,但是开启线框模式会发现屏幕上只绘制了一个矩形。

OpenGL代码

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 创建帧缓冲对象并绑定
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);

// 生成纹理
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);
glBindTexture(GL_TEXTURE_2D, texColorBuffer);
// 大小为屏幕的宽高,data为nullptr则只开辟内存而不填充数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SCREEN_WIDTH, SCREEN_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 附加到当前绑定的帧缓冲对象:可读可写,作为颜色缓冲,最后一个参数指MipMap的级别
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);

// 生成渲染缓冲对象
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCREEN_WIDTH, SCREEN_HEIGHT);// 分配内存
// 附加到当前绑定的帧缓冲对象,作为深度和模板缓冲
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

// 检查帧缓冲完整性
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE){
std::cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << std::endl;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 随时解绑,防止错误

// 创建屏幕纹理(一个屏幕空间四边形)
constexpr float vertices[] = {
// position // texcoord
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,

1.0f, 1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
};
unsigned int screenVAO, screenVBO; // 因为用的数据格式里坐标只有xy,Mesh类用不了
glGenVertexArrays(1, &screenVAO);
glGenBuffers(1, &screenVBO);
glBindVertexArray(screenVAO);
glBindBuffer(GL_ARRAY_BUFFER, screenVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0); // aPos
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), nullptr);
glEnableVertexAttribArray(1); // aTexCoords
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));

Shader screenShader("res/shaders/framebuffer.vert", "res/shaders/framebuffer.frag");
screenShader.Bind();
screenShader.SetUniform1i("screenTexture", 0);

//渲染循环中
{
// 第一阶段,渲染到上面创建的帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // 清除前一帧的缓存信息
glEnable(GL_DEPTH_TEST);
/* 正常绘制 */

// 第二阶段,渲染到默认的帧缓冲,绘制四边形
glBindFramebuffer(GL_FRAMEBUFFER, 0);
//glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // GL_FRONT看不到对角线
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); // 清除原有的颜色缓冲

screenShader.Bind();
glBindVertexArray(screenVAO); // 绑定四边形纹理
glBindTexture(GL_TEXTURE_2D, texColorBuffer); // 将颜色缓冲贴图作为四边形的贴图
glDisable(GL_DEPTH_TEST); // 关闭深度测试,不然看不到对角线
// 绘制矩形(两个三角形)
glDrawArrays(GL_TRIANGLES, 0, 6);
}

framebuffer.vert

1
2
3
4
5
6
7
8
9
10
#version 330

layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;
void main(){
gl_Position = vec4(aPos.x, aPos.y, 0, 1); // 直接传递标准化设备坐标
TexCoords = aTexCoords;
}

framebuffer.frag

1
2
3
4
5
6
7
8
9
10
11
#version 330

in vec2 TexCoords;

uniform sampler2D screenTexture; // 通过帧缓冲得到的屏幕纹理

out vec4 FragColor;
void main(){
// 直接输出
FragColor = texture(screenTexture, TexCoords);
}

后期处理

将屏幕绘制到纹理上之后,就可以动用各种图像处理技术来处理这个纹理,然后再绘制到屏幕上,从而实现后处理效果。

片段着色器

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#version 330

in vec2 TexCoords;

uniform sampler2D screenTexture; // 通过帧缓冲得到的屏幕纹理

out vec4 FragColor;
void main(){
// 直接输出
// FragColor = texture(screenTexture, TexCoords);

// rgb通道反相
// FragColor = vec4(vec3(1 - texture(screenTexture, TexCoords)), 1);

// 灰度
// FragColor = texture(screenTexture, TexCoords);
// float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
// float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b; 人眼对蓝色不敏感,降低蓝色比重
// FragColor = vec4(average, average, average, 1); 灰度为rgb的均值

// 锐化、模糊、边缘检测
// 周围采样像素点
const float offset = 1.0 / 300;
vec2 offsets[] = {
vec2(-offset, offset), // 左上
vec2(0, offset), // 正上
vec2(offset, offset), // 右上
vec2(-offset, 0), // 左
vec2(0, 0), // 中
vec2(offset, 0), // 右
vec2(-offset, -offset), // 左下
vec2(0, -offset), // 正下
vec2(offset, -offset) // 右下
};
// 三种卷积核(卷积核就是求和权重)
// 锐化卷积核(对周围像素取负值,从而增强像素点的颜色,边缘更明显)
// float kernel[] = {
// -1, -1, -1,
// -1, 9, -1,
// -1, -1, -1
// };
// 模糊卷积核(对周围进行加权平均)
// float kernel[] = {
// 1.0 / 16, 2.0 / 16, 1.0 / 16,
// 2.0 / 16, 4.0 / 16, 2.0 / 16,
// 1.0 / 16, 2.0 / 16, 1.0 / 16
// };
// 边缘检测核(对自身取大负值,把像素点变暗,最后只留下边缘是亮的)
float kernel[] = {
1, 1, 1,
1, -9, 1,
1, 1, 1
};
// 对周围采样rgb,(如果是GL_REPEAT,边缘采样的是另一头的像素,最好用GL_CLAMP_TO_EDGE)
vec3 sampleTex[9];
for(int i = 0; i < 9; i++){
sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
}
// rgb加权求和
vec3 col = vec3(0);
for(int i = 0; i < 9; i++){
col += sampleTex[i] * kernel[i];
}
FragColor = vec4(col, 1);
}

MRT

多渲染目标,给一个帧缓冲对象同时绑定多个颜色附件,在glFramebufferTexture2D的第二个参数指定对应的附加槽位,如GL_COLOR_ATTACHMENT1。然后用glDrawBuffers指定这些槽位的数组顺序,之后在片段着色器中输出对应的值到对应槽位

1
layout (location = 1) out vec4 outColor;

平时输出的FragColor就是默认帧缓冲的0号槽位的颜色附件。

这里需要注意的是,像FragColor一样,声明的输出变量都要是vec4(不管颜色附件的实际格式如何),否则之后在用texture采样出来的结果会不对,比如声明为vec3,那么之后的第四个分量会默认为0!,如果之后是采样出来作为颜色,那就看不到啦!

立方体贴图

包含了立方体的六个面,只需要从立方体的中心取一个方向向量即可进行采样。

天空盒

先定义一个立方体,在shader中定义一个samplerCube,采样使用的uv坐标是这个立方体上的顶点坐标(由顶点着色器直接传递过来)

为了天空盒永远在物体后面,也就是让天空盒深度值为1,将z设为w,从而执行透视除法之后得到的深度z/w永远为1,并设置GL_LEQUAL通过测试

为了防止天空盒移动,需要去掉观察矩阵的位移量

1
glm::mat4 skyboxViewMat = glm::mat4(glm::mat3(viewMat));

注意天空盒仍需要在透明物体之前绘制

顶点着色器

1
2
3
4
5
6
7
8
9
10
11
#version 330
layout(location = 0) in vec3 aPos;

uniform mat4 projMat;
uniform mat4 viewMat;

out vec3 TexCoords;
void main(){
TexCoords = aPos; // 将立方体的顶点数据作为立方体贴图的采样点
gl_Position = (projMat * viewMat * vec4(aPos, 1)).xyww; // 令z=w,从而深度值为1
}

片段着色器

1
2
3
4
5
6
7
8
9
10
11
#version 330 core

in vec3 TexCoords;

uniform samplerCube skybox;

out vec4 FragColor;

void main(){
FragColor = texture(skybox, TexCoords);
}

反射天空盒

在片段着色器里,用观察方向和法向(逆转置计算过的)求出反射方向,并用来对天空盒采样,作为输出的颜色

顶点着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

uniform mat4 modelMat;
uniform mat4 viewMat;
uniform mat4 projMat;

out vec3 Normal;
out vec3 Position;

void main(){
Normal = mat3(transpose(inverse(modelMat))) * aNormal; // 用逆转置矩阵计算法向量
Position = vec3(modelMat * vec4(aPos, 1)); // 世界坐标
gl_Position = projMat * viewMat * modelMat * vec4(aPos, 1);
}

片段着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 330 core

in vec3 Normal;
in vec3 Position;

uniform vec3 cameraPos;
uniform samplerCube skybox;

out vec4 FragColor;
void main(){
vec3 I = normalize(Position - cameraPos);
vec3 R = reflect(I, normalize(Normal)); // 反射方向
FragColor = vec4(texture(skybox, R).rgb, 1); // 用反射向量对天空盒采样
}

折射天空盒

用观察方向和法向求出折射方向(需要折射率之比,比如玻璃材质需要空气1.0/玻璃1.52),然后对天空盒采样

片段着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#version 330 core

in vec3 Normal;
in vec3 Position;

uniform vec3 cameraPos;
uniform samplerCube skybox;

out vec4 FragColor;
void main(){
float ratio = 1.0 / 1.52; // 空气与玻璃的折射率之比
vec3 I = normalize(Position - cameraPos);
vec3 R = refract(I, normalize(Normal), ratio); // 折射
FragColor = vec4(texture(skybox, R).rgb, 1); // 用折射向量对天空盒采样
}

高级数据

  • glBufferData填充缓冲的内存,如果数据参数传空指针则只分配内存

  • glBufferSubData只填充部分缓冲区

  • glMapBuffer直接获取缓冲区指针,用指针赋值数据,之后glUnMapBuffer令指针无效

  • glVertexAttribPointer结合glBufferSubData可以实现分批顶点属性(先先存放所有顶点的位置,然后是所有顶点的uv等),处理起来简单、看起来更整洁

  • glCopyBufferSubData复制缓冲

高级GLSL

着色器的所有内建变量

内建变量在使用时并不需要声明,下面的gl_Position: out vec4表示:gl_Position这个内建变量是浮点型输出变量,输出变量需要赋值,输入变量是只读的

顶点着色器变量

  • gl_Position: out vec4:设置剪裁空间位置

  • gl_PointSize: out float:设置图元GL_POINTS的大小,需要glEnable(GL_PROGRAM_POINT_SIZE)开启。也可以用glPointSize函数来设置。可以用于实现粒子效果

  • gl_VertexID: in int:储存了正在绘制顶点的当前ID。如果绘制调用的是glDrawElements,则ID是当前顶点索引;如果是glDrawArrays,则ID是已处理顶点数量

片段着色器变量

  • gl_FragCoord: in vec3:x和y分量是片段的屏幕空间坐标,z分量等于对应片段的深度值(屏幕空间)。可以对屏幕的不同部分进行不同处理
  • gl_FrontFacing: in bool:当前片段是否为正向面的一部分。可以用于对模型内外表面进行不同处理(要关闭剔除)
  • gl_FragCoord: out float:设置片段的深度值[0.0, 1.0]。没有设置的话会等于gl_FragCoord.z,设置的话OpenGL会禁用提前深度测试,除非采用OpenGL 4.2以上才支持的深度条件做重新声明,比如layout (depth_greater) out float gl_FragDepth;声明只会把深度增大

接口块

类似于struct,把几个变量打包进行输入输出,但是这里省掉了struct关键字,而且只需要类型相同即可传递数据,而不需要实例名相同

1
2
3
4
5
6
7
8
9
10
// 顶点着色器,设置vs_out.TexCoords
out VS_OUT{
vec2 TexCoord;
}vs_out;

// 片段着色器,读取fs_in.TexCoords
in VS_OUT // 类型名一样
{
vec2 TexCoords;
} fs_in; // 实例名可以不同

Uniform块

定义相同的Uniform块的着色器可以共享数据

1
2
3
4
5
6
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
// 不需要实例,可以直接访问其中变量

像C++的结构体有内存对齐一样,Uniform也有内存布局。Uniform块保存在缓冲对象上,默认采用共享布局来节省空间(程序中保持一致,但不一定是变量定义的顺序)

而上面采用的是std140布局,对应有一个偏移量计算规则,在不同的shader里的布局相同,从而可以统一设置数值,这也就是接下来要用的Uniform缓冲

Uniform缓冲

使用Uniform块之后可以在不同shader中共享数据,而不用每次都要单独设置,尤其是经常用到的viewMatprojMat

首先把顶点着色器中的

1
2
uniform mat4 viewMat;
uniform mat4 projMat;

改成

1
2
3
4
layout (std140) uniform Matrices{
mat4 viewMat;
mat4 projMat;
};

在C++程序里创建Uniform缓冲对象并和Uniform块链接到相同的绑定点

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 获取Uniform块Matrices的位置索引,所有shader都要进行一遍(新版本不需要,直接指定链接点)
unsigned int uniformBlockIndexShader = glGetUniformBlockIndex(shader.GetID(), "Matrices");
unsigned int uniformBlockIndexBorderShader = glGetUniformBlockIndex(borderShader.GetID(), "Matrices");
unsigned int uniformBlockIndexGrassShader = glGetUniformBlockIndex(grassShader.GetID(), "Matrices");
//unsigned int uniformBlockIndexSkyboxShader = glGetUniformBlockIndex(skyboxShader.GetID(), "Matrices");
unsigned int uniformBlockIndexSkyboxReflectionShader = glGetUniformBlockIndex(skyboxReflectionShader.GetID(), "Matrices");
unsigned int uniformBlockIndexSkyboxRefractionShader = glGetUniformBlockIndex(skyboxRefractionShader.GetID(), "Matrices");
// 将Uniform块Matrices链接到绑定点0
glUniformBlockBinding(shader.GetID(), uniformBlockIndexShader, 0);
glUniformBlockBinding(borderShader.GetID(), uniformBlockIndexBorderShader, 0);
glUniformBlockBinding(grassShader.GetID(), uniformBlockIndexGrassShader, 0);
//glUniformBlockBinding(skyboxShader.GetID(), uniformBlockIndexSkyboxShader, 0);
glUniformBlockBinding(skyboxReflectionShader.GetID(), uniformBlockIndexSkyboxReflectionShader, 0);
glUniformBlockBinding(skyboxRefractionShader.GetID(), uniformBlockIndexSkyboxRefractionShader, 0);

// 创建Uniform缓冲对象ubo
unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), nullptr, GL_STATIC_DRAW); // 分配内存
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// 将ubo链接到相同的绑定点上
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
//glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); // 这个是直接绑定,而上面那个指定了偏移和大小,可以实现只绑定缓冲的一部分

上面把天空盒的shader注释掉了,因为天空盒的观察矩阵需要去掉位移部分,不应该跟其它shader共享。

然后在渲染循环中去掉原先单独为每个shader设置两个矩阵的操作,改为

1
2
3
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(viewMat));
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(projMat));

这样每个shader就都能拿到这两个矩阵的数据了

几何着色器

顶点和片段着色器之间运行。输入一个图元(如点或三角形)的所有顶点,可以随意变换,然后输出不同图元或更多的点

跟另外两种着色器一样需要创建(类型为GL_GEOMETRY_SHADER)、编译、附加、链接,可以在着色器之间传递数据

示例

以每个输入点为中心创建一条水平线

1
2
3
4
5
6
7
8
9
10
11
12
13
#version 330 core
layout (points) in; // 声明输入的图元类型(这里对应GL_POINTS,就是一个点)
layout (line_strip, max_vertices = 2) out; // 声明输出的图元类型(这里是线条),最大能够输出的顶点数量

void main() {
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
EmitVertex(); // 把gl_Position添加到图元中

gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
EmitVertex();

EndPrimitive(); // 合成顶点为指定的输出渲染图元
}

内建变量gl_in是一个数组,是一个图元的所有顶点

可能的定义形式

1
2
3
4
5
6
in gl_Vertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];

如果需要其它顶点数据,可以使用接口块从顶点着色器传过来,但是要声明为数组(加个[]),如果是传递给片段着色器的变量,比如顶点颜色,在调用EmitVertex之前,这个变量最后的值就是这个顶点的颜色,从而可以为每个点指定颜色。

实现爆破

在几何着色器中,由输入的三角形的三个顶点计算出法向量(同平面内的两个向量叉积),然后把三个点都沿着法向量位移sin(time)+1,从而可以把模型上的每个三角形都分离出来。

因为这个操作实际上是作用在模型本体的,应该算作是模型变换,所以在顶点着色器里不使用MVP矩阵,而是直接把顶点数据传递给几何着色器,在几何着色器中计算完位移的顶点坐标后再应用MVP矩阵计算出gl_Position、用M矩阵计算出FragPos传递给片段着色器。

法向量可视化

先正常绘制,然后再由几何着色器绘制一遍。在几何着色器里对三角形的每个顶点都按照顶点法向位移出另一个点,同样因为位移操作视为模型变换,所以MVP放到几何着色器里做

顶点着色器直接传递法向量

1
2
gl_Position = vec4(aPos, 1.0); 
vs_out.normal = aNormal;

几何着色器在每个点绘制法向线段(位移出一个点)

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const float MAGNITUDE = 0.4;

void GenerateLine(mat3 normalMatrix,int index)
{
gl_Position = projMat * viewMat * modelMat * gl_in[index].gl_Position;
EmitVertex();

vec4 ptOnNormal = viewMat * modelMat * gl_in[index].gl_Position
+ vec4( normalize(normalMatrix * gs_in[index].normal) * MAGNITUDE, 0);
gl_Position = projMat * ptOnNormal; // 如果是投影之后位移,结果不正确
EmitVertex();
EndPrimitive();
}

void main()
{
mat3 nMat = mat3(transpose(inverse(viewMat * modelMat))); // 用来求法线
GenerateLine(nMat,0); // 第一个顶点法线
GenerateLine(nMat,1); // 第二个顶点法线
GenerateLine(nMat,2); // 第三个顶点法线
}

实例化

绘制调用的开销

glDrawArraysglDrawElements函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)

实例化绘制

改用glDrawArraysInstancedglDrawElementsInstanced,告诉GPU要绘制的物体实例数量,每个实例有对应的gl_InstanceID,从0开始。于是可以向顶点着色器传递数组,然后使用gl_InstanceID进行索引。

但是Uniform数据大小有限制,需要改用实例化数组,其实就是作为顶点属性绑定上去,所以基本和普通的顶点属性一样需要创建缓冲对象并绑定属性指针。不同的是最后需要调用glVertexAttribDivisor(属性位置号,属性除数)。默认情况属性除数是0,在顶点着色器的每次迭代时更新顶点属性;设置为1时,在渲染一个新实例的时候更新顶点属性,也就是说对应位置的属性是一个实例化数组。在着色器中还是可以使用gl_InstanceID取得实例索引。

小行星带

在顶点着色器中把小行星的modelMat定义为顶点属性,比如layout(location=3) in mat4 instanceMatrix,但是顶点属性最大为vec4,所以在外部需要把mat4分成四列分别绑定到位置3、4、5、6(都要设置glVertexAttribDivisor)。因为内存连续分布,矩阵本来也是按列存放,所以并不影响着色器里使用矩阵。

实例化渲染适用于渲染草、植被、粒子等大量重复的形状。

抗锯齿

光栅器

运行在顶点着色器之后、片段着色器之前,输入一个图元的所有顶点,输出一系列片段。片段坐标就是屏幕像素点坐标,需要用像素点中心作为采样点来判断与三角形的内外关系。之后得到像素点组成的三角形,由于像素点本身的大小,锯齿很明显。

多重采样

原本在像素里只取中心的一个采样点改为使用多个采样点,用这些子采样点来决定像素的遮盖度(会增大颜色缓冲)。之后如果直接对几个采样点分别运行片段着色器,最后给像素点取平均值,那就会多次运行片段着色器。但是实际上一个像素点只运行一次片段着色器,每个采样点都有颜色、深度和模板值。

glfwWindowHint(GLFW_SAMPLES, 4)提示GLFW使用多重采样缓冲,glEnable(GL_MULTISAMPLE)启用多重采样(默认是启用的)。

如果要做离屏渲染,跟之前在帧缓冲中做的差不多,可以手动创建多重采样缓冲对象、附加多重采样纹理并渲染到多重采样缓冲。不一样的是一个多重采样的图像包含比普通图像更多的信息,需要缩小或者还原(Resolve)图像。如果要做后期处理,需要将多重采样缓冲位块传送到一个没有使用多重采样纹理附件的FBO中。然后用这个普通的颜色附件来做后期处理。