WebGL從2012年開始接觸,后面因為開始專注前端其他方面的事情,慢慢地就把它給遺忘。最近前端開始又流行起繪畫制作,游戲、VR等等又開始引起前端人們的注意。所以,是時候開始重新拾起。
3D繪畫是一個很復雜的數(shù)學物理綜合體,會涉及到很多基礎概念,了解了這些概念后才能進行實際的開發(fā)工作。
基礎概念
- WebGL不同于canvas,它是一個三維繪畫模擬技術,在我們二維的顯示器里,其由深度決定前后的關系,根據(jù)遠近進行放大和縮小,通過坐標變換(模型變換,視圖變換和投影變換)組合描繪出畫面的內容。
- WebGL中,所謂的固定渲染管線(簡單來說是3D渲染所進行的一連串的計算流程)是不存在的,所有的坐標變換必須用自己全部完成,而且,這個計述坐標變換的機制就叫做著色器(Shader),這樣可以由程序員控制的機制就叫做可編輯渲染管線。而著色器又有處理幾何圖形頂點的頂點著色器和處理像素的片段著色器兩種類型。
- 在HTML中,著色器是利用script標簽進行定義存放
<script id="vshader" type="x-shader/x-vertex">
※頂點著色器
</script>
<script id="fshader" type="x-shader/x-fragment">
※片段著色器
</script>
- 3D渲染變換繪制和演變使用的是4x4的矩陣,也就是行數(shù)和列數(shù)同為4的矩陣,在數(shù)學中我們都知道它叫方陣。實際3D渲染時,準備個各種坐標變換的矩陣,然后相乘,將最終得到的矩陣傳給WebGL的頂點著色器。頂點著色器從傳過來的矩陣中,獲得模型的坐標,加工到畫面上顯示出來。也就是說,操作坐標變換的矩陣,就可以決定模型在畫面上如何繪制。
- WebGL的世界里任何東西都是可以描畫,但是描畫的最基本東西也就是點、線段和三角形。頂點,就是三維空間上存在的一個點,包括X,Y,Z坐標信息。頂點的連接順序是判斷3D繪畫多邊形的基準的。順時針連接頂點的多邊形是在外側,而逆時針連接的多邊形在內側。為了減少處理,3D世界里看不到的東西不繪制,而這種機制就叫做遮擋剔除,如果我們設定了遮擋剔除,那么WebGL就只繪制外側看得見的東西,內側所有多邊形就都不會再繪制。
基礎代碼
頁面初始化
<html>
<head>
<title>WebGL TEST</title>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
這段HTML代碼,純粹只是在頁面上放置一個canvas,我們將從這個canvas中獲取context,然后進行WebGL初始化。
獲取context
首先獲取canvas對象并設置其大小
var c = document.getElementById('canvas');
c.width = 500;
c.height = 300;
獲取WebGL的context
// 兼容處理
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
畫面初始化
WebGL的context和普通的canvas是一樣的,包含了繪畫相關的各種各樣的處理對象、函數(shù)、常量和屬性。例如:
// 使用指定常量顏色來清空畫面
gl.clear(gl.COLOR_BUFFER_BIT);
// 使用顏色值(RGBA)來清空畫面
gl.clearColor(0.0,0.0,0.0,1.0);
上面幾步簡單的代碼塊就能夠完成一個幾步的WebGL使用,整個代碼運行起來就是一個大小為500*300的黑色塊畫面。
認識GLSL
我們已經(jīng)知道WebGL是無法利用固定渲染管線的,所以代替它的是可編輯渲染管線中的一種著色語言,叫做GLSL(OpenGL Shading Language)。
GLSL使用C語言為基礎,并且有自己獨立的語法。WebGL編程難點之一也就是這個GLSL的使用。
- WebGL里有頂點著色器和片段著色器兩種著色器。無論哪一種都可以使用GLSL來編寫。頂點著色器和片段著色器是相互依賴的,缺一不可,并且首先被調用的是頂點著色器。我們可以把頂點相關的情報信息(位置、法線、紋理坐標、顏色等)傳遞給頂點著色器去處理我們要繪制的頂點。而片段著色器則是決定畫面用什么顏色輸出。片段著色器英文是fragment,其實就是斷片,碎片的意思。而畫面上的像素實際上就是最小的斷片,總而言之,片段著色器操作的是顏色。
- GLSL編寫基礎
首先,不管是頂點著色器還是片段著色器,都必須定義一個main函數(shù),函數(shù)里記錄你要做的處理。而且,頂點著色器的話,必須要把頂點信息傳給一個叫做gl_Position的變量。例如:
// attibute修飾符是用來接收不同頂點傳來的不同信息
// vec*表示的是向量,*部分是一個2~4的數(shù)字,vec3表示的是一個3維的向量。其元素是浮點型
// position變量定義頂點信息
attribute vec3 position;
void main(void) {
gl_Position = position;
}
在WebGL中,頂點相關處理就是坐標變換,模型變換、視圖變換和投影變換也就是頂點著色器的工作之一。一般來說,WebGL程序中,首先生成模型、視圖、投影的各個矩陣,然后進行合并,最后將得到的坐標變換的矩陣傳給頂點著色器。這時,我們定義傳遞這些矩陣值:
attribute vec3 position;
// uniform修飾符是用來接收所有頂點一致的情報信息
// mat*表示的是方陣,可指定范圍2~4,mat4表示的是4x4的方陣。其元素是浮點型
uniform mat4 mvpMatrix;
void main(void) {
gl_Position = mvpMatrix * position;
}
頂點著色器與片段著色器的連接
GLSL里還有一個重要的修飾符,也就是varying修飾符,是用來連接頂點著色器和片段著色器之間的橋梁。
比如要把繪制的模型變成半透明,要怎么做?
方法雖然有很多,但是一般的做法是,向頂點里添加顏色的情報信息,然后通過操作顏色的透明度的變化來使模型半透明或者完全透明。這時候,如果想操作頂點里的顏色信息和畫面上的顏色信息的話,就需要向片段著色器里傳入一些必要的信息。首先是頂點著色器部分:
attribute vec4 position;
attribute vec4 color;
uniform mat4 mvpMatrix;
varying vec4 vColor
void main(void) {
vColor = color;
gl_Position = mvpMatrix * position;
}
接著,片段著色器接收通過varying修飾符所定義的變量vColor:
varying vec4 vColor;
void main(void) {
gl_FragColor = vColor;
}
和頂點著色器中必需要把數(shù)據(jù)傳給gl_Position類似,片段著色器要把數(shù)據(jù)傳給gl_FragColor,只是與頂點著色器不同的是,片段著色器的gl_FragColor不是必須要賦值的。但是一般都會輸出一種什么顏色,所以gl_FragColor就變成必要的了。
頂點緩存
局部坐標
?? ? ? 頂點最終在畫面上繪制的時候,要經(jīng)過模型坐標變換,視圖坐標變換和投影坐標變換,這個已經(jīng)說過好多遍了。但是,在使用坐標情報之前,首先必須定義這些頂點群的構成,否則就沒有辦法開始了。定點群放到什么位置,就表現(xiàn)為坐標,一般叫做局部坐標。局部坐標就是模型的各個頂點相對于原點(x,y,z都為0)的坐標。比如,一個局部坐標為(1.0,0.0,0.0)的頂點,x軸方向距離原點的距離是1.0。同樣,各個頂點都依次定義了局域坐標,這樣頂點的位置就形成了。
頂點保存
?? ? ? 這些頂點的局部坐標,必須在WebGL程序中進行變換,然后傳給頂點著色器。在WebGL中,為了處理這些頂點的信息,并將這些頂點信息保存,則需要使用頂點緩存。緩存(buffer),是表示數(shù)據(jù)保存空間的一般的計算機用語。WebGL中還有幀緩存,索引緩存等各種緩存,但是不管哪種緩存,你只需要把它想成保存數(shù)據(jù)的一塊兒空間就行了。頂點緩存是其中的一種,就是用來保存頂點信息的,WebGL中的頂點緩存叫做VBO(vertex buffer object)。
頂點緩存和attribute
?? ? ? WebGL的程序中,先把頂點的信息保存到VBO中,接著,通知著色器哪個VBO和哪個attribute變量相關,然后頂點著色器就可以正確的處理這些頂點了。根據(jù)前面的內容,頂點緩存相關的處理的具體流程如下:
- 頂點的各種信息保存到數(shù)組里
- 使用WebGL的方法生成VBO
- 使用WebGL的方法將數(shù)組中的信息傳給VBO
- 頂點著色器中的attribute函數(shù)和VBO結合
VBO的生成過程中,首先在最初的時候必須把數(shù)據(jù)保存到數(shù)組中,因為頂點的信息(位置)中必須有x,y,z,所以數(shù)組的長度必須是頂點數(shù)x3,這個時候需要注意,數(shù)組不可以使用多維數(shù)組,VBO的生成需要使用一維數(shù)組。準備好保存頂點信息的數(shù)組之后,使用WebGL的context的方法生成VBO,當然生成的時候VBO是空的,然后將頂點信息的數(shù)組傳給它。然后,比如把頂點著色器中的attribute函數(shù)和VBO關聯(lián)起來。上面也說了,VBO中不是只能保存一種信息,位置情報以外的法線和顏色等信息存在的時候,要準備合適的VBO,然后通知WebGL哪個VBO和哪個attribute變量相關聯(lián)。
矩陣計算和外部庫
矩陣計算
?? ? ? 矩陣的計算方法,也不是什么特別奇怪復雜的東西,如果數(shù)學好好學習的話,沒有基礎也可以進行基本的矩陣計算。但是,如果不知道矩陣的加法和乘法運算的話,要進行稍微復雜一些的矩陣計算是非常難的。
矩陣的使用方法,并不是詳細的計算方法。特別是在3D開發(fā)中,矩陣能夠做什么,通過什么運算能得到什么樣的結果,主要是掌握矩陣的使用方法,這一點很重要。
外部庫
?? ? ? DirectX和OpenGL中,內置了許多矩陣相關的處理,即使不使用外部庫也可以進行矩陣計算。但是,WebGL中這些矩陣相關的計算是沒有的,可能為了簡化吧,當然,不是說沒有辦法了,而是,矩陣相關的一切計算,都需要自己來處理。話雖如此,但是WebGL中的矩陣計算還是一個很大的問題。數(shù)學好的人當然是沒有問題了,但是對于其他人數(shù)學不太好的人就太困難了。但是,不用怕,有很多使用JavaScript寫的矩陣計算的外部庫,使用這些外部庫的話,就算自己不會矩陣計算,也可以進行矩陣相關的處理,下面是其中的幾個:
- glMatrix
http://code.google.com/p/glmatrix/ - mjs
http://code.google.com/p/webgl-mjs/ - Sylvester
http://sylvester.jcoglan.com/ - closure
http://code.google.com/p/closure-library/ - TDL
http://code.google.com/p/threedlibrary/ - minMatrix.js
http://wgld.org/j/minMatrix.js
著色器的編譯和連接
用個繪制多邊形的例子來開始吧,首先,確認一下繪制的步驟:
- 從HTML中獲取canvas對象
- 從canvas中獲取WebGL的context
- 編譯著色器
- 準備模型數(shù)據(jù)
- 頂點緩存(VBO)的生成和通知
- 坐標變換矩陣的生成和通知
- 發(fā)出繪圖命令
- 更新canvas并渲染
步驟1、2和著色器的定義代碼之前的“基礎代碼”和“認識GLSL”已經(jīng)學習過,再次整理一下:
<html>
<head>
<title>WebGL TEST</title>
</head>
<body>
<canvas id="canvas"></canvas>
<!-- ※頂點著色器 -->
<script id="vs" type="x-shader/x-vertex">
attribute vec3 position;
uniform mat4 mvpMatrix;
void main(void){
gl_Position = mvpMatrix * vec4(position, 1.0);
}
</script>
<!-- ※片段著色器 -->
<script id="fs" type="x-shader/x-fragment">
void main(void){
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}
</script>
<script>
//首先獲取canvas對象并設置其大小
var c = document.getElementById('canvas');
c.width = 500;
c.height = 300;
// 從canvas中獲取WebGL的context
var gl = c.getContext('webgl') || c.getContext('experimental-webgl');
</script>
</body>
</html>
編譯著色器
?? ? ? 編譯也不需要什么特別的編譯器,只需要調用WebGL內部的函數(shù)就可以進行編譯了。準備一個函數(shù),從著色器的編譯,到實際著色器的生成這一連串的流程,都在這一個函數(shù)中來完成。下面是這個函數(shù)的代碼:
function create_shader(id){
// 用來保存著色器的變量
var shader;
// 根據(jù)id從HTML中獲取指定的script標簽
var scriptElement = document.getElementById(id);
// 如果指定的script標簽不存在,則返回
if(!scriptElement){return;}
// 判斷script標簽的type屬性
switch(scriptElement.type){
// 頂點著色器的時候
case 'x-shader/x-vertex':
shader = gl.createShader(gl.VERTEX_SHADER);
break;
// 片段著色器的時候
case 'x-shader/x-fragment':
shader = gl.createShader(gl.FRAGMENT_SHADER);
break;
default :
return;
}
// 將標簽中的代碼分配給生成的著色器
gl.shaderSource(shader, scriptElement.text);
// 編譯著色器
gl.compileShader(shader);
// 判斷一下著色器是否編譯成功
if(gl.getShaderParameter(shader, gl.COMPILE_STATUS)){
// 編譯成功,則返回著色器
return shader;
}else{
// 編譯失敗,彈出錯誤消息
alert(gl.getShaderInfoLog(shader));
}
}
程序對象的生成和連接
?? ? ? 使用varying修飾符定義的變量,可以從頂點著色器向片段著色器中傳遞數(shù)據(jù)。其實,實現(xiàn)從一個著色器向另一個著色器傳遞數(shù)據(jù)的,不是別的,就是程序對象。程序對象是管理頂點著色器和片段著色器,或者WebGL程序和各個著色器之間進行數(shù)據(jù)的互相通信的重要的對象。
那么,生成程序對象,并把著色器傳給程序對象,然后連接著色器,將這些處理函數(shù)化:
function create_program(vs, fs){
// 程序對象的生成
var program = gl.createProgram();
// 向程序對象里分配著色器
gl.attachShader(program, vs);
gl.attachShader(program, fs);
// 將著色器連接
gl.linkProgram(program);
// 判斷著色器的連接是否成功
if(gl.getProgramParameter(program, gl.LINK_STATUS)){
// 成功的話,將程序對象設置為有效
gl.useProgram(program);
// 返回程序對象
return program;
}else{
// 如果失敗,彈出錯誤信息
alert(gl.getProgramInfoLog(program));
}
}
VBO的生成
?? ? ?生成VBO的時候使用WebGL的createBuffer函數(shù),這個函數(shù)就是用來生成緩存的。但是這個函數(shù)并不是用來直接生成VBO的,它只是生成了一個緩存對象,根據(jù)它里面保存的內容不同,用途也是不用的。
要操作緩存,首先必須跟WebGL進行綁定,就是說,要向“緩存”這個“光盤”中寫入數(shù)據(jù)的時候,必須連接到WebGL這個“光驅”上。
綁定了緩存之后,使用bufferData函數(shù)來向緩存中寫入數(shù)據(jù),把這些處理寫成一個函數(shù),就是下面這樣:
function create_vbo(data){
// 生成緩存對象
var vbo = gl.createBuffer();
// 綁定緩存
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
// 向緩存中寫入數(shù)據(jù)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
// 將綁定的緩存設為無效
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 返回生成的VBO
return vbo;
}
這個函數(shù),接受一個矩陣作為參數(shù),最后返回生成的VBO。首先使用createBuffer生成緩存對象,接著綁定緩存,然后寫入數(shù)據(jù)。
綁定緩存的時候使用bindBuffer函數(shù),這個函數(shù)有兩個參數(shù),第一個參數(shù)是緩存的類型,第二個參數(shù)是指定緩存對象。將第一個參數(shù)指定為gl.ARRAY_BUFFER就可以生成VBO。
另外,bufferData函數(shù)的第二個參數(shù)中出現(xiàn)的Float32Array對象,是javascript的類型數(shù)組,和一般的Array對象類似,是處理浮點型小數(shù)的時候使用的數(shù)組對象。3D世界里小數(shù)的精確度非常重要,所以使用類型數(shù)組來傳遞數(shù)據(jù)。而第三個參數(shù)中的gl.STATIC_DRAW這個常量,定義了這個緩存中內容的更新頻率。VBO的話,模型數(shù)據(jù)基本上就是直接這么反復用,所以使用這個常量。
可以綁定WebGL的緩存,一次只能綁定一個,所以要操作其他的緩存的時候,必須要綁定相應的緩存。所以在函數(shù)的最后,再次使用bindBuffer函數(shù),設定第二個參數(shù)為null,來將上次的綁定無效化,這是為了防止WebGL中的緩存一致保留,而出現(xiàn)和預想不一致的情況。
坐標變換矩陣的基本功能
?? ? ?進行基本的3D渲染的時候,需要準備3個坐標變換矩陣。
第一個是模型變換矩陣,DirectX中叫做世界變換矩陣。模型變換矩陣影響的是所繪制的模型,模型的位置,模型的旋轉,模型的放大和縮小等相關的情況。
第二個是視圖變換矩陣,簡單來說,就是定義拍攝3D空間的鏡頭(攝像機),決定了鏡頭的位置,鏡頭的參考點,鏡頭的方向等。
第三個是投影變換矩陣,這個坐標變換定義了屏幕的橫豎比例,剪切的領域等,另外獲取遠近法則的效果也需要用這個變換矩陣。
根據(jù)這些內容,差不多知道了需要對矩陣進行哪些操作。使用minMatrix.js可以對矩陣進行基本的操作,來看一下minMatrix.js都能完成哪些操作吧。
- minMatrix.js的基本功能
minMatrix.js包含矩陣的生成和矩陣的基本操作,minMatrix.js的核心是一個叫做matIV的對象,通過這個對象可以進行所有的矩陣操作,使用minMatrix.js來操作矩陣的時候,首先,需要生成一個matIV對象:
var m = new matIV();
像上面這樣,變量m就是matIV對象的一個實例,通過m.方法名可以調用matIV對象中存在的方法。
下面,列舉一下minMatrix.js中定義的matIV對象的方法:
.create
函數(shù): matIV.create()
參數(shù): 無
返回值: 矩陣
生成一個4x4的方陣,里面包含16個元素,其實是一個Float32Array對象,所有的元素都被初始化為0
.identity
函數(shù): matIV.identity(dest)
參數(shù): dest > 初始化的矩陣
返回值: 初始化后的矩陣
將接收的矩陣參數(shù)進行初始化并返回
.multiply
函數(shù): matIV.multiply(mat1,mat2,dest)
參數(shù): mat1 > 相乘的原始矩陣
參數(shù): mat2 > 作為乘數(shù)的矩陣
參數(shù): dest > 用來保存計算結果的矩陣
mat1在左,mat2在右,相乘后的結果保存到dest中
.scale
函數(shù): matIV.scale(mat,vec,dest)
參數(shù): mat > 原始矩陣
參數(shù): vec > 縮放向量
參數(shù): dest > 用來保存計算結果的矩陣
模型變換中的放大縮小,mat是原始矩陣,vec是X,Y,Z的各個縮放值組成的向量,最后的計算結果保存在dest中
.translate
函數(shù): matIV.translate(mat,vec,dest)
參數(shù): mat > 原始矩陣
參數(shù): vec > 表示從原點開始移動一定距離的向量
參數(shù): dest > 用來保存計算結果的矩陣
模型變換中的坐標移動,mat是原始矩陣,vec是X,Y,Z的各個方向上的移動量組成的向量,最后將計算結果保存到dest中
.rotate
函數(shù): matIV.rotate(mat,angle,axis,dest)
參數(shù): mat > 原始矩陣
參數(shù): angle > 旋轉角度
參數(shù): axis > 旋轉軸的向量
參數(shù): dest > 用來保存計算結果的矩陣
模型變換中的旋轉,mat是原始矩陣,angle是旋轉角度,axis是旋轉軸向量,最后將計算結果保存到dest中
.lookAt
函數(shù): matIV.lookAt(eye,center,up,dest)
參數(shù): eye > 鏡頭位置向量
參數(shù): center > 鏡頭參考點的向量
參數(shù): up > 鏡頭的方向向量
參數(shù): dest > 用來保存計算結果的矩陣
視圖變換矩陣的生成,eye是鏡頭在三維空間中的位置,center是這個鏡頭的參考點,up是鏡頭的方向向量,最后將計算結果保存到dest中
.perspective
函數(shù): matIV.perspective(fovy,aspect,near,far,dest)
參數(shù): fovy > 視角
參數(shù): aspect > 屏幕的寬高比例
參數(shù): near > 近截面的位置
參數(shù): far > 遠截面的位置
參數(shù): dest > 用來保存計算結果的矩陣
投影變換矩陣的生成,這里生成的是一般被稱為[透視射影]的投影變換矩陣,包含遠近法則。fovy是視角,aspect是屏幕的橫豎比例,near是近截面的位置(必須是大于0的數(shù)值),far遠截面的位置(任意數(shù)值),最后將計算結果保存到dest中
.transpose
函數(shù): matIV.transpose()
參數(shù): mat > 原始矩陣
參數(shù): dest > 用來保存計算結果的矩陣
矩陣的行列互換,將計算結果保存到dest中
.inverse
函數(shù): matIV.inverse(mat,dest)
參數(shù): mat > 原始矩陣
參數(shù): dest > 用來保存計算結果的矩陣
求矩陣的逆矩陣,mat是原始矩陣,求的的逆矩陣保存到dest中
- 矩陣變換的流程
使用minMatrix.js的話,可以操作矩陣,那么先來確認一下操作順序。
模型變換也好,視圖變換,投影變換也好,如果不先生成矩陣的話,就什么也做不了。所以首先執(zhí)行matIV.create生成矩陣,然后通過matIV.identity來初始化矩陣,代碼如下:
// 生成matIV對象
var m = new matIV();
// 矩陣生成及初始化
var Matrix = m.identity(m.create());
(未完待續(xù))
注:學習內容來自于https://wgld.org/