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 |
|
其中,glFramebufferTexture2D
用于把纹理对象附加到帧缓冲上。具体参数有:
target
:帧缓冲的目标(绘制、读取或者两者皆有)attachment
:我们想要附加的附件类型。除了颜色缓冲外,还有GL_DEPTH_ATTACHMENT
、GL_STENCIL_ATTACHMENT
和GL_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 |
|
上面两步是渲染循环之前,让帧缓冲对象变完整的过程。
- 接着,绑定刚才创建的帧缓冲对象,然后绘制场景。注意绘制完要解绑,回到默认帧缓冲。
1 |
|
- 然后,在默认帧缓冲绘制面片。
1 |
|
后处理
简单的后处理
- 反向:One Minus 采样
- 灰度:采样值的r、g、b相加除以3,作为新的r、g、b
核处理
使用卷积核对图像进行卷积。
1 |
|
卷积核不同,处理的效果也不同。
- 锐化: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纹理的纹理,通过三维方向向量(立方体中心为原点)进行采样。
创建、绑定立方体贴图的方法与2D纹理类似,只是目标GL_TEXTURE_2D
要更改为GL_TEXTURE_CUBE_MAP
。
1 |
|
OpenGL并没有专门为Cube Map提供输入数据的函数。但Cube Map本质上是六个2D纹理组成的纹理,所以可以通过调用六次glTexImage2D
的方式输入数据。
Cube Map的每个面都有单独的Target,前缀都为GL_TEXTURE_CUBE_MAP
,后缀依次为:POSITIVE_X
、NEGATIVE_X
、POSITIVE_Y
、NEGATIVE_Y
、POSITIVE_Z
、NEGATIVE_Z
,分别对应右、左、上、下、后、前。它们作为unsigned int,依次递增1,因此可以用循环赋值。
1 |
|
完成纹理数据输入后,同样需要设置纹理的过滤和环绕方式。与2D纹理不同的是,Cube Map在环绕方式上除了S、T还有R维度。它类似于三维空间中的Z轴,当方向矢量未击中任何面(如接缝处)时,返回边界值。
1 |
|
在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()));
反射
使用视线方向的反射向量作为采样立方体贴图的方向向量。
折射
使用视线方向的折射方向作为采样立方体贴图的方向向量。
一些材质的折射率:
材质 | 折射率 |
---|---|
空气 | 1.00 |
水 | 1.33 |
冰 | 1.309 |
玻璃 | 1.52 |
钻石 | 2.42 |
GLSL内建函数
refract
包含三个参数,分别为:从视线出发的视线向量;法向量;1/折射率。前两个参数必须被归一化。
高级数据
调用glBufferData
为缓冲目标分配内存并填充数据时,若参数data
设置为NULL,就会仅分配内存而不填充。
glBufferSubData(TARGET, offset, length, *data)
用于向已分配内存的TARGET
缓冲区,距离头部指针offset
字节的内存位置写入长度为length
的data
数据。
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_BUFFER
和GL_COPY_READ_BUFFER
缓冲区。