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圖像加反光。

相關文章
相關標籤/搜索