天天看點

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​

​的執行是單線程的,是以隻能逐個逐個像素進行計算和繪制。就像一個細長的漏鬥,一滴一滴水的往下漏。

GLSL着色器,來玩

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

GLSL着色器,來玩

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​

​代勞了,實作的作用就是固定了場景中鏡頭和螢幕的位置。

而片段着色器的作用就是計算平面上每一個片段(在這裡是螢幕上每一個像素)輸出的顔色值,也是這篇文章研究的對象。

片段着色器入參有​

​varying​

​和​

​uniform​

​兩種,​

​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​

​,這個值存儲的是片段(像素)的座标​

​x​

​,​

​y​

​值,使用這兩個值就可以知道目前着色器計算的是畫布上哪個位置的顔色。舉個例子:

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

可以看到這樣的圖像:

GLSL着色器,來玩

上面的着色器代碼,使用歸一化後的​

​x​

​、​

​y​

​座标輸出到​

​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着色器基本的開發工具,現在就可以開始開發你自己的着色器,剩下就是使用數學方面的技能了。

繼續閱讀