Meshコンポーネントを作る

さて、これまではメッシュの描画をMeshクラスのdraw()メソッドを直接呼んで行っていましたが、もう少し汎用性のある形にしましょう。
新たにMeshコンポーネントを作成し、Entityに積ませることでEntityにメッシュ描画の機能を持たせます。

ちなみに、この回あたりから、ソースコードの整理や細かい微修正などについてはいちいち解説しないことにします。
ソースコードの記述量も増えてきて、それらを全て解説していたら膨大な量になってかえってわかりづらくなりますからね。
詳細は、Git履歴の方をご覧ください。

MeshComponent.ts

import { Mesh } from "../../geometry/Mesh.js"; import { Component } from "../Component.js"; import { Entity } from "../Entity.js"; export class MeshComponent extends Component { private _mesh: Mesh; private constructor(entity: Entity, mesh: Mesh) { super(entity); this._mesh = mesh; } draw() { this._mesh.draw(this.entity); } /** * @private * @param mesh * @returns a Mesh component */ static _create(entity: Entity, mesh: Mesh) { return new MeshComponent(entity, mesh); } }

EntityクラスにMesh Compoentを扱うための記述を加えます。

import { MeshComponent } from "./components/MeshComponent.js"; // 追記 import { SceneGraphComponent } from "./components/SceneGraphComponent.js"; import { TransformComponent } from "./components/TransformComponent.js"; import type { Mesh } from "../geometry/Mesh.js"; // 追記 export class Entity { private _name: string; private _id: number; private static _entities: Entity[] = []; private _transform: TransformComponent; private _sceneGraph: SceneGraphComponent; private _mesh?: MeshComponent; // 追記 private constructor(id: number) { this._id = id; this._name = "Entity_" + id; this._transform = TransformComponent._create(this); this._sceneGraph = SceneGraphComponent._create(this); } getId() { return this._id; } getName() { return this._name.concat(); } setName(name: string) { this._name = name; } addMesh(mesh: Mesh) { // 追記 this._mesh = MeshComponent._create(this, mesh); } getTransform(): TransformComponent { return this._transform; } getSceneGraph(): SceneGraphComponent { return this._sceneGraph; } getMesh(): MeshComponent | undefined { return this._mesh; } static create(): Entity { const entity = new Entity(this._entities.length); this._entities.push(entity); return entity; } static get(id: number): Entity | undefined { if (id < 0 || id >= this._entities.length) { return undefined; } return this._entities[id]; } static getAllMeshEntities(): Entity[] { // 追記 return this._entities.filter(entity => entity.getMesh() !== undefined); } static reset() { this._entities = []; } }

Gltf2Importer.tsを改修

さらに、Gltf2Importer.tsも改修します。新たに_loadNodeメソッドを追加し、そこでglTFの各ノード情報を処理します。
ノードには姿勢情報とメッシュを持っているかの情報があり、それぞれの情報をEntityのTransformコンポーネントとMeshコンポーネントに適切に設定します。

