UNPKG

mdx-m3-viewer

Version:

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

543 lines (435 loc) 15.9 kB
import {vec3, quat} from 'gl-matrix'; import TexturedModelInstance from '../../texturedmodelinstance'; import {createSkeletalNodes} from '../../node'; import Node from './node'; import AttachmentInstance from './attachmentinstance'; import ParticleEmitterView from './particleemitterview'; import ParticleEmitter2View from './particleemitter2view'; import RibbonEmitterView from './ribbonemitterview'; import EventObjectEmitterView from './eventobjectemitterview'; // Heap allocations needed for this module. let visibilityHeap = new Float32Array(1); let translationHeap = vec3.create(); let rotationHeap = quat.create(); let scaleHeap = vec3.create(); let colorHeap = new Float32Array(3); let alphaHeap = new Float32Array(1); let textureIdHeap = new Float32Array(1); /** * An MDX model instance. */ export default class ModelInstance extends TexturedModelInstance { /** * @param {MdxModel} model */ constructor(model) { super(model); this.attachments = []; this.particleEmitters = []; this.particleEmitters2 = []; this.ribbonEmitters = []; this.eventObjectEmitters = []; this.nodes = []; this.sortedNodes = []; this.frame = 0; this.counter = 0; // Global sequences this.sequence = -1; this.sequenceLoopMode = 0; this.teamColor = 0; this.vertexColor = new Uint8Array([255, 255, 255, 255]); this.allowParticleSpawn = false; // Particles do not spawn when the sequence is -1, or when the sequence finished and it's not repeating // 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(). this.forced = true; } /** * Called when the model finishes loading, or immediately if it was already loaded when this instance was created. */ load() { let model = this.model; let geosetCount = model.geosets.length; let layerCount = model.layers.length; this.geosetColors = new Uint8Array(geosetCount * 4); this.layerAlphas = new Uint8Array(layerCount); this.uvOffsets = new Float32Array(layerCount * 4); this.uvScales = new Float32Array(layerCount); this.uvRots = new Float32Array(layerCount * 2); // 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 emitter of model.particleEmitters) { let emitterView = new ParticleEmitterView(this, emitter); this.particleEmitters.push(emitterView); this.initNode(nodes, nodes[nodeIndex++], emitter, emitterView); } for (let emitter of model.particleEmitters2) { let emitterView = new ParticleEmitter2View(this, emitter); this.particleEmitters2.push(emitterView); this.initNode(nodes, nodes[nodeIndex++], emitter, emitterView); } for (let emitter of model.ribbonEmitters) { let emitterView = new RibbonEmitterView(this, emitter); this.ribbonEmitters.push(emitterView); this.initNode(nodes, nodes[nodeIndex++], emitter, emitterView); } for (let camera of model.cameras) { this.initNode(nodes, nodes[nodeIndex++], camera); } for (let emitter of model.eventObjects) { let emitterView = new EventObjectEmitterView(this, emitter); this.eventObjectEmitters.push(emitterView); this.initNode(nodes, nodes[nodeIndex++], emitter, emitterView); } 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); } /** * Clear all of the emitted objects that belong to this instance. */ clearEmittedObjects() { if (this.modelView) { for (let sceneData of this.modelView.sceneData.values()) { for (let emitter of sceneData.particleEmitters) { emitter.clear(this); } for (let emitter of sceneData.particleEmitters2) { emitter.clear(this); } for (let emitter of sceneData.ribbonEmitters) { emitter.clear(this); } for (let emitter of sceneData.eventObjectEmitters) { emitter.clear(this); } } } } /** * Initialize a skeletal node. * * @param {Array<SkeletalNode>} nodes * @param {SkeletalNode} node * @param {GenericObject} genericObject * @param {*} object */ initNode(nodes, node, genericObject, object) { node.pivot.set(genericObject.pivot); if (genericObject.parentId === -1) { node.parent = this; } else { node.parent = nodes[genericObject.parentId]; } 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. * * @override */ hide() { super.hide(); for (let attachment of this.attachments) { attachment.internalInstance.hide(); } } /** * Updates the animation timers. * Emits a 'seqend' event every time a sequence ends. */ updateTimers() { if (this.sequence !== -1) { let model = this.model; let sequence = model.sequences[this.sequence]; 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.emit('seqend', this); } } } /** * 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. * * @param {boolean} forced */ updateNodes(forced) { let sortedNodes = this.sortedNodes; let sequence = this.sequence; let sortedGenericObjects = this.model.sortedGenericObjects; let 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.parent; genericObject.getVisibility(visibilityHeap, this); let objectVisible = visibilityHeap[0] > 0; let nodeVisible = forced || (parent.visible && objectVisible); node.visible = nodeVisible; // Every node only needs to be updated if this is a forced update, or if both the parent node and the generic object corresponding to this node are visible. // Incoming messy code for optimizations! if (nodeVisible) { let wasDirty = false; let variants = genericObject.variants; let localLocation = node.localLocation; let localRotation = node.localRotation; let localScale = node.localScale; // Only update the local node data if there is a need to if (forced || variants.generic[sequence]) { wasDirty = true; // Translation if (forced || variants.translation[sequence]) { genericObject.getTranslation(translationHeap, this); localLocation[0] = translationHeap[0]; localLocation[1] = translationHeap[1]; localLocation[2] = translationHeap[2]; } // Rotation if (forced || variants.rotation[sequence]) { genericObject.getRotation(rotationHeap, this); localRotation[0] = rotationHeap[0]; localRotation[1] = rotationHeap[1]; localRotation[2] = rotationHeap[2]; localRotation[3] = rotationHeap[3]; } // Scale if (forced || variants.scale[sequence]) { genericObject.getScale(scaleHeap, this); localScale[0] = scaleHeap[0]; localScale[1] = scaleHeap[1]; localScale[2] = scaleHeap[2]; } } let wasReallyDirty = forced || wasDirty || parent.wasDirty || genericObject.anyBillboarding; node.wasDirty = wasReallyDirty; // 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 (wasReallyDirty) { node.recalculateTransformation(scene); } // If there is an instance object associated with this node, and the node is visible (which might not be the case for a forced update!), update the object. // This includes attachments and emitters. let object = node.object; if (object && objectVisible) { object.update(); } // Update all of the node's non-skeletal children, which will update their children, and so on. node.updateChildren(scene); } } } /** * Update the batch data. * * @param {boolean} forced */ updateBatches(forced) { let model = this.model; let geosets = model.geosets; let layers = model.layers; let geosetColors = this.geosetColors; let layerAlphas = this.layerAlphas; let uvOffsets = this.uvOffsets; let uvScales = this.uvScales; let uvRots = this.uvRots; let sequence = this.sequence; // Geosets for (let i = 0, l = geosets.length; i < l; i++) { let geoset = geosets[i]; let i4 = i * 4; // Color if (forced || geoset.variants.color[sequence]) { geoset.getColor(colorHeap, this); // Some Blizzard models have values greater than 1, which messes things up. // Geoset animations are supposed to modulate colors, not intensify them. geosetColors[i4] = colorHeap[0] * 255; geosetColors[i4 + 1] = colorHeap[1] * 255; geosetColors[i4 + 2] = colorHeap[2] * 255; } // Alpha if (forced || geoset.variants.alpha[sequence]) { geoset.getAlpha(alphaHeap, this); geosetColors[i4 + 3] = alphaHeap[0] * 255; } } // Layers for (let i = 0, l = layers.length; i < l; i++) { let layer = layers[i]; let i2 = i * 2; let i4 = i * 4; // Alpha if (forced || layer.variants.alpha[sequence]) { layer.getAlpha(alphaHeap, this); layerAlphas[i] = alphaHeap[0] * 255; } // UV translation animation if (forced || layer.variants.translation[sequence]) { layer.getTranslation(translationHeap, this); uvOffsets[i4] = translationHeap[0]; uvOffsets[i4 + 1] = translationHeap[1]; } // UV rotation animation if (forced || layer.variants.rotation[sequence]) { layer.getRotation(rotationHeap, this); uvRots[i2] = rotationHeap[2]; uvRots[i2 + 1] = rotationHeap[3]; } // UV scale animation if (forced || layer.variants.scale[sequence]) { layer.getScale(scaleHeap, this); uvScales[i] = scaleHeap[0]; } // Sprite animation if (forced || layer.variants.slot[sequence]) { layer.getTextureId(textureIdHeap, this); let uvDivisor = layer.uvDivisor; let textureId = textureIdHeap[0]; uvOffsets[i4 + 2] = textureId % uvDivisor[0]; uvOffsets[i4 + 3] = (textureId / uvDivisor[1]) | 0; } } } /** * Update all of the animated data. */ updateAnimations() { let forced = this.forced; if (forced || this.sequence !== -1) { this.forced = false; // Update the nodes this.updateNodes(forced); // Update the batches this.updateBatches(forced); } } /** * Set the team color of this instance. * * @param {number} id * @return {this} */ setTeamColor(id) { this.teamColor = id; return this; } /** * Set the vertex color of this instance. * * @param {Uint8Array|Array<number>} color * @return {this} */ setVertexColor(color) { this.vertexColor.set(color); return this; } /** * Set the sequence of this instance. * * @param {number} id * @return {this} */ setSequence(id) { this.sequence = id; if (this.model.ok) { let sequences = this.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. * * @param {number} mode * @return {this} */ setSequenceLoopMode(mode) { this.sequenceLoopMode = mode; return this; } /** * Get an attachment node. * * @param {number} id * @return {SkeletalNode} */ getAttachment(id) { let attachment = this.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() { for (let eventEmitterView of this.eventObjectEmitters) { eventEmitterView.reset(); } } }