トランスフォームクラスを作る

前回、回転を表すQuaternionクラスを作りましたので、これで並行移動とスケールを表すVectorクラスとともに使用することで、姿勢を表すTransformクラスを作ることができます。

Transformクラスを作る

import { Matrix4 } from "./Matrix4.js"; import { Quaternion } from "./Quaternion.js"; import { Vector3 } from "./Vector3.js"; import { Vector4 } from "./Vector4.js"; export class Transform { private _position: Vector3; private _rotation: Quaternion; private _scale: Vector3; constructor(position: Vector3, rotation: Quaternion, scale: Vector3) { this._position = position; this._rotation = rotation; this._scale = scale; } isEqual(transform: Transform, delta: number = Number.EPSILON) { return ( this._position.isEqual(transform.getPosition(), delta) && this._rotation.isEqual(transform.getRotation(), delta) && this._scale.isEqual(transform.getScale(), delta) ); } toString() { return `position: ${this._position.toString()} rotation: ${this._rotation.toString()} scale: ${this._scale.toString()}`; } setPosition(value: Vector3) { this._position = value; } getPosition() { return this._position.clone(); } getRotation() { return this._rotation.clone(); } setRotation(value: Quaternion) { this._rotation = value; } setEulerAngles(value: Vector3) { this._rotation = Quaternion.fromEulerAngles(value); } getEulerAngles() { return this._rotation.toEulerAngles(); } getScale() { return this._scale.clone(); } setScale(value: Vector3) { this._scale = value; } setMatrix(value: Matrix4) { this._position = value.getTranslation(); this._rotation = value.getRotation(); this._scale = value.getScale(); } getMatrix() { const t = Matrix4.translation(this._position); const r = Matrix4.fromQuaternion(this._rotation); const s = Matrix4.scale(this._scale); return t.multiply(r).multiply(s); } transformVector(vec: Vector4) { return this.getMatrix().multiplyVector(vec); } clone(): Transform { return new Transform(this._position.clone(), this._rotation.clone(), this._scale.clone()); } }

getPositionなどのGetterメソッドに注目して欲しいのですが、内部のプロパティのクローン(複製)を作ってそれを返しています。

getPosition() { return this._position.clone(); }

一般的に、クラスが内部プロパティの実体をそのまま外部に渡してしまうのは設計的に良くないとされています。内部にあったプロパティが外部に渡り、そこで何をされるか予想がつかないためです。JavaScriptはシングルスレッド言語であり、またガベージコレクションもあるため、内部プロパティを外部に渡してもそこまで致命的な動作エラーになることは少ないですが、それでもSpinelでは設計の安全性をとって、getterではクローンを返すように徹底します。

これは設計が綺麗になる反面、実行時にcloneのコストが常に付きまとうことも意味しています。しかしSpinelでは可読性や設計の綺麗さを実行速度より優先したいと思います。

テストクラス

import { Quaternion } from "./Quaternion.js"; import { Vector3 } from "./Vector3.js"; import { Transform } from "./Transform.js"; import { Vector4 } from "./Vector4.js"; import { Matrix4 } from "./Matrix4.js"; test("Transform.matrix", () => { const t = new Transform( new Vector3(1, 2, 3), new Quaternion(0.5, 0.5, 0.5, 0.5), new Vector3(1, 2, 3) ); const m = t.getMatrix(); const t2 = t.clone(); t2.setMatrix(m); expect(t.isEqual(t2, 0.001)).toBe(true); }); test("Transform.transformVector", () => { const t = new Transform( new Vector3(1, 2, 3), new Quaternion(0.5, 0.5, 0.5, 0.5), new Vector3(1, 2, 3) ); const v = new Vector4(1, 2, 3, 1); const v2 = t.transformVector(v); const mt = Matrix4.translation(new Vector3(1, 2, 3)); const mr = Matrix4.fromQuaternion(new Quaternion(0.5, 0.5, 0.5, 0.5)); const ms = Matrix4.scale(new Vector3(1, 2, 3)); const m = mt.multiply(mr).multiply(ms); const v3 = m.multiplyVector(v); expect(v2.isEqual(v3, 0.001)).toBe(true); });

一部、既存クラスにメソッド追加

上記コードを動作させるために、各既存クラスに以下のメソッドを追加します。

Vector3.ts

... clone(): Vector3 { return new Vector3(this.x, this.y, this.z); } toString() { return `${this.x}, ${this.y}, ${this.z}`; }

Vector4.ts

... toString() { return `${this.x}, ${this.y}, ${this.z}, ${this.w}`; }

Quaternion.ts

... toString() { return `${this.x}, ${this.y}, ${this.z}, ${this.w}`; } static fromEulerAngles(vec: Vector3) { const sx = Math.sin(vec.x * 0.5); const cx = Math.cos(vec.x * 0.5); const sy = Math.sin(vec.y * 0.5); const cy = Math.cos(vec.y * 0.5); const sz = Math.sin(vec.z * 0.5); const cz = Math.cos(vec.z * 0.5); return new Quaternion( sx * cy * cz - cx * sy * sz, cx * sy * cz + sx * cy * sz, cx * cy * sz - sx * sy * cz, cx * cy * cz + sx * sy * sz ); }

Matrix4.ts

determinant() { return ( this.m30 * ( + this.m03 * this.m12 * this.m21 - this.m02 * this.m13 * this.m21 - this.m03 * this.m11 * this.m22 + this.m01 * this.m13 * this.m22 + this.m02 * this.m11 * this.m23 - this.m01 * this.m12 * this.m23 ) + this.m31 * ( + this.m00 * this.m12 * this.m23 - this.m00 * this.m13 * this.m22 + this.m03 * this.m10 * this.m22 - this.m02 * this.m10 * this.m23 + this.m02 * this.m13 * this.m20 - this.m03 * this.m12 * this.m20 ) + this.m32 * ( + this.m00 * this.m13 * this.m21 - this.m00 * this.m11 * this.m23 - this.m03 * this.m10 * this.m21 + this.m01 * this.m10 * this.m23 + this.m03 * this.m11 * this.m20 - this.m01 * this.m13 * this.m20 ) + this.m33 * ( - this.m02 * this.m11 * this.m20 - this.m00 * this.m12 * this.m21 + this.m00 * this.m11 * this.m22 + this.m02 * this.m10 * this.m21 - this.m01 * this.m10 * this.m22 + this.m01 * this.m12 * this.m20 ) ); } getTranslation() { return new Vector3(this.m03, this.m13, this.m23); } getRotation() { return Quaternion.fromMatrix4(this); } getScale() { return new Vector3( Math.hypot(this.m00, this.m10, this.m20), Math.hypot(this.m01, this.m11, this.m21), Math.hypot(this.m02, this.m12, this.m22) ); }

一部、既存クラスの不具合修正

実装してテストを書いていく中で、既存実装で計算を間違えていた部分がありました。すみません、私も学びながらやってますので(汗)
ただ、間違いに気づくためにも、やはりテストは重要であることが再認識できました。

Quaternion.tsのfromMatrix4関数を以下の内容に入れ替えてください。

// Quaternion.ts static fromMatrix4(mat: Matrix4) { let sx = Math.hypot(mat.m00, mat.m10, mat.m20); const sy = Math.hypot(mat.m01, mat.m11, mat.m21); const sz = Math.hypot(mat.m02, mat.m12, mat.m22); const det = mat.determinant(); if (det < 0) { sx = -sx; } const m = mat.clone(); const invSx = 1 / sx; const invSy = 1 / sy; const invSz = 1 / sz; m.m00 *= invSx; m.m10 *= invSx; m.m20 *= invSx; m.m01 *= invSy; m.m11 *= invSy; m.m21 *= invSy; m.m02 *= invSz; m.m12 *= invSz; m.m22 *= invSz; const trace = m.m00 + m.m11 + m.m22; if (trace > 0) { const S = 0.5 / Math.sqrt(trace + 1.0); const x = (m.m21 - m.m12) * S; const y = (m.m02 - m.m20) * S; const z = (m.m10 - m.m01) * S; const w = 0.25 / S; return new Quaternion(x, y, z, w); } else if (m.m00 > m.m11 && m.m00 > m.m22) { const S = Math.sqrt(1.0 + m.m00 - m.m11 - m.m22) * 2; const x = 0.25 * S; const y = (m.m01 + m.m10) / S; const z = (m.m02 + m.m20) / S; const w = (m.m21 - m.m12) / S; return new Quaternion(x, y, z, w); } else if (m.m11 > m.m22) { const S = Math.sqrt(1.0 + m.m11 - m.m00 - m.m22) * 2; const x = (m.m01 + m.m10) / S; const y = 0.25 * S; const z = (m.m12 + m.m21) / S; const w = (m.m02 - m.m20) / S; return new Quaternion(x, y, z, w); } else { const S = Math.sqrt(1.0 + m.m22 - m.m00 - m.m11) * 2; const x = (m.m02 + m.m20) / S; const y = (m.m12 + m.m21) / S; const z = 0.25 * S; const w = (m.m10 - m.m01) / S; return new Quaternion(x, y, z, w); } }

最後に

姿勢を表すTransformクラスを作成できました。だいぶ数学クラスが充実してきましたね。

image.png

指示の通りに作業してもうまくいかない場合は、GithubのGit履歴を参照してください。

14日目:クォータニオンクラスを作る

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