HDR(Hight Dynamic Range,高动态范围):
一般来讲,在我们的屏幕上显示的一个帧(Frame),在OPENGL中是以**帧缓冲(Framebuffer)**的形式存储的,默认情况下它的画面的亮度和颜色的值是被限制在0.0到1.0之间的一个值。
这导致倘若我们在着色器的使用过程中有超过的了1.0的亮度范围,(比如:我们正常使用的点光源亮度为1.0,若有多个点光源叠加照射在物体上,就有可能超过1.0,就会导致一部分片段的光照强度会被约束在1.0,造成画面失真。)
而我们需要做的就是代替系统,将当前的亮度合理的分配至0.0-1.0的屏幕亮度范围,即可使画面恢复正常的显示。
这个亮度分配通常会在片段着色器内进行。但是由于目前我们的渲染过程中,使用了很多个着色器。
如果我们只修改了一个着色器内的亮度分配,其他的着色器却没有更改,就会导致只有一部分的画面显示正常。
所以最懒的方式是将整个画面作为一个纹理导入一个专用的着色器中进行处理,再将画面输出。
那么如何将画面导出为一个纹理呢?
其实很简单,我们的屏幕的每一帧的画面的存储方式就是一个纹理,只不过它处理完成后直接被输出到屏幕上了。
那么我们只需要创建一个帧,这个帧拥有一个可以存储更大颜色范围的纹理,让最初的绘制不再绘制在屏幕上,而是绘制在这个帧的纹理上。
要进行画面亮度的重分配,首先我们需要将原本会显示在 默认帧(屏幕) 上的画面,用一个新的帧作为中间人存储,这个帧应该含有一个颜色范围更大的纹理。
随后将这个纹理,作为默认帧的输入,在绘制过程中调节纹理的亮度,并绘制在默认帧(屏幕) 上即可。
由于我们最后在默认帧上绘制时,只需要绘制一个整幅的经过亮度调节的图像,所以绘制一个带有该纹理的屏幕大小的四边形就可以。
我们预计将会得到如下的结果。
我们之前一直都是在默认帧渲染,它的默认光照强度是0-1.0。
glBindFramebuffer(GL_FRAMEBUFFER, 0);
我们需要创建一个新的帧,它的纹理应该可以存储那些大于1.0的浮点数据,也就是我们需要它的数据类型可以更大。
像其他创建opengl顶点数组或纹理一样,我们首先需要向系统获取一个可用的该类型的索引,这类索引通常为一个GLuint类型的值,所以我们新建一个GLuint类型的变量fbo(frame buffer object) 来存储。
//创建一个新的帧
GLuint fbo;
//注意传的是地址
glGenFramebuffers(1, &fbo);
随后由于我们要为这个帧进行一些操作,所以需要告诉系统,当前的帧要切换为我们新建的fbo先生。
这个过程我们称为绑定,在OPENGL中大部分的类型的操作都需要在绑定后进行操作,比如顶点数组(vao),顶点缓冲(vbo),纹理,还有接下来将会介绍的渲染缓冲(Render buffer)。
虽然我们可以在对它进行操作的时候再去绑定。但是,我们接下来肯定要对它进行操作的,所以先绑上也没什么坏处,对吧?
//绑定当前的帧缓冲为索引号fbo的帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
一个可用的帧,必须包含一个用于输出的纹理缓冲区,用来存放渲染生成的图像。我们之前使用的默认帧(默认索引为:0) 的纹理缓冲区就是我们的屏幕。
屏幕上印刷的每一帧实际上就是一张2D的图片。
而一个帧还可以包含,深度缓冲和模板缓冲用于相应的需求。
在HDR中我们只需对图片的亮度进行处理,所以只要在新建的帧上挂载纹理缓冲即可。
同样,我们第一步要获取一个可用的纹理索引,随后绑定纹理。
GLuint texture;
glGenTextures(texNum, &texture);
glBindTextures(1,&texture);
在纹理绑定完成后,我们要为其申请空间,并设置纹理的属性。(请确保在对类似的opengl的对象操作前,你已经绑定过他们。)
再申请空间的过程中,我们会将纹理的数据属性设置为16位的浮点数。
//(纹理类型,多级渐远纹理级别,将图像存储为何种格式,纹理的宽度,纹理的高度,0(历史遗留),源图的加载格式,源图的数据地址)
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);
现在我们已经创建好了一个浮点属性的纹理,接下来需要将它绑定到我们创建的帧上,这样帧才能具有可以使用的浮点缓冲区。
//设定其纹理(target:帧缓冲类型,attachment:附件的类型,textarget:纹理的类型,texture:纹理对象,Mipmap level:渐远纹理等级)
glFramebufferTexture2D(
GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0 , GL_TEXTURE_2D, texture, 0);
参数含义分别为
我们注意到,颜色附件为零号,这意味着一个帧可能拥有多个颜色附件,也就是说我们的帧可以在一次渲染中,将不同的数据分别输出到两种纹理上。这个技巧将在稍后的Bloom中使用到。
接下来由于我们的绘图中会使用到深度缓冲,所以还需要为帧添加一个深度缓冲区。方法类似。
GLuint rboDepthandStencil;
//注意导入的是地址
glGenRenderbuffers(1, &rboDepthandStencil);
//绑定类型为渲染缓冲,
glBindRenderbuffer(GL_RENDERBUFFER, rboDepthandStencil);
//为其申请空间 类型为24位的深度缓冲8位模板缓冲的封装形式
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, screenWidth, screenHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepthandStencil)
到此我们的的帧已经创建完成,在使用它之前,需要检查他的完整性,确保我们操作的正确。
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
不要忘记对其操作完成后将它解绑哦。
完成以上的帧缓冲,我们就可以开始在我们的渲染循环中进行猛如虎的操作了。
使用新的帧缓冲渲染与我们之前使用默认帧缓冲进行渲染,并无太大区别。只是渲染前绑定的帧变为新建的浮点帧,其他步骤基本相同。
整体代码大致流程如下:
while (!glfwWindowShouldClose(window))
{
/*---------帧缓冲------------*/
//绑定帧缓冲,必有
glBindFramebuffer(GL_FRAMEBUFFER,fbo);
//清除屏幕颜色缓存,一定不要忘
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//正常渲染
//[...]
/*------完成,此时图像应已经存入帧绑定的纹理中--------*/
//绑定屏幕帧,必有
glBindFramebuffer(GL_FRAMEBUFFER, 0);
//清屏,不要忘记
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//激活HDR的shader
hdrShader.use();
//绑定刚刚的帧内渲染生成的纹理
glBindTexture(GL_TEXTURE_2D, texture);
//绘制一个四边形
RenderQuad();
/*-----------------------------------------*/
//常规操作,常规操作,用了不闪腰
glBindVertexArray(0);
glfwSwapBuffers(window);
}
接下来为了正确的渲染,我们需要配置hdrShader的着色器。
由于HDR是在顶点着色器内进行,所以我们只需要使用一个最简单的顶点着色器。
#version 330 core
out vec4 color;
in vec2 TexCoords;
uniform sampler2D hdrBuffer;
void main()
{
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
color = vec4(hdrColor, 1.0);
}
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
hdrColor = pow(hdrColor, vec3(gamma));
// Reinhard色调映射
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
// Gamma校正
mapped = pow(mapped, vec3(1.0 / gamma));
color = vec4(mapped, 1.0);
}
#version 330 core
out vec4 color;
in vec2 TexCoords;
uniform sampler2D scene;
uniform float exposure;
uniform bool hdr;
void main()
{
const float gamma = 2.2;
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
hdrColor = pow(hdrColor, vec3(gamma));
if(hdr){
// 曝光色调映射
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
// Gamma校正
mapped = pow(mapped, vec3(1.0 / gamma));
color = vec4(mapped, 1.0);
}
else{
color = vec4(hdrColor,1.0);
}
}
[…]
此函数将会画出一个满屏的四边形
//plus quad
GLuint quadVAO = 0;
GLuint quadVBO;
void RenderQuad()
{
if (quadVAO == 0)
{
GLfloat quadVertices[] = {
// Positions // Texture Coords
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
};
// Setup plane VAO
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), &quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
}
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
}
泛光是指物体边缘由于倍光照射时产生的光晕,下面两张图分别为开启光晕效果和未开启光晕效果的显示图。
可以看到,开启光晕效果的物体边缘有了更柔和的光线变化。那么这是怎么做到的呢?
首先我们需要提取场景中的亮色,
随后将亮色,进行高斯模糊处理,使其具有光晕的效果。
再将亮色与未提取亮色前的结果混合就可以得到我们先要的结果。
最关键的一步就是需要提取亮色。
先创建一个帧,将我们要绘制的所有场景绘制在上面。
随后再创建一个拥有两个输出纹理帧,读入上一个帧的纹理图像。使用一个可以提取画面亮度的着色器。
由于只是处理纹理,同样只需要更改片段着色器,顶点着色器用上面给出的最简单的就可以。
#version 330 core
in vec2 TexCoords;
layout(location = 0) out vec4 FragColor;
layout(location = 1) out vec4 BrightColor;
uniform sampler2D source;
void main()
{
FragColor=vec4(texture(source,TexCoords).rgb,1.0f);
float brightness = dot(FragColor.rgb,vec3(0.2126, 0.7152, 0.0722));
if(brightness >0.9) //要为0.9 否则光源上不去
BrightColor= vec4(FragColor.rgb,1.0f);
else{
//网上没有
BrightColor= vec4(0,0,0,1.0f);
}
}
注意最后的的判断亮度,要设置为0.9,否则若是亮度为1.0的光源会被忽略。而正常情况下,我们也希望光源也有光晕。
此外需要将BrightColor非亮光的情况的值设为黑色,否则将可能被赋予错误的值。
#version 330 core
in vec2 TexCoords;
layout(location = 0) out vec4 FragColor;
layout(location = 1) out vec4 BrightColor;
uniform sampler2D source;
void main()
{
FragColor=vec4(texture(source,TexCoords).rgb,1.0f);
float brightness = dot(FragColor.rgb,vec3(0.2126, 0.7152, 0.0722));
if(brightness >0.9) //要为0.9 否则光源上不去
BrightColor= vec4(FragColor.rgb,1.0f);
else{
//网上没有
BrightColor= vec4(0,0,0,1.0f);
}
}
//Frame.h
#ifndef FRAME_SML
#define FRAME_SML
#include
#include
#include
using namespace std;
struct Frame {
public:
GLuint fbo;
GLuint *textures;
GLuint texNum;
GLuint rboDepthandStencil;
Frame(int screenWidth, int screenHeight, int Num = 1):texNum(Num){
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
textures = new GLuint[texNum];
glGenTextures(texNum, textures);
GLuint *attachments = new GLuint[texNum];
for (int i = 0; i < texNum; i++) {
attachments[i] = GL_COLOR_ATTACHMENT0 + i;
glBindTexture(GL_TEXTURE_2D, textures[i]);
//生成帧缓冲的纹理
//数据参数为NULL(只分配内存,不填充)
glTexImage2D(
GL_TEXTURE_2D, 0, GL_RGB16F, screenWidth, screenHeight, 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_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);
//设定其纹理(target:帧缓冲类型,attachment:附件的类型,textarget:纹理的类型,texture:纹理对象,Mipmap level:渐远纹理等级)
glFramebufferTexture2D(
GL_FRAMEBUFFER, attachments[i], GL_TEXTURE_2D, textures[i], 0);
}
glDrawBuffers(texNum, attachments);
delete []attachments;
//产生深度缓冲区
glGenRenderbuffers(1, &rboDepthandStencil);
//将生成的缓冲区与对象绑定
glBindRenderbuffer(GL_RENDERBUFFER, rboDepthandStencil);
//用一个单一的渲染缓冲对象,缓冲深度测试和模板测试 深度附件
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, screenWidth, screenHeight);
//将纹理缓冲和深度缓冲都绑定到帧缓冲上
//设定深度缓冲
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rboDepthandStencil);
//检查帧缓冲附件是否完整
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
cout << "ERROR::FRAMEBUFFER:: Framebuffer is not complete!" << endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
~Frame() {
glDeleteRenderbuffers(1, &rboDepthandStencil);
glDeleteTextures(texNum, textures);
delete[] textures;
glDeleteFramebuffers(1, &fbo);
}
};
#endif // !FRAME_SML