glTFで全てのプリミティブを表示しよう

glTFの構造では、ポリゴンメッシュの構造は以下のように多層になっています。

Mesh -> 複数のPrimitives -> それぞれのPrimitiveについて各頂点属性とひとつのMaterial

今までのプログラムでは、コードを簡単にするために、glTFの各Meshについて、0番目のPrimitive以外を無視してしまっていました。今回から、全てのPrimitiveを処理の対象とするようにしてみましょう。

大きなコード変更が必要に……

さて、ここで困ったことがあります。glTFではポリゴンメッシュの構造がMesh->Primitivesと2階層あるのでしたね。

ところが、現段階のSpinelライブラリはPrimitiveに相当するものがなく、Meshクラスしかありません。SpinelのMeshクラスで「glTFのMeshが持つ0番目のPrimitive」の情報を持っているという少しわかりにくい対応関係です。このままでは、複数のPrimitivesをうまく扱えません。

そこで、glTFの構造に合わせて、SpinelライブラリにもPrimitiveクラスを導入しましょう。

正確には、今までのMeshクラスを内容ほぼそのままにPrimitiveクラスに改名し、新たにそれらを取りまとめる新Meshクラスを導入します。

Meshクラス->Primitiveクラス

まず、Mesh.tsのファイル名をPrimitive.tsに変更しましょう。さらに、ファイルの中のクラス名記述もMeshからPrimitiveに変更します。

// Mesh.ts -> Primitive.ts import Material from "./Material.js"; import Context from "./Context.js"; export type VertexAttributeSet = { position: number[] | Float32Array, color?: number[] | Float32Array, normal?: number[] | Float32Array, texcoord?: number[] | Float32Array, indices?: number[] | Uint16Array } export default class Primitive { private _positionBuffer: WebGLBuffer; private _colorBuffer: WebGLBuffer; private _indexBuffer?: WebGLBuffer; private _vertexNumber = 0; private _indexNumber = 0; private _material: Material; private _context: Context; private static readonly _positionComponentNumber = 3; private static readonly _colorComponentNumber = 4; constructor(material: Material, context: Context, vertexData: VertexAttributeSet) { this._material = material; this._context = context; this._vertexNumber = vertexData.position.length / Primitive._positionComponentNumber; this._positionBuffer = this._setupVertexBuffer(vertexData.position, [0, 0, 0]); this._colorBuffer = this._setupVertexBuffer(vertexData.color!, [1, 1, 1, 1]); if (vertexData.indices != null) { this._indexBuffer = this._setupIndexBuffer(vertexData.indices); this._indexNumber = vertexData.indices.length; } } 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; } private _setVertexAttribPointer(vertexBuffer: WebGLBuffer, attributeSlot: number, componentNumber: number) { if (vertexBuffer != null) { const gl = this._context.gl; gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); gl.vertexAttribPointer( attributeSlot, componentNumber, gl.FLOAT, false, 0, 0); } } draw() { const gl = this._context.gl; this._setVertexAttribPointer(this._positionBuffer, this.material.program!._attributePosition, Primitive._positionComponentNumber); this._setVertexAttribPointer(this._colorBuffer!, this.material.program!._attributeColor, Primitive._colorComponentNumber); this._material.useProgram(gl); this._material.setUniformValues(gl); if (this._indexBuffer != null) { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this._indexBuffer); gl.drawElements(gl.TRIANGLES, this._indexNumber, gl.UNSIGNED_SHORT, 0); } else { gl.drawArrays(gl.TRIANGLES, 0, this.vertexNumber); } } get vertexNumber() { return this._vertexNumber; } get material() { return this._material; } }

新Meshクラス

次に、複数のPrimitiveをまとめる新しいMeshクラスを作ります。これがglTFのMeshにそのまま対応します。

// Mesh.ts import Primitive from "./Primitive"; export default class Mesh { private _primitives: Primitive[]; constructor(primitives: Primitive[]) { this._primitives = primitives; } draw() { for (let primitive of this._primitives) { primitive.draw(); } } }

Gltf2Importerの書き換え

次に、GLtf2Importerも書き換えます。全てのPrimitiveについて繰り返し処理をする必要があるので、ループが1階層増えます。

// Gltf2Importor import Primitive, { VertexAttributeSet } from './Primitive.js'; // 変更 import { Gltf2Accessor, Gltf2BufferView, Gltf2, Gltf2Attribute } from './glTF2.js'; import Material from './Material.js'; import Context from './Context.js'; import Vector4 from './Vector4.js'; import Mesh from './Mesh.js'; // 追加 ... private _loadMesh(arrayBufferBin: ArrayBuffer, json: Gltf2, context: Context) { const meshes: Mesh[] = [] // 以下、変更 for (let meshJson of json.meshes) { const primitives: Primitive[] = []; for (let primitiveJson of meshJson.primitives) { const attributes = primitiveJson.attributes; let materialIndex = -1; if (primitiveJson.material != null) { materialIndex = primitiveJson.material; } const material = this._loadMaterial(json, materialIndex, context); 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 primitive = new Primitive(material, context, vertexData); primitives.push(primitive); } const mesh = new Mesh(primitives); meshes.push(mesh); } return meshes; }

これで設計変更が完了です。ライブラリのメッシュ仕様としては大変更ですが、内部実装はそのまま使い回せたので、思ったほど作業量はなかったですね。

これまで使ってきたサンプルモデルでは、残念ながら2つのメッシュとも1つ(0番目)のプリミティブしかないため、表示結果に差はありません。

image.png

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

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

11日目:トランスフォーム(姿勢の変換)について理解しよう