在之前的文章中我分享过一些图形学的知识,比如相机,比如空间。但那些更多是3D的范畴,而制作这些效果的时间成本会略大一下,而其实工作中很多时候,web需要处理的更多的是2D的效果,而2D时间确实就没有3D辣么复杂(其实也不简单),所以这个系列我们着重于一些2Dshader的分享。当然之前的3D图形学也不是弃坑(缓更)!
万恶从马赛克说起
说起像素,其实并不是仅仅出现在那些考研资料的视频之中,让某些器官不能清晰的呈现。其实很多时候,低清是为了性能考虑,比如我们建好了一个3D模型,摆好了镜头需要贴图了。此时距离镜头近处的贴图使用高清无码的自然无可厚非。那么远处的呢?那些可能只占画面10几个像素点的建筑,我们需要用一张1920+的贴图来绘制?这是没有必要的。
对于性能已经十分优良的现代设备尚且如此,在那个整个游戏容量还没有如今一个小姐姐图片大的年代(初代超级玛丽卡带容量为 40KB)更是如此,用更少的像素变现场景也成为了一种风格---“像素风”
其实如果你仔细看,初代马里奥为了节省空间不仅仅使用了像素风格,很多东西都是左右对称的(马里奥的身体),甚至天空的云和地上的草也只是涂上了不同颜色(这种调色的shader以后也会说到)。
当然如今像素风早已不是为了节省空间的拖鞋,他俨然成了一种风格。比如前些人的游戏“歧路旅人”就是一款用虚幻4引擎开发的奢华像素大餐。
扯了这么多,让我们回到正题,今天我们要做shader就是将一张原本高清的图片像素画化。
如何打码?
要让图片形成马赛克的效果很容易,假设一张19201080的图像,那么就有19201080个像素点,如果44个像素都呈现一个颜色,图片就模糊了一点,如果扩大成88,图片就又模糊了一点,以此类推,这就是厚码与薄码。不过打码依旧是一门学问,就说这44个像素要选谁的颜色?我们可以取全部平均值,可以抽样区平均,也可以使用特殊的卷积核(这个概念之前的文章有略微提到)要让图片形成马赛克的效果很容易,假设一张100100的图像,那么就有10000个像素点,如果44个像素都呈现一个颜色,图片就模糊了一点,如果扩大成1010,图片就又模糊了一点,以此类推,这就是厚码与薄码。
不过打码依旧是一门学问,就说这4*4个像素要选谁的颜色?我们可以取全部平均值,可以抽样区平均,也可以使用特殊的卷积核),不过我们今天简单粗暴一点就用像素区域左下角的颜色来填充正片区域。
原理解释
当然这里最关键的就是如何像素画,我们采用floor函数来实现。我们都知道在GLSL中floor函数在[n,n+1)区间y值是唯一且递增的如图:
我们就可以使用这个性质来处理shader中的uv坐标:
uv = floor(uv);
然鹅这样写并没什么卵用,因为我们的uv本来就是在[0,1]之间的,所以这里我们引入浮点型step将floor形成的阶梯细分,例如:
float step = 10.;
y = floor(x*step)/step;
这样得到的函数如下图所示:
那么我们的GLSL代码:
这里我们使用uv的xy值绘制了一个颜色图像(step设为10即最终的图片会是一个10*10的图片):
uv = floor(uv*10.0)/10.0;
vec3 color = vec3(uv,1.0);
代码实现:
对于webgl我们需要实现一个物体(position)和一个贴图(uv)的绑定。
这里需要注意webgl的空间是xyz[-1,1]而UV的坐标是[0,1],且仅需要在canvas上画出图片:
// .......
var positionData = [
-1, 1,
1, 1,
-1, -1,
-1, -1,
1, 1,
1, -1,
];
var uvData = [
0.0, 1.0,
1.0, 1.0,
0.0, 0.0,
0.0, 0.0,
1.0, 1.0,
1.0, 0.0
];
// ......
var a_vert_position = gl.getAttribLocation(program, 'a_vert_position');
gl.vertexAttribPointer(
a_vert_position,
2,
gl.FLOAT,
gl.FALSE,
0,
0
);
// ......
var a_uv_coordinate = gl.getAttribLocation(program, 'a_uv_coordinate');
gl.vertexAttribPointer(
a_uv_coordinate,
2,
gl.FLOAT,
gl.FALSE,
0,
0
);
// ......
如上代码,我们声明一个a_vert_position用来存放位置,a_uv_coordinate用来存放纹理坐标。在shader中也有同名的两个变量。下面我们来看看顶点着色器:
attribute vec2 a_vert_position;
attribute vec2 a_uv_coordinate;
varying vec2 fragColor;
void main() {
vec3 position = vec3(a_vert_position,1.0);
fragColor = floor(a_uv_coordinate*1.0)/1.0;
fragColor.y = 1.0 - fragColor.y;
gl_Position = vec4(position,1.0);
}
其中a_vert_position是一个2维坐标所以我们拼接一个向量vec3(a_vert_position,1.0),这里的z值不论是0,1,0.5都是没有差别的。大家可以动手试试,这里我就不解释了留给大家思考。
另外一个需要注意的是由于图片(贴图)的坐标和webgl坐标是相反的,所以需要对y轴做一个翻转fragColor.y = 1.0 - fragColor.y
最后我们看看片段着色器:
precision mediump float;
uniform sampler2D sampler;
varying vec2 fragColor;
uniform float u_pixelate_size;
void main() {
vec2 uv = fragColor;
uv = floor(uv*u_pixelate_size)/u_pixelate_size;
vec4 color = texture2D(sampler, uv);
gl_FragColor = color;
}
这里的fragColor是从定点着色器传递过来的,并使用一个floor函数构造uv坐标,最后我们使用texture2D方法将sampler2D的图像和uv坐标关联在一起。
上面代码中我们传入一个变量u_pixelate_size,作为step,在js代码中实现如下:
var u_pixelate_size = gl.getUniformLocation(program, "u_pixelate_size");
gl.uniform1f(u_pixelate_size, 10);
最终结果如下:
这里我们可以看到一张小骑士的图片随着u_pixelate_size改变而不断变化。到此为止,我们的像素画shader基本大功告成,但是通常我们在生产中拿到的图片都是png格式的,上例中也是。png图在处理时,会有带透明度的像素,这种白边的感觉会在像素化的过程中被放大到肉眼可见。
优化
因此我们需要对透明度进行一定的剔除。
如上图左侧是未对剔除的结果,右边是做了剔除的结果。
代码实现也很简单。我们在js脚本中传入一个变量u_alphe_cut:
var u_alphe_cut = gl.getUniformLocation(program, "u_alphe_cut");
gl.uniform1f(u_alphe_cut,0.9);
在片段着色器中:
// .......
uniform float u_alphe_cut;
// .......
bool clip = color.a - u_alphe_cut <=0.01 ? true : false;
if(clip){
color.a = 0.0;
}
color.rgb *= color.a;
gl_FragColor = color;
// .......
定义一个bool值(GLSL中尽量少写if,即便要写if中的逻辑也尽量简单),如果u_alphe_cut的值大于alpha,或是两者之间的差值小于0.01,此时alpha值就变为0,之后在用color.a与rbg相乘。就实现了对alpha通道的剔除。
上例中我们对一个u_pixelate_size为70的像素画,传入不同的cut可以看到不同的结果。
但这个shader还有一个缺点就是目前他只能处理正方形图片,如果图片是长宽比不同的,那么马赛克的点也是方形的:
如图所示,我们传入的小姐姐不是一张方形的图,像素化后每个马赛克点也不是方形的,此时如果介意我们可以对step进行处理:
uniform vec2 u_texture_size;
// ......
float rate = u_texture_size.y/u_texture_size.x;
vec2 step = vec2(u_pixelate_size,u_pixelate_size*rate);
uv = floor(uv*step)/step;
// ......
我们传入一个记录图像长宽信息的全局变量u_texture_size = vec2(width,height)得到一个宽度和长度的比值,来修正我们的step:
现在我们的小姐姐就是正方形码赛克了(话说这个小姐姐也不用打马赛克吧)
至此这个像素画shader就制作完毕了,下起我们来说说如何给2D图像加反光。