WebGL - 可視空間,深度處理,頂點索引

研究內容大概如下:

  • 以用戶視角進入三圍世界
  • 控制三圍可視空間
  • 裁剪
  • 處理物體的前後關係
  • 繪製三圍的立方體

上面會講到如何使用模型矩陣、視圖矩陣、以及該投影矩陣的使用,並且最後會繪製一個三維的立方體;

1、立方體由三角形構成

在這裏插入圖片描述

如圖所示,三維物體都是由三角面構成的,因此逐個的去繪製三角形就能繪製出三維物體。但是三維跟二維有一個顯著的區別,繪製二維只需要考慮x,y軸,而繪製三維物體的時候,需要考慮物體的 深度信息,還需要定義三維世界的觀察者,在什麼地方,朝哪裏看,視野有多寬,能夠看多遠;

2、視點和視線

三維物體具有深度,深度也就是z軸,因此需要考慮一下觀察者的兩點問題:

  • 觀察方向,即觀察者自己再什麼位置,在看場景的那一部分?
  • 可視距離,即觀察者能夠看多遠?

視點:觀察者所處的位置

視線:從視點出發沿觀察方向的射線

webgl系統中,默認情況下視點處於(0,0,0)原點,視線時z軸負半軸;

3、視點、觀察目標點和上方向

爲了確保觀察者的狀態也就是相機的屬性,需要確定兩項信息

  • 視點:觀察者所在的三維空間的位置,視線的起點
  • 觀察目標點:被觀察目標所在的點
  • 上方向:最終繪製在屏幕上的影像中的向上的方向

如圖所示以上三個條件確定一個觀察者(相機)
在這裏插入圖片描述

webgl中可以通過三個矢量,創建一個視圖矩陣,然後將矩陣傳給頂點着色器,矩陣表示觀察者的視點,觀察目標點,上方向等信息

  • 視圖矩陣:影響顯示在屏幕上的視圖

通過《webgl編程指南》這本書提供的矩陣方法庫,可以設置視圖矩陣

Matrix4.setLookAt(eyeX,eyeY,eyeZ,atX,atY,atZ,upX,upY,upZ)

// 視點:(eyeX,eyeY,eyeZ)
// 觀察點:(atX,atY,atZ)
// 上方向:(upX,upY,upZ)

// 示例
var viewMatrix = new Matrix4();

viewMatrix.setLookAt(0, 0, 0, 0, 0, -1, 0, 1, 0);

// 視點矢量:vec3(0, 0, 0) 視點爲原點 webgl 默認
// 觀察點矢量:vec3(0, 0, -1) 即z軸負方向
// 上方向矢量:vec3(0, 1, 0) 以y軸爲上方向 

1、設置視圖矩陣

頂點着色器

attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix;
varying vec4 v_Color;
void main(){
    gl_Position = u_ViewMatrix * a_Position;
    v_Color = a_Color;
}

片元着色器僅僅是接收varying變量,此處不再寫出;

javaScript將視圖矩陣傳遞給頂點着色器

// 獲取u_ViewMatrix變量的存儲地址
var u_ViewMatrix = gl.getUniformLocation(gl.program,'u_ViewMatrix');

// 設置視點,視線,和上方向
var viewMatrix = new Matrix4();
viewMatrix.setLookAt(0.20, 0.25, 0.25, 0, 0, 0, 0, 1, 0);

// 將視圖矩陣傳給u_Matrix變量
gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);

示例:設置觀察者狀態

設置了觀察者的狀態,可以調整看物體的角度以及遠近距離,但是如果我們不通過調正觀察者的狀態,而對三維對象進行平移、旋轉等變換,也是可以視線上面的行爲,因爲兩者是相對的;

改變觀察者的狀態對整個世界進行平移和旋轉變換本質上是一樣的,都可以使用矩陣來描述

4、指定視點觀察旋轉後的三角形

矩陣乘以頂點座標,得到的結果是頂點經過旋轉變換之後的新座標,也就是說用旋轉矩陣乘以頂點座標,就可以得到旋轉後的頂點座標;

<旋轉後的頂點座標> = <旋轉矩陣>*原始矩陣

