mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
291 lines (245 loc) • 7.96 kB
text/typescript
import { vec3, vec4, quat, mat4 } from 'gl-matrix';
import { VEC3_UNIT_Y, VEC3_UNIT_X, VEC3_UNIT_Z, unproject, unpackPlanes, quatLookAt } from '../common/gl-matrix-addon';
const vectorHeap = vec3.create();
const vectorHeap2 = vec3.create();
const vectorHeap3 = vec3.create();
const quatHeap = quat.create();
const facingCorrection = quat.setAxisAngle(quat.create(), VEC3_UNIT_X, Math.PI / 2);
/**
* A camera.
*/
export default class Camera {
/**
* The rendered viewport.
*/
viewport: vec4 = vec4.create();
isPerspective: boolean = true;
fov: number = 0;
aspect: number = 0;
isOrtho: boolean = false;
leftClipPlane: number = 0;
rightClipPlane: number = 0;
bottomClipPlane: number = 0;
topClipPlane: number = 0;
nearClipPlane: number = 0;
farClipPlane: number = 0;
location: vec3 = vec3.create();
rotation: quat = quat.create();
inverseRotation: quat = quat.create();
/**
* World -> View.
*/
viewMatrix: mat4 = mat4.create();
/**
* View -> Clip.
*/
projectionMatrix: mat4 = mat4.create();
/**
* World -> Clip.
*/
viewProjectionMatrix: mat4 = mat4.create();
/**
* View -> World.
*/
inverseViewMatrix: mat4 = mat4.create();
/**
* Clip -> World.
*/
inverseViewProjectionMatrix: mat4 = mat4.create();
/**
* The X axis in camera space.
*/
directionX: vec3 = vec3.create();
/**
* The Y axis in camera space.
*/
directionY: vec3 = vec3.create();
/**
* The Z axis in camera space.
*/
directionZ: vec3 = vec3.create();
/**
* The four corners of a 2x2 rectangle.
*/
vectors: vec3[] = [vec3.fromValues(-1, -1, 0), vec3.fromValues(-1, 1, 0), vec3.fromValues(1, 1, 0), vec3.fromValues(1, -1, 0)];
/**
* Same as vectors, however these are all billboarded to the camera.
*/
billboardedVectors: vec3[] = [vec3.create(), vec3.create(), vec3.create(), vec3.create()];
/**
* The camera frustum planes in this order: left, right, top, bottom, near, far.
*/
planes: vec4[] = [vec4.create(), vec4.create(), vec4.create(), vec4.create(), vec4.create(), vec4.create()];
dirty: boolean = true;
/**
* Set the camera to perspective projection mode.
*/
perspective(fov: number, aspect: number, near: number, far: number) {
this.isPerspective = true;
this.isOrtho = false;
this.fov = fov;
this.aspect = aspect;
this.nearClipPlane = near;
this.farClipPlane = far;
this.dirty = true;
}
/**
* Set the camera to orthogonal projection mode.
*/
ortho(left: number, right: number, bottom: number, top: number, near: number, far: number) {
this.isPerspective = false;
this.isOrtho = true;
this.leftClipPlane = left;
this.rightClipPlane = right;
this.bottomClipPlane = bottom;
this.topClipPlane = top;
this.nearClipPlane = near;
this.farClipPlane = far;
this.dirty = true;
}
/**
* Set the camera's viewport.
*/
setViewport(x: number, y: number, width: number, height: number) {
let viewport = this.viewport;
viewport[0] = x;
viewport[1] = y;
viewport[2] = width;
viewport[3] = height;
this.aspect = width / height;
this.dirty = true;
}
/**
* Set the camera location in world coordinates.
*/
setLocation(location: vec3) {
vec3.copy(this.location, location);
this.dirty = true;
}
/**
* Move the camera by the given offset in world coordinates.
*/
move(offset: vec3) {
vec3.add(this.location, this.location, offset);
this.dirty = true;
}
/**
* Set the camera rotation.
*/
setRotation(rotation: quat) {
quat.copy(this.rotation, rotation);
this.dirty = true;
}
/**
* Rotate the camera by the given rotation.
*/
rotate(rotation: quat) {
quat.mul(this.rotation, this.rotation, rotation);
this.dirty = true;
}
/**
* Look at `to`.
*/
face(to: vec3, worldUp: vec3) {
quat.mul(this.rotation, facingCorrection, quatLookAt(quatHeap, to, this.location, worldUp));
this.dirty = true;
}
/**
* Move to `from` and look at `to`.
*/
moveToAndFace(from: vec3, to: vec3, worldUp: vec3) {
vec3.copy(this.location, from);
this.face(to, worldUp);
}
/**
* Reset the location and angles.
*/
reset() {
vec3.set(this.location, 0, 0, 0);
quat.identity(this.rotation);
this.dirty = true;
}
/**
* Recalculate the camera's transformation.
*/
update() {
if (this.dirty) {
let location = this.location;
let rotation = this.rotation;
let inverseRotation = this.inverseRotation;
let viewMatrix = this.viewMatrix;
let projectionMatrix = this.projectionMatrix;
let viewProjectionMatrix = this.viewProjectionMatrix;
let vectors = this.vectors;
let billboardedVectors = this.billboardedVectors;
// View -> Clip.
if (this.isPerspective) {
mat4.perspective(projectionMatrix, this.fov, this.aspect, this.nearClipPlane, this.farClipPlane);
} else {
mat4.ortho(projectionMatrix, this.leftClipPlane, this.rightClipPlane, this.bottomClipPlane, this.topClipPlane, this.nearClipPlane, this.farClipPlane);
}
// World -> View.
mat4.fromQuat(viewMatrix, rotation);
mat4.translate(viewMatrix, viewMatrix, vec3.negate(vectorHeap, location));
// World -> Clip.
mat4.mul(viewProjectionMatrix, projectionMatrix, viewMatrix);
// View -> World.
mat4.invert(this.inverseViewMatrix, viewMatrix);
// Clip -> World.
mat4.invert(this.inverseViewProjectionMatrix, viewProjectionMatrix);
// Recaculate the camera's frusum planes
unpackPlanes(this.planes, viewProjectionMatrix);
quat.conjugate(inverseRotation, rotation);
// View-space axes.
vec3.transformQuat(this.directionX, VEC3_UNIT_X, inverseRotation);
vec3.transformQuat(this.directionY, VEC3_UNIT_Y, inverseRotation);
vec3.transformQuat(this.directionZ, VEC3_UNIT_Z, inverseRotation);
// View-space rectangle, aka billboarded.
for (let i = 0; i < 4; i++) {
vec3.transformQuat(billboardedVectors[i], vectors[i], inverseRotation);
}
this.dirty = false;
}
}
/**
* Given a vector in camera space, return the vector transformed to world space.
*/
cameraToWorld(out: vec3, v: vec3) {
return vec3.transformMat4(out, v, this.inverseViewMatrix);
}
/**
* Given a vector in world space, return the vector transformed to camera space.
*/
worldToCamera(out: vec3, v: vec3) {
return vec3.transformMat4(out, v, this.viewMatrix);
}
/**
* Given a vector in world space, return the vector transformed to screen space.
*/
worldToScreen(out: Float32Array, v: Float32Array) {
let viewport = this.viewport;
vec3.transformMat4(vectorHeap, <vec3>v, this.viewProjectionMatrix);
out[0] = Math.round(((vectorHeap[0] + 1) / 2) * viewport[2]);
out[1] = Math.round(((vectorHeap[1] + 1) / 2) * viewport[3]);
return out;
}
/**
* Given a vector in screen space, return a ray from the near plane to the far plane.
*/
screenToWorldRay(out: Float32Array, v: Float32Array) {
let a = vectorHeap;
let b = vectorHeap2;
let c = vectorHeap3;
let x = v[0];
let y = v[1];
let inverseViewProjectionMatrix = this.inverseViewProjectionMatrix;
let viewport = this.viewport;
// Intersection on the near-plane
unproject(a, vec3.set(c, x, y, 0), inverseViewProjectionMatrix, viewport);
// Intersection on the far-plane
unproject(b, vec3.set(c, x, y, 1), inverseViewProjectionMatrix, viewport);
out.set(a, 0);
out.set(b, 3);
return out;
}
}