UNPKG

mdx-m3-viewer

Version:

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

500 lines (493 loc) 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.testExtent = exports.getAnimation = exports.cleanNode = exports.testAnimation = exports.testGeosetSkinning = exports.getTextureIds = exports.testReference = exports.testAndGetReference = exports.testObjects = exports.getObjectName = exports.getObjectTypeName = exports.hasAnimation = exports.animatedTypeNames = exports.replaceableIds = exports.sequenceNames = exports.isBetween = void 0; const arrayunique_1 = require("../../../common/arrayunique"); const path_1 = require("../../../common/path"); const animations_1 = require("../../../parsers/mdlx/animations"); const animatedobject_1 = require("../../../parsers/mdlx/animatedobject"); const genericobject_1 = require("../../../parsers/mdlx/genericobject"); const extent_1 = require("../../../parsers/mdlx/extent"); const sequence_1 = require("../../../parsers/mdlx/sequence"); const texture_1 = require("../../../parsers/mdlx/texture"); const material_1 = require("../../../parsers/mdlx/material"); const layer_1 = require("../../../parsers/mdlx/layer"); const textureanimation_1 = require("../../../parsers/mdlx/textureanimation"); const geoset_1 = require("../../../parsers/mdlx/geoset"); const geosetanimation_1 = require("../../../parsers/mdlx/geosetanimation"); const bone_1 = require("../../../parsers/mdlx/bone"); const light_1 = require("../../../parsers/mdlx/light"); const helper_1 = require("../../../parsers/mdlx/helper"); const attachment_1 = require("../../../parsers/mdlx/attachment"); const particleemitter_1 = require("../../../parsers/mdlx/particleemitter"); const particleemitter2_1 = require("../../../parsers/mdlx/particleemitter2"); const particleemitterpopcorn_1 = require("../../../parsers/mdlx/particleemitterpopcorn"); const ribbonemitter_1 = require("../../../parsers/mdlx/ribbonemitter"); const eventobject_1 = require("../../../parsers/mdlx/eventobject"); const camera_1 = require("../../../parsers/mdlx/camera"); const collisionshape_1 = require("../../../parsers/mdlx/collisionshape"); const faceeffect_1 = require("../../../parsers/mdlx/faceeffect"); const tracks_1 = require("./tracks"); function isBetween(x, minVal, maxVal) { return x >= minVal && x <= maxVal; } exports.isBetween = isBetween; exports.sequenceNames = new Set([ 'attack', 'birth', 'cinematic', 'death', 'decay', 'dissipate', 'morph', 'portrait', 'sleep', 'spell', 'stand', 'walk', 'ready', // HeroMountainKing.mdx has "Alternate Ready" which I assume is the same as or a replacement to "Alternate Stand Ready"? ]); exports.replaceableIds = new Set([ 1, 2, 11, 21, 31, 32, 33, 34, 35, 36, 37, ]); exports.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'], ]); function hasAnimation(object, name) { for (const animation of object.animations) { if (animation.name === name) { return true; } } return false; } exports.hasAnimation = hasAnimation; function getObjectTypeName(object) { if (object instanceof extent_1.default) { return 'Extent'; } else if (object instanceof sequence_1.default) { return 'Sequence'; } else if (typeof object === 'number') { return 'GlobalSequence'; } else if (object instanceof texture_1.default) { return 'Texture'; } else if (object instanceof material_1.default) { return 'Material'; } else if (object instanceof layer_1.default) { return 'Layer'; } else if (object instanceof textureanimation_1.default) { return 'TextureAnimation'; } else if (object instanceof geoset_1.default) { return 'Geoset'; } else if (object instanceof geosetanimation_1.default) { return 'GeosetAnimation'; } else if (object instanceof bone_1.default) { return 'Bone'; } else if (object instanceof light_1.default) { return 'Light'; } else if (object instanceof helper_1.default) { return 'Helper'; } else if (object instanceof attachment_1.default) { return 'Attachment'; } else if (object instanceof particleemitter_1.default) { return 'ParticleEmitter'; } else if (object instanceof particleemitter2_1.default) { return 'ParticleEmitter2'; } else if (object instanceof particleemitterpopcorn_1.default) { return 'ParticleEmitterPopcorn'; } else if (object instanceof ribbonemitter_1.default) { return 'RibbonEmitter'; } else if (object instanceof eventobject_1.default) { return 'EventObject'; } else if (object instanceof camera_1.default) { return 'Camera'; } else if (object instanceof collisionshape_1.default) { return 'CollisionShape'; } else if (object instanceof faceeffect_1.default) { return 'FaceEffect'; } else if (object instanceof animations_1.Animation) { return exports.animatedTypeNames.get(object.name); } else { console.warn('Unknown object type', object); return 'Unknown'; } } exports.getObjectTypeName = getObjectTypeName; function getObjectName(object, index) { let name = getObjectTypeName(object); if (!(object instanceof animations_1.Animation) && !(object instanceof extent_1.default)) { name += ` ${index}`; } if (object instanceof sequence_1.default || object instanceof genericobject_1.default || object instanceof camera_1.default) { name += ` - "${object.name}"`; } if (object instanceof texture_1.default || object instanceof faceeffect_1.default) { if (object.path.length) { name += ` - "${(0, path_1.basename)(object.path)}"`; } if (object instanceof texture_1.default) { if (object.replaceableId === 1) { name += ' - Team color'; } else if (object.replaceableId === 2) { name += ' - Team glow'; } else if (object.replaceableId > 0) { name += ` - Replaceable ID ${object.replaceableId}`; } } } return name; } exports.getObjectName = getObjectName; function testObjects(data, objects, handler) { const l = objects.length; if (l) { const isAnimated = objects[0] instanceof animatedobject_1.default; const isGeneric = objects[0] instanceof genericobject_1.default; for (let i = 0; i < l; i++) { const object = objects[i]; data.push(object, i); if (handler) { handler(data, object, i); } if (isAnimated) { const asAnimated = object; testObjects(data, asAnimated.animations, testAnimation); } if (isGeneric) { const asGeneric = object; const objectId = asGeneric.objectId; const parentId = asGeneric.parentId; data.assertError(parentId === -1 || hasGenericObject(data, parentId), `Invalid parent ${parentId}`); data.assertError(objectId !== parentId, 'Same object and parent'); } data.pop(); } } } exports.testObjects = testObjects; function testAndGetReference(data, objects, index, typeNameIfError) { if (index >= 0 && index < objects.length) { data.addReference(objects[index]); return objects[index]; } else { data.addError(`Invalid ${typeNameIfError} ${index}`); return; } } exports.testAndGetReference = testAndGetReference; function testReference(data, objects, index, typeNameIfError) { // This explicit test against undefined is needed because global sequences are numbers and could be equal to 0. return testAndGetReference(data, objects, index, typeNameIfError) !== undefined; } exports.testReference = testReference; /** * Get all of the texture indices referenced by a layer. */ function getTextureIds(layer) { for (const animation of layer.animations) { if (animation.name === 'KMTF') { return (0, arrayunique_1.default)(animation.values.map((value) => value[0])); } } return [layer.textureId]; } exports.getTextureIds = getTextureIds; function testVertexSkinning(data, vertex, bone, isHd) { const object = data.objects[bone]; if (isHd) { data.assertError(isBetween(bone, 0, 255), `Vertex ${vertex}: References bone ${bone} but there can only be 256 bones in an HD model`); } if (object) { if (!(object instanceof bone_1.default)) { data.addSevere(`Vertex ${vertex}: Attached to "${object.name}" which is not a bone`); } else { // Add a use for this bone, to check later if bones have vertices attached to them. const uses = data.boneUsageMap.get(bone) || 0; data.boneUsageMap.set(bone, uses + 1); } } else { data.addError(`Vertex ${vertex}: Attached to object ${bone} which does not exist`); } } /** * Test geoset skinning. */ function testGeosetSkinning(data, geoset) { if (data.model.version > 800 && geoset.skin.length) { data.assertWarning(geoset.vertexGroups.length === 0, 'This geoset has both skin/weights and vertex groups'); const skin = geoset.skin; for (let i = 0, l = skin.length / 8; i < l; i++) { const offset = i * 8; const bone0 = skin[offset]; const bone1 = skin[offset + 1]; const bone2 = skin[offset + 2]; const bone3 = skin[offset + 3]; const weight0 = skin[offset + 4]; const weight1 = skin[offset + 5]; const weight2 = skin[offset + 6]; const weight3 = skin[offset + 7]; if (weight0 > 0) { testVertexSkinning(data, i, bone0, true); } if (weight1 > 0) { testVertexSkinning(data, i, bone1, true); } if (weight2 > 0) { testVertexSkinning(data, i, bone2, true); } if (weight3 > 0) { testVertexSkinning(data, i, bone3, true); } const 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`); } } } else { // A model having no bones at all is also valid. // I don't know if the skinning information in that case can be anything whatsoever, or if there are rules. if (data.model.bones.length) { const vertexGroups = geoset.vertexGroups; const matrixGroups = geoset.matrixGroups; const matrixIndices = geoset.matrixIndices; const 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++) { const slice = slices[vertexGroups[i]]; if (slice) { for (const bone of slice) { testVertexSkinning(data, i, bone, false); } } else { const 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`); } } } } } } exports.testGeosetSkinning = testGeosetSkinning; /** * Is the given ID a valid generic object? */ function hasGenericObject(data, id) { for (const object of data.objects) { if (object.objectId === id) { return true; } } return false; } function testAnimation(data, animation) { const name = animation.name; const interpolationType = animation.interpolationType; // 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(exports.animatedTypeNames.get(name) !== 'Visibility' || interpolationType === 0 /* DontInterp */, 'Interpolation type not set to None'); (0, tracks_1.default)(data, animation); } exports.testAnimation = testAnimation; function cleanNode(node) { const nodes = node.nodes; for (let i = nodes.length - 1; i >= 0; i--) { const child = nodes[i]; if (child.type === 'node') { if (child.errors || child.severe || child.warnings || child.unused || (child.uses !== undefined && !child.uses)) { cleanNode(child); } else { nodes.splice(i, 1); } } } } exports.cleanNode = cleanNode; function getAnimation(object, name) { for (const animation of object.animations) { if (animation.name === name) { return animation; } } return; } exports.getAnimation = getAnimation; function testExtent(data, extent) { data.push(extent, 0); const { max, min } = extent; if ((max[0] - min[0] < 0) || (max[1] - min[1] < 0) || (max[2] - min[2] < 0)) { data.addWarning('Negative extents'); } data.pop(); } exports.testExtent = testExtent; /* 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; } */ //# sourceMappingURL=utils.js.map