Webgl的魅力在於可以創造一個自己的3D世界,但相比較canvas2D來說,除了物體的移動旋轉變換完全依賴矩陣增加了複雜度,就連生成一個物體都變得很複雜。
什麼?!爲什麼不用Threejs?Threejs等庫確實可以很大程度的提高開發效率,而且各方面封裝的非常棒,但是不推薦初學者直接依賴Threejs,最好是把webgl各方面都學會,再去擁抱Three等相關庫。
上篇矩陣入門中介紹了矩陣的基本知識,讓大家瞭解到了基本的仿射變換矩陣,可以對物體進行移動旋轉等變化,而這篇文章將教大家快速生成一個物體,並且結合變換矩陣在物體在你的世界裏動起來。
注:本文適合稍微有點webgl基礎的人同學,至少知道shader,知道如何畫一個物體在webgl畫布中
爲什麼說webgl生成物體麻煩
我們先稍微對比下基本圖形的創建代碼
矩形:
canvas2D
ctx1.rect(50, 50, 100, 100);
ctx1.fill();
webgl(shader和webgl環境代碼忽略)
Crayon Syntax Highlighter v_2.7.2_betavar aPo = [
-0.5, -0.5, 0,
0.5, -0.5, 0,
0.5, 0.5, 0,
-0.5, 0.5, 0
];
var aIndex = [0, 1, 2, 0, 2, 3];
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 0, 0, 0);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
webgl.drawElements(webgl.TRIANGLES, 6, webgl.UNSIGNED_SHORT, 0);
完整代碼地址:
https://vorshen.github.io/simple-3d-text-universe/rect.html
結果:
圓:
canvas2D
ctx1.arc(100, 100, 50, 0, Math.PI * 2, false);
ctx1.fill();
webgl
Crayon Syntax Highlighter v_2.7.2_betavar angle;
var x, y;
var aPo = [0, 0, 0];
var aIndex = [];
var s = 1;
for(var i = 1; i <= 36; i++) {
angle = Math.PI * 2 * (i / 36);
x = Math.cos(angle) * 0.5;
y = Math.sin(angle) * 0.5;
aPo.push(x, y, 0);
aIndex.push(0, s, s+1);
s++;
}
aIndex[aIndex.length - 1] = 1; // hack一下
webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(aPo), webgl.STATIC_DRAW);
webgl.vertexAttribPointer(aPosition, 3, webgl.FLOAT, false, 0, 0);
webgl.vertexAttrib3f(aColor, 0, 0, 0);
webgl.bindBuffer(webgl.ELEMENT_ARRAY_BUFFER, webgl.createBuffer());
webgl.bufferData(webgl.ELEMENT_ARRAY_BUFFER, new Uint16Array(aIndex), webgl.STATIC_DRAW);
webgl.drawElements(webgl.TRIANGLES, aIndex.length, webgl.UNSIGNED_SHORT, 0);
完整代碼地址:
https://vorshen.github.io/simple-3d-text-universe/circle.html
結果:
總結:我們拋開shader中的代碼和webgl初始化環境的代碼,發現webgl比canvas2D就是麻煩很多啊。光是兩種基本圖形就多了這麼多行代碼,抓其根本多的原因就是因爲
我們需要頂點信息
。簡單如矩形我們可以直接寫出它的頂點,但是複雜一點的圓,我們還得用數學方式去生成,明顯阻礙了人類文明的進步。
相比較數學方式生成,如果我們能直接獲得頂點信息那應該是最好的,有沒有快捷的方式獲取頂點信息呢?
有,使用建模軟件生成obj文件。
Obj文件簡單來說就是包含一個3D模型信息的文件,這裏信息包含:頂點、紋理、法線以及該3D模型中紋理所使用的貼圖
下面這個是一個obj文件的地址:
https://vorshen.github.io/simple-3d-text-universe/assets/a1.obj
簡單分析一下這個obj文件
前兩行看到#符號就知道這個是註釋了,該obj文件是用blender導出的。Blender是一款很好用的建模軟件,最主要的它是免費的!
Mtllib(material library)指的是該obj文件所使用的材質庫文件(.mtl)
單純的obj生成的模型是白模的,它只含有紋理座標的信息,但沒有貼圖,有紋理座標也沒用
V 頂點vertex
Vt 貼圖座標點
Vn 頂點法線
Usemtl 使用材質庫文件中具體哪一個材質
F是面,後面分別對應 頂點索引 / 紋理座標索引 / 法線索引
這裏大部分也都是我們非常常用的屬性了,還有一些其他的,這裏就不多說,可以google搜一下,很多介紹很詳細的文章。
如果有了obj文件,那我們的工作也就是將obj文件導入,然後讀取內容並且按行解析就可以了。
先放出最後的結果,一個模擬銀河系的3D文字效果。
在線地址查看:
https://vorshen.github.io/simple-3d-text-universe/index.html
在這裏順便說一下,2D文字是可以通過分析獲得3D文字模型數據的,將文字寫到canvas上之後讀取像素,獲取路徑。我們這裏沒有采用該方法,因爲雖然這樣理論上任何2D文字都能轉3D,還能做出類似input輸入文字,3D展示的效果。但是本文是教大家快速搭建一個小世界,所以我們還是採用blender去建模。
具體實現
1、首先建模生成obj文件
這裏我們使用blender生成文字
2、讀取分析obj文件
Crayon Syntax Highlighter v_2.7.2_betavar regex = { // 這裏正則只去匹配了我們obj文件中用到數據
vertex_pattern: /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, // 頂點
normal_pattern: /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, // 法線
uv_pattern: /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, // 紋理座標
face_vertex_uv_normal: /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, // 面信息
material_library_pattern: /^mtllib\s+([\d|\w|\.]+)/, // 依賴哪一個mtl文件
material_use_pattern: /^usemtl\s+([\S]+)/
};
function loadFile(src, cb) {
var xhr = new XMLHttpRequest();
xhr.open('get', src, false);
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
cb(xhr.responseText);
}
};
xhr.send();
}
function handleLine(str) {
var result = [];
result = str.split('\n');
for(var i = 0; i < result.length; i++) {
if(/^#/.test(result[i]) || !result[i]) { // 註釋部分過濾掉
result.splice(i, 1);
i--;
}
}
return result;
}
function handleWord(str, obj) {
var firstChar = str.charAt(0);
var secondChar;
var result;
if(firstChar === 'v') {
secondChar = str.charAt(1);
if(secondChar === ' ' && (result = regex.vertex_pattern.exec(str)) !== null) {
obj.position.push(+result[1], +result[2], +result[3]); // 加入到3D對象頂點數組
} else if(secondChar === 'n' && (result = regex.normal_pattern.exec(str)) !== null) {
obj.normalArr.push(+result[1], +result[2], +result[3]); // 加入到3D對象法線數組
} else if(secondChar === 't' && (result = regex.uv_pattern.exec(str)) !== null) {
obj.uvArr.push(+result[1], +result[2]); // 加入到3D對象紋理座標數組
}
} else if(firstChar === 'f') {
if((result = regex.face_vertex_uv_normal.exec(str)) !== null) {
obj.addFace(result); // 將頂點、發現、紋理座標數組變成面
}
} else if((result = regex.material_library_pattern.exec(str)) !== null) {
obj.loadMtl(result[1]); // 加載mtl文件
} else if((result = regex.material_use_pattern.exec(str)) !== null) {
obj.loadImg(result[1]); // 加載圖片
}
}
代碼核心的地方都進行了註釋,注意這裏的正則只去匹配我們obj文件中含有的字段,其他信息沒有去匹配,如果有對obj文件所有可能含有的信息完成匹配的同學可以去看下Threejs中objLoad部分源碼
3、將obj中數據真正的運用3D對象中去
Crayon Syntax Highlighter v_2.7.2_betaText3d.prototype.addFace = function(data) {
this.addIndex(+data[1], +data[4], +data[7], +data[10]);
this.addUv(+data[2], +data[5], +data[8], +data[11]);
this.addNormal(+data[3], +data[6], +data[9], +data[12]);
};
Text3d.prototype.addIndex = function(a, b, c, d) {
if(!d) {
this.index.push(a, b, c);
} else {
this.index.push(a, b, c, a, c, d);
}
};
Text3d.prototype.addNormal = function(a, b, c, d) {
if(!d) {
this.normal.push(
3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
3 * this.normalArr[b], 3 * this.normalArr[b] + 1, 3 * this.normalArr[b] + 2,
3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2
);
} else {
this.normal.push(
3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
3 * this.normalArr[b], 3 * this.normalArr[b] + 1, 3 * this.normalArr[b] + 2,
3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,
3 * this.normalArr[a], 3 * this.normalArr[a] + 1, 3 * this.normalArr[a] + 2,
3 * this.normalArr[c], 3 * this.normalArr[c] + 1, 3 * this.normalArr[c] + 2,
3 * this.normalArr[d], 3 * this.normalArr[d] + 1, 3 * this.normalArr[d] + 2
);
}
};
Text3d.prototype.addUv = function(a, b, c, d) {
if(!d) {
this.uv.push(2 * this.uvArr[a], 2 * this.uvArr[a] + 1);
this.uv.push(2 * this.uvArr[b], 2 * this.uvArr[b] + 1);
this.uv.push(2 * this.uvArr[c], 2 * this.uvArr[c] + 1);
} else {
this.uv.push(2 * this.uvArr[a], 2 * this.uvArr[a] + 1);
this.uv.push(2 * this.uvArr[b], 2 * this.uvArr[b] + 1);
this.uv.push(2 * this.uvArr[c], 2 * this.uvArr[c] + 1);
this.uv.push(2 * this.uvArr[d], 2 * this.uvArr[d] + 1);
}
};
這裏我們考慮到兼容obj文件中f(ace)行中4個值的情況,導出obj文件中可以強行選擇只有三角面,不過我們在代碼中兼容一下比較穩妥
4、旋轉平移等變換
物體全部導入進去,剩下來的任務就是進行變換了,首先我們分析一下有哪些動畫效果
因爲我們模擬的是一個宇宙,3D文字就像是星球一樣,有公轉和自轉;還有就是我們導入的obj文件都是基於(0,0,0)點的,所以我們
還需要把它們進行平移操作
先上核心代碼~
......
this.angle += this.rotate; // 自轉的角度
var s = Math.sin(this.angle);
var c = Math.cos(this.angle);
// 公轉相關數據
var gs = Math.sin(globalTime * this.revolution); // globalTime是全局的時間
var gc = Math.cos(globalTime * this.revolution);
webgl.uniformMatrix4fv(
this.program.uMMatrix, false, mat4.multiply([
gc,0,-gs,0,
0,1,0,0,
gs,0,gc,0,
0,0,0,1
], mat4.multiply(
[
1,0,0,0,
0,1,0,0,
0,0,1,0,
this.x,this.y,this.z,1 // x,y,z是偏移的位置
],[
c,0,-s,0,
0,1,0,0,
s,0,c,0,
0,0,0,1
]
)
)
);
一眼望去uMMatrix(模型矩陣)裏面有三個矩陣,爲什麼有三個呢,它們的順序有什麼要求麼?
因爲矩陣不滿足交換率,所以我們矩陣的平移和旋轉的順序十分重要,先平移再旋轉和先旋轉再平移有如下的差異
(下面圖片來源於網絡)
先旋轉後平移:
先平移後旋轉:
從圖中明顯看出來
先旋轉後平移是自轉
,而
先平移後旋轉是公轉
所以我們矩陣的順序一定是 公轉 * 平移 * 自轉 * 頂點信息(右乘)
具體矩陣爲何這樣寫可見上一篇矩陣入門文章
這樣一個3D文字的8大行星就形成啦
4、裝飾星星
光禿禿的幾個文字肯定不夠,所以我們還需要一點點綴,就用幾個點當作星星,非常簡單
注意
默認渲染webgl.POINTS是方形的
,所以我們得在fragment shader中加工處理一下
precision highp float;
void main() {
float dist = distance(gl_PointCoord, vec2(0.5, 0.5)); // 計算距離
if(dist < 0.5) {
gl_FragColor = vec4(0.9, 0.9, 0.8, pow((1.0 - dist * 2.0), 3.0));
} else {
discard; // 丟棄
}
}
結語
需要關注的是這裏我用了另外一對shader,此時就涉及到了關於是用多個program shader還是在同一個shader中使用if statements,這兩者性能如何,有什麼區別
這裏將放在下一篇webgl相關優化中去說
本文就到這裏啦,有問題和建議的小夥伴歡迎留言一起討論~!