因此可以可以得到以下組合

<從視點看上去的旋轉後的頂點座標> = <視圖矩陣>*<旋轉矩陣>*<原始頂點座標>

除了旋轉矩陣,還可以使用 平移、縮放等基本變換或他們的組合此時的矩陣被稱爲 模型矩陣

所以可以寫成這樣

<視圖矩陣>*<模型矩陣>*<原始頂點座標>

着色器程序可以這樣寫

attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_ViewMatrix; // 視圖矩陣
uniform mat4 u_ModeMatrix; // 模型矩陣
varying vec4 v_Color;
void main(){
	gl_Position = u_ViewMatrix * u_ModelMatrix * a_Position;
	v_Color = a_Color;
}

其實視圖和模型矩陣可以寫到一起,簡稱模型視圖矩陣

<模型視圖矩陣> = <視圖矩陣>*<模型矩陣>

<模型視圖矩陣> * <頂點座標>

着色器代碼如下

...
uniform mat4 u_ModelViewMatrix; 
void mina(){
    gl_Position = u_ModelVIewMatrix * a_Position;
    ...
}
    

兩個矩陣相乘

var modelViewMatrix = viewMatrix.multipy(modelMatrix);//兩個矩陣相乘

顯示效果和之前一樣

示例:<模型視圖矩陣>

5、可視範圍

雖然我們可以將三維物體放到三維空間中的任何地方,但只有當它在可視範圍內時webgl纔會繪製它,不繪製可視範圍外的對象,是基本的降低程序開銷的手段,因爲繪製可視範圍外的東西是沒有意義的,即使繪製出來也不會在屏幕上顯示;

除了水平和垂直範圍的限制,webgl還限制觀察者的 可視深度,就是能看多遠,包括水平視角、垂直視角;

1、可視空間

可視空間:水平視角、垂直視角和可視深度定義了可視空間
可視空間由兩種:

  • 長方體可視空間,盒裝空間,由 正射投影產生;
  • 四棱錐/金字塔可視空間,由 透視投影產生;

透視投影下產生的三維場景看上去更有深度感覺,我們平時觀察的真世界用的也是透視投影;
而正射投影與物體的遠近無關,在機械建模以及建築類等技術中應用廣泛;

1、盒裝空間(正射投影)

在這裏插入圖片描述

盒裝可視空間,由前後兩個矩形表面來確定,分別稱爲 近裁剪面遠裁剪面
<canvas>上顯示的就是可視空間中物體在近裁剪面上的投影;

通過《webgl編程指南》這本書作者視線工具類,cuon-matrix.js提供的Matrix4.setOrtho()可以用來設置投影矩陣,定義盒裝可視空間

Matrix4.prototype.setOrtho = function (left, right, bottom, top, near, far){
    // ...
}

// left, right -- 指定遠近裁剪面的左邊界和有邊界
// bottom,top -- 指定遠近裁剪面的上邊界和下邊界
// near,top -- 指定近裁剪面和遠裁剪面的位置,即可視空間的近遠邊界

這個矩陣被叫做 正射投影矩陣

加入正射投影矩陣示例如下:

頂點着色器部分

...
uniform mat4 u_ProjMatrix;
..
void main(){
	gl_Position = u_ProjMatrix * a_Position;
}

然後通過js代碼設置正射投影的參數

var projMatrix = new Matrix4();
projMatrix.setOrtho(-1, 1, 01, 1, 0, 100);

示例:<可視區域>

通過鍵盤左右鍵可以調整視角

2、可視空間(透視投影)

在正射投影的可視空間中,不管三角形與視點的距離是近是遠,它由多麼大就會畫出來多麼大,但是場景沒有深度感,因此想要模擬顯示的觀察效果就需要使用 透視投影,來定義可視空間;

定義透視投影可視空間
在這裏插入圖片描述

透視投影可視空間也有視點、視線、近裁剪面和遠裁剪面,同樣也使用投影矩陣來表示;

