1import { Vec3, Mat4 } from "gl-matrix";
2
3export type CameraState = {
4 position: Float32Array; // [x, y, z]
5 rotation: Float32Array; // [theta, phi]
6 view: Float32Array;
7 inverseView: Float32Array;
8 projection: Float32Array;
9 currentViewProj: Float32Array;
10 previousViewProj: Float32Array;
11 dirty: boolean;
12};
13
14const moveVec = Vec3.create();
15const lastPosition = Vec3.create();
16const lastRotation = new Float32Array(2);
17const rotateDelta = new Float32Array(2);
18const keyState = new Set<string>();
19const sprintMultiplier = 2.5;
20
21let isPointerLocked = false;
22let oldTime = 0;
23
24export function createCamera(canvas: HTMLCanvasElement): CameraState {
25 const position = Vec3.fromValues(0, -75, 20);
26 const view = Mat4.create();
27 const projection = Mat4.create();
28 const inverseView = Mat4.create();
29
30 const lookAtTarget = Vec3.fromValues(0, 0, 20);
31 const up = Vec3.fromValues(0, 0, 1);
32 Mat4.lookAt(view, position, lookAtTarget, up);
33 Mat4.invert(inverseView, view);
34 Mat4.perspectiveZO(projection, Math.PI / 4, canvas.width / canvas.height, 0.1, 100);
35
36 const forward = Vec3.create();
37 Vec3.subtract(forward, lookAtTarget, position);
38 Vec3.normalize(forward, forward);
39 const phi = Math.asin(forward[2]);
40 const theta = Math.atan2(forward[0], forward[1]);
41
42 Vec3.copy(lastPosition, position);
43 lastRotation[0] = theta;
44 lastRotation[1] = phi;
45
46 return {
47 position,
48 rotation: new Float32Array([theta, phi]),
49 view,
50 inverseView,
51 projection,
52 currentViewProj: Mat4.create(),
53 previousViewProj: Mat4.create(),
54 dirty: true,
55 };
56}
57
58export function updateCamera(cam: CameraState): void {
59 const now = performance.now() * 0.001;
60 const dt = now - oldTime;
61 oldTime = now;
62
63 const theta = cam.rotation[0] += rotateDelta[0] * dt * 60;
64 const phi = cam.rotation[1] -= rotateDelta[1] * dt * 60;
65 cam.rotation[1] = Math.min(88, Math.max(-88, cam.rotation[1]));
66 rotateDelta[0] = rotateDelta[1] = 0;
67
68 updateMovementInput();
69
70 const scaledMove = Vec3.create();
71 Vec3.scale(scaledMove, moveVec, dt);
72
73 const moved = Vec3.length(scaledMove) > 1e-5;
74 const rotated = theta !== lastRotation[0] || phi !== lastRotation[1];
75 const changedPos = !Vec3.exactEquals(cam.position, lastPosition);
76 cam.dirty = moved || rotated || changedPos;
77
78 if (!cam.dirty) return;
79
80 const forwards = Vec3.fromValues(
81 Math.sin(toRad(theta)) * Math.cos(toRad(phi)),
82 Math.cos(toRad(theta)) * Math.cos(toRad(phi)),
83 Math.sin(toRad(phi))
84 );
85 const right = Vec3.create();
86 Vec3.cross(right, forwards, [0, 0, 1]);
87 Vec3.normalize(right, right);
88
89 const up = Vec3.create();
90 Vec3.cross(up, right, forwards);
91 Vec3.normalize(up, up);
92
93 Vec3.scaleAndAdd(cam.position, cam.position, forwards, scaledMove[0]);
94 Vec3.scaleAndAdd(cam.position, cam.position, right, scaledMove[1]);
95 Vec3.scaleAndAdd(cam.position, cam.position, up, scaledMove[2]);
96
97 const target = Vec3.create();
98 Vec3.add(target, cam.position, forwards);
99 Mat4.lookAt(cam.view, cam.position, target, up);
100 Mat4.invert(cam.inverseView, cam.view);
101
102 Vec3.copy(lastPosition, cam.position);
103 lastRotation[0] = theta;
104 lastRotation[1] = phi;
105}
106
107export function updateMovementInput(): void {
108 moveVec[0] = moveVec[1] = moveVec[2] = 0;
109 const speed = keyState.has("shift") ? 10 * sprintMultiplier : 10;
110
111 if (keyState.has("w")) moveVec[0] += speed;
112 if (keyState.has("s")) moveVec[0] -= speed;
113 if (keyState.has("d")) moveVec[1] += speed;
114 if (keyState.has("a")) moveVec[1] -= speed;
115 if (keyState.has("q")) moveVec[2] += speed;
116 if (keyState.has("e")) moveVec[2] -= speed;
117}
118
119function toRad(deg: number): number {
120 return (deg * Math.PI) / 180;
121}
122
123export function setupCameraInput(canvas: HTMLCanvasElement): void {
124 canvas.addEventListener("click", () => canvas.requestPointerLock());
125
126 document.addEventListener("pointerlockchange", () => {
127 isPointerLocked = document.pointerLockElement != null;
128 });
129
130 window.addEventListener("keydown", e => keyState.add(e.key.toLowerCase()));
131 window.addEventListener("keyup", e => keyState.delete(e.key.toLowerCase()));
132
133 window.addEventListener("mousemove", (e) => {
134 if (!isPointerLocked) return;
135 const sensitivity = 0.5;
136 rotateDelta[0] = e.movementX * sensitivity;
137 rotateDelta[1] = e.movementY * sensitivity;
138 });
139}