Gamemaker 笔记 - Gaussian Blur & Bloom effect
- 在其它设备中阅读本文章
普通模糊
一个像素的颜色为周围矩形的像素颜色取平均。
评价:很丑
高斯分布
高斯分布,也即正态分布 (Normal Distribution) 。按照高斯分布对颜色进行加权取平均可以得到更平滑的模糊结果。
下方是一维及二维高斯分布的公式:
$$f_1(x) = \frac{1}{\sqrt{2\pi}\sigma}\exp \left(-\frac{(x-\mu)^2}{2\sigma^2}\right)$$
$${\displaystyle f(x,y)={\frac {1}{2\pi \sigma _{X}\sigma _{Y}{\sqrt {1-\rho ^{2}}}}}\exp \left(-{\frac {1}{2(1-\rho ^{2})}}\left[\left({\frac {x-\mu _{X}}{\sigma _{X}}}\right)^{2}-2\rho \left({\frac {x-\mu _{X}}{\sigma _{X}}}\right)\left({\frac {y-\mu _{Y}}{\sigma _{Y}}}\right)+\left({\frac {y-\mu _{Y}}{\sigma _{Y}}}\right)^{2}\right]\right)}$$
其中 $\rho$ 影响二维高斯分布的形状(椭圆 / 圆,长轴在 X 轴上还是 Y 轴上),$\sigma$ 影响曲线的宽度及平缓度。方便起见可以令 $\sigma_1 = \sigma_2 = \sigma$ 且 $\rho = \mu = 0$,则第二个公式可以简化为:
$$f_2(x, y) = \frac{1}{2\pi\sigma^2}\exp \left(-\frac{x^2+y^2}{2\sigma^2}\right)$$
简化后的函数通过观察可以发现一个有趣的结论:
$$f_2(x, y) = f_1(x)f_1(y)$$
这为我们之后优化高斯模糊提供了方便。
高斯模糊
1-Pass 高斯模糊
1-Pass,顾名思义,将 sprite 或 surface 传递给 shader 处理 仅一次 就得到结果。
下面是 1-Pass Gaussian Blur Fragment Shader 的 GLSL 代码示例:
#define pow2(x) (x * x)
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec3 size; // texel_width, texel_height, blur_range
uniform vec4 uvs;
uniform float sigma;
const highp float pi = 3.1415926535 * 2.;
int samples;
// 以二维高斯分布作为颜色的权值
float gaussian(vec2 i) {
return 1.0 / (pi * pow2(sigma)) * exp(-((pow2(i.x) + pow2(i.y)) / (2.0 * pow2(sigma))));
}
highp vec3 blur(vec2 uv) {
highp vec3 col = vec3(0.0);
float accum = 0.0;
float weight;
vec2 offset;
// 对矩形范围内的所有颜色求加权平均
for (int x = -samples / 2; x < samples / 2; ++x) {
for (int y = -samples / 2; y < samples / 2; ++y) {
offset = vec2(x, y);
weight = gaussian(offset);
col += texture2D( gm_BaseTexture, clamp(uv + offset * size.xy, uvs.xy, uvs.zw)).rgb * weight;
accum += weight;
}
}
return col / accum;
}
void main()
{
samples = int(size.z)+2;
highp vec4 color;
color.rgb = blur(v_vTexcoord);
color.a = 1.0;
gl_FragColor = color * v_vColour;
}
关于上方加 highp
的原因在于 Gamemaker 的 shader 对不同设备的 float 精度处理会有变化,于是就能看到这样会得到奇怪的块状模糊... 加上 highp
(high precision) 可以提高浮点数的精度来尝试修复这个问题。
这样处理的好处就是 只用传递一遍 ,故在一些特殊的场合之下可以使用这种方法,比如,不方便使用 surface 的场合,或者绘制较小的 sprite 等。
但问题在于这个算法是 $n^2$ 的,效率奇低。考虑上方 blur_range=20
的情况,则需要对每一个像素做 1600 次运算,以目前通常的屏幕大小 1080p 按每秒 60 帧计算,一秒运算量达到 $1920\times 1080\times 1600\times 60= 199065600000$ 次,效率实在太低。
故在大多数情况下为了优化运算量,使用的是 2-Pass 版本的高斯模糊。
2-Pass 高斯模糊
优化 1-Pass 高斯模糊的思想在于在上方得到的一个关于高斯分布的有趣结论。
我们可以将 surface 进行一次横向的模糊得到:
$$\frac{\sum_x Col(x, y)f_1(x)}{\sum_x f_1(x)}$$
我们将结果保存到一个临时的 surface 中,并将该临时 surface 再进行一次纵向的模糊得到:
$$\frac{\sum_y (\frac{\sum_x Col(x, y)f_1(x)}{\sum_x f_1(x)}) f_1(y)}{\sum_y f_1(y)} = \frac{\sum_{x,y} Col(x, y)f_2(x, y)}{\sum_{x,y} f_2(x, y)}$$
也即我们最终想要的结果,与 1-Pass 得到的结果相同。从上也可以看出,横向与纵向的高斯模糊顺序并不重要。
以对 application_surface 模糊为例,绘制顺序为:
shader_vertical shader_horizontal
application_surface -----------------> surf_ping -------------------> screen
其中 surf_ping
为我们所申请的缓冲 surface,用于存储中途一次 pass 的结果。
而事实上,纵向与横向模糊的 shader 可以用统一的一个 shader 来实现,我们只需要向目标 shader 传递一个 blur_vector 代表模糊的方向即可。
下方是 2-Pass Gaussian Blur Fragment Shader 的 GLSL 示例代码:
#define pow2(x) (x * x)
varying vec2 v_vTexcoord;
varying vec4 v_vColour;
uniform vec4 size;//width,height,radius,sigma
uniform vec2 blur_vector;
const float pi = 3.1415926535 * 2.;
int samples;
float sigma;
float gaussian(float i) {
return 1.0 / sqrt(pi * pow2(sigma)) * exp(-pow2(i) / (2.0 * pow2(sigma)));
}
vec3 blur(vec2 uv, vec2 scale) {
vec3 col = vec3(0.0);
float accum = 0.0;
float weight;
float offset;
weight = gaussian(0.);
col += texture2D( gm_BaseTexture, uv).rgb * weight;
accum = weight;
for (int x = 1; x < samples / 2; ++x) {
offset = float(x);
weight = gaussian(offset);
col += texture2D( gm_BaseTexture, uv + scale * blur_vector * offset).rgb * weight;
col += texture2D( gm_BaseTexture, uv - scale * blur_vector * offset).rgb * weight;
accum += weight * 2.;
}
return col / accum;
}
void main()
{
samples = int(size.z)+2;
sigma = size.w;
vec4 color;
vec2 ps=vec2(1.0)/size.xy;
color.rgb=blur(v_vTexcoord, ps);
color.a=1.0;
gl_FragColor = color * v_vColour;
}
假设我们创建了一个 object,并在 object 存在期间关闭 application_surface 的自动绘制,在 Draw GUI 事件中对后处理过后的 application_surface 进行绘制,则绘制代码可概括如下:
shader_set(shd_blur);
surface_set_target(surf_ping);
shader_set_uniform_f(u_blur_vector, 0, 1);
// ....
draw_surface(application_surface, 0, 0);
surface_reset_target();
shader_set_uniform_f(u_blur_vector, 1, 0);
draw_surface(surf_ping);
shader_reset();
Bloom effect 泛光效果
用比较低的代价和简单的代码实现较好的画面效果。
实现方式:将画面亮度较高的地方提取出来,并使用 blur shader 将提取的部分进行模糊,最后用叠加的混合方式涂到被处理的表面上以实现泛光效果。
(咕咕咕)
本文链接:https://pst.iorinn.moe/archives/gmshd-blur.html
许可: https://pst.iorinn.moe/license.html若无特别说明,博客内的文章默认将采用 CC BY 4.0 许可协议 进行许可☆