LearnOpenGL学习笔记(九) - 面剔除、帧缓冲、CubeMap与高级数据

本文最后更新于 2024年8月6日 下午

面剔除

每个封闭形状的面都有正反之分。在OpenGL中,通过三角形片段三个顶点的绘制顺序判断该三角形所在面的正反。默认情况下,逆时针顶点所定义的三角形为正向三角形。

背向观察者的面通常不会被渲染。如果能够取消这些面的渲染,程序速度将会提高约50%。

glEnable(GL_CULL_FACE)用于开启面剔除。

glCullFace(GLEnum mode)用于指定剔除的面。

  • GL_FRONT:剔除正面
  • GL_BACK:剔除背面
  • GL_FRONT_AND_BACK:正反面都剔除

glFrontFace(GLEnum mode)用于指定正向三角形的定义。

  • GL_CCW:逆时针三角形为正
  • GL_CW:顺时针三角形为正。

使用面缓冲时,必须确保顶点数据的定义是”逆时针为正“的顺序。否则会出现渲染错误。

帧缓冲

帧缓冲(Framebuffer)是所有屏幕缓冲(包括颜色缓冲、深度缓冲、模板缓冲)的集合。在默认情况下,我们的绘制操作都在默认帧缓冲的渲染缓冲上进行,默认帧缓冲由GLFW创建。通过创建帧缓冲,可以获得额外的Render Target。

使用glGenFramebuffers(int count, unsigned int *FBO)生成帧缓冲对象(Framebuffer Object)。使用glBindFramebuffer(GL_FRAMEBUFFER, unsigned int FBO)绑定FBO对象。

完成绑定后,所有读取、写入缓冲的操作都会影响当前绑定的帧缓冲。

可以通过绑定到GL_READ_FRAMEBUFFER或GL_DRAW_FRAMEBUFFER绑定到只读/只写的目标上。

只进行生成、绑定操作的帧缓冲是不完整的。我们可以通过布尔表达式glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE检查帧缓冲是否完整。

视口中的视觉输出对应的帧缓冲永远是默认帧缓冲(0号)。因此,当完成非默认帧缓冲的渲染(离屏渲染,Off-screen Rendering)时,一定要记得重新绑定默认帧缓冲(glBindFramebuffers(GL_FRAMEBUFFER, 0)),并且把不需要的帧缓冲对象删除(glDeleteFramebuffers(1,&FBO))。

为了使帧缓冲完整,我们需要给它绑定颜色附件(Attachment)。

附件是一个内存位置,它能作为帧缓冲的一个缓冲,类似于一个图像。附件分为纹理附件(Texture Attachment)和渲染缓冲对象附件(Renderbuffer Object)。

也可以这么理解:

纹理附件

当帧缓冲被附加上纹理附件时,对这个帧缓冲执行的所有指令都会被渲染到这个纹理上(类似于RenderTexture)。

使用类似创建纹理的方式创建纹理附件,随后将其绑定到帧缓冲。

1
2
3
4
5
6
7
8
9
10
11
12
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
//无图片数据,data参数为NULL
//若附加深度缓冲纹理,Format和Internalformat参数应当为GL_DEPTH_COMPONENT
//若附加模板缓冲纹理,则为GL_STENCIL_INDEX
//若同时附加深度和模板缓冲纹理,Format为GL_DEPTH24_STENCIL8,Internalformat为GL_DEPTH_STENCIL,Type为GL_UNSIGNED_INT_24_8
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
//纹理附件的大小总是为屏幕大小,所以不关心环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

其中,glFramebufferTexture2D用于把纹理对象附加到帧缓冲上。具体参数有:

  • target:帧缓冲的目标(绘制、读取或者两者皆有)
  • attachment:我们想要附加的附件类型。除了颜色缓冲外,还有GL_DEPTH_ATTACHMENTGL_STENCIL_ATTACHMENTGL_DEPTH_STENCIL_ATTACHMENT
  • textarget:附加的纹理类型
  • texture:附加的纹理本身
  • level:多级渐远纹理的级别。

渲染缓冲对象附件

渲染缓冲对象(Renderbuffer Object,RBO)是真正的缓冲(相对于纹理等通用数据缓冲, General Purpose Data Buffer),它相比于纹理缓冲具有更快的读取速度。

渲染缓冲对象是只写的,但可以通过glReadPixels函数来读取当前绑定的帧缓冲中的特定像素。

如何理解上面这句话?

