从0开始的OpenGL学习(三十四)-HDR和Bloom

标签:HDR、Bloom

为什么要HDR和Bloom技术?

面对一项新技术,我们首先要解决的一个问题是,我们为什要用它,它有什么好处?在真实世界中,我们的光照强度范围非常广,近乎是无限的。但在计算机中,我们只能把强度范围压缩到[0,1]之间,这对我们来说就非常不公平,凭什么像太阳光那种比手电筒要强上几十万倍的强度和手电筒一样要被限制到1,这太扯淡了。要是手电筒是1的话,那么太阳光就是要几十万,这才能有接近真实世界的效果。于是,HDR就粉墨登场了。

HDR(High Dynamic Range,高动态范围)技术,让我们能用超过1.0的数据表示颜色值。我们可以把颜色的格式设置成16位浮点数或者32位浮点数的方式来扩大颜色值的范围。到目前为止,我们用的都是LDR(低动态范围),所以我们所有的渲染都被限制了。HDR技术对我们来说非常有用,举个简单的例子,如果场景中有很多光源,每个都照在同一个物体上,这个物体的亮度很快就会超过1.0。如果我们强制将它限制到1.0,我们就会失去这个物体上的细节信息,例如纹理状况。所以,为了配合HDR,我们还需要独特的色调映射(tone mapping)算法来有选择地显示物体高光部分的细节,低光部分的细节,还是都显示,达到类似这样的效果:


HDR示意图

随着曝光度的增加,原本场景中高光部分(窗户玻璃)的细节逐渐消失,低光部分(楼梯扶手背部)的细节逐渐显现。这就是色调映射算法的威力。

Bloom,中文名叫泛光(或者眩光,怎么顺怎么来),是用来模拟光源那种发光或发热感觉的技术。举个例子:


泛光效果与非泛光效果的对比

有Bloom技术的支持,场景的显示效果提升了一个档次。发光发热就是要有这种效果,这种一眼看上去就是光源的效果,不像左边的图那么干巴巴的,像是没电了一样。之所以把Bloom和HDR放在一起讲,是因为很多人都把HDR和Bloom搞混了,认为他们就是一个东西,其实不然。我可以只使用HDR来渲染场景,也可以只使用Bloom来渲染,两者是互相独立互不影响的。当然,只使用一种技术产生的效果不好,这个回头再说。这次我们会先创建一个只使用HDR的场景,然后创建一个将HDR和Bloom融合起来的场景看效果。

使用HDR

要使用HDR非常简单,我们只需要将帧缓存的颜色缓存格式设置成16或者32位浮点数就行。这样,当我们渲染场景到帧缓存时,OpenGL就不会把颜色值截断到1.0,我们也就保留了场景的HDR信息。你可以设置默认帧缓存的颜色缓存,也可以像我一样新建一个帧缓存,附加一个16位浮点数格式的颜色缓存。当然了,推荐使用自己新建帧缓存,后面我们有大用。具体的设置代码你可以参考这里:

    //颜色缓存
    unsigned int colorBuffer;
    glGenTextures(1, &colorBuffer);
    glBindTexture(GL_TEXTURE_2D, colorBuffer);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    //深度缓存
    unsigned int rboDepth;
    glGenRenderbuffers(1, &rboDepth);
    glBindRenderbuffer(GL_RENDERBUFFER, rboDepth);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);

    //帧缓存
    unsigned int hdrFBO;
    glGenFramebuffers(1, &hdrFBO);
    glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBuffer, 0);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rboDepth);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "帧缓存未初始化完毕!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

上述代码中,需要重点关注的只有一行,就是glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);。你可以看到,我们对纹理的内部格式做了一些小改动,把原本我们熟悉的GL_RGBA改成了GL_RGBA16F,这就是告诉OpenGL我们的纹理内部格式要16位浮点数,你不要把它截断了,我有大用。就是这么简单!

先来构造一个场景

为了看看HDR对场景的影响,我们先要来创造一个可以看出不同的场景。这里,我们假想自己处于一个隧道之中,隧道一端有一盏非常亮的灯(亮度大约是200),然后,在隧道里面零星地放着几盏小灯,这些灯远没有隧道尽头的灯亮(亮度大约是0.1),我们就在隧道的另一端看这个隧道的样子。

