收錄待用,修改轉載已取得騰訊雲受權html
做者:TAT.vorshengit
Webgl的魅力在於能夠創造一個本身的3D世界,但相比較canvas2D來講,除了物體的移動旋轉變換徹底依賴矩陣增長了複雜度,就連生成一個物體都變得很複雜。github
什麼?!爲何不用Threejs?Threejs等庫確實能夠很大程度的提升開發效率,並且各方面封裝的很是棒,可是不推薦初學者直接依賴Threejs,最好是把webgl各方面都學會,再去擁抱Three等相關庫。web
上篇矩陣入門中介紹了矩陣的基本知識,讓你們瞭解到了基本的仿射變換矩陣,能夠對物體進行移動旋轉等變化,而這篇文章將教你們快速生成一個物體,而且結合變換矩陣在物體在你的世界裏動起來。canvas
注:本文適合稍微有點webgl基礎的人同窗,至少知道shader,知道如何畫一個物體在webgl畫布中數組
咱們先稍微對比下基本圖形的建立代碼網絡
矩形:canvas2D性能
ctx1.rect(50, 50, 100, 100); ctx1.fill();
webgl(shader和webgl環境代碼忽略)優化
var 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
var 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文件是用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去建模。
這裏咱們使用blender生成文字
[][https://vorshen.github.io/simple-3d-text-universe/doc/assets/help.gif](https://vorshen.github.io/simple-3d-text-universe/doc/assets/help.gif))
var 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部分源碼
Text3d.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文件中能夠強行選擇只有三角面,不過咱們在代碼中兼容一下比較穩妥
物體所有導入進去,剩下來的任務就是進行變換了,首先咱們分析一下有哪些動畫效果
由於咱們模擬的是一個宇宙,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大行星就造成啦
光禿禿的幾個文字確定不夠,因此咱們還須要一點點綴,就用幾個點看成星星,很是簡單
注意默認渲染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相關優化中去說。