GLSL著色器,來玩

凹凸實驗室發表於2021-05-27

對實現動畫的前端同學們來說,canvas可以說是最自由,最能全面控制的一個動畫實現載體。不但能通過javascript控制點、線、面的繪製,使用圖片資源填充;還能改變輸入引數作出互動動畫,完全控制動畫過程中的動作軌跡、速度、彈性等要素。

但使用canvas開發過較複雜一點的動畫的同學,可能會發現,完全使用javascript繪製、控制的動畫,某些效果不太好實現(這篇文章只討論2D),像模糊,光照,水滴等效果。雖然用逐畫素處理的方法也可以實現,但javascript對這型別大量資料的計算並不擅長,實現出來每一幀繪製的時間十分感人,用他實現動畫並不現實。

canvas除了最常用的javascript API繪製方式(getContext('2d')),還有WebGL的方式(getContext(webgl)),對前面說到的大量資料計算的場景,可以說是最適合發揮的地方。WebGL對很多同學來說就是實現3D場景的,其實對2D繪圖來說,也有很大的發揮場景。

為什麼WebGL會比較厲害

我們來看看javascript API繪製和webGL繪製原理上的不同之處:

如果使用javascript對畫布的逐個畫素進行處理,那這部分處理工作就需要在javascript的執行環境裡進行,我們知道javascript的執行是單執行緒的,所以只能逐個逐個畫素進行計算和繪製。就像一個細長的漏斗,一滴一滴水的往下漏。

https://img2020.cnblogs.com/other/1992869/202105/1992869-20210527214807532-416866496.jpg

而WebGL的處理方式,是用GPU驅動的,對每一個畫素的處理,都是在GPU上執行,而GPU有許多渲染管道,這些處理可以在這些管道中並行執行,這就是WebGL擅長這種大量資料計算場景的原因。

https://img2020.cnblogs.com/other/1992869/202105/1992869-20210527214809762-435239037.jpg

WebGL那麼厲害,都用它繪圖就好喇

WebGL雖然有上面說的優點,但也有個致命的缺點:不好學,想要簡單畫根線也要費一番力氣。

GPU並行管道之間是不知道另一個管道輸出的是什麼,只知道自己管道的輸入和需要執行的程式;而且不保留狀態,管道自己並不知道在這次任務之前執行過什麼程式,有什麼輸入輸出值,類似現在純函式的概念。這些觀念上的不同就提升了使用WebGL繪圖的門檻。

另外這些跑在GPU裡的程式不是javascript,是一種類C語言,這也需要前端同學們另外再學習。

Hello, world

那門檻再高也總有需要跨過去的一天的,下面一步一步控制WebGL去一點圖案,大家也可以體會一下,適合在什麼時候使用這一門技術。

基礎環境——大熒幕

為儘快進入GLSL著色器的階段,這裡基礎WebGL環境搭建用了Three.js,大家可以研究下這個基礎環境的搭建,不用第三方庫其實也用不了多少程式碼量。

以下是基礎環境的搭建:

function init(canvas) {
  const renderer = new THREE.WebGLRenderer({canvas});
  renderer.autoClearColor = false;
 
  const camera = new THREE.OrthographicCamera(
    -1, // left
     1, // right
     1, // top
    -1, // bottom
    -1, // near,
     1, // far
  );
  const scene = new THREE.Scene();
  const plane = new THREE.PlaneGeometry(2, 2);

  const fragmentShader = '............'
  const uniforms = {
    u_resolution:  { value: new THREE.Vector2(canvas.width, canvas.height) },
    u_time: { value: 0 }
  };
  const material = new THREE.ShaderMaterial({
    fragmentShader,
    uniforms,
  });
  scene.add(new THREE.Mesh(plane, material));
 
  function render() {
    material.uniforms.u_time.value++;
    renderer.render(scene, camera);
    requestAnimationFrame(render);
  }

  render()
}

解釋一下上面這段程式碼做了什麼:建立了一個3D場景(說好的2D呢?),把一個矩形平面糊在攝像機前面,佔滿攝像機視覺範圍,就像看IMAX坐最前排,你能看到的就只有面前的螢幕的感覺,螢幕上的畫面就是你的整個世界。我們的繪圖就在這個螢幕上。

再說明一下,著色器分為頂點著色器VERTEX_SHADER和片段著色器FRAGMENT_SHADER

頂點著色器對3D場景裡物體的每個頂點計算值,如顏色、法線向量等,在這裡我們只討論2D畫面,頂點著色器的部分就由Three.js代勞了,實現的作用就是固定了場景中鏡頭和螢幕的位置。

而片段著色器的作用就是計算平面上每一個片段(在這裡是螢幕上每一個畫素)輸出的顏色值,也是這篇文章研究的物件。

片段著色器入參有varyinguniform兩種,varying簡單說一下是由頂點著色器傳入的,每個片段輸入的值由相關的頂點線性插值得到,所以每個片段上的值不一樣,本文先不討論這部分(不然寫不完了)。uniform是統一值,由著色器外部傳入,每個片段得到的值是一樣的,在這裡就是我們從javascript輸入變數的入口。上面的程式碼我們就為片段著色器傳入了u_resolution,包含畫布的寬高值。

第一個著色器

fragmentShader為著色器的程式程式碼,一般的構成為:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}

在前3行檢查了是否定義了GL_ES,這通常在移動端或瀏覽器下會定義,第2行指定了浮點數float的精度為中等,也可以指定為低精度lowp或高精度highp,精度越低執行速度越快,但質量會降低。值得一提的是,同樣的設定在不同的執行環境下可能會表現不一樣,例如某些移動端的瀏覽器環境,需要指定為高精度才能獲得和PC端瀏覽器裡中等精度一樣的表現。

第5行指定了著色器可以接收哪些入參,這裡就只有一個入參:型別為vec2的u_resolution

最後3行描述了著色器的主程式,其中可以對入參和其他資訊作處理,最後輸出顏色到gl_FragColor,代表這個片段顯示的顏色,其中4個數值代表RGBA(紅、綠、藍、透明度),數值範圍為0.0 ~ 1.0

為什麼要寫0.0而不是0呢,因為GLSL裡不像javascript數字只有一個型別,而是分成整形(int)和浮點數(float),而浮點數必須包含小數點,當小數點前是0的時候,寫成.0也可以。

那大家看完這段解說,應該能猜到上面的著色器會輸出什麼吧,對,就是全屏的紅色。

這就是最基礎的片段著色器。

使用uniform

大家應該注意到上面的例子沒有用到傳入的uniform值,下面來說一下這些值怎麼用。

看之前搭建基礎環境的javascript程式碼可以看到,u_resolution儲存了畫布的寬高,這個值在著色器有什麼用呢?

這要說到片元著色器的另一個內建的值gl_FragCoord,這個值儲存的是片段(畫素)的座標xy值,使用這兩個值就可以知道當前著色器計算的是畫布上哪個位置的顏色。舉個例子:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

void main() {
  vec2 st = gl_FragCoord.xy / u_resolution;
  gl_FragColor = vec4(st, 0.0, 1.0);
}

可以看到這樣的影像:

https://img2020.cnblogs.com/other/1992869/202105/1992869-20210527214810013-196127321.png

上面的著色器程式碼,使用歸一化後的xy座標輸出到gl_FragColor的紅、綠色部分。

從圖中可以看出,gl_FragCoord(0, 0)點在左下角,x軸和y軸方向分別為向右和向上。

另一個uniform值u_time就是一個隨著時間不斷增加的值,利用這個值可以使影像隨時間變化,實現動畫的效果。

上面的著色器再改寫一下:

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform float u_time;

void main() {
  vec2 st = gl_FragCoord.xy / u_resolution;
  gl_FragColor = vec4(st, sin(u_time / 100.0), 1.0);
}

可以看到下圖的效果:

http://storage.360buyimg.com/element-video/QQ20210330-195823.mp4

著色器中使用三角函式sin,在顏色輸出的藍色通道做一個從0到1的週期變化。

還能做什麼?

掌握基本的原理後,就是開始從大師的作品中學習了。shadertoy是一個類似codepen的著色器playgroud,上面的著色器都是利用上面的基本工具,還有一些造型函式,造出各種眼花繚亂的特效、動畫。

上面就是GLSL著色器基本的開發工具,現在就可以開始開發你自己的著色器,剩下就是使用數學方面的技能了。

歡迎關注凹凸實驗室部落格:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

歡迎關注凹凸實驗室公眾號

相關文章