【译】Lesson 1: 一个三角形和一个方块
【聲明】:本系列文章譯自:http://learningwebgl.com/blog/?page_id=1217, 感謝Giles Thomas;限于我的英文水平,本文翻譯并不一定嚴格遵從原文,但也不會嚴重背離原文(如果有,請務必知會我一下,多謝);如果處于非商業目的,你可以自由轉載并修改完善之;一切目的都是促進交流。如果能注明出處就最好不過了~~
-----------------------------------------
歡迎來到我的第一篇WebGL教程。這篇教程基于著名的Nehe Opengl教程的第二課。將向你展示怎樣在網頁中畫一個三角形和一個方塊。這個可能引不起你的多大興趣,但是確實是WebGL的很好的基礎介紹;如果你理解了它怎樣工作,剩下的就非常簡單了。。。
下面是本課運行在支持WebGL的瀏覽器上的效果:
猛擊這里可以看到真實運行的WebGL版本(前提是你有一個支持其運行的瀏覽器);如果沒有,猛擊這里。
更多的關于其如何工作的在下面。。。
一個小知會:這些教程的目標人群是有適當的編程經驗,但是沒有多少3d圖形編程的經驗的人;目標是讓你知其然并能知其所以然的理解代碼里將的是什么,然后你就可以盡快的開發自己的3D網頁程序。我自己邊學習WebGL邊寫這個的,所以可能會有一些錯誤;使用這些代碼你需要自己承擔風險。但是我正在修改并修正我所知道的bug和誤解,如果你發現了任何不對的請留言讓我知道。
有兩種途徑獲取這個例子的代碼:當你觀看實時運行的版本時,通過瀏覽器的”查看源代碼“即可;或者如果你使用GitHub,你就可以從倉庫中克隆一份出來(包括以后的所有課程)。無論哪種方式,當你獲取了代碼之后,用你喜歡的文本編輯器中加載閱讀。初一見的時候難免會覺得代碼不堪入目,即使你以前已經有了一些初步的經驗,比如:OpenGL。剛一開始我們就定義了兩個Shader,這通常被看做是比較先進的技術。。。但是不要氣餒,它只是紙老虎。
像很多程序一樣,這個WebGL頁面一開頭就定義了一堆會被后面高層邏輯調用的底層函數。為了解釋他們,我將從底下的代碼講起,所以如果你正在順序的跟讀代碼,請跳轉到最底下。
你將看到如下的HTML代碼:
<body onload="webGLStart();"><a href="http://learningwebgl.com/blog/?p=28"><< Back to Lesson 1</a><br />
<canvas id="lesson01-canvas" style="border: none;" width="500" height="500"></canvas>
<br/>
<a href="http://learningwebgl.com/blog/?p=28"><< Back to Lesson 1</a><br />
</body>
這個就是整個頁面的body部分 -- 所有其他的東西都用JavaScript實現(但是如果你通過”查看源代碼“來取得代碼,你將看到很多額外的代碼段用來解析我的網站,這些你可以跳過)。顯然我們可以放更多的HTML代碼到<body>標簽中來讓我們的WebGL圖像來構建一個常見的網頁,但對于這個簡單的demo我們只需要一個回到本blog文章的鏈接和一個容納3d實時圖形的<canvas>標簽。Canvas 是HTML5的新特性,就是他們在網頁中支持了JavaScript的各種新的繪制元素,包括2D和3D(webGL)。在這個標簽中,除了一些簡單的層級屬性我們不再涉及canvas的其他任何具體東西,取而代之的是將WebGL的初始化代碼集中到一個叫webGLStart的Javascript函數中,從代碼可以看出這個函數當頁面加載的時候被調用。
現在讓我們翻到這個函數并看一下代碼:
function webGLStart() {var canvas = document.getElementById("lesson01-canvas");
initGL(canvas);
initShaders();
initBuffers();
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.enable(gl.DEPTH_TEST);
drawScene();
}
代碼中首先調用函數初始化WebGL和我之前提到過的shaders,并將我們想繪制3d圖形的canvas元素作為參數傳遞到前者中,然后用initBuffers初始化一些緩存;所謂緩存就是保存我們將要繪制的三角形和方塊的地方 -- 后面我們將詳述之。然后,執行了一些基本的WebGL初始化函數,比如:將畫布清除為黑色并開啟深度測試(即Z-Test),這些步驟都是通過調用gl對象上的方法實現的 -- 后面我們將看到它是怎樣初始化的。最后,調用函數drawScene;就像你看到的名字一樣這個函數使用buffers繪制出那個三角形和方塊。
因為其對于理解頁面怎樣工作具有很重要的作用,后面我們將講到initGL和initShaders;但是首先,我們先看一下initBuffers和drawScene兩個函數。
首先initBuffers:
var triangleVertexPositionBuffer;var squareVertexPositionBuffer;
我們定義兩個全局變量來操作緩存。(真實情況下不可能為每個對象都定義一個全局變量,但是這里因為我們才剛開始,所以一切從簡)
下一步:
function initBuffers() {triangleVertexPositionBuffer = gl.createBuffer();
我們為三角形的頂點位置創建一個緩沖區,頂點就是3d空間中定義我們需要繪制的形體的點。對于我們的三角形,我們將有三個頂點。這些緩沖區實際上就是顯卡上的一些顯存;通過在初始化時將頂點位置傳遞到顯卡上,然后當我們繪制場景時,本質上講就是告訴WebGL去繪制之前告訴它要繪制的那些東西,這樣我們就可以使得我們的代碼有很高的執行效率,特別是當我們開始讓場景動起來時,想通過每秒繪制幾十次來使得物體動起來時。當然,在現在的例子里因為只有三個頂點,所以將他們壓到顯卡里是不會有太多消耗的 -- 但是當我們處理擁有成千上萬頂點的大模型的時候,這樣的處理方式將會顯示出其優勢的。接著:
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);這一行告訴WebGL接下來所有buffer上的操作都使用我們指定的這個buffer。這里有一個概念“當前數組緩沖區”,并且所有函數操作都是基于這個“當前”緩沖區而非讓你自己指定哪個你想操作的buffer。奇怪,但是我相信在這個的背后肯定是有性能方面的原因的。
var vertices = [0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];
接著,我們用javascript的list來定義頂點位置??梢钥吹竭@是一個中心點在(0,0,0)的等腰三角形。
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);現在,我們基于前邊的Javascript的list創建一個Float32Array的對象,并且告訴WebGL使用它來填充“當前緩沖”,當然它就是我們的triangleVertexPositionBuffer。我們將在未來的課程里詳細討論Float32Arrays,但是現在你只需要知道我們是有方法可以將Javascript的list轉化成WebGL可以用來填充緩沖區的東西的。
triangleVertexPositionBuffer.itemSize = 3;triangleVertexPositionBuffer.numItems = 3;
最后關于buffer的操作時為其設置兩個新的屬性。這不是WebGL內置的什么東西,但是后面將會非常有用。Javascript一個很好的特性是其不需要顯示的聲明一個你想設置的屬性(對此,褒貶不一)。所以即使buffer之前并不存在itemSize和numItems兩個屬性,現在它也有了。我們稱作這樣的buffer為9元素buffer,實際上表示其右三個獨立的頂點位置(numItems),每個位置有三個數組成(itemSize)。
現在我們已經完整建立了三角形的緩沖區,接著是方塊的:
squareVertexPositionBuffer = gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);
vertices = [
1.0, 1.0, 0.0,
-1.0, 1.0, 0.0,
1.0, -1.0, 0.0,
-1.0, -1.0, 0.0
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
squareVertexPositionBuffer.itemSize = 3;
squareVertexPositionBuffer.numItems = 4;
}
所有這些代碼都是很顯然的 -- 方塊有四個頂點位置而不是三個,所以數組要大一些并且numItems不一樣。
OK,這就是我們將兩個物體的頂點坐標壓到顯卡所需要做的一切?,F在讓我們看看drawScene,這個就是我們使用那些buffer進行繪制的地方。一步一步來:
function drawScene() {gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
第一步是用viewport函數告訴WebGL畫布的大小;很后面的課程里我們將講到為什么這個非常重要;現在,你只需要知道在你繪制之前需要調用這個函數就好了。接著,我們清除畫布:
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);…然后:
mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);這里我們設置好我們要觀察場景的透視角度。默認,WebGL使用正交投影繪制物體(作者單詞拼錯了。。。)。為了使得遠處的物體看起來小一些,我們需要告知WebGL我們使用的透視角度。對于當前場景,我們說我們的FOV是45°,并告訴其我們畫布的長寬比,并且說遠近裁剪面的距離分別是0.1個單位和100個單位。
正如你所看到的,這個透視的東東用的是一個叫mat4的模塊中的函數設置的。并且涉及到一個叫pMatrix的變量。(好多廢話)
現在我們設置好了透視角度,然后我們繼續到繪制的部分:
mat4.identity(mvMatrix);【一大段關于gl矩陣運算的廢話,不翻了。。?!可厦孢@句就是初始化一個矩陣變量留作后面用,identity就是單位陣。
OK,接著講代碼,將我們的三角形繪制到畫布的左手邊:
mat4.translate(mvMatrix, [-1.5, 0.0, -7.0]);通過設置矩陣為單位陣將物體移動到3d空間的中心,然后我們將三角形向左移1.5個單位(即沿X軸負向移動),并且向里移動7個單位(即,遠離觀察者,沿Z軸負向)。
接下來的就開始了實際的繪制工作:
gl.bindBuffer(gl.ARRAY_BUFFER, triangleVertexPositionBuffer);gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, triangleVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);你應該記得為了使用我們的buffer,必須首先調用gl.bindBuffer來指定一個當前緩沖區,然后調用其上的操作代碼。這里我們選擇我們的triangleVertexPositionBuffer,然后告訴WebGL緩沖區里的內容用來做頂點位置坐標。這里可以看到我們告訴WebGL緩沖區里的每個元素都是3個數字的長途(itemSize)。
接下來,就是:
setMatrixUniforms();這里讓WebGL考慮到我們的WVP矩陣,這個是需要的,因為所有關于矩陣的東東都不是WebGL內置的??梢钥闯鲞@個的方法是:你可以通過改變mvMatrix的值來移動物體,但是所有這些都是在Javascript私有空間內執行的,setMatrixUniforms,將計算結果傳遞到顯卡,此函數將在文件的后面定義。
一旦這些都搞定,WebGL就擁有了一個被當做頂點坐標的數組,并且知道要設置的矩陣。下一步就是告訴它怎么處理這些東東:
gl.drawArrays(gl.TRIANGLES, 0, triangleVertexPositionBuffer.numItems);或者,換句話講:從之前我給你的被當做三角形的頂點數組中的0號元素起至第numItems個元素止,依次畫數組中的頂點
一旦這個操作完成,WebGL就已經畫好了我們的三角形。下一步,繪制方塊:
mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);我們首先將我們的MV矩陣向右移動3個單位。記得吧,之前我們已經向左移動了1.5個單位,并距離屏幕向里移動了7個單位,所以最后我們就向右移動了1.5個單位,并向里移動了7個單位。接著:
gl.bindBuffer(gl.ARRAY_BUFFER, squareVertexPositionBuffer);gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, squareVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);即,告訴WebGL使用我們的方塊buffer當做其頂點坐標。。。
setMatrixUniforms();。。。我們將mvp矩陣重新壓入顯卡(這樣就可以使得mvTranslate生效),這樣最后我們就可以:
gl.drawArrays(gl.TRIANGLE_STRIP, 0, squareVertexPositionBuffer.numItems);畫點。【這里作者自問自答的解釋了一堆為什么使用TRANGLE_STRIP,咱就不廢話了】
不管怎樣,一旦執行到這里,我們就完成了我們的drawScene函數。
}如果你已經走到這里了,那你完全可以準備開始實驗了。復制代碼到一個本地文件,既可以從GitHub或者直接從實時版本獲取;如果是后者,你需要index.html和glMatrix-0.9.4.min.js兩個文件。先在本地運行確保能工作,然后嘗試修改一些頂點坐標;特別是,現在的場景還很平;嘗試改變方塊的Z坐標為2或者-3,然后看其移動到后邊和前邊變小和變大。或者嘗試改變其中的一個或者2個,在透視角度下觀察期變形。。。。
...
OK,你應該回來了,現在讓我們來看那些支持函數?!竞竺嬗钟幸粚U話。。?!?/p>
首先來看initGL這個函數,下面是它的實現:
var gl;function initGL(canvas) {
try {
gl = canvas.getContext("experimental-webgl");
gl.viewportWidth = canvas.width;
gl.viewportHeight = canvas.height;
} catch(e) {
}
if (!gl) {
alert("Could not initialise WebGL, sorry :-(");
}
}
這個函數非常簡單。你可能已經注意到,initBuffers和drawScene函數經常涉及一個叫做gl的對象,很明顯該對象牽涉某種WebGL核心。這個函數獲取了這一核心——稱為WebGL context,它是通過使用標準的context名稱請求canvas賦予的(你可能猜測,在某個時候context的名字將從“experimental-webgl”變成“webgl”,那時我將更新我的課程和博客)。一旦得到上下文,我們將使用viewport函數告訴WebGL畫布的大小;在后面的課程中我們會回過來講講這為什么會很重要;現在,你只需知道該函數調用時需要畫布的大小。一旦函數執行完,GL的context就設置好了。
調用InitGL之后,webGLStart函數繼續調用initShaders。這個函數用來初始化著色器xu。我們稍后再回來看這個函數,因為我們首先要來看看處理模型視圖矩陣的應用函數。下面是其代碼:
var mvMatrix = mat4.create();var pMatrix = mat4.create();我們定義mvMatrix變量來保存模型視圖矩陣,接著定義使用該變量的loadIdentity和mvTranslate函數以及multMatrix應用函數。如果你知道JavaScript,你就會明白我們在此使用的矩陣代數函數并不是一般的JavaScript的API;事實上它們由先前提到的兩個文件所支持,這兩個文件位于HTML網頁的頂部:
第一個文件,?Sylvester,是一個處理矩陣和矢量代數的開源JavaScript庫,第二個文件是由Vladimir Vuki?evi?開發的Sylvester庫的一系列擴展。
總之,在這些簡單函數和有用的庫文件的幫助下,我們可以維護模型視圖矩陣。這里需要說明另一個矩陣,就是我前面提到的投影矩陣。你也許記得,WebGL不內置perspective函數。但是模型視圖矩陣封裝了像移動和旋轉物體這樣的過程,這正是矩陣擅長的事情。你現在肯定已經猜到,投影矩陣正是這么一個矩陣。下面是其代碼:?
var pMatrix;
function perspective(fovy, aspect, znear, zfar) {
pMatrix = makePerspective(fovy, aspect, znear, zfar);
}
makePerspective函數是另一個定義在glUtils.js文件中的函數;它返回一個我們需要應用在指定的全景透視圖中的特定矩陣。
現在,我們除了setMatrixUniforms函數外已經瀏覽了所有函數,正如我先前所說,這個函數將模型視圖矩陣和投影矩陣從JavaScript中轉移到WebGL中,并與著色器相關。由于它們相互關聯,所以我們將從一些背景知識開始。
你也許會問:什么是著色器?在三維圖形的歷史上,著色器曾經“名副其實”過:在繪制一個場景之前,它指示系統如何渲染或著色。然而,隨著時間的推移,它的功能日益增強,以至于如今要這樣定義它才更為合適:著色器是這樣一些代碼,在一個場景開始繪制之前,它能對場景的任何部分做任何處理。這的確十分有用,由于它運行在圖形卡上,所以它能很快運行且能很便利地做各種變換,哪怕是在這個簡單的例子中。
我們引入著色器是為了一個簡單的WebGL例子(在OpenGL教程中這至少算是“中級”),該例子能運行在圖形卡上且使用著色器獲得WebGL系統。它把模型視圖矩陣和投影矩陣應用到場景中,而不需要使用相對較慢的JavaScript來移動場景中的每一個點和每一個三角形頂點。這相當有用并且值得額外的開銷。
下面是如何設置它們。正如你所記得的,webGLStart函數調用initShaders函數,那么讓我們一步一步地來看一看:? var shaderProgram;
function initShaders() {
var fragmentShader = getShader(gl, "shader-fs");
var vertexShader = getShader(gl, "shader-vs");
shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
alert("Could not initialise shaders");
}
gl.useProgram(shaderProgram);
正如你所見,它使用getShader函數來獲得兩個著色器:一個片段著色器和一個頂點著色器,接著將兩者綁定在一個WebGL“程序”上。一個程序是一段放置在系統WebGL上的代碼;你可以把它視作一種運行在圖形卡上的特定方式。正如你所期望的,你可以將它和一些著色器聯系在一起,每個著色器都可以視為程序中的一個代碼片段;確切地說,每個程序可以擁有一個片段著色器和一個頂點著色器。讓我們簡單地看一下:
shaderProgram.vertexPositionAttribute =?
gl.getAttribLocation(shaderProgram, "aVertexPosition");
?gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
?一旦設置好程序并綁定了著色器,函數將得到一個“屬性”的引用,該屬性存儲在vertexPositionAttribute對象中。我們再次利用JavaScript把任一字段設置在任一對象上;默認情況下對象沒有vertexPositionAttribute字段,但是對于我們來說將兩個值保留在一起是很方便的,因此我們僅設置程序中新字段的屬性。
?那么,vertexPositionAttribute是做什么的呢?也許你還記得,我們在drawScene函數中使用過它;如果你回過去看一看從適當的緩沖區設置三角形頂點位置的那段代碼,你將看到我們所做的就是將緩沖區與該屬性關聯在一起。稍后你將明白這是什么意思。現在,只需注意到我們也使用gl.enableVertexAttribArray函數來指示WebGL使用一個數組來為該屬性提供數值。
?shaderProgram.pMatrixUniform =?gl.getUniformLocation(shaderProgram, "uPMatrix");
?shaderProgram.mvMatrixUniform =?gl.getUniformLocation(shaderProgram, "uMVMatrix");
}
initShaders函數所做的最后一件事就是從程序中獲取兩個多的值,這兩個變量稱作uniform變量,我們很快會再遇見它們,但現在你只需注意到,正如屬性一樣,我們為了方便而將其存儲在對象中。
現在,我們來看看getShader函數:
function getShader(gl, id) {var shaderScript = document.getElementById(id);
if (!shaderScript) {
return null;
}
var str = "";
var k = shaderScript.firstChild;
while (k) {
if (k.nodeType == 3)
str += k.textContent;
k = k.nextSibling;
}
var shader;
if (shaderScript.type == "x-shader/x-fragment") {
shader = gl.createShader(gl.FRAGMENT_SHADER);
}
else if (shaderScript.type == "x-shader/x-vertex") {
shader = gl.createShader(gl.VERTEX_SHADER);
}
else {
return null;
}
gl.shaderSource(shader, str);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
這是另一個比看起來要簡單的函數。我們要做的是在HTML網頁中尋找一個元素,其具有與傳入參數匹配的ID,取出其內容并基于其類型創建一個片段渲染器或者一個頂點渲染器(以后我們將更多地解釋它們的區別),接著將其傳入到WebGL中編譯成可以在圖形卡上運行的形式。接下來,代碼進行出錯處理,最后完成整個處理。當然,我們只能在JavaScript中將渲染器定義為字符串而不能從HTML中提取——通過這樣做,我們使其更易讀,因為它們被定義為網頁中的腳本,就像它們本身就是JavaScript一樣。
看完這個以后,我們應該來看看渲染器的代碼:
關于這些你要記住的第一件事就是:這些代碼不是用JavaScript所寫,即使這兩種腳本語言的祖先十分相似。事實上,它們使用一種特殊的與C語言有很大關系的著色器語言(當然,JavaScript也是如此)所寫。第一個著色器——即片段著色器——什么也不做;它簡單地規定了被繪制的物體將被繪制成白色(怎么給物體著色是下一節課程的話題)。第二個著色器有點意思,它是一個頂點著色器——還記得吧,它是一段圖形卡上的代碼,能用一個頂點完成它想做的任何事。與之相關聯的是,它有兩個uniform變量:uMVMatrix和uPMatrix。uniform變量十分有用,因為它們能在著色器之外獲得——實際上是在包含它們的程序之外,你可能還記得,當時我們從initShaders函數獲得了它們,它們也可以從將其設置為模型視圖矩陣和投影矩陣的代碼(我敢肯定你已經實現了它)中獲得。你可能認為著色器程序是一個對象(在面向對象的場景中),而統一變量是對象中的字段。現在,著色器在每個頂點上調用,頂點作為aVertexPosition參數傳入到著色器的代碼中,由于在drawScene函數中使用vertexPositionAttribute,此時,我們將其屬性與緩沖區關聯在一起。著色器主程序中的這部分代碼只是將頂點與模型視圖矩陣和投影矩陣相乘,然后作為頂點最終位置的結果傳出。webGLStart函數調用initShaders函數,在網頁的腳本中使用getShader函數裝載了片段著色器和頂點著色器,以便它們能被編譯后傳入到WebGL,最終使用WebGL渲染出三維場景。
剩下還沒有說明的代碼是setMatrixUniforms函數,一旦你理解了上面所講的,就很容易理解它。
function setMatrixUniforms() {gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false,
new WebGLFloatArray(pMatrix.flatten()));
gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false,
new WebGLFloatArray(mvMatrix.flatten()));
}
通過引用uniform來表示initShaders中的投影矩陣和模型視圖矩陣,我們將值從JavaScript類型矩陣傳遞給了WebGL。
第一課的內容真的很多,但是希望你(也包括我)能夠理解所有這些基本知識,我們將以此為基礎創建更加有趣的模型:五彩繽紛的、可移動的、真正的三維WebGL模型。為了了解更多關于WebGL的知識,請閱讀第2課。
【發現翻譯果然是一件很累的事情,尤其是作者寫的廢話較多的時候。。。,翻譯最后一段的時候google了一下發現譯言網上已經有這個系列的譯文了。。。,很汗,然后后面的幾個lesson就不想繼續了,但是善始善終本篇還是想完成,所以就繼續參考了一些譯言網的內容,糾正了其一些說法的問題,把shader翻譯成渲染器總是不大對勁,我改成”著色器“了】
轉載于:https://www.cnblogs.com/konlil/archive/2011/03/06/1971905.html
總結
以上是生活随笔為你收集整理的【译】Lesson 1: 一个三角形和一个方块的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: sql中的并、交、差
- 下一篇: 数据结构C语言实现—队列操作