要模拟隧道,我们可以把一个标准的立方体在某一个轴(比如说z轴)上进行放大,再设置一些光源的位置和颜色:

    //光源信息
    //位置
    std::vector<glm::vec3> lightPositions;
    lightPositions.push_back(glm::vec3(0.0f, 0.0f, 49.5f)); // 后面的光
    lightPositions.push_back(glm::vec3(-1.4f, -1.9f, 9.0f));
    lightPositions.push_back(glm::vec3(0.0f, -1.8f, 4.0f));
    lightPositions.push_back(glm::vec3(0.8f, -1.7f, 6.0f));
    //颜色
    std::vector<glm::vec3> lightColors;
    lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f));   //亮到不像话的光
    lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f));
    lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f));
    lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f));

完成之后,添加一些代码,将场景渲染到之前我们创建的帧缓存上,然后显示出来,代码结构大概是这个样子:

    glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (GLfloat)SCR_WIDTH / (GLfloat)SCR_HEIGHT, 0.1f, 100.0f);
    glm::mat4 view = camera.GetViewMatrix();
    shader.use();
    shader.setMat4("projection", glm::value_ptr(projection));
    shader.setMat4("view", glm::value_ptr(view));
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, woodTexture);
    for (int i = 0; i < lightPositions.size(); ++i) {
        shader.setVec3("lights[" + std::to_string(i) + "].Position", lightPositions[i]);
        shader.setVec3("lights[" + std::to_string(i) + "].Color", lightColors[i]);
    }
    shader.setVec3("viewPos", camera.Position);
    //渲染隧道
    glm::mat4 model = glm::mat4();
    model = glm::translate(model, glm::vec3(0.0f, 0.0f, 25.0));
    model = glm::scale(model, glm::vec3(2.5f, 2.5f, 27.5f));
    shader.setMat4("model", glm::value_ptr(model));
    shader.setInt("inverse_normals", true);
    renderCube();
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    //2、渲染到四边形中
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    hdrShader.use();
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, colorBuffer);
    renderQuad();

没啥好说的,代码都非常直观,接下来是着色器部分。顶点着色器非常简单,只需要接收两个值的输入,再将这两个值输出就行了,这两个值分别是位置和纹理坐标。重点在片元着色器!片元着色器的框架是这样:

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D hdrBuffer;
//一些设置

void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

    //一些其他的操作
    ...

    vec3 result = pow(hdrColor, vec3(1.0 / gamma));
    FragColor = vec4(result, 1.0);
}

这是一个简单的片元着色器,非常简单。说实话,你可以直接用这个着色器看看没有经过特殊的色调映射算法时显示的效果,效果也不会差。这里要注意的是,在计算颜色的时候,我们运用了gamma校正。这就要求我们必须在加载图片的时候指定SRGB格式,然后,在渲染场景时使用二次项的衰减系数。这两步操作都很容易实现,你可以参考前面的Gamma校正一文进行编码,这里就不多费口舌了。接下来,我们来到HDR技术最重要的一个环节:色调映射(tone mapping)。

色调映射(tone Mapping)

对HDR技术来说,最重要的就是色调映射算法。当我们有了一个场景的HDR信息后,我们就无可避免地面临这样一个选择,是更多地表现高光部分的细节,还是低光部分的细节,或者是两个部分细节都表现出来?你可能觉得,这还用想吗,当然是两个部分的细节都要。这个你还真不能急,因为如果你把两个部分的细节都显示出来了,整个场景就会显的非常不真实,有一种置身魔法世界的玄幻感,如果你要显示的是人的话,可能会有一种顶着光环的上帝的既视感,很诡异。所以,我们基本上都是在高光和低光之间做出选择,这就衍生出我们最常用的一个映射算法:曝光度运算算法。


曝光度运算算法公式

上面这个公式就是算法计算公式,其中hdr表示我们得到的hdr颜色信息,1.0表示LDR的亮度上限值,e是自然底数。将这个公式翻译成代码就是:

result = vec3(1.0) - exp(-hdrColor * exposure); 

加上这行核心代码之后,就可以运行了。且慢动手,我们还没对这个exposure变量赋值呢。为了方便观察不同exposure值的效果,我们要将这个变量设置成uniform,然后main文件中添加按键控制功能,具体的代码不用我多说,相信各位都已经非常熟悉了。好了,我们来运行一下看看效果:


