WebGL學習筆記【一】概述及三角形


  最近開始研究起WebGL來,發現以前在圖形學課上看javascript還真是不太理智的做法。

  這一系列學習筆記是自己學習過程的總結,難免有錯和不正確,希望發現問題的同學可以“慘無人道”的指出。

  WebGL簡單說就是OpenGL在瀏覽器端的實現。那OpenGL又是什么?OpenGL就是一組提供了生成2d、3d圖形的API。

  其實,要想用WebGL來真正“畫”出一些東西,首先要對圖形學的一些基本概念有理解。

  簡明圖形學

  圖形學,指利用計算機來生成圖形(creation)、繪制或者叫渲染圖形(render)、處理圖形(manipulation)的學科。

  (一)

  首先是生成的問題。我對圖形生成的理解,就是怎么樣來描述各種需要進行繪制的圖形,尤其是那些復雜的人物啊、建築啊等等。

  在圖形學中,我們從描述最最基本的點(vertex)開始,描述一個點,圖形學中的點和幾何意義上的點稍有不同,除了基本的位置外,可能還會需要顏色、頂點發向量(用於計算光照)、紋理坐標(用於貼圖)等額外的信息。正如圖像是由像素構成,圖形學中的圖形其實可以理解成由點構成。比如:我們按照一個人的形狀,在三維空間中定義出這個人的輪廓,就已經大致可以描述這個人了。那有了點,我們就可以把它們連起來,可是這么多的點該怎樣連?圖形學里會將點連成三角形(polygon)。然后這許多的三角形就可以組成模型的骨架,線框網格(wire mesh)。

  比如下面這個有點像人的惡心模型, 我們可以看到組成它的點連接起來,形成了模型的網格(mesh):

 這就是模型的最最基本的描述,如果再加上一些材質、打上燈光什么的,它看起來會是這樣:

  (二)

  大致說了圖形描述的問題,那接着是繪制。也就是怎么樣把三維的東西繪制到二維的屏幕。

  這里,我們想到,要是有一個圖形渲染機,我們把我們對模型的描述,也就上面說的一堆點的信息,統統倒入這個機器,機器一陣處理后就可以在屏幕上繪制出模型,那就好了。確實也有這樣一個機器,它的輸入可以理解成一堆點(用數組之類的表示),輸出就是我們屏幕上閃閃動人的模型。其實,這個機器可以理解成顯卡。那它是怎么做到的?

  首先是我們怎么樣把點的信息告訴顯卡,OpenGL(或者WebGL)這時終於是派上用場了,它封裝了底層的調用,提供給了我們和設備無關的函數來進行這些操作;顯卡拿到了點的信息,就必須進行處理,因為最后屏幕上顯示的是像素信息呀,這些處理簡單說只有2個部分:點處理+像素處理(又叫光柵化),下面詳細敘述;生成了像素數據后,這些像素數據會保存到一塊叫幀緩存的內存中,然后,這些像素數據就最終在屏幕上顯示了。

  上面我們說,處理過程分為點處理和像素處理。點處理,主要是根據輸入的點的信息再進行一些必要的計算,最終產生每個點實際在屏幕上的顯示位置;像素處理,則是真正計算每個像素應該顯示的顏色。

  接着,我們需要想想更詳細的東西。

  我們通過點來描述模型,那必然會需要一個坐標系,否則,點的位置、法向量該如何描述。這個參照的坐標系一般稱為模型坐標系(或者object/local space)。那現在我們有了很多個模型,就需要有一個放置它們的世界,或者叫場景吧,這時也需要一個坐標系來定位模型的位置,這個坐標系就叫做世界坐標系(或者world space)。世界如此之大,我們或許不可能把整個世界都顯示出來,那有點貪心了,這時我們需要一台攝像機,或者稱它觀察者吧,透過它的眼睛來看我們的世界,它一般會形成一個觀察體,在這個觀察體里的才顯示,不在的就當作隱形了,這個坐標系就叫做觀察坐標系(或者view space)。處在觀察者注視下的世界其實還是一個3d的世界,可是我們需要顯示在2d平面上啊,這時就需要一個很重要的變換過程了,叫做投影(projection),有正交投影和透視投影二種,投影變換后3d的世界就會被投影到一個2d的投影平面上,並且同時基本保持了在3d空間的性質,如:近大遠小等等。最后,我們再進行一個視口(viewport)變換,將投影窗口變換為屏幕上的一個矩形區域(其實就是我們顯示圖形的窗口)。這些變換后,我們應該就會開始計算視口中每個像素該顯示的顏色,然后繪制出來,整個繪制過程就完成了。這一系列操作,可以稱為渲染管線。就好象linux的管道一樣,這個管道輸出的信息作為下一個管道輸入的信息,不斷進行下去。

  這個過程,其實簡單說來,就是將我們輸入的點從一個坐標系變換到另一個坐標系,那這種變換,矩陣運算就是最拿手的了。比如:在world space會對模型進行一些平移、旋轉、縮放操作,都可以定義成一個變換矩陣;在view space,要將世界坐標中的點變換為觀察坐標中的點,也是定義成一個變換矩陣;投影變換、視口變換也都是變換矩陣。這些細節,OpenGL的API其實都有函數能夠直接使用,但是WebGL又稍微有點特殊,需要我們深入到渲染管線。

  (三)

  總結下,渲染管線的流程大致是這樣:

  點信息(vertices data) -> 世界坐標系中的變換,如:平移、縮放、選擇(world space transformation) -> 世界坐標系轉為觀察坐標系,需要定義攝像機(view space transformation) -> 投影變換(projection transformation) -> 裁剪(clipping,攝像機外的就不顯示),背面剔除(backface culling,背對攝像機的那個面不顯示) -> 齊次裁剪空間 -> 視口變換(viewport transformation) -> 光柵化(rasterize,可能會包括:貼圖生成、光照生成、場景霧生成等) -> 最終像素顏色 -> 幀緩存(frame buffer) -> 顯示到屏幕

  在這個管線中,大部分都是顯卡在做工作,但是,我們卻有機會直接對顯卡編程,來操作這些數據,可以編程的2個部分分別叫做:vertex shader和fragment shader,其實就是分別對應對點的操作和對像素的操作。其中,vertex shader是對輸入的每個點依次執行,生成該點的最終位置;fragment shader對每個像素操作,生成該像素的顯示顏色;這2個shader之間也可以傳遞數據,不過只能是vertex shader傳遞給fragment shader,因為總是先執行vertex shader(比如:vertex shader內先根據點的法向量計算一些光照參數,然后傳給fragment shader生成最終有光照考慮的像素顏色;vertex shader直接傳遞貼圖坐標給fragment shader,fragment shader根據貼圖坐標計算加上了貼圖考慮的像素顏色等)。使用WebGL惡心的地方就是,就算只是顯示一個三角形,都需要自己寫shader。

  對應渲染管線的話,插入這2個可編程部件后,大致應該是這樣:

  點信息(vertices data) ->

  vertex shader { 世界坐標系中的變換,如:平移、縮放、選擇(world space transformation) -> 世界坐標系轉為觀察坐標系,需要定義攝像機(view space transformation) -> 投影變換(projection transformation) -> 其他一些計算 } -> 

 裁剪(clipping,攝像機外的就不顯示),背面剔除(backface culling,背對攝像機的那個面不顯示) -> 齊次裁剪空間 -> 視口變換(viewport transformation) ->

 fragment shader { 貼圖生成、光照生成、場景霧生成、其他一些計算 } ->

 光柵化(rasterize,可能會包括:貼圖生成、光照生成、場景霧生成等) -> 最終像素顏色 ->

 幀緩存(frame buffer) -> 顯示到屏幕

  WebGL繪制三角形

  簡單的總結完圖形學的基本概念后,我們可以動手寫程序了。就好象第一個程序都是Hello World,個人覺得圖形學里的Hello World應該就是畫一個三角形。

  我們可以先來看看OpenGL寫的話,或許會是這樣的(引用自:http://fly.cc.fer.hr/~unreal/theredbook/chapter01.html):

#include <whateverYouNeed.h>

main() {

OpenAWindowPlease();

glClearColor(0.0, 0.0, 0.0, 0.0);
glClear(GL_COLOR_BUFFER_BIT);

glColor3f(1.0, 1.0, 1.0);

glOrtho(-2.0, 2.0, -2.0, 2.0, -1.0, 1.0);

glBegin(GL_TRIANGLES);
glVertex2f(0.0, 1.0);
glVertex2f(-1.0, 1.0);
glVertex2f(1.0, -1.0);
glEnd();

glFlush();

KeepTheWindowOnTheScreenForAWhile();
}

  glClearColor/glClear那里是清屏,為繪制做准備;glOrtho那句就是定義一個正交投影的“攝像機”;glBegin/glEnd那里就是通過三個點定義了一個三角形;glFlush就是將幀緩存畫到屏幕。挺簡潔呀~但是,WebGL沒有像glBegin/glEnd這種東西,也不會很好心的自己幫你把點根據你定義的攝像機進行合適的變換,我們需要做更多的工作。

  以下代碼,引用自MDN的文檔(https://developer.mozilla.org/en/WebGL),文檔的demo代碼真是太亂了,然后做了適當的調整和修改。為了偷懶,我就對自己學習時覺得不好理解的部分進行一下記錄,全部代碼可以在這里獲取:https://github.com/KohPoll/webgl-learn

  (一)關於shader及program的創建

function getShader(gl, id) {
var shaderScript = document.getElementById(id),
theSource = '',
shader = null;

if (!shaderScript) return shader;

theSource = text(shaderScript);

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 shader;
}

gl.shaderSource(shader, theSource);
gl.compileShader(shader);

if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return null;

return shader;
}

function initShaders() {
var fragmentShader, vertexShader;

fragmentShader = getShader(gl, 'shader-fs');
vertexShader = getShader(gl, 'shader-vs');

shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, fragmentShader);
gl.attachShader(shaderProgram, vertexShader);
gl.linkProgram(shaderProgram);

if (gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
gl.useProgram(shaderProgram);
}
}

  shader的創建步驟:1.創建一個shader(gl.createShader);2.獲取shader的源代碼(這里是從dom節點中獲取)並進行設置(gl.shaderSource);3.編譯shader(gl.compileShader)。

  將shader“注入”到可編程組件program的步驟:1.創建一個program(gl.createProgram);2.依附shader到program上(gl.attachShader);3.鏈接program(gl.linkProgram);4.使用該program(gl.useProgram)。

  (二)關於點信息的創建(buffer的使用)

  我們上面說,可以將點的描述傳送給顯卡,這些信息其實是存放在內存里面的。

