研究內容大概如下:
- 以用戶視角進入三圍世界
- 控制三圍可視空間
- 裁剪
- 處理物體的前後關係
- 繪製三圍的立方體
上面會講到如何使用模型矩陣、視圖矩陣、以及該投影矩陣的使用,並且最後會繪製一個三維的立方體;
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_BUFFER
和gl.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
的話,立方體的每個面都是白色的;
<純白色立方體>
此時顯示的效果是看不到棱角分明的,但顯示世界中白色的立方體是可以看到棱角分明的效果的,這是因爲此時的立方體沒有燈光信息的計算,所以都是白色的,下一章會講解燈光在三維世界中的重要作用;