SceneGraphコンポーネントを作る

SceneGraphコンポーネントは、Entityの親子関係を構築するためのコンポーネントです。
このコンポーネントは親から遡って積算した姿勢情報を持っています(計算します)。

このSceneGraphコンポーネントからとれる姿勢情報を「World空間での姿勢」と呼びます。
また、Transformコンポーネントからとれる姿勢情報は「Local空間での姿勢」と呼びます

例えば、3D空間上に立方体エンティティと円錐エンティティがあるとします。立方体エンティティが親で円錐エンティティがその子です。
円錐エンティティは原点から右に5程度移動しているものとします。

image.png

コードで示すと次のような感じです。

const cubeEntity = Entity.create(); const coneEntity = Entity.create(); cubeEntity.getSceneGraph().addChild(coneEntity.getSceneGraph()); // 円錐エンティティを立方体エンティティの子供にする coneEntity.getTransform().setLocalPosition(new Vector3(5, 0, 0)); // 円錐エンティティは(5, 0, 0)の位置にいる

さて、ここで親である立方体エンティティが上に5動いたらどうなるでしょうか。3Dソフトを扱ったことのある人ならわかりますよね。親に引きづられて子供の円錐エンティティも同じように動きます。

結果、円錐は右に5,上に5移動した場所に位置するようになります。

image.png

これを自動で計算してくれるのがSceneGraphコンポーネントです。

cubeEntity.getTransform().setLocalPosition(new Vector3(0, 5, 0)); // 親である立方体エンティティを(0, 5, 0)の位置に移動 const worldPosition = coneEntity.getSceneGraph().getPosition(); // World空間で言うと立方体Entityは console.log(worldPosition); // (5, 5, 0)の位置にいる

SceneGraphComponent.ts

import { Vector3 } from "../../math/Vector3.js"; import { Matrix4 } from "../../math/Matrix4.js"; import { Quaternion } from "../../math/Quaternion.js"; import { Component } from "../Component.js"; import { Entity } from "../Entity.js"; import { Transform } from "../../math/Transform.js"; export class SceneGraphComponent extends Component { private _children: SceneGraphComponent[]; private _parent?: SceneGraphComponent; private constructor(entity: Entity) { super(entity); this._children = []; } get children() { return this._children; } addChild(child: SceneGraphComponent) { child._parent = this; this._children.push(child); } removeChild(child: SceneGraphComponent) { const index = this._children.indexOf(child); if (index >= 0) { this._children.splice(index, 1); child._parent = undefined; } } get parent(): SceneGraphComponent | undefined { return this._parent; } getTransform(): Transform { const localTransform = this.entity.getTransform().getLocalTransform(); if (this.parent === undefined) { return localTransform; } else { const parentWorldTransform = this.parent.getTransform(); return parentWorldTransform.multiply(localTransform); } } setTransform(transform: Transform) { if (this.parent === undefined) { this.entity.getTransform().setLocalTransform(transform); } else { const parentWorldTransform = this.parent.getTransform(); const invParentWorldTransform = parentWorldTransform.invert(); this.entity.getTransform().setLocalTransform(invParentWorldTransform.multiply(transform)); } } getMatrix(): Matrix4 { const localMatrix = this.entity.getTransform().getLocalMatrix(); if (this.parent === undefined) { return localMatrix; } else { const parentWorldMatrix = this.parent.getMatrix(); return parentWorldMatrix.multiply(localMatrix); } } setMatrix(mat: Matrix4) { if (this.parent === undefined) { this.entity.getTransform().setLocalMatrix(mat); } else { const parentWorldMatrix = this.parent.getMatrix(); const invParentWorldMatrix = parentWorldMatrix.invert(); this.entity.getTransform().setLocalMatrix(invParentWorldMatrix.multiply(mat)); } } setPosition(vec: Vector3) { if (this.parent === undefined) { this.entity.getTransform().setLocalPosition(vec); } else { const invertMat = this.parent.entity.getSceneGraph().getMatrix().invert(); this.entity.getTransform().setLocalPosition(invertMat.multiplyVector(vec).toVector3()); } } getPosition(): Vector3 { return this.getMatrix().getTranslation(); } getRotation(): Quaternion { const parent = this.parent; if (parent !== undefined) { return parent.getRotation().multiply(this.entity.getTransform().getLocalRotation()); } return this.entity.getTransform().getLocalRotation(); } setRotation(quat: Quaternion) { if (this.parent === undefined) { this.entity.getTransform().setLocalRotation(quat); } else { const quatInner = this.parent.entity.getSceneGraph().getRotation(); const invQuat = quatInner.invert(); this.entity.getTransform().setLocalRotation(quat.multiply(invQuat)); } } getEulerAngles(): Vector3 { return this.getRotation().toEulerAngles(); } setEulerAngles(vec: Vector3) { this.setRotation(Quaternion.fromEulerAngles(vec)); } getScale(): Vector3 { return this.getMatrix().getScale(); } setScale(vec: Vector3) { if (this.parent === undefined) { this.entity.getTransform().setLocalScale(vec); } else { const mat = this.parent.entity.getSceneGraph().getMatrix(); mat.m03 = 0; mat.m13 = 0; mat.m23 = 0; const invMat = mat.invert(); this.entity.getTransform().setLocalScale(invMat.multiplyVector(vec).toVector3()); } } /** * @private * @param entity * @returns a SceneGraph component */ static _create(entity: Entity) { return new SceneGraphComponent(entity); } }

