UNPKG

mdx-m3-viewer

Version:

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

318 lines (246 loc) 11.2 kB
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 Geoset from '../../parsers/mdlx/geoset'; import GeosetAnimation from '../../parsers/mdlx/geosetanimation'; import Bone from '../../parsers/mdlx/bone'; import Light from '../../parsers/mdlx/light'; 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 SanityTestData from './data'; import { sequenceNames, replaceableIds, testObjects, testReference, getTextureIds, testGeosetSkinning, testAnimation } from './utils'; import testTracks from './tracks'; export function testHeader(data: SanityTestData) { let version = data.model.version; if (version !== 800 && version !== 900 && version !== 1000) { data.addWarning(`Unknown version: ${version}`) } if (version === 900) { data.addError('Version 900 is not supported by Warcrft 3') } if (data.model.animationFile !== '') { data.addWarning(`The animation file should probably be empty, currently set to: "${data.model.animationFile}"`); } } export function testSequences(data: SanityTestData) { let sequences = data.model.sequences; if (sequences.length) { testObjects(data, sequences, testSequence); data.assertSevere(data.foundStand, 'Missing "Stand" sequence'); data.assertSevere(data.foundDeath, 'Missing "Death" sequence'); } else { data.addWarning('No sequences'); } } function testSequence(data: SanityTestData, sequence: Sequence) { let name = sequence.name; let tokens = name.toLowerCase().trim().split('-')[0].split(/\s+/); let token = tokens[0]; let interval = sequence.interval; let length = interval[1] - interval[0]; let sequences = data.model.sequences; let index = sequences.indexOf(sequence); for (let i = 0; i < index; i++) { let otherSequence = sequences[i]; let otherInterval = otherSequence.interval; if (interval[0] < otherInterval[1]) { data.addSevere(`This sequence starts before sequence ${i} "${otherSequence.name}" ends`); } } if (token === 'alternate') { token = tokens[1]; } if (token === 'stand') { data.foundStand = true; } if (token === 'death') { data.foundDeath = true; } if (sequenceNames.has(token)) { data.addImplicitReference(); } else { data.addWarning(`Unknown sequence: "${token}"`); } data.assertWarning(length !== 0, 'Zero length'); data.assertWarning(length > -1, `Negative length: ${length}`); } export function testGlobalSequence(data: SanityTestData, sequence: number) { data.assertWarning(sequence !== 0, 'Zero length'); data.assertWarning(sequence >= 0, `Negative length: ${sequence}`); } export function testTextures(data: SanityTestData) { let textures = data.model.textures; if (textures.length) { testObjects(data, textures, testTexture); } else { data.addWarning('No textures'); } } function testTexture(data: SanityTestData, texture: Texture) { let replaceableId = texture.replaceableId; let path = texture.path.toLowerCase(); let ext = path.slice(path.lastIndexOf('.')); data.assertError(path === '' || ext === '.blp' || ext === '.tga' || ext === '.tif', `Corrupted path: "${path}"`); data.assertError(replaceableId === 0 || replaceableIds.has(replaceableId), `Unknown replaceable ID: ${replaceableId}`); data.assertWarning(path === '' || replaceableId === 0, `Path "${path}" and replaceable ID ${replaceableId} used together`); } export function testMaterials(data: SanityTestData) { let materials = data.model.materials; if (materials.length) { testObjects(data, materials, testMaterial); } else { data.addWarning('No materials'); } } function testMaterial(data: SanityTestData, material: Material) { let layers = material.layers; let shader = material.shader; if (data.model.version > 800) { data.assertWarning(shader === 'Shader_SD_FixedFunction' || shader === 'Shader_HD_DefaultUnit', `Unknown shader: "${shader}"`); } if (layers.length) { testObjects(data, layers, testLayer); } else { data.addWarning('No layers'); } } function testLayer(data: SanityTestData, layer: Layer) { let textures = data.model.textures; let textureAnimations = data.model.textureAnimations; for (let textureId of getTextureIds(layer)) { testReference(data, textures, textureId, 'texture'); } let textureAnimationId = layer.textureAnimationId; if (textureAnimationId !== -1) { testReference(data, textureAnimations, textureAnimationId, 'texture animation'); } let filterMode = layer.filterMode; data.assertWarning(filterMode >= 0 && filterMode <= 6, `Invalid filter mode: ${layer.filterMode}`); } export function testGeoset(data: SanityTestData, geoset: Geoset, index: number) { let geosetAnimations = data.model.geosetAnimations; let materialId = geoset.materialId; data.assertSevere(geoset.vertices.length < 65536, `Too many vertices in one geoset: ${geoset.vertices.length}`); testGeosetSkinning(data, geoset, index); if (geosetAnimations.length) { let references = []; for (let j = 0, k = geosetAnimations.length; j < k; j++) { if (geosetAnimations[j].geosetId === index) { references.push(j); } } data.assertWarning(references.length <= 1, `Referenced by ${references.length} geoset animations: ${references.join(', ')}`); } testReference(data, data.model.materials, materialId, 'material'); if (geoset.faces.length) { data.addImplicitReference(); } else { // The game and my code have no issue with geosets containing no faces, but Magos crashes, so add a warning in addition to it being useless. data.addWarning('Zero faces'); } // The game and my code have no issue with geosets having any number of sequence extents, but Magos fails to parse, so add a warning. if (geoset.sequenceExtents.length !== data.model.sequences.length) { data.addWarning(`Number of sequence extents (${geoset.sequenceExtents.length}) does not match the number of sequences (${data.model.sequences.length})`); } } export function testGeosetAnimation(data: SanityTestData, geosetAnimation: GeosetAnimation) { let geosets = data.model.geosets; let geosetId = geosetAnimation.geosetId; data.addImplicitReference(); testReference(data, geosets, geosetId, 'geoset'); } export function testBones(data: SanityTestData) { let bones = data.model.bones; if (bones.length) { testObjects(data, bones, testBone); } else { data.addWarning('No bones'); } } export function testBone(data: SanityTestData, bone: Bone) { let geosets = data.model.geosets; let geosetAnimations = data.model.geosetAnimations; let geosetId = bone.geosetId; let geosetAnimationId = bone.geosetAnimationId; if (geosetId !== -1) { testReference(data, geosets, geosetId, 'geoset'); } if (geosetAnimationId !== -1) { testReference(data, geosetAnimations, geosetAnimationId, 'geoset animation'); } } export function testLight(data: SanityTestData, light: Light) { let attenuation = light.attenuation; data.assertWarning(attenuation[0] >= 80, `Minimum attenuation should probably be bigger than or equal to 80, but is ${attenuation[0]}`); data.assertWarning(attenuation[1] <= 200, `Maximum attenuation should probably be smaller than or equal to 200, but is ${attenuation[0]}`); data.assertWarning(attenuation[1] - attenuation[0] > 0, `The maximum attenuation should be bigger than the minimum, but isn't`); } export function testAttachment(data: SanityTestData, attachment: Attachment) { // NOTE: I can't figure out what exactly the rules for attachment names even are. /* let path = attachment.path; if (path === '') { assertWarning(data, testAttachmentName(attachment), `${objectName}: Invalid attachment "${attachment.node.name}"`); } else { let lowerCase = path.toLowerCase(); assertError(data, lowerCase.endsWith('.mdl') || lowerCase.endsWith('.mdx'), `${objectName}: Invalid path "${path}"`); } */ } export function testPivotPoints(data: SanityTestData) { let pivotPoints = data.model.pivotPoints; let objects = data.objects; data.assertWarning(pivotPoints.length === objects.length, `Expected ${objects.length} pivot points, got ${pivotPoints.length}`); } export function testParticleEmitter(data: SanityTestData, emitter: ParticleEmitter) { data.assertError(emitter.path.toLowerCase().endsWith('.mdl'), 'Invalid path'); } export function testParticleEmitter2(data: SanityTestData, emitter: ParticleEmitter2) { let replaceableId = emitter.replaceableId; testReference(data, data.model.textures, emitter.textureId, 'texture'); let filterMode = emitter.filterMode; data.assertWarning(filterMode >= 0 && filterMode <= 4, `Invalid filter mode: ${emitter.filterMode}`); data.assertError(replaceableId === 0 || replaceableIds.has(replaceableId), `Invalid replaceable ID: ${replaceableId}`); } export function testParticleEmitterPopcorn(data: SanityTestData, emitter: ParticleEmitterPopcorn) { let path = emitter.path; if (path.length) { data.assertError(path.endsWith('.pkfx'), `Corrupted path: "${path}"`); } } export function testRibbonEmitter(data: SanityTestData, emitter: RibbonEmitter) { testReference(data, data.model.materials, emitter.materialId, 'material'); } export function testEventObject(data: SanityTestData, eventObject: EventObject) { let globalSequenceId = eventObject.globalSequenceId; if (globalSequenceId !== -1) { testReference(data, data.model.globalSequences, globalSequenceId, 'global sequence'); } if (eventObject.tracks.length) { testTracks(data, eventObject); } else { data.addError('Zero keys'); } } export function testCamera(data: SanityTestData, camera: Camera) { // I don't know what the rules are as to when cameras are used for portraits. // Therefore, for now never report them as not used. data.addImplicitReference(); } export function testFaceEffect(data: SanityTestData) { let path = data.model.faceEffect; if (path.length) { data.assertError(path.endsWith('.facefx') || path.endsWith('.facefx_ingame'), `Corrupted face effect path: "${path}"`); } } export function testBindPose(data: SanityTestData) { let matrices = data.model.bindPose; let objects = data.objects; data.assertWarning(matrices.length === objects.length, `Expected ${objects.length} matrices, got ${matrices.length}`); }