美文网首页OpenGL首页投稿(暂停使用,暂停投稿)游戏编程
从0开始的OpenGL学习(三十五)-延迟着色

从0开始的OpenGL学习(三十五)-延迟着色

作者: 闪电的蓝熊猫 | 来源:发表于2018-02-11 16:06 被阅读208次

    标签:延迟着色

    延迟着色的效果

    前向着色和延迟着色

    在开始本章的新内容前,我们先来回忆一下之前的渲染流程是什么:在渲染一个物体前,我们需要把shader准备好。由于需要在片元着色器中进行光照计算,我们需要把场景中所有的光源信息都传递到shader中保存,这样在渲染物体的时候,就可以计算出这个物体在光照下的显示效果。如果场景中有多个物体(事实上场景中只有一个物体的情况非常少),那么我们就必须对每一个物体都走一遍上述的流程,直到所有物体都渲染完成为止。

    上面的流程有没有问题?没有。这个流程非常容易理解,而且实现的效果也不错。按照这个流程,我们可以在大部分的场景中获得一个不错的渲染效果。但是(没错,又是但是),如果我们的场景中物体和光源都非常多,那么上述的流程就显得有些笨拙了。

    分析一下,假设场景中有m个物体和n个光源,我们执行渲染流程的次数就是m * n次,算法的时间复杂度就是O(m * n)。这个复杂度很高,如果m=n,那么这个复杂度就是一个平方级的复杂度,在算法的领域里,平方级的复杂度属于不可接受的范围,这就逼着我们去想办法降低这个复杂度,使其达到一个可以让人接受的范围。

    在算法中,复杂度有这么几种等级:lg(n), n的平方根,n,n * lg(n),n的平方,n的三次方,2的n次方,n的阶乘。速度最快的等级是lg(n),这种速度在现实生活中就像是光速一样,快地让人感觉不到。而n的阶乘的速度,就像是蜗牛爬行的速度一样慢的令人发指,可能运行到世界末日都没有结果。一般而言,可以接受的复杂度是:lg(n),n的平方根,n,以及n * lg(n)。

    这下,我们本章要讲的东西就有了发挥的余地。我们把上述的渲染流程称作前向渲染或者前向着色,接下来要介绍的方法被称作延迟渲染或者延迟着色。为了方便起见,我们统一用前向着色和延迟着色来称呼两种方法。延迟着色方法,是先把场景中所有物体渲染一次,将其相关数据(包括位置、法向量、纹理等)保存到一个帧缓存中,然后,用这些数据配合上光源信息再进行一次计算,得到最终的效果。这样,我们的算法复杂度就降低成O(m + n),也就是线性复杂度,这就是一个可以接受的复杂度等级。

    延迟着色的流程

    一个完整的延迟着色包括以下4个步骤

    • 1、几何阶段:这个阶段要将场景中的物体渲染到帧缓存中,将物体的数据保存起来。
    • 2、光照阶段:在这个阶段,运用上述帧缓存中的数据,结合光源信息,计算出场景经过光照后的结果
    • 3、后期处理阶段:经过光照后,还需要对场景进行一些抗锯齿等后期处理,保证场景的效果
    • 4、最终阶段:将图像传递到主缓存中去,然后在屏幕上显示

    下面这张图很好的展示了延迟着色的流程,请仔细看:


    延迟着色的流程

    在本章的例子中,我们不去实现抗锯齿等后期特效,专注于前面两个阶段,整个场景在光照阶段完成后直接输出到屏幕上,不需要再做其他处理。下面,我们分别来看一下两个阶段到底要做些什么事情。

    几何阶段

    几何阶段中,我们要将场景的信息保存到帧缓存,以便后面的光照阶段使用。这就产生了一个问题,我们需要保存哪些数据呢?先来列举我们在前向着色中用到的数据:

    • 顶点的坐标
    • 顶点的纹理
    • 顶点的法向量
    • 镜面高光强度
    • 光源位置
    • 光源颜色
    • 观察者位置

    在几何阶段,我们需要准备的东西是可以让光照阶段使用的数据。研究一下之前的片元着色器代码,我们发现,光源位置、光源颜色、观察者位置都是可以在光照阶段直接传递给片元着色器的。而需要从顶点着色器传递过来的数据是:法向量、片元位置、纹理坐标。也就是说,物体的信息都需要从几何阶段传递过去,这样,我们就能得出结论:顶点坐标、纹理、法向量以及镜面高光强度都是需要保存的数据。我们要将这些数据保存到一个名叫G-Buffer的缓存中。

    G-buffer是一个我们创造出来的概念,它本质上是一个帧缓存,是那些我们在几何阶段用来保存数据缓存的统称。这些缓存可能是纹理图,也可能是其他东西我们不知道,我们把存有这些数据的帧缓存统称为G-buffer。

    创建帧缓存的流程我们已经非常熟悉了,由于我们要保存4种数据,我们至少需要将3个颜色缓存(顶点的纹理和高光强度共用一个缓存)附加到帧缓存上,构成MRT。这样,我们就能使用一次渲染得到所有的物体信息。(有关MRT的内容,请参考HDR和Bloom一文。)如果要把G-Buffer中的数据显示出来,结果就是这个样子:

    G-Buffer中的数据

    这里只给出了顶点、法线和纹理数据,没有高光信息。因为高光信息和纹理数据是保存在一起的,它只占了1个字节的空间,显示出来的话就是一片红色,没有太大意义,有兴趣的同学不妨尝试显示出来。

    光照阶段

    我们已经拥有了光照计算要用到的所有数据,这些数据保存在G-Buffer中,以纹理图的格式保存。到了光照阶段,我们就要用起来了。这一阶段的主要工作集中在片元着色器中,我们通过绑定纹理图的方式将G-Buffer中的3个颜色缓存纹理图传递到片元着色器中。同时,也将场景中的光源信息传递到片元着色器中,这些信息包括:光源位置、光源颜色等。然后,在片元着色器中只要像平常一样进行光照计算,就可以使整个场景都得到光照的效果了。

    这一个阶段完成后,我们的场景就是这个样子:


    运行效果

    这个场景中,我们用了128个聚光灯光源去照射前面的盒子,每个光源还能移动其位置,完成计算后,我们的场景就是这么华丽。

    延迟着色的实现

    终于到了编码实现的时候了。在编码之前,请先下载本章要用到的工程源码,我们会在这些代码的基础上添加场景,完成延迟着色的功能。在动手之前,我们先来理一下实现的思路,上面的源码是一个空壳子里面没有任何的物体,也没有光源,更加没有帧缓存的东西,这些都是要我们一步步去实现的,因此,我们的实现思路是:

    • 一、在场景中的固定位置放置一些立方体盒子,放置的位置是xy平面,在原点的周围放置11x11个盒子。
    • 二、创建G-Buffer,包括3个颜色附件和一个深度值附件
    • 三、创建着色器,将场景渲染到G-Buffer中
    • 四、显示G-Buffer中的信息
    • 五、创建光源,为光源添加移动的功能
    • 六、光照计算,显示最终场景
    • 七、显示光源,用纯色立方体代替光源显示

    顺着这个思路,我们就能写出上面的场景,想想有点小激动,赶紧开始吧。

    第一步、创建场景

    创建场景的方法很简单,使用renderTextureCube()函数就可以了。在循环体中,我们只要设定好盒子的位置、大小、以及旋转量就可以非常容易的绘制出这个场景。代码如下:

    const int dim = 11;
    const float offset = (float(dim - 1) * 1.5f) * 0.5f;
    glm::mat4 model;
    for (int yy = 0; yy < dim; ++yy) {
        for (int xx = 0; xx < dim; ++xx) {
            model = glm::mat4();
            model = glm::translate(model, glm::vec3(-offset + float(xx) * 1.5f, -offset + float(yy) * 1.5f, 0.0f));
            model = glm::rotate(model, glm::radians(currentFrame * -25.0f), glm::vec3(0.5f, 0.5f, 0.0f));
            model = glm::scale(model, glm::vec3(0.5f));
            ourShader.setMat4("model", glm::value_ptr(model));
            renderTextureCube(ourShader);
        }
    }
    

    代码本身非常直观,不多解释。

    第二步、创建G-Buffer

    运用前一章学到的知识,我们绑定的纹理缓存需要高精度的数据格式,这里我们选择GL_RGB16F格式,但是纹理和镜面高光的数据不用,因为它们本身就是颜色值,所以我们仍然是用GL_RGBA格式,如下:

    /** G-Buffer的创建 */
    unsigned int gBuffer;
    glGenFramebuffers(1, &gBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    
    //位置缓存
    unsigned int gPosition;
    glGenTextures(1, &gPosition);
    glBindTexture(GL_TEXTURE_2D, gPosition);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 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_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);
    
    //法线缓存
    unsigned int gNormal;
    glGenTextures(1, &gNormal);
    glBindTexture(GL_TEXTURE_2D, gNormal);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 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_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);
    
    //漫反射和镜面高光缓存
    unsigned int gAlbedoSpec;
    glGenTextures(1, &gAlbedoSpec);
    glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, 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, gAlbedoSpec, 0);
    
    //告诉OpenGL要渲染三个缓存
    unsigned int attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
    glDrawBuffers(3, attachments);
    
    //深度缓存
    unsigned int rboDepth;
    glGenRenderbuffers(1, &rboDepth);
    glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
    // 检查
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "帧缓存初始化失败!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    /** G-Buffer的创建-结束 */
    

    位置缓存和法线缓存都采用GL_RGB16F的格式保存,这样我们得到的位置和法线数据都是高精度的。漫反射和镜面高光缓存就要用GL_RGBA格式,注意在写的时候后面的参数要从GL_FLOAT改成GL_UNSIGNED_BYTE。然后,告诉OpenGL在渲染这个帧缓存的时候需要渲染3个颜色缓存。最后,使用渲染缓存作为深度缓存附加到帧缓存上,这样,我们就大功告成了。

    第三步、创建着色器,绘制场景

    顶点着色器中,其他的东西都一样只有一点要注意,那就是输出的片元位置是世界空间中的位置,我们来看:

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNormal;
    layout (location = 2) in vec2 aTexCoords;
    
    out vec3 FragPos;
    out vec2 TexCoords;
    out vec3 Normal;
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        vec4 worldPos = model * vec4(aPos, 1.0);
        FragPos = worldPos.xyz; 
        TexCoords = aTexCoords;
        
        mat3 normalMatrix = transpose(inverse(mat3(model)));
        Normal = normalMatrix * aNormal;
    
        gl_Position = projection * view * worldPos;
    }
    

    可以看到,我们将顶点的世界坐标位置赋值给FragPos使其传递给片元着色器,当然,纹理坐标、法向量也需要传递给片元着色器。

    #version 330 core
    layout (location = 0) out vec3 gPosition;
    layout (location = 1) out vec3 gNormal;
    layout (location = 2) out vec4 gAlbedoSpec;
    
    in vec2 TexCoords;
    in vec3 FragPos;
    in vec3 Normal;
    
    uniform sampler2D diffuse;
    uniform sampler2D specular;
    
    void main()
    {
        //保存位置信息
        gPosition = FragPos;
        //保存法线信息
        gNormal = normalize(Normal);
        //保存漫反射颜色信息
        gAlbedoSpec.rgb = texture(diffuse, TexCoords).rgb;
        //保存镜面高光颜色信息
        gAlbedoSpec.a = texture(specular, TexCoords).r;
    }
    

    跟前一章(本文中多次提到了前一章的知识,如果你对前一章还不了解的话,强烈建议你仔细阅读了前一章之后再来阅读本章,这会使你理解更为容易。)的片元着色器一样,我们需要用诸如layout (location = 0)这样的代码来指定输出数据保存到哪个颜色缓存中。对于传入的数值,片元位置可以直接输出,法向量需要规范化一下,漫反射纹理需要从输入的diffuse中采样保存,同样,镜面高光信息也需要从输入的纹理中采样保存。

    准备好着色器之后,在主函数中加载它,在循环体中使用它就可以了:

    /** 1、几何阶段 */
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    glClearColor(0.05f, 0.05f, 0.05f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    ourShader.use();
    
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    glm::mat4 view = camera.GetViewMatrix();
    ourShader.setMat4("projection", glm::value_ptr(projection));
    ourShader.setMat4("view", glm::value_ptr(view));
    ...
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    

    至此,我们的G-Buffer中就有了下一阶段要用到的所有数据,先将它显示出来看看效果。

    四、显示G-Buffer

    要显示G-Buffer中的数据,我们就需要对G-Buffer的颜色缓存采样,然后绘制到默认帧缓存中。要做到这一点,我们首先得要新建两个着色器,分别命名为:showShader.vs、showShader.fs。在.vs文件(顶点着色器)中,我们需要获得位置和纹理输入,并将其输出给片元着色器,片元着色器只要根据纹理坐标对其内的纹理图采样就行了,两个文件的代码如下所示:

    //showShader.vs
    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aTexCoords;
    
    out vec2 TexCoords;
    
    void main()
    {
        TexCoords = aTexCoords;
        gl_Position = vec4(aPos, 1.0);
    }
    
    //showShader.fs
    #version 330 core
    out vec4 FragColor;
    
    in vec2 TexCoords;
    
    uniform sampler2D showMap;
    
    void main()
    {             
        FragColor = texture(showMap, TexCoords);
    }
    

    有了着色器程序之后,创建一个Shader对象加载它们,这个Shader对象命名为showShader。使用showShader.setInt("showMap", 0);将showMap成员设置好后,就可以在主循环中根据不同的渲染模式绘制不同的信息了:

    //2、渲染到四边形中
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    if (showMapType == 1) {
        showShader.use();
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, gPosition);
        renderQuad();
    }
    else if (showMapType == 2) {
        showShader.use();
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, gNormal);
        renderQuad();
    }
        
    else if (showMapType == 3) {
        showShader.use();
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
        renderQuad();
    }
    

    绘制之前,自然是要把颜色缓存和深度缓存都清理一遍,然后,根据showMapType的值该设置不同的纹理,这里我们在showMapType为1时绘制位置信息,为2时绘制法向量信息,为3时绘制纹理信息。showMapType的值可以通过键盘上的1、2、3键进行设置,这样方便我们观察不同的数据。完成编码之后,编译运行,你就能看到下面的场景:


    运行效果

    按下键盘上的2或者3,你就能看到另外的两个场景,不错吧!到这一步的源代码打开这里的链接下载。

    五、创建光源,为光源添加移动功能

    这一步,我们要来创建场景中的光源。既然延迟着色要在光源多的环境下才能体现优势,那我们就多创建几个光源好了,建它个128个光源:

    /** 光源信息 */
    const int NR_LIGHTS = 128;
    std::vector<glm::vec3> lightPositions;
    std::vector<glm::vec3> originLightPositions;    //光源初始位置,光源只能在此位置2.5的矩形范围内移动
    std::vector<glm::vec2> lightMoveDirections;     //光源移动方向
    std::vector<glm::vec3> lightColors;
    srand(13);
    for (int i = 0; i < NR_LIGHTS; i++) {
        float xPos = ((rand() % 100) / 100.0) * 15.0 - 7.5;
        float yPos = ((rand() % 100) / 100.0) * 15.0 - 7.5;
        float zPos = 2.0;
        lightPositions.push_back(glm::vec3(xPos, yPos, zPos));
        originLightPositions.push_back(glm::vec3(xPos, yPos, zPos));
    
        float xDir = ((rand() % 100) / 100.0) * 2 - 1;
        float yDir = ((rand() % 100) / 100.0) * 2 - 1;
        lightMoveDirections.push_back(glm::normalize(glm::vec2(xDir, yDir)));
    
        //颜色值,在0.5到1之间
        float rColor = ((rand() % 100) / 200.0f) + 0.5;
        float gColor = ((rand() % 100) / 200.0f) + 0.5;
        float bColor = ((rand() % 100) / 200.0f) + 0.5;
        lightColors.push_back(glm::vec3(rColor, gColor, bColor));
    }
    /** 光源信息-结束 */
    

    代码非常直观,不多解释。移动功能也非常容易,为了方便起见,我们允许光源在其出生点周围2.5的矩形范围内移动,如果超过这个范围,则改变其运动方向,返回到2.5的范围之内:

    //移动光源
    for (int i = 0; i < lightMoveDirections.size(); ++i) {
        glm::vec2 moveStep = lightMoveDirections[i] * speed;
        lightPositions[i] += glm::vec3(moveStep, 0.0f);
        if (lightPositions[i].x < (originLightPositions[i].x - 2.5) || lightPositions[i].x >(originLightPositions[i].x + 2.5))
            lightMoveDirections[i].x = -lightMoveDirections[i].x;
        if (lightPositions[i].y < (originLightPositions[i].y - 2.5) || lightPositions[i].y >(originLightPositions[i].y + 2.5))
            lightMoveDirections[i].y = -lightMoveDirections[i].y;
    }
    

    每当有一个轴的坐标超过范围时,我们就将相应的移动方向置反,使其往回移动,这样我们就将光源的移动范围限制在了2.5的矩形范围内。

    六、光照计算,显示最终场景

    执行光照计算的着色器和之前的着色器都不同,所以,我们还需要新建两个着色器,命名为:deferred_shading.vs和deferred_shading.fs。光照计算的重点在于片元着色器,顶点着色器的代码我就直接贴出了,我们要将注意力集中到片元着色器上:

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aTexCoords;
    
    out vec2 TexCoords;
    
    void main()
    {
        TexCoords = aTexCoords;
        gl_Position = vec4(aPos, 1.0);
    }
    

    片元着色器要比顶点着色器“粗壮”的多,它是骨干是精英,它完成了所有的光照计算操作,所以看起来要比顶点着色器“可怕”许多:

    #version 330 core
    out vec4 FragColor;
    
    in vec2 TexCoords;
    
    uniform sampler2D gPosition;
    uniform sampler2D gNormal;
    uniform sampler2D gAlbedoSpec;
    
    struct SpotLight{
        vec3 Direction;
        vec3 Position;
        float cutOff;
        float outerCutOff;
    
        vec3 Color;
    
        float Linear;
        float Quadratic;
    };
    
    const int NR_LIGHTS = 128;
    uniform SpotLight lights[NR_LIGHTS];
    uniform vec3 viewPos;
    
    //计算聚光灯的效果
    vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir, vec3 diffuse, float specular);
    
    void main() {
        //对图片采样
        vec3 FragPos = texture(gPosition, TexCoords).rgb;
        vec3 Normal = texture(gNormal, TexCoords).rgb;
        vec3 Diffuse = texture(gAlbedoSpec, TexCoords).rgb;
        float Specular = texture(gAlbedoSpec, TexCoords).a;
    
        //和平常一样计算光照
        vec3 lighting = vec3(0.0f);
        vec3 viewDir = normalize(viewPos - FragPos);
        for (int i = 0; i < NR_LIGHTS; ++i) {
            lighting += CalcSpotLight(lights[i], Normal, FragPos, viewDir, Diffuse, Specular);
        }
        FragColor = vec4(lighting, 1.0);
    }
    
    //计算聚光灯的影响
    vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir, vec3 Diffuse, float Specular){
       //环境光
        vec3 ambient = 0.1 * Diffuse;
    
        //漫反射光
        vec3 norm = normalize(normal);
        vec3 lightDir = normalize(light.Position - fragPos);  
            
        float diff = max(dot(norm, lightDir), 0.0);
        vec3 diffuse = light.Color * diff * Diffuse;
    
        //镜面高光
        vec3 reflectDir = reflect(-lightDir, norm);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
        vec3 specular = light.Color * spec * Specular;
    
        //聚光灯
        float theta = dot(lightDir, normalize(-light.Direction));   //计算片元角度的cos值
        float epsilon = light.cutOff - light.outerCutOff;   //计算epsilon的值,用内锥角的cos值减去外锥角的cos值
        float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);   //根据公式计算光照强度,并限制结果的范围
    
        diffuse *= intensity;
        specular *= intensity;
    
        //衰减
        float distance = length(light.Position - fragPos);
        float attenuation = 1.0 / (1 + light.Linear * distance + light.Quadratic * (distance * distance));
        ambient *= attenuation;
        diffuse *= attenuation;
        specular *= attenuation;
    
    
        return ambient + diffuse + specular;
    }
    

    聚光灯的计算函数,我们直接从三种光源模型一章中复制过来,修改一下参数就行。原理是一样的,所以计算流程没有改变。本章的实现是main函数中的代码,对输入的三组数据进行采样,作为参数传递给CalcSpotLight进行计算,将所有光源的计算结果累加,得到最终的颜色值,输出!

    准备好着色器之后,我们就可以使用了。首先,片元着色器的纹理需要设置好:

    shaderLightingPass.use();
    shaderLightingPass.setInt("gPosition", 0);
    shaderLightingPass.setInt("gNormal", 1);
    shaderLightingPass.setInt("gAlbedoSpec", 2);
    

    接着,绘制的时候绑定所需的纹理:

    else if (showMapType == 4) {
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, gPosition);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, gNormal);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
    ...
    }
    

    新建一个显示类型的分支,在showMapType等于4的时候显示延迟着色的效果。然后,设置光源属性,进行绘制:

    shaderLightingPass.use();
    for (int i = 0; i < lightPositions.size(); ++i) {
        shaderLightingPass.setVec3("lights[" + std::to_string(i) + "].Position", lightPositions[i]);
        shaderLightingPass.setVec3("lights[" + std::to_string(i) + "].Color", lightColors[i]);
        shaderLightingPass.setFloat("lights[" + std::to_string(i) + "].Linear", linear);
        shaderLightingPass.setFloat("lights[" + std::to_string(i) + "].Quadratic", quadratic);
        shaderLightingPass.setVec3("lights[" + std::to_string(i) + "].Direction", glm::vec3(0.0f, 0.0f, -3.0f));
        shaderLightingPass.setFloat("lights[" + std::to_string(i) + "].cutOff", glm::cos(glm::radians(17.5f)));
        shaderLightingPass.setFloat("lights[" + std::to_string(i) + "].outerCutOff", glm::cos(glm::radians(20.0f)));
    }
    shaderLightingPass.setVec3("viewPos", camera.Position);
    renderQuad();
    

    光源的所有信息都需要这样设置,完成之后,编译运行,就能看到效果了:


    运行效果

    Very good!这样的场景才够爽!

    到这一阶段的代码,你可以在这里下载。

    七、显示光源,前向着色和延迟着色的结合

    理论上,到上一步为止,我们的延迟着色功能已经实现了,不过,我们还可以来继续搞点事情,把前向着色和延迟着色结合起来!利用前向着色显示光源,用延迟着色显示场景。听起来好像挺复杂的,做起来非常容易。就是在渲染了场景之后按照以前的流程绘制一系列的立方体而已。而且绘制立方体的着色器也不需要有什么特别的功能,直接输出某一种颜色就可以了。

    //显示所有的光源
    glClear(GL_DEPTH_BUFFER_BIT);
    shaderLightBox.use();
    shaderLightBox.setMat4("projection", glm::value_ptr(projection));
    shaderLightBox.setMat4("view", glm::value_ptr(view));
    for (unsigned int i = 0; i < lightPositions.size(); i++) {
        glm::mat4 model = glm::mat4();
        model = glm::translate(model, lightPositions[i]);
        model = glm::scale(model, glm::vec3(0.1f));
        shaderLightBox.setMat4("model", glm::value_ptr(model));
        shaderLightBox.setVec3("lightColor", lightColors[i]);
        renderCube();
    }
    

    顶点着色器和片元着色器非常简单,我就不贴出来了,从前面的例子中拷贝一份就可以。绘制光源的时候,最重要的一点是一定要将深度信息清空!否则,无法看到光源。完成所有这些操作后,我们的场景就变的更好玩了:

    运行效果

    这实在是太有趣了。好了,完整的代码在这里,有兴趣的同学在此基础上继续搞事情吧。

    延迟着色的缺点

    当然了,延迟着色也不是万能的,它的缺点也十分明显,主要的缺点有以下四个:

    • 内存开销大
    • 读写G-Buffer的内存带宽用量是性能瓶颈
    • 无法实现透明物体的渲染
    • 对MSAA的支持不友好,主要原因是需要开启MRT

    由于这些缺点,如果场景中光源数量较少,使用延迟着色的效率可能还比不上前向着色,所以,慎重而有目的性地选择一种合适的着色方式是一个不错的主意。

    延迟着色的优化

    既然知道了延迟着色的缺点,那我们就可以对延迟着色进行一些改进。针对读写G-Buffer速度慢的缺点,我们最直接的想法就是使G-Buffer数据结构变小就行了,这就衍生出了延迟光照(Deferred Lighting)方法。另一种方法是对光源进行分组,每一组光源一起处理,这就是分块延迟渲染方法(Tile-Based Deferred Rendering)。

    限于篇幅,我们只是提一下这两种方法,有兴趣的同学可以阅读本文最后的参考资料获取更详细的信息。

    总结

    在本章中,我们学习了延迟着色方法。这种方法主要用于场景中物体和光源数量都较多的情况。它的思路是将场景中的物体信息先渲染到G-Buffer中,在用G-Buffer中的数据结合光源信息进行一次计算,得到最终的光照效果,这样它的时间复杂度就从O(m*n)降到O(m+n),变得非常有效率。当然,延迟着色也有其缺点,比如内存消耗大、读写G-Buffer容易成为性能瓶颈等等。针对其缺点,我们也提到了两个优化方法,分别是延迟光照和分块延迟渲染法。具体的内容就需要你自己研究了。

    目录
    上一篇

    参考资料

    Deferred Shading
    延迟渲染(Deferred Rendering)的前生今世
    延迟着色
    bgfx的延迟着色example代码
    Light Pre-Pass Rednerer
    Deferred Rendering for Current and Future Rendering Pipelines

    相关文章

      网友评论

        本文标题:从0开始的OpenGL学习(三十五)-延迟着色

        本文链接:https://www.haomeiwen.com/subject/ylaaaxtx.html