Matrix4.prototype.setPerspective = function (fov, aspect, near, far){
    //...
}
// fov -- 指定垂直視角,即可視空間頂面和地面間的夾角,必須大於0
// aspect -- 指定近裁剪面的寬高比(寬度/高度)
// near,far -- 指定近裁剪面和遠裁剪面的位置,即可視空間的近邊界和遠邊界(near和far都必須大於0)

這個矩陣叫做 透視投影矩陣

示例程序如下:

...
uniform mat4 u_ViewMatrix;
uniform mat4 u_ProjMatrix;
...
void main(){
	gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;
	...
}

js代碼如下

...
var viewMatrix = new Matrix4();// 視圖矩陣
var projMatrix = new Matrix4();// 投影矩陣

// 計算視圖矩陣和投影矩陣
viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);// 設置視點,目標點和上方向
projMatrix.setPrespective(30, canvas.width/canvas.height, 1, 100);

2、模型、視圖和投影矩陣

通過以下式子可以得出

<投影矩陣> * <視圖矩陣> * <模型矩陣> * <頂點座標>

如果投影矩陣和模型矩陣都是單位陣那麼由上面式子可得

<模型視圖投影矩陣> * <頂點座標>

上面三個矩陣變成一個矩陣即 模型視圖投影矩陣

因此示例如下:

頂點着色器部分

...
uniform mat4 u_MvpMatrix;
...
void main(){
    gl_Position = u_MvpMatrix * a_Position;
    ...
}

js代碼部分

var modelMatrix = new Matrix4(); // 模型矩陣
var viewMatrix = new Matrix4();  // 視圖矩陣
var projMatrix = new Matrix4();  // 投影矩陣
var mvpMatrix = new Matrix4();   // 模型視圖投影矩陣

...

// 計算模型視圖投影矩陣
mvpMatrix.set(projMatrix).multipy(viewMatrix).multipy(modelMatrix);
// 然後將模型視圖投影矩陣傳遞給着色器即可

帶有set前綴的方法,就使得矩陣相乘的結果又寫入到 mvpMatrix中;

6、對象前後關係

真實世界中,如果兩個盒子一前一後放到桌子上,前面的盒子會擋住後面的盒子,之前繪製三角形的示例貌似webgl可以處理遮擋關係,其實webgl爲了加速繪製操作,是按照頂點在緩衝區的順序來處理他們的,之前的示例的頂點是故意定義成那樣的;

webgl在默認情況下,會按照緩衝區中的順序去繪製圖形,而且後繪製的圖形覆蓋先繪製的圖形;

如果場景中的對象不發生運動,觀察者的狀態也是唯一的,那麼這種做法沒有問題,但是如果視點移動,從不同角度看物體的畫,那麼就不能不事先決定對象出現的順序;

1、隱藏面消除

因此webgl提供了 隱藏面消除的功能,這個特性會幫助我們爲您消除那些被遮擋的隱藏面,因此我們可以放心的繪製場景而不必擔心物體在緩衝區的順序,這個功能已經被內嵌在 webgl中了,只需要開啓就行;

1、開啓隱藏面

遵循以下兩步

  • 開啓隱藏面消除功能

    gl.enable(gl.DEPTH_TEST)

  • 在繪製之前,清除深度緩衝區

    gl.clear(gl.DEPTH_BUFFER_BIT)

gl.enable(cap)方法的使用

可以去查看MDN接口文檔介紹 https://developer.mozilla.org/zh-CN/docs/Web/API/WebGLRenderingContext/enable

方法作用是:讓webgl開啓某種特性,具體參數如下

參數 介紹
gl.BLEND 激活片元的顏色融合計算,參見WebGLRenderingContext.blendFunc()
gl.CULL_FACE 激活多邊形正反面剔除,參見WebGLRenderingContext.cullFace()
gl.DEPTH_TEST 激活深度檢測,需要更新深度緩衝區,參見WebGLRenderingContext.depthFunc()
gl.DITHER 激活在寫入顏色緩衝區之前,抖動顏色成分
gl.POLYGON_OFFSET_FILL 激活添加多邊形片段的深度偏移值,參見WebGLRendering.polygonOffset()
gl.SAMPLE_ALPHA_TO_COVERAGE 激活通過alpha值決定的臨時覆蓋之計算(抗鋸齒)
gl.SAMP_COVERAGE 激活使用臨時覆蓋值,位和運算片段的覆蓋值,參見WebGLRenderingContext.sampleCoverage()
gl.SCISSON_TEST 激活裁剪檢測,即丟棄剪裁矩形範圍外的片段,參見WebGLRenderingContext.scisson()
gl.STENCIL_TEST 激活模板測試並且更新模板緩衝區,參見WebGLRenderingContext.stenciFunc()

