目錄javascript
通常來講,圖形渲染老是須要從磁盤數據開始,最終保存到磁盤數據中,保存這種數據的就是3D模型文件。3D模型文件通常會把頂點、索引、紋理、材質等等信息都保存起來,方便下次直接讀取。3D模型文件格式通常是與圖形渲染工做強關聯的,瞭解3D模型文件格式的組成,有助於進一步瞭解圖形渲染的流程。html
glTF能夠說是專門爲WebGL量身定製的數據格式,具備如下特色:前端
從以上特性能夠看出,glTF特別方便與互聯網的使用場景,便於傳輸且預處理程度小。在這篇教程中,就經過一個帶紋理的地形文件,具體解析如下glTF格式,順便加深一下WebGL中初始化數據的理解。java
使用的地形glTF文件已經處理好並上傳到文章末尾的地址中(具體的轉換過程能夠參看《DEM轉換爲gltf》)。glTF是這樣一個JSON文件:node
{ "asset": { "generator": "CL", "version": "2.0" }, "scene": 0, "scenes": [ { "nodes": [ 0 ] } ], "nodes": [ { "mesh": 0 } ], "meshes": [ { "primitives": [ { "attributes": { "POSITION": 1, "TEXCOORD_0": 2 }, "indices": 0, "material": 0 } ] } ], "materials": [ { "pbrMetallicRoughness": { "baseColorTexture": { "index": 0 } } } ], "textures": [ { "sampler": 0, "source": 0 } ], "images": [ { "uri": "tex.jpg" } ], "samplers": [ { "magFilter": 9729, "minFilter": 9987, "wrapS": 33648, "wrapT": 33648 } ], "buffers": [ { "uri": "new.bin", "byteLength": 595236 } ], "bufferViews": [ { "buffer": 0, "byteOffset": 374400, "byteLength": 220836, "target": 34963 }, { "buffer": 0, "byteStride": 20, "byteOffset": 0, "byteLength": 374400, "target": 34962 } ], "accessors": [ { "bufferView": 0, "byteOffset": 0, "componentType": 5123, "count": 110418, "type": "SCALAR", "max": [ 18719 ], "min": [ 0 ] }, { "bufferView": 1, "byteOffset": 0, "componentType": 5126, "count": 18720, "type": "VEC3", "max": [ 770, 0.0, 1261.151611328125 ], "min": [ 0.0, -2390, 733.5555419921875 ] }, { "bufferView": 1, "byteOffset": 12, "componentType": 5126, "count": 18720, "type": "VEC2", "max": [ 1, 1 ], "min": [ 0, 0 ] } ] }
能夠看到這個文件連接了兩個外部文件new.bin和tex.jpg。new.bin也就是保存的頂點數據信息,是個二進制文件,tex.jpg也就是紋理圖片。將這個數據導入到glTF Viewer網站上查看,顯示結果以下:git
注意,因爲安全策略的緣由,瀏覽器導入數據時應該將new.gltf、new.bin、tex.jpg這三個文件一同導入,不然沒法正確讀取顯示。github
因爲須要一次性加載多個文件,因此須要將input控件改爲支持多文件的:web
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title> 顯示地形 </title> </head> <body onload="main()"> <div><input type='file' id='demFile' multiple="multiple"></div> <div> <canvas id="webgl" width="600" height="600"> 請使用支持WebGL的瀏覽器 </canvas> </div> <script src="../lib/webgl-utils.js"></script> <script src="../lib/webgl-debug.js"></script> <script src="../lib/cuon-utils.js"></script> <script src="../lib/cuon-matrix.js"></script> <script src="TerrainViewer.js"></script> </body> </html>
在glTF Viewer網站中查看glTF的原理並非將數據提交到後臺,而是直接交給前段頁面的JS進行讀取。能夠經過FileReader對象來進行讀取。FileReader讀取的好處是不會觸發瀏覽器的安全策略,不用設置跨域(至少chrome不用):chrome
var demFile = document.getElementById('demFile'); if (!demFile) { console.log("Failed to get demFile element!"); return; } //加載文件後的事件 demFile.addEventListener("change", function (event) { //判斷瀏覽器是否支持FileReader接口 if (typeof FileReader == 'undefined') { console.log("你的瀏覽器不支持FileReader接口!"); return; } //讀取文件後的事件 var reader = new FileReader(); reader.onload = function () { if (reader.result) { var gltfObj = JSON.parse(reader.result); for (var fi = 0; fi < input.files.length; fi++) { //讀取bin文件 if (gltfObj.buffers[0].uri === input.files[fi].name) { var binReader = new FileReader(); binReader.onload = function () { if (binReader.result) { for (var fi = 0; fi < input.files.length; fi++) { if (gltfObj.images[0].uri === input.files[fi].name) { //讀取紋理圖像 var imgReader = new FileReader(); imgReader.onload = function () { //建立一個image對象 var image = new Image(); if (!image) { console.log('Failed to create the image object'); return false; } //圖像加載的響應函數 image.onload = function () { //繪製函數 onDraw(gl, canvas, gltfObj, binReader.result, image); }; //瀏覽器開始加載圖像 image.src = imgReader.result; } imgReader.readAsDataURL(input.files[fi]); //按照base64格式讀取 break; } } } } binReader.readAsArrayBuffer(input.files[fi]); //按照ArrayBuffer格式讀取 break; } } } } var input = event.target; var flag = false; for (var fi = 0; fi < input.files.length; fi++) { if (getFileSuffix(input.files[fi].name) === "gltf") { flag = true; reader.readAsText(input.files[fi]); //按照字符串格式讀取 break; } } if (!flag) { alert("沒有找到gltf"); } });
這段代碼看起來很繁複,其實原理很簡單:遍歷加載的文件,對於gltf文件採用FileReader.readAsText()也就是字符串格式的方法讀取,這個字符串隨後被解析成JSON;對於bin文件採用FileReader.readAsArrayBuffer()讀取,將其讀取成ArrayBuffer對象;對於jpg文件採用FileReader.readAsDataURL讀取,將其讀取成data:url開頭的base64字符串,這個字符串能夠直接生成JS的Image對象。編程
注意FileReader的讀取方式都是異步讀取,必須等到三個文件都讀取完成,才調用onDraw()函數進行繪製。讀取獲得的對象也不用再多作處理,能夠直接在後面的初始化步驟中使用。
初始化頂點緩衝區函數initVertexBuffers()中就用到了以前獲取的對象。gltfObj是獲取的JSON對象,裏面記錄了對三維物體的描述信息。具體解析以下:
"asset": { "generator": "CL", "version": "2.0" }, "scene": 0, "scenes": [ { "nodes": [ 0 ] } ], "nodes": [ { "mesh": 0 } ],
asset表示的是元數據信息,version通常爲2.0。
scene是整個場景的入口,0表示scenes數組的第一個;scenes節點又包含了一個nodes數組,其中每一個nodes對象包含一個children數組,這一數組引用了nodes對象的全部子結點。經過孩子結點,構成了整個場景結構:
這一段描述的實際上是場景的結構層次模型。基本上來說,通常的三維渲染引擎都會將三維場景中的物體分解成節點,採用樹的結構來描述場景,這樣作可以很方便的進行狀態控制以及姿態傳遞。這裏沒有那麼複雜的結構,就簡化爲0。
mesh則表示場景節點中的幾何對象。
"meshes": [ { "primitives": [ { "attributes": { "POSITION": 1, "TEXCOORD_0": 2 }, "indices": 0, "material": 0 } ] } ],
mesh對象包含了一個primitive數組對象。primitive表達的是一個圖元,描述每一個網格是怎樣的幾何圖形。其attributes對象表達了圖元頂點的屬性。這裏的POSITION屬性表示頂點的位置信息,屬性值1表示訪問器對象accessors數組的索引;TEXCOORD_0表示頂點的紋理位置信息,屬性值2表示訪問器對象accessors數組的索引。
indices屬性表示圖元頂點數據是經過索引來描述的,其值3表示訪問器對象accessors數組的索引。
而material則表示圖元用到了材質,在materials節點中能夠找到其具體的描述。
"buffers": [ { "uri": "new.bin", "byteLength": 595236 } ], "bufferViews": [ { "buffer": 0, "byteOffset": 374400, "byteLength": 220836, "target": 34963 }, { "buffer": 0, "byteStride": 20, "byteOffset": 0, "byteLength": 374400, "target": 34962 } ], "accessors": [ { "bufferView": 0, "byteOffset": 0, "componentType": 5123, "count": 110418, "type": "SCALAR", "max": [ 18719 ], "min": [ 0 ] }, { "bufferView": 1, "byteOffset": 0, "componentType": 5126, "count": 18720, "type": "VEC3", "max": [ 770, 0.0, 1261.151611328125 ], "min": [ 0.0, -2390, 733.5555419921875 ] }, { "bufferView": 1, "byteOffset": 12, "componentType": 5126, "count": 18720, "type": "VEC2", "max": [ 1, 1 ], "min": [ 0, 0 ] } ]
這裏詳細描述了上面提到的訪問器對象accessors。之因此定義這個屬性對象,是由於頂點數據信息被直接保存爲二進制buffer了,須要去區分描述buffer哪些是位置信息,哪些是紋理座標信息,哪些是索引信息。
buffers對象就是頂點數據的二進制buffer,url表示被保存爲外部的二進制文件new.bin,byteLength表示其長度爲595236,這個文件在導入的時候會被讀取成JS的ArrayBuffer對象。
bufferViews對象將buffers分紅兩個視圖:前374400個字節表達的是頂點數據,步長byteStride爲20個表示每20個字節的數據表達一個頂點,target爲34962表示的就是ARRAY_BUFFER;而從374400開始的220836個字節表示的是頂點索引的數據,target爲34963表示的就是ELEMENT_ARRAY_BUFFER。
accessors對象則進一步描述了頂點數據的組織。
經過以上屬性值,就可以正確區分描述頂點數據信息了。注意頂點數據的bufferViews對象在accessors對象被進一步劃分視圖,分別描述了位置信息和紋理座標信息:bufferViews對象的步長byteStride被設置爲20,accessors對象的偏移量byteOffset分別設置爲0和12,說明二進制bin中的組織的結構爲:
位置X座標 位置Y座標 位置Z座標 紋理S座標 紋理T座標
位置X座標 位置Y座標 位置Z座標 紋理S座標 紋理T座標
位置X座標 位置Y座標 位置Z座標 紋理S座標 紋理T座標
...
固然,二進制bin中是沒有空格和回車的,這裏只是爲了方便好看。
"materials": [ { "pbrMetallicRoughness": { "baseColorTexture": { "index": 0 } } } ], "textures": [ { "sampler": 0, "source": 0 } ], "images": [ { "uri": "tex.jpg" } ], "samplers": [ { "magFilter": 9729, "minFilter": 9987, "wrapS": 33648, "wrapT": 33648 } ],
在primitives對象的material的屬性中,指向的就是這個materials節點的索引值。materials對象又指向了紋理對象textures,textures對象經過索引引用了一個sampler對象和一個image對象。image對象包含了一個uri,引用了一個外部圖像文件。samplers是一個採樣器,用於設置紋理具體的採樣方式,其設置參數與WebGL中設置紋理的方式向對應。
讀取後的數據能夠直接交給initVertexBuffers()初始化頂點緩衝區,具體的實現代碼以下:
// function initVertexBuffers(gl, gltfObj, binBuf) { //獲取頂點數據位置信息 var positionAccessorId = gltfObj.meshes[0].primitives[0].attributes.POSITION; if (gltfObj.accessors[positionAccessorId].componentType != 5126) { return 0; } var positionBufferViewId = gltfObj.accessors[positionAccessorId].bufferView; var verticesColors = new Float32Array(binBuf, gltfObj.bufferViews[positionBufferViewId].byteOffset, gltfObj.bufferViews[positionBufferViewId].byteLength / Float32Array.BYTES_PER_ELEMENT); gltfObj.cuboid = new Cuboid(gltfObj.accessors[positionAccessorId].min[0], gltfObj.accessors[positionAccessorId].max[0], gltfObj.accessors[positionAccessorId].min[1], gltfObj.accessors[positionAccessorId].max[1], gltfObj.accessors[positionAccessorId].min[2], gltfObj.accessors[positionAccessorId].max[2]); // 建立緩衝區對象 var vertexColorBuffer = gl.createBuffer(); var indexBuffer = gl.createBuffer(); if (!vertexColorBuffer || !indexBuffer) { console.log('Failed to create the buffer object'); return -1; } // 將緩衝區對象綁定到目標 gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer); // 向緩衝區對象寫入數據 gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW); //獲取着色器中attribute變量a_Position的地址 var a_Position = gl.getAttribLocation(gl.program, 'a_Position'); if (a_Position < 0) { console.log('Failed to get the storage location of a_Position'); return -1; } // 將緩衝區對象分配給a_Position變量 gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, gltfObj.bufferViews[positionBufferViewId].byteStride, gltfObj.accessors[positionAccessorId].byteOffset); // 鏈接a_Position變量與分配給它的緩衝區對象 gl.enableVertexAttribArray(a_Position); //獲取頂點數據紋理信息 var txtCoordAccessorId = gltfObj.meshes[0].primitives[0].attributes.TEXCOORD_0; if (gltfObj.accessors[txtCoordAccessorId].componentType != 5126) { return 0; } var txtCoordBufferViewId = gltfObj.accessors[txtCoordAccessorId].bufferView; //獲取着色器中attribute變量a_TxtCoord的地址 var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord'); if (a_TexCoord < 0) { console.log('Failed to get the storage location of a_TexCoord'); return -1; } // 將緩衝區對象分配給a_Color變量 gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, gltfObj.bufferViews[txtCoordBufferViewId].byteStride, gltfObj.accessors[txtCoordAccessorId].byteOffset); // 鏈接a_Color變量與分配給它的緩衝區對象 gl.enableVertexAttribArray(a_TexCoord); //獲取頂點數據索引信息 var indicesAccessorId = gltfObj.meshes[0].primitives[0].indices; var indicesBufferViewId = gltfObj.accessors[indicesAccessorId].bufferView; var indices = new Uint16Array(binBuf, gltfObj.bufferViews[indicesBufferViewId].byteOffset, gltfObj.bufferViews[indicesBufferViewId].byteLength / Uint16Array.BYTES_PER_ELEMENT); // 將頂點索引寫入到緩衝區對象 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW); return indices.length; }
這段代碼的原理很是簡單,讀取的glTF被直接解析爲JSON後,經過primitives屬性找到頂點位置座標和頂點紋理座標的訪問器對象accessors,繼而找到緩衝區buffer和緩衝區視圖bufferView。因爲緩衝區數據文件new.bin已經被讀取成ArrayBuffer,能夠將這個ArrayBuffer分紅兩個視圖[6],一組視圖爲Float32Array類型的頂點數組,一組視圖爲Uint16Array類型的頂點數組索引。其中,頂點數組能夠經過 gl.vertexAttribPointer()函數作進一步分配,分別給着色器分配位置變量和紋理座標變量(能夠複習一下《WebGL簡易教程(三):繪製一個三角形(緩衝區對象)》建立緩衝區對象的五個步驟)。
程序其餘的步驟基本上沒有變化,因爲數據讀取後JS的Image對象已經生成,仍然按照之前的方式根據Image對象生成紋理對象。着色器部分也很是簡單:
// 頂點着色器程序 var VSHADER_SOURCE = 'attribute vec4 a_Position;\n' + //位置 'attribute vec2 a_TexCoord;\n' + //顏色 'varying vec2 v_TexCoord;\n' + //紋理座標 'uniform mat4 u_MvpMatrix;\n' + 'void main() {\n' + ' gl_Position = u_MvpMatrix * a_Position;\n' + // 設置頂點座標 ' v_TexCoord = a_TexCoord;\n' + //紋理座標 '}\n'; // 片元着色器程序 var FSHADER_SOURCE = 'precision mediump float;\n' + 'uniform sampler2D u_Sampler;\n' + 'varying vec2 v_TexCoord;\n' + //紋理座標 'void main() {\n' + ' gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' + '}\n';
紋理座標傳入頂點着色器再傳入片元着色器,經過紋理對象插值獲得片元最終值。
從以上解析過程能夠看到,glTF的格式設計確實很是精妙,讀取的數據可以直接爲WebGL所用,既節省了空間又省略了一些預處理的過程,值得進一步深刻研究。
打開HTML頁面,導入new.gltf、new.bin、tex.jpg,顯示的效果以下:
這個例子是經過JS的FileReader來處理數據,因此不須要設置瀏覽器跨域。
1.《WebGL編程指南》
2.glTF格式詳解(目錄)
3.glTF Tutorial
4.前端H5中JS用FileReader對象讀取blob對象二進制數據,文件傳輸
5.gltf2.0規範
6.JavaScript 之 ArrayBuffer