WebGL 实现图片简易马赛克滤镜

前言

马赛克效果可以说是是我们日常生活中最常用的效果了。之前复习 WebGL 纹理和各种参数的意义的时候突然发现使用不同的 texParameter 能生成马赛克效果,结合之前在 The Book of Shaders 中学习随机效果时总结的坐标分区生成重复纹理的经验,总结出两个在 WebGL 中实现马赛克效果,以本文作下笔记。

我们首先来看看这两个效果的 Demo:

使用扩展 EXT_shader_texture_lod

在 WebGL 中,使用纹理材质可以说是最常用的操作了。在使用静态图片构造 Texture 的过程中,我们常使用 gl.texParameteri() 配置不同的参数,以适配图片被构造成纹理时不可避免的缩放、拉伸或平铺等场景。尤其在缩小图像时,需要按照采样密度选择一定数量的像素进行计算获得最终的像素颜色,为了平衡采样计算的开销和最终呈现效果,我们通过生成 MIPMAP 的方式,在创建纹理的过程中同时生成不同等级的 MIPMAP,提供给后续缩放的采样过程,避免渲染时不必要的重复运算。

需要注意的是,只有当源纹理的宽/高均为 2 的幂次方时才可以生成 MIPMAP。如下图所示,生成的 MIPMAP 层级越高,像素越低,通常每个子图都是前一级的双线性插值;假设源纹理像素为 512 * 512,第 N 层的面积为 ${512 \times 512} / 2 ^ {2N}$

gl.TEXTURE_MIN_FILTER 支持以下几种参数:

  • NEAREST_MIPMAP_NEAREST
  • NEAREST_MIPMAP_LINEAR
  • LINEAR_MIPMAP_NEAREST
  • LINEAR_MIPMAP_LINEAR

它们都是通过选择多个像素、多层 MIPMAP 进行内插计算出最终像素颜色。从 MIPMAP 中的哪一级纹理取色,取决于在此纹理上采样的密度(当前图案的缩放程度)。此时 EXT_shader_texture_lod 就出场啦~

EXT_shader_texture_lod 扩展是 WebGL API 的一部分(LOD:Level of Detail),可以让开发者精准地选择需要使用的 MIPMAP 层为纹理进行取色。WebGL2 将会默认支持该扩展的功能,不需要额外开启。WebGL1 使用方法如下:

1
2
// 在 js 中开启扩展
gl.getExtension('EXT_shader_texture_lod');
1
2
3
4
5
// 在片元着色器中加入 #extension 宏
#extension GL_EXT_shader_texture_lod: enable

// 使用 texture2DLodEXT() 注明需要采用的 MIPMAP 层数
gl_FragColor = texture2DLodEXT(uTexture, st, uExtLodLevel);

利用 EXT_shader_texture_lod 能随意选择 MIPMAP 层的特点,指定的层数越高,采样的密度越低,获得的像素颜色越偏离原图像的颜色,所得的纹理越是失真。同时,gl.NEAREST_MIPMAP_NEARESTgl.NEAREST_MIPMAP_LINEAR 均从获得 MIPMAP 层中选择一个最佳像素作为结果(区别于 gl.LINEAR_MIPMAP_XXX 参数从获得 MIPMAP 层各自再挑选4个像素进行融合),离散的结果也形成了马赛克效果。具体可以参考以下例子,可以发现选择 MIPMAP 层越高,马赛克的像素点越大:

GLSL Shader 实现

参考 EXT_shader_texture_lod 的思路,马赛克的格子样式是由采样产生的:某一区域的颜色是该区域当前像素位置该区域原纹理的颜色共同决定的。于是我们便可以通过变换纹理坐标 a_texCoord 将某一区域的像素全部映射到该区域中的一固定像素,使得 GPU 在该区域进行纹理采样时,全部取得相同的颜色。

这个过程分成三个步骤:1) 纹理分区;2) 坐标映射;3) 色彩采样。举个🌰,如下图,我们将一个由 UV 坐标生成的颜色纹理分成四个区域,每个区域中的采样坐标均映射到该区域的左下角,如区域2中,A、B、C等其他坐标均映射到坐标 O 上,GPU 在处理属于区域 2 的像素时均采用原纹理在坐标 O 的颜色:

这样我们便可以得到如右图的新纹理。当纹理分区越多,采样密度越高,便可形成更多的马赛克小格子~具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Fragment Shader
uniform sampler2D u_texture;
uniform float u_mosaicLevel;
varying vec2 v_texCoord;

void main(){
vec2 st = v_texCoord;

// 变换纹理采样坐标,使每 (1 / scale) 范围均映射到各自同一个采样坐标
float scale = max(1., (10. - u_mosaicLevel) * 5.);
st = floor(st * scale) / scale;

vec3 color = texture2D(u_texture, st).rgb;
gl_FragColor = vec4(color, 1.);
}

在例子的片元着色器中,我们结合传入的 u_mosaicLevel 对纹理坐标 v_texCoord 进行了一定的变换,马赛克的 Level 越高,对原纹理的采样密度越低,形成的马赛克越大。同时,为减少纹理参数的影响,同时降低计算开销,我们直接将 gl.TEXTURE_MIN_FILTER 的值设为 gl.NEAREST 即可:

1
2
// in javascript render function
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

相关阅读