mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
324 lines (287 loc) • 8.88 kB
JavaScript
import {clamp} from '../../../common/math';
import {interpolateScalar, interpolateVector, interpolateQuaternion} from '../../../common/interpolator';
import {UintAnimation, FloatAnimation, Vector3Animation, Vector4Animation} from '../../../parsers/mdlx/animations';
/**
* Templated animated data for a specific sequence.
*
* @param {function} interpolator
* @return {class}
*/
const templatedSdSequence = (interpolator) => class {
/**
* @param {Sd} sd
* @param {number} start
* @param {number} end
* @param {Array<Track>} keyframes
* @param {boolean} isGlobalSequence
*/
constructor(sd, start, end, keyframes, isGlobalSequence) {
let defval = sd.defval;
this.sd = sd;
this.start = start;
this.end = end;
this.keyframes = [];
// When using a global sequence, where the first key is outside of the sequence's length, it becomes its constant value.
// When having one key in the sequence's range, and one key outside of it, results seem to be non-deterministic.
// Sometimes the second key is used too, sometimes not.
// It also differs depending where the model is viewed - the WE previewer, the WE itself, or the game.
// All three show different results, none of them make sense.
// Therefore, only handle the case where the first key is outside.
// This fixes problems spread over many models, e.g. HeroMountainKing (compare in WE and in Magos).
if (isGlobalSequence && keyframes[0].frame > end) {
this.keyframes.push(keyframes[0]);
}
// Go over the keyframes, and add all of the ones that are in this sequence (start <= frame <= end).
for (let i = 0, l = keyframes.length; i < l; i++) {
let keyframe = keyframes[i];
let frame = keyframe.frame;
if (frame >= start && frame <= end) {
this.keyframes.push(keyframe);
}
}
let keyframeCount = this.keyframes.length;
if (keyframeCount === 0) {
// If there are no keys, use the default value directly.
this.constant = true;
this.keyframes.push({frame: start, value: defval, inTan: defval, outTan: defval});
} else if (keyframeCount === 1) {
// If there's only one key, use it directly.
this.constant = true;
} else {
// If all of the values in this sequence are the same, might as well make it constant.
let constant = true;
let firstValue = this.keyframes[0].value;
for (let i = 1, l = this.keyframes.length; i < l; i++) {
let keyframe = this.keyframes[i];
let value = keyframe.value;
if (value.length > 0) {
for (let j = 0, k = value.length; j < k; j++) {
if (firstValue[j] !== value[j]) {
constant = false;
break;
}
}
} else {
if (value !== firstValue) {
constant = false;
break;
}
}
}
if (constant) {
this.constant = true;
} else {
this.constant = false;
// If there is no opening keyframe for this sequence, inject one with the default value.
if (this.keyframes[0].frame !== start) {
this.keyframes.unshift({frame: start, value: defval, inTan: defval, outTan: defval});
}
// If there is no closing keyframe for this sequence, inject one with the default value.
if (this.keyframes[this.keyframes.length - 1].frame !== end) {
this.keyframes.push({frame: end, value: this.keyframes[0].value, inTan: this.keyframes[0].outTan, outTan: this.keyframes[0].inTan});
}
}
}
}
/**
* @param {Uint32Array|Float32Array|vec3|quat} out
* @param {number} frame
* @return {number}
*/
getValue(out, frame) {
let keyframes = this.keyframes;
let index = this.getKeyframe(frame);
let length = keyframes.length;
if (index === -1) {
out.set(keyframes[0].value);
return 0;
} else if (index === length) {
out.set(keyframes[length - 1].value);
return length - 1;
} else {
let start = keyframes[index - 1];
let end = keyframes[index];
let t = clamp((frame - start.frame) / (end.frame - start.frame), 0, 1);
interpolator(out, start.value, start.outTan, end.inTan, end.value, t, this.sd.interpolationType);
return index;
}
}
/**
* @param {number} frame
* @return {number}
*/
getKeyframe(frame) {
if (this.constant) {
return -1;
} else {
let keyframes = this.keyframes;
let l = keyframes.length;
if (frame < this.start) {
return -1;
} else if (frame >= this.end) {
return l;
} else {
for (let i = 1; i < l; i++) {
let keyframe = keyframes[i];
if (keyframe.frame > frame) {
return i;
}
}
}
}
}
};
const forcedInterpMap = {
KLAV: 0,
KATV: 0,
KPEV: 0,
KP2V: 0,
KRVS: 0,
};
const defVals = {
// LAYS
KMTF: [0],
KMTA: [1],
// TXAN
KTAT: [0, 0, 0],
KTAR: [0, 0, 0, 1],
KTAS: [1, 1, 1],
// GEOA
KGAO: [1],
KGAC: [0, 0, 0],
// LITE
KLAS: [0],
KLAE: [0],
KLAC: [0, 0, 0],
KLAI: [0],
KLBI: [0],
KLBC: [0, 0, 0],
KLAV: [1],
// ATCH
KATV: [1],
// PREM
KPEE: [0],
KPEG: [0],
KPLN: [0],
KPLT: [0],
KPEL: [0],
KPES: [0],
KPEV: [1],
// PRE2
KP2S: [0],
KP2R: [0],
KP2L: [0],
KP2G: [0],
KP2E: [0],
KP2N: [0],
KP2W: [0],
KP2V: [1],
// RIBB
KRHA: [0],
KRHB: [0],
KRAL: [1],
KRCO: [0, 0, 0],
KRTX: [0],
KRVS: [1],
// CAMS
KCTR: [0, 0, 0],
KTTR: [0, 0, 0],
KCRL: [0],
// NODE
KGTR: [0, 0, 0],
KGRT: [0, 0, 0, 1],
KGSC: [1, 1, 1],
};
/**
* Templated sequence data.
*
* @param {UintSequence|FloatSequence|Vector3Sequence|Vector4Sequence} SequenceType
* @return {class}
*/
const templatedSd = (SequenceType) => class {
/**
* @param {MdxModel} model
* @param {UintAnimation|FloatAnimation|Vector3Animation|Vector4Animation} animation
*/
constructor(model, animation) {
let globalSequences = model.globalSequences;
let globalSequenceId = animation.globalSequenceId;
let tracks = animation.tracks;
let forcedInterp = forcedInterpMap[animation.name];
this.model = model;
this.name = animation.name;
this.defval = defVals[animation.name];
this.globalSequence = null;
this.sequences = [];
// Allow to force an interpolation type.
// The game seems to do this with visibility tracks, where the type is forced to None.
// It came up as a bug report by a user who used the wrong interpolation type.
this.interpolationType = forcedInterp !== undefined ? forcedInterp : animation.interpolationType;
if (globalSequenceId !== -1 && globalSequences) {
this.globalSequence = new SequenceType(this, 0, globalSequences[globalSequenceId], tracks, true);
} else {
for (let sequence of model.sequences) {
this.sequences.push(new SequenceType(this, ...sequence.interval, tracks, false));
}
}
}
/**
* @param {Uint32Array|Float32Array|vec3|quat} out
* @param {ModelInstance} instance
* @return {number}
*/
getValue(out, instance) {
if (this.globalSequence) {
return this.globalSequence.getValue(out, instance.counter % this.globalSequence.end);
} else if (instance.sequence !== -1) {
return this.sequences[instance.sequence].getValue(out, instance.frame);
} else {
out.set(this.defval);
return -1;
}
}
/**
* @param {number} sequence
* @return {boolean}
*/
isVariant(sequence) {
if (this.globalSequence) {
return !this.globalSequence.constant;
} else {
return !this.sequences[sequence].constant;
}
}
/**
* @return {Array<number|vec3|quat>}
*/
getValues() {
if (this.globalSequence) {
return this.globalSequence.keyframes.map((track) => track.value);
} else {
let values = [];
for (let sequence of this.sequences) {
values.push(...sequence.keyframes.map((track) => track.value));
}
return values;
}
}
};
const ScalarSd = templatedSd(templatedSdSequence(interpolateScalar));
const Vector3Sd = templatedSd(templatedSdSequence(interpolateVector));
const Vector4Sd = templatedSd(templatedSdSequence(interpolateQuaternion));
/**
* @param {Model} model
* @param {UintAnimation|FloatAnimation|Vector3Animation|Vector4Animation} animation
* @return {ScalarSd|Vector3Sd|Vector4Sd}
*/
export default function createTypedSd(model, animation) {
let ClassObject;
if (animation instanceof UintAnimation || animation instanceof FloatAnimation) {
ClassObject = ScalarSd;
} else if (animation instanceof Vector3Animation) {
ClassObject = Vector3Sd;
} else if (animation instanceof Vector4Animation) {
ClassObject = Vector4Sd;
}
return new ClassObject(model, animation);
}