WebGL教程

歡迎來到WebGL教程第三課。這次我們將學習如何移動物體。本課基於NeHe OpenGL教程的第4課。

如果你的瀏覽器已經支持WebGL,請點擊此處,你將看到本課WebGL的現場版;如果不支持,你從此處可以獲取一個支持WebGL的瀏覽器。





一點提示:這些課程是面向那些具有一定編程知識但沒有實際3D圖形開發經驗的開發人員的;其目的是讓你對代碼層上發生了什麼事 有很好的理解,以便你能儘可 能快地創建出自己的3D網頁。如果你還沒看過第一課和第二課的話,你應該在開始本課之前看 看它們。因爲我在這裏僅僅解釋與第二課代碼的不同之處和一些新的代碼。



同之前的課程一樣,本課也可能存在一些缺陷和錯誤概念。如果你發現有什麼不對的話,請留言讓我知道,我會糾正它。



獲取這個例子的代碼有兩種方法:一種就是當你觀看實時版的時候點擊“查看源碼”的鏈接,另一種是你從 GitHub的代碼庫獲取(包括 以後課程的代碼)。對於任一種方式,一旦你獲得源碼,你就可以用你喜歡的文本編輯器打開並查看它。



在講解代碼之前,我要澄清一件事。在webGL中製作3D場景的動畫是十分容易的——你只需重複地繪製該場景,每次都把它繪製 得不一樣。這對許多讀者來說也許是一件顯而易見的事,但當我開始學習webGL時,對此卻有點驚訝。可能對於那些第一次使用webGL繪製3D圖形的人來 說也有點吃驚吧。起初讓我困惑的原因是,我想象它應該使用更高級的抽象方法,即它應該這樣運行:“告訴3D系統有一個正方形在點X處(我起初繪製它的地方),接着移動這個正方形,告訴3D系統該正方形已經移動到了點Y處。”然而事實是:“你告訴3D系統有一個正方形在點X處,接着在下次繪製它時,告訴系 統它在點Y處,再下一次它在點Z處”,以此類推。



我希望上面這段話至少能讓部分人有一個更加清晰的概念(如果它讓人迷惑的話,請留言給我,我將刪除它:-)



由於到目前爲止我們的示例代碼一直使用drawScene函數來繪 制物體,並一直使用如下代碼:

setInterval(drawScene, 15);



來告訴JavaScript每隔15ms就調用一次drawScene函 數,爲了製作場景動畫並讓三角形和正方形移動,我們所需要做的就是改變此處代碼以便每次調用drawScene函數時,它繪製的物體略有不同。



這意味着我們對第二課中的代碼改動最大地方在drawScene函數中,因此讓我們就從這裏(大約在index.html文件三分之二的地方)開始吧。第一件需要注意的事就是在函數聲明之前,我們要定義兩個新的全局變量。

var rTri = 0;

var rSquare = 0;



這兩個變量分別用來 跟蹤三角形和正方形的旋轉。它們都從0度開始旋轉,然後角度將隨時間增加——稍後你將看到如何進行——,從而漸漸旋轉(提示:在一個三維程序中像這樣使用 全局變量並不是很好的應用。我將在第九課中使用一種更合適的方式來構造程序。)



對drawScene函數的另一個改變在我們繪製三角形的點。我將通過上下文的方式來介紹繪製三角形的所有代碼,新添加的代碼用紅色標示:

    perspective(45, gl.viewportWidth / gl.viewportHeight,0.1, 100.0);
    loadIdentity();

    mvTranslate([-1.5, 0.0, -7.0])

    mvPushMatrix();
    mvRotate(rTri, [0, 1, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER,triangleVertexPositionBuffer);
   gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,triangleVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0,triangleVertexPositionBuffer.numItems);

    mvPopMatrix();

爲了解釋這些代碼,讓我們回到第一課。在那裏,我曾經說過:在OpenGL中,當我們繪製一個場景時,你要告訴它用“當前 的”旋轉方法在“當前的”位置上繪製每一個物體——因此,例如你說“向前移動20個單位,旋轉32度,接着繪製機器人”,這非常有用,因爲你能將“繪製機器人”的代碼封裝在一個函數中,然後,只需在調用函數前改變“平移/旋轉”參數,就能輕鬆繪製機器人。

你應該記得這個當前狀態存儲於一個模型視圖矩陣中。考慮到這點,下面這個函數調用的目的是十分顯而易見的:

mvRotate(rTri, [0, 1, 0]);



改變存儲在模型視圖矩陣中的當前旋轉狀態,圍繞垂直軸(通過第二個矢量參數指定)旋轉rTri度。這意味着繪製三角形時,三角形將被旋轉rTri度。mvRotate函數就像我們在第一課中看到的mvTranslate函數一樣使用JavaScript編寫——稍後我們再來看它。



