UNPKG

mdx-m3-viewer

Version:

A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.

836 lines (687 loc) 22.2 kB
import EventEmitter from 'events'; import {vec3, quat, mat4} from 'gl-matrix'; import {VEC3_ZERO, VEC3_ONE, QUAT_DEFAULT} from '../common/gl-matrix-addon'; // Heap allocations needed for this module. let locationHeap = vec3.create(); let rotationHeap = quat.create(); let scalingHeap = vec3.create(); /** * A node mixin. * Used by SceneNode and EventNode. * * @param {class} superclass * @return {class} */ let nodeMixin = (superclass) => class extends superclass { /** * */ constructor() { super(); /** @member {vec3} */ this.pivot = vec3.create(); /** @member {vec3} */ this.localLocation = vec3.create(); /** @member {quat} */ this.localRotation = quat.create(); /** @member {vec3} */ this.localScale = vec3.fromValues(1, 1, 1); /** @member {vec3} */ this.worldLocation = vec3.create(); /** @member {quat} */ this.worldRotation = quat.create(); /** @member {vec3} */ this.worldScale = vec3.create(); /** @member {vec3} */ this.inverseWorldLocation = vec3.create(); /** @member {vec4} */ this.inverseWorldRotation = quat.create(); /** @member {vec3} */ this.inverseWorldScale = vec3.create(); /** @member {mat4} */ this.localMatrix = mat4.create(); /** @member {mat4} */ this.worldMatrix = mat4.create(); /** @member {?SceneNode} */ this.parent = null; /** @member {Array<SceneNode>} */ this.children = []; /** @member {boolean} */ this.dontInheritTranslation = false; /** @member {boolean} */ this.dontInheritRotation = false; /** @member {boolean} */ this.dontInheritScaling = true; this.visible = true; this.wasDirty = false; this.dirty = true; } /** * Sets the node's pivot. * * @param {vec3} pivot The new pivot. * @return {this} */ setPivot(pivot) { vec3.copy(this.pivot, pivot); this.dirty = true; return this; } /** * Sets the node's local location. * * @param {vec3} location The new location. * @return {this} */ setLocation(location) { vec3.copy(this.localLocation, location); this.dirty = true; return this; } /** * Sets the node's local rotation. * * @param {quat} rotation The new rotation. * @return {this} */ setRotation(rotation) { quat.copy(this.localRotation, rotation); this.dirty = true; return this; } /** * Sets the node's local scale. * * @param {vec3} varying The new scale. * @return {this} */ setScale(varying) { vec3.copy(this.localScale, varying); this.dirty = true; return this; } /** * Sets the node's local scale uniformly. * * @param {number} uniform The new scale. * @return {this} */ setUniformScale(uniform) { vec3.set(this.localScale, uniform, uniform, uniform); this.dirty = true; return this; } /** * Sets the node's local location, rotation, and scale. * * @param {vec3} location The new location. * @param {quat} rotation The new rotation. * @param {vec3} scale The new scale. * @return {this} */ setTransformation(location, rotation, scale) { 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. * * @return {this} */ 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. * * @param {vec3} offset The offset. * @return {this} */ movePivot(offset) { vec3.add(this.pivot, this.pivot, offset); this.dirty = true; return this; } /** * Moves the node's local location. * * @param {vec3} offset The offset. * @return {this} */ move(offset) { vec3.add(this.localLocation, this.localLocation, offset); this.dirty = true; return this; } /** * Rotates the node's local rotation in world space. * * @param {vec3} rotation The rotation. * @return {this} */ rotate(rotation) { quat.mul(this.localRotation, this.localRotation, rotation); this.dirty = true; return this; } /** * Rotates the node's local rotation in local space. * * @param {vec3} rotation The rotation. * @return {this} */ rotateLocal(rotation) { quat.mul(this.localRotation, rotation, this.localRotation); this.dirty = true; return this; } /** * Scales the node. * * @param {vec3} scale The scale. * @return {this} */ scale(scale) { vec3.mul(this.localScale, this.localScale, scale); this.dirty = true; return this; } /** * Scales the node uniformly. * * @param {number} scale The scale. * @return {this} */ uniformScale(scale) { vec3.scale(this.localScale, this.localScale, scale); this.dirty = true; return this; } /* orthoNormalize(vectors) { for (let i = 0; i < vectors.length; i++) { let accum = vec3.create(), p = vec3.create(); for (let j = 0; j < i; j++) { vec3.add(accum, accum, this.project(p, vectors[i], vectors[j])); } vec3.sub(vectors[i], vectors[i], accum); vec3.normalize(vectors[i], vectors[i]); } } project(out, u, v) { let d = vec3.dot(u, v), d_div = d / vec3.sqrLen(u); return vec3.scale(out, v, d_div); } lookAt(target, upDirection) { let lookAt = vec3.create(); vec3.sub(lookAt, target, this.worldLocation); let forward = vec3.clone(lookAt); let up = vec3.clone(upDirection); this.orthoNormalize([forward, up]); let right = vec3.create(); vec3.cross(right, forward, up); // vec3.normalize(forward, forward); // vec3.normalize(up, up); // vec3.normalize(right, right); quat.setAxes(this.localRotation, forward, right, up); quat.conjugate(this.localRotation, this.localRotation); this.recalculateTransformation(); return this; } //* / /* lookAt(target) { let v1 = target, v2 = this.worldLocation; let angle = Math.atan2(v2[2], v2[0]) - Math.atan2(v1[2], v1[0]); //console.log(Math.toDeg(angle)) }, */ /** * Sets the node's parent. * * @param {Node=} parent The parent. NOTE: don't set parent to null manually, instead use setParent(). * @return {this} */ setParent(parent) { // 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; // If the new parent is na 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. * Also updates the object part of this node, if there is any (e.g. model instances). * Continues the update hierarchy. * * @param {Scene} scene */ update(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(scene); this.updateChildren(scene); } /** * Update the object part of this node. * Used by model instances. * * @param {Scene} scene */ updateObject(scene) { } /** * Update this node's children and continue the update hierarchy. * * @param {Scene} scene */ updateChildren(scene) { let children = this.children; for (let i = 0, l = children.length; i < l; i++) { children[i].update(scene); } } }; /** * A scene node that can be moved, rotated, scaled, parented, etc. */ export class SceneNode extends nodeMixin(Object) {} /** * A scene node that is also an event dispatcher. */ export class EventNode extends nodeMixin(EventEmitter) {} /** * A skeletal node used for skeletons. * Expected to be created with createSharedNodes() below. */ export class SkeletalNode { /** * @param {Array<Float32Array>} shared */ constructor(shared) { /** @member {vec3} */ this.pivot = shared[0]; /** @member {vec3} */ this.localLocation = shared[1]; /** @member {quat} */ this.localRotation = shared[2]; /** @member {vec3} */ this.localScale = shared[3]; /** @member {vec3} */ this.worldLocation = shared[4]; /** @member {quat} */ this.worldRotation = shared[5]; /** @member {vec3} */ this.worldScale = shared[6]; /** @member {vec3} */ this.inverseWorldLocation = shared[7]; /** @member {vec4} */ this.inverseWorldRotation = shared[8]; /** @member {vec3} */ this.inverseWorldScale = shared[9]; /** @member {mat4} */ this.localMatrix = shared[10]; /** @member {mat4} */ this.worldMatrix = shared[11]; /** @member {boolean} */ this.dontInheritTranslation = false; /** @member {boolean} */ this.dontInheritRotation = false; /** @member {boolean} */ this.dontInheritScaling = false; /** @member {Array<SceneNode>} */ this.children = []; this.visible = true; this.wasDirty = false; /** * The object associated with this node, if there is any. * * @member {?} */ 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. * * @param {Scene} scene */ recalculateTransformation(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 = 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! * It may be called by skeletal nodes to continue the update hierarchy. * * @param {Scene} scene */ updateChildren(scene) { let children = this.children; for (let i = 0, l = children.length; i < l; i++) { children[i].update(scene); } } /** * Allows inherited node classes to run extra transformations when billboarding. * This is needed because the different model formats are in different vector spaces. * * @param {quat} rotation */ convertBasis(rotation) { } } 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. * * @param {number} count * @param {function(new:SkeletalNode)=} Node * @return {Object} */ export function createSkeletalNodes(count, Node) { 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, }; }