Walkカメラコントロールを追加する

もう一つ、FPSゲームみたいなカメラコントロールもあると面白そうですね。早速作ってみましょう。

WalkCameraController.ts

import { Vector3 } from "../../../math/Vector3.js"; import { Entity } from "../../Entity.js"; import { CameraComponent } from "../Camera/CameraComponent.js"; export class WalkCameraController { private _entity: Entity; private _lastKeyCode = ""; private _keyDownFn = this._keyDown.bind(this); private _keyUpFn = this._keyUp.bind(this); private _pointerDownFn = this._pointerDown.bind(this); private _pointerMoveFn = this._pointerMove.bind(this); private _pointerUpFn = this._pointerUp.bind(this); private _isPointerDown = false; private _pointerBgnX = 0; private _pointerX = 0; private _rotX = 0; private rotationRatio = 0.0001; constructor(entity: Entity) { this._entity = entity; this._registerEvents(); } private _keyDown(e: KeyboardEvent) { this._lastKeyCode = e.code; console.log(this._lastKeyCode); } private _keyUp(e: KeyboardEvent) { this._lastKeyCode = ""; } private _pointerDown(e: PointerEvent) { this._isPointerDown = true; this._pointerBgnX = e.clientX; } private _pointerMove(e: PointerEvent) { if (this._entity.getCamera() !== CameraComponent.activeCamera) { return; } if (!this._isPointerDown) { return; } this._pointerX = e.clientX; const diffX = this._pointerX - this._pointerBgnX; this._rotX -= diffX; this._entity.getSceneGraph().setEulerAngles(new Vector3(0, this._rotX * this.rotationRatio, 0)); } private _pointerUp(e: PointerEvent) { this._isPointerDown = false; this._pointerBgnX = this._pointerX; } process() { if (this._entity.getCamera() !== CameraComponent.activeCamera) { return; } const rotation = this._entity.getSceneGraph().getRotation(); let direction = new Vector3(0, 0, 0); if (this._lastKeyCode === "KeyW") { direction = new Vector3(0, 0, -1); } else if (this._lastKeyCode === "KeyS") { direction = new Vector3(0, 0, 1); } else if (this._lastKeyCode === "KeyA") { direction = new Vector3(-1, 0, 0); } else if (this._lastKeyCode === "KeyD") { direction = new Vector3(1, 0, 0); } else if (this._lastKeyCode === "KeyQ") { direction = new Vector3(0, -1, 0); } else if (this._lastKeyCode === "KeyE") { direction = new Vector3(0, 1, 0); } const rotatedDirection = rotation.transformVector(direction); const currentPos = this._entity.getSceneGraph().getPosition(); this._entity.getSceneGraph().setPosition(currentPos.add(rotatedDirection)); } private _registerEvents() { document.addEventListener("keydown", this._keyDownFn); document.addEventListener("keyup", this._keyUpFn); window.addEventListener("pointerdown", this._pointerDownFn); window.addEventListener("pointermove", this._pointerMoveFn); window.addEventListener("pointerup", this._pointerUpFn); } }

このWalkCameraControllerは、イベント処理の関係上、毎フレームprocess()メソッドを明示的に呼ぶ必要があります。
クライアントプログラマ側に明示的に呼ばせるようにするのは、ちょっとライブラリとしての使い勝手が良くないですね。
そうしたこともSpinel側のクラスで良きに計らってくれるようにしましょう。
Contxtクラスにその機能を持たせます。しかしそうした機能まで持たせるとなると、Contextという名前の範囲から逸脱しそうですね。
ContextクラスからSystemクラスに名前を変えてしまいましょう。

Systemクラスの実装

import { CameraComponent } from "./ec/components/Camera/CameraComponent.js"; import { Entity } from "./ec/Entity.js"; export class System { private static _gl: WebGLRenderingContext; static setup(canvas: HTMLCanvasElement) { const gl = canvas.getContext('webgl') as WebGLRenderingContext; if (gl == null) { alert('Failed to initialize WebGL.'); } gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.enable(gl.DEPTH_TEST); this._gl = gl; } static process(entities: Entity[]) { const gl = System._gl; gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); for (const entity of entities) { entity.getMesh()?.draw(); } this._processCameraControl(); } static processAuto() { const gl = System._gl; gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const meshEntities = Entity.getAllMeshEntities(); for (const meshEntity of meshEntities) { meshEntity.getMesh()?.draw(); } this._processCameraControl(); } private static _processCameraControl() { const cameraController = CameraComponent.activeCamera?.entity.getCameraController(); const walk = cameraController?.getWalkController(); if (walk != null) { walk.process(); } } static get gl() { return this._gl; } static get canvasWidth() { return this._gl.canvas.width; } static get canvasHeight() { return this._gl.canvas.height; } static get canvasAspectRatio() { return this.canvasWidth / this.canvasHeight; } static resize(width: number, height: number) { this._gl.canvas.width = width; this._gl.canvas.height = height; this._gl.viewport(0, 0, width, height); } }

それまでクライアント側で明示的に呼んでいたgl.clearColor(0.0, 0.0, 0.0, 1.0);gl.enable(gl.DEPTH_TEST);もSystemクラスのsetupメソッドの中で呼ぶようにしました。
また、新たにprocessメソッドとprocessAutoメソッドを導入します。これらが描画を行うメソッドです。描画を行うだけでなく、WalkCameraControllerのprocessメソッドも内部で読んでくれる親切設計になっています。

サンプルコード側

以上の変更により、サンプル側のコードもだいぶスッキリしましたね。

import Spinel from '../../dist/index.js' async function main() { const canvas = document.getElementById('world') as HTMLCanvasElement; Spinel.System.setup(canvas); const entities = await Spinel.Gltf2Importer.import('../../assets/gltf/glTF-Sample-Models/2.0/Buggy/glTF/Buggy.gltf'); const cameraEntity = Spinel.Entity.create(); cameraEntity.addCamera(Spinel.CameraType.Perspective); Spinel.CameraComponent.activeCamera = cameraEntity.getCamera()!; cameraEntity.addCameraController(Spinel.CameraControllerType.Walk); const cameraController = cameraEntity.getCameraController()!; const draw = () => { Spinel.System.processAuto(); requestAnimationFrame(draw); }; draw(); } main();

最後に

キャプチャ画像だとわかりにくいかもしれませんが、FPSのような視線移動ができるようになりました。

image.png

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

22日目:AABBを実装する