那麼,mvPushMatrix和mvPopMatrix這兩個函數又是做什麼的呢?通過函數名,你可能會猜到他們也和模型視圖矩陣有關。回到先前繪製機器人的那個例子,處在最高層的代碼需要移至A點,繪製機器人,接着從A點做些偏移並繪製一個茶壺。繪製機器 人的代碼可能會給模型視圖矩陣帶來各種各樣的變化;它可能從機器人的身體開始繪製,然後向下移動到腿部,接着向上移動到頭部,最後繪製完胳膊。問題是如果你在繪製完機器人之後試圖移動至偏移點,那此時的移動不是相對於A點,而是相對於最後繪製的點。這就意味着如果機器人擡起了它的胳膊,那麼茶壺的位置也將向上移動。這可不是什麼好事情。



現在需要做的,是在你開始繪製機器人之前將模型視圖矩陣的狀態存儲起來,之後再將其恢復。當然,這就是mvPushMatrix和mvPopMatrix這兩個函數所做的事情。mvPushMatrix將矩陣放入一個堆棧,而 mvPopMatrix放棄當前矩陣並從堆棧頂部取出一個矩陣,然後恢復其狀態。使用堆棧意味着我們可以嵌套任意多層的繪圖代碼,每層對模型視圖矩陣進行操作,然後再將其恢復。因此,一旦繪製好旋轉的三角形,我們應該用mvPopMatrix來恢復模型視圖矩陣,所以代碼如下:

mvTranslate([3.0, 0.0, 0.0]);



...在一個非旋轉的參考幀中移動整個場景。(如果對此仍然不是很清楚的話,我建議你拷貝該代碼並移除push/pop代碼看看會發生什麼,然後再重新運行它,不同的效果很快就會顯現)



因此,對代碼的這三處改變將使得三角形圍繞垂直軸的中心旋轉,但並不影響正方形。同樣也有三行類似的代碼使得正方形圍繞水平軸的中心旋轉。

    mvPushMatrix();
    mvRotate(rSquare, [1, 0, 0]);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
   gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute,squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexColorBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexColorAttribute,squareVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLE_STRIP, 0,squareVertexPositionBuffer.numItems);

    mvPopMatrix();
  }

... 以上就是drawScene函數所有變動的地方。



顯然,爲了將場景製作成動畫我們還需要做另一件事,這就是隨時間的變化而改變rTri和 rSquare的數值,以使每次繪製的場景略有不同。我們使用animate函數來做到這一點,它就像drawScene函數一樣每隔一段時間就被調用一次。其代碼如下:

var lastTime = 0;
  function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
      var elapsed = timeNow - lastTime;
      rTri += (90 * elapsed) / 1000.0;
      rSquare += (75 * elapsed) / 1000.0;
    }
    lastTime = timeNow;

  }



一種製作場景動畫的簡單方法是在每次調用animate時增加固定值(這也是我編寫本教程所參考的的原始教程所使用的方法),但在這裏我將使用一種我認爲比較好的方法:用距離函數上次被調用的時間長短來決定一個物體旋轉多少。特別地,三角形每秒旋轉90度,正方形每秒旋轉75度。這樣做的好處是:無論你們的機器有多快,大家在場景中看到的都是相同的移動速度;只是在較慢的機器上圖像會發生抖動。這對於像本例這樣一個簡單演示並不重要,但是對於像遊戲或類似的應用就比較重要了。



接下來的變化是我們必須每隔一段時間有規律地調用animate,就像對drawScene所做的那樣。我們創建一個名爲tick的新函數,該函數用來調用 這兩個函數並且自身每隔15毫秒被調用一次。

function tick() {
    drawScene();
    animate();
  }

  function webGLStart() {
    var canvas =document.getElementById("lesson03-canvas");
    initGL(canvas);
    initShaders();
    initTexture();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clearDepth(1.0);

    gl.enable(gl.DEPTH_TEST);
    gl.depthFunc(gl.LEQUAL);

    setInterval(tick, 15);

  }



這就是在繪製並製作場景動畫代碼中所有變動的地方。現在,讓我們來看看需要添加的代碼。首先是mvPushMatrix和mvPopMatrix:

  var mvMatrixStack = [];

  function mvPushMatrix(m) {
    if (m) {
      mvMatrixStack.push(m.dup());
      mvMatrix = m.dup();
    } else {
      mvMatrixStack.push(mvMatrix.dup());
    }
  }

  function mvPopMatrix() {
    if (mvMatrixStack.length == 0) {
      throw "Invalid popMatrix!";
    }
    mvMatrix = mvMatrixStack.pop();
    return mvMatrix;

  }



這裏並沒有什麼令人吃驚的地方。我們用一個列表來保留矩陣堆棧並適當地定義push和pop。



現在,來看看mvRotate函數:

function mvRotate(ang, v) {
    var arad = ang * Math.PI / 180.0;
    var m = Matrix.Rotation(arad, $V([v[0], v[1],v[2]])).ensure4x4();
    multMatrix(m);

  }



創建一個矩陣用以表示旋轉的所有困難工作通過Sylvester庫來完成——這非常簡單。

發佈了15 篇原創文章 · 獲贊 1 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章