EntityクラスにSceneGraphコンポーネントを持たせます。SceneGraphコンポーネントもEntityの3D空間上での位置の計算に重要なので、Entityの必須要素です。

import { SceneGraphComponent } from "./components/SceneGraphComponent.js"; import { TransformComponent } from "./components/TransformComponent.js"; export class Entity { private _name: string; private _id: number; private static _entities: Entity[] = []; private _transform: TransformComponent; private _sceneGraph: SceneGraphComponent; // 追記 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; } getTransform(): TransformComponent { return this._transform; } getSceneGraph(): SceneGraphComponent { // 追記 return this._sceneGraph; } 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 reset() { this._entities = []; } }

なお、各数学クラスにも追加の定義があります。

// Vector4.ts toVector3() { return new Vector3(this.x, this.y, this.z); } // Vector3.ts get w() { return 1; } // Matrix4.ts invert(): Matrix4 { const n00 = this.m00 * this.m11 - this.m01 * this.m10; const n01 = this.m00 * this.m12 - this.m02 * this.m10; const n02 = this.m00 * this.m13 - this.m03 * this.m10; const n03 = this.m01 * this.m12 - this.m02 * this.m11; const n04 = this.m01 * this.m13 - this.m03 * this.m11; const n05 = this.m02 * this.m13 - this.m03 * this.m12; const n06 = this.m20 * this.m31 - this.m21 * this.m30; const n07 = this.m20 * this.m32 - this.m22 * this.m30; const n08 = this.m20 * this.m33 - this.m23 * this.m30; const n09 = this.m21 * this.m32 - this.m22 * this.m31; const n10 = this.m21 * this.m33 - this.m23 * this.m31; const n11 = this.m22 * this.m33 - this.m23 * this.m32; const det = n00 * n11 - n01 * n10 + n02 * n09 + n03 * n08 - n04 * n07 + n05 * n06; if (det === 0) { console.error('the determinant is 0!'); } const m00 = (this.m11 * n11 - this.m12 * n10 + this.m13 * n09) / det; const m01 = (this.m02 * n10 - this.m01 * n11 - this.m03 * n09) / det; const m02 = (this.m31 * n05 - this.m32 * n04 + this.m33 * n03) / det; const m03 = (this.m22 * n04 - this.m21 * n05 - this.m23 * n03) / det; const m10 = (this.m12 * n08 - this.m10 * n11 - this.m13 * n07) / det; const m11 = (this.m00 * n11 - this.m02 * n08 + this.m03 * n07) / det; const m12 = (this.m32 * n02 - this.m30 * n05 - this.m33 * n01) / det; const m13 = (this.m20 * n05 - this.m22 * n02 + this.m23 * n01) / det; const m20 = (this.m10 * n10 - this.m11 * n08 + this.m13 * n06) / det; const m21 = (this.m01 * n08 - this.m00 * n10 - this.m03 * n06) / det; const m22 = (this.m30 * n04 - this.m31 * n02 + this.m33 * n00) / det; const m23 = (this.m21 * n02 - this.m20 * n04 - this.m23 * n00) / det; const m30 = (this.m11 * n07 - this.m10 * n09 - this.m12 * n06) / det; const m31 = (this.m00 * n09 - this.m01 * n07 + this.m02 * n06) / det; const m32 = (this.m31 * n01 - this.m30 * n03 - this.m32 * n00) / det; const m33 = (this.m20 * n03 - this.m21 * n01 + this.m22 * n00) / det; return new Matrix4( m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33 ); } // Quaternion.ts length() { return Math.hypot(this.x, this.y, this.z, this.w); } invert(): Quaternion { const norm = this.length(); if (norm === 0.0) { return new Quaternion(0, 0, 0, 0); } const x = -this.x / norm; const y = -this.y / norm; const z = -this.z / norm; const w = this.w / norm; return new Quaternion(x, y, z, w); }

また、TransformComponentクラスのgetTransformとsetTransformをそれぞれgetLocalTransformとsetLocalTransformに名前を変更しました。
image.png

SceneGraphコンポーネントのテスト

SceneGraphコンポーネントのテストコードです。よく見ると、何をやっているのか分かるのではないでしょうか。

import { Matrix4 } from "../../math/Matrix4.js"; import { Quaternion } from "../../math/Quaternion.js"; import { Transform } from "../../math/Transform.js"; import { Vector3 } from "../../math/Vector3.js"; import { Entity } from "../Entity.js"; test("SceneGraphComponent Position", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalPosition(new Vector3(10, 0, 0)); child.getTransform().setLocalPosition(new Vector3(0, 10, 0)); const childPos = child.getSceneGraph().getPosition(); expect(childPos.isEqual(new Vector3(10, 10, 0), 0.001)).toBe(true); }); test("SceneGraphComponent Rotation", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalEulerAngles(new Vector3(0, Math.PI / 2, 0)); child.getTransform().setLocalPosition(new Vector3(10, 0, 0)); const childPos = child.getSceneGraph().getPosition(); expect(childPos.isEqual(new Vector3(0, 0, -10), 0.001)).toBe(true); }); test("SceneGraphComponent Scale", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalScale(new Vector3(2, 2, 2)); child.getTransform().setLocalPosition(new Vector3(10, 0, 0)); const childPos = child.getSceneGraph().getPosition(); expect(childPos.isEqual(new Vector3(20, 0, 0), 0.001)).toBe(true); }); test("SceneGraphComponent Matrix", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalMatrix(Matrix4.translation(new Vector3(10, 0, 0))); child.getTransform().setLocalMatrix(Matrix4.translation(new Vector3(0, 10, 0))); expect(child.getSceneGraph().getMatrix().isEqual(new Matrix4( 1, 0, 0, 10, 0, 1, 0, 10, 0, 0, 1, 0, 0, 0, 0, 1 ), 0.001)).toBe(true); }); test("SceneGraphComponent Transform", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); const t = new Transform(new Vector3(10, 0, 0), Quaternion.identity(), Vector3.one()); const t2 = new Transform(new Vector3(0, 10, 0), Quaternion.identity(), Vector3.one()); parent.getTransform().setLocalTransform(t); child.getTransform().setLocalTransform(t2); const childPos = child.getSceneGraph().getTransform().getPosition(); expect(childPos.isEqual(new Vector3(10, 10, 0), 0.001)).toBe(true); }); test("SceneGraphComponent Position 2", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalPosition(new Vector3(10, 0, 0)); child.getTransform().setLocalPosition(new Vector3(10, 0, 0)); expect(child.getSceneGraph().getPosition().isEqual(new Vector3(20, 0, 0), 0.001)).toBe(true); child.getSceneGraph().setPosition(new Vector3(10, 0, 0)); expect(child.getSceneGraph().getPosition().isEqual(new Vector3(10, 0, 0), 0.001)).toBe(true); expect(parent.getTransform().getLocalPosition().isEqual(new Vector3(10, 0, 0), 0.001)).toBe(true); expect(child.getTransform().getLocalPosition().isEqual(new Vector3(0, 0, 0), 0.001)).toBe(true); }); test("SceneGraphComponent Rotation 2", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalEulerAngles(new Vector3(0, Math.PI / 4, 0)); child.getTransform().setLocalEulerAngles(new Vector3(0, Math.PI / 4, 0)); expect(child.getSceneGraph().getEulerAngles().isEqual(new Vector3(0, Math.PI / 2, 0), 0.001)).toBe(true); child.getSceneGraph().setEulerAngles(new Vector3(0, Math.PI / 4, 0)); expect(parent.getTransform().getLocalEulerAngles().isEqual(new Vector3(0, Math.PI / 4, 0), 0.001)).toBe(true); expect(child.getTransform().getLocalEulerAngles().isEqual(new Vector3(0, 0, 0), 0.001)).toBe(true); }); test("SceneGraphComponent Scale 2", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalScale(new Vector3(2, 2, 2)); child.getTransform().setLocalScale(new Vector3(2, 2, 2)); expect(child.getSceneGraph().getScale().isEqual(new Vector3(4, 4, 4), 0.001)).toBe(true); child.getSceneGraph().setScale(new Vector3(2, 2, 2)); expect(parent.getTransform().getLocalScale().isEqual(new Vector3(2, 2, 2), 0.001)).toBe(true); expect(child.getTransform().getLocalScale().isEqual(new Vector3(1, 1, 1), 0.001)).toBe(true); }); test("SceneGraphComponent Matrix 2", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); parent.getTransform().setLocalMatrix(Matrix4.translation(new Vector3(10, 0, 0))); child.getTransform().setLocalMatrix(Matrix4.translation(new Vector3(10, 0, 0))); expect(child.getSceneGraph().getMatrix().isEqual(new Matrix4( 1, 0, 0, 20, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ), 0.001)).toBe(true); child.getSceneGraph().setMatrix(Matrix4.translation(new Vector3(10, 0, 0))); expect(parent.getTransform().getLocalMatrix().isEqual(new Matrix4( 1, 0, 0, 10, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ), 0.001)).toBe(true); expect(child.getTransform().getLocalMatrix().isEqual(new Matrix4( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ), 0.001)).toBe(true); }); test("SceneGraphComponent Transform", () => { const parent = Entity.create(); const child = Entity.create(); parent.getSceneGraph().addChild(child.getSceneGraph()); const t = new Transform(new Vector3(10, 0, 0), Quaternion.identity(), Vector3.one()); const t2 = new Transform(new Vector3(10, 0, 0), Quaternion.identity(), Vector3.one()); parent.getTransform().setLocalTransform(t); child.getTransform().setLocalTransform(t2); const childPos = child.getSceneGraph().getTransform().getPosition(); expect(childPos.isEqual(new Vector3(20, 0, 0), 0.001)).toBe(true); child.getSceneGraph().setTransform(new Transform(new Vector3(10, 0, 0), Quaternion.identity(), Vector3.one())); expect(parent.getTransform().getLocalTransform().isEqual(new Transform(new Vector3(10, 0, 0), Quaternion.identity(), Vector3.one()), 0.001)).toBe(true); expect(child.getTransform().getLocalTransform().isEqual(new Transform(new Vector3(0, 0, 0), Quaternion.identity(), Vector3.one()), 0.001)).toBe(true); });

各階層のindex.tsを作成・更新する

そういえば、各ソース階層のindex.tsの更新を忘れていました。更新どころか、index.tsの作成すら忘れてしまっていた階層がありますね(汗)
更新し直しておきましょう。

// src/ec/components/index.ts export * from "./SceneGraphComponent.js"; export * from "./TransformComponent.js";
// src/ec/index.ts export * from "./components/index.js"; // export * from "./Component.js"; // this is private export * from "./Entity.js";
// src/math/index.ts export * from "./Vector3.js"; export * from "./Vector4.js"; export * from "./Matrix4.js"; export * from "./Quaternion.js"; export * from "./Transform.js";

最後に

SceneGraphコンポーネントまで実装が完了しました。
これで親子構造を持ったglTFモデルを適切に表示するための足がかりができました。

16日目:エンティティとTransformコンポーネントを作る

18日目:Meshコンポーネントを作り、glTFから位置情報も読み出す