帧缓冲 Framebuffer
帧缓冲有点类似于引擎中的渲染目标Render Target,可以让用户自定义颜色缓冲、深度缓冲和模板缓冲。
在默认的设置状况下,我们的渲染操作都是在默认帧缓冲的渲染缓冲上进行的,GLFW帮我们配置好了这个环境,即glBindFramebuffer(GL_FRAMEBUFFER, 0)
。
我们可以通过glGenFramebuffers()
来创建自己的帧缓冲,同纹理等一样,创建后要绑定到当前目标GL_FRAMEBUFFER
来对它进行接下来的操作。
通过if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
可以检查帧缓冲是否完整可用。那么如何确定一个帧缓冲是完整的呢?
- 附加至少一个缓冲(颜色、深度或模板缓冲)。
- 至少有一个颜色附件(Attachment)。
- 所有的附件都必须是完整的(保留了内存)。
- 每个缓冲都应该有相同的样本数。
附件 Attachment
附件是一个内存位置,它能够作为帧缓冲的一个缓冲。附件有两种类型:纹理(Texture)或渲染缓冲对象(RenderbufferObject).
纹理附件 Texture as Attachment
创建纹理附件的方式和创建纹理基本相同,注意在data
参数传入NULL,即可在创建时仅分配内存,后序渲染时再写入数据。
创建后要将其附加到帧缓冲:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
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);
如果是深度缓冲,类型和格式应该设置为GL_DEPTH_ATTACHMENT
和GL_DEPTH_COMPONENT
.
具体参数可以参考LearnOpenGL中的教程。
渲染缓冲对象附件 Renderbuffer Object as Attachment
渲染缓冲对象的数据储存是OpenGL原生的渲染格式,它是只写的,只能通过glReadPixels()
来读取。
它在写入或复制数据到其他缓冲时是更快的,如果不需要采样(如深度和模板),可以选择渲染缓冲对象来进行优化。
怎么选呢?举个栗子
如果希望结合延迟渲染和前向渲染,在前向渲染时需要获取先前的深度值以便深度测试,就适合用Renderbuffer Object
来传递深度值。
但在阴影计算中,我们需要用纹理坐标对灯光空间的深度图进行采样,就适合用Texture
来传递深度值。
渲染到纹理
当进行后处理或延迟缓冲时,我们需要渲染到纹理,再对渲染纹理采样。这个步骤在OpenGL中是这样的:
[Pass 1]
- 将新的帧缓冲绑定为激活的帧缓冲
- 和往常一样渲染场景 [Pass 2]
- 绑定默认的帧缓冲
- 绘制一个横跨整个屏幕的四边形,将帧缓冲的颜色缓冲作为它的纹理。
我们需要为Pass2单独准备一套后处理的Shader,可以搬这个轮子:
VertexShader:
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoords;
out vec2 TexCoords;
void main()
{
TexCoords = aTexCoords;
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
}
FragmentShader:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D screenTexture;
void main()
{
vec3 col = texture(screenTexture, TexCoords).rgb;
//some post process operations HERE
FragColor = vec4(col, 1.0);
}
绘制全屏四边形:
// renderQuad() renders a 1x1 XY quad in NDC
// -----------------------------------------
unsigned int quadVAO = 0;
unsigned int quadVBO;
void renderQuad()
{
if (quadVAO == 0)
{
float quadVertices[] = {
// positions // texture Coords
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
};
// setup plane VAO
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
}
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
}
*** 延迟渲染
延迟渲染主要适用于场景中含有大量光源的情况,原理和优缺点就不详细说了,主要关注一下OpenGL中的实现。
首先为延迟渲染创建一个帧缓冲GBuffer,需要准备两个高精度纹理(16位,GL_RGB16F
)来储存位置和法向量,然后用一个或多个默认的纹理(8位,GL_RGBA
)来存储其他材质纹理信息(albedo/roughness等)。
纹理创建后分别绑定到GL_COLOR_ATTACHMENT0/1/2
,并通过glDrawBuffers()命令告知那些颜色附件要被写入——
GLuint attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
glDrawBuffers(3, attachments);
由于多个颜色缓冲需要被写入,我们在Geometry Pass的片元着色器中也需要做这样的声明:
#version 330 core
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;
注意,这里就不需要常规的out FragColor
了~
在延迟光照时,将gPosition、gNormal、gAlbeldoSpec设置为纹理采样即可。 完整的代码可以在这里找到。
完成的延迟渲染应该是和之前前向渲染没有太大区别的,因为我们光源很少,所以对性能的影响差别也不大。 下一节正式开始SSS!