视口上呈现的视觉输出始终对应于默认帧缓冲,但无论是纹理附件还是渲染缓冲对象附件,改变的都是我们自己生成的帧缓冲。纹理附件可以以纹理的形式呈现在默认帧缓冲输出中,但渲染缓冲对象附件却不行,因为它不可读。但是,我们依然可以使用glReadPixels函数来访问被其改变后的帧缓冲的像素。

通过glGenRenderbuffers生成渲染缓冲对象,通过glBindRenderbuffer(GL_RENDERBUFFER, unsigned int RBO)绑定渲染缓冲对象。

当我们不需要从这些缓冲中采样的时候,通常都会选择渲染缓冲对象,因为它会更优化一点。

所谓“采样“指的是从纹理中读取像素数据,用于着色、纹理映射等操作。而测试所需要的数值比较并非采样,因此可以认为测试无需读取,可以使用RBO。

使用glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, SCREEN_WIDTH, SCREEN_HEIGHT)创建深度和模板RBO,随后使用glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, unsigned int RBO)附加渲染缓冲对象。

通常的规则是,如果你不需要从一个缓冲中采样数据,那么对这个缓冲使用渲染缓冲对象会是明智的选择。如果你需要从缓冲中采样颜色或深度值等数据,那么你应该选择纹理附件。

流程:创建帧缓冲->绑定帧缓冲->创建附件->配置附件->绑定附件->进行渲染操作->删除附件->解绑帧缓冲

渲染到纹理

  1. 首先,创建、绑定帧缓冲对象和纹理对象,并将纹理附加到帧缓冲
1
2
3
4
5
6
glGenTextures(1,&texture);
glBindTexture(GL_TEXTURE_2D,texture);
glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,SCR_WIDTH,SCR_HEIGHT,0,GL_RGB,GL_UNSIGNED_BYTE,NULL);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texture,0);
  1. 然后,创建、绑定渲染缓冲对象,并附加到帧缓冲,注意帧缓冲对象的解绑
1
2
3
4
5
6
7
8
9
10
unsigned int RBO;
glGenRenderbuffers(1,&RBO);
glBindRenderbuffer(GL_RENDERBUFFER,RBO);
glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH24_STENCIL8,SCR_WIDTH,SCR_HEIGHT);
glBindRenderbuffer(GL_RENDERBUFFER,0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_STENCIL_ATTACHMENT,GL_RENDERBUFFER, RBO);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER)==GL_FRAMEBUFFER_COMPLETE) {
cout<<"Framebuffer complete!"<<endl;
}
glBindFramebuffer(GL_FRAMEBUFFER,0);

上面两步是渲染循环之前,让帧缓冲对象变完整的过程。

  1. 接着,绑定刚才创建的帧缓冲对象,然后绘制场景。注意绘制完要解绑,回到默认帧缓冲。
1
2
3
4
5
6
7
glBindFramebuffer(GL_FRAMEBUFFER,framebuffer);
glClearColor(skyboxColor.x,skyboxColor.y,skyboxColor.z,1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
DrawScene();
glBindFramebuffer(GL_FRAMEBUFFER,0);
glDisable(GL_DEPTH_TEST); //绘制面片无需启用深度测试
  1. 然后,在默认帧缓冲绘制面片。
1
2
3
4
5
6
glClearColor(skyboxColor.x,skyboxColor.y,skyboxColor.z,1.0f);
glClear(GL_COLOR_BUFFER_BIT);
quadShader.use();
glBindVertexArray(quadVAO);
glBindTexture(GL_TEXTURE_2D,texture);
glDrawArrays(GL_TRIANGLES,0,6);

后处理

简单的后处理

  • 反向:One Minus 采样
  • 灰度:采样值的r、g、b相加除以3,作为新的r、g、b

核处理

使用卷积核对图像进行卷积。

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
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, 15, -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);
}

卷积核不同,处理的效果也不同。

  • 锐化:2 2 2 2 -15 2 2 2 2
  • 模糊:(1 2 1 2 4 2 1 2 1)/16
  • 边缘检测:1 1 1 1 -8 1 1 1 1

在对屏幕边缘的像素进行采样时,超出边缘的像素会按环绕方式进行采样。为了避免错误,需要设置为GL_CLAMP_TO_EDGE

立方体贴图

立方体贴图(Cube Map)是包含了六个2D纹理的纹理,通过三维方向向量(立方体中心为原点)进行采样。

img

创建、绑定立方体贴图的方法与2D纹理类似,只是目标GL_TEXTURE_2D要更改为GL_TEXTURE_CUBE_MAP

