mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
718 lines (587 loc) • 20.5 kB
text/typescript
import { vec3, quat, mat4 } from 'gl-matrix';
import { VEC3_ZERO, VEC3_ONE, QUAT_DEFAULT, quatLookAt } from '../common/gl-matrix-addon';
import Scene from './scene';
const locationHeap = vec3.create();
const rotationHeap = quat.create();
const scalingHeap = vec3.create();
const faceHeap = mat4.create();
/**
* A node.
*/
export class Node {
pivot: vec3;
localLocation: vec3;
localRotation: quat;
localScale: vec3;
worldLocation: vec3;
worldRotation: quat;
worldScale: vec3;
inverseWorldLocation: vec3;
inverseWorldRotation: quat;
inverseWorldScale: vec3;
localMatrix: mat4;
worldMatrix: mat4;
parent: Node | SkeletalNode | null;
children: Node[];
dontInheritTranslation: boolean;
dontInheritRotation: boolean;
dontInheritScaling: boolean;
visible: boolean;
wasDirty: boolean;
dirty: boolean;
constructor() {
this.pivot = vec3.create();
this.localLocation = vec3.create();
this.localRotation = quat.create();
this.localScale = vec3.fromValues(1, 1, 1);
this.worldLocation = vec3.create();
this.worldRotation = quat.create();
this.worldScale = vec3.create();
this.inverseWorldLocation = vec3.create();
this.inverseWorldRotation = quat.create();
this.inverseWorldScale = vec3.create();
this.localMatrix = mat4.create();
this.worldMatrix = mat4.create();
this.parent = null;
this.children = [];
this.dontInheritTranslation = false;
this.dontInheritRotation = false;
this.dontInheritScaling = true;
this.visible = true;
this.wasDirty = false;
this.dirty = true;
}
/**
* Sets the node's pivot.
*/
setPivot(pivot: vec3) {
vec3.copy(this.pivot, pivot);
this.dirty = true;
return this;
}
/**
* Sets the node's local location.
*/
setLocation(location: vec3) {
vec3.copy(this.localLocation, location);
this.dirty = true;
return this;
}
/**
* Sets the node's local rotation.
*/
setRotation(rotation: quat) {
quat.copy(this.localRotation, rotation);
this.dirty = true;
return this;
}
/**
* Sets the node's local scale.
*/
setScale(varying: vec3) {
vec3.copy(this.localScale, varying);
this.dirty = true;
return this;
}
/**
* Sets the node's local scale uniformly.
*/
setUniformScale(uniform: number) {
vec3.set(this.localScale, uniform, uniform, uniform);
this.dirty = true;
return this;
}
/**
* Sets the node's local location, rotation, and scale.
*/
setTransformation(location: vec3, rotation: quat, scale: vec3) {
let localLocation = this.localLocation;
let localRotation = this.localRotation;
let localScale = this.localScale;
localLocation[0] = location[0];
localLocation[1] = location[1];
localLocation[2] = location[2];
// vec3.copy(this.localLocation, location);
localRotation[0] = rotation[0];
localRotation[1] = rotation[1];
localRotation[2] = rotation[2];
localRotation[3] = rotation[3];
// quat.copy(this.localRotation, rotation);
localScale[0] = scale[0];
localScale[1] = scale[1];
localScale[2] = scale[2];
// vec3.copy(this.localScale, scale);
this.dirty = true;
return this;
}
/**
* Resets the node's local location, pivot, rotation, and scale, to the default values.
*/
resetTransformation() {
vec3.copy(this.pivot, VEC3_ZERO);
vec3.copy(this.localLocation, VEC3_ZERO);
quat.copy(this.localRotation, QUAT_DEFAULT);
vec3.copy(this.localScale, VEC3_ONE);
this.dirty = true;
return this;
}
/**
* Moves the node's pivot.
*/
movePivot(offset: vec3) {
vec3.add(this.pivot, this.pivot, offset);
this.dirty = true;
return this;
}
/**
* Moves the node's local location.
*/
move(offset: vec3) {
vec3.add(this.localLocation, this.localLocation, offset);
this.dirty = true;
return this;
}
/**
* Rotates the node's local rotation in world space.
*/
rotate(rotation: quat) {
quat.mul(this.localRotation, this.localRotation, rotation);
this.dirty = true;
return this;
}
/**
* Rotates the node's local rotation in local space.
*/
rotateLocal(rotation: quat) {
quat.mul(this.localRotation, rotation, this.localRotation);
this.dirty = true;
return this;
}
/**
* Scales the node.
*/
scale(scale: vec3) {
vec3.mul(this.localScale, this.localScale, scale);
this.dirty = true;
return this;
}
/**
* Scales the node uniformly.
*/
uniformScale(scale: number) {
vec3.scale(this.localScale, this.localScale, scale);
this.dirty = true;
return this;
}
face(to: vec3, worldUp: vec3) {
quat.conjugate(this.localRotation, quatLookAt(this.localRotation, this.localLocation, to, worldUp));
this.dirty = true;
}
/**
* Sets the node's parent.
*/
setParent(parent?: Node | SkeletalNode) {
// If the node already had a parent, detach from it first.
if (this.parent) {
let children = this.parent.children;
let index = children.indexOf(this);
if (index !== -1) {
children.splice(index, 1);
}
}
this.parent = parent || null;
// If the new parent is an actual thing, add this node as a child.
if (parent) {
parent.children.push(this);
}
// this.recalculateTransformation();
this.dirty = true;
return this;
}
/**
* Recalculate this node's transformation data.
*/
recalculateTransformation() {
let dirty = this.dirty;
let parent = this.parent;
// Need to update if this node is dirty, or if its parent was dirty.
this.wasDirty = this.dirty;
if (parent) {
dirty = dirty || parent.wasDirty;
}
this.wasDirty = dirty;
if (dirty) {
this.dirty = false;
let localMatrix = this.localMatrix;
let localLocation = this.localLocation;
let localRotation = this.localRotation;
let localScale = this.localScale;
let worldMatrix = this.worldMatrix;
let worldLocation = this.worldLocation;
let worldRotation = this.worldRotation;
let worldScale = this.worldScale;
let inverseWorldLocation = this.inverseWorldLocation;
let inverseWorldRotation = this.inverseWorldRotation;
let inverseWorldScale = this.inverseWorldScale;
if (parent) {
let computedLocation;
let computedRotation;
let computedScaling;
let parentPivot = parent.pivot;
computedLocation = locationHeap;
computedLocation[0] = localLocation[0] + parentPivot[0];
computedLocation[1] = localLocation[1] + parentPivot[1];
computedLocation[2] = localLocation[2] + parentPivot[2];
// vec3.add(computedLocation, localLocation, parentPivot);
// If this node shouldn't inherit the parent's rotation, rotate it by the inverse.
if (this.dontInheritRotation) {
computedRotation = rotationHeap;
quat.mul(computedRotation, localRotation, parent.inverseWorldRotation);
} else {
computedRotation = localRotation;
}
// If this node shouldn't inherit the parent's translation, translate it by the inverse.
// if (this.dontInheritTranslation) {
// mat4.translate(worldMatrix, worldMatrix, parent.inverseWorldLocation);
// }
if (this.dontInheritScaling) {
computedScaling = scalingHeap;
let parentInverseScale = parent.inverseWorldScale;
computedScaling[0] = parentInverseScale[0] * localScale[0];
computedScaling[1] = parentInverseScale[1] * localScale[1];
computedScaling[2] = parentInverseScale[2] * localScale[2];
// vec3.mul(computedScaling, parent.inverseWorldScale, localScale);
worldScale[0] = localScale[0];
worldScale[1] = localScale[1];
worldScale[2] = localScale[2];
// vec3.copy(worldScale, localScale);
} else {
computedScaling = localScale;
let parentScale = parent.worldScale;
worldScale[0] = parentScale[0] * localScale[0];
worldScale[1] = parentScale[1] * localScale[1];
worldScale[2] = parentScale[2] * localScale[2];
// vec3.mul(worldScale, parentScale, localScale);
}
mat4.fromRotationTranslationScale(localMatrix, computedRotation, computedLocation, computedScaling);
mat4.mul(worldMatrix, parent.worldMatrix, localMatrix);
quat.mul(worldRotation, parent.worldRotation, computedRotation);
} else {
// Local matrix
mat4.fromRotationTranslationScale(localMatrix, localRotation, localLocation, localScale);
// World matrix
worldMatrix[0] = localMatrix[0];
worldMatrix[1] = localMatrix[1];
worldMatrix[2] = localMatrix[2];
worldMatrix[3] = localMatrix[3];
worldMatrix[4] = localMatrix[4];
worldMatrix[5] = localMatrix[5];
worldMatrix[6] = localMatrix[6];
worldMatrix[7] = localMatrix[7];
worldMatrix[8] = localMatrix[8];
worldMatrix[9] = localMatrix[9];
worldMatrix[10] = localMatrix[10];
worldMatrix[11] = localMatrix[11];
worldMatrix[12] = localMatrix[12];
worldMatrix[13] = localMatrix[13];
worldMatrix[14] = localMatrix[14];
worldMatrix[15] = localMatrix[15];
// mat4.copy(worldMatrix, localMatrix);
// World rotation
worldRotation[0] = localRotation[0];
worldRotation[1] = localRotation[1];
worldRotation[2] = localRotation[2];
worldRotation[3] = localRotation[3];
// quat.copy(worldRotation, localRotation);
// World scale
worldScale[0] = localScale[0];
worldScale[1] = localScale[1];
worldScale[2] = localScale[2];
// vec3.copy(worldScale, localScale);
}
// Inverse world rotation
inverseWorldRotation[0] = -worldRotation[0];
inverseWorldRotation[1] = -worldRotation[1];
inverseWorldRotation[2] = -worldRotation[2];
inverseWorldRotation[3] = worldRotation[3];
// quat.conjugate(inverseWorldRotation, worldRotation);
// Inverse world scale
inverseWorldScale[0] = 1 / worldScale[0];
inverseWorldScale[1] = 1 / worldScale[1];
inverseWorldScale[2] = 1 / worldScale[2];
// vec3.inverse(this.inverseWorldScale, worldScale);
// World location
worldLocation[0] = worldMatrix[12];
worldLocation[1] = worldMatrix[13];
worldLocation[2] = worldMatrix[14];
// Inverse world location
inverseWorldLocation[0] = -worldLocation[0];
inverseWorldLocation[1] = -worldLocation[1];
inverseWorldLocation[2] = -worldLocation[2];
// vec3.negate(this.inverseWorldLocation, worldLocation);
}
}
/**
* Update this node, and continue down the node hierarchy.
*
* Also updates the object part of this node, if there is any (e.g. model instances).
*/
update(dt: number, scene: Scene) {
if (this.dirty || (this.parent && this.parent.wasDirty)) {
this.dirty = true; // In case this node isn't dirty, but the parent was.
this.wasDirty = true;
this.recalculateTransformation();
} else {
this.wasDirty = false;
}
this.updateObject(dt, scene);
this.updateChildren(dt, scene);
}
/**
* Update the object part of this node.
*
* Used by model instances.
*/
updateObject(dt: number, scene: Scene) {
}
/**
* Update this node's children and continue the update hierarchy.
*/
updateChildren(dt: number, scene: Scene) {
let children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
children[i].update(dt, scene);
}
}
}
/**
* A skeletal node used for skeletons.
*
* Expected to be created with createSharedNodes() below.
*/
export class SkeletalNode {
pivot: vec3;
localLocation: vec3;
localRotation: quat;
localScale: vec3;
worldLocation: vec3;
worldRotation: quat;
worldScale: vec3;
inverseWorldLocation: vec3;
inverseWorldRotation: quat;
inverseWorldScale: vec3;
localMatrix: mat4;
worldMatrix: mat4;
dontInheritTranslation: boolean;
dontInheritRotation: boolean;
dontInheritScaling: boolean;
parent: SkeletalNode | Node | null;
children: Node[];
wasDirty: boolean;
/**
* The object associated with this node, if there is any.
*/
object: any;
dirty: boolean;
billboarded: boolean;
billboardedX: boolean;
billboardedY: boolean;
billboardedZ: boolean;
/**
*
*/
constructor(shared: Float32Array[]) {
this.pivot = <vec3>shared[0];
this.localLocation = <vec3>shared[1];
this.localRotation = <quat>shared[2];
this.localScale = <vec3>shared[3];
this.worldLocation = <vec3>shared[4];
this.worldRotation = <quat>shared[5];
this.worldScale = <vec3>shared[6];
this.inverseWorldLocation = <vec3>shared[7];
this.inverseWorldRotation = <quat>shared[8];
this.inverseWorldScale = <vec3>shared[9];
this.localMatrix = <mat4>shared[10];
this.worldMatrix = <mat4>shared[11];
this.dontInheritTranslation = false;
this.dontInheritRotation = false;
this.dontInheritScaling = false;
this.parent = null;
this.children = [];
this.wasDirty = false;
this.object = null;
this.localRotation[3] = 1;
this.localScale.fill(1);
this.localMatrix[0] = 1;
this.localMatrix[5] = 1;
this.localMatrix[10] = 1;
this.localMatrix[15] = 1;
this.dirty = true;
this.billboarded = false;
this.billboardedX = false;
this.billboardedY = false;
this.billboardedZ = false;
}
/**
* Recalculate this skeletal node.
*/
recalculateTransformation(scene: Scene) {
let localMatrix = this.localMatrix;
let localRotation = this.localRotation;
let localScale = this.localScale;
let worldMatrix = this.worldMatrix;
let worldLocation = this.worldLocation;
let worldRotation = this.worldRotation;
let worldScale = this.worldScale;
let pivot = this.pivot;
let inverseWorldLocation = this.inverseWorldLocation;
let inverseWorldRotation = this.inverseWorldRotation;
let inverseWorldScale = this.inverseWorldScale;
let parent = <SkeletalNode | Node>this.parent;
let computedRotation;
let computedScaling;
if (this.dontInheritScaling) {
computedScaling = scalingHeap;
let parentInverseScale = parent.inverseWorldScale;
computedScaling[0] = parentInverseScale[0] * localScale[0];
computedScaling[1] = parentInverseScale[1] * localScale[1];
computedScaling[2] = parentInverseScale[2] * localScale[2];
worldScale[0] = localScale[0];
worldScale[1] = localScale[1];
worldScale[2] = localScale[2];
} else {
computedScaling = localScale;
let parentScale = parent.worldScale;
worldScale[0] = parentScale[0] * localScale[0];
worldScale[1] = parentScale[1] * localScale[1];
worldScale[2] = parentScale[2] * localScale[2];
}
if (this.billboarded) {
computedRotation = rotationHeap;
quat.copy(computedRotation, parent.inverseWorldRotation);
quat.mul(computedRotation, computedRotation, scene.camera.inverseRotation);
this.convertBasis(computedRotation);
} else {
computedRotation = localRotation;
}
mat4.fromRotationTranslationScaleOrigin(localMatrix, computedRotation, this.localLocation, computedScaling, pivot);
mat4.mul(worldMatrix, parent.worldMatrix, localMatrix);
quat.mul(worldRotation, parent.worldRotation, computedRotation);
// Inverse world rotation
inverseWorldRotation[0] = -worldRotation[0];
inverseWorldRotation[1] = -worldRotation[1];
inverseWorldRotation[2] = -worldRotation[2];
inverseWorldRotation[3] = worldRotation[3];
// Inverse world scale
inverseWorldScale[0] = 1 / worldScale[0];
inverseWorldScale[1] = 1 / worldScale[1];
inverseWorldScale[2] = 1 / worldScale[2];
// World location
// vec3.transformMat4(worldLocation, pivot, worldMatrix);
let x = pivot[0];
let y = pivot[1];
let z = pivot[2];
worldLocation[0] = worldMatrix[0] * x + worldMatrix[4] * y + worldMatrix[8] * z + worldMatrix[12];
worldLocation[1] = worldMatrix[1] * x + worldMatrix[5] * y + worldMatrix[9] * z + worldMatrix[13];
worldLocation[2] = worldMatrix[2] * x + worldMatrix[6] * y + worldMatrix[10] * z + worldMatrix[14];
// Inverse world location
inverseWorldLocation[0] = -worldLocation[0];
inverseWorldLocation[1] = -worldLocation[1];
inverseWorldLocation[2] = -worldLocation[2];
}
/**
* Update this skeletal node's children.
*
* Note that this does not update other skeletal nodes!
*/
updateChildren(dt: number, scene: Scene) {
let children = this.children;
for (let i = 0, l = children.length; i < l; i++) {
children[i].update(dt, scene);
}
}
/**
* Allows inherited node classes to run extra transformations when billboarding.
*
* This is needed because the different model formats are in different vector spaces.
*/
convertBasis(rotation: quat) {
}
}
const NODE_SHARED_SIZE = 65;
/**
* Creates an array of skeletal nodes with shared memory.
*
* The returned object contains the node array itself, the backing buffer, and all of the different shared arrays.
*/
export function createSkeletalNodes(count: number, Node: typeof SkeletalNode = SkeletalNode) {
let data = new Float32Array(count * NODE_SHARED_SIZE);
let nodes = [];
let offset = 0;
let count3 = count * 3;
let count4 = count * 4;
let count16 = count * 16;
// Allow to also create inherited nodes.
Node = Node || SkeletalNode;
let pivots = data.subarray(offset, offset + count3);
offset += count3;
let localLocations = data.subarray(offset, offset + count3);
offset += count3;
let localRotations = data.subarray(offset, offset + count4);
offset += count4;
let localScales = data.subarray(offset, offset + count3);
offset += count3;
let worldLocations = data.subarray(offset, offset + count3);
offset += count3;
let worldRotations = data.subarray(offset, offset + count4);
offset += count4;
let worldScales = data.subarray(offset, offset + count3);
offset += count3;
let inverseWorldLocations = data.subarray(offset, offset + count3);
offset += count3;
let invereseWorldRotations = data.subarray(offset, offset + count4);
offset += count4;
let inverseWorldScales = data.subarray(offset, offset + count3);
offset += count3;
let localMatrices = data.subarray(offset, offset + count16);
offset += count16;
let worldMatrices = data.subarray(offset, offset + count16);
for (let i = 0; i < count; i++) {
let i3 = i * 3;
let i33 = i3 + 3;
let i4 = i * 4;
let i44 = i4 + 4;
let i16 = i * 16;
let i1616 = i16 + 16;
nodes[i] = new Node([
pivots.subarray(i3, i33),
localLocations.subarray(i3, i33),
localRotations.subarray(i4, i44),
localScales.subarray(i3, i33),
worldLocations.subarray(i3, i33),
worldRotations.subarray(i4, i44),
worldScales.subarray(i3, i33),
inverseWorldLocations.subarray(i3, i33),
invereseWorldRotations.subarray(i4, i44),
inverseWorldScales.subarray(i3, i33),
localMatrices.subarray(i16, i1616),
worldMatrices.subarray(i16, i1616),
]);
}
return {
data,
nodes,
pivots,
localLocations,
localRotations,
localScales,
worldLocations,
worldRotations,
worldScales,
inverseWorldLocations,
invereseWorldRotations,
inverseWorldScales,
localMatrices,
worldMatrices,
};
}