mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
539 lines (435 loc) • 16.3 kB
text/typescript
import { vec3, quat } from 'gl-matrix';
import ModelInstance from '../../modelinstance';
import { createSkeletalNodes, SkeletalNode } from '../../node';
import Node from './node';
import AttachmentInstance from './attachmentinstance';
import ParticleEmitter from './particleemitter';
import ParticleEmitter2 from './particleemitter2';
import RibbonEmitter from './ribbonemitter';
import EventObjectSpnEmitter from './eventobjectspnemitter';
import EventObjectSplEmitter from './eventobjectsplemitter';
import EventObjectUbrEmitter from './eventobjectubremitter';
import EventObjectSndEmitter from './eventobjectsndemitter';
import DataTexture from '../../gl/datatexture';
import MdxModel from './model';
import GenericObject from './genericobject';
import Scene from '../../scene';
const visibilityHeap = new Float32Array(1);
const translationHeap = vec3.create();
const rotationHeap = quat.create();
const scaleHeap = vec3.create();
const colorHeap = new Float32Array(3);
const alphaHeap = new Float32Array(1);
const textureIdHeap = new Uint32Array(1);
/**
* An MDX model instance.
*/
export default class MdxModelInstance extends ModelInstance {
attachments: AttachmentInstance[] = [];
particleEmitters: ParticleEmitter[] = [];
particleEmitters2: ParticleEmitter2[] = [];
ribbonEmitters: RibbonEmitter[] = [];
eventObjectEmitters: (EventObjectSpnEmitter | EventObjectSplEmitter | EventObjectUbrEmitter | EventObjectSndEmitter)[] = [];
nodes: SkeletalNode[] = [];
sortedNodes: SkeletalNode[] = [];
frame: number = 0;
// Global sequences
counter: number = 0;
sequence: number = -1;
sequenceLoopMode: number = 0;
sequenceEnded: boolean = false;
teamColor: number = 0;
vertexColor: Float32Array = new Float32Array([1, 1, 1, 1]);
// Particles do not spawn when the sequence is -1, or when the sequence finished and it's not repeating
allowParticleSpawn: boolean = false;
// If forced is true, everything will update regardless of variancy.
// Any later non-forced update can then use variancy to skip updating things.
// It is set to true every time the sequence is set with setSequence().
forced: boolean = true;
geosetColors: Float32Array[] = [];
layerAlphas: number[] = [];
layerTextures: number[] = [];
uvAnims: Float32Array[] = [];
worldMatrices: Float32Array | null = null;
boneTexture: DataTexture | null = null;
load() {
let model = <MdxModel>this.model;
for (let i = 0, l = model.geosets.length; i < l; i++) {
this.geosetColors[i] = new Float32Array(4);
}
for (let i = 0, l = model.layers.length; i < l; i++) {
this.layerAlphas[i] = 0;
this.layerTextures[i] = 0;
this.uvAnims[i] = new Float32Array(5);
}
// Create the needed amount of shared nodes.
let sharedNodeData = createSkeletalNodes(model.genericObjects.length, Node);
let nodes = sharedNodeData.nodes;
let nodeIndex = 0;
this.nodes.push(...nodes);
// A shared typed array for all world matrices of the internal nodes.
this.worldMatrices = sharedNodeData.worldMatrices;
// And now initialize all of the nodes and objects
for (let bone of model.bones) {
this.initNode(nodes, nodes[nodeIndex++], bone);
}
for (let light of model.lights) {
this.initNode(nodes, nodes[nodeIndex++], light);
}
for (let helper of model.helpers) {
this.initNode(nodes, nodes[nodeIndex++], helper);
}
for (let attachment of model.attachments) {
let attachmentInstance;
// Attachments may have game models attached to them, such as Undead and Nightelf building animations.
if (attachment.internalModel) {
attachmentInstance = new AttachmentInstance(this, attachment);
this.attachments.push(attachmentInstance);
}
this.initNode(nodes, nodes[nodeIndex++], attachment, attachmentInstance);
}
for (let emitterObject of model.particleEmitters) {
let emitter = new ParticleEmitter(this, emitterObject);
this.particleEmitters.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (let emitterObject of model.particleEmitters2) {
let emitter = new ParticleEmitter2(this, emitterObject);
this.particleEmitters2.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (let emitterObject of model.ribbonEmitters) {
let emitter = new RibbonEmitter(this, emitterObject);
this.ribbonEmitters.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (let emitterObject of model.eventObjects) {
let type = emitterObject.type;
let emitter;
if (type === 'SPN') {
emitter = new EventObjectSpnEmitter(this, emitterObject);
} else if (type === 'SPL') {
emitter = new EventObjectSplEmitter(this, emitterObject);
} else if (type === 'UBR') {
emitter = new EventObjectUbrEmitter(this, emitterObject);
} else {
emitter = new EventObjectSndEmitter(this, emitterObject);
}
this.eventObjectEmitters.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (let collisionShape of model.collisionShapes) {
this.initNode(nodes, nodes[nodeIndex++], collisionShape);
}
// Save a sorted array of all of the nodes, such that every child node comes after its parent.
// This allows for flat iteration when updating.
let hierarchy = model.hierarchy;
for (let i = 0, l = nodes.length; i < l; i++) {
this.sortedNodes[i] = nodes[hierarchy[i]];
}
// If the sequence was changed before the model was loaded, reset it now that the model loaded.
this.setSequence(this.sequence);
if (model.bones.length) {
this.boneTexture = new DataTexture(model.viewer.gl, 4, model.bones.length * 4, 1);
}
}
/**
* Clear all of the emitted objects that belong to this instance.
*/
clearEmittedObjects() {
for (let emitter of this.particleEmitters) {
emitter.clear();
}
for (let emitter of this.particleEmitters2) {
emitter.clear();
}
for (let emitter of this.ribbonEmitters) {
emitter.clear();
}
for (let emitter of this.eventObjectEmitters) {
emitter.clear();
}
}
/**
* Initialize a skeletal node.
*/
initNode(nodes: SkeletalNode[], node: SkeletalNode, genericObject: GenericObject, object?: any) {
vec3.copy(node.pivot, genericObject.pivot);
if (genericObject.parentId === -1) {
node.parent = this;
} else {
node.parent = nodes[genericObject.parentId];
}
/// TODO: single-axis billboarding
if (genericObject.billboarded) {
node.billboarded = true;
}// else if (genericObject.billboardedX) {
// node.billboardedX = true;
// } else if (genericObject.billboardedY) {
// node.billboardedY = true;
// } else if (genericObject.billboardedZ) {
// node.billboardedZ = true;
// }
if (object) {
node.object = object;
}
}
/**
* Overriden to hide also attachment models.
*/
hide() {
super.hide();
for (let attachment of this.attachments) {
attachment.internalInstance.hide();
}
}
/**
* Updates all of this instance internal nodes and objects.
* Nodes that are determined to not be visible will not be updated, nor will any of their children down the hierarchy.
*/
updateNodes(dt: number, forced: boolean) {
let sequence = this.sequence;
let frame = this.frame;
let counter = this.counter;
let sortedNodes = this.sortedNodes;
let model = <MdxModel>this.model;
let sortedGenericObjects = model.sortedGenericObjects;
let scene = <Scene>this.scene;
// Update the nodes
for (let i = 0, l = sortedNodes.length; i < l; i++) {
let genericObject = sortedGenericObjects[i];
let node = sortedNodes[i];
let parent = <Node | SkeletalNode>node.parent;
let wasDirty = forced || parent.wasDirty || genericObject.anyBillboarding;
let variants = genericObject.variants;
// Local node transformation.
// Use variants to skip animation data when possible.
if (forced || variants.generic[sequence]) {
wasDirty = true;
// Translation
if (forced || variants.translation[sequence]) {
genericObject.getTranslation(node.localLocation, sequence, frame, counter);
}
// Rotation
if (forced || variants.rotation[sequence]) {
genericObject.getRotation(node.localRotation, sequence, frame, counter);
}
// Scale
if (forced || variants.scale[sequence]) {
genericObject.getScale(node.localScale, sequence, frame, counter);
}
}
node.wasDirty = wasDirty;
// If this is a forced update, or this node's local data was updated, or the parent node was updated, do a full world update.
if (wasDirty) {
node.recalculateTransformation(scene);
}
// If there is an instance object associated with this node (emitter/attachment), and it is visible, update it.
if (node.object) {
genericObject.getVisibility(visibilityHeap, sequence, frame, counter);
// If the attachment/emitter is visible, update it.
if (visibilityHeap[0] > 0) {
node.object.update(dt);
}
}
// Update all of the node's non-skeletal children, which will update their children, and so on.
node.updateChildren(dt, scene);
}
}
/**
* Update the batch data.
*/
updateBatches(forced: boolean) {
let sequence = this.sequence;
let frame = this.frame;
let counter = this.counter;
let model = <MdxModel>this.model;
let geosets = model.geosets;
let layers = model.layers;
let geosetColors = this.geosetColors;
let layerAlphas = this.layerAlphas;
let layerTextures = this.layerTextures;
let uvAnims = this.uvAnims;
// Geosets
for (let i = 0, l = geosets.length; i < l; i++) {
let geoset = geosets[i];
let geosetAnimation = geoset.geosetAnimation;
let geosetColor = geosetColors[i];
if (geosetAnimation) {
// Color
if (forced || geosetAnimation.variants.color[sequence]) {
geosetAnimation.getColor(colorHeap, sequence, frame, counter);
geosetColor[0] = colorHeap[0];
geosetColor[1] = colorHeap[1];
geosetColor[2] = colorHeap[2];
}
// Alpha
if (forced || geosetAnimation.variants.alpha[sequence]) {
geosetAnimation.getAlpha(alphaHeap, sequence, frame, counter);
geosetColor[3] = alphaHeap[0];
}
} else if (forced) {
geosetColor[0] = 1;
geosetColor[1] = 1;
geosetColor[2] = 1;
geosetColor[3] = 1;
}
}
// Layers
for (let i = 0, l = layers.length; i < l; i++) {
let layer = layers[i];
let textureAnimation = layer.textureAnimation;
let uvAnim = uvAnims[i];
// Alpha
if (forced || layer.variants.alpha[sequence]) {
layer.getAlpha(alphaHeap, sequence, frame, counter);
layerAlphas[i] = alphaHeap[0];
}
// Sprite animation
if (forced || layer.variants.textureId[sequence]) {
layer.getTextureId(textureIdHeap, sequence, frame, counter);
layerTextures[i] = textureIdHeap[0];
}
if (textureAnimation) {
// UV translation animation
if (forced || textureAnimation.variants.translation[sequence]) {
textureAnimation.getTranslation(<Float32Array>translationHeap, sequence, frame, counter);
uvAnim[0] = translationHeap[0];
uvAnim[1] = translationHeap[1];
}
// UV rotation animation
if (forced || textureAnimation.variants.rotation[sequence]) {
textureAnimation.getRotation(<Float32Array>rotationHeap, sequence, frame, counter);
uvAnim[2] = rotationHeap[2];
uvAnim[3] = rotationHeap[3];
}
// UV scale animation
if (forced || textureAnimation.variants.scale[sequence]) {
textureAnimation.getScale(<Float32Array>scaleHeap, sequence, frame, counter);
uvAnim[4] = scaleHeap[0];
}
} else if (forced) {
uvAnim[0] = 0;
uvAnim[1] = 0;
uvAnim[2] = 0;
uvAnim[3] = 1;
uvAnim[4] = 1;
}
}
}
updateBoneTexture() {
if (this.boneTexture) {
this.boneTexture.bindAndUpdate(<Float32Array>this.worldMatrices);
}
}
renderOpaque() {
let model = <MdxModel>this.model;
for (let group of model.opaqueGroups) {
group.render(this);
}
}
renderTranslucent() {
let model = <MdxModel>this.model;
for (let group of model.translucentGroups) {
group.render(this);
}
}
updateAnimations(dt: number) {
let model = <MdxModel>this.model;
let sequenceId = this.sequence;
if (sequenceId !== -1) {
let sequence = model.sequences[sequenceId];
let interval = sequence.interval;
let frameTime = model.viewer.frameTime;
this.frame += frameTime;
this.counter += frameTime;
this.allowParticleSpawn = true;
if (this.frame >= interval[1]) {
if (this.sequenceLoopMode === 2 || (this.sequenceLoopMode === 0 && sequence.flags === 0)) {
this.frame = interval[0];
this.resetEventEmitters();
} else {
this.frame = interval[1];
this.counter -= frameTime;
this.allowParticleSpawn = false;
}
this.sequenceEnded = true;
} else {
this.sequenceEnded = false;
}
}
let forced = this.forced;
if (sequenceId !== -1 || forced) {
// Update the nodes
this.updateNodes(dt, forced);
// Update the bone texture.
this.updateBoneTexture();
// Update the batches
this.updateBatches(forced);
}
this.forced = false;
}
/**
* Set the team color of this instance.
*/
setTeamColor(id: number) {
this.teamColor = id;
return this;
}
/**
* Set the vertex color of this instance.
*/
setVertexColor(color: Float32Array | number[]) {
this.vertexColor.set(color);
return this;
}
/**
* Set the sequence of this instance.
*/
setSequence(id: number) {
let model = <MdxModel>this.model;
this.sequence = id;
if (model.ok) {
let sequences = model.sequences;
if (id < 0 || id > sequences.length - 1) {
this.sequence = -1;
this.frame = 0;
this.allowParticleSpawn = false;
} else {
this.frame = sequences[id].interval[0];
}
this.resetEventEmitters();
this.forced = true;
}
return this;
}
/**
* Set the seuqnece loop mode.
* 0 to never loop, 1 to loop based on the model, and 2 to always loop.
*/
setSequenceLoopMode(mode: number) {
this.sequenceLoopMode = mode;
return this;
}
/**
* Get an attachment node.
*/
getAttachment(id: number) {
let model = <MdxModel>this.model;
let attachment = model.attachments[id];
if (attachment) {
return this.nodes[attachment.index];
}
}
/**
* Event emitters depend on keyframe index changes to emit, rather than only values.
* To work, they need to check what the last keyframe was, and only if it's a different one, do something.
* When changing sequences, these states need to be reset, so they can immediately emit things if needed.
*/
resetEventEmitters() {
/// TODO: Update this.
// for (let eventEmitterView of this.eventObjectEmitters) {
// eventEmitterView.reset();
// }
}
}