mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
495 lines • 19.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const gl_matrix_1 = require("gl-matrix");
const modelinstance_1 = require("../../modelinstance");
const skeletalnode_1 = require("../../skeletalnode");
const datatexture_1 = require("../../gl/datatexture");
const node_1 = require("./node");
const attachmentinstance_1 = require("./attachmentinstance");
const particleemitter_1 = require("./particleemitter");
const particleemitter2_1 = require("./particleemitter2");
const ribbonemitter_1 = require("./ribbonemitter");
const eventobjectspnemitter_1 = require("./eventobjectspnemitter");
const eventobjectsplemitter_1 = require("./eventobjectsplemitter");
const eventobjectubremitter_1 = require("./eventobjectubremitter");
const eventobjectsndemitter_1 = require("./eventobjectsndemitter");
const geometryemitterfuncs_1 = require("./geometryemitterfuncs");
const visibilityHeap = new Float32Array(1);
const translationHeap = gl_matrix_1.vec3.create();
const rotationHeap = gl_matrix_1.quat.create();
const scaleHeap = gl_matrix_1.vec3.create();
const colorHeap = new Float32Array(3);
const alphaHeap = new Float32Array(1);
const textureIdHeap = new Uint32Array(1);
/**
* An MDX model instance.
*/
class MdxModelInstance extends modelinstance_1.default {
constructor(model) {
super(model);
this.attachments = [];
this.particleEmitters = [];
this.particleEmitters2 = [];
this.ribbonEmitters = [];
this.eventObjectEmitters = [];
this.nodes = [];
this.sortedNodes = [];
this.frame = 0;
// Global sequences
this.counter = 0;
this.sequence = -1;
this.sequenceLoopMode = 0;
this.sequenceEnded = false;
this.teamColor = 0;
this.vertexColor = 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
this.allowParticleSpawn = 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().
this.forced = true;
this.geosetColors = [];
this.layerAlphas = [];
this.layerTextures = [];
this.uvAnims = [];
this.worldMatrices = null;
this.boneTexture = null;
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.
const sharedNodeData = (0, skeletalnode_1.createSkeletalNodes)(model.genericObjects.length, node_1.default);
const 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 (const bone of model.bones) {
this.initNode(nodes, nodes[nodeIndex++], bone);
}
for (const light of model.lights) {
this.initNode(nodes, nodes[nodeIndex++], light);
}
for (const helper of model.helpers) {
this.initNode(nodes, nodes[nodeIndex++], helper);
}
for (const 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_1.default(this, attachment);
this.attachments.push(attachmentInstance);
}
this.initNode(nodes, nodes[nodeIndex++], attachment, attachmentInstance);
}
for (const emitterObject of model.particleEmitters) {
const emitter = new particleemitter_1.default(this, emitterObject);
this.particleEmitters.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (const emitterObject of model.particleEmitters2) {
const emitter = new particleemitter2_1.default(this, emitterObject);
this.particleEmitters2.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (const emitterObject of model.ribbonEmitters) {
const emitter = new ribbonemitter_1.default(this, emitterObject);
this.ribbonEmitters.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (const emitterObject of model.eventObjects) {
const type = emitterObject.type;
let emitter;
if (type === 'SPN') {
emitter = new eventobjectspnemitter_1.default(this, emitterObject);
}
else if (type === 'SPL') {
emitter = new eventobjectsplemitter_1.default(this, emitterObject);
}
else if (type === 'UBR') {
emitter = new eventobjectubremitter_1.default(this, emitterObject);
}
else {
emitter = new eventobjectsndemitter_1.default(this, emitterObject);
}
this.eventObjectEmitters.push(emitter);
this.initNode(nodes, nodes[nodeIndex++], emitterObject, emitter);
}
for (const 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.
const hierarchy = model.hierarchy;
for (let i = 0, l = nodes.length; i < l; i++) {
this.sortedNodes[i] = nodes[hierarchy[i]];
}
if (model.bones.length) {
this.boneTexture = new datatexture_1.default(model.viewer.gl, 4, model.bones.length * 4, 1);
}
}
/**
* Override the texture at the given index.
*
* If a texture isn't given, removes the override if there was one.
*/
setTexture(index, texture) {
this.overrideTexture(index, texture);
}
/**
* Override the texture of the particle emitter the given index.
*
* If a texture isn't given, removes the override if there was one.
*/
setParticle2Texture(index, texture) {
this.overrideTexture(geometryemitterfuncs_1.EMITTER_PARTICLE2_TEXTURE_OFFSET + index, texture);
}
/**
* Override the texture of the event emitter the given index.
*
* If a texture isn't given, removes the override if there was one.
*/
setEventTexture(index, texture) {
this.overrideTexture(geometryemitterfuncs_1.EMITTER_EVENT_TEXTURE_OFFSET + index, texture);
}
/**
* Clear all of the emitted objects that belong to this instance.
*/
clearEmittedObjects() {
for (const emitter of this.particleEmitters) {
emitter.clear();
}
for (const emitter of this.particleEmitters2) {
emitter.clear();
}
for (const emitter of this.ribbonEmitters) {
emitter.clear();
}
for (const emitter of this.eventObjectEmitters) {
emitter.clear();
}
}
/**
* Initialize a skeletal node.
*/
initNode(nodes, node, genericObject, object) {
gl_matrix_1.vec3.copy(node.pivot, genericObject.pivot);
if (genericObject.parentId === -1) {
node.parent = this;
}
else {
node.parent = nodes[genericObject.parentId];
}
node.dontInheritTranslation = genericObject.dontInheritTranslation;
node.dontInheritRotation = genericObject.dontInheritRotation;
node.dontInheritScaling = genericObject.dontInheritScaling;
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();
this.resetAttachments();
}
/**
* 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, forced) {
const sequence = this.sequence;
const frame = this.frame;
const counter = this.counter;
const sortedNodes = this.sortedNodes;
const model = this.model;
const sortedGenericObjects = model.sortedGenericObjects;
// Update the nodes
for (let i = 0, l = sortedNodes.length; i < l; i++) {
const genericObject = sortedGenericObjects[i];
const node = sortedNodes[i];
const parent = node.parent;
let wasDirty = forced || parent.wasDirty || genericObject.anyBillboarding;
const 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(this);
}
// 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);
}
}
// Recalculate and update child nodes.
// Note that this only affects normal nodes such as instances, and not skeletal nodes.
for (const child of node.children) {
if (wasDirty) {
child.recalculateTransformation();
}
child.update(dt);
}
}
}
/**
* If a model has no sequences or is running no sequence, it will only update once since it will never be forced to update.
* This is generally the desired behavior, except when it is moved by the client.
* Therefore, if an instance is transformed, always do a forced update.
*/
recalculateTransformation() {
super.recalculateTransformation();
this.forced = true;
}
/**
* Update the batch data.
*/
updateBatches(forced) {
const sequence = this.sequence;
const frame = this.frame;
const counter = this.counter;
const model = this.model;
const geosets = model.geosets;
const layers = model.layers;
const geosetColors = this.geosetColors;
const layerAlphas = this.layerAlphas;
const layerTextures = this.layerTextures;
const uvAnims = this.uvAnims;
// Geosets
for (let i = 0, l = geosets.length; i < l; i++) {
const geoset = geosets[i];
const geosetAnimation = geoset.geosetAnimation;
const 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++) {
const layer = layers[i];
const textureAnimation = layer.textureAnimation;
const 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(translationHeap, sequence, frame, counter);
uvAnim[0] = translationHeap[0];
uvAnim[1] = translationHeap[1];
}
// UV rotation animation
if (forced || textureAnimation.variants['rotation'][sequence]) {
textureAnimation.getRotation(rotationHeap, sequence, frame, counter);
uvAnim[2] = rotationHeap[2];
uvAnim[3] = rotationHeap[3];
}
// UV scale animation
if (forced || textureAnimation.variants['scale'][sequence]) {
textureAnimation.getScale(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(this.worldMatrices);
}
}
renderOpaque() {
const model = this.model;
for (const group of model.opaqueGroups) {
group.render(this);
}
}
renderTranslucent() {
const model = this.model;
for (const group of model.translucentGroups) {
group.render(this);
}
}
updateAnimations(dt) {
const model = this.model;
const sequenceId = this.sequence;
if (sequenceId !== -1) {
const sequence = model.sequences[sequenceId];
const interval = sequence.interval;
const frameTime = dt * 1000;
this.frame += frameTime;
this.counter += frameTime;
this.allowParticleSpawn = true;
if (this.frame >= interval[1]) {
if (this.sequenceLoopMode === 2 || (this.sequenceLoopMode === 0 && sequence.nonLooping === 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;
}
}
const 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) {
this.teamColor = id;
return this;
}
/**
* Set the vertex color of this instance.
*/
setVertexColor(color) {
this.vertexColor.set(color);
return this;
}
/**
* Set the sequence of this instance.
*/
setSequence(id) {
const model = this.model;
const sequences = model.sequences;
this.sequence = id;
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.resetAttachments();
this.forced = true;
return this;
}
getBounds() {
const model = this.model;
if (this.sequence === -1) {
return model.bounds;
}
const bounds = model.sequences[this.sequence].bounds;
if (bounds.r === 0) {
return model.bounds;
}
return bounds;
}
/**
* Set the sequence loop mode.
* 0 to never loop, 1 to loop based on the model, and 2 to always loop.
*/
setSequenceLoopMode(mode) {
this.sequenceLoopMode = mode;
return this;
}
/**
* Get an attachment node.
*/
getAttachment(id) {
const model = this.model;
const attachment = model.attachments[id];
if (attachment) {
return this.nodes[attachment.index];
}
return;
}
resetEventEmitters() {
for (const emitter of this.eventObjectEmitters) {
emitter.lastValue = 0;
}
}
resetAttachments() {
for (const attachment of this.attachments) {
attachment.internalInstance.hide();
}
}
}
exports.default = MdxModelInstance;
//# sourceMappingURL=modelinstance.js.map