使用WebGL作色器基础知识实现PIXI.js高斯三角模糊

发布于 · 最后修改时间 · 标签: javascript

官方给出的模糊滤镜效果不尽人意,所以就想自己写一个,顺带学习了一些 WebGL 的作色器相关的基础知识。
说真的网上的文章讲得不是很乱,以下是我总结出来的。

WebGL 作色器

作色器的基本理念我不赘述。不了解的看下面猜测一下也能猜出个大概。
这里从 PIXI.js 的源码中来看,用blurXFilter为例,顶点作色器的代码如下:

attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;
attribute vec4 aColor;

uniform float strength;
uniform mat3 projectionMatrix;

varying vec2 vTextureCoord;
varying vec4 vColor;
varying vec2 vBlurTexCoords[6];

void main(void)
{
gl_Position = vec4((projectionMatrix * vec3((aVertexPosition), 1.0)).xy, 0.0, 1.0);
vTextureCoord = aTextureCoord;

vBlurTexCoords[ 0] = aTextureCoord + vec2(-0.012 * strength, 0.0);
vBlurTexCoords[ 1] = aTextureCoord + vec2(-0.008 * strength, 0.0);
vBlurTexCoords[ 2] = aTextureCoord + vec2(-0.004 * strength, 0.0);
vBlurTexCoords[ 3] = aTextureCoord + vec2( 0.004 * strength, 0.0);
vBlurTexCoords[ 4] = aTextureCoord + vec2( 0.008 * strength, 0.0);
vBlurTexCoords[ 5] = aTextureCoord + vec2( 0.012 * strength, 0.0);

vColor = vec4(aColor.rgb * aColor.a, aColor.a);
}

顶点作色器程序执行过程中需要数据,首先了解这三种数据:

  1. Attribute (从缓冲区对象中拉取数据)
  2. Uniform (在绘制过程中所有顶点都需要的、固定的数据值)
  3. Texture (像素/纹理元素数据)
    片元作色器也需要数据,也是三种获取方式:
  4. Uniform (在绘制过程中每个像素都需要的、固定的变量值)
  5. Texture (像素/纹理元素数据)
  6. Varying (从顶点着色器传递过来并且经过内插过程的数据)

简单的站在我这个新手的角度来说,我是这样看的:
attribute 是 PIXI 框架传入的数据,顶点作色器最终的计算数据是反应到gl_Position上面。
uniform 是用户自己传入的数据。
varying 是定点作色器和片元作色器的共享的数据。

所以对比默认的顶点作色器代码 TextureShader,可以看出有三行都是一样的:

gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
vTextureCoord = aTextureCoord;
vColor = vec4(aColor.rgb * aColor.a, aColor.a);

不管这三行,看其它部分:

    vBlurTexCoords[ 0] = aTextureCoord + vec2(-0.012 * strength, 0.0);
vBlurTexCoords[ 1] = aTextureCoord + vec2(-0.008 * strength, 0.0);
vBlurTexCoords[ 2] = aTextureCoord + vec2(-0.004 * strength, 0.0);
vBlurTexCoords[ 3] = aTextureCoord + vec2( 0.004 * strength, 0.0);
vBlurTexCoords[ 4] = aTextureCoord + vec2( 0.008 * strength, 0.0);
vBlurTexCoords[ 5] = aTextureCoord + vec2( 0.012 * strength, 0.0);

当我把 strength 值调整到 100 后,是这样的效果:

image

我不得不诟病这个模糊效果(不然也不会有这篇文章了)。所以现在理解一下上面的代码:
vec2( X , Y) 意味着一个偏移量,其中 Y=0,而aTextureCoord从命名理解就是纹理坐标,这里 j 把纹理贴图左右两边的数据拿了过来,在执行片元作色器的时候用上了:

precision lowp float;

varying vec2 vTextureCoord;
varying vec2 vBlurTexCoords[6];
varying vec4 vColor;

uniform sampler2D uSampler;

void main(void)
{
gl_FragColor = vec4(0.0);

gl_FragColor += texture2D(uSampler, vBlurTexCoords[ 0])*0.004431848411938341;
gl_FragColor += texture2D(uSampler, vBlurTexCoords[ 1])*0.05399096651318985;
gl_FragColor += texture2D(uSampler, vBlurTexCoords[ 2])*0.2419707245191454;
gl_FragColor += texture2D(uSampler, vTextureCoord )*0.3989422804014327;
gl_FragColor += texture2D(uSampler, vBlurTexCoords[ 3])*0.2419707245191454;
gl_FragColor += texture2D(uSampler, vBlurTexCoords[ 4])*0.05399096651318985;
gl_FragColor += texture2D(uSampler, vBlurTexCoords[ 5])*0.004431848411938341;
}

