2D shader 小打小闹 之像素化

在以前的文章中我分享过一些图形学的知识,好比相机,好比空间。但那些更可能是3D的范畴,而制做这些效果的时间成本会略大一下,而其实工做中不少时候,web须要处理的更多的是2D的效果,而2D时间确实就没有3D辣么复杂(其实也不简单),因此这个系列咱们着重于一些2Dshader的分享。固然以前的3D图形学也不是弃坑(缓更)!

万恶从马赛克提及


提及像素,其实并非仅仅出如今那些考研资料的视频之中,让某些器官不能清晰的呈现。其实不少时候,低清是为了性能考虑,好比咱们建好了一个3D模型,摆好了镜头须要贴图了。此时距离镜头近处的贴图使用高清无码的天然无可厚非。那么远处的呢?那些可能只占画面10几个像素点的建筑,咱们须要用一张1920+的贴图来绘制?这是没有必要的。web

对于性能已经十分优良的现代设备尚且如此,在那个整个游戏容量尚未现在一个小姐姐图片大的年代(初代超级玛丽卡带容量为 40KB)更是如此,用更少的像素变现场景也成为了一种风格---“像素风”canvas

其实若是你仔细看,初代马里奥为了节省空间不只仅使用了像素风格,不少东西都是左右对称的(马里奥的身体),甚至天空的云和地上的草也只是涂上了不一样颜色(这种调色的shader之后也会说到)。函数

固然现在像素风早已不是为了节省空间的拖鞋,他俨然成了一种风格。好比前些人的游戏“歧路旅人”就是一款用虚幻4引擎开发的奢华像素大餐。性能

扯了这么多,让咱们回到正题,今天咱们要作shader就是将一张本来高清的图片像素画化。优化

如何打码?webgl


要让图片造成马赛克的效果很容易,假设一张19201080的图像,那么就有19201080个像素点,若是44个像素都呈现一个颜色,图片就模糊了一点,若是扩大成88,图片就又模糊了一点,以此类推,这就是厚码与薄码。不过打码依旧是一门学问,就说这44个像素要选谁的颜色?咱们能够取所有平均值,能够抽样区平均,也可使用特殊的卷积核(这个概念以前的文章有略微提到)要让图片造成马赛克的效果很容易,假设一张100100的图像,那么就有10000个像素点,若是44个像素都呈现一个颜色,图片就模糊了一点,若是扩大成1010,图片就又模糊了一点,以此类推,这就是厚码与薄码。spa

不过打码依旧是一门学问,就说这4*4个像素要选谁的颜色?咱们能够取所有平均值,能够抽样区平均,也可使用特殊的卷积核),不过咱们今天简单粗暴一点就用像素区域左下角的颜色来填充正片区域。3d

原理解释


固然这里最关键的就是如何像素画,咱们采用floor函数来实现。咱们都知道在GLSL中floor函数在[n,n+1)区间y值是惟一且递增的如图:code

咱们就可使用这个性质来处理shader中的uv坐标:orm

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);

最终结果以下:

image

这里咱们能够看到一张小骑士的图片随着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通道的剔除。

image

上例中咱们对一个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图像加反光。

相关文章
相关标签/搜索