mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
244 lines (200 loc) • 8.72 kB
text/typescript
import urlWithParams from '../../../common/urlwithparams';
import { decodeAudioData } from '../../../common/audio';
import { FetchDataType } from '../../../common/fetchdatatype';
import { MappedData } from '../../../utils/mappeddata';
import EventObject from '../../../parsers/mdlx/eventobject';
import GenericResource from '../../genericresource';
import Texture from '../../texture';
import MdxModel from './model';
import GenericObject from './genericobject';
import { emitterFilterMode } from './filtermode';
import { EMITTER_SPLAT, EMITTER_UBERSPLAT } from './geometryemitterfuncs';
import MdxModelInstance from './modelinstance';
const mappedDataCallback = (data: FetchDataType) => new MappedData(<string>data);
const decodedDataCallback = (data: FetchDataType) => decodeAudioData(<ArrayBuffer>data);
/**
* An event object.
*/
export default class EventObjectEmitterObject extends GenericObject {
geometryEmitterType: number = -1;
type: string;
id: string;
tracks: Uint32Array;
globalSequence: number = -1;
defval: Uint32Array = new Uint32Array(1);
internalModel: MdxModel | null = null;
internalTexture: Texture | null = null;
colors: Float32Array[] | null = null;
intervalTimes: Float32Array | null = null;
scale: number = 0;
columns: number = 0;
rows: number = 0;
lifeSpan: number = 0;
blendSrc: number = 0;
blendDst: number = 0;
intervals: Float32Array[] | null = null;
distanceCutoff: number = 0;
maxDistance: number = 0;
minDistance: number = 0;
pitch: number = 0;
pitchVariance: number = 0;
volume: number = 0;
decodedBuffers: AudioBuffer[] = [];
/**
* If this is an SPL/UBR emitter object, ok will be set to true if the tables are loaded.
*
* This is because, like the other geometry emitters, it is fine to use them even if the textures don't load.
*
* The particles will simply be black.
*/
ok: boolean = false;
constructor(model: MdxModel, eventObject: EventObject, index: number) {
super(model, eventObject, index);
let viewer = model.viewer;
let name = eventObject.name;
let type = name.substring(0, 3);
let id = name.substring(4);
// Same thing
if (type === 'FPT') {
type = 'SPL';
}
if (type === 'SPL') {
this.geometryEmitterType = EMITTER_SPLAT;
} else if (type === 'UBR') {
this.geometryEmitterType = EMITTER_UBERSPLAT;
}
this.type = type;
this.id = id;
this.tracks = eventObject.tracks;
let globalSequenceId = eventObject.globalSequenceId;
if (globalSequenceId !== -1) {
this.globalSequence = model.globalSequences[globalSequenceId];
}
let tables = [];
let pathSolver = model.pathSolver;
let solverParams = model.solverParams;
if (type === 'SPN') {
tables[0] = viewer.loadGeneric(urlWithParams(pathSolver('Splats\\SpawnData.slk')[0], solverParams), 'text', mappedDataCallback);
} else if (type === 'SPL') {
tables[0] = viewer.loadGeneric(urlWithParams(pathSolver('Splats\\SplatData.slk')[0], solverParams), 'text', mappedDataCallback);
} else if (type === 'UBR') {
tables[0] = viewer.loadGeneric(urlWithParams(pathSolver('Splats\\UberSplatData.slk')[0], solverParams), 'text', mappedDataCallback);
} else if (type === 'SND') {
if (!model.reforged) {
tables.push(viewer.loadGeneric(urlWithParams(pathSolver('UI\\SoundInfo\\AnimLookups.slk')[0], solverParams), 'text', mappedDataCallback));
}
tables.push(viewer.loadGeneric(urlWithParams(pathSolver('UI\\SoundInfo\\AnimSounds.slk')[0], solverParams), 'text', mappedDataCallback));
} else {
// Units\Critters\BlackStagMale\BlackStagMale.mdx has an event object named "Point01".
return;
}
let promise = viewer.promise();
viewer.whenLoaded(tables, (tables) => {
for (let table of tables) {
if (!table.ok) {
promise.resolve();
return;
}
}
this.load(<GenericResource[]>tables);
promise.resolve();
})
}
load(tables: GenericResource[]) {
let firstTable = <MappedData>tables[0].data;
let row = firstTable.getRow(this.id);
let type = this.type;
if (row) {
let model = this.model;
let viewer = model.viewer;
let pathSolver = model.pathSolver;
if (type === 'SPN') {
this.internalModel = <MdxModel>viewer.load((<string>row.Model).replace('.mdl', '.mdx'), pathSolver, model.solverParams);
if (this.internalModel) {
this.internalModel.whenLoaded((model) => this.ok = model.ok);
}
} else if (type === 'SPL' || type === 'UBR') {
let texturesExt = model.reforged ? '.dds' : '.blp';
this.internalTexture = <Texture>viewer.load(`replaceabletextures/splats/${row.file}${texturesExt}`, pathSolver, model.solverParams);
this.scale = <number>row.Scale;
this.colors = [
new Float32Array([<number>row.StartR, <number>row.StartG, <number>row.StartB, <number>row.StartA]),
new Float32Array([<number>row.MiddleR, <number>row.MiddleG, <number>row.MiddleB, <number>row.MiddleA]),
new Float32Array([<number>row.EndR, <number>row.EndG, <number>row.EndB, <number>row.EndA]),
];
if (type === 'SPL') {
this.columns = <number>row.Columns;
this.rows = <number>row.Rows;
this.lifeSpan = <number>row.Lifespan + <number>row.Decay;
this.intervalTimes = new Float32Array([<number>row.Lifespan, <number>row.Decay]);
this.intervals = [
new Float32Array([<number>row.UVLifespanStart, <number>row.UVLifespanEnd, <number>row.LifespanRepeat]),
new Float32Array([<number>row.UVDecayStart, <number>row.UVDecayEnd, <number>row.DecayRepeat]),
];
} else {
this.columns = 1;
this.rows = 1;
this.lifeSpan = <number>row.BirthTime + <number>row.PauseTime + <number>row.Decay;
this.intervalTimes = new Float32Array([<number>row.BirthTime, <number>row.PauseTime, <number>row.Decay]);
}
let blendModes = emitterFilterMode(<number>row.BlendMode, viewer.gl);
this.blendSrc = blendModes[0];
this.blendDst = blendModes[1];
this.ok = true;
} else if (type === 'SND') {
// Only load sounds if audio is enabled.
// This is mostly to save on bandwidth and loading time, especially when loading full maps.
if (viewer.audioEnabled) {
let animSounds = <MappedData>tables[1].data;
row = animSounds.getRow(<string>row.SoundLabel);
if (row) {
this.distanceCutoff = <number>row.DistanceCutoff;
this.maxDistance = <number>row.MaxDistance;
this.minDistance = <number>row.MinDistance;
this.pitch = <number>row.Pitch;
this.pitchVariance = <number>row.PitchVariance;
this.volume = <number>row.Volume;
let fileNames = (<string>row.FileNames).split(',');
let resources = fileNames.map((fileName) => viewer.loadGeneric(urlWithParams(pathSolver(row.DirectoryBase + fileName)[0], model.solverParams), 'arrayBuffer', decodedDataCallback));
viewer.whenLoaded(resources, (resources) => {
for (let resource of resources) {
this.decodedBuffers.push((<GenericResource>resource).data);
}
this.ok = true;
});
}
}
}
} else {
console.warn('Unknown event object ID', type, this.id);
}
}
getValue(out: Uint32Array, instance: MdxModelInstance) {
if (this.globalSequence !== -1) {
let globalSequence = this.globalSequence;
return this.getValueAtTime(out, instance.counter % globalSequence, 0, globalSequence);
} else if (instance.sequence !== -1) {
let interval = this.model.sequences[instance.sequence].interval;
return this.getValueAtTime(out, instance.frame, interval[0], interval[1]);
} else {
out[0] = this.defval[0];
return -1;
}
}
getValueAtTime(out: Uint32Array, frame: number, start: number, end: number) {
let tracks = this.tracks;
if (frame >= start && frame <= end) {
for (let i = tracks.length - 1; i > -1; i--) {
if (tracks[i] < start) {
out[0] = 0;
return i;
} else if (tracks[i] <= frame) {
out[0] = 1;
return i;
}
}
}
out[0] = 0;
return -1;
}
}