使用之前可以進行檢測該參數是否已經開啓,使用以下方法

WebGLRenderingContext.isEnabled()

gl.enable(gl.DITHER);//開啓

gl.isEnabled(gl.DITHER);// true 該屬性已經開啓

2、清空深度緩衝區

深度緩衝區是一個隱藏對象,其作用就是幫助webgl進行隱藏面消除

webgl在顏色緩衝區中,繪製幾何圖形,繪製完成之後將顏色緩衝區顯示到canvas上,要將隱藏面消除就必須知道幾何圖形的深度信息,而深度緩衝區就是用來存儲深度信息的,由於深度方向通常是z軸方向,所以有時候也曾爲z緩衝區;

在繪製每一幀之前,都必須清除深度緩衝區,以消除繪製上一幀時在其中留下的痕跡;

gl.clear(gl.DEPTH_BUFFER_BIT)

當然還需要清除顏色緩衝區,因此可以這樣寫

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// 類似的同時清除兩個緩衝區都可以使用按位或符號

示例程序如下

// 開啓隱藏面消除,也就是深度檢測
gl.enable(gl.DEPTH_TEST);
// 清空顏色和深度緩衝區
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

示例:<深度檢測消除隱藏面>

在任何三維場景中,都應該開啓隱藏面消除,並在適當的時候清空深度緩衝區,通常是在繪製每一幀之前;

2、深度衝突

消除隱藏面時webgl的一項複雜而又強大的特性,然而當兩個幾何體的表面極爲接近時,就會出現新的問題,使得表面看上去斑斑駁駁的,如下圖所示,這種現象稱爲 深度衝突

在這裏插入圖片描述

之所以會產生深度衝突,是因爲兩個表面極爲接近,深度緩衝區有限的精度已經不能區分,哪個在前哪個在後,嚴格的來說在創建三維模型的時候是可以避免深度衝突,但是在運動着的物體,是很難避免的;

webgl提供一種被稱爲 多邊形偏移的機制來解決這個問題,該機制會自動在z值上加一個偏移量,偏移量的值,由物體表面相對於觀察者視線的角度來確定;

1、啓用多邊形偏移

只需要兩步

  • gl.enable(gl.POLYGON_OFFSET_FILL) 啓用多邊形偏移
  • gl.polygonOffset(1.0, 1.0) 在繪製之前用來計算偏移量的參數

激活參數gl.POLYGON_OFFSET_FILL啓用多邊形偏移,然後通過設置偏移量參數;

gl.polygonOffset(factor,units)

指定加到每個頂點繪製z值上的偏移量,偏移量按照公式m * factor + r * units計算,其中m表示頂點所在表面相對於觀察者的視線的角度,而r表示硬件能夠區分兩個z值之差的最小值

啓用多邊形偏移消除深度衝突後的效果

示例:<多邊形偏移消除深度衝突>

7、繪製立方體

立方體是三維模型,但它是由多個二維三角面組成的,下面就開始繪製有八個頂點組成的立方體,每個頂點定義顏色後,立方體表面的顏色會根據頂點顏色內插出來,形成一種光滑的漸變效果;

立方體的每一個面都有兩個三角形組成,每個三角形有3個頂點,所以每個面都要用到6個頂點,立方體6個面,因此一共需要36個頂點;

但問題是立方體實際上只有八個頂點,而我們卻定義了36個,這是因爲每個頂點都會被多個三角形所共用;

webgl提供了一種通過頂點索引的方式,來解決頂點複用的問題,通過gl.drawELements()代替gl.drawArrays()進行繪製,就能夠避免重複定義頂點;

