UNPKG

mdx-m3-viewer

Version:

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

430 lines (375 loc) 12.3 kB
import unique from '../../common/arrayunique'; import { Animation } from '../../parsers/mdlx/animations'; import AnimatedObject from '../../parsers/mdlx/animatedobject'; import GenericObject from '../../parsers/mdlx/genericobject'; import Sequence from '../../parsers/mdlx/sequence'; import Texture from '../../parsers/mdlx/texture'; import Material from '../../parsers/mdlx/material'; import Layer from '../../parsers/mdlx/layer'; import TextureAnimation from '../../parsers/mdlx/textureanimation'; import Geoset from '../../parsers/mdlx/geoset'; import GeosetAnimation from '../../parsers/mdlx/geosetanimation'; import Bone from '../../parsers/mdlx/bone'; import Light from '../../parsers/mdlx/light'; import Helper from '../../parsers/mdlx/helper'; import Attachment from '../../parsers/mdlx/attachment'; import ParticleEmitter from '../../parsers/mdlx/particleemitter'; import ParticleEmitter2 from '../../parsers/mdlx/particleemitter2'; import ParticleEmitterPopcorn from '../../parsers/mdlx/particleemitterpopcorn'; import RibbonEmitter from '../../parsers/mdlx/ribbonemitter'; import EventObject from '../../parsers/mdlx/eventobject'; import Camera from '../../parsers/mdlx/camera'; import CollisionShape from '../../parsers/mdlx/collisionshape'; import SanityTestData from './data'; import testTracks from './tracks'; import { SanityTestNode } from './data'; export const sequenceNames = new Set([ 'attack', 'birth', 'cinematic', 'death', 'decay', 'dissipate', 'morph', 'portrait', 'sleep', 'spell', 'stand', 'walk', ]); export const replaceableIds = new Set([ 1, 2, 11, 21, 31, 32, 33, 34, 35, 36, 37, ]); export const animatedTypeNames = new Map([ // Layer ['KMTF', 'Texture ID'], ['KMTA', 'Alpha'], ['KMTE', 'Emissive Gain'], ['KFC3', 'Fresnel Color'], ['KFCA', 'Fresnel Opacity'], ['KFTC', 'Fresnel Team Color'], // TextureAnimation ['KTAT', 'Translation'], ['KTAR', 'Rotation'], ['KTAS', 'Scaling'], // GeosetAnimation ['KGAO', 'Alpha'], ['KGAC', 'Color'], // GenericObject ['KGTR', 'Translation'], ['KGRT', 'Rotation'], ['KGSC', 'Scaling'], // Light ['KLAS', 'Attenuation Start'], ['KLAE', 'Attenuation End'], ['KLAC', 'Color'], ['KLAI', 'Intensity'], ['KLBI', 'Ambient Intensity'], ['KLBC', 'Ambient Color'], ['KLAV', 'Visibility'], // Attachment ['KATV', 'Visibility'], // ParticleEmitter ['KPEE', 'Emission Rate'], ['KPEG', 'Gravity'], ['KPLN', 'Longitude'], ['KPLT', 'Latitude'], ['KPEL', 'Lifespan'], ['KPES', 'Speed'], ['KPEV', 'Visibility'], // ParticleEmitter2 ['KP2E', 'Emission Rate'], ['KP2G', 'Gravity'], ['KP2L', 'Latitude'], ['KP2R', 'Variation'], ['KP2N', 'Length'], ['KP2W', 'Width'], ['KP2S', 'Speed'], ['KP2V', 'Visibility'], // ParticleEmitterCorn ['KPPA', 'Alpha'], ['KPPC', 'Color'], ['KPPE', 'EmissionRate'], ['KPPL', 'LifeSpan'], ['KPPS', 'Speed'], ['KPPV', 'Visibility'], // RibbonEmitter ['KRHA', 'Height Above'], ['KRHB', 'Height Below'], ['KRAL', 'Alpha'], ['KRCO', 'Color'], ['KRTX', 'Texture Slot'], ['KRVS', 'Visibility'], // Camera ['KCTR', 'Translation'], ['KTTR', 'Rotation'], ['KCRL', 'Target Translation'], ]); export type MdlxType = Sequence | number | Texture | Material | Layer | TextureAnimation | Geoset | GeosetAnimation | Bone | Light | Helper | Attachment | ParticleEmitter | ParticleEmitter2 | ParticleEmitterPopcorn | RibbonEmitter | EventObject | Camera | CollisionShape | Animation; export function getObjectTypeName(object: MdlxType) { if (object instanceof Sequence) { return 'Sequence'; } else if (typeof object === 'number') { return 'GlobalSequence'; } else if (object instanceof Texture) { return 'Texture'; } else if (object instanceof Material) { return 'Material'; } else if (object instanceof Layer) { return 'Layer'; } else if (object instanceof TextureAnimation) { return 'TextureAnimation'; } else if (object instanceof Geoset) { return 'Geoset'; } else if (object instanceof GeosetAnimation) { return 'GeosetAnimation'; } else if (object instanceof Bone) { return 'Bone'; } else if (object instanceof Light) { return 'Light'; } else if (object instanceof Helper) { return 'Helper'; } else if (object instanceof Attachment) { return 'Attachment'; } else if (object instanceof ParticleEmitter) { return 'ParticleEmitter'; } else if (object instanceof ParticleEmitter2) { return 'ParticleEmitter2'; } else if (object instanceof ParticleEmitterPopcorn) { return 'ParticleEmitterPopcorn'; } else if (object instanceof RibbonEmitter) { return 'RibbonEmitter'; } else if (object instanceof EventObject) { return 'EventObject'; } else if (object instanceof Camera) { return 'Camera'; } else if (object instanceof CollisionShape) { return 'CollisionShape'; } else if (object instanceof Animation) { return <string>animatedTypeNames.get(object.name); } else { console.warn('Unknown object type', object); return 'Unknown'; } } export function testObjects(data: SanityTestData, objects: MdlxType[], handler?: (data: SanityTestData, object: any, index: number) => void) { let l = objects.length; if (l) { let isAnimated = objects[0] instanceof AnimatedObject; let isGeneric = objects[0] instanceof GenericObject; for (let i = 0; i < l; i++) { let object = objects[i]; data.push(object, i); if (handler) { handler(data, object, i); } if (isAnimated) { let asAnimated = <AnimatedObject>object; testObjects(data, asAnimated.animations, testAnimation); } if (isGeneric) { let asGeneric = <GenericObject>object; let objectId = asGeneric.objectId; let parentId = asGeneric.parentId; data.assertError(parentId === -1 || hasGenericObject(data, parentId), `Invalid parent ${parentId}`); data.assertError(objectId !== parentId, 'Same object and parent'); } data.pop(); } } } export function testReference(data: SanityTestData, objects: MdlxType[], index: number, typeNameIfError: string) { if (index >= 0 && index < objects.length) { data.addReference(objects[index]); } else { data.addError(`Invalid ${typeNameIfError} ${index}`); } } /** * Get all of the texture indices referenced by a layer. */ export function getTextureIds(layer: Layer) { for (let animation of layer.animations) { if (animation.name === 'KMTF') { return unique(animation.values.map((value) => value[0])); } } return [layer.textureId]; } function testVertexSkinning(data: SanityTestData, vertex: number, bone: number, geoset: number) { let object = data.objects[bone]; if (object) { if (!(object instanceof Bone)) { data.addSevere(`Vertex ${vertex}: Attached to "${object.name}" which is not a bone`); } } else { data.addError(`Vertex ${vertex}: Attached to object ${bone} which does not exist`); } } /** * Test geoset skinning. */ export function testGeosetSkinning(data: SanityTestData, geoset: Geoset, index: number) { let vertexGroups = geoset.vertexGroups; let matrixGroups = geoset.matrixGroups; let matrixIndices = geoset.matrixIndices; let slices = []; for (let i = 0, l = matrixGroups.length, k = 0; i < l; i++) { slices.push(matrixIndices.subarray(k, k + matrixGroups[i])); k += matrixGroups[i]; } for (let i = 0, l = vertexGroups.length; i < l; i++) { let slice = slices[vertexGroups[i]]; if (slice) { for (let bone of slice) { testVertexSkinning(data, i, bone, index); } } else { let vertexGroup = vertexGroups[i]; if (vertexGroup === 255) { data.addSevere(`Vertex ${i}: Not attached to anything`); } else { data.addSevere(`Vertex ${i}: Attached to vertex group ${vertexGroup} which does not exist`); } } } if (data.model.version > 800 && geoset.skin.length) { let skin = geoset.skin; for (let i = 0, l = skin.length / 8; i < l; i++) { let offset = i * 8; let bone0 = skin[offset]; let bone1 = skin[offset + 1]; let bone2 = skin[offset + 2]; let bone3 = skin[offset + 3]; let weight0 = skin[offset + 4]; let weight1 = skin[offset + 5]; let weight2 = skin[offset + 6]; let weight3 = skin[offset + 7]; if (weight0 > 0) { testVertexSkinning(data, i, bone0, index); } if (weight1 > 0) { testVertexSkinning(data, i, bone1, index); } if (weight2 > 0) { testVertexSkinning(data, i, bone2, index); } if (weight3 > 0) { testVertexSkinning(data, i, bone3, index); } let weight = weight0 + weight1 + weight2 + weight3; if (weight === 0) { data.addSevere(`Vertex ${i}: Not attached to anything`); } else if (weight !== 255) { data.addSevere(`Vertex ${i}: The weights are not normalized to 1`); } } } } /** * Is the given ID a valid generic object? */ function hasGenericObject(data: SanityTestData, id: number) { for (let object of data.objects) { if (object.objectId === id) { return true; } } return false; } export function testAnimation(data: SanityTestData, animation: Animation) { let name = animation.name; let interpolationType = animation.interpolationType; let globalSequenceId = animation.globalSequenceId; let frames = animation.frames; if (globalSequenceId !== -1) { testReference(data, data.model.globalSequences, globalSequenceId, 'global sequence'); } // Particle emitter 2 variation animations are not implemented in Magos for the MDX format. data.assertWarning(name !== 'KP2R', 'Using a variation animation.'); // Particle emitter 2 gravity animations are not implemented in Magos for the MDX format. data.assertWarning(name !== 'KP2G', 'Using a gravity animation.'); // The game seems to force visiblity (and others?) interpolation types to none. data.assertWarning(animatedTypeNames.get(name) !== 'Visibility' || interpolationType === 0, 'Interpolation type not set to None'); data.assertWarning(frames.length > 0, 'Zero tracks'); testTracks(data, animation); } export function cleanNode(node: SanityTestNode) { let nodes = node.nodes; for (let i = nodes.length - 1; i >= 0; i--) { let child = nodes[i]; if (child.type === 'node') { if (child.errors || child.severe || child.warnings || child.unused || !child.uses) { cleanNode(child); } else { nodes.splice(i, 1); } } } } /* let attachmentNames = new Set([ 'chest', 'feet', 'foot', 'hand', 'head', 'origin', 'overhead', 'sprite', 'weapon', ]); let attachmentQualifiers = new Set([ 'alternate', 'left', 'mount', 'right', 'rear', 'smart', 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'small', 'medium', 'large', 'gold', 'rallypoint', 'eattree', ]); function testAttachmentName(attachment) { let tokens = attachment.node.name.toLowerCase().trim().split(/\s+/), valid = true; if (tokens.length > 1) { let names = attachmentNames, firstToken = tokens[0], lastToken = tokens[tokens.length - 1]; if (!names.has(tokens[0]) || lastToken !== 'ref') { valid = false; } if (tokens.length > 2) { let qualifiers = attachmentQualifiers; for (let i = 1, l = tokens.length - 1; i < l; i++) { if (!qualifiers.has(tokens[i])) { valid = false; } } } } else { valid = false; } return valid; } */