function initBuffers() {
var vertices, colors;

// vertex buffer
vertices = [
0.0, 1.0, 0.0,
-1.0, -1.0, 0.0,
1.0, -1.0, 0.0
];

verticesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);

// vertex color buffer
colors = [
1.0, 0.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0,
0.0, 0.0, 1.0, 1.0
];

verticesColorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
}

  大致步驟是這樣的:1.我們將點信息存放在數組里;2.然后創建buffer(gl.createBuffer),並綁定它(gl.bindBuffer),以便可以對它進行操作;3.設置數據(gl.bufferData)。PS:那個gl.STATIC_DRAW的意思我也不是很理解,大概是這樣的:STATIC_DRAW保存的數據內容只被程序定義一次,GL繪制命令可以使用多次;DYNAMIC_DRAW保存的數據內容將被程序重復定義,GL繪制命令可以使用多次。

  (三)關於渲染

function drawScene() {
var projectMatrix, worldMatrix, viewMatrix,
pUniform, wUniform, vUniform;

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// {{ phase 1
// bind to a shader attribute so the shader code can access.
vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition');
gl.enableVertexAttribArray(vertexPositionAttribute);
gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);

vertexColorAttribute = gl.getAttribLocation(shaderProgram, 'aVertexColor');
gl.enableVertexAttribArray(vertexColorAttribute);
gl.bindBuffer(gl.ARRAY_BUFFER, verticesColorBuffer);
gl.vertexAttribPointer(vertexColorAttribute, 4, gl.FLOAT, false, 0, 0);
// }}


