![Opengl实时渲染器(六)延迟渲染与SSAO](https://puluo.top/wp-content/uploads/2023/11/image-26.png)
Opengl实时渲染器(六)延迟渲染与SSAO
- 22 11 月, 2023
- by
- pladmin
引言
前面一节我们实现了PBR和IBL,其中PBR材质使得模型的外观更加真实,IBL光照使得模型拥有更正确的环境光反射。但是全局光照并不止有PBR和IBL,还有环境光遮蔽、镜面反射等非常重要的内容。
在早期,实现AO和镜面反射的算法都是基于屏幕空间的(算法名称分别为SSAO和SSR)。什么叫基于屏幕空间呢,就是我们需要在屏幕空间上获取和分析所有像素对应的世界坐标、法线等信息,从而推测其AO的系数和镜面反射的结果。
但是普通的前向渲染管线 在片元着色器完成光照着色后,就丢弃了所有可用信息,直到屏幕空间时已经损失了所有数据。为了能在屏幕空间保留上述信息,我们必须将光照着色的时期推迟,具体的方法是 在片元着色器中,不直接计算光照,而是将所有片元的坐标信息、法线信息、材质信息等内容暂时记下,并写入缓冲区的纹理中。直到最后进入屏幕空间时,我们重新整理所有像素的数据信息,从而完成着色、SSAO和SSR的计算。这样的渲染方式我们称作延迟渲染。
接下来简单说下延迟渲染的实现。
Part1. 延迟渲染管线
前面我们总结到,延迟渲染管线其实就是把片元着色器 计算光照的过程推迟了,因此它一共需要分两个pass:第一个pass将世界坐标、世界法线、材质参数全部记录到G-Buffer中,第二个pass在屏幕空间对每个像素进行着色。
①创建G-Buffer
首先需要创建一个G-Buffer,并创建相应的世界坐标纹理、世界法线纹理、材质参数纹理 并绑定到G-Buffer中。同时不要忘了绑定深度缓冲:
glGenFramebuffers(1, &gBuffer); glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); // - 位置颜色缓冲 glGenTextures(1, &gPosition); glBindTexture(GL_TEXTURE_2D, gPosition); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, screenWidth, screenHeight, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // - 法线颜色缓冲 glGenTextures(1, &gNormal); glBindTexture(GL_TEXTURE_2D, gNormal); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, screenWidth, screenHeight, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // - 颜色 + 镜面颜色缓冲 glGenTextures(1, &gAlbedoSpec); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, screenWidth, screenHeight, 0, GL_RGBA, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // - ARM颜色缓冲 glGenTextures(1, &gARM); glBindTexture(GL_TEXTURE_2D, gARM); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, screenWidth, screenHeight, 0, GL_RGB, GL_FLOAT, NULL); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gPosition, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT3, GL_TEXTURE_2D, gNormal, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT4, GL_TEXTURE_2D, gAlbedoSpec, 0); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT5, GL_TEXTURE_2D, gARM, 0); GLuint attachments2[4] = { GL_COLOR_ATTACHMENT2, GL_COLOR_ATTACHMENT3, GL_COLOR_ATTACHMENT4, GL_COLOR_ATTACHMENT5 }; glDrawBuffers(4, attachments2); //深度缓冲 GLuint rboDepth; glGenRenderbuffers(1, &rboDepth); glBindRenderbuffer(GL_RENDERBUFFER, rboDepth); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, screenWidth, screenHeight); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
这里简单解释下绑定流程(老是容易忘)。首先需要创建一个整体的Buffer,Buffer是一个完整的空间,在这个空间中可以定义若干个用于接收数据的纹理(纹理必须要与Buffer绑定才能接收数据)。在片元着色器和Buffer之间会有一系列交换区 称作Attachment,通过glDrawBuffers可以指定 片元着色器的一系列out 分别要输出到哪个Attachment槽位中,而glFramebufferTexture2D就是声明哪个Attachment槽位的数据最后会写入Buffer中的哪个纹理。
纹理的设置上有点说法,为了节省纹理数量,将AO、金属度和粗糙度三者压缩成了arm贴图;对于世界坐标和法线这类方位信息需要非常精确,所以给的是double类型的浮点数,而对于ARM和漫射高光这类材质信息只需要用float即可。
②第一个pass
在完成光照的shadowmap后,我们开始对场景中的物体进行着色。在这里我们进行第一个pass,将所有片元的坐标信息、法线信息、材质信息等内容写入G-Buffer的各个纹理中。
//第一个pass,写入各种材质信息到G_Buffer glDisable(GL_BLEND); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); glViewport(0, 0, screenWidth, screenHeight); /*设置G_Buffer*/ //先要把G_Buffer的贴图绑定上 glBindFramebuffer(GL_FRAMEBUFFER, gBuffer); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); for (int i = 0; i < scene->models.size(); i++) { glm::mat4 parent_model = transform(scene->models[i].pos, scene->models[i].rot, scene->models[i].scale); for (int j = 0; j < scene->models[i].meshes.size(); j++) { shader_g_buffer.use(); //////shader空间变换设置 glm::mat4 leaf_model = transform(scene->models[i].meshes[j].pos, scene->models[i].meshes[j].rot, scene->models[i].meshes[j].scale); glm::mat4 model = parent_model * leaf_model; projection = glm::perspective(scene->cameras[0].fov, screenWidth / screenHeight, 0.1f, 500.0f); shader_g_buffer.setMatrix("model", model); shader_g_buffer.setMatrix("view", view); shader_g_buffer.setMatrix("projection", projection); scene->models[i].meshes[j].Draw(shader_g_buffer); } } glBindFramebuffer(GL_FRAMEBUFFER, 0);
需要传入shader的信息不多,无非就是MVP矩阵和所有材质信息。shader_g_buffer可以这样写:
///////////////////////Vertex Shader/////////////////////// #version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aUV; layout (location = 2) in vec3 aNormal; layout (location = 3) in vec3 aTangent; layout (location = 4) in vec3 aBitangent; out VS_OUT { vec3 normal; vec2 texcoord; vec3 FragPos; mat3 TBN; }vs_out; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); vs_out.FragPos = vec3(model * vec4(aPos, 1.0)); vs_out.normal = mat3(transpose(inverse(model))) * aNormal; vs_out.texcoord = aUV; vec3 T = normalize(vec3(model * vec4(aTangent, 0.0))); vec3 N = normalize(vec3(model * vec4(aNormal, 0.0))); T = normalize(T - dot(T, N) * N); vec3 B = cross(T, N); vs_out.TBN = mat3(T, B, N); } ///////////////////////Geometry Shader略掉,就是转了个法线/////////////////////// ///////////////////////Fragment Shader/////////////////////// #version 330 core layout (location = 0) out vec3 gPosition; layout (location = 1) out vec3 gNormal; layout (location = 2) out vec4 gAlbedoSpec; layout (location = 3) out vec3 gARM; in vec3 f_normal; in vec2 f_texcoord; in vec3 f_FragPos; in mat3 f_TBN; struct Material { vec4 diffuse; bool diffuse_texture_use; sampler2D diffuse_texture; float specular; bool specular_texture_use; sampler2D specular_texture; float metallic; bool metallic_texture_use; sampler2D metallic_texture; float roughness; bool roughness_texture_use; sampler2D roughness_texture; bool normal_texture_use; sampler2D normal_texture; float ambient; bool ambient_texture_use; sampler2D ambient_texture; }; uniform Material material; void main() { gPosition = f_FragPos; if(material.normal_texture_use == true) { gNormal = texture(material.normal_texture, f_texcoord).rgb; gNormal = normalize(gNormal * 2.0 - 1.0); gNormal = normalize(f_TBN * gNormal); } else gNormal = normalize(f_normal); gNormal = normalize((gNormal + 1) / 2); if(material.diffuse_texture_use == true){ gAlbedoSpec.rgb = texture(material.diffuse_texture, f_texcoord).rgb * material.diffuse.rgb; } else gAlbedoSpec.rgb = material.diffuse.rgb; if(material.specular_texture_use == true){ gAlbedoSpec.a = texture(material.specular_texture, f_texcoord).r * material.specular; } else gAlbedoSpec.a = material.specular; if(material.ambient_texture_use == true){ gARM.r = texture(material.ambient_texture, f_texcoord).r * material.ambient; } else gARM.r = material.ambient; if(material.roughness_texture_use == true){ gARM.g = texture(material.roughness_texture, f_texcoord).r * material.roughness; } else gARM.g = material.roughness; if(material.metallic_texture_use == true){ gARM.b = texture(material.metallic_texture, f_texcoord).r * material.metallic; } else gARM.b = material.metallic; }
这块内容虽然冗长但逻辑非常简单。。。就直接将世界坐标、世界法线和各种材质信息写进out即可。
现在我们可以直接获取到G-Buffer中的各个纹理,在进行第二个pass之前,我们可以先把环境贴图/skybox渲染一下。
③第二个pass
第二个pass就不是对场景中的模型进行shader计算了,而是直接对屏幕进行shader计算。这里我们需要将场景中的灯光、相机坐标、shadowmap以及IBL相关的所有贴图传入:
shader_deferred.use(); shader_deferred.setFloat("time", glfwGetTime()); /*shader光源设置*/ shader_deferred.setVec3("dl[0].dir", scene->lights[0]->getDir()); shader_deferred.setVec3("dl[0].color", scene->lights[0]->getColor()* scene->lights[0]->getIntensity()); shader_deferred.setVec3("pl[0].pos", scene->lights[1]->getPos()); shader_deferred.setVec3("pl[0].color", scene->lights[1]->getColor()* scene->lights[1]->getIntensity()); shader_deferred.setFloat("pl[0].constant", scene->lights[1]->getConstant()); shader_deferred.setFloat("pl[0].linear", scene->lights[1]->getLinear()); shader_deferred.setFloat("pl[0].quadratic", scene->lights[1]->getQuadratic()); /*shader相机设置*/ shader_deferred.setVec3("cameraPos", scene->cameras[0].cameraPos); /*shader空间变换设置*/ //projection = glm::perspective(scene->cameras[0].fov, screenWidth / screenHeight, 0.1f, 500.0f); shader_deferred.setMatrix("view", view); shader_deferred.setMatrix("projection", projection); glBindVertexArray(quadVAO); /*shader材质参数设置*/ glActiveTexture(GL_TEXTURE7); glBindTexture(GL_TEXTURE_2D, depthMap); shader_deferred.setInt("dir_shadowMap", 7); glActiveTexture(GL_TEXTURE8); glBindTexture(GL_TEXTURE_CUBE_MAP, depthCubemap); shader_deferred.setInt("point_shadowMap", 8); glActiveTexture(GL_TEXTURE9); glBindTexture(GL_TEXTURE_CUBE_MAP, hdrCubemap_convolution); shader_deferred.setInt("diffuse_convolution", 9); glActiveTexture(GL_TEXTURE10); glBindTexture(GL_TEXTURE_CUBE_MAP, hdrCubemap_mipmap); shader_deferred.setInt("reflect_mipmap", 10); glActiveTexture(GL_TEXTURE11); glBindTexture(GL_TEXTURE_2D, brdfLUTTexture); shader_deferred.setInt("reflect_lut", 11); shader_deferred.setMatrix("lightSpaceMatrix", lightSpaceMatrix); shader_deferred.setFloat("far_plane", light->getPlane().y); shader_deferred.setFloat("totalTime", time); glActiveTexture(GL_TEXTURE12); glBindTexture(GL_TEXTURE_2D, gPosition); shader_deferred.setInt("gPosition", 12); glActiveTexture(GL_TEXTURE13); glBindTexture(GL_TEXTURE_2D, gNormal); shader_deferred.setInt("gNormal", 13); glActiveTexture(GL_TEXTURE14); glBindTexture(GL_TEXTURE_2D, gAlbedoSpec); shader_deferred.setInt("gAlbedoSpec", 14); glActiveTexture(GL_TEXTURE15); glBindTexture(GL_TEXTURE_2D, gARM); shader_deferred.setInt("gARM", 15); glDrawArrays(GL_TRIANGLES, 0, 6);
传入后,就可以写shader来做延迟渲染的着色了。着色方式几乎和PBR那章的shader一样,只是需要提前手动采样一下世界坐标、世界法线和各个材质参数:
void main() { f_FragPos = texture(gPosition, f_texcoord).rgb; vec4 diffuse_ = vec4(texture(gAlbedoSpec, f_texcoord).rgb, 1); float specular_ = texture(gAlbedoSpec, f_texcoord).a; float metallic_ = texture(gARM, f_texcoord).b; float roughness_ = texture(gARM, f_texcoord).g; float ambient_ = texture(gARM, f_texcoord).r; vec3 N = texture(gNormal, f_texcoord).rgb; if(roughness_<0.01)roughness_ = 0.01; diffuse_.rgb = pow(diffuse_.rgb,vec3(2.2)); if(diffuse_.r + diffuse_.g + diffuse_.b == 0) discard; ...
要注意,当某一个像素的所有信息都是空时,说明该像素是没有物体的,我们可以直接将该像素discard丢弃掉,这样该像素最终会呈现环境贴图的颜色。
还有一个坑就是,采样出来的漫反射diffuse需要做一次伽马变换,我也不知道为什么要做这步,如果不做的话材质会特别白。。。
最终效果:
![](https://puluo.top/wp-content/uploads/2023/11/image-26.png)
整体效果和前向渲染差不多,那就说明没有明显bug。我们可以开始筹划后续的SSAO和SSR工作了。