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

标签:延迟着色

延迟着色的效果

前向着色和延迟着色

在开始本章的新内容前,我们先来回忆一下之前的渲染流程是什么:在渲染一个物体前,我们需要把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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容