譯序
Three.js是一個偉大的開源WebGL庫,WebGL允許JavaScript操作GPU,在瀏覽器端實現真正意義的3D。但是目前這項技術還處在發展階段,資料極為匱乏,愛好者學習基本要通過Demo源碼和Three.js本身的源碼來學習。
國外網站 aerotwist.com 有六篇較為簡單的入門教程,我嘗試着將其翻譯過來,與大家分享。
0.簡介
這是WebGL着色器教程的后半部分,如果你沒看過前一篇,閱讀這一篇教程可能會使你感到困惑,建議你翻閱前面的教程。
上一篇結束的時候,我們在屏幕中央畫了一個好看的粉紅色的球體。現在我要開始創建一些更加有意思的東西了。
在這一篇教程中,我們會先花點時間來加入一個動畫循環,然后是頂點attributes變量和一個uniform變量。我們還要加一些varying變量,這樣頂點着色器就可以向片元着色器傳遞信息了。最終的結果是哪個粉紅色的球體會從頂部開始向兩側“點燃”,然后作有規律的運動。這有一點迷幻,但是會幫助你對着色器中的三種變量有更好的了解:他們互相聯系,實現了整個集合體。當然我們會在Three.js的框架中做這些。
1.模擬光照
讓我們更新顏色吧,這樣球體看起來就不會是個扁平晦暗的圓了。如果我們想看看Three.js是怎樣處理光照的,我敢肯定你會發現這比我們需要的要復雜得多,所以我們先模擬光照吧。你應該瀏覽一下Three.js中那些奇妙的着色器,還有一些來自最近的一個 Chris Milk 和 Google, Rome 的WebGL項目。
回到着色器,我們要更新頂點着色器來向片元着色器傳遞頂點的法向量。利用一個varying變量:
// 創建一個varying變量vNormal,頂點着色器和片元着色器都包含了該變量 varying vec3 vNormal; void main() { // 將vNormal設置為normal,后者是Three.js創建並傳遞給着色器的attribute變量 vNormal = normal; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }
在片元着色器中,我們將會創建一個相同變量名的變量,然后將法線向量和另一個表示來自右上方光線的向量點乘,並將結果作用於顏色。最后結果的效果有點像平行光。
// 和頂點着色器中一樣的變量vNormal varying vec3 vNormal; void main() { // 定義光線向量 vec3 light = vec3(0.5,0.2,1.0); // 確保其歸一化 light = normalize(light); // 計算光線向量和法線向量的點積,如果點積小於0(即光線無法照到),就設為0 float dProd = max(0.0, dot(vNormal, light)); // 填充片元顏色 gl_FragColor = vec4(dProd, // R dProd, // G dProd, // B 1.0); // A }
使用點積的原因是:兩個向量的點積表明他們有多么“相似”。如果兩個向量都是歸一化的,而且他們的方向一模一樣,點積的值就是1;如果兩個向量的方向恰巧完全相反,點積的值就是-1。我們所做的就是把點積的值拿來作用到光纖上,所以如果這個點在球體的右上方,點積的值就是1,也就是完全照亮了;而在另一邊的點,獲得的點積值接近0,甚至到了-1。我們將獲得的任何負值都設置為0。當你將數據傳入之后,你就會看到最基本的光照效果了。
下面是什么?我們會將頂點的坐標摻和進來。
2.Attribut變量
接下來我要通過Attribute變量為每一個頂點傳遞一個隨機數,這個隨機數被用來將頂點沿着法線向量推出去一段距離。新的結果有點像一個怪異的不規則物體,每次刷新頁面物體都會隨機變化。現在,他還不會動(后面我會讓他動起來),但是幾次刷新就可以很好地觀察到,他的形狀是隨機的。
讓我們開始為頂點着色器加入attribute變量吧:
attribute float displacement; varying vec3 vNormal; void main() { vNormal = normal; // 將隨機數displacement轉化為三維向量,這樣就可以和法線相乘了 vec3 newPosition = position + normal * vec3(displacement); gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); }
你看到什么都沒變,因為attribute變量displacement還沒有被設定你,所以着色器就使用了0作為默認值。這時displacement還沒起作用,但我們馬上就要在着色器材質中加上attribute變量了,然后Three.js就會自動地把它們綁在一起運行了。
同時也要注意這樣一個事實,我將更新后的位置指定給了一個新的三維向量變量,因為原來的位置變量position,就像所有的attribute變量一樣,都是只讀的。
3.更新着色器材質
現在我們來更新着色器材質,傳入一些東西給attribute對象displacement。記住,attribute對象是和頂點一一對應的,所以我們對球體的每一個頂點都有一個值,就像這樣:
var attributes = { displacement: { type: 'f', // 浮點數 value: [] // 空數組 } }; var vShader = $('#vertexshader'); var fShader = $('#fragmentshader'); // 創建一個包含attribute屬性的着色器材質 var shaderMaterial = new THREE.MeshShaderMaterial({ attributes: attributes, vertexShader: vShader.text(), fragmentShader: fShader.text() }); // 向displacement中填充隨機數 var verts = sphere.geometry.vertices; var values = attributes.displacement.value; for(var v = 0; v < verts.length; v++) { values.push(Math.random() * 30); }
這樣,就可以看到一個變形的球體了。最Cool的是:所有這些變形都是在GPU中完成的。
4.動起來
要使這東西動起來,應該怎么做?好吧,應該做這兩件事情。
一個uniform變量amplitude,在每一幀控制displacement實際造成了多少位移。我們可以使用正弦或余弦函數來在每一幀中生成它,因為這兩個函數的取值范圍從-1到1。
一個幀循環。
我們需要將這個uniform變量加入到着色器材質中,同時也需要加入到頂點着色器中。先來看頂點着色器:
uniform float amplitude; attribute float displacement; varying vec3 vNormal; void main() { vNormal = normal; // 將displacement乘以amplitude,當我們在每一幀中平滑改變amplitude時,畫面就動起來了 vec3 newPosition = position + normal * vec3(displacement * amplitude); gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0); }
然后更新着色器材質:
var uniforms = { amplitude: { type: 'f', // a float value: 0 } }; var vShader = $('#vertexshader'); var fShader = $('#fragmentshader'); // 創建最終的着色器材質 var shaderMaterial = new THREE.MeshShaderMaterial({ uniforms: uniforms, attributes: attributes, vertexShader: vShader.text(), fragmentShader: fShader.text() });
我們的着色器也已經就緒了。但我們好像又倒退了一步,屏幕中又只剩下光滑的球了。別擔心,這是因為amplitude值設置為0,因為我們將amplitude乘上了displacement,所以現在看不到任何變化。我們還沒設置循環呢,所以amplitude只可能是0.
在我們的JavaScript中,需要將渲染過程打包成一個函數,然后用requestAnimationFrame去調用該函數。在這個函數里,我們更新uniform(譯者注:即amplitude)的值。
var frame = 0; function update() { // amplitude來自於frame的正弦值 uniforms.amplitude.value = Math.sin(frame); // 更新全局變量frame frame += 0.1; renderer.render(scene, camera); // 指定下一次屏幕刷新時,調用update requestAnimFrame(update); } requestAnimFrame(update);
5.小結
就是它了!你看到球體正在奇怪地脈動着。關於着色器,還有太多的內容沒有講到呢,但是我希望這篇教程能夠對你有一些幫助。現在,當你看到一些其他的着色器時,我希望你能夠理解它們,而且你應該有信心去創建自己的着色器了!