LearnOpenGL——HDR、Bloom学习笔记
- HDR
- 一、基本概念
- 二、浮点帧缓冲 Floating Point Framebuffer
- 三、色调映射 Tone Mapping
- Reinhard色调映射
- 曝光色调映射
- Bloom
- 一、提取亮色
- 二、高斯模糊
- 三、将两个纹理进行混合
HDR
一、基本概念
显示器被限制只能显示0.0-1.0的颜色,但是在光照方程中没有这个限制,通过将颜色超过1.0范围,我们将其称为HDR(High Dynamic Range, 高动态范围)。可以增加颜色表现力,亮的更亮,暗的更暗。
我们允许用HDR渲染来获取大范围的黑暗与明亮的场景细节,最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。将HDR转换到LDR的过程叫做色调映射(Tone Mapping)
二、浮点帧缓冲 Floating Point Framebuffer
我们想要将颜色信息存储的范围超过0.0-1.0范围。如果我们使用GL_RGB这样的格式创建图像时,OpenGL会自动将颜色值在存储到帧缓冲之前就约束到0.0-1.0之间。如果我们使用了浮点帧缓冲—— GL_RGB16F, GL_RGBA16F, GL_RGB32F 或者GL_RGBA32F,此时浮点帧缓冲可以存储超过0.0-1.0范围的浮点值,非常适合HDR渲染
可以直接在创建图像纹理时,使用 GL_RGB16F
glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH,
SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
默认的帧缓冲默认一个颜色分量只占用8位,当使用32位每颜色分量的浮点帧缓冲时,就相当于要4倍的内存,所以除非非常高的精度,一般16够用
三、色调映射 Tone Mapping
色调映射(Tone Mapping)是一个损失很小的转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程,通常会伴有特定的风格的色平衡(Stylistic Color Balance)。其中一个最简单的色调映射算法就是Reinhard色调映射。
Reinhard色调映射
这个算法平均地将所有亮度值分散到LDR上。我们将此算法应用到之前的片元着色器上,并增加一个Gamma矫正
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
// Reinhard色调映射
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
//Gamma矫正
mapped = pow(mapped, vec3(1.0/gamma));
color = vec4(mapped,1.0);
}
曝光色调映射
我们还可以引入曝光参数,如果我们有一个场景要展现日夜交替,我们当然会在白天使用低曝光,在夜间使用高曝光,就像人眼调节方式一样。
uniform float exposure;
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer,TexCoords).rgb;
// 曝光色调映射
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
//Gamma矫正
mapped = pow(mapped,vec3(1.0/gamma));
color = vec4(mapped,1.0);
}
高曝光值会使隧道的黑暗部分显示更多的细节,然而低曝光值会显著减少黑暗区域的细节,但允许我们看到更多明亮区域的细节。
Bloom
光流,或发光效果,是通过一种叫做泛光(Bloom)的后期处理效果来实现的。
实现Bloom效果的基本步骤:提取HDR颜色缓冲以及图中明亮的部分,然后对明亮的部分进行模糊处理,然后将结果添加到原始HDR场景图像上面
一、提取亮色
第一步我们要从渲染出来的场景中提取两张图片。如果渲染两次场景,每次使用不同的着色器渲染到不同的帧缓冲中,这样开销很大。我们可以使用MRT(Multiple Render Targets,多渲染目标),这样就可以指定多个片元着色器的输出。我么可以通过MRT,在单独的一个渲染pass中提取两张图片。
在片元着色器的输出前,我们指定一个布局location标识符,这样我们便可控制一个片元着色器写入到哪个颜色缓冲:
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;
有多个颜色缓冲附加到了当前绑定的帧缓冲对象上时才可以使用多个片元着色器输出。我们在进行颜色附件的附加时使用 GL_COLOR_ATTACHMENT0 和 GL_COLOR_ATTACHMENT1
GLuint hdrFBO;
glGenFrameBuffer(1, &hdrFBO);
glBindFrameBuffer(GL_FRAMEBUFFER, hdrFBO);
GLuint colorBuffers[2];
glGenTextures(2, colorBuffers);
for(GLuint i = 0; i < 2; i++)
{
glBindTexture(GL_TEXTURE_2D, colorBuffer[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);
}
然后告知OpenGL我们正在通过glDrawBuffers渲染到多个颜色缓冲
GLuint attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glDrawBuffers(2, attachments);
在片元着色器中,在计算完光照后,然后传递给第一个变量FragColor,然后使用当前储存在FragColor的东西来决定它的亮度是否超过了阈值。我们通过FragColor.rgb, vec3(0.2126, 0.7152, 0.0722)的点乘来计算光照亮度
#version 330 core
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;
[...]
void main()
{
[...] // first do normal lighting calculations and output results
FragColor = vec4(lighting, 1.0f);
// Check whether fragment output is higher than threshold, if so output as brightness color
float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
if(brightness > 1.0)
BrightColor = vec4(FragColor.rgb, 1.0);
}
二、高斯模糊
高斯模糊基于高斯曲线,高斯曲线通常被描述为一个钟形曲线,中间的值达到最大化,随着距离的增加,两边的值不断减少。
以一个32×32的模糊kernel为例,高斯模糊可以分成两个部分:水平模糊和竖直模糊(32次+32次),这样可以避免二维一次性采样(32次×32次)的采样数过大。
这样的话需要我们对图像进行采样两次,帧缓冲是最好的办法。
高斯模糊的片元着色器
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D image;
uniform bool horizontal;
uniform float weight[5] = float[] (0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
void main()
{
vec2 tex_offset = 1.0/textureSize(image,0);
vec3 result = texture(image,TexCoords).rgb * weight[0];
if(horizontal)
{
for(int i = 1; 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 = 1; 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);
}
然后我们为图像创建两个基本帧缓冲,每个只有一个颜色缓冲纹理
GLuint pingpongFBO[2];
GLuint pingpongBuffer[2];
glGenFramebuffers(2, pingpongFBO);
glGenTextures(2, pingpongBuffer);
for (GLuint i = 0; i < 2; i++)
{
glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]);
glBindTexture(GL_TEXTURE_2D, pingpongBuffer[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, pingpongBuffer[i], 0
);
}
得到一个HDR纹理后,我们用提取出来的亮区纹理填充一个帧缓冲,然后对其模糊处理10次(5次垂直5次水平):
GLboolean horizontal = true, first_iteration = true;
GLuint amount = 10;
shaderBlur.Use();
for (GLuint i = 0; i < amount; i++)
{
glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]);
glUniform1i(glGetUniformLocation(shaderBlur.Program, "horizontal"), horizontal);
glBindTexture(
GL_TEXTURE_2D, first_iteration ? colorBuffers[1] : pingpongBuffers[!horizontal]
);
RenderQuad();
horizontal = !horizontal;
if (first_iteration)
first_iteration = false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
- pingpongFBO是一个包含两个FBO的数组,一个用于水平模糊,一个用于垂直模糊。horizontal变量决定了使用哪一个FBO
- 这行代码绑定了一个2D纹理,它是将要被模糊的图像。在第一次迭代时,它绑定的是colorBuffers[1](这是原始图像的纹理),在随后的迭代中,它绑定的是另一个FBO的颜色缓冲区(pingpongBuffers[!horizontal]),这样就可以在之前模糊的结果上继续模糊。
三、将两个纹理进行混合
最终的像素着色器,要注意的是我们要在应用色调映射之前添加泛光效果。这样添加的亮区的泛光,也会柔和转换为LDR,光照效果相对会更好。
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D scene;
uniform sampler2D bloomBlur;
uniform float exposure;
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(scene, TexCoords).rgb;
vec3 bloomColor = texture(bloomBlur, TexCoords).rgb;
hdrColor += bloomColor; // additive blending
// tone mapping
vec3 result = vec3(1.0) - exp(-hdrColor * exposure);
// also gamma correct while we're at it
result = pow(result, vec3(1.0 / gamma));
FragColor = vec4(result, 1.0f);
}