Amp-Thread-ID: https://ampcode.com/threads/T-01a01ff8-b91f-724f-8952-f07c6b5042fd Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
import type { Scene } from "../Scene";
|
|
import { Camera, type CameraOptions } from "./Camera";
|
|
|
|
export type FreeCameraControls = {
|
|
element: HTMLElement;
|
|
keyboard?: boolean;
|
|
pointer?: boolean;
|
|
controller?: boolean;
|
|
speed?: number;
|
|
sensitivity?: number;
|
|
};
|
|
export type FreeCameraOptions = CameraOptions & { controls?: FreeCameraControls };
|
|
|
|
/** Spectator-style WASD/mouse/gamepad camera backed entirely by shared camera and transform rows. */
|
|
export class FreeCamera extends Camera {
|
|
#controls?: FreeCameraControls;
|
|
#keys = new Set<string>();
|
|
#frame = 0;
|
|
#last = 0;
|
|
|
|
constructor(scene: Scene, options: FreeCameraOptions = {}) {
|
|
super(scene, options);
|
|
const cameraReady = this.ready;
|
|
this.ready = cameraReady.then(() => {
|
|
this.cameraRow()[19] = 2;
|
|
if (options.controls) this.attachControls(options.controls);
|
|
return this;
|
|
});
|
|
}
|
|
|
|
attachControls(controls: FreeCameraControls) {
|
|
this.detachControls();
|
|
this.#controls = controls;
|
|
if (controls.keyboard !== false) {
|
|
addEventListener("keydown", this.#keyDown);
|
|
addEventListener("keyup", this.#keyUp);
|
|
}
|
|
if (controls.pointer !== false) {
|
|
controls.element.addEventListener("click", this.#lock);
|
|
addEventListener("mousemove", this.#mouse);
|
|
}
|
|
this.#last = performance.now();
|
|
this.#frame = requestAnimationFrame(this.#update);
|
|
return this;
|
|
}
|
|
|
|
detachControls() {
|
|
const element = this.#controls?.element;
|
|
if (element) element.removeEventListener("click", this.#lock);
|
|
removeEventListener("keydown", this.#keyDown);
|
|
removeEventListener("keyup", this.#keyUp);
|
|
removeEventListener("mousemove", this.#mouse);
|
|
cancelAnimationFrame(this.#frame);
|
|
this.#frame = 0;
|
|
this.#keys.clear();
|
|
this.#controls = undefined;
|
|
}
|
|
|
|
#keyDown = (event: KeyboardEvent) => this.#keys.add(event.code);
|
|
#keyUp = (event: KeyboardEvent) => this.#keys.delete(event.code);
|
|
#lock = () => this.#controls?.element.requestPointerLock?.();
|
|
#mouse = (event: MouseEvent) => {
|
|
if (!this.#controls || document.pointerLockElement !== this.#controls.element) return;
|
|
const sensitivity = this.#controls.sensitivity ?? 0.002;
|
|
this.cameraRow()[13] -= event.movementX * sensitivity;
|
|
this.cameraRow()[14] = Math.min(1.55, Math.max(-1.55, this.cameraRow()[14] - event.movementY * sensitivity));
|
|
this.#writeRotor();
|
|
};
|
|
|
|
#writeRotor() {
|
|
const yaw = this.cameraRow()[13];
|
|
const pitch = this.cameraRow()[14];
|
|
const sy = Math.sin(yaw / 2), cy = Math.cos(yaw / 2);
|
|
const sx = Math.sin(pitch / 2), cx = Math.cos(pitch / 2);
|
|
this.setRotor([sx * cy, cx * sy, -sx * sy, cx * cy]);
|
|
}
|
|
|
|
#update = (time: number) => {
|
|
if (!this.#controls) return;
|
|
const delta = Math.min(0.1, (time - this.#last) / 1000);
|
|
this.#last = time;
|
|
let x = Number(this.#keys.has("KeyD")) - Number(this.#keys.has("KeyA"));
|
|
let y = Number(this.#keys.has("Space")) - Number(this.#keys.has("ControlLeft"));
|
|
let z = Number(this.#keys.has("KeyW")) - Number(this.#keys.has("KeyS"));
|
|
if (this.#controls.controller) {
|
|
const pad = navigator.getGamepads?.().find(Boolean);
|
|
if (pad) { x += pad.axes[0] ?? 0; y -= pad.axes[3] ?? 0; z -= pad.axes[1] ?? 0; }
|
|
}
|
|
const speed = (this.#controls.speed ?? 4) * delta;
|
|
const yaw = this.cameraRow()[13];
|
|
const position = this.position;
|
|
this.setPosition([
|
|
position.x + (x * Math.cos(yaw) + z * Math.sin(yaw)) * speed,
|
|
position.y + y * speed,
|
|
position.z + (x * -Math.sin(yaw) + z * Math.cos(yaw)) * speed,
|
|
]);
|
|
this.#frame = requestAnimationFrame(this.#update);
|
|
};
|
|
|
|
override async dispose() {
|
|
this.detachControls();
|
|
await super.dispose();
|
|
}
|
|
}
|