glTFから読み込んだポリゴンを表示する

さて、.binファイルから取ってきたポリゴンを実際に表示してみましょう。

ポリゴンの表示はMeshクラスが担当していましたから、このクラスにポリゴンデータを渡せば良さそうです。
受け渡し方は、MeshクラスのコンストラクタにVertexAttributeSetという型の値として渡せばよいのでしたね。

ただ、現状のVertexAttributeSetは各プロパティの値として通常の配列しか受け取れません。.binファイルから取ってきた頂点情報はFloat32Arrayなどのいわゆる「TypedArray」でしたね。これらも受け付けるように型定義を変更しましょう。

// Mesh.ts export type VertexAttributeSet = { position: number[] | Float32Array, // <-変更 color?: number[] | Float32Array, // <-変更 normal?: number[] | Float32Array, // <-変更 texcoord?: number[] | Float32Array, // <-変更 indices?: number[] | Uint16Array // <-変更 }

また、WebGLのVertexBufferやIndexBufferを作成する関数は、配列が渡されることを想定していましたから、TypedArrayが渡されても対応できるように少し書き換えます。

private _setupVertexBuffer(_array: number[] | Float32Array, defaultArray: number[]) { // <-変更 let array = _array; if (array == null) { array = []; for (let i=0; i<this._vertexNumber; i++) { array = array.concat(defaultArray); } } const gl = this._context.gl; const buffer = gl.createBuffer() as WebGLBuffer; gl.bindBuffer(gl.ARRAY_BUFFER, buffer); const typedArray = (array.constructor === Float32Array) ? array as Float32Array : new Float32Array(array); // <-追加 gl.bufferData(gl.ARRAY_BUFFER, typedArray, gl.STATIC_DRAW); // <-変更 return buffer; } private _setupIndexBuffer(indicesArray: number[] | Uint16Array) { // <-変更 const gl = this._context.gl; const buffer = gl.createBuffer() as WebGLBuffer; gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); const typedArray = (indicesArray.constructor === Uint16Array) ? indicesArray as Uint16Array : new Uint16Array(indicesArray); // <-追加 gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, typedArray, gl.STATIC_DRAW); // <-変更 return buffer; }

さて、Meshクラス側の準備はできたので、Gltf2Importerクラス側も変更しましょう。Meshクラスを生成して頂点情報をセットし、呼び出し元にMeshを返すようにしてあげます。
Meshインスタンスの生成にはContextとMaterialのインスタンスが必要です。これはimportメソッドの引数として呼び出し元から渡してあげることにしましょう。これで、glTFファイルから読み込んだメッシュに対して、ユーザーが好きな質感(シェーダー)を適用することができます。

// Gltf2Importer.ts async import(uri: string, context: Context, material: Material) { // <-変更 let response: Response; try { response = await fetch(uri); } catch (err) { console.log('glTF2 load error.', err); }; const arrayBuffer = await response!.arrayBuffer(); const gotText = this._arrayBufferToString(arrayBuffer); const json = JSON.parse(gotText) as Gltf2 const arrayBufferBin = await this._loadBin(json, uri); const meshes = this._loadMesh(arrayBufferBin, json, context, material); // <-変更 return meshes; // <-追加 } ... private _loadMesh(arrayBufferBin: ArrayBuffer, json: Gltf2, context: Context, material: Material) { const meshes: Mesh[] = [] for (let mesh of json.meshes) { const primitive = mesh.primitives[0]; const attributes = primitive.attributes; const positionAccessor = json.accessors[attributes.POSITION] as Gltf2Accessor; const positionBufferView = json.bufferViews[positionAccessor.bufferView!] as Gltf2BufferView; const byteOffsetOfBufferView = positionBufferView.byteOffset!; const byteOffsetOfAccessor = positionAccessor.byteOffset!; const byteOffset = byteOffsetOfBufferView + byteOffsetOfAccessor; const positionComponentBytes = this._componentBytes(positionAccessor.componentType); const positionComponentNum = this._componentNum(positionAccessor.type); const count = positionAccessor.count; const typedArrayComponentCount = positionComponentNum * count; const positionTypedArrayClass = this._componentTypedArray(positionAccessor.componentType); const positionTypedArray = new positionTypedArrayClass(arrayBufferBin, byteOffset, typedArrayComponentCount) as Float32Array; const vertexData: VertexAttributeSet = { // <-追加 position: positionTypedArray // <-追加 } // <-追加 const libMesh = new Mesh(material, context, vertexData); // <-追加 meshes.push(libMesh); // <-追加 } return meshes; // <-追加 }

呼び出し側のユーザーコードも大きく書き換えます。これまでのサンプルでやった三角形のテスト描画のコードは、紛らわしいのでもう削除してしまいましょう。読み込んだglTFファイルのみを描画するように以下のように書き換えます。

// main.ts import Spinel from '../dist/index.js' const vertexShaderStr = ` precision highp float; attribute vec3 a_position; attribute vec4 a_color; varying vec4 v_color; void main(void) { gl_Position = vec4(a_position, 1.0); v_color = a_color; } `; const fragmentShaderStr = ` precision highp float; varying vec4 v_color; void main(void) { gl_FragColor = v_color; } `; async function main() { const canvas = document.getElementById('world') as HTMLCanvasElement; const context = new Spinel.Context(canvas); const material = new Spinel.Material(context, vertexShaderStr, fragmentShaderStr); const glTF2Importer = Spinel.Gltf2Importer.getInstance(); const meshes = await glTF2Importer.import('../assets/gltf/BoxAnimated/glTF/BoxAnimated.gltf', context, material); const gl = context.gl; gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); for (let mesh of meshes) { mesh.draw(); } } main();

実行してみましょう。真っ白い四角形が表示されました。これは……あっているのでしょうか?
読み込んでいるサンプルのglTFファイルは、こういう感じ (リンク先のスクリーンショットのGifアニメ参照)のデータです。大小2つのキューブがあり、そのうちの少し小さい1つがアニメーションして、下の大きめのキューブに出たり入ったりします。

今回、アニメーションデータは取り出していませんので、初期位置として大小2つのキューブが同じ場所に重なっています。さらに、今回の読み込みでは頂点位置座標しか取り出していませんので、カラーはMeshクラス初期色の白で描画されています。また、カメラクラスなどはまだ作成しておらず、座標変換もしていませんので、リンク先のGifアニメと違い、ちょうど真正面からキューブを見る構図になっています。

ということは、どうやら現状の実装の結果としては正しそうですね。

image.png

さて、現在は頂点の位置座標のみを読み込むコードですが、より一般化して、その他の頂点属性も読めるようにしましょう。カラーにも対応させます。

// Gltf2Importer.ts private _loadMesh(arrayBufferBin: ArrayBuffer, json: Gltf2, context: Context, material: Material) { const meshes: Mesh[] = [] for (let mesh of json.meshes) { const primitive = mesh.primitives[0]; const attributes = primitive.attributes; const positionTypedArray = this.getAttribute(json, attributes.POSITION, arrayBufferBin); let colorTypedArray: Float32Array; if (attributes.COLOR_0) { colorTypedArray = this.getAttribute(json, attributes.COLOR_0, arrayBufferBin); } const vertexData: VertexAttributeSet = { position: positionTypedArray, color: colorTypedArray! } const libMesh = new Mesh(material, context, vertexData); meshes.push(libMesh); } return meshes; } private getAttribute(json: Gltf2, attributeIndex: number, arrayBufferBin: ArrayBuffer) { const accessor = json.accessors[attributeIndex] as Gltf2Accessor; const bufferView = json.bufferViews[accessor.bufferView!] as Gltf2BufferView; const byteOffsetOfBufferView = bufferView.byteOffset!; const byteOffsetOfAccessor = accessor.byteOffset!; const byteOffset = byteOffsetOfBufferView + byteOffsetOfAccessor; const componentBytes = this._componentBytes(accessor.componentType); const componentNum = this._componentNum(accessor.type); const count = accessor.count; const typedArrayComponentCount = componentNum * count; const typedArrayClass = this._componentTypedArray(accessor.componentType); const typedArray = new typedArrayClass(arrayBufferBin, byteOffset, typedArrayComponentCount) as Float32Array; return typedArray; }

このモデルは、本来色がついているのですが、どうやら色の情報はマテリアルとして持っているようで、頂点カラーとしては持っていないようです。ですので、結果は先ほどと同様、真っ白な画像のままです。

次は、マテリアルも読めるようにしていきましょう。

ここまでのプロジェクトコードは、こちらで公開しています 

7日目:glTFのメッシュを表示してみよう

9日目:glTFのマテリアルを描画に反映しよう