1
2
3
unsigned int textureID;
glGenTextures(1,&textureID);
glBindTexture(GL_TEXTURE_CUBE_MAP,textureID);

OpenGL并没有专门为Cube Map提供输入数据的函数。但Cube Map本质上是六个2D纹理组成的纹理,所以可以通过调用六次glTexImage2D的方式输入数据。

Cube Map的每个面都有单独的Target,前缀都为GL_TEXTURE_CUBE_MAP,后缀依次为:POSITIVE_XNEGATIVE_XPOSITIVE_YNEGATIVE_YPOSITIVE_ZNEGATIVE_Z,分别对应右、左、上、下、后、前。它们作为unsigned int,依次递增1,因此可以用循环赋值。

1
2
3
4
5
6
7
8
9
10
int width, height, nrChannels;
unsigned char *data;
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
glTexImage2D(
GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
);
}

完成纹理数据输入后,同样需要设置纹理的过滤和环绕方式。与2D纹理不同的是,Cube Map在环绕方式上除了S、T还有R维度。它类似于三维空间中的Z轴,当方向矢量未击中任何面(如接缝处)时,返回边界值。

1
2
3
4
5
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

在GLSL中,samplerCube用于定义一个Cube Map采样器。它使用一个vec3(方向矢量)而非vec2作为采样坐标。

FragColor = texture(cubemap, textureDir)

通过将天空盒顶点着色器的gl_Position设置为原本的xyww,即可让顶点z轴,即深度值,始终等于一。并且要把glDepthFunc修改为GL_LEQUAL,让深度值等于1的天空盒片元能够通过测试。

需要注意,经过view和proj矩阵处理过过的gl_Position的z值始终在0-1之间。因此,z轴等于1时意味着这个顶点位于无限远处。因此,这样可以让所有物体都“位于”天空盒的前面。

对于view矩阵,通过取其左上方的3*3矩阵,可以移除位移效果。这使得天空盒不会随着玩家移动。

glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));

反射

使用视线方向的反射向量作为采样立方体贴图的方向向量。

img

折射

使用视线方向的折射方向作为采样立方体贴图的方向向量。

img

一些材质的折射率:

材质 折射率
空气 1.00
1.33
1.309
玻璃 1.52
钻石 2.42

GLSL内建函数refract包含三个参数,分别为:从视线出发的视线向量;法向量;1/折射率。前两个参数必须被归一化。

高级数据

调用glBufferData为缓冲目标分配内存并填充数据时,若参数data设置为NULL,就会仅分配内存而不填充。

glBufferSubData(TARGET, offset, length, *data)用于向已分配内存的TARGET缓冲区,距离头部指针offset字节的内存位置写入长度为lengthdata数据。

glBufferSubData提供了一种更简洁的写入数据的方式。

使用glBufferData写入数据时,我们必须确保单个顶点的各个属性在内存上是连续的。但用glBufferSubData,我们就可以把各属性作为单独的数组,分别调用glBufferSubData填充数据、调用glVertexAttribPointer指定顶点属性。例如:

1
2
3
4
5
6
7
8
9
10
11
float positions[] = { ... };
float normals[] = { ... };
float tex[] = { ... };
// 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));
glVertexAttribPointer(
2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals)));

glMapBuffer(TARGET, GL_WRITE_ONLY)返回一个void*指针,指向TARGET缓冲区的头部位置。可以使用memcpy函数向指针指向的内存空间写入数据。完成数据写入后,需要调用glUnmapBuffer(TAGRET)解除映射。该函数返回一个GL_BOOL值,若成功映射数据到缓冲,则为GL_TRUE,否则(如写入内存超过分配内存)返回GL_FALSE。该函数在直接从文件读入数据写入缓冲目标时很有用。

glCopySubData(READ_TARGET, WRITE_TARGET, readoffset, writeoffset, size)用于从READ_TARGET缓冲区距离头部readoffset字节的内存位置复制长度为size的数据到WRITE_TARGET缓冲区距离头部writeoffset字节的内存位置。

如果要复制的两个缓冲区类型相同,可以先把其中之一或者二者换为专用于复制的GL_COPY_WRITE_BUFFERGL_COPY_READ_BUFFER缓冲区。


LearnOpenGL学习笔记(九) - 面剔除、帧缓冲、CubeMap与高级数据
http://example.com/2024/08/06/LearnOpenGL学习笔记(九)-面剔除、帧缓冲、CubeMap与高级数据/
作者
Yoi
发布于
2024年8月6日
许可协议