Gamemaker 笔记 - Gaussian Blur & Bloom effect

Author Avatar
空気浮遊 2023年02月22日
  • 在其它设备中阅读本文章

普通模糊

一个像素的颜色为周围矩形的像素颜色取平均。
评价:很丑

高斯分布

高斯分布,也即正态分布 (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 将提取的部分进行模糊,最后用叠加的混合方式涂到被处理的表面上以实现泛光效果。

(咕咕咕)