// {{ phase 2
//projectMatrix= makePerspective(75, canvas.width / canvas.height, 1.0, 100.0);
projectMatrix = makeOrtho(-10.0, 10.0, -10.0, 10.0, 1.0, 100.0);

//modelviewMatrix= Matrix.I(4);
worldMatrix = Matrix.I(4);
worldMatrix = worldMatrix.x(Matrix.RotationZ(0.6).ensure4x4());

viewMatrix = Matrix.I(4);
viewMatrix = viewMatrix.x(Matrix.Translation($V([0.0, 0.0, -95.0])).ensure4x4());

// generate and deliver to the shader.
pUniform = gl.getUniformLocation(shaderProgram, 'uPMatrix');
gl.uniformMatrix4fv(pUniform, false, new Float32Array(projectMatrix.flatten()));

wUniform = gl.getUniformLocation(shaderProgram, 'uWMatrix');
gl.uniformMatrix4fv(wUniform, false, new Float32Array(worldMatrix.flatten()));

vUniform = gl.getUniformLocation(shaderProgram, 'uVMatrix');
gl.uniformMatrix4fv(vUniform, false, new Float32Array(viewMatrix.flatten()));
// }}

gl.drawArrays(gl.TRIANGLES, 0, 3); // (mode, first, count of point used to draw)
}

  這里有很多重要的東西。

  首先是js怎么和shader交互的問題,就是怎么把相應的數據傳遞給shader使用。簡單說明下shader的變量的“類型”,attribute只有vertex shader有,是通過程序(js)傳遞給它的變量。uniform兩種shader都有,而且是不能改變的,可以理解成常量;varying是vertex shader向fragment shader傳遞數據,fragment shader接受數據的方式。

  然后,我們看上面的phase 1部分的代碼,這里就是將剛剛設置到buffer中的點信息作為attribute傳遞給shader使用的代碼。

  1.調用vertexPositionAttribute = gl.getAttribLocation(shaderProgram, 'aVertexPosition')會返回一個“位置”,這個位置可以理解成shader中對名為aVertexPosition這個attribute的引用(指針);

  2.使用gl.enableVertexAttribArray(vertexPositionAttribute)開啟attribute的數組傳遞(大概是這樣吧?);

  3.綁定我們創建並填充了點信息的那塊verticesBuffer,gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);

  4.讓步驟1中的shader的attribute指向這塊buffer,gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0),我們之前創建buffer時傳遞的數據都是一維的,那第2個參數3就用來說明每3個數組元素組成一個attribute(其實就是一個vector3,代表點的位置)。表示顏色的attribute的過程與此類似。

  接着,我們看phase 2部分的代碼,這里就是設置觀察變換矩陣、投影變換矩陣,並作為uniform傳遞給shader使用的代碼。

  1.projectMatrix = makeOrtho(-10.0, 10.0, -10.0, 10.0, 1.0, 100.0),創建正交投影矩陣,用於投影變換;

  2.worldMatrix = Matrix.I(4);worldMatrix = worldMatrix.x(Matrix.RotationZ(0.6).ensure4x4());創建worldMatrix,並繞z軸旋轉,這里其實就是在進行世界坐標系中的變換(平移、選擇、縮放);

  3.viewMatrix= Matrix.I(4);modelviewMatrix = modelviewMatrix.x(Matrix.Translation($V([0.0, 0.0, -95.0])).ensure4x4());創建viewMatrix,並進行平移,這里之所以要進行平移,是因為我們的點的z軸設置的都是0,而我們的攝像機的z軸范圍是1到100,進行這個平移,以便攝像機能看到這些點,實際上就是從世界坐標系到觀察坐標系的一個變換;

  4.pUniform = gl.getUniformLocation(shaderProgram, 'uPMatrix'),與attribute類似,會返回一個“位置”,這個位置可以理解成shader中對名為uPMatrix這個uniform的引用(指針);

  5.gl.uniformMatrix4fv(pUniform, false, new Float32Array(projectMatrix.flatten()));設置這個uniform的數據,那個flatten是將2維的矩陣轉成1維數組的方法。其它的uniform設置與此類似。

  最后,我們調用gl.drawArrays(gl.TRIANGLES, 0, 3); // (mode, first, count of point used to draw),告訴程序以三角形的模式繪制,使用3個點。關於模式的參數,可以參考這里:http://fly.cc.fer.hr/~unreal/theredbook/figures/fig2-6.gif

 (四)關於shader

  一切看起來都挺好,但是,shader呢?沒有shader來進行真正的處理,傳遞這些數據是一點用處也沒有的啊。我們就來依次來看看2個shader。

  首先是vertex shader:

