泛光
摊牌
- 表现光源的一种特效
- 使光源的颜色向周围发散
- 也叫做光晕
实现过程
- 将场景渲染到指定的帧缓冲,并从中提取出光源的部分
- 将光源的部分进行模糊操作
- 将模糊后的光源与第一次场景渲染的结果混合,得到最终的结果
- 需要注意的是:因为要提取光源的部分,为了这一步好处理,我们利用之前提到的HDR,将光源的颜色设置为大于1.0的值,其他物体的颜色都是正常的小于1.0的值;因此使用HDR的原因是为了方便提取光源
- 从上面的步骤看,实现这个效果并不复杂,关键是模糊算法
关键步骤
- 上面加粗那句话说看似直白其实另有玄机,翻译翻译就是:渲染场景一次,但得到两副纹理贴图;一副是场景的纹理贴图,另一副是只有光源的纹理贴图
- 要达到这个目的,需要get一个新的技巧——多渲染目标(Multiple Render Targets,MRT)
多渲染目标(MRT)
- 把他翻译翻译就是:在片段着色器中指定多个输出结果,做到一次渲染得到多个图片
- 需要注意的地方:
- 在客户端,已经有多个颜色缓冲附加到了当前绑定的帧缓冲对象上
- 并且显式的告诉OpenGL我们要渲染到多个颜色缓冲,否则OpenGL只会渲染到帧缓冲的第一个颜色附件,而忽略所有其他的。我们通过传递多个颜色附件的枚举来做这件事
- 在片段着色器中,通过指定一个布局location标识符,来控制结果写入到哪个颜色缓冲
- 这三条分别对应下面三段代码
for (GLuint 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 );
//...
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorBuffers[i], 0 );
}
GLuint attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 }; glDrawBuffers(2, attachments);
layout (location = 0) out vec4 FragColor;
layout (location = 1) out vec4 BrightColor;
void main()
{
//...
FragColor = vec4(lighting, 1.0);
float brightness = dot(FragColor.rgb, vec3(0.2126, 0.7152, 0.0722));
if(brightness > 1.0)
BrightColor = vec4(FragColor.rgb, 1.0);
}
- 第三段代码有必要说明一点:根据亮度提取光源颜色时,通常需要把颜色转化为灰度
- sRGB空间的灰阶转换公式为:
G
r
a
y
=
(
R
2.2
∗
0.2126
+
G
2.2
∗
0.7152
+
B
2.2
∗
0.0722
)
1
2.2
Gray = (R^{2.2} * 0.2126 + G^{2.2} * 0.7152 + B^{2.2} * 0.0722)^{\frac{1}{2.2}}
Gray=(R2.2∗0.2126+G2.2∗0.7152+B2.2∗0.0722)2.21 - 看过之前的“Gamma校正”应该可以知道,在当前情况下转换灰阶时不需要乘以2.2次幂,最后又没有乘以2.2分之一次幂是因为当前不需要进行gamma校正
高斯模糊
- 模糊算法的质量决定泛光效果的质量,所以这里选用一个相对既简单又优质的模糊算法——高斯模糊
- 高斯模糊基于高斯曲线,高斯曲线换个名字你肯定熟悉,那就是二年学的正太分布函数
- 他背后的数学原理复杂,这里忽略不计。。。
- 在接下来,我们将用到高斯曲线的特性是:x越靠近0,他的y值就越接近最大值;x离0越远,他的y值会快速减小
- 在用法上是:对高斯曲线采样,将采样值组成一个n x n二维高斯核,这里说的核在之前“帧缓冲”中有提到过,就是用来进行卷积计算的
- 由于高斯模糊的广泛应用,人们总结出了一种更快更节省资源的计算方法:两步高斯模糊,即用一个一维的高斯核,分别在水平和垂直方向上进行卷积计算
- 上图可以理解为水平方向上的卷积结果(实际不会这么明显,这个图夸张了)
- 需要注意的是:核中的权重之和要接近1.0,并且不能大于1.0
片段着色器
uniform sampler2D image;
uniform bool horizontal;
uniform float weight[5] = float[] (0.227, 0.194, 0.121, 0.054, 0.016);
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);
}
客户端程序
- 为了分别实现水平和垂直方向上的卷积计算,需要另外创建两个新的帧缓冲,并附加上颜色缓冲
- 第一次传入光源的贴图,即需要进行模糊处理的贴图
- 之后在水平方向和垂直方向上交替进行高斯模糊
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 );
//...
glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, pingpongBuffer[i], 0 );
}
//...
bool horizontal = true;
bool first = true;
shaderBlur.Use();
for (GLuint i = 0; i < 10; i++)
{
glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]);
shaderBlur.setValue("horizontal", horizontal);
glBindTexture( GL_TEXTURE_2D, first ? lightBuffer : pingpongBuffers[!horizontal] );
Render();
horizontal = !horizontal;
if (first)
first = false;
}
混合
- 将光源的模糊贴图与原始的场景混合
- 在混合时需要注意:由于前面为了方便提取光源颜色,使用了HDR,所以不能忘了色调映射。色调映射之后为了显示效果更好,还需要进行gamma校正
uniform sampler2D scene;
uniform sampler2D bloomBlur;
uniform float exposure;
void main()
{
vec3 hdrColor = texture(scene, TexCoords).rgb;
vec3 bloomColor = texture(bloomBlur, TexCoords).rgb;
hdrColor += bloomColor;
vec3 result = vec3(1.0) - exp(-hdrColor * exposure);//色调映射
const float gamma = 2.2;
result = pow(result, vec3(1.0 / gamma));
FragColor = vec4(result, 1.0f);
}
- 效果
- 下面的图这是用模版缓冲和高斯模糊做成的效果