今天我們將要和大家分享一些 WebGL 實驗,在這個實驗中我們將建立一個非常逼真的雨滴效果,並把它放到不同的場景中去。在這篇文章中,我們將給出製作這種效果所用到的一些一般性技術和技巧的概覽。
請注意:文中製作的效果還處於試驗階段,可能無法在所有瀏覽器中都看到預期的效果。最好使用 Chrome。
入門
如果我們想製作一個基於現實世界的效果,那麼首先我們需要剖析一下它看起來究竟是什麼樣子的,這樣製作出的效果才能顯得真實。如果你去找一些水滴落在窗戶上的圖片來看(當然,你肯定已經在生活中觀察過他們了),你會發現由於折射,雨滴似乎會把它後面的影像上下顛倒。
圖片來源:Wikipedia, GGB reflection in raindrop
同時你還會看到相互之間距離很近的雨滴會合併成一個——而且如果超過了一定的尺寸,它就會向下滑落,並且留下一道小小的痕跡。
為了模擬這種行為,我們必須繪製大量的雨滴,在每一幀上都更新它們的折射效果,並且要在一個合適的幀率下做這些事情,為此我們需要極好的效能———所以,為了能夠使用顯示卡的硬體加速,我們將使用 WebGL。
WebGL
WebGL 是一個繪製 2D 和 3D 圖形的 JavaScript 介面,並且允許使用 GPU 以獲得更好的效能。它基於 OpenGL ES,著色器由一門叫做 GLSL 的語言寫成,而不是 JS。
總之,如果你僅僅做過網頁開發,那麼它看起來是很難使用的——這不僅僅是一門新的語言,而且還是一個全新的概念——但是一旦你掌握了一些核心的概念,它就會變得容易不少。
在這篇文章中我們將僅給出一些基本的使用示例,更多深入的解析請參閱 WebGl Fundamentals 。
首先我們需要一個 canvas
標籤。WebGL 是在 canvas
上繪製的,它是一個繪製環境,類似於我們用 getContext('2d')
獲取到的繪製環境。
1 |
<canvas id="container" width="800" height="600"></canvas> |
1 2 |
var canvas = document.getElementById("container"); var gl = canvas.getContext("webgl"); |
接下來我們需要一段程式,它由頂點著色器和片段著色器(譯註:『片段著色器』又稱『畫素著色器』)構成。著色器就是一些函式:頂點著色器在每個頂點執行一次,而片段著色器在每個畫素上都被呼叫一次。它們的任務分別是返回座標和顏色。這是我們的 WebGL 應用的核心。
首先來建立我們的著色器。這是一個頂點著色器,我們不會對頂點做任何修改,所以簡單地讓資料穿過它就好了:
1 2 3 4 5 6 7 8 9 |
<script id="vert-shader" type="x-shader/x-vertex"> // gets the current position attribute vec4 a_position; void main() { // returns the position gl_Position = a_position; } </script> |
這個是片段著色器。這個著色器將會根據座標來設定每個畫素點的顏色。
1 2 3 4 5 6 7 8 9 10 |
<script id="frag-shader" type="x-shader/x-fragment"> precision mediump float; void main() { // current coordinates vec4 coord = gl_FragCoord; // sets the color gl_FragColor = vec4(coord.x/800.0,coord.y/600.0, 0.0, 1.0); } </script> |
現在我們把著色器連線到 WebGL 環境中去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function createShader(gl,source,type){ var shader = gl.createShader(type); source = document.getElementById(source).text; gl.shaderSource(shader, source); gl.compileShader(shader); return shader; } var vertexShader = createShader(gl, 'vert-shader', gl.VERTEX_SHADER); var fragShader = createShader(gl, 'frag-shader', gl.FRAGMENT_SHADER); var program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragShader); gl.linkProgram(program); gl.useProgram(program); |
接下來我們建立一個物件然後在它上面繪製我們的著色器。這裡我們來畫個矩形——確切地說,畫兩個矩形。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// create rectangle var buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bufferData( gl.ARRAY_BUFFER, new Float32Array([ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0]), gl.STATIC_DRAW); // vertex data var positionLocation = gl.getAttribLocation(program, "a_position"); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); |
最後,繪製整個影像:
1 |
gl.drawArrays(gl.TRIANGLES, 0, 6); |
結果如下:
之後你可以盡情玩弄這些著色器以便搞明白它是怎麼工作的。你可以在 ShaderToy 上找到很多很棒的著色器的例子。
雨滴
現在讓我們來看看如何製作雨滴的效果。首先我們來看一下單個的雨滴是什麼樣的:
現在這裡發生了很多事情。
Alpha 通道變成了這樣是因為我們使用了類似於文章 Creative Gooey Effects 中提到的一個技術來讓雨滴粘連到一起。
顏色變成這樣也是有原因的:我們使用了類似 法線貼圖 的技術來實現折射效果。我們將利用雨滴的顏色來獲取我們透過雨滴看到的貼圖的座標。這是沒有遮罩時它的樣子:
在這張圖片中,我們將通過綠色通道的資料來獲取 X 座標,通過紅色通道的資料來獲取 Y 座標。
現在我們可以寫我們的著色器了,並且可以同時使用貼圖資料和雨滴的位置來翻轉並扭曲雨滴後方的貼圖了。
下雨過程
在建立雨滴之後,我們就可以開始對下雨進行模擬了。
讓雨點之間相互作用是很難快速計算的——隨著新的雨點的到來,運算量將會呈指數級增長——所以我們必須做一點優化。
在這個示例中,我把大雨點和小雨點分開了。小雨點繪製在一個單獨的 canvas
上,並且沒有沒追蹤。這樣我就可以繪製上千個小雨滴而且不會讓速度有任何減慢。缺點是它們都是靜態的,而且由於我們每幀都在建立新雨滴,他們將會累積起來。為了修復這個問題,我們將會使用大一點的雨滴。
由於大雨滴是會移動的,於是我們可以利用它們來清除它們下方的小雨滴。擦除操作在 canvas 中比較麻煩:實際上我們還是要畫一些東西出來,但是要使用 globalCompositeOperation='destination-out
。因此,每當一個大的雨滴移動,我們就會在小雨滴的 canvas
上繪製一個圓,並使用複合操作來清除這些雨滴,使效果更加逼真。
最後,我們把所有這些繪製在一個大的 canvas
上,然後把它作為我們的 WebGL 著色器的貼圖。
為了把它做得更輕便一點,我們要利用背景會失焦這一事實,因此我們用了一個小尺寸的貼圖,然後把它放大。在 WebGL 中,貼圖的尺寸會直接影響到效能。我們需要用另外一個沒有失焦的貼圖來製作雨滴。模糊是一個代價很高的操作,實時的模糊處理應該儘量避免掉——但是由於雨滴很小,我們可以把貼圖也變得很小。
總結
為了製作像雨滴這樣的逼真的效果,我們需要考慮很多複雜的細節。先將效果從現實世界中分離出來是重建任何一個效果的關鍵所在,一旦知道了它在現實世界中是如何工作的,我們就可以把它的行為對映到虛擬世界。有了 WebGL,我們可以獲得很高的效能(我們可以使用顯示卡的硬體加速)因此對於這類效果,它是一個很不錯的選擇。
希望各位喜歡這個實驗並且受到啟發!