不同曝光度的显示效果

可以看到,随着曝光度越来越低,高光部分的细节越来越清晰,当然,低光部分的细节就越来越模糊了。如果这是一个真实的游戏场景,这个效果会更加震撼!

好了,来看看我的代码是怎样的吧。我的代码里还有一种更加简单的reinhard色调映射算法,有兴趣的童鞋自己百度下吧。

Bloom(泛光)

要实现Bloom特效,我们首先就要知道需要在哪些地方实现,换句话说,我们需要知道光源的位置。这个步骤,配合上HDR技术,我们可以非常容易的实现。通常,光源都是场景中最亮的东西,我们可以把它设置成一个大于1.0的数值,然后,在渲染场景的时候,在片元着色器中添加判断:如果当前片元的颜色值大于1.0,我们就认为它是光源,将其渲染出来;如果不是,则把它的颜色设置成黑色。这样,我们就能得到一个张只有光源的渲染图,接着就可以通过这张图来实现模糊效果进而实现Bloom特效。

高斯模糊

实现Bloom特效中最难也是最重要的一个步骤,就是对光源场景进行高斯模糊。之前我们也实现过高斯模糊效果,是在帧缓存的文章中,我们使用了一个核矩阵来实现整个场景的模糊效果,但是这种模糊效果太low了,这次我们会用另一种方法,叫做双通道高斯模糊(two-pass Gaussian blur)。

从模糊的原理角度来讲,所有的模糊都基于这样一点事实:当前片元的最终颜色值是当前片元的颜色以及周围的颜色共同作用的结果。各种模糊效果不同点仅仅是所有这些起作用的片元对最终颜色值起多大的作用而已。在帧缓存一文中,我们认为,当前片元颜色权重为4/16=0.25,与之相邻的四个片元颜色的权重为2/16=0.125,角落里的四个片元是1/16=0.0625。将这些颜色值合并起来之后就是最终的颜色值。双通道高斯模糊采样的权重与计算方式和这个不同,来看下面这张图:


双通道高斯模糊

我们认为,当前片元颜色值是由横竖两条线上的片元颜色值决定的,其权重由近到远分别是:0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162。并且,为了得到更好的模糊效果,我们会对同一张图进行不止一次的双通道高斯模糊操作。

好,原理的内容已经弄清了,下面就实现这个效果。

实现Bloom特效

先来整理一下实现的步骤:

  • 1、使用两个帧缓存或者用给一个帧缓存附加两个颜色缓存用来分别渲染场景和光源。
  • 2、对光源图进行5次双通道高斯模糊操作,将结果保存到一个新的帧缓存中
  • 3、将场景和经过高斯模糊操作的光源图进行合并
  • 4、对合并的图进行色调映射处理,形成最终的效果图输出

流程很明确,关键在于如何实现!

1、渲染场景和光源

用两个帧缓存渲染场景和光源这种操作实在是太low了,我们怎么能如此不思进取,当然要选择后一种方法:用一个帧缓存的两个不同的颜色缓存来渲染。

要做到这一点,我们就需要对原本绑定颜色缓存的流程做一点改动:

    /** 物体本身和光源场景帧缓存 */
    unsigned int hdrFBO;
    glGenFramebuffers(1, &hdrFBO);
    glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
    //创建两个颜色缓存
    unsigned int colorBuffers[2];
    glGenTextures(2, colorBuffers);
    for (int i = 0; i < 2; ++i) {
        glBindTexture(GL_TEXTURE_2D, colorBuffers[i]);
        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_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);  // 这里我们把纹理截断到边缘,因为不希望超出的部分会再重复采样
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

        //附加到帧缓存
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0);
    }
    //创建深度缓存(渲染缓存)
    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);

    //告诉OpenGL这个帧缓存要用到哪些颜色缓存
    unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
    glDrawBuffers(2, attachments);

    //检查帧缓存是否设置成功
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        std::cout << "帧缓存设置失败,请检查代码!" << std::endl;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    /** 物体本身和光源场景帧缓存-结束 */

使用2个颜色缓存的时候要注意两点:1、创建两个颜色缓存,附加到帧缓存的时候分别指定成GL_COLOR_ATTACHMENT0和GL_COLOR_ATTACHMENT1。这一步,上面的代码通过一个循环,然后GL_COLOR_ATTACHMENT0+i的方式来设置。2、告诉OpenGL渲染这个帧缓存的时候,需要渲染2个颜色缓存。这一步,是通过调用glDrawBuffers函数,指定数量并且指定要渲染的颜色缓存数组来实现的。剩下的代码都是平常的创建帧缓存的代码,不多解释。

在片元着色器中,我们也要指定两个缓存输出的颜色是什么,这个操作和指定顶点着色器输入的操作非常类似,我们来看:

//片元着色器
#version 330 core
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;

...

void main() {
    // 检查结果值是否高于某个门槛,如果高于就渲染到光源颜色缓存中
    float brightness = dot(result, vec3(0.2126, 0.7152, 0.0722));
    if(brightness > 1.0)
        BrightColor = vec4(result, 1.0);
    else
        BrightColor = vec4(0.0, 0.0, 0.0, 1.0);
    FragColor = vec4(result, 1.0);
}

如你所见,我们要用layout的方法指定缓存0和缓存1对应的颜色,然后在主函数中对这些颜色赋值,这样我们就能得到相应的场景效果了,是不是很简单?我们把门槛值设置成1.0,凡是超过这个值的我们都认为是发光区域(不仅仅是光源才超过,有些亮的部分也会超过),将颜色输出到缓存1中,正常的场景输出到缓存0中。

这个阶段的代码,请参考这里

编译运行之后,你应该看到这样的场景:


运行效果
2、进行高斯模糊操作

按照我们之前分析的原理,我们可以使用一些小技巧巧妙地实现高斯模糊。我们先对整个场景进行横向的高斯模糊渲染,然后再进行纵向的高斯模糊渲染,重复5次达到效果。为了能实现这个目标,我们要创建两个帧缓存来回渲染。将进行横向渲染的场景保存到缓存1中,再用1进行纵向的渲染到缓存2,这才算是完成了一次高斯模糊渲染。然后,继续用缓存2中的图片渲染到缓存1中,一直这样来回渲染,直到完成5次完整的双通道高斯模糊渲染。听上去很带劲是不是,实现起来也很带劲:

    /** 乒乓帧缓存 */
    unsigned int pingpongFBO[2];
    unsigned int pingpongColorBuffer[2];
    glGenFramebuffers(2, pingpongFBO);
    glGenTextures(2, pingpongColorBuffer);
    for (int i = 0; i < 2; ++i) {
        glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]);
        glBindTexture(GL_TEXTURE_2D, pingpongColorBuffer[i]);
        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_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glFramebufferTexture2D(
            GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongColorBuffer[i], 0);

        if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
            std::cout << "帧缓存准备失败!" << std::endl;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    /** 乒乓帧缓存-结束 */

创建两个帧缓存,来回渲染的过程很像打乒乓球,所以我们形象地将它命名为乒乓帧缓存。由于我们不需要深度信息,所以两个帧缓存只要附加上颜色缓存就可以了。

    //进行高斯模糊
    bool horizontal = true, first_iteration = true;
    shaderBlur.use();
    for (int i = 0; i < two_passGaussianBlurCount * 2; ++i) {
        glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]);
        shaderBlur.setInt("horizontal", horizontal);
        glBindTexture(GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongColorBuffer[!horizontal]);
        renderQuad();
        horizontal = !horizontal;
        if (first_iteration)
            first_iteration = false;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

上面这段代码是用来进行来回渲染用的。首先,在第一次迭代的时候,我们要用光源场景图进行首次渲染。然后,在之后的迭代中用的都是上一次完成高斯模糊渲染之后的图。直到我们进行了10次循环。horizontal变量用来设置当前是进行水平模糊计算还是垂直模糊计算,在片元着色器中有这个uniform接受主函数的控制:

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D image;

uniform bool horizontal;
uniform float weight[5] = float[](0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162);

void main() {
    vec2 tex_offset = 1.0 / textureSize(image, 0);      //每个像素的尺寸
    vec3 result = texture(image, TexCoords).rgb * weight[0];
    if (horizontal) {
        for (int i = 0; i < 5; ++i) {
            //左右两个方向上都要进行采样
            result += texture(image, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
            result += texture(image, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
        }
    }
    else {
        for (int i = 0; i < 5; ++i) {
            //上下两个方向上都要进行采样
            result += texture(image, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i];
            result += texture(image, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i];
        }
    }
    FragColor = vec4 (result, 1.0);
}

片元着色器的代码应当说是相当直观的:我们首先计算了每个片元的位置偏移值,采用的方式是获取当前纹理图尺寸然后取倒数。接着,result变量初始化为当前片元颜色乘上权重数组中的首元素。当然这不是必须的,你也可以直接初始化为0,或者乘上其他的数值当成是权重。接着,接收horizontal变量的控制,如果是true则进行水平方向的模糊计算,要注意当前片元的左右两边都需要计算,所以每次循环需要进行两次计算。实现之后,我们就可以来看看高斯模糊之后的效果了:


5次双通道高斯模糊的效果

这个效果就非常明显了,不像之前的核效果那样,就是稍微模糊了一点,不仔细看还看不出来。

3、合并场景和模糊图,进行色调映射

合并场景和模糊图的方式也非常简单,进行色调映射的方式更加简单,把前面HDR例子里色调映射的算法复制过来就好了。先来看主函数中合并的代码:

    shaderBloomFinal.use();
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, colorBuffers[0]);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, pingpongColorBuffer[!horizontal]);
    shaderBloomFinal.setInt("bloom", 1);
    shaderBloomFinal.setFloat("exposure", 1.0);
    renderQuad();

就是用一个新的着色器来处理两张图,然后输出,没啥花头。片元着色器的代码也很简单:

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D image;
uniform sampler2D imageBlur;
uniform bool bloom;
uniform float exposure;

void main() {   
    const float gamma = 2.2;
    vec3 hdrColor = texture(image, TexCoord).rgb;
    vec3 bloomColor = texture(imageBlur, TexCoord).rgb;
    if (bloom)
        hdrColor += bloomColor;     //添加融合
    
    //色调映射
    vec3 result = vec3 (1.0) - exp(-hdrColor * exposure);
    //进行gamma校正
    result = pow(result, vec3 (1.0 / gamma));
    FragColor = vec4(result, 1.0);
}

如果启用了Bloom,就将两张图的对应片元颜色相加,然后进行色调映射,再加上gamma校正,形成最终的颜色值输出。最终的效果如下所示:


最终效果

效果不错,完整的代码你可以在这里下载。

更加深入的内容

说实话,我还是不满意这个效果,但是也找不出更加好的权重值了。如果你使用一些成熟的游戏引擎(比如虚幻4),你得到的效果肯定比这好无数倍(不信可以看下面的“Bloom Post Process Effect”链接)。更好的方法比如使用更加复杂的核矩阵,使用一组核矩阵(5x5,11x11,21x21,41x41)共同渲染一张图,这样得到的效果就会非常好,当然,你要耗费的资源也非常大,你总是要在性能和效果之间做出抉择的。

说点题外话

在把原始场景和光源场景显示出来的时候,我改了一个哭笑不得的BUG。这个BUG让我查了2天的代码,看得那叫一个仔细啊,检查了帧缓存、检查了渲染,检查了着色器发现,都没有啥问题。甚至最后在从网上down下来例子去改,依然没有发现问题,当时真是砸电脑的心都有了。后来,在不经意的时候,灵光一闪想到了一个可能的原因,结果一试之下果然如此,真是哭笑不得。原来我把一个变量名给写错了,少了一个s,顶点着色器输出变量名和片元着色器输入变量名不一致,链接程序不会报错,简直了。用一张图来形容我的心情就是:


我的心情

来个总结

好,来个总结吧。这次我们学了HDR和Bloom的知识,HDR本质上是用一个更广的范围表示颜色值,然后通过色调映射算法显示出来。Bloom则是通过高斯模糊的方法将场景中发光物体表现地像是在发光发热的样子。HDR的核心在于色调映射算法,这次我们只介绍了曝光度的算法,公式是:
image.png

高斯模糊的算法是对某一个片元进行水平和垂直的两次权重计算,重复几次,得到最终的结果。原理上非常简单,记住也非常容易,经常使用就可以了。

下一篇
目录
上一篇

参考资料

Bloom
Efficient Gaussian Blur with linear sampling
Bloom Post Process Effect
How to do good bloom for HDR rendering
HDR
What is tone mapping? How does it relate to HDR?

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

推荐阅读更多精彩内容