GLSL着色器,來玩

對實現動畫的前端同學們來說,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://oscimg.oschina.net/oscnet/up-ddd41b1cba1309604648935b5c8c7f62322.JPEG

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

https://oscimg.oschina.net/oscnet/up-fcfbc53aa439e46c3c0b1ecd51c827b2cf5.JPEG

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://oscimg.oschina.net/oscnet/up-a5efa23ce966319430674f5f49c9fdbd5c5.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),不定時推送文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章