可以看到实现方式就是把左右纹理贴图的数据以渐变的方式贴到一个点上,最终看到的就是上面效果图展示的多层贴图以不同透明度重叠的效果。PS: vColor这个参数我猜是遮罩或者透明通道

参考 glfx 实现高斯模糊

对比triangleblur.js。可以看出它的作色器代码其实是片元作色器代码,顶点作色器放空 null 使用默认。所以对比一下参数其实就好办了:
其中delta这个参数都是用户传入的,剩下的只有texture|texCoord分别对应 PIXI.js 中的uSampler|vTextureCoord

所以代码关系对上后就好说了,接下来用 TypeScript 来实现 PIXI.js 中的高斯模糊滤镜。

PIXI.js 中高斯模糊的实现

参考官方写法,先搭建出大概的类架构:

class GaussianBlur extends PIXI.AbstractFilter {
_delta: number;
constructor() {
let vert = "";
let frag = "";
super(vert, frag, {});
}
applyFilter(renderer, input, output, clear) {}
get blur() {
return this._delta;
}
set blur(value) {
this._delta = value;
}
}
export default GaussianBlur;

然后就是要传入作色器代码了:

var randomShaderFunc =
"\
float random(vec3 scale, float seed) {\
/* use the fragment position for a different seed per-pixel */\
return fract(sin(dot(gl_FragCoord.xyz + seed, scale)) * 43758.5453 + seed);\
}\
"
;
let vert = `attribute vec2 aVertexPosition;
attribute vec2 aTextureCoord;
attribute vec4 aColor;

uniform vec2 delta;
uniform mat3 projectionMatrix;

varying vec2 vTextureCoord;
varying vec4 vColor;
varying vec2 vDelta;

void main(void)
{
gl_Position = vec4((projectionMatrix * vec3((aVertexPosition), 1.0)).xy, 0.0, 1.0);
vTextureCoord = aTextureCoord;

vDelta = delta;

vColor = vec4(aColor.rgb * aColor.a, aColor.a);
}
`
;
let frag = `precision lowp float;

varying vec2 vTextureCoord;
varying vec2 vDelta;
varying vec4 vColor;

uniform sampler2D uSampler;
${randomShaderFunc}
void main() {
vec4 color = vec4(0.0);
float total = 0.0;

/* randomize the lookup values to hide the fixed number of samples */
float offset = random(vec3(12.9898, 78.233, 151.7182), 0.0);

for (float t = -30.0; t <= 30.0; t++) {
float percent = (t + offset - 0.5) / 30.0;
float weight = 1.0 - abs(percent);
vec4 sample = texture2D(uSampler, vTextureCoord + vDelta * percent);

/* switch to pre-multiplied alpha to correctly blur transparent images */
sample.rgb *= sample.a;

color += sample * weight;
total += weight;
}

gl_FragColor = color / total;

/* switch back from pre-multiplied alpha */
gl_FragColor.rgb /= gl_FragColor.a + 0.00001;
}
`
;

要注意的是,PIXI.js 的风格就是数据都是从顶点作色器那边传入的,所以 detal 参数就从顶点作色器那边进行赋值并在片元作色器那边使用。片元作色器那边基本就是把 glfx 的代码拷贝过来,然后把参数命名改成 PIXI 中的参数命名即可。要注意的是precision lowp float;这一句 glfx 没有,PIXI 中一定要加,好像是声明精度的问题,作为一个小白,暂时不理解,只是在做的过程中发现如果没有这句的话作色器编译的时候会报错。
最终效果(blur=10):

image

还有一个要注意的问题就是这个滤镜要跑两次才行,就是 X\Y 是分开来模糊的,如果一起的话,会变成斜方向的模糊。PIXI 中类似的实现参考BlurFilter,因为它也是 X、Y 两次模糊滤镜后的效果。以下是我 applyFilter 的代码:

var shader = this.getShader(renderer);
var renderTarget = renderer.filterManager.getRenderTarget(true);
this.uniforms.delta.value = {
x: this._delta / input.size.width,
y: 0,
};
renderer.filterManager.applyFilter(shader, input, renderTarget, clear);

this.uniforms.delta.value = {
x: 0,
y: this._delta / input.size.height,
};
renderer.filterManager.applyFilter(shader, renderTarget, output, clear);

// 很重要,不加的话会引发内存泄漏
renderer.filterManager.returnRenderTarget(renderTarget);

最后贴出完整代码:
Typescript、ES6 版本:Gaussianblur.ts
ES5 版本:Gaussianblur.js