【OpenGL】LearnOpenGL学习笔记27 - HDR、Bloom
上接:https://blog.csdn.net/weixin_44506615/article/details/151930651
完整代码:https://gitee.com/Duo1J/learn-open-gl | https://github.com/Duo1J/LearnOpenGL
一、高动态范围 (High Dynamic Range) (HDR)
一般来说当存储在帧缓冲中时,亮度和颜色的值是默认被限制在 [0, 1] 之间的
如果多个亮光源的叠加使其总和超过了1.0的话,这些片段仍然会被限制在1.0内
这会导致它们都呈现为白色,场景混成一片难以分辨
Main.cpp
for (int i = 0; i < 4; i++)
{// 加亮第一盏灯if (i == 0){shader.SetVec3("pointLight[" + std::to_string(i) + "].ambient", glm::vec3(0.05f));shader.SetVec3("pointLight[" + std::to_string(i) + "].diffuse", glm::vec3(10.0f));shader.SetVec3("pointLight[" + std::to_string(i) + "].specular", glm::vec3(10.0f));}else{shader.SetVec3("pointLight[" + std::to_string(i) + "].ambient", glm::vec3(0.05f));shader.SetVec3("pointLight[" + std::to_string(i) + "].diffuse", glm::vec3(0.8f));shader.SetVec3("pointLight[" + std::to_string(i) + "].specular", glm::vec3(1.0f));}// ...
}
可以看到我们的背包面板非常的白,看不清细节
现在我们需要引入一个更大的,超过 [0, 1] 的颜色范围空间,称作 HDR (High Dynamic Range, 高动态范围)
在渲染过程中,我们会使用超过 [0, 1] 的颜色值进行计算,最后我们会通过 色调映射 (Tone Mapping) 的方式,将 HDR 映射回 LDR (Low Dynamic Range,低动态范围) 空间用以在屏幕上显示
浮点帧缓冲
可目前的帧缓冲并不能支持这一点,我们需要使用到浮点帧缓冲
要创建浮点帧缓冲,我们只需要改变 glTexImage2D 的参数即可
Main.cpp
// 第三个参数GL_RGB改为了GL_RGB16F
// 倒数第二个参数GL_UNSIGNED_BYTE 改为了GL_FLOAT
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, screenWidth, screenHeight, 0, GL_RGB, GL_FLOAT, NULL);
色调映射
在渲染计算完之后,我们在屏幕后处理中进行色调映射,将HDR映射回LDR
一个最简单的色调映射算法为 Reinhard 色调映射,其思想使用颜色值除以其自身加上一个常量来限制颜色值在 [0, 1] 范围内
ScreenFragment.glsl
vec4 ToneMapping_ReinHard(vec4 hdrColor)
{vec3 mapped = hdrColor.rgb / (hdrColor.rgb + vec3(1.0));return vec4(mapped, hdrColor.a);
}void main()
{ vec4 color = vec4(vec3(texture(screenTexture, TexCoords)), 1.0);color = GammaCorrection(ToneMapping_ReinHard(color));FragColor = color;
}
另外一个色调映射的应用是 曝光(Exposure) 参数的使用,如果有一个场景需要日夜交替,我们会在白天使用低曝光,在夜间使用高曝光来适应不同的光照强度
ScreenFragment.glsl
vec4 ToneMapping_Exposure(vec4 hdrColor)
{vec3 mapped = vec3(1) - exp(-hdrColor.rgb * exposure);return vec4(mapped, hdrColor.a);
}
这里 exp(x)
是以e为底x的指数,即 e^x
,这里 exp(-hdrColor.rgb * exposure)
最终会落在 [0, 1] 之间
可以增加按键O和P来运行时改变曝光度,方便查看效果
Main.cpp
void ProcessKeyboardInput(GLFWwindow* window)
{// ...// 改变曝光度static bool pressedExposure = false;if (glfwGetKey(window, GLFW_KEY_O)){pressedExposure = true;exposure -= 0.1f;}else if (glfwGetKey(window, GLFW_KEY_P)){pressedExposure = true;exposure += 0.1f;}else{pressedExposure = false;}// ...
}
// ...
screenShader.SetFloat("exposure", exposure);
二、泛光 (Bloom)
如果我们想要表达高亮的光源,最常用到的一个技巧就是 泛光 (Bloom) ,它可以表现出光源发出光芒,光芒向四周发散的效果,如下图所示 (图片来自于Epic Games)
泛光的实现思路如下:
- 设定一个亮度阈值,提取出超过阈值的片段
- 将提取出来的图像进行模糊处理
- 将模糊后的纹理叠加回原图像中
如下图所示
提取超过亮度阈值的片段
我们可以新创建一个FBO brightnessFBO
,在屏幕后处理前先采样原图像,提取高亮后写入到一张单独的纹理brightnessBufferTex
中
Main.cpp
// 亮度提取阈值
float brightnessThreshold = 1;// [main]
// 提取高亮FBO,高亮片段写入到brightnessBufferTex
unsigned int brightnessFBO;
glGenFramebuffers(1, &brightnessFBO);
glBindFramebuffer(GL_FRAMEBUFFER, brightnessFBO);
glGenTextures(1, &brightnessBufferTex);
glBindTexture(GL_TEXTURE_2D, brightnessBufferTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, screenWidth, screenHeight, 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);
glBindTexture(GL_TEXTURE_2D, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, brightnessBufferTex, 0
);// 提取高亮Shader
Shader brightnessShader("Shader/ScreenVertex.glsl", "Shader/BrightnessFragment.glsl");// [主循环]
// 提取高亮
glBindFramebuffer(GL_FRAMEBUFFER, brightnessFBO);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);brightnessShader.Use();
brightnessShader.SetInt("screenTexture", 0);
brightnessShader.SetFloat("brightnessThreshold", brightnessThreshold);glBindVertexArray(screenQuadVAO);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, framebufferTex);
glDrawArrays(GL_TRIANGLES, 0, 6);
BrightnessFragment.glsl 新建
#version 330 coreout vec4 FragColor;in vec2 TexCoords;uniform sampler2D screenTexture;
uniform float brightnessThreshold;void main()
{ vec4 color = vec4(vec3(texture(screenTexture, TexCoords)), 1.0);// 输出超过阈值亮度的片段float brightness = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));if (brightness > brightnessThreshold)FragColor = vec4(color.rgb, 1);elseFragColor = vec4(0, 0, 0, 1);
}
直接在后处理中输出 brightnessBufferTex
高斯模糊
高斯模糊是基于高斯曲线的一种模糊算法,我们不在这里讨论具体细节
我们只需要知道它是通过之前在后处理时用过的卷积核方式进行计算,并且它有一个特性是它允许我们把这个二维方程分解成水平和竖直权重方程,我们可以先用水平权重在纹理上进行水平模糊,然后再在已改变的纹理上进行垂直模糊,这会得到直接进行卷积计算一样的效果
对此,我们会创建两个FBO,暂且先称之为A和B
首先我们会将 brightnessBufferTex
采样后进行水平模糊并写入A的纹理附件,然后进行垂直模糊后写入B的纹理附件,接着再进行水平模糊写入A
以此来进行多次迭代
Main.cpp
// [main]
unsigned int pingpongFBO[2];
unsigned int pingpongBuffer[2];
glGenFramebuffers(2, pingpongFBO);
glGenTextures(2, pingpongBuffer);
for (unsigned int i = 0; i < 2; i++)
{glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[i]);glBindTexture(GL_TEXTURE_2D, pingpongBuffer[i]);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, screenWidth, screenHeight, 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);
}// 高斯模糊Shader
Shader blurShader("Shader/ScreenVertex.glsl", "Shader/BlurFragment.glsl");// [主循环]
// 高斯模糊
bool horizontal = true, first_iteration = true;
unsigned int amount = 10;
blurShader.Use();
for (unsigned int i = 0; i < amount; i++)
{glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]);blurShader.SetInt("horizontal", horizontal);glBindTexture(GL_TEXTURE_2D, first_iteration ? brightnessBufferTex : pingpongBuffer[!horizontal]);glBindVertexArray(screenQuadVAO);glDrawArrays(GL_TRIANGLES, 0, 6);horizontal = !horizontal;first_iteration = false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
BlurFragment.glsl 新建
#version 330 coreout 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);
}
直接在后处理中输出 pingpongBuffer[!horizontal]
混合
最后将原图像与高斯模糊后的高亮图像混合即可
Main.cpp
// [主循环]
// 后处理
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);screenShader.Use();
screenShader.SetInt("screenTexture", 0);
// 绑定纹理槽位1
screenShader.SetInt("bloomBlur", 1);
screenShader.SetFloat("exposure", exposure);
glBindVertexArray(screenQuadVAO);
glDisable(GL_DEPTH_TEST);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, framebufferTex);
glActiveTexture(GL_TEXTURE1);
// 传入模糊后的图像
glBindTexture(GL_TEXTURE_2D, pingpongBuffer[!horizontal]);
glDrawArrays(GL_TRIANGLES, 0, 6);
glEnable(GL_DEPTH_TEST);
ScreenFragment.glsl
void main()
{ vec4 color = vec4(vec3(texture(screenTexture, TexCoords)), 1.0);// 采样模糊图像vec3 bloomColor = texture(bloomBlur, TexCoords).rgb;// 混合color += vec4(bloomColor, 1);// 进行色调映射和gamma矫正color = GammaCorrection(ToneMapping_Exposure(color));FragColor = color;
}
编译运行,顺利的话可以看见以下图像
有一种背包在发光的效果
同样,我们可以增加按键操作来实时调整亮度提取阈值,观察效果
// 改变亮度提取阈值
static bool pressedBrightnessThreshold = false;
if (glfwGetKey(window, GLFW_KEY_U))
{pressedBrightnessThreshold = true;brightnessThreshold -= 0.1f;
}
else if (glfwGetKey(window, GLFW_KEY_I))
{pressedBrightnessThreshold = true;brightnessThreshold += 0.1f;
}
else
{pressedBrightnessThreshold = false;
}
完整代码可在顶部Git仓库找到