/** * @author takahiro / https://github.com/takahirox * * Dependencies * charset-encoder-js https://github.com/takahirox/charset-encoder-js * * * This loader loads and parses PMD/PMX and VMD binary files * then creates mesh for Three.js. * * PMD/PMX is a model data format and VMD is a motion data format * used in MMD(Miku Miku Dance). * * MMD is a 3D CG animation tool which is popular in Japan. * * * MMD official site * http://www.geocities.jp/higuchuu4/index_e.htm * * PMD, VMD format * http://blog.goo.ne.jp/torisu_tetosuki/e/209ad341d3ece2b1b4df24abf619d6e4 * * PMX format * http://gulshan-i-raz.geo.jp/labs/2012/10/17/pmx-format1/ * * Model data requirements * convert .tga files to .png files if exist. (Should I use THREE.TGALoader?) * resize the texture image files to power_of_2*power_of_2 * * * TODO * separate model/vmd loaders. * multi vmd files support. * edge(outline) support. * culling support. * toon(cel) shadering support. * add-sphere-mapping support. * physics support. * camera motion in vmd support. * light motion in vmd support. * music support. * make own shader for the performance and functionarity. * SDEF support. * uv/material/bone morphing support. * tga file loading support. * supply skinning support. * shadow support. */ THREE.MMDLoader = function ( showStatus, manager ) { THREE.Loader.call( this, showStatus ); this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; }; THREE.MMDLoader.prototype = Object.create( THREE.Loader.prototype ); THREE.MMDLoader.prototype.constructor = THREE.MMDLoader; THREE.MMDLoader.prototype.extractExtension = function ( url ) { var index = url.lastIndexOf( '.' ); if ( index < 0 ) { return null; } return url.slice( index + 1 ); }; THREE.MMDLoader.prototype.load = function ( modelUrl, vmdUrl, callback, onProgress, onError ) { var texturePath = this.extractUrlBase( modelUrl ); var modelExtension = this.extractExtension( modelUrl ); this.loadModelFile( modelUrl, vmdUrl, texturePath, modelExtension, callback, onProgress, onError ); }; THREE.MMDLoader.prototype.loadFileAsBuffer = function ( url, onLoad, onProgress, onError ) { var loader = new THREE.XHRLoader( this.manager ); loader.setResponseType( 'arraybuffer' ); loader.load( url, function ( buffer ) { onLoad( buffer ); }, onProgress, onError ); }; THREE.MMDLoader.prototype.loadModelFile = function ( modelUrl, vmdUrl, texturePath, modelExtension, callback, onProgress, onError ) { var scope = this; this.loadFileAsBuffer( modelUrl, function ( buffer ) { scope.loadVmdFile( buffer, vmdUrl, texturePath, modelExtension, callback, onProgress, onError ); }, onProgress, onError ); }; THREE.MMDLoader.prototype.loadVmdFile = function ( modelBuffer, vmdUrl, texturePath, modelExtension, callback, onProgress, onError ) { var scope = this; if ( ! vmdUrl ) { scope.parse( modelBuffer, null, texturePath, modelExtension, callback ); return; } this.loadFileAsBuffer( vmdUrl, function ( buffer ) { scope.parse( modelBuffer, buffer, texturePath, modelExtension, callback ); }, onProgress, onError ); }; THREE.MMDLoader.prototype.parse = function ( modelBuffer, vmdBuffer, texturePath, modelExtension, callback ) { var model = this.parseModel( modelBuffer, modelExtension ); var vmd = vmdBuffer !== null ? this.parseVmd( vmdBuffer ) : null; var mesh = this.createMesh( model, vmd, texturePath ); callback( mesh ); }; THREE.MMDLoader.prototype.parseModel = function ( buffer, modelExtension ) { // Should I judge from model data header? switch( modelExtension.toLowerCase() ) { case 'pmd': return this.parsePmd( buffer ); case 'pmx': return this.parsePmx( buffer ); default: throw 'extension ' + modelExtension + ' is not supported.'; } }; THREE.MMDLoader.prototype.parsePmd = function ( buffer ) { var scope = this; var pmd = {}; var dv = new THREE.MMDLoader.DataView( buffer ); pmd.metadata = {}; pmd.metadata.format = 'pmd'; var parseHeader = function () { var metadata = pmd.metadata; metadata.magic = dv.getChars( 3 ); if ( metadata.magic !== 'Pmd' ) { throw 'PMD file magic is not Pmd, but ' + metadata.magic; } metadata.version = dv.getFloat32(); metadata.modelName = dv.getSjisStringsAsUnicode( 20 ); metadata.comment = dv.getSjisStringsAsUnicode( 256 ); }; var parseVertices = function () { var parseVertex = function () { var p = {}; p.position = dv.getFloat32Array( 3 ); p.normal = dv.getFloat32Array( 3 ); p.uv = dv.getFloat32Array( 2 ); p.skinIndices = dv.getUint16Array( 2 ); p.skinWeights = [ dv.getUint8() / 100 ]; p.skinWeights.push( 1.0 - p.skinWeights[ 0 ] ); p.edgeFlag = dv.getUint8(); return p; }; var metadata = pmd.metadata; metadata.vertexCount = dv.getUint32(); pmd.vertices = []; for ( var i = 0; i < metadata.vertexCount; i++ ) { pmd.vertices.push( parseVertex() ); } }; var parseFaces = function () { var parseFace = function () { var p = {}; p.indices = dv.getUint16Array( 3 ); return p; }; var metadata = pmd.metadata; metadata.faceCount = dv.getUint32() / 3; pmd.faces = []; for ( var i = 0; i < metadata.faceCount; i++ ) { pmd.faces.push( parseFace() ); } }; var parseMaterials = function () { var parseMaterial = function () { var p = {}; p.diffuse = dv.getFloat32Array( 4 ); p.shiness = dv.getFloat32(); p.specular = dv.getFloat32Array( 3 ); p.emissive = dv.getFloat32Array( 3 ); p.toonIndex = dv.getUint8(); p.edgeFlag = dv.getUint8(); p.faceCount = dv.getUint32() / 3; p.fileName = dv.getChars( 20 ); return p; }; var metadata = pmd.metadata; metadata.materialCount = dv.getUint32(); pmd.materials = []; for ( var i = 0; i < metadata.materialCount; i++ ) { pmd.materials.push( parseMaterial() ); } }; var parseBones = function () { var parseBone = function () { var p = {}; // Skinning animation doesn't work when bone name is Japanese Unicode in r73. // So using charcode strings as workaround and keep original strings in .originalName. p.originalName = dv.getSjisStringsAsUnicode( 20 ); p.name = dv.toCharcodeStrings( p.originalName ); p.parentIndex = dv.getUint16(); p.tailIndex = dv.getUint16(); p.type = dv.getUint8(); p.ikIndex = dv.getUint16(); p.position = dv.getFloat32Array( 3 ); return p; }; var metadata = pmd.metadata; metadata.boneCount = dv.getUint16(); pmd.bones = []; for ( var i = 0; i < metadata.boneCount; i++ ) { pmd.bones.push( parseBone() ); } }; var parseIks = function () { var parseIk = function () { var p = {}; p.target = dv.getUint16(); p.effector = dv.getUint16(); p.linkCount = dv.getUint8(); p.iteration = dv.getUint16(); p.maxAngle = dv.getFloat32(); p.links = []; for ( var i = 0; i < p.linkCount; i++ ) { var link = {} link.index = dv.getUint16(); p.links.push( link ); } return p; }; var metadata = pmd.metadata; metadata.ikCount = dv.getUint16(); pmd.iks = []; for ( var i = 0; i < metadata.ikCount; i++ ) { pmd.iks.push( parseIk() ); } }; var parseMorphs = function () { var parseMorph = function () { var p = {}; p.name = dv.getSjisStringsAsUnicode( 20 ); p.elementCount = dv.getUint32(); p.type = dv.getUint8(); p.elements = []; for ( var i = 0; i < p.elementCount; i++ ) { p.elements.push( { index: dv.getUint32(), position: dv.getFloat32Array( 3 ) } ) ; } return p; }; var metadata = pmd.metadata; metadata.morphCount = dv.getUint16(); pmd.morphs = []; for ( var i = 0; i < metadata.morphCount; i++ ) { pmd.morphs.push( parseMorph() ); } }; parseHeader(); parseVertices(); parseFaces(); parseMaterials(); parseBones(); parseIks(); parseMorphs(); return pmd; }; THREE.MMDLoader.prototype.parsePmx = function ( buffer ) { var scope = this; var pmx = {}; var dv = new THREE.MMDLoader.DataView( buffer ); pmx.metadata = {}; pmx.metadata.format = 'pmx'; var parseHeader = function () { var metadata = pmx.metadata; metadata.magic = dv.getChars( 4 ); // Note: don't remove the last blank space. if ( metadata.magic !== 'PMX ' ) { throw 'PMX file magic is not PMX , but ' + metadata.magic; } metadata.version = dv.getFloat32(); if ( metadata.version !== 2.0 && metadata.version !== 2.1 ) { throw 'PMX version ' + metadata.version + ' is not supported.'; } metadata.headerSize = dv.getUint8(); metadata.encoding = dv.getUint8(); metadata.additionalUvNum = dv.getUint8(); metadata.vertexIndexSize = dv.getUint8(); metadata.textureIndexSize = dv.getUint8(); metadata.materialIndexSize = dv.getUint8(); metadata.boneIndexSize = dv.getUint8(); metadata.morphIndexSize = dv.getUint8(); metadata.rigidbodyIndexSize = dv.getUint8(); metadata.modelName = dv.getTextBuffer(); metadata.englishModelName = dv.getTextBuffer(); metadata.comment = dv.getTextBuffer(); metadata.englishComment = dv.getTextBuffer(); }; var parseVertices = function () { var parseVertex = function () { var p = {}; p.position = dv.getFloat32Array( 3 ); p.normal = dv.getFloat32Array( 3 ); p.uv = dv.getFloat32Array( 2 ); p.auvs = []; for ( var i = 0; i < pmx.metadata.additionalUvNum; i++ ) { p.auvs.push( dv.getFloat32Array( 4 ) ); } p.type = dv.getUint8(); var indexSize = metadata.vertexIndexSize; if ( p.type === 0 ) { // BDEF1 p.skinIndices = dv.getNumberArray( indexSize, 1 ); p.skinWeights = [ 1.0 ]; } else if ( p.type === 1 ) { // BDEF2 p.skinIndices = dv.getNumberArray( indexSize, 2 ); p.skinWeights = dv.getFloat32Array( 1 ); p.skinWeights.push( 1.0 - p.skinWeights[ 0 ] ); } else if ( p.type === 2 ) { // BDEF4 p.skinIndices = dv.getNumberArray( indexSize, 4 ); p.skinWeights = dv.getFloat32Array( 4 ); } else if ( p.type === 3 ) { // SDEF p.skinIndices = dv.getNumberArray( indexSize, 2 ); p.skinWeights = dv.getFloat32Array( 1 ); p.skinWeights.push( 1.0 - p.skinWeights[ 0 ] ); p.skinC = dv.getFloat32Array( 3 ); p.skinR0 = dv.getFloat32Array( 3 ); p.skinR1 = dv.getFloat32Array( 3 ); // SDEF is not supported yet and is handled as BDEF2 so far. // TODO: SDEF support p.type = 1; } else { throw 'unsupport bone type ' + p.type + ' exception.'; } p.edgeRatio = dv.getFloat32(); return p; }; var metadata = pmx.metadata; metadata.vertexCount = dv.getUint32(); pmx.vertices = []; for ( var i = 0; i < metadata.vertexCount; i++ ) { pmx.vertices.push( parseVertex() ); } }; var parseFaces = function () { var parseFace = function () { var p = {}; p.indices = dv.getNumberArray( metadata.vertexIndexSize, 3 ); return p; }; var metadata = pmx.metadata; metadata.faceCount = dv.getUint32() / 3; pmx.faces = []; for ( var i = 0; i < metadata.faceCount; i++ ) { pmx.faces.push( parseFace() ); } }; var parseTextures = function () { var parseTexture = function () { return dv.getTextBuffer(); }; var metadata = pmx.metadata; metadata.textureCount = dv.getUint32(); pmx.textures = []; for ( var i = 0; i < metadata.textureCount; i++ ) { pmx.textures.push( parseTexture() ); } }; var parseMaterials = function () { var parseMaterial = function () { var p = {}; p.name = dv.getTextBuffer(); p.englishName = dv.getTextBuffer(); p.diffuse = dv.getFloat32Array( 4 ); p.specular = dv.getFloat32Array( 3 ); p.shiness = dv.getFloat32(); p.emissive = dv.getFloat32Array( 3 ); p.flag = dv.getUint8(); p.edgeColor = dv.getFloat32Array( 4 ); p.edgeSize = dv.getFloat32(); p.textureIndex = dv.getNumber( pmx.metadata.textureIndexSize ); p.envTextureIndex = dv.getNumber( pmx.metadata.textureIndexSize ); p.envFlag = dv.getUint8(); p.toonFlag = dv.getUint8(); if ( p.toonFlag === 0 ) { p.toonTextureIndex = dv.getNumber( pmx.metadata.textureIndexSize ); } else if ( p.toonFlag === 1 ) { p.toonTextureIndex = dv.getUint8(); } else { throw 'unknown toon flag ' + p.toonFlag + ' exception.'; } p.comment = dv.getTextBuffer(); p.faceCount = dv.getUint32() / 3; return p; }; var metadata = pmx.metadata; metadata.materialCount = dv.getUint32(); pmx.materials = []; for ( var i = 0; i < metadata.materialCount; i++ ) { pmx.materials.push( parseMaterial() ); } }; var parseBones = function () { var parseBone = function () { var p = {}; // Skinning animation doesn't work when bone name is Japanese Unicode in r73. // So using charcode strings as workaround and keep original strings in .originalName. p.originalName = dv.getTextBuffer(); p.name = dv.toCharcodeStrings( p.originalName ); p.englishName = dv.getTextBuffer(); p.position = dv.getFloat32Array( 3 ); p.parentIndex = dv.getNumber( pmx.metadata.boneIndexSize ); p.transformationClass = dv.getUint32(); p.flag = dv.getUint16(); if ( p.flag & 0x1 ) { p.connectIndex = dv.getNumber( pmx.metadata.boneIndexSize ); } else { p.offsetPosition = dv.getFloat32Array( 3 ); } if ( p.flag & 0x100 || p.flag & 0x200 ) { p.supplyParentIndex = dv.getNumber( pmx.metadata.boneIndexSize ); p.supplyRatio = dv.getFloat32(); } if ( p.flag & 0x400 ) { p.fixAxis = dv.getFloat32Array( 3 ); } if ( p.flag & 0x800 ) { p.localXVector = dv.getFloat32Array( 3 ); p.localZVector = dv.getFloat32Array( 3 ); } if ( p.flag & 0x2000 ) { p.key = dv.getUint32(); } if ( p.flag & 0x20 ) { var ik = {}; ik.effector = dv.getNumber( pmx.metadata.boneIndexSize ); ik.target = null; ik.iteration = dv.getUint32(); ik.maxAngle = dv.getFloat32(); ik.linkCount = dv.getUint32(); ik.links = []; for ( var i = 0; i < ik.linkCount; i++ ) { var link = {}; link.index = dv.getNumber( pmx.metadata.boneIndexSize ); link.angleLimitation = dv.getUint8(); if ( link.angleLimitation === 1 ) { link.lowerLimitationAngle = dv.getFloat32Array( 3 ); link.upperLimitationAngle = dv.getFloat32Array( 3 ); } ik.links.push( link ); } p.ik = ik; } return p; }; var metadata = pmx.metadata; metadata.boneCount = dv.getUint32(); pmx.bones = []; for ( var i = 0; i < metadata.boneCount; i++ ) { pmx.bones.push( parseBone() ); } }; var parseMorphs = function () { var parseMorph = function () { var p = {}; p.name = dv.getTextBuffer(); p.englishName = dv.getTextBuffer(); p.panel = dv.getUint8(); p.type = dv.getUint8(); p.elementCount = dv.getUint32(); p.elements = []; for ( var i = 0; i < p.elementCount; i++ ) { if ( p.type === 0 ) { // group morph var m = {}; m.index = dv.getNumber( pmx.metadata.morphIndexSize ); m.ratio = dv.getFloat32(); p.elements.push( m ); } else if ( p.type === 1 ) { // vertex morph var m = {}; m.index = dv.getNumber( pmx.metadata.vertexIndexSize ); m.position = dv.getFloat32Array( 3 ); p.elements.push( m ); } else if ( p.type === 2 ) { // bone morph var m = {}; m.index = dv.getNumber( pmx.metadata.boneIndexSize ); m.position = dv.getFloat32Array( 3 ); m.rotation = dv.getFloat32Array( 4 ); p.elements.push( m ); } else if ( p.type === 3 ) { // uv morph var m = {}; m.index = dv.getNumber( pmx.metadata.vertexIndexSize ); m.uv = dv.getFloat32Array( 4 ); p.elements.push( m ); } else if ( p.type === 8 ) { // material morph var m = {}; m.index = dv.getNumber( pmx.metadata.materialIndexSize ); m.type = dv.getUint8(); m.diffuse = dv.getFloat32Array( 4 ); m.specular = dv.getFloat32Array( 3 ); m.shiness = dv.getFloat32(); m.emissive = dv.getFloat32Array( 3 ); m.edgeColor = dv.getFloat32Array( 4 ); m.edgeSize = dv.getFloat32(); m.textureColor = dv.getFloat32Array( 4 ); m.sphereTextureColor = dv.getFloat32Array( 4 ); m.toonColor = dv.getFloat32Array( 4 ); p.elements.push( m ); } } return p; }; var metadata = pmx.metadata; metadata.morphCount = dv.getUint32(); pmx.morphs = []; for ( var i = 0; i < metadata.morphCount; i++ ) { pmx.morphs.push( parseMorph() ); } }; parseHeader(); parseVertices(); parseFaces(); parseTextures(); parseMaterials(); parseBones(); parseMorphs(); // console.log( pmx ); // for console debug return pmx; }; THREE.MMDLoader.prototype.parseVmd = function ( buffer ) { var scope = this; var vmd = {}; var dv = new THREE.MMDLoader.DataView( buffer ); vmd.metadata = {}; var parseHeader = function () { var metadata = vmd.metadata; metadata.magic = dv.getChars( 30 ); if ( metadata.magic !== 'Vocaloid Motion Data 0002' ) { throw 'VMD file magic is not Vocaloid Motion Data 0002, but ' + metadata.magic; } metadata.name = dv.getSjisStringsAsUnicode( 20 ); }; var parseMotions = function () { var parseMotion = function () { var p = {}; // Skinning animation doesn't work when bone name is Japanese Unicode in r73. // So using charcode strings as workaround and keep original strings in .originalName. p.originalBoneName = dv.getSjisStringsAsUnicode( 15 ); p.boneName = dv.toCharcodeStrings( p.originalBoneName ); p.frameNum = dv.getUint32(); p.position = dv.getFloat32Array( 3 ); p.rotation = dv.getFloat32Array( 4 ); p.interpolation = dv.getUint8Array( 64 ); return p; }; var metadata = vmd.metadata; metadata.motionCount = dv.getUint32(); vmd.motions = []; for ( var i = 0; i < metadata.motionCount; i++ ) { vmd.motions.push( parseMotion() ); } }; var parseMorphs = function () { var parseMorph = function () { var p = {}; p.morphName = dv.getSjisStringsAsUnicode( 15 ); p.frameNum = dv.getUint32(); p.weight = dv.getFloat32(); return p; }; var metadata = vmd.metadata; metadata.morphCount = dv.getUint32(); vmd.morphs = []; for ( var i = 0; i < metadata.morphCount; i++ ) { vmd.morphs.push( parseMorph() ); } }; parseHeader(); parseMotions(); parseMorphs(); return vmd; }; // maybe better to create json and then use JSONLoader... THREE.MMDLoader.prototype.createMesh = function ( model, vmd, texturePath, onProgress, onError ) { var scope = this; var geometry = new THREE.Geometry(); var material = new THREE.MeshFaceMaterial(); var leftToRight = function() { var convertVector = function ( v ) { v[ 2 ] = -v[ 2 ]; }; var convertQuaternion = function ( q ) { q[ 0 ] = -q[ 0 ]; q[ 1 ] = -q[ 1 ]; }; var convertIndexOrder = function ( p ) { var tmp = p[ 2 ]; p[ 2 ] = p[ 0 ]; p[ 0 ] = tmp; }; for ( var i = 0; i < model.metadata.vertexCount; i++ ) { convertVector( model.vertices[ i ].position ); convertVector( model.vertices[ i ].normal ); } for ( var i = 0; i < model.metadata.faceCount; i++ ) { convertIndexOrder( model.faces[ i ].indices ); } for ( var i = 0; i < model.metadata.boneCount; i++ ) { convertVector( model.bones[ i ].position ); } // TODO: support other morph for PMX for ( var i = 0; i < model.metadata.morphCount; i++ ) { var m = model.morphs[ i ]; if ( model.metadata.format === 'pmx' ) { if ( m.type === 1 ) { m = m.elements; } else { continue; } } for ( var j = 0; j < m.elementCount; j++ ) { convertVector( m.elements[ j ].position ); } } if ( vmd === null ) { return; } for ( var i = 0; i < vmd.metadata.motionCount; i++ ) { convertVector( vmd.motions[ i ].position ); convertQuaternion( vmd.motions[ i ].rotation ); } }; var initVartices = function () { for ( var i = 0; i < model.metadata.vertexCount; i++ ) { var v = model.vertices[ i ]; geometry.vertices.push( new THREE.Vector3( v.position[ 0 ], v.position[ 1 ], v.position[ 2 ] ) ); geometry.skinIndices.push( new THREE.Vector4( v.skinIndices.length >= 1 ? v.skinIndices[ 0 ] : 0.0, v.skinIndices.length >= 2 ? v.skinIndices[ 1 ] : 0.0, v.skinIndices.length >= 3 ? v.skinIndices[ 2 ] : 0.0, v.skinIndices.length >= 4 ? v.skinIndices[ 3 ] : 0.0 ) ); geometry.skinWeights.push( new THREE.Vector4( v.skinWeights.length >= 1 ? v.skinWeights[ 0 ] : 0.0, v.skinWeights.length >= 2 ? v.skinWeights[ 1 ] : 0.0, v.skinWeights.length >= 3 ? v.skinWeights[ 2 ] : 0.0, v.skinWeights.length >= 4 ? v.skinWeights[ 3 ] : 0.0 ) ); } }; var initFaces = function () { for ( var i = 0; i < model.metadata.faceCount; i++ ) { geometry.faces.push( new THREE.Face3( model.faces[ i ].indices[ 0 ], model.faces[ i ].indices[ 1 ], model.faces[ i ].indices[ 2 ] ) ); for ( var j = 0; j < 3; j++ ) { geometry.faces[ i ].vertexNormals[ j ] = new THREE.Vector3( model.vertices[ model.faces[ i ].indices[ j ] ].normal[ 0 ], model.vertices[ model.faces[ i ].indices[ j ] ].normal[ 1 ], model.vertices[ model.faces[ i ].indices[ j ] ].normal[ 2 ] ); } } }; var initBones = function () { var bones = []; for ( var i = 0; i < model.metadata.boneCount; i++ ) { var bone = {}; var b = model.bones[ i ]; bone.parent = b.parentIndex; if ( model.metadata.format === 'pmd' ) { bone.parent = ( b.parentIndex === 0xFFFF ) ? -1 : b.parentIndex; } bone.name = b.name; bone.pos = [ b.position[ 0 ], b.position[ 1 ], b.position[ 2 ] ]; bone.rotq = [ 0, 0, 0, 1 ]; bone.scl = [ 1, 1, 1 ]; if ( bone.parent !== -1 ) { bone.pos[ 0 ] -= model.bones[ bone.parent ].position[ 0 ]; bone.pos[ 1 ] -= model.bones[ bone.parent ].position[ 1 ]; bone.pos[ 2 ] -= model.bones[ bone.parent ].position[ 2 ]; } bones.push( bone ); } geometry.bones = bones; }; var initIKs = function () { var iks = []; // TODO: remove duplicated codes between PMD and PMX if ( model.metadata.format === 'pmd' ) { for ( var i = 0; i < model.metadata.ikCount; i++ ) { var ik = model.iks[i]; var param = {}; param.target = ik.target; param.effector = ik.effector; param.iteration = ik.iteration; param.maxAngle = ik.maxAngle * 4; param.links = []; for ( var j = 0; j < ik.links.length; j++ ) { var link = {}; link.index = ik.links[ j ].index; // Checking with .originalName, not .name. // See parseBone() for the detail. if ( model.bones[ link.index ].originalName.indexOf( 'ひざ' ) >= 0 ) { link.limitation = new THREE.Vector3( 1.0, 0.0, 0.0 ); } param.links.push( link ); } iks.push( param ); } } else { for ( var i = 0; i < model.metadata.boneCount; i++ ) { var b = model.bones[ i ]; var ik = b.ik; if ( ik === undefined ) { continue; } var param = {}; param.target = i; param.effector = ik.effector; param.iteration = ik.iteration; param.maxAngle = ik.maxAngle; param.links = []; for ( var j = 0; j < ik.links.length; j++ ) { var link = {}; link.index = ik.links[ j ].index; if ( ik.links[ j ].angleLimitation === 1 ) { link.limitation = new THREE.Vector3( 1.0, 0.0, 0.0 ); // TODO: use limitation angles // link.lowerLimitationAngle; // link.upperLimitationAngle; } param.links.push( link ); } iks.push( param ); } } geometry.iks = iks; }; var initMorphs = function () { function updateVertex ( params, index, v, ratio ) { params.vertices[ index ].x += v.position[ 0 ] * ratio; params.vertices[ index ].y += v.position[ 1 ] * ratio; params.vertices[ index ].z += v.position[ 2 ] * ratio; }; function updateVertices ( params, m, ratio ) { for ( var i = 0; i < m.elementCount; i++ ) { var v = m.elements[ i ]; var index; if ( model.metadata.format === 'pmd' ) { index = model.morphs[ 0 ].elements[ v.index ].index; } else { index = v.index; } updateVertex( params, index, v, ratio ); } }; for ( var i = 0; i < model.metadata.morphCount; i++ ) { var m = model.morphs[ i ]; var params = {}; params.name = m.name; params.vertices = []; for ( var j = 0; j < model.metadata.vertexCount; j++ ) { params.vertices[ j ] = new THREE.Vector3( 0, 0, 0 ); params.vertices[ j ].x = geometry.vertices[ j ].x; params.vertices[ j ].y = geometry.vertices[ j ].y; params.vertices[ j ].z = geometry.vertices[ j ].z; } if ( model.metadata.format === 'pmd' ) { if ( i !== 0 ) { updateVertices( params, m, 1.0 ); } } else { if ( m.type === 0 ) { for ( var j = 0; j < m.elementCount; j++ ) { var m2 = model.morphs[ m.elements[ j ].index ]; var ratio = m.elements[ j ].ratio; if ( m2.type === 1 ) { updateVertices( params, m2, ratio ); } } } else if ( m.type === 1 ) { updateVertices( params, m, 1.0 ); } } // TODO: skip if this's non-vertex morphing of PMX to reduce CPU/Memory use geometry.morphTargets.push( params ); } }; var initMaterials = function () { var offset = 0; var materialParams = []; for ( var i = 1; i < model.metadata.materialCount; i++ ) { geometry.faceVertexUvs.push( [] ); } for ( var i = 0; i < model.metadata.materialCount; i++ ) { var m = model.materials[ i ]; var params = {}; for ( var j = 0; j < m.faceCount; j++ ) { geometry.faces[ offset ].materialIndex = i; var uvs = []; for ( var k = 0; k < 3; k++ ) { var v = model.vertices[ model.faces[ offset ].indices[ k ] ]; uvs.push( new THREE.Vector2( v.uv[ 0 ], v.uv[ 1 ] ) ); } geometry.faceVertexUvs[ 0 ].push( uvs ); offset++; } params.shading = 'phong'; params.colorDiffuse = [ m.diffuse[ 0 ], m.diffuse[ 1 ], m.diffuse[ 2 ] ]; params.opacity = m.diffuse[ 3 ]; params.colorSpecular = [ m.specular[ 0 ], m.specular[ 1 ], m.specular[ 2 ] ]; params.specularCoef = m.shiness; // temporal workaround // TODO: implement correctly params.doubleSided = true; if ( model.metadata.format === 'pmd' ) { if ( m.fileName ) { var fileName = m.fileName; var fileNames = []; // temporal workaround, disable sphere mapping so far // TODO: sphere mapping support var index = fileName.lastIndexOf( '*' ); if ( index >= 0 ) { fileNames.push( fileName.slice( 0, index ) ); fileNames.push( fileName.slice( index + 1 ) ); } else { fileNames.push( fileName ); } for ( var j = 0; j < fileNames.length; j++ ) { var n = fileNames[ j ]; // TODO: support spa if ( /* n.indexOf( '.spa' ) >= 0 || */ n.indexOf( '.sph' ) >= 0 ) { params.mapEnv = n; } else { // temporal workaround, use .png instead of .tga // TODO: tga file support params.mapDiffuse = n.replace( '.tga', '.png' ); } } } } else { if ( m.textureIndex !== -1 ) { var n = model.textures[ m.textureIndex ]; // temporal workaround, use .png instead of .tga // TODO: tga file support params.mapDiffuse = n.replace( '.tga', '.png' ); } // TODO: support m.envFlag === 0, 2, 3 if ( m.envTextureIndex !== -1 && m.envFlag === 1 ) { var n = model.textures[ m.envTextureIndex ]; params.mapEnv = n; } } if ( params.mapDiffuse === undefined ) { params.colorEmissive = [ m.emissive[ 0 ], m.emissive[ 1 ], m.emissive[ 2 ] ]; } materialParams.push( params ); } var materials = scope.initMaterials( materialParams, texturePath ); for ( var i = 0; i < materials.length; i++ ) { var m = materials[ i ]; var p = materialParams[ i ]; if ( m.map ) { m.map.flipY = false; } // this should be in THREE.Loader.createMaterial. // remove this if it supports. // TODO: make patch of THREE.Loader.createMaterial? if ( p.mapEnv !== undefined ) { var fullPath = texturePath + p.mapEnv; var loader = THREE.Loader.Handlers.get( fullPath ); if ( loader === null ) { loader = new THREE.TextureLoader( this.manager ); } var texture = loader.load( fullPath ); // currently only support multiply-sphere-mapping // TODO: support add-sphere-mapping texture.mapping = THREE.SphericalReflectionMapping; m.envMap = texture; } m.skinning = true; m.morphTargets = true; material.materials.push( m ); } }; var initMotionAnimations = function () { var orderedMotions = []; var boneTable = {}; for ( var i = 0; i < model.metadata.boneCount; i++ ) { var b = model.bones[ i ]; boneTable[ b.name ] = i; orderedMotions[ i ] = []; } for ( var i = 0; i < vmd.motions.length; i++ ) { var m = vmd.motions[ i ]; var num = boneTable[ m.boneName ]; if ( num === undefined ) continue; orderedMotions[ num ].push( m ); } for ( var i = 0; i < orderedMotions.length; i++ ) { orderedMotions[ i ].sort( function ( a, b ) { return a.frameNum - b.frameNum; } ) ; } var animation = { name: 'Action', fps: 30, length: 0.0, hierarchy: [] }; for ( var i = 0; i < geometry.bones.length; i++ ) { animation.hierarchy.push( { parent: geometry.bones[ i ].parent, keys: [] } ); } var maxTime = 0.0; for ( var i = 0; i < orderedMotions.length; i++ ) { var array = orderedMotions[ i ]; for ( var j = 0; j < array.length; j++ ) { var t = array[ j ].frameNum / 30; var p = array[ j ].position; var r = array[ j ].rotation; animation.hierarchy[ i ].keys.push( { time: t, pos: [ geometry.bones[ i ].pos[ 0 ] + p[ 0 ], geometry.bones[ i ].pos[ 1 ] + p[ 1 ], geometry.bones[ i ].pos[ 2 ] + p[ 2 ] ], rot: [ r[ 0 ], r[ 1 ], r[ 2 ], r[ 3 ] ], scl: [ 1, 1, 1 ] } ); if ( t > maxTime ) maxTime = t; } } // add 2 secs as afterglow maxTime += 2.0; animation.length = maxTime; for ( var i = 0; i < orderedMotions.length; i++ ) { var keys = animation.hierarchy[ i ].keys; if ( keys.length === 0 ) { keys.push( { time: 0.0, pos: [ geometry.bones[ i ].pos[ 0 ], geometry.bones[ i ].pos[ 1 ], geometry.bones[ i ].pos[ 2 ] ], rot: [ 0, 0, 0, 1 ], scl: [ 1, 1, 1 ] } ); } var k = keys[ 0 ]; if ( k.time !== 0.0 ) { keys.unshift( { time: 0.0, pos: [ k.pos[ 0 ], k.pos[ 1 ], k.pos[ 2 ] ], rot: [ k.rot[ 0 ], k.rot[ 1 ], k.rot[ 2 ], k.rot[ 3 ] ], scl: [ 1, 1, 1 ] } ); } k = keys[ keys.length - 1 ]; if ( k.time < maxTime ) { keys.push( { time: maxTime, pos: [ k.pos[ 0 ], k.pos[ 1 ], k.pos[ 2 ] ], rot: [ k.rot[ 0 ], k.rot[ 1 ], k.rot[ 2 ], k.rot[ 3 ] ], scl: [ 1, 1, 1 ] } ); } } // geometry.animation = animation; geometry.animations = []; geometry.animations.push( THREE.AnimationClip.parseAnimation( animation, geometry.bones ) ); }; var initMorphAnimations = function () { var orderedMorphs = []; var morphTable = {} for ( var i = 0; i < model.metadata.morphCount; i++ ) { var m = model.morphs[ i ]; morphTable[ m.name ] = i; orderedMorphs[ i ] = []; } for ( var i = 0; i < vmd.morphs.length; i++ ) { var m = vmd.morphs[ i ]; var num = morphTable[ m.morphName ]; if ( num === undefined ) continue; orderedMorphs[ num ].push( m ); } for ( var i = 0; i < orderedMorphs.length; i++ ) { orderedMorphs[ i ].sort( function ( a, b ) { return a.frameNum - b.frameNum; } ) ; } var morphAnimation = { fps: 30, length: 0.0, hierarchy: [] }; for ( var i = 0; i < model.metadata.morphCount; i++ ) { morphAnimation.hierarchy.push( { keys: [] } ); } var maxTime = 0.0; for ( var i = 0; i < orderedMorphs.length; i++ ) { var array = orderedMorphs[ i ]; for ( var j = 0; j < array.length; j++ ) { var t = array[ j ].frameNum / 30; var w = array[ j ].weight; morphAnimation.hierarchy[ i ].keys.push( { time: t, value: w } ); if ( t > maxTime ) { maxTime = t; } } } // add 2 secs as afterglow maxTime += 2.0; // use animation's length if exists. animation is master. maxTime = ( geometry.animation !== undefined && geometry.animation.length > 0.0 ) ? geometry.animation.length : maxTime; morphAnimation.length = maxTime; for ( var i = 0; i < orderedMorphs.length; i++ ) { var keys = morphAnimation.hierarchy[ i ].keys; if ( keys.length === 0 ) { keys.push( { time: 0.0, value: 0.0 } ); } var k = keys[ 0 ]; if ( k.time !== 0.0 ) { keys.unshift( { time: 0.0, value: k.value } ); } k = keys[ keys.length - 1 ]; if ( k.time < maxTime ) { keys.push( { time: maxTime, value: k.value } ); } } // geometry.morphAnimation = morphAnimation; var tracks = []; for ( var i = 1; i < orderedMorphs.length; i++ ) { var keys = morphAnimation.hierarchy[ i ].keys; var times = []; var values = []; for ( var j = 0; j < keys.length; j++ ) { var key = keys[ j ]; times.push( key.time ); values.push( key.value ); } tracks.push( new THREE.NumberKeyframeTrack( '.morphTargetInfluences[' + i + ']', times, values ) ); } geometry.morphAnimations = []; geometry.morphAnimations.push( new THREE.AnimationClip( 'morphAnimation', -1, tracks ) ); }; leftToRight(); initVartices(); initFaces(); initBones(); initIKs(); initMorphs(); initMaterials(); if ( vmd !== null ) { initMotionAnimations(); initMorphAnimations(); } geometry.computeFaceNormals(); geometry.verticesNeedUpdate = true; geometry.normalsNeedUpdate = true; geometry.uvsNeedUpdate = true; geometry.mmdFormat = model.metadata.format; var mesh = new THREE.SkinnedMesh( geometry, material ); // console.log( mesh ); // for console debug return mesh; }; THREE.MMDLoader.DataView = function ( buffer, littleEndian ) { this.dv = new DataView( buffer ); this.offset = 0; this.littleEndian = ( littleEndian !== undefined ) ? littleEndian : true; this.encoder = new CharsetEncoder(); }; THREE.MMDLoader.DataView.prototype = { constructor: THREE.MMDLoader.DataView, getInt8: function () { var value = this.dv.getInt8( this.offset ); this.offset += 1; return value; }, getInt8Array: function ( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getInt8() ); } return a; }, getUint8: function () { var value = this.dv.getUint8( this.offset ); this.offset += 1; return value; }, getUint8Array: function ( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getUint8() ); } return a; }, getInt16: function () { var value = this.dv.getInt16( this.offset, this.littleEndian ); this.offset += 2; return value; }, getInt16Array: function ( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getInt16() ); } return a; }, getUint16: function () { var value = this.dv.getUint16( this.offset, this.littleEndian ); this.offset += 2; return value; }, getUint16Array: function ( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getUint16() ); } return a; }, getInt32: function () { var value = this.dv.getInt32( this.offset, this.littleEndian ); this.offset += 4; return value; }, getInt32Array: function ( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getInt32() ); } return a; }, getUint32: function () { var value = this.dv.getUint32( this.offset, this.littleEndian ); this.offset += 4; return value; }, getUint32Array: function ( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getUint32() ); } return a; }, getFloat32: function () { var value = this.dv.getFloat32( this.offset, this.littleEndian ); this.offset += 4; return value; }, getFloat32Array: function( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getFloat32() ); } return a; }, getFloat64: function () { var value = this.dv.getFloat64( this.offset, this.littleEndian ); this.offset += 8; return value; }, getFloat64Array: function( size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getFloat64() ); } return a; }, getNumber: function ( type ) { switch ( type ) { case 1: return this.getInt8(); case 2: return this.getInt16(); case 4: return this.getInt32(); default: throw 'unknown number type ' + type + ' exception.'; } }, getNumberArray: function ( type, size ) { var a = []; for ( var i = 0; i < size; i++ ) { a.push( this.getNumber( type ) ); } return a; }, getChars: function ( size ) { var str = ''; while ( size > 0 ) { var value = this.getUint8(); size--; if ( value === 0 ) { break; } str += String.fromCharCode( value ); } while ( size > 0 ) { this.getUint8(); size--; } return str; }, getSjisStringsAsUnicode: function ( size ) { var a = []; while ( size > 0 ) { var value = this.getUint8(); size--; if ( value === 0 ) { break; } a.push( value ); } while ( size > 0 ) { this.getUint8(); size--; } return this.encoder.s2u( new Uint8Array( a ) ); }, /* * Note: Sometimes to use Japanese Unicode characters runs into problems in Three.js. * In such a case, use this method to convert it to Unicode hex charcode strings, * like 'あいう' -> '0x30420x30440x3046' */ toCharcodeStrings: function ( s ) { var str = ''; for ( var i = 0; i < s.length; i++ ) { str += '0x' + ( '0000' + s[ i ].charCodeAt().toString( 16 ) ).substr( -4 ); } return str; }, getUnicodeStrings: function ( size ) { var str = ''; while ( size > 0 ) { var value = this.getUint16(); size -= 2; if ( value === 0 ) { break; } str += String.fromCharCode( value ); } while ( size > 0 ) { this.getUint8(); size--; } return str; }, getTextBuffer: function () { var size = this.getUint32(); return this.getUnicodeStrings( size ); } };