... static async import(uri: string, context: Context): Promise<Entity[]> { 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); const entities = this._loadNode(json, meshes); return entities; } ... private static _loadNode(json: Gltf2, meshes: Mesh[]) { const entities: Entity[] = []; for (let node of json.nodes) { const entity = Entity.create(); entities.push(entity); // transform if (node.matrix != null) { const v = node.matrix; entity.getTransform().setLocalMatrix(new Matrix4( v[0], v[4], v[8], v[12], v[1], v[5], v[9], v[13], v[2], v[6], v[10], v[14], v[3], v[7], v[11], v[15] )); } else { if (node.translation != null) { const v = node.translation; entity.getTransform().setLocalPosition(new Vector3(v[0], v[1], v[2])); } if (node.rotation != null) { const v = node.rotation; entity.getTransform().setLocalRotation(new Quaternion(v[0], v[1], v[2], v[3])); } if (node.scale != null) { const v = node.scale; entity.getTransform().setLocalScale(new Vector3(v[0], v[1], v[2])); } } // mesh if (node.mesh != null) { const mesh = meshes[node.mesh]; entity.addMesh(mesh); } } // make hierarchy for (let nodeIndex = 0; nodeIndex < json.nodes.length; nodeIndex++) { const node = json.nodes[nodeIndex]; if (node.children != null) { const parent = entities[nodeIndex]; for (let childIndex of node.children) { const child = entities[childIndex]; parent.getSceneGraph().addChild(child.getSceneGraph()); } } } ...

さらに、シェーダーにSceneGraphコンポーネントが持つワールド行列を保持するu_worldMatrixというUniform変数を導入します。

// Gltf2Importer.ts export class Gltf2Importer { private static readonly vertexShaderStr = ` precision highp float; attribute vec3 a_position; attribute vec4 a_color; varying vec4 v_color; uniform mat4 u_worldMatrix; // 追記 void main(void) { gl_Position = u_worldMatrix * vec4(a_position, 1.0); // 変更 v_color = a_color; } `;

glTFから位置情報も読み出す

さて、だいぶ前にTransformコンポーネントとSceneGraphコンポーネントを作成済みなので、ここでglTFからオブジェクトの位置情報を読むようにしましょう。これで、glTFモデルを読む時に各種Meshの位置関係が適切に考慮されて表示されるはずです。

Gltf2Importerクラスに以下の関数を加えます。

private static _loadNode(json: Gltf2, meshes: Mesh[]) { const entities: Entity[] = []; for (let node of json.nodes) { const entity = Entity.create(); entities.push(entity); // transform if (node.matrix != null) { const v = node.matrix; entity.getTransform().setLocalMatrix(new Matrix4( v[0], v[4], v[8], v[12], v[1], v[5], v[9], v[13], v[2], v[6], v[10], v[14], v[3], v[7], v[11], v[15] )); } else { if (node.translation != null) { const v = node.translation; entity.getTransform().setLocalPosition(new Vector3(v[0], v[1], v[2])); } if (node.rotation != null) { const v = node.rotation; entity.getTransform().setLocalRotation(new Quaternion(v[0], v[1], v[2], v[3])); } if (node.scale != null) { const v = node.scale; entity.getTransform().setLocalScale(new Vector3(v[0], v[1], v[2])); } } // mesh if (node.mesh != null) { const mesh = meshes[node.mesh]; entity.addMesh(mesh); } } // make hierarchy for (let nodeIndex = 0; nodeIndex < json.nodes.length; nodeIndex++) { const node = json.nodes[nodeIndex]; if (node.children != null) { const parent = entities[nodeIndex]; for (let childIndex of node.children) { const child = entities[childIndex]; parent.getSceneGraph().addChild(child.getSceneGraph()); } } } return entities; }

この関数を呼び出します。これでGltf2Importerが返すのはMeshの配列ではなくEntityの配列になりました。

image.png

これを利用するサンプルプログラム側は以下のように書き換えます。

image.png

glTF-Sample-ModelsをGit Submodule導入

そろそろ別のglTFモデルを表示したいのですが、ちょうど良いリポジトリがあります。KhronosグループのglTF-Sample-Models です。
これをgit submoduleとして追加しておきました。詳しくはGit履歴 をご覧ください。

最後に

徐々にライブラリらしくなってきました。ここまでの作業はリポジトリのこちら からご覧いただけます。

そろそろ別のglTFモデルを表示したいのですが、残念ながらまだカメラ情報を扱うクラスを作っていないので、glTFモデルをうまく視界に収められない可能性が高いです。

そこで、次はCameraコンポーネントクラスを作りましょう。やれやれ、glTFモデルをいっぱしに表示できるようになるまで、やることは多いですね。

17日目:SceneGraphコンポーネントを作る

19日目:Cameraコンポーネントを作る