在上一篇的《WebGL 簡單範例》裡,已經示範了如何用 WebGL 來畫出一個平面的三角形;實際上,該範例主要是要展示如何建立基本的 WebGL 環境,在 render 的部分並沒有真正考慮到 3D 空間、而是以 2D 的方式來畫的,所以應該也是最簡單的 WebGL 例子了∼
在知道了 WebGL 最基本的操作後,接下來就是要真的來畫一個 3D 的物體了!這邊會以《WebGL 簡單範例》的程式來做改寫,並以 WebGL Wiki 的 Spinning Box Tutorial 為主要參考資料;不過由於 Heresy 在這邊主要只是先加入 3D 環境的矩陣計算,會先忽略掉 texture(貼圖)和 lighting(打光)的部分,所以結果看起來會和 WebGL 的範例差滿多的(會更接近該教學頁面最下方、沒有貼圖但是有 lighting 版本的範例)。
以官方的 Spinning Box 範例來說,他使用了兩個屬於 Apple 的 JavaScript 函式庫,來簡化 WebGL 的程式開發。其中一個是 utils3d.js(網址),他的功能包含了 WebGL 的初始化、Shader 的讀取與建立、texture / object 的讀取或產生;而另一個則是 CanvasMatrix.js(網址),提供了 WebGL 一般會用到矩陣計算功能。
而 Heresy 在這邊只會用到 CanvasMatrix.js 來做矩陣計算,並不會用到 utils3d.js 的函式;如果連 CanvasMatrix.js 都不想用的話,就是要自己寫相關的矩陣計算演算法了∼
接下來還是看程式的部分吧∼這個 WebGL 程式在 HTML 的部分,基本上是和上一個範例完全相同,所以就不贅述了。而主程式 RunWebGL() 的部分,內容則如下:
function RunWebGL()
{
// try to get WebGL Context
try
{
var canvas = document.getElementById( "canvas_object" );
gWGL = canvas.getContext( "experimental-webgl" );
}
catch( e )
{
alert( "WebGL 初始化失敗" );
return;
}
// setup viewport
gWGL.viewport( 0, 0, canvas.width, canvas.height );
// set clear color and depth
gWGL.clearColor( 0, 0, 0, 1 );
gWGL.clearDepth( 10000 );
// set OpenGL ES
gWGL.enable( gWGL.DEPTH_TEST );
gWGL.enable( gWGL.BLEND );
gWGL.blendFunc( gWGL.SRC_ALPHA, gWGL.ONE_MINUS_SRC_ALPHA );
// create shader and data
CreateShader();
CreateData();
// get the matrix location in shader
gWGL.u_mvpMatrixLoc = gWGL.getUniformLocation( gWGL.ShaderProgram, "u_Matrix");
gWGL.mvpMatrix = new CanvasMatrix4();
// set projection matrix
gWGL.ProjectionMatrix = new CanvasMatrix4();
gWGL.ProjectionMatrix.lookat( 0, 0, 7, 0, 0, 0, 0, 1, 0 );
gWGL.ProjectionMatrix.perspective( 30, canvas.width / canvas.height, 1, 10000 );
// main loop
setInterval( display, 30 );
}
這部分的程式和之前的差異,就是上方標記成黃色的區塊,主要是:
- 加入 depth 以及 blending 的參數設定。
- 取得 shader 程式中 matrix 的位址
- 加入了 gWGL.ProjectionMatrix、也就是 projection matrix 的計算。這邊是設定 look at 以及 perspective 投影。
而 vertex shader 主要就是加入對於每個 vertex 的投影計算了∼除了加入 u_Matrix 這個 uniform 變數外,vertex attribute 也多加了 color 的部分;不過由於這個範例沒有考率 lighting,所以也不需要各 vertex 的 normal、也沒有對 color 進行計算。而這部分的程式則是變成:
<script id="vs_02" type="x-shader/x-vertex">
uniform mat4 u_Matrix;
attribute vec4 vPosition;
attribute vec4 vColor;
varying vec4 v_Color;
void main()
{
gl_Position = u_Matrix * vPosition;
v_Color = vColor;
}
</script>
Fragment shader 的部分,則是將 vertex shader 計算(實際上也沒算就是了)完的顏色直接輸出。
<script id="fs_02" type="x-shader/x-fragment">
varying vec4 v_Color;
void main()
{
gl_FragColor = v_Color;
}
</script>
在完成 shader 程式之後,則是 CreateData()、產生所需要的方塊的部分:
function CreateData()
{
gWGL.RenderObject = {};
// create vertex
gWGL.enableVertexAttribArray( gWGL.getAttribLocation( gWGL.ShaderProgram, "vPosition" ) );
gWGL.RenderObject.oVertex = gWGL.createBuffer();
gWGL.bindBuffer( gWGL.ARRAY_BUFFER, gWGL.RenderObject.oVertex );
var vertices = new WebGLFloatArray(
[ 1, 1, 1, -1, 1, 1, -1,-1, 1, 1,-1, 1, // v0-v1-v2-v3 front
1, 1, 1, 1,-1, 1, 1,-1,-1, 1, 1,-1, // v0-v3-v4-v5 right
1, 1, 1, 1, 1,-1, -1, 1,-1, -1, 1, 1, // v0-v5-v6-v1 top
-1, 1, 1, -1, 1,-1, -1,-1,-1, -1,-1, 1, // v1-v6-v7-v2 left
-1,-1,-1, 1,-1,-1, 1,-1, 1, -1,-1, 1, // v7-v4-v3-v2 bottom
1,-1,-1, -1,-1,-1, -1, 1,-1, 1, 1,-1 ] // v4-v7-v6-v5 back
);
gWGL.bufferData( gWGL.ARRAY_BUFFER, vertices, gWGL.STATIC_DRAW );
gWGL.vertexAttribPointer( 0, 3, gWGL.FLOAT, false, 0, 0 );
// create color
gWGL.enableVertexAttribArray( gWGL.getAttribLocation( gWGL.ShaderProgram, "vColor" ) );
gWGL.RenderObject.oColor = gWGL.createBuffer();
gWGL.bindBuffer( gWGL.ARRAY_BUFFER, gWGL.RenderObject.oColor );
var colors = new WebGLUnsignedByteArray(
[ 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, // v0-v1-v2-v3 front
1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, // v0-v3-v4-v5 right
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, // v0-v5-v6-v1 top
1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, // v1-v6-v7-v2 left
1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, // v7-v4-v3-v2 bottom
0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1 ] // v4-v7-v6-v5 back
);
gWGL.bufferData( gWGL.ARRAY_BUFFER, colors, gWGL.STATIC_DRAW );
gWGL.vertexAttribPointer( 1, 4, gWGL.UNSIGNED_BYTE, false, 0, 0 );
// create index
gWGL.RenderObject.oIndex = gWGL.createBuffer();
gWGL.bindBuffer( gWGL.ELEMENT_ARRAY_BUFFER, gWGL.RenderObject.oIndex );
var indices = new WebGLUnsignedByteArray(
[ 0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9,10, 8,10,11, // top
12,13,14, 12,14,15, // left
16,17,18, 16,18,19, // bottom
20,21,22, 20,22,23 ] // back
);
gWGL.bufferData( gWGL.ELEMENT_ARRAY_BUFFER, indices, gWGL.STATIC_DRAW );
gWGL.RenderObject.numIndices = indices.length;
}
這部分的程式主要分成三段,除了上一個範例裡有的 vertex 的位置資料外,還要另外產生顏色資料以及 index array 的資料;而同時,也要將這些資料傳到 GPU 裡。
最後,則是 display() 的部分了∼
function display()
{
// Clear the canvas
gWGL.clear( gWGL.COLOR_BUFFER_BIT | gWGL.DEPTH_BUFFER_BIT );
// Compute a model/view matrix.
gWGL.mvpMatrix.makeIdentity();
gWGL.mvpMatrix.rotate( currentAngle, 0, 1, 0 );
gWGL.mvpMatrix.rotate( 20, 1, 0, 0 );
// Coompute the model-view * projection matrix
gWGL.mvpMatrix.multRight( gWGL.ProjectionMatrix );
// pass the matrix into shader
gWGL.uniformMatrix4fv( gWGL.u_mvpMatrixLoc, false, gWGL.mvpMatrix.getAsWebGLFloatArray() );
// Draw the cube
gWGL.drawElements( gWGL.TRIANGLES, gWGL.RenderObject.numIndices, gWGL.UNSIGNED_BYTE, 0 );
// Finish up.
gWGL.flush();
// update angle
currentAngle = incAngle;
if( currentAngle > 360 )
currentAngle -= 360;
}
這部分主要的差異,就是多加入了當下的 model view matrix 和 projecttion matrix 的計算、並將計算後的結果傳入 vertex shader,接著再透過 drawElements() 將 index array 形式的方塊資料畫出來了。
而另外為了讓這個方塊可以旋轉,所以在最後還要加上旋轉角度的計算;這邊的做法是每畫一次後,就會把現在的角度(currentAngle)再加上 incAngle,然後用以計算下一次的 model view matrix。
最後的結果網頁,可以在這邊下載。