本文基於這個系列第一部分中介紹的框架,另外還增長了一個模型導入器,和針對3D對象定製的類。 你會從中瞭解到動畫和控制,內容不少,咱們趕忙開始吧。前端
由於嚴重依賴於上一篇文章,因此,若是你還沒讀過,建議先讀一下。
WebGL在3D世界中操縱物體的方式是使用稱爲數組
有三種基本變換可做用於3D對象。緩存
這些函數中的每個均可做用於X軸、Y軸或Z軸,於是組合獲得9種基本的變換。它們經過不一樣的方式來影響3D對象的4x4變換矩陣。 爲了在同一個對象中執行多個變換,而不產生重疊的問題,咱們要將將每一個變換乘到對象的矩陣中去,而不是逐一地直接應用到對象的矩陣上。 移動變換是最簡單的,咱們先從移動開始。bash
移動一個3D對象是最簡單的一種變換,由於在4x4矩陣中爲它保留了特殊的位置。 咱們能夠不用涉及任何數學;只須要把X,Y和Z座標放到矩陣中指定位置上,就能夠了。若是你觀察這個4x4矩陣,你會發現它們被放在最後一行上。 此外,你須要知道的是,正Z軸指向攝像機後面。於是,Z值爲-100時,會致使對象深刻屏幕100個單元。在咱們的代碼中會對此進行補償。微信
爲了執行多個變換,你不能簡單地修改對象的真實矩陣;你必須將變換應用於一個新的空白矩陣,稱爲app
矩陣乘法理解起來會有些困難,但基本思想是第一個矩陣的豎直的列乘以第二個矩陣的水平行。 好比,新矩陣第一個數爲第一個矩陣第一行乘以另外一矩陣的第一列。新矩陣第二個數是第一個矩陣的第一行乘以第二個矩陣的第二列,依此類推。框架
下面的代碼片段是JavaScript中實現的矩陣乘法。將其加到你的.js
文件中,參見本系列教程第一部分。函數
function MH(A, B) {
var Sum = 0;
for (var i = 0; i < A.length; i++) {
Sum += A[i] * B[i];
}
return Sum;
}
function MultiplyMatrix(A, B) {
var A1 = [A[0], A[1], A[2], A[3]];
var A2 = [A[4], A[5], A[6], A[7]];
var A3 = [A[8], A[9], A[10], A[11]];
var A4 = [A[12], A[13], A[14], A[15]];
var B1 = [B[0], B[4], B[8], B[12]];
var B2 = [B[1], B[5], B[9], B[13]];
var B3 = [B[2], B[6], B[10], B[14]];
var B4 = [B[3], B[7], B[11], B[15]];
return [
MH(A1, B1), MH(A1, B2), MH(A1, B3), MH(A1, B4),
MH(A2, B1), MH(A2, B2), MH(A2, B3), MH(A2, B4),
MH(A3, B1), MH(A3, B2), MH(A3, B3), MH(A3, B4),
MH(A4, B1), MH(A4, B2), MH(A4, B3), MH(A4, B4)];
}複製代碼
我認爲咱們無需糾纏於如何理解這個過程,由於它們只不過是數學上矩陣乘法的必要步驟。咱們接着介紹縮放吧。post
縮放一個模型一樣簡單-由於它也是乘法。你須要將第三個對角元素乘以縮放係數。 再一次,記得順序是X,Y和Z。因此,若是你想讓你的對象在全部三個座標軸上都變成兩倍大,則你須要讓第一個,第六個和第十一個元素都乘以2。動畫
旋轉是最難懂的變換,由於旋轉軸在三個座標軸上時,旋轉矩陣都不同。下圖給出了每一個座標軸上的旋轉方程。
若是你徹底看不懂也不要緊;咱們立刻會在JavaScript的具體實現中複習一下的。
重要的一點是,執行變換的順序是很關鍵的;不一樣的順序會產生不一樣的結果。
重要的一點是,執行變換的順序是很關鍵的;不一樣的順序會產生不一樣的結果。 若是你先移動對象而後再旋轉,WebGL會像揮舞球拍同樣舞動你的對象,而不僅是讓對象在原地旋轉。 若是你先旋轉再移動,則你會將對象移動到指定的位置上,只不過它會朝向你指定的方向上。 這是由於在3D世界中,變換是繞原點-0,0,0-來執行的。不存在對的或錯的順序。最終都是取決於你想要實現的效果。
要實現一些高級的動畫,須要的每一種變換可能都會多個。好比,若是你想讓一扇門繞絞鏈轉動,你會先移動門,讓它的絞鏈位於Y軸上,即在X軸和Z軸上都爲零。 而後,繞Y軸旋轉,這樣門就能夠繞絞鏈轉動了。最後,你還須要將其再次移動,使得它能夠放到場景中的指定位置上。
這些類型的動畫在不一樣的場合下須要進行不一樣的定製,因此就沒有必要專門寫一個函數了。 不過,我會寫一個函數執行最基本的順序的變換:縮放,旋轉,移動。這確保了全部物體都在指定位置,並有正確的朝向。
如今你已經對全部幕後的數學有了基本的理解,並瞭解了動畫的工做原理,讓咱們建立一個JavaScript數據類型,來存儲咱們的3D對象。
回憶本系列教程的第一部分,你須要三個數組來繪製一個基本的3D對象:頂點數組,三角數組和紋理數組。它們將是咱們的數據類型的基礎。 咱們還須要用一些變量來表示在每個軸上的三種變換。最後,咱們須要用一個變量來表示紋理圖像,並用來指示模型是否已經加載完畢。
下面是一個3D對象在JavaScript中的實現。
function GLObject(VertexArr, TriangleArr, TextureArr, ImageSrc) {
this.Pos = {
X: 0,
Y: 0,
Z: 0
};
this.Scale = {
X: 1.0,
Y: 1.0,
Z: 1.0
};
this.Rotation = {
X: 0,
Y: 0,
Z: 0
};
this.Vertices = VertexArr;
this.Triangles = TriangleArr;
this.TriangleCount = TriangleArr.length;
this.TextureMap = TextureArr;
this.Image = new Image();
this.Image.onload = function () {
this.ReadyState = true;
};
this.Image.src = ImageSrc;
this.Ready = false;
//Add Transformation function Here
}複製代碼
我增長了兩個獨立的「ready」變量:一個用來表示圖像是否準備好了,一個用於模型。當圖像準備完畢,咱們將經過將圖像變換爲WebGL紋理,以及將三個數組緩存於WebGL的緩存中,從而準備咱們的模型。 這會加速咱們的程序,由於不須要在每一個繪製循環中都緩存一次數據。由於咱們將數組存到緩存中去了,咱們須要將三角形的數目存於一個獨立的變量中。
如今,讓咱們加一個函數,來計算對象的變換矩陣。這個函數將取出全部的局部變量,並讓它們以以前提到的順序 (縮放,旋轉,而後平移) 相乘。 你能夠在這個變換順序下獲得一些不一樣的效果。將註釋//Add Transformation function Here
換成以下代碼:
this.GetTransforms = function () {
//Create a Blank Identity Matrix
var TMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
//Scaling
var Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
Temp[0] *= this.Scale.X;
Temp[5] *= this.Scale.Y;
Temp[10] *= this.Scale.Z;
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Rotating X
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
var X = this.Rotation.X * (Math.PI / 180.0);
Temp[5] = Math.cos(X);
Temp[6] = Math.sin(X);
Temp[9] = -1 * Math.sin(X);
Temp[10] = Math.cos(X);
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Rotating Y
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
var Y = this.Rotation.Y * (Math.PI / 180.0);
Temp[0] = Math.cos(Y);
Temp[2] = -1 * Math.sin(Y);
Temp[8] = Math.sin(Y);
Temp[10] = Math.cos(Y);
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Rotating Z
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
var Z = this.Rotation.Z * (Math.PI / 180.0);
Temp[0] = Math.cos(Z);
Temp[1] = Math.sin(Z);
Temp[4] = -1 * Math.sin(Z);
Temp[5] = Math.cos(Z);
TMatrix = MultiplyMatrix(TMatrix, Temp);
//Moving
Temp = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
Temp[12] = this.Pos.X;
Temp[13] = this.Pos.Y;
Temp[14] = this.Pos.Z * -1;
return MultiplyMatrix(TMatrix, Temp);
}
由於旋轉公式相互重疊,它們必須一次執行一個。這個函數替換了上一個教程中的MakeTransform函數,因此你能夠將它從腳本中刪除。複製代碼
如今,咱們有了一個3D類,咱們還須要一種方式來導入數據。咱們將編寫一個簡單的模型導入器,它會將.obj
文件變換爲必要的數據,而後獲得一個咱們新建立的GLObject
的對象。 使用.obj
模型格式的緣由在於,它用一種原始的形式來存儲全部的數據,而且它有很好的文檔介紹它的信息存儲方式。 若是你的3D建模程序不支持導出.obj
文件,則你老是能夠編寫一個基它數據格式的導入器。 .obj
是一種標準的3D文件類型;因此,應該不會有什麼問題。或者,你也能夠安裝Blender,這是一個跨平臺的3D建模程序,它是支持導出.obj
的。
在.obj
文件中,每一行的頭兩個字母告訴咱們該行中包含了什麼類型的數據。 "v
"表示一個"頂點座標"行,"vt
"表示一個"紋理座標"行,而"f
"是一個映射行。基於這些信息,我編寫了下面的函數:
function LoadModel(ModelName, CB) {
var Ajax = new XMLHttpRequest();
Ajax.onreadystatechange = function () {
if (Ajax.readyState == 4 && Ajax.status == 200) {
//Parse Model Data
var Script = Ajax.responseText.split("\n");
var Vertices = [];
var VerticeMap = [];
var Triangles = [];
var Textures = [];
var TextureMap = [];
var Normals = [];
var NormalMap = [];
var Counter = 0;複製代碼
此函數接受兩個參數:模型名稱和回調函數。回調函數接受四個數組做爲參數:頂點,三角形,紋理和法向量數組。 我以前還沒介紹過法向量,因此你能夠如今暫時忽略。我會在接下來的文章中討論光照時進行介紹。
這個導入器首先建立一個XMLHttpRequest
對象,並定義它的onreadystatechange
事件處理器。在此處理器內部,咱們將文件分割成行,而後定義了一些變量。 .obj
文件首先定義了全部的惟一座標,並定義它們的順序。這也是爲何爲頂點、紋理和法向量定義了兩個變量的緣由。 計數器counter變量用於填充三角形數組,由於.obj
文件是按照順序定義這些三角形的。
接下來,咱們必須遍歷文件的每一行,並檢查它們各自是哪種類型:
for (var I in Script) {
var Line = Script[I];
//If Vertice Line
if (Line.substring(0, 2) == "v ") {
var Row = Line.substring(2).split(" ");
Vertices.push({
X: parseFloat(Row[0]),
Y: parseFloat(Row[1]),
Z: parseFloat(Row[2])
});
}
//Texture Line
else if (Line.substring(0, 2) == "vt") {
var Row = Line.substring(3).split(" ");
Textures.push({
X: parseFloat(Row[0]),
Y: parseFloat(Row[1])
});
}
//Normals Line
else if (Line.substring(0, 2) == "vn") {
var Row = Line.substring(3).split(" ");
Normals.push({
X: parseFloat(Row[0]),
Y: parseFloat(Row[1]),
Z: parseFloat(Row[2])
});
複製代碼
前三行很是簡單;它們包含了惟一性座標的一個列表,用於頂點、紋理和法向量。 咱們須要作的是將這些座標存入相應的數組中。最後一種行的類型稍微複雜一些,由於它包含了多個東西。 它能夠包含頂點,或頂點和紋理,或頂點、紋理和法向量。這樣,咱們不得不檢查是這三種狀況中的哪種。下面的代碼實現了這個功能:
//Mapping Line
else if (Line.substring(0, 2) == "f ") {
var Row = Line.substring(2).split(" ");
for (var T in Row) {
//Remove Blank Entries
if (Row[T] != "") {
//If this is a multi-value entry
if (Row[T].indexOf("/") != -1) {
//Split the different values
var TC = Row[T].split("/");
//Increment The Triangles Array
Triangles.push(Counter);
Counter++;
//Insert the Vertices
var index = parseInt(TC[0]) - 1;
VerticeMap.push(Vertices[index].X);
VerticeMap.push(Vertices[index].Y);
VerticeMap.push(Vertices[index].Z);
//Insert the Textures
index = parseInt(TC[1]) - 1;
TextureMap.push(Textures[index].X);
TextureMap.push(Textures[index].Y);
//If This Entry Has Normals Data
if (TC.length > 2) {
//Insert Normals
index = parseInt(TC[2]) - 1;
NormalMap.push(Normals[index].X);
NormalMap.push(Normals[index].Y);
NormalMap.push(Normals[index].Z);
}
}
//For rows with just vertices
else {
Triangles.push(Counter); //Increment The Triangles Array
Counter++;
var index = parseInt(Row[T]) - 1;
VerticeMap.push(Vertices[index].X);
VerticeMap.push(Vertices[index].Y);
VerticeMap.push(Vertices[index].Z);
}
}
}
}
複製代碼
這個代碼雖然長,但並不算複雜。雖然我討論了.obj文件中只包含有頂點數據的狀況,但咱們的框架還須要頂點座標和紋理座標。 若是一個.obj文件只包含頂點數據,你將必須手動地添加紋理座標數據。
如今,讓咱們將這些數據傳遞給回調函數,並完成咱們的LoadModel
函數。
}
//Return The Arrays
CB(VerticeMap, Triangles, TextureMap, NormalMap);
}
}
Ajax.open("GET", ModelName + ".obj", true);
Ajax.send();
}複製代碼
你須要當心的是,咱們的WebGL框架是很是基本的,只能畫用三角形構造出來的模型。因此,你須要相應地編輯你的3D模型。 幸運的是,大部分3D應用都支持或有插件支持模型的三角化。我經過基本的建模技術構造了一個簡單的房子的模型,包含在源碼中,供你使用。
如今,讓咱們修改上篇文章中的Draw
函數,使之可以處理咱們新的3D模型的數據類型。
this.Draw = function (Model) {
if (Model.Image.ReadyState == true && Model.Ready == false) {
this.PrepareModel(Model);
}
if (Model.Ready) {
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Model.Vertices);
this.GL.vertexAttribPointer(this.VertexPosition, 3, this.GL.FLOAT, false, 0, 0);
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Model.TextureMap);
this.GL.vertexAttribPointer(this.VertexTexture, 2, this.GL.FLOAT, false, 0, 0);
this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, Model.Triangles);
//Generate The Perspective Matrix
var PerspectiveMatrix = MakePerspective(45, this.AspectRatio, 1, 1000.0);
var TransformMatrix = Model.GetTransforms();
//Set slot 0 as the active Texture
this.GL.activeTexture(this.GL.TEXTURE0);
//Load in the Texture To Memory
this.GL.bindTexture(this.GL.TEXTURE_2D, Model.Image);
//Update The Texture Sampler in the fragment shader to use slot 0
this.GL.uniform1i(this.GL.getUniformLocation(this.ShaderProgram, "uSampler"), 0);
//Set The Perspective and Transformation Matrices
var pmatrix = this.GL.getUniformLocation(this.ShaderProgram, "PerspectiveMatrix");
this.GL.uniformMatrix4fv(pmatrix, false, new Float32Array(PerspectiveMatrix));
var tmatrix = this.GL.getUniformLocation(this.ShaderProgram, "TransformationMatrix");
this.GL.uniformMatrix4fv(tmatrix, false, new Float32Array(TransformMatrix));
//Draw The Triangles
this.GL.drawElements(this.GL.TRIANGLES, Model.TriangleCount, this.GL.UNSIGNED_SHORT, 0);
}
};複製代碼
新的繪製函數首先檢查模型是否已經爲WebGL準備好。若是紋理已經加載,它會開始準備繪製模型。咱們呆會兒會介紹這個PrepareModel
函數。 若是模型準備好了,它會鏈接到着色器中的緩存,並和以前同樣,加載透視矩陣和變換矩陣。惟一實在的差異在於,它的全部數據都來自於模型對象。
PrepareModel
函數只不過是將紋理和數據數組轉變爲與WebGL兼容的變量。下面就是這個函數;將它加到繪製函數以前。
this.PrepareModel = function (Model) {
Model.Image = this.LoadTexture(Model.Image);
//Convert Arrays to buffers
var Buffer = this.GL.createBuffer();
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Buffer);
this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Model.Vertices), this.GL.STATIC_DRAW);
Model.Vertices = Buffer;
Buffer = this.GL.createBuffer();
this.GL.bindBuffer(this.GL.ELEMENT_ARRAY_BUFFER, Buffer);
this.GL.bufferData(this.GL.ELEMENT_ARRAY_BUFFER, new Uint16Array(Model.Triangles), this.GL.STATIC_DRAW);
Model.Triangles = Buffer;
Buffer = this.GL.createBuffer();
this.GL.bindBuffer(this.GL.ARRAY_BUFFER, Buffer);
this.GL.bufferData(this.GL.ARRAY_BUFFER, new Float32Array(Model.TextureMap), this.GL.STATIC_DRAW);
Model.TextureMap = Buffer;
Model.Ready = true;
};複製代碼
如今,咱們的框架已經完成,咱們能夠開始修改HTML頁面。
你能夠清除script
標籤中的全部代碼,因爲新的GLObject
的功勞,咱們能夠把代碼寫得更緊湊一些。
下面是完整的JavaScript代碼:
var GL;
var Building;
function Ready() {
GL = new WebGL("GLCanvas", "FragmentShader", "VertexShader");
LoadModel("House", function (VerticeMap, Triangles, TextureMap) {
Building = new GLObject(VerticeMap, Triangles, TextureMap, "House.png");
Building.Pos.Z = 650;
//My Model Was a bit too big
Building.Scale.X = 0.5;
Building.Scale.Y = 0.5;
Building.Scale.Z = 0.5;
//And Backwards
Building.Rotation.Y = 180;
setInterval(Update, 33);
});
}
function Update() {
Building.Rotation.Y += 0.2
GL.Draw(Building);
}複製代碼
咱們加載一個模型,告訴頁面每秒鐘更新30次。Update
函數讓模型繞Y軸旋轉,這是經過更新這個對象的Y軸Rotation
實現的。 個人模型對於WebGL來講仍是大了一些,這不太好,因此我須要在代碼中稍做調整。
除非你想要那種影院般的WebGL展現,你極可能但願添加一些控制功能。讓咱們看看如何在應用中添加鼠標控制功能。
這只不過是原生的JavaScript功能,並不是WebGL的技術,但它對於控制和放置3D模型是頗有幫助的。你須要作的所有事情只是爲鍵盤的keydown
或keyup
事件添加一個事件監聽器,並檢查究竟是哪一個鍵被按下了。 每一個鍵都一個特殊的代碼,找出這種對應關係的一種較好的辦法是在事件觸發時在終端中記錄下按鍵的代碼。因此,在加載模型的代碼處,在setInterval
行以後添加以下的代碼:
document.onkeydown = handleKeyDown;複製代碼
這會設置函數handleKeyDown
,來處理keydown
事件。下面是handleKeyDown
函數的代碼:
function handleKeyDown(event) {
//You can uncomment the next line to find out each key's code //alert(event.keyCode); if (event.keyCode == 37) { //Left Arrow Key Building.Pos.X -= 4; } else if (event.keyCode == 38) { //Up Arrow Key Building.Pos.Y += 4; } else if (event.keyCode == 39) { //Right Arrow Key Building.Pos.X += 4; } else if (event.keyCode == 40) { //Down Arrow Key Building.Pos.Y -= 4; } }複製代碼
這個函數的功能是更新對象的屬性;而咱們WebGL框架會處理剩下的全部事情。
更多精彩內容,請微信關注」前端達人」公衆號!