引言

前面一节我们实现了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需要做一次伽马变换,我也不知道为什么要做这步,如果不做的话材质会特别白。。。

最终效果:

整体效果和前向渲染差不多,那就说明没有明显bug。我们可以开始筹划后续的SSAO和SSR工作了。