1、頂點索引

之前都是使用gl.drawArrays()進行繪製,現在使用gl.drawElements()來進行繪製,這種繪製模式需要在gl.ELEMENT_ARRAY_BUFFER中,指定頂點索引值,gl.ARRAY_BUFFERgl.ELEMENT_ARRAY_BUFFER兩者最終要的區別就是,gl.ELEMENT_ARRAY_BUFFER管理着具有索引結構的三維模型數據;

gl.drawElements(mode,count,type,offset)

執行着色器,按照mode參數指定的方式,根據綁定到gl.ELEMENT_ARRAY_BUFFER的緩衝區只能的索引值繪製圖形;

  • mode:指定繪製的方式
  • count:指定繪製頂點的個數(整形數)
  • offset:指定索引數組中開始繪製的位置,以字節爲單位

因此,需要將頂點索引,寫入到緩衝區,並綁定到gl.ELEMENT_ARRAY_BUFFER上;

此處把創建緩衝區對象的過程封裝一個方法

// 初始化緩衝區對象
function initArrayBuffer(gl, data, num, type, attribute) {

    // 創建緩衝區
    var buffer = gl.createBuffer();

    if (!buffer) {

        console.log('創建緩衝區失敗!');
        return false;

    }

    // 寫入數據到緩衝區
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

    // 將緩衝區對象分配給屬性變量
    var a_attribute = gl.getAttribLocation(gl.program, attribute);

    if (a_attribute < 0) {

        console.log('獲取變量存儲地址失敗' + attribute);
        return false;

    }

    gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);

    // 啓用緩衝區屬性變量
    gl.enableVertexAttribArray(a_attribute);

    return true;

}

然後首先定義頂點數組,再定義頂點索引數組

var indices = new Uint8Array([       // Indices of the vertices
    0, 1, 2, 0, 2, 3,    // front
    4, 5, 6, 4, 6, 7,    // right
    8, 9, 10, 8, 10, 11,    // up
    12, 13, 14, 12, 14, 15,    // left
    16, 17, 18, 16, 18, 19,    // down
    20, 21, 22, 20, 22, 23     // back
]);

創建頂點索引緩衝區

var indexBuffer = gl.createBuffer();
if (!indexBuffer) return -1;

然後向頂點索引緩衝區中綁定數據

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

示例效果如下:

<通過頂點索引繪製立方體>

打開示例後你會發現立方體在按照y做自轉運動,這是因爲我們在代碼中循環渲染,來改變它自身的旋轉角度,重要代碼如下:

// 循環渲染 旋轉立方體
function animate() {

    requestAnimationFrame(animate);

    mvpMatrix.rotate(-1, 0, 1, 0);// 繞y軸進行順時針旋轉,每次旋轉-1度(角度制)

    gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

    // 每一幀都需要清空顏色和深度緩衝區
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);

}

animate();

2、每個面指定顏色

最然已經證明了gl.drawElements()是高校的繪製三維圖形的方式,但是我們無法通過將顏色定義在索引值上,顏色仍然是依賴於頂點的;

我們希望每個立方體的表面都是不同的單一顏色,而非漸變色效果,或者是紋理圖像,因此需要把每個面的顏色或紋理信息寫入三角形列表、索引和頂點數據中;

頂點着色器是逐頂點的計算,接收的是逐頂點的信息,因此如果想要指定表面的顏色,也需要將顏色定義爲逐頂點的信息;

此時需要單獨創建一個顏色緩衝區,用來放顏色值的信息;

var colors = new Float32Array([
    ......
]);
// 然後將顏色數據寫入緩衝區
var color_num = initArrayBuffer(gl, color, 3, gl.FLOAT,'a_Color');

效果案例如下:

<指定顏色信息的立方體>

此時如果我們把所有的顏色值都變成1.0的話,立方體的每個面都是白色的;

<純白色立方體>

此時顯示的效果是看不到棱角分明的,但顯示世界中白色的立方體是可以看到棱角分明的效果的,這是因爲此時的立方體沒有燈光信息的計算,所以都是白色的,下一章會講解燈光在三維世界中的重要作用;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章