Cameraコンポーネントを作る

Cameraコンポーネントを作ります。

import { CameraType } from "../../definitions.js"; import { Matrix4 } from "../../math/Matrix4.js"; import { Vector3 } from "../../math/Vector3.js"; import { Component } from "../Component.js"; import { Entity } from "../Entity.js"; export class CameraComponent extends Component { private _projectionMatrix: Matrix4; private _type: CameraType; // common camera properties private _near = 0.1; private _far = 1000; // orthographic camera properties private _left = -1; private _right = 1; private _bottom = -1; private _top = 1; // perspective camera properties private _fovy = 45; private _aspect = 1; static activeCamera?: CameraComponent; private constructor(entity: Entity, type: CameraType) { super(entity); this._type = type; this._projectionMatrix = Matrix4.identity(); this.calculateProjectionMatrix(); } getProjectionMatrix() { return this._projectionMatrix.clone(); } getViewMatrix() { const eye = Vector3.zero(); const center = new Vector3(0, 0, -1); const up = new Vector3(0, 1, 0); const lookAtMatrix = Matrix4.lookAt(eye, center, up); return lookAtMatrix.multiply(this.entity.getSceneGraph().getMatrix().invert()); } calculateProjectionMatrix() { if (this._type === CameraType.Perspective) { this._projectionMatrix = Matrix4.perspective(this._fovy, this._aspect, this._near, this._far); } else if (this._type === CameraType.Orthographic) { this._projectionMatrix = Matrix4.orthographic(this._left, this._right, this._bottom, this._top, this._near, this._far); } else { throw new Error("Unknown camera type"); } } get fovy() { return this._fovy; } set fovy(fovy: number) { this._fovy = fovy; this.calculateProjectionMatrix(); } get near() { return this._near; } set near(near: number) { this._near = near; this.calculateProjectionMatrix(); } get far() { return this._far; } set far(far: number) { this._far = far; this.calculateProjectionMatrix(); } get aspect() { return this._aspect; } set aspect(aspect: number) { this._aspect = aspect; this.calculateProjectionMatrix(); } get left() { return this._left; } set left(left: number) { this._left = left; this.calculateProjectionMatrix(); } get right() { return this._right; } set right(right: number) { this._right = right; this.calculateProjectionMatrix(); } get bottom() { return this._bottom; } set bottom(bottom: number) { this._bottom = bottom; this.calculateProjectionMatrix(); } get top() { return this._top; } set top(top: number) { this._top = top; this.calculateProjectionMatrix(); } set xmag(xmag: number) { this._left = -xmag; this._right = xmag; this.calculateProjectionMatrix(); } set ymag(ymag: number) { this._bottom = -ymag; this._top = ymag; this.calculateProjectionMatrix(); } get cameraType() { return this._type; } set cameraType(cameraType: CameraType) { this._type = cameraType; this.calculateProjectionMatrix(); } static _create(entity: Entity, type: CameraType) { return new CameraComponent(entity, type); } }

プロジェクション行列を作るための処理は、Matrix4クラスに以下のように移譲しています。

// Matrix4.ts static perspective(fovy: number, aspect: number, near: number, far: number) { const f = 1 / Math.tan(fovy / 2); const nf = 1 / (near - far); if (far === Infinity) { return new Matrix4( f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, -1, -2 * near, 0, 0, -1, 0 ); } else { return new Matrix4( f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, (2 * far * near) * nf, 0, 0, -1, 0 ); } } static orthographic(left: number, right: number, bottom: number, top: number, near: number, far: number) { const rl = 1 / (right - left); const tb = 1 / (top - bottom); const fn = 1 / (far - near); return new Matrix4( 2 * rl, 0, 0, -(right + left) * rl, 0, 2 * tb, 0, -(top + bottom) * tb, 0, 0, -2 * fn, -(far + near) * fn, 0, 0, 0, 1 ); } static lookAt(eye: Vector3, center: Vector3, up: Vector3) { const z = eye.subtract(center).normalize(); const x = up.cross(z).normalize(); const y = z.cross(x).normalize(); return new Matrix4( x.x, x.y, x.z, -x.dot(eye), y.x, y.y, y.z, -y.dot(eye), z.x, z.y, z.z, -z.dot(eye), 0, 0, 0, 1 ); }

また、上記の関数を作成する上で必要となった、以下のメソッドをVector3に追加しています。

// Vector3.ts add(vec: Vector3) { return new Vector3(this.x + vec.x, this.y + vec.y, this.z + vec.z); } subtract(vec: Vector3) { return new Vector3(this.x - vec.x, this.y - vec.y, this.z - vec.z); } dot(vec: Vector3) { return this.x * vec.x + this.y * vec.y + this.z * vec.z; } length() { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } cross(vec: Vector3) { return new Vector3( this.y * vec.z - this.z * vec.y, this.z * vec.x - this.x * vec.z, this.x * vec.y - this.y * vec.x ); } normalize() { const length = this.length(); return new Vector3(this.x / length, this.y / length, this.z / length); }

また、Gltf2Importerクラスで、glTFファイル中のカメラ情報を読み込むようにコードを追加します。

// Gltf2Importer.ts private static _loadNode(json: Gltf2, meshes: Mesh[]) { ... // camera if (node.camera != null) { const cameraJson = json.cameras[node.camera]; cameraJson.type if (cameraJson.type === 'perspective') { const cameraComponent = entity.addCamera(CameraType.Perspective); cameraComponent.fovy = cameraJson.perspective!.yfov; cameraComponent.aspect = cameraJson.perspective!.aspectRatio ?? 1; cameraComponent.near = cameraJson.perspective!.znear; cameraComponent.far = cameraJson.perspective!.zfar ?? Infinity; if (CameraComponent.activeCamera == null) { CameraComponent.activeCamera = cameraComponent; } } else { const cameraComponent = entity.addCamera(CameraType.Orthographic); cameraComponent.xmag = cameraJson.orthographic!.xmag; cameraComponent.ymag = cameraJson.orthographic!.ymag; cameraComponent.near = cameraJson.orthographic!.znear; cameraComponent.far = cameraJson.orthographic!.zfar; if (CameraComponent.activeCamera == null) { CameraComponent.activeCamera = cameraComponent; } } }

頂点シェーダーでは、頂点データに対してエンティティのワールド行列、ビュー行列、プロジェクション行列を掛け算します。

ワールド行列(u_worldMatrix)にはmeshEntity.getSceneGraph().getMatrix()で取得できる行列を、
ビュー行列(u_viewMatrix)にはcameraEntity.getCamera().getViewMatrix()で取得できる行列を、
プロジェクション行列(u_projectionMatrix)にはcameraEntity.getCamera().getProjectionMatrix()で取得できる行列を設定しています。

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; uniform mat4 u_viewMatrix; uniform mat4 u_projectionMatrix; void main(void) { gl_Position = u_projectionMatrix * u_viewMatrix * u_worldMatrix * vec4(a_position, 1.0); v_color = a_color; } `;

さて、ここでサンプルプログラム内で別のglTFモデル「Buggy」を表示するようにしてみましょう。

import Spinel from '../dist/index.js' async function main() { const canvas = document.getElementById('world') as HTMLCanvasElement; const context = new Spinel.Context(canvas); const entities = await Spinel.Gltf2Importer.import('../assets/gltf/glTF-Sample-Models/2.0/Buggy/glTF/Buggy.gltf', context); const gl = context.gl; gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); const meshEntities = Spinel.Entity.getAllMeshEntities(); const draw = () => { gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); for (const meshEntity of meshEntities) { meshEntity.getMesh()!.draw(); } requestAnimationFrame(draw); }; draw(); }

image.png

あれ? 各メッシュの位置関係はそれっぽいですが、表示がなんか変ですね。
これ、私のミスなのですが、コードを調べてみたら、glTFからインデックス情報を取得するコードを書くのを忘れていました。
インデックス描画のためのコードは書いていたのに、読み込みを忘れるなんてドジですね。すみません。

早速追加しましょう。以下のgetIndices関数を追加します。

// Gltf2Importer.ts private static getIndices(json: Gltf2, indicesIndex: number, arrayBufferBin: ArrayBuffer) { const accessor = json.accessors[indicesIndex] 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 Uint16Array | Uint32Array; return typedArray; }

この関数を呼び出すのは以下の箇所です。

image.png

さて、これでようやく複数Meshから構成されたglTFモデルを適切に表示することができました。ここまで長かったですね。

image.png

ここまでの作業は、リポジトリのこちら から参照できます。

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

20日目:リファクタリングする