目錄
- 1. 概述
- 2. 實體
- 2.1. 資料
- 2.2. 程式
- 2.2.1. 檔案讀取
- 2.2.2. glTF格式決議
- 2.2.2.1. 場景節點
- 2.2.2.2. 網格
- 2.2.2.3. 緩沖,緩沖視圖和訪問器
- 2.2.2.4. 紋理材質
- 2.2.3. 初始化頂點緩沖區
- 2.2.4. 其他
- 3. 結果
- 4. 參考
- 5. 相關
1. 概述
一般來說,圖形渲染總是需要從磁盤資料開始,最終保存到磁盤資料中,保存這種資料的就是3D模型檔案,3D模型檔案一般會把頂點、索引、紋理、材質等等資訊都保存起來,方便下次直接讀取,3D模型檔案格式一般是與圖形渲染作業強關聯的,了解3D模型檔案格式的組成,有助于進一步了解圖形渲染的流程,
glTF可以說是專門為WebGL量身定制的資料格式,具有以下特點:
- 場景資料結構是使用JSON來描述的,讀取后即可決議,無需再自定義組織物件,
- buffer資料被保存為二進制檔案,占用空間小,讀取后即可使用,無需轉換程序,
- 紋理資料可以使用jpg檔案,方便壓縮和傳輸,
從以上特性可以看出,glTF特別方便與互聯網的使用場景,便于傳輸且預處理程度小,在這篇教程中,就通過一個帶紋理的地形檔案,具體決議以下glTF格式,順便加深一下WebGL中初始化資料的理解,
2. 實體
2.1. 資料
使用的地形glTF檔案已經處理好并上傳到文章末尾的地址中(具體的轉換程序可以參看《DEM轉換為gltf》),glTF是這樣一個JSON檔案:
{
"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網站上查看,顯示結果如下:
注意,由于安全策略的原因,瀏覽器匯入資料時應該將new.gltf、new.bin、tex.jpg這三個檔案一同匯入,否則無法正確讀取顯示,
2.2. 程式
2.2.1. 檔案讀取
由于需要一次性加載多個檔案,所以需要將input控制元件改成支持多檔案的:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title> 顯示地形 </title>
</head>
<body onl oad="main()">
<div><input type='file' id='demFile' multiple="multiple"></div>
<div>
<canvas id="webgl" width="600" height="600">
請使用支持WebGL的瀏覽器
</canvas>
</div>
<script src="https://www.cnblogs.com/charlee44/lib/webgl-utils.js"></script>
<script src="https://www.cnblogs.com/charlee44/lib/webgl-debug.js"></script>
<script src="https://www.cnblogs.com/charlee44/lib/cuon-utils.js"></script>
<script src="https://www.cnblogs.com/charlee44/lib/cuon-matrix.js"></script>
<script src="https://www.cnblogs.com/charlee44/p/TerrainViewer.js"></script>
</body>
</html>
在glTF Viewer網站中查看glTF的原理并不是將資料提交到后臺,而是直接交給前段頁面的JS進行讀取,可以通過FileReader物件來進行讀取,FileReader讀取的好處是不會觸發瀏覽器的安全策略,不用設定跨域(至少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 = https://www.cnblogs.com/charlee44/p/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()函式進行繪制,讀取得到的物件也不用再多做處理,可以直接在后面的初始化步驟中使用,
2.2.2. glTF格式決議
初始化頂點緩沖區函式initVertexBuffers()中就用到了之前獲取的物件,gltfObj是獲取的JSON物件,里面記錄了對三維物體的描述資訊,具體決議如下:
2.2.2.1. 場景節點
"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則表示場景節點中的幾何物件,
2.2.2.2. 網格
"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節點中可以找到其具體的描述,
2.2.2.3. 緩沖,緩沖視圖和訪問器
"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物件則進一步描述了頂點資料的組織,
- 屬性bufferView表示的就是前面bufferViews物件的索引值,
- byteOffset表示資料從那個位元組開始;componentType表示保存的資料型別,5123表示為UNSIGNED_SHORT型,占用2個位元組;而5126表示FLOAT信號,占用4個位元組,
- count表示資料的個數,
- type表示資料的型別,可以為標量SCALAR,也可以為矢量"VEC2"、"VEC3"等,甚至可以為矩陣"MAT3"等,
- min,max則表示每個值得最大最小值,填寫正確的范圍,有助于瀏覽操作,
通過以上屬性值,就能夠正確區分描述頂點資料資訊了,注意頂點資料的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中是沒有空格和回車的,這里只是為了方便好看,
2.2.2.4. 紋理材質
"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中設定紋理的方式向對應,
2.2.3. 初始化頂點緩沖區
讀取后的資料可以直接交給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簡易教程(三):繪制一個三角形(緩沖區物件)》創建緩沖區物件的五個步驟),
2.2.4. 其他
程式其他的步驟基本上沒有變化,由于資料讀取后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';
紋理坐標傳入頂點著色器再傳入片元著色器,通過紋理物件插值得到片元最終值,
3. 結果
從以上決議程序可以看到,glTF的格式設計確實非常精妙,讀取的資料能夠直接為WebGL所用,既節省了空間又省略了一些預處理的程序,值得進一步深入研究,
打開HTML頁面,匯入new.gltf、new.bin、tex.jpg,顯示的效果如下:
這個例子是通過JS的FileReader來處理資料,所以不需要設定瀏覽器跨域,
4. 參考
1.《WebGL編程指南》
2.glTF格式詳解(目錄)
3.glTF Tutorial
4.前端H5中JS用FileReader物件讀取blob物件二進制資料,檔案傳輸
5.gltf2.0規范
6.JavaScript 之 ArrayBuffer
5. 相關
代碼和資料地址
上一篇
目錄
下一篇
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/14256.html
標籤:其他
上一篇:2D地圖擦除演算法
