mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
366 lines (305 loc) • 12.3 kB
text/typescript
import BinaryStream from '../../common/binarystream';
import TokenStream from './tokenstream';
import Extent from './extent';
/**
* A geoset.
*/
export default class Geoset {
vertices: Float32Array = new Float32Array(0);
normals: Float32Array = new Float32Array(0);
faceTypeGroups: Uint32Array = new Uint32Array(0);
faceGroups: Uint32Array = new Uint32Array(0);
faces: Uint16Array = new Uint16Array(0);
vertexGroups: Uint8Array = new Uint8Array(0);
matrixGroups: Uint32Array = new Uint32Array(0);
matrixIndices: Uint32Array = new Uint32Array(0);
materialId: number = 0;
selectionGroup: number = 0;
selectionFlags: number = 0;
/**
* @since 900
*/
lod: number = -1;
/**
* @since 900
*/
lodName: string = '';
extent: Extent = new Extent();
sequenceExtents: Extent[] = [];
/**
* @since 900
*/
tangents: Float32Array = new Float32Array(0);
/**
* An array of bone indices and weights.
* Every 8 consecutive elements describe the following:
* [B0, B1, B2, B3, W0, W1, W2, W3]
* Where:
* Bn is a bone index.
* Wn is a weight, which can be normalized with Wn/255.
*
* @since 900
*/
skin: Uint8Array = new Uint8Array(0);
uvSets: Float32Array[] = [];
readMdx(stream: BinaryStream, version: number) {
stream.readUint32(); // Don't care about the size.
stream.skip(4); // VRTX
this.vertices = stream.readFloat32Array(stream.readUint32() * 3);
stream.skip(4); // NRMS
this.normals = stream.readFloat32Array(stream.readUint32() * 3);
stream.skip(4); // PTYP
this.faceTypeGroups = stream.readUint32Array(stream.readUint32());
stream.skip(4); // PCNT
this.faceGroups = stream.readUint32Array(stream.readUint32());
stream.skip(4); // PVTX
this.faces = stream.readUint16Array(stream.readUint32());
stream.skip(4); // GNDX
this.vertexGroups = stream.readUint8Array(stream.readUint32());
stream.skip(4); // MTGC
this.matrixGroups = stream.readUint32Array(stream.readUint32());
stream.skip(4); // MATS
this.matrixIndices = stream.readUint32Array(stream.readUint32());
this.materialId = stream.readUint32();
this.selectionGroup = stream.readUint32();
this.selectionFlags = stream.readUint32();
if (version > 800) {
this.lod = stream.readInt32();
this.lodName = stream.read(80);
}
this.extent.readMdx(stream);
for (let i = 0, l = stream.readUint32(); i < l; i++) {
let extent = new Extent();
extent.readMdx(stream);
this.sequenceExtents.push(extent);
}
// Non-reforged models that come with reforged are saved with version >800, however they don't have TANG and SKIN.
if (version > 800) {
if (stream.peek(4) === 'TANG') {
stream.skip(4); // TANG
this.tangents = stream.readFloat32Array(stream.readUint32() * 4);
}
if (stream.peek(4) === 'SKIN') {
stream.skip(4); // SKIN
this.skin = stream.readUint8Array(stream.readUint32());
}
}
stream.skip(4); // UVAS
for (let i = 0, l = stream.readUint32(); i < l; i++) {
stream.skip(4); // UVBS
this.uvSets.push(stream.readFloat32Array(stream.readUint32() * 2));
}
}
writeMdx(stream: BinaryStream, version: number) {
stream.writeUint32(this.getByteLength(version));
stream.write('VRTX');
stream.writeUint32(this.vertices.length / 3);
stream.writeFloat32Array(this.vertices);
stream.write('NRMS');
stream.writeUint32(this.normals.length / 3);
stream.writeFloat32Array(this.normals);
stream.write('PTYP');
stream.writeUint32(this.faceTypeGroups.length);
stream.writeUint32Array(this.faceTypeGroups);
stream.write('PCNT');
stream.writeUint32(this.faceGroups.length);
stream.writeUint32Array(this.faceGroups);
stream.write('PVTX');
stream.writeUint32(this.faces.length);
stream.writeUint16Array(this.faces);
stream.write('GNDX');
stream.writeUint32(this.vertexGroups.length);
stream.writeUint8Array(this.vertexGroups);
stream.write('MTGC');
stream.writeUint32(this.matrixGroups.length);
stream.writeUint32Array(this.matrixGroups);
stream.write('MATS');
stream.writeUint32(this.matrixIndices.length);
stream.writeUint32Array(this.matrixIndices);
stream.writeUint32(this.materialId);
stream.writeUint32(this.selectionGroup);
stream.writeUint32(this.selectionFlags);
if (version > 800) {
stream.writeInt32(this.lod);
stream.write(this.lodName);
stream.skip(80 - this.lodName.length);
}
this.extent.writeMdx(stream);
stream.writeUint32(this.sequenceExtents.length);
for (let sequenceExtent of this.sequenceExtents) {
sequenceExtent.writeMdx(stream);
}
if (version > 800) {
if (this.tangents.length) {
stream.write('TANG');
stream.writeUint32(this.tangents.length / 4);
stream.writeFloat32Array(this.tangents);
}
if (this.skin.length) {
stream.write('SKIN');
stream.writeUint32(this.skin.length);
stream.writeUint8Array(this.skin);
}
}
stream.write('UVAS');
stream.writeUint32(this.uvSets.length);
for (let uvSet of this.uvSets) {
stream.write('UVBS');
stream.writeUint32(uvSet.length / 2);
stream.writeFloat32Array(uvSet);
}
}
readMdl(stream: TokenStream) {
for (let token of stream.readBlock()) {
if (token === 'Vertices') {
this.vertices = <Float32Array>stream.readVectorsBlock(new Float32Array(stream.readInt() * 3), 3);
} else if (token === 'Normals') {
this.normals = <Float32Array>stream.readVectorsBlock(new Float32Array(stream.readInt() * 3), 3);
} else if (token === 'TVertices') {
this.uvSets.push(<Float32Array>stream.readVectorsBlock(new Float32Array(stream.readInt() * 2), 2));
} else if (token === 'VertexGroup') {
// Vertex groups are stored in a block with no count, can't allocate the buffer yet.
let vertexGroups = [];
for (let vertexGroup of stream.readBlock()) {
vertexGroups.push(parseInt(vertexGroup));
}
this.vertexGroups = new Uint8Array(vertexGroups);
} else if (token === 'Tangents') {
this.tangents = <Float32Array>stream.readVectorsBlock(new Float32Array(stream.readInt() * 4), 4);
} else if (token === 'SkinWeights') {
this.skin = <Uint8Array>stream.readVector(new Uint8Array(stream.readInt() * 8));
} else if (token === 'Faces') {
// For now hardcoded for triangles, until I see a model with something different.
this.faceTypeGroups = new Uint32Array([4]);
// Number of vectors the indices are spread over.
let vectors = stream.readInt();
// Total number of indices.
let count = stream.readInt();
stream.read(); // {
stream.read(); // Triangles
this.faces = <Uint16Array>stream.readVectorsBlock(new Uint16Array(count), count / vectors);
// Declare that all of the faces are in one group to conform with MDX.
this.faceGroups = new Uint32Array([count]);
stream.read(); // }
} else if (token === 'Groups') {
let indices = [];
let groups = [];
stream.readInt(); // matrices count
stream.readInt(); // total indices
for (let _ of stream.readBlock()) {
let size = 0;
for (let index of stream.readBlock()) {
indices.push(parseInt(index));
size += 1;
}
groups.push(size);
}
this.matrixIndices = new Uint32Array(indices);
this.matrixGroups = new Uint32Array(groups);
} else if (token === 'MinimumExtent') {
stream.readVector(this.extent.min);
} else if (token === 'MaximumExtent') {
stream.readVector(this.extent.max);
} else if (token === 'BoundsRadius') {
this.extent.boundsRadius = stream.readFloat();
} else if (token === 'Anim') {
let extent = new Extent();
for (token of stream.readBlock()) {
if (token === 'MinimumExtent') {
stream.readVector(extent.min);
} else if (token === 'MaximumExtent') {
stream.readVector(extent.max);
} else if (token === 'BoundsRadius') {
extent.boundsRadius = stream.readFloat();
}
}
this.sequenceExtents.push(extent);
} else if (token === 'MaterialID') {
this.materialId = stream.readInt();
} else if (token === 'SelectionGroup') {
this.selectionGroup = stream.readInt();
} else if (token === 'Unselectable') {
this.selectionFlags = 4;
} else if (token === 'LevelOfDetail') {
this.lod = stream.readInt();
} else if (token === 'Name') {
this.lodName = stream.read();
} else {
throw new Error(`Unknown token in Geoset: "${token}"`);
}
}
}
writeMdl(stream: TokenStream, version: number) {
stream.startBlock('Geoset');
stream.writeVectorArrayBlock('Vertices', this.vertices, 3);
stream.writeVectorArrayBlock('Normals', this.normals, 3);
for (let uvSet of this.uvSets) {
stream.writeVectorArrayBlock('TVertices', uvSet, 2);
}
if (version > 800 && this.tangents.length) {
stream.writeVectorArrayBlock('Tangents', this.tangents, 4);
}
// If this is a Reforged HD geoset, write the skin.
// Otherwise, write the vertex groups, which are used for both TFT and Reforged SD geosets.
if (version > 800 && this.skin.length) {
stream.startBlock('SkinWeights', this.skin.length / 8);
for (let i = 0, l = this.skin.length; i < l; i += 8) {
stream.writeLine(`${this.skin.subarray(i, i + 8).join(', ')},`);
}
} else {
stream.startBlock('VertexGroup');
for (let i = 0, l = this.vertexGroups.length; i < l; i++) {
stream.writeLine(`${this.vertexGroups[i]},`);
}
}
// End the vertex group or the skin weight block.
stream.endBlock();
// For now hardcoded for triangles, until I see a model with something different.
stream.startBlock('Faces', 1, this.faces.length);
stream.startBlock('Triangles');
stream.writeVector(this.faces);
stream.endBlock();
stream.endBlock();
stream.startBlock('Groups', this.matrixGroups.length, this.matrixIndices.length);
let index = 0;
for (let groupSize of this.matrixGroups) {
stream.writeVectorAttrib('Matrices', this.matrixIndices.subarray(index, index + groupSize));
index += groupSize;
}
stream.endBlock();
this.extent.writeMdl(stream);
for (let sequenceExtent of this.sequenceExtents) {
stream.startBlock('Anim');
sequenceExtent.writeMdl(stream);
stream.endBlock();
}
stream.writeNumberAttrib('MaterialID', this.materialId);
stream.writeNumberAttrib('SelectionGroup', this.selectionGroup);
if (this.selectionFlags === 4) {
stream.writeFlag('Unselectable');
}
if (version > 800) {
stream.writeNumberAttrib('LevelOfDetail', this.lod);
if (this.lodName.length) {
stream.writeStringAttrib('Name', this.lodName);
}
}
stream.endBlock();
}
getByteLength(version: number) {
let size = 120 + this.vertices.byteLength + this.normals.byteLength + this.faceTypeGroups.byteLength + this.faceGroups.byteLength + this.faces.byteLength + this.vertexGroups.byteLength + this.matrixGroups.byteLength + this.matrixIndices.byteLength + this.sequenceExtents.length * 28;
if (version > 800) {
size += 84;
if (this.tangents.length) {
size += 8 + this.tangents.byteLength;
}
if (this.skin.length) {
size += 8 + this.skin.byteLength;
}
}
for (let uvSet of this.uvSets) {
size += 8 + uvSet.byteLength;
}
return size;
}
}