<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 aVertexPosition;
attribute vec4 aVertexColor;
//attribute vec2 aTextureCoord;

uniform mat4 uVMatrix;
uniform mat4 uWMatrix;
uniform mat4 uPMatrix;

varying lowp vec4 vColor;
//varing lowp vec2 vTextureCoord;

void main(void) {
gl_Position = uPMatrix * uVMatrix * uWMatrix * vec4(aVertexPosition, 1.0);
vColor = aVertexColor;
//vTextureCoord = aTextureCoord;
}
</script>

  可以看到,vertex shader我們定義了2個attribute,分別表示點的位置和點的顏色信息;3個uniform,分別表示世界變換矩陣、觀察變換矩陣、投影變換矩陣,這些值通過程序傳遞給shader。我們還定義了一個varying,用於傳遞給fragmeng shader顏色信息(因為vertex shader實際上無法操作像素,所以把顏色信息傳遞下去比較合理)。

  gl_Position = uPMatrix * uVMatrix * uWMatrix * vec4(aVertexPosition, 1.0);就是對每一個點進行對應的變換,先是世界坐標中的變換(乘以uWMatrix);然后是觀察坐標變換(乘以uVMatrix),最后投影變換(乘以uPMatrix)。然后賦值給shader內置的變量gl_Position,表示點的最終計算出的位置。

  vColor = aVertexColor;將傳遞進來的點的顏色信息,直接賦值給vColor,以便fragment shader使用。

  然后,看看fragment shader:

<script id="shader-fs" type="x-shader/x-fragment">
//uniform sampler2D uSampler;

varying lowp vec4 vColor;
//varing lowp vec2 vTextureCoord;

void main(void) {
gl_FragColor = vColor;
//gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord,t));
}
</script>

  可以看到,fragment shader定義了一個varying,用於接收vertex shader傳遞的顏色信息;然后將這個顏色信息賦值給shader的內置變量gl_FragColor,表示頂點顏色。光柵化時,實際上,會對頂點表示的這個圖元(三角形)的像素顏色進行插值,然后確定出最終顏色,用來插值的就是頂點顏色。

  所以,最后的效果就是這樣:

 





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM