AABBを実装する

前回で、カメラを動かすことはできるようになりましたが、カメラの位置を対象物からある程度離すように手動で設定しなければいけませんでした。
対象物がカメラの視界に収まるように、ちょうど良い距離にカメラを自動配置してくれる機能がカメラコントローラにあれば嬉しいですよね。
その機能を実装してみましょう。
カメラをどれだけ対象物から引き離せば良いか、その距離の計算にあたって、対象物の大きさを求める必要があります。

AABBを実装しましょう。AABBは、座標軸に並行な、対象物を囲むバウンディングボックスのことです。

AABB.ts

import { Matrix4 } from "./Matrix4.js"; import { Vector3 } from "./Vector3.js"; export class AABB { private _min: Vector3 = new Vector3(Infinity, Infinity, Infinity); private _max: Vector3 = new Vector3(-Infinity, -Infinity, -Infinity); constructor() {} addPoint(point: Vector3) { this._min = new Vector3( Math.min(this._min.x, point.x), Math.min(this._min.y, point.y), Math.min(this._min.z, point.z) ); this._max = new Vector3( Math.max(this._max.x, point.x), Math.max(this._max.y, point.y), Math.max(this._max.z, point.z) ); } setMinAndMax(min: Vector3, max: Vector3) { this._min = min.clone(); this._max = max.clone(); } clone() { const aabb = new AABB(); aabb.setMinAndMax(this._min, this._max); return aabb; } merge(aabb: AABB) { if (aabb.isVanilla()) { return; } this.addPoint(aabb.getMin()); this.addPoint(aabb.getMax()); } transformByMatrix(matrix: Matrix4) { if (this.isVanilla()) { return this; } const newAABB = new AABB(); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._min.x, this._min.y, this._min.z)).toVector3()); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._min.x, this._min.y, this._max.z)).toVector3()); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._min.x, this._max.y, this._min.z)).toVector3()); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._max.x, this._min.y, this._min.z)).toVector3()); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._min.x, this._max.y, this._max.z)).toVector3()); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._max.x, this._min.y, this._max.z)).toVector3()); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._max.x, this._max.y, this._min.z)).toVector3()); newAABB.addPoint(matrix.multiplyVector(new Vector3(this._max.x, this._max.y, this._max.z)).toVector3()); return newAABB; } isVanilla() { return this._min.x === Infinity; } getMin(): Vector3 { return this._min.clone(); } getMax(): Vector3 { return this._max.clone(); } getSizeX(): number { return this._max.x - this._min.x; } getSizeY(): number { return this._max.y - this._min.y; } getSizeZ(): number { return this._max.z - this._min.z; } getCenterPoint(): Vector3 { return this._min.add(this._max).divide(2); } getLengthCornerToCorner(): number { return this._max.lengthTo(this._min); } }

AABBにはAABBの端から端までの距離を返すgetLengthCornerToCornerというメソッドがあり、これが対象物の大きさとして使えます。

PrimitiveクラスにAABBを持たせる

PrimitiveクラスにAABBを持たせます。また、コンストラクト時に頂点データを渡しますので、その頂点データをもとにAABBを構築します。最初からAABBが渡される場合は、そのAABBを採用します。これは、通常はglTFデータがAABBも提供してくれるからです。

export type VertexAttributeSet = { position: Float32Array, color?: Float32Array, normal?: Float32Array, texcoord?: Float32Array, indices?: Uint16Array | Uint32Array, mode: PrimitiveMode, aabb?: AABB // 追加 } export class Primitive { private _positionBuffer: WebGLBuffer; private _colorBuffer?: WebGLBuffer; private _indexBuffer?: WebGLBuffer; private _indexType: 5123 | 5125 = 5123; // gl.UNSIGNED_SHORT | gl.UNSIGNED_INT private _mode: PrimitiveMode = PrimitiveMode.Triangles; private _vertexNumber = 0; private _indexNumber = 0; private _material: Material; private static readonly _positionComponentNumber = 3; private static readonly _colorComponentNumber = 4; private _localAabb = new AABB(); // 追加 constructor(material: Material, vertexData: VertexAttributeSet) { this._material = material; this._vertexNumber = vertexData.position.length / Primitive._positionComponentNumber; this._positionBuffer = this._setupVertexBuffer(vertexData.position)!; this._colorBuffer = this._setupVertexBuffer(vertexData.color); if (vertexData.indices != null) { this._indexBuffer = this._setupIndexBuffer(vertexData.indices); this._indexNumber = vertexData.indices.length; } this.setupAABB(vertexData); // 追加 } private setupAABB(vertexData: VertexAttributeSet) { // 追加 this._mode = vertexData.mode; if (vertexData.aabb != null) { this._localAabb = vertexData.aabb.clone(); } else { for (let i = 0; i < this._vertexNumber; i++) { const point = new Vector3(vertexData.position[i * 3], vertexData.position[i * 3 + 1], vertexData.position[i * 3 + 2]); this._localAabb.addPoint(point); } } } ...

MeshにAABBを持たせる

MeshにもAABBを持たせます。Meshは複数のPrimitiveを持っている可能性があるため、それらのPrimitiveのAABBを全てマージしたものをMeshのAABBとします。

import { Entity } from "../ec/Entity.js"; import { AABB } from "../math/AABB.js"; import { Primitive } from "./Primitive.js"; export class Mesh { private _primitives: Primitive[]; private _localAABB = new AABB(); constructor(primitives: Primitive[]) { this._primitives = primitives; for (let primitive of primitives) { this._localAABB.merge(primitive.getLocalAABB()); } } getLocalAABB() { return this._localAABB.clone(); } draw(entity: Entity) { for (let primitive of this._primitives) { primitive.draw(entity); } } }

MeshComponent

MeshComponentでは、内部のMeshのAABBをただ返すgetLocalAABB()メソッドを追加します。

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); } getLocalAABB() { // 追加 return this._mesh.getLocalAABB(); } ...

SceneGraphにAABBを持たせる

SceneGraphにもAABBを持たせます(計算させます)。二つあり、一つ目はMeshComponent(Mesh)のローカル座標系におけるAABBにワールド行列をかけてワールド座標系
にしたAABB(WorldAABB)、もう一つは、自分のWorldAABBと自分の子供たちのWorldAABBを全てマージした AABB(WorldMergedAABB)です。

// SceneGraphComponent.ts ... getWorldAABB() { const localAABB = (this.entity.getMesh() != null) ? this.entity.getMesh()!.getLocalAABB() : new AABB(); const worldAABB = localAABB.transformByMatrix(this.getMatrix()); return worldAABB; } getWorldMergedAABB() { const aabb = this.getWorldAABB(); for (const child of this.children) { const childAABB = child.getWorldMergedAABB(); aabb.merge(childAABB); } return aabb; } ...

OrbitCameraComponentでAABBを利用する

OrbitCameraComponent.tsのsetTargetメソッドをAABBを利用するように改良します。
targets(配列でEntityを複数指定できるようにしました)全体を囲む AABBを計算し、その中心点をthis._targetEntityの位置に設定。そしてそのthis._targetEntity(注視点)との距離distanceToTargetの計算でAABBのgetLengthCornerToCorner()メソッドを使っています。

これで、画面にちょうどよく収まるように、適切にカメラ位置が調整されます。

// OrbitCameraComponent.ts ... setTarget(targets: Entity[]) { this._targets = targets; const targetAABB = new AABB(); for (let target of targets) { targetAABB.merge(target.getSceneGraph().getWorldMergedAABB()); } this._targetEntity.getSceneGraph().setPosition(targetAABB.getCenterPoint()); const fovy = this._entity.getCamera()!.fovy; // calculate the distance to the target so that the target fits in the viewport const distanceToTarget = targetAABB.getLengthCornerToCorner() / Math.sin(fovy / 2); this._entity.getTransform().setLocalPosition(new Vector3(0, 0, distanceToTarget)); } ...

最後に

これで、OrbitコントローラーのsetTargetを呼ぶことで、適切にカメラの初期位置を設定することができるようになりました。

image.png

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

21日目:Orbitカメラコントロールを追加する

23日目:Walkカメラコントロールを追加する