camera.ts

webgpu-based path tracer

src/camera.ts

4.2 KB
import { Vec3, Mat4 } from "gl-matrix";

export type CameraState = {
  position: Float32Array; // [x, y, z]
  rotation: Float32Array; // [theta, phi]
  view: Float32Array;
  inverseView: Float32Array;
  projection: Float32Array;
  currentViewProj: Float32Array;
  previousViewProj: Float32Array;
  dirty: boolean;
};

const moveVec = Vec3.create();
const lastPosition = Vec3.create();
const lastRotation = new Float32Array(2);
const rotateDelta = new Float32Array(2);
const keyState = new Set<string>();
const sprintMultiplier = 2.5;

let isPointerLocked = false;
let oldTime = 0;

export function createCamera(canvas: HTMLCanvasElement): CameraState {
  const position = Vec3.fromValues(0, -75, 20);
  const view = Mat4.create();
  const projection = Mat4.create();
  const inverseView = Mat4.create();

  const lookAtTarget = Vec3.fromValues(0, 0, 20);
  const up = Vec3.fromValues(0, 0, 1);
  Mat4.lookAt(view, position, lookAtTarget, up);
  Mat4.invert(inverseView, view);
  Mat4.perspectiveZO(projection, Math.PI / 4, canvas.width / canvas.height, 0.1, 100);

  const forward = Vec3.create();
  Vec3.subtract(forward, lookAtTarget, position);
  Vec3.normalize(forward, forward);
  const phi = Math.asin(forward[2]);
  const theta = Math.atan2(forward[0], forward[1]);

  Vec3.copy(lastPosition, position);
  lastRotation[0] = theta;
  lastRotation[1] = phi;

  return {
    position,
    rotation: new Float32Array([theta, phi]),
    view,
    inverseView,
    projection,
    currentViewProj: Mat4.create(),
    previousViewProj: Mat4.create(),
    dirty: true,
  };
}

export function updateCamera(cam: CameraState): void {
  const now = performance.now() * 0.001;
  const dt = now - oldTime;
  oldTime = now;

  const theta = cam.rotation[0] += rotateDelta[0] * dt * 60;
  const phi = cam.rotation[1] -= rotateDelta[1] * dt * 60;
  cam.rotation[1] = Math.min(88, Math.max(-88, cam.rotation[1]));
  rotateDelta[0] = rotateDelta[1] = 0;

  updateMovementInput();

  const scaledMove = Vec3.create();
  Vec3.scale(scaledMove, moveVec, dt);

  const moved = Vec3.length(scaledMove) > 1e-5;
  const rotated = theta !== lastRotation[0] || phi !== lastRotation[1];
  const changedPos = !Vec3.exactEquals(cam.position, lastPosition);
  cam.dirty = moved || rotated || changedPos;

  if (!cam.dirty) return;

  const forwards = Vec3.fromValues(
    Math.sin(toRad(theta)) * Math.cos(toRad(phi)),
    Math.cos(toRad(theta)) * Math.cos(toRad(phi)),
    Math.sin(toRad(phi))
  );
  const right = Vec3.create();
  Vec3.cross(right, forwards, [0, 0, 1]);
  Vec3.normalize(right, right);

  const up = Vec3.create();
  Vec3.cross(up, right, forwards);
  Vec3.normalize(up, up);

  Vec3.scaleAndAdd(cam.position, cam.position, forwards, scaledMove[0]);
  Vec3.scaleAndAdd(cam.position, cam.position, right, scaledMove[1]);
  Vec3.scaleAndAdd(cam.position, cam.position, up, scaledMove[2]);

  const target = Vec3.create();
  Vec3.add(target, cam.position, forwards);
  Mat4.lookAt(cam.view, cam.position, target, up);
  Mat4.invert(cam.inverseView, cam.view);

  Vec3.copy(lastPosition, cam.position);
  lastRotation[0] = theta;
  lastRotation[1] = phi;
}

export function updateMovementInput(): void {
  moveVec[0] = moveVec[1] = moveVec[2] = 0;
  const speed = keyState.has("shift") ? 10 * sprintMultiplier : 10;

  if (keyState.has("w")) moveVec[0] += speed;
  if (keyState.has("s")) moveVec[0] -= speed;
  if (keyState.has("d")) moveVec[1] += speed;
  if (keyState.has("a")) moveVec[1] -= speed;
  if (keyState.has("q")) moveVec[2] += speed;
  if (keyState.has("e")) moveVec[2] -= speed;
}

function toRad(deg: number): number {
  return (deg * Math.PI) / 180;
}

export function setupCameraInput(canvas: HTMLCanvasElement): void {
  canvas.addEventListener("click", () => canvas.requestPointerLock());

  document.addEventListener("pointerlockchange", () => {
    isPointerLocked = document.pointerLockElement != null;
  });

  window.addEventListener("keydown", e => keyState.add(e.key.toLowerCase()));
  window.addEventListener("keyup", e => keyState.delete(e.key.toLowerCase()));

  window.addEventListener("mousemove", (e) => {
    if (!isPointerLocked) return;
    const sensitivity = 0.5;
    rotateDelta[0] = e.movementX * sensitivity;
    rotateDelta[1] = e.movementY * sensitivity;
  });
}