對實現動畫的前端同學們來說,canvas
可以說是最自由,最能全面控制的一個動畫實現載體。不但能通過javascript
控制點、線、面的繪制,使用圖片資源填充;還能改變輸入參數作出交互動畫,完全控制動畫過程中的動作軌跡、速度、彈性等要素。
但使用canvas
開發過較復雜一點的動畫的同學,可能會發現,完全使用javascript
繪制、控制的動畫,某些效果不太好實現(這篇文章只討論2D),像模糊,光照,水滴等效果。雖然用逐像素處理的方法也可以實現,但javascript
對這類型大量數據的計算並不擅長,實現出來每一幀繪制的時間十分感人,用他實現動畫並不現實。
但canvas
除了最常用的javascript
API繪制方式(getContext('2d')
),還有WebGL的方式(getContext(webgl)
),對前面說到的大量數據計算的場景,可以說是最適合發揮的地方。WebGL對很多同學來說就是實現3D場景的,其實對2D繪圖來說,也有很大的發揮場景。
為什么WebGL會比較厲害
我們來看看javascript
API繪制和webGL繪制原理上的不同之處:
如果使用javascript
對畫布的逐個像素進行處理,那這部分處理工作就需要在javascript
的運行環境里進行,我們知道javascript
的執行是單線程的,所以只能逐個逐個像素進行計算和繪制。就像一個細長的漏斗,一滴一滴水的往下漏。
而WebGL的處理方式,是用GPU驅動的,對每一個像素的處理,都是在GPU上執行,而GPU有許多渲染管道,這些處理可以在這些管道中並行執行,這就是WebGL擅長這種大量數據計算場景的原因。
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);
}
可以看到這樣的圖像:
上面的着色器代碼,使用歸一化后的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着色器基本的開發工具,現在就可以開始開發你自己的着色器,剩下就是使用數學方面的技能了。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章: