three-stdlib
Version:
stand-alone library of threejs examples
1 lines • 77.7 kB
Source Map (JSON)
{"version":3,"file":"MMDLoader.cjs","sources":["../../src/loaders/MMDLoader.js"],"sourcesContent":["import {\n AddOperation,\n AnimationClip,\n Bone,\n BufferGeometry,\n Color,\n CustomBlending,\n DoubleSide,\n DstAlphaFactor,\n Euler,\n FileLoader,\n Float32BufferAttribute,\n FrontSide,\n Interpolant,\n Loader,\n LoaderUtils,\n MeshToonMaterial,\n MultiplyOperation,\n NearestFilter,\n NumberKeyframeTrack,\n OneMinusSrcAlphaFactor,\n Quaternion,\n QuaternionKeyframeTrack,\n RepeatWrapping,\n Skeleton,\n SkinnedMesh,\n SrcAlphaFactor,\n TextureLoader,\n Uint16BufferAttribute,\n Vector3,\n VectorKeyframeTrack,\n} from 'three'\nimport { TGALoader } from '../loaders/TGALoader'\nimport { Parser } from '../libs/mmdparser'\n\n/**\n * Dependencies\n * - mmd-parser https://github.com/takahirox/mmd-parser\n * - TGALoader\n * - OutlineEffect\n *\n * MMDLoader creates Three.js Objects from MMD resources as\n * PMD, PMX, VMD, and VPD files.\n *\n * PMD/PMX is a model data format, VMD is a motion data format\n * VPD is a posing data format used in MMD(Miku Miku Dance).\n *\n * MMD official site\n * - https://sites.google.com/view/evpvp/\n *\n * PMD, VMD format (in Japanese)\n * - http://blog.goo.ne.jp/torisu_tetosuki/e/209ad341d3ece2b1b4df24abf619d6e4\n *\n * PMX format\n * - https://gist.github.com/felixjones/f8a06bd48f9da9a4539f\n *\n * TODO\n * - light motion in vmd support.\n * - SDEF support.\n * - uv/material/bone morphing support.\n * - more precise grant skinning support.\n * - shadow support.\n */\n\n/**\n * @param {THREE.LoadingManager} manager\n */\nclass MMDLoader extends Loader {\n constructor(manager) {\n super(manager)\n\n this.loader = new FileLoader(this.manager)\n\n this.parser = null // lazy generation\n this.meshBuilder = new MeshBuilder(this.manager)\n this.animationBuilder = new AnimationBuilder()\n }\n\n /**\n * @param {string} animationPath\n * @return {MMDLoader}\n */\n setAnimationPath(animationPath) {\n this.animationPath = animationPath\n return this\n }\n\n // Load MMD assets as Three.js Object\n\n /**\n * Loads Model file (.pmd or .pmx) as a SkinnedMesh.\n *\n * @param {string} url - url to Model(.pmd or .pmx) file\n * @param {function} onLoad\n * @param {function} onProgress\n * @param {function} onError\n */\n load(url, onLoad, onProgress, onError) {\n const builder = this.meshBuilder.setCrossOrigin(this.crossOrigin)\n\n // resource path\n\n let resourcePath\n\n if (this.resourcePath !== '') {\n resourcePath = this.resourcePath\n } else if (this.path !== '') {\n resourcePath = this.path\n } else {\n resourcePath = LoaderUtils.extractUrlBase(url)\n }\n\n const modelExtension = this._extractExtension(url).toLowerCase()\n\n // Should I detect by seeing header?\n if (modelExtension !== 'pmd' && modelExtension !== 'pmx') {\n if (onError) onError(new Error('THREE.MMDLoader: Unknown model file extension .' + modelExtension + '.'))\n\n return\n }\n\n this[modelExtension === 'pmd' ? 'loadPMD' : 'loadPMX'](\n url,\n function (data) {\n onLoad(builder.build(data, resourcePath, onProgress, onError))\n },\n onProgress,\n onError,\n )\n }\n\n /**\n * Loads Motion file(s) (.vmd) as a AnimationClip.\n * If two or more files are specified, they'll be merged.\n *\n * @param {string|Array<string>} url - url(s) to animation(.vmd) file(s)\n * @param {SkinnedMesh|THREE.Camera} object - tracks will be fitting to this object\n * @param {function} onLoad\n * @param {function} onProgress\n * @param {function} onError\n */\n loadAnimation(url, object, onLoad, onProgress, onError) {\n const builder = this.animationBuilder\n\n this.loadVMD(\n url,\n function (vmd) {\n onLoad(object.isCamera ? builder.buildCameraAnimation(vmd) : builder.build(vmd, object))\n },\n onProgress,\n onError,\n )\n }\n\n /**\n * Loads mode file and motion file(s) as an object containing\n * a SkinnedMesh and a AnimationClip.\n * Tracks of AnimationClip are fitting to the model.\n *\n * @param {string} modelUrl - url to Model(.pmd or .pmx) file\n * @param {string|Array{string}} vmdUrl - url(s) to animation(.vmd) file\n * @param {function} onLoad\n * @param {function} onProgress\n * @param {function} onError\n */\n loadWithAnimation(modelUrl, vmdUrl, onLoad, onProgress, onError) {\n const scope = this\n\n this.load(\n modelUrl,\n function (mesh) {\n scope.loadAnimation(\n vmdUrl,\n mesh,\n function (animation) {\n onLoad({\n mesh: mesh,\n animation: animation,\n })\n },\n onProgress,\n onError,\n )\n },\n onProgress,\n onError,\n )\n }\n\n // Load MMD assets as Object data parsed by MMDParser\n\n /**\n * Loads .pmd file as an Object.\n *\n * @param {string} url - url to .pmd file\n * @param {function} onLoad\n * @param {function} onProgress\n * @param {function} onError\n */\n loadPMD(url, onLoad, onProgress, onError) {\n const parser = this._getParser()\n\n this.loader\n .setMimeType(undefined)\n .setPath(this.path)\n .setResponseType('arraybuffer')\n .setRequestHeader(this.requestHeader)\n .setWithCredentials(this.withCredentials)\n .load(\n url,\n function (buffer) {\n onLoad(parser.parsePmd(buffer, true))\n },\n onProgress,\n onError,\n )\n }\n\n /**\n * Loads .pmx file as an Object.\n *\n * @param {string} url - url to .pmx file\n * @param {function} onLoad\n * @param {function} onProgress\n * @param {function} onError\n */\n loadPMX(url, onLoad, onProgress, onError) {\n const parser = this._getParser()\n\n this.loader\n .setMimeType(undefined)\n .setPath(this.path)\n .setResponseType('arraybuffer')\n .setRequestHeader(this.requestHeader)\n .setWithCredentials(this.withCredentials)\n .load(\n url,\n function (buffer) {\n onLoad(parser.parsePmx(buffer, true))\n },\n onProgress,\n onError,\n )\n }\n\n /**\n * Loads .vmd file as an Object. If two or more files are specified\n * they'll be merged.\n *\n * @param {string|Array<string>} url - url(s) to .vmd file(s)\n * @param {function} onLoad\n * @param {function} onProgress\n * @param {function} onError\n */\n loadVMD(url, onLoad, onProgress, onError) {\n const urls = Array.isArray(url) ? url : [url]\n\n const vmds = []\n const vmdNum = urls.length\n\n const parser = this._getParser()\n\n this.loader\n .setMimeType(undefined)\n .setPath(this.animationPath)\n .setResponseType('arraybuffer')\n .setRequestHeader(this.requestHeader)\n .setWithCredentials(this.withCredentials)\n\n for (let i = 0, il = urls.length; i < il; i++) {\n this.loader.load(\n urls[i],\n function (buffer) {\n vmds.push(parser.parseVmd(buffer, true))\n\n if (vmds.length === vmdNum) onLoad(parser.mergeVmds(vmds))\n },\n onProgress,\n onError,\n )\n }\n }\n\n /**\n * Loads .vpd file as an Object.\n *\n * @param {string} url - url to .vpd file\n * @param {boolean} isUnicode\n * @param {function} onLoad\n * @param {function} onProgress\n * @param {function} onError\n */\n loadVPD(url, isUnicode, onLoad, onProgress, onError) {\n const parser = this._getParser()\n\n this.loader\n .setMimeType(isUnicode ? undefined : 'text/plain; charset=shift_jis')\n .setPath(this.animationPath)\n .setResponseType('text')\n .setRequestHeader(this.requestHeader)\n .setWithCredentials(this.withCredentials)\n .load(\n url,\n function (text) {\n onLoad(parser.parseVpd(text, true))\n },\n onProgress,\n onError,\n )\n }\n\n // private methods\n\n _extractExtension(url) {\n const index = url.lastIndexOf('.')\n return index < 0 ? '' : url.slice(index + 1)\n }\n\n _getParser() {\n if (this.parser === null) {\n this.parser = new Parser()\n }\n\n return this.parser\n }\n}\n\n// Utilities\n\n/*\n * base64 encoded defalut toon textures toon00.bmp - toon10.bmp.\n * We don't need to request external toon image files.\n * This idea is from http://www20.atpages.jp/katwat/three.js_r58/examples/mytest37/mmd.three.js\n */\nconst DEFAULT_TOON_TEXTURES = [\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/bWiiMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh8aBHZBl14e8wAAAABJRU5ErkJggg==',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOUlEQVRYR+3WMREAMAwDsYY/yoDI7MLwIiP40+RJklfcCCBAgAABAgTqArfb/QMCCBAgQIAAgbbAB3z/e0F3js2cAAAAAElFTkSuQmCC',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAN0lEQVRYR+3WQREAMBACsZ5/B5ilMvgEBTt5cW37hjsBBAgQIECAwFwgyfYPCCBAgAABAgTWAh81dWyx0gFwKAAAAABJRU5ErkJggg==',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAOklEQVRYR+3WoREAMAwDsWb/UQtCy9wxTOQJ/oQ8SXKKGwEECBAgQIBAXeDt7f4BAQQIECBAgEBb4AOz8Hzx7WLY4wAAAABJRU5ErkJggg==',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABPUlEQVRYR+1XwW7CMAy1+f9fZOMysSEOEweEOPRNdm3HbdOyIhAcklPrOs/PLy9RygBALxzcCDQFmgJNgaZAU6Ap0BR4PwX8gsRMVLssMRH5HcpzJEaWL7EVg9F1IHRlyqQohgVr4FGUlUcMJSjcUlDw0zvjeun70cLWmneoyf7NgBTQSniBTQQSuJAZsOnnaczjIMb5hCiuHKxokCrJfVnrctyZL0PkJAJe1HMil4nxeyi3Ypfn1kX51jpPvo/JeCNC4PhVdHdJw2XjBR8brF8PEIhNVn12AgP7uHsTBguBn53MUZCqv7Lp07Pn5k1Ro+uWmUNn7D+M57rtk7aG0Vo73xyF/fbFf0bPJjDXngnGocDTdFhygZjwUQrMNrDcmZlQT50VJ/g/UwNyHpu778+yW+/ksOz/BFo54P4AsUXMfRq7XWsAAAAASUVORK5CYII=',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACMElEQVRYR+2Xv4pTQRTGf2dubhLdICiii2KnYKHVolhauKWPoGAnNr6BD6CvIVaihYuI2i1ia0BY0MZGRHQXjZj/mSPnnskfNWiWZUlzJ5k7M2cm833nO5Mziej2DWWJRUoCpQKlAntSQCqgw39/iUWAGmh37jrRnVsKlgpiqmkoGVABA7E57fvY+pJDdgKqF6HzFCSADkDq+F6AHABtQ+UMVE5D7zXod7fFNhTEckTbj5XQgHzNN+5tQvc5NG7C6BNkp6D3EmpXHDR+dQAjFLchW3VS9rlw3JBh+B7ys5Cf9z0GW1C/7P32AyBAOAz1q4jGliIH3YPuBnSfQX4OGreTIgEYQb/pBDtPnEQ4CivXYPAWBk13oHrB54yA9QuSn2H4AcKRpEILDt0BUzj+RLR1V5EqjD66NPRBVpLcQwjHoHYJOhsQv6U4mnzmrIXJCFr4LDwm/xBUoboG9XX4cc9VKdYoSA2yk5NQLJaKDUjTBoveG3Z2TElTxwjNK4M3LEZgUdDdruvcXzKBpStgp2NPiWi3ks9ZXxIoFVi+AvHLdc9TqtjL3/aYjpPlrzOcEnK62Szhimdd7xX232zFDTgtxezOu3WNMRLjiKgjtOhHVMd1loynVHvOgjuIIJMaELEqhJAV/RCSLbWTcfPFakFgFlALTRRvx+ok6Hlp/Q+v3fmx90bMyUzaEAhmM3KvHlXTL5DxnbGf/1M8RNNACLL5MNtPxP/mypJAqcDSFfgFhpYqWUzhTEAAAAAASUVORK5CYII=',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=',\n 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAL0lEQVRYR+3QQREAAAzCsOFfNJPBJ1XQS9r2hsUAAQIECBAgQIAAAQIECBAgsBZ4MUx/ofm2I/kAAAAASUVORK5CYII=',\n]\n\n// Builders. They build Three.js object from Object data parsed by MMDParser.\n\n/**\n * @param {THREE.LoadingManager} manager\n */\nclass MeshBuilder {\n constructor(manager) {\n this.crossOrigin = 'anonymous'\n this.geometryBuilder = new GeometryBuilder()\n this.materialBuilder = new MaterialBuilder(manager)\n }\n\n /**\n * @param {string} crossOrigin\n * @return {MeshBuilder}\n */\n setCrossOrigin(crossOrigin) {\n this.crossOrigin = crossOrigin\n return this\n }\n\n /**\n * @param {Object} data - parsed PMD/PMX data\n * @param {string} resourcePath\n * @param {function} onProgress\n * @param {function} onError\n * @return {SkinnedMesh}\n */\n build(data, resourcePath, onProgress, onError) {\n const geometry = this.geometryBuilder.build(data)\n const material = this.materialBuilder\n .setCrossOrigin(this.crossOrigin)\n .setResourcePath(resourcePath)\n .build(data, geometry, onProgress, onError)\n\n const mesh = new SkinnedMesh(geometry, material)\n\n const skeleton = new Skeleton(initBones(mesh))\n mesh.bind(skeleton)\n\n // console.log( mesh ); // for console debug\n\n return mesh\n }\n}\n\n// TODO: Try to remove this function\n\nfunction initBones(mesh) {\n const geometry = mesh.geometry\n\n const bones = []\n\n if (geometry && geometry.bones !== undefined) {\n // first, create array of 'Bone' objects from geometry data\n\n for (let i = 0, il = geometry.bones.length; i < il; i++) {\n const gbone = geometry.bones[i]\n\n // create new 'Bone' object\n\n const bone = new Bone()\n bones.push(bone)\n\n // apply values\n\n bone.name = gbone.name\n bone.position.fromArray(gbone.pos)\n bone.quaternion.fromArray(gbone.rotq)\n if (gbone.scl !== undefined) bone.scale.fromArray(gbone.scl)\n }\n\n // second, create bone hierarchy\n\n for (let i = 0, il = geometry.bones.length; i < il; i++) {\n const gbone = geometry.bones[i]\n\n if (gbone.parent !== -1 && gbone.parent !== null && bones[gbone.parent] !== undefined) {\n // subsequent bones in the hierarchy\n\n bones[gbone.parent].add(bones[i])\n } else {\n // topmost bone, immediate child of the skinned mesh\n\n mesh.add(bones[i])\n }\n }\n }\n\n // now the bones are part of the scene graph and children of the skinned mesh.\n // let's update the corresponding matrices\n\n mesh.updateMatrixWorld(true)\n\n return bones\n}\n\n//\n\nclass GeometryBuilder {\n /**\n * @param {Object} data - parsed PMD/PMX data\n * @return {BufferGeometry}\n */\n build(data) {\n // for geometry\n const positions = []\n const uvs = []\n const normals = []\n\n const indices = []\n\n const groups = []\n\n const bones = []\n const skinIndices = []\n const skinWeights = []\n\n const morphTargets = []\n const morphPositions = []\n\n const iks = []\n const grants = []\n\n const rigidBodies = []\n const constraints = []\n\n // for work\n let offset = 0\n const boneTypeTable = {}\n\n // positions, normals, uvs, skinIndices, skinWeights\n\n for (let i = 0; i < data.metadata.vertexCount; i++) {\n const v = data.vertices[i]\n\n for (let j = 0, jl = v.position.length; j < jl; j++) {\n positions.push(v.position[j])\n }\n\n for (let j = 0, jl = v.normal.length; j < jl; j++) {\n normals.push(v.normal[j])\n }\n\n for (let j = 0, jl = v.uv.length; j < jl; j++) {\n uvs.push(v.uv[j])\n }\n\n for (let j = 0; j < 4; j++) {\n skinIndices.push(v.skinIndices.length - 1 >= j ? v.skinIndices[j] : 0.0)\n }\n\n for (let j = 0; j < 4; j++) {\n skinWeights.push(v.skinWeights.length - 1 >= j ? v.skinWeights[j] : 0.0)\n }\n }\n\n // indices\n\n for (let i = 0; i < data.metadata.faceCount; i++) {\n const face = data.faces[i]\n\n for (let j = 0, jl = face.indices.length; j < jl; j++) {\n indices.push(face.indices[j])\n }\n }\n\n // groups\n\n for (let i = 0; i < data.metadata.materialCount; i++) {\n const material = data.materials[i]\n\n groups.push({\n offset: offset * 3,\n count: material.faceCount * 3,\n })\n\n offset += material.faceCount\n }\n\n // bones\n\n for (let i = 0; i < data.metadata.rigidBodyCount; i++) {\n const body = data.rigidBodies[i]\n let value = boneTypeTable[body.boneIndex]\n\n // keeps greater number if already value is set without any special reasons\n value = value === undefined ? body.type : Math.max(body.type, value)\n\n boneTypeTable[body.boneIndex] = value\n }\n\n for (let i = 0; i < data.metadata.boneCount; i++) {\n const boneData = data.bones[i]\n\n const bone = {\n index: i,\n transformationClass: boneData.transformationClass,\n parent: boneData.parentIndex,\n name: boneData.name,\n pos: boneData.position.slice(0, 3),\n rotq: [0, 0, 0, 1],\n scl: [1, 1, 1],\n rigidBodyType: boneTypeTable[i] !== undefined ? boneTypeTable[i] : -1,\n }\n\n if (bone.parent !== -1) {\n bone.pos[0] -= data.bones[bone.parent].position[0]\n bone.pos[1] -= data.bones[bone.parent].position[1]\n bone.pos[2] -= data.bones[bone.parent].position[2]\n }\n\n bones.push(bone)\n }\n\n // iks\n\n // TODO: remove duplicated codes between PMD and PMX\n if (data.metadata.format === 'pmd') {\n for (let i = 0; i < data.metadata.ikCount; i++) {\n const ik = data.iks[i]\n\n const param = {\n target: ik.target,\n effector: ik.effector,\n iteration: ik.iteration,\n maxAngle: ik.maxAngle * 4,\n links: [],\n }\n\n for (let j = 0, jl = ik.links.length; j < jl; j++) {\n const link = {}\n link.index = ik.links[j].index\n link.enabled = true\n\n if (data.bones[link.index].name.indexOf('ひざ') >= 0) {\n link.limitation = new Vector3(1.0, 0.0, 0.0)\n }\n\n param.links.push(link)\n }\n\n iks.push(param)\n }\n } else {\n for (let i = 0; i < data.metadata.boneCount; i++) {\n const ik = data.bones[i].ik\n\n if (ik === undefined) continue\n\n const param = {\n target: i,\n effector: ik.effector,\n iteration: ik.iteration,\n maxAngle: ik.maxAngle,\n links: [],\n }\n\n for (let j = 0, jl = ik.links.length; j < jl; j++) {\n const link = {}\n link.index = ik.links[j].index\n link.enabled = true\n\n if (ik.links[j].angleLimitation === 1) {\n // Revert if rotationMin/Max doesn't work well\n // link.limitation = new Vector3( 1.0, 0.0, 0.0 );\n\n const rotationMin = ik.links[j].lowerLimitationAngle\n const rotationMax = ik.links[j].upperLimitationAngle\n\n // Convert Left to Right coordinate by myself because\n // MMDParser doesn't convert. It's a MMDParser's bug\n\n const tmp1 = -rotationMax[0]\n const tmp2 = -rotationMax[1]\n rotationMax[0] = -rotationMin[0]\n rotationMax[1] = -rotationMin[1]\n rotationMin[0] = tmp1\n rotationMin[1] = tmp2\n\n link.rotationMin = new Vector3().fromArray(rotationMin)\n link.rotationMax = new Vector3().fromArray(rotationMax)\n }\n\n param.links.push(link)\n }\n\n iks.push(param)\n\n // Save the reference even from bone data for efficiently\n // simulating PMX animation system\n bones[i].ik = param\n }\n }\n\n // grants\n\n if (data.metadata.format === 'pmx') {\n // bone index -> grant entry map\n const grantEntryMap = {}\n\n for (let i = 0; i < data.metadata.boneCount; i++) {\n const boneData = data.bones[i]\n const grant = boneData.grant\n\n if (grant === undefined) continue\n\n const param = {\n index: i,\n parentIndex: grant.parentIndex,\n ratio: grant.ratio,\n isLocal: grant.isLocal,\n affectRotation: grant.affectRotation,\n affectPosition: grant.affectPosition,\n transformationClass: boneData.transformationClass,\n }\n\n grantEntryMap[i] = { parent: null, children: [], param: param, visited: false }\n }\n\n const rootEntry = { parent: null, children: [], param: null, visited: false }\n\n // Build a tree representing grant hierarchy\n\n for (const boneIndex in grantEntryMap) {\n const grantEntry = grantEntryMap[boneIndex]\n const parentGrantEntry = grantEntryMap[grantEntry.parentIndex] || rootEntry\n\n grantEntry.parent = parentGrantEntry\n parentGrantEntry.children.push(grantEntry)\n }\n\n // Sort grant parameters from parents to children because\n // grant uses parent's transform that parent's grant is already applied\n // so grant should be applied in order from parents to children\n\n function traverse(entry) {\n if (entry.param) {\n grants.push(entry.param)\n\n // Save the reference even from bone data for efficiently\n // simulating PMX animation system\n bones[entry.param.index].grant = entry.param\n }\n\n entry.visited = true\n\n for (let i = 0, il = entry.children.length; i < il; i++) {\n const child = entry.children[i]\n\n // Cut off a loop if exists. (Is a grant loop invalid?)\n if (!child.visited) traverse(child)\n }\n }\n\n traverse(rootEntry)\n }\n\n // morph\n\n function updateAttributes(attribute, morph, ratio) {\n for (let i = 0; i < morph.elementCount; i++) {\n const element = morph.elements[i]\n\n let index\n\n if (data.metadata.format === 'pmd') {\n index = data.morphs[0].elements[element.index].index\n } else {\n index = element.index\n }\n\n attribute.array[index * 3 + 0] += element.position[0] * ratio\n attribute.array[index * 3 + 1] += element.position[1] * ratio\n attribute.array[index * 3 + 2] += element.position[2] * ratio\n }\n }\n\n for (let i = 0; i < data.metadata.morphCount; i++) {\n const morph = data.morphs[i]\n const params = { name: morph.name }\n\n const attribute = new Float32BufferAttribute(data.metadata.vertexCount * 3, 3)\n attribute.name = morph.name\n\n for (let j = 0; j < data.metadata.vertexCount * 3; j++) {\n attribute.array[j] = positions[j]\n }\n\n if (data.metadata.format === 'pmd') {\n if (i !== 0) {\n updateAttributes(attribute, morph, 1.0)\n }\n } else {\n if (morph.type === 0) {\n // group\n\n for (let j = 0; j < morph.elementCount; j++) {\n const morph2 = data.morphs[morph.elements[j].index]\n const ratio = morph.elements[j].ratio\n\n if (morph2.type === 1) {\n updateAttributes(attribute, morph2, ratio)\n } else {\n // TODO: implement\n }\n }\n } else if (morph.type === 1) {\n // vertex\n\n updateAttributes(attribute, morph, 1.0)\n } else if (morph.type === 2) {\n // bone\n // TODO: implement\n } else if (morph.type === 3) {\n // uv\n // TODO: implement\n } else if (morph.type === 4) {\n // additional uv1\n // TODO: implement\n } else if (morph.type === 5) {\n // additional uv2\n // TODO: implement\n } else if (morph.type === 6) {\n // additional uv3\n // TODO: implement\n } else if (morph.type === 7) {\n // additional uv4\n // TODO: implement\n } else if (morph.type === 8) {\n // material\n // TODO: implement\n }\n }\n\n morphTargets.push(params)\n morphPositions.push(attribute)\n }\n\n // rigid bodies from rigidBodies field.\n\n for (let i = 0; i < data.metadata.rigidBodyCount; i++) {\n const rigidBody = data.rigidBodies[i]\n const params = {}\n\n for (const key in rigidBody) {\n params[key] = rigidBody[key]\n }\n\n /*\n * RigidBody position parameter in PMX seems global position\n * while the one in PMD seems offset from corresponding bone.\n * So unify being offset.\n */\n if (data.metadata.format === 'pmx') {\n if (params.boneIndex !== -1) {\n const bone = data.bones[params.boneIndex]\n params.position[0] -= bone.position[0]\n params.position[1] -= bone.position[1]\n params.position[2] -= bone.position[2]\n }\n }\n\n rigidBodies.push(params)\n }\n\n // constraints from constraints field.\n\n for (let i = 0; i < data.metadata.constraintCount; i++) {\n const constraint = data.constraints[i]\n const params = {}\n\n for (const key in constraint) {\n params[key] = constraint[key]\n }\n\n const bodyA = rigidBodies[params.rigidBodyIndex1]\n const bodyB = rigidBodies[params.rigidBodyIndex2]\n\n // Refer to http://www20.atpages.jp/katwat/wp/?p=4135\n if (bodyA.type !== 0 && bodyB.type === 2) {\n if (\n bodyA.boneIndex !== -1 &&\n bodyB.boneIndex !== -1 &&\n data.bones[bodyB.boneIndex].parentIndex === bodyA.boneIndex\n ) {\n bodyB.type = 1\n }\n }\n\n constraints.push(params)\n }\n\n // build BufferGeometry.\n\n const geometry = new BufferGeometry()\n\n geometry.setAttribute('position', new Float32BufferAttribute(positions, 3))\n geometry.setAttribute('normal', new Float32BufferAttribute(normals, 3))\n geometry.setAttribute('uv', new Float32BufferAttribute(uvs, 2))\n geometry.setAttribute('skinIndex', new Uint16BufferAttribute(skinIndices, 4))\n geometry.setAttribute('skinWeight', new Float32BufferAttribute(skinWeights, 4))\n geometry.setIndex(indices)\n\n for (let i = 0, il = groups.length; i < il; i++) {\n geometry.addGroup(groups[i].offset, groups[i].count, i)\n }\n\n geometry.bones = bones\n\n geometry.morphTargets = morphTargets\n geometry.morphAttributes.position = morphPositions\n geometry.morphTargetsRelative = false\n\n geometry.userData.MMD = {\n bones: bones,\n iks: iks,\n grants: grants,\n rigidBodies: rigidBodies,\n constraints: constraints,\n format: data.metadata.format,\n }\n\n geometry.computeBoundingSphere()\n\n return geometry\n }\n}\n\n//\n\n/**\n * @param {THREE.LoadingManager} manager\n */\nclass MaterialBuilder {\n constructor(manager) {\n this.manager = manager\n\n this.textureLoader = new TextureLoader(this.manager)\n this.tgaLoader = null // lazy generation\n\n this.crossOrigin = 'anonymous'\n this.resourcePath = undefined\n }\n\n /**\n * @param {string} crossOrigin\n * @return {MaterialBuilder}\n */\n setCrossOrigin(crossOrigin) {\n this.crossOrigin = crossOrigin\n return this\n }\n\n /**\n * @param {string} resourcePath\n * @return {MaterialBuilder}\n */\n setResourcePath(resourcePath) {\n this.resourcePath = resourcePath\n return this\n }\n\n /**\n * @param {Object} data - parsed PMD/PMX data\n * @param {BufferGeometry} geometry - some properties are dependend on geometry\n * @param {function} onProgress\n * @param {function} onError\n * @return {Array<MeshToonMaterial>}\n */\n build(data, geometry /*, onProgress, onError */) {\n const materials = []\n\n const textures = {}\n\n this.textureLoader.setCrossOrigin(this.crossOrigin)\n\n // materials\n\n for (let i = 0; i < data.metadata.materialCount; i++) {\n const material = data.materials[i]\n\n const params = { userData: {} }\n\n if (material.name !== undefined) params.name = material.name\n\n /*\n * Color\n *\n * MMD MeshToonMaterial\n * diffuse - color\n * ambient - emissive * a\n * (a = 1.0 without map texture or 0.2 with map texture)\n *\n * MeshToonMaterial doesn't have ambient. Set it to emissive instead.\n * It'll be too bright if material has map texture so using coef 0.2.\n */\n params.color = new Color().fromArray(material.diffuse)\n params.opacity = material.diffuse[3]\n params.emissive = new Color().fromArray(material.ambient)\n params.transparent = params.opacity !== 1.0\n\n //\n\n params.skinning = geometry.bones.length > 0 ? true : false\n params.morphTargets = geometry.morphTargets.length > 0 ? true : false\n params.fog = true\n\n // blend\n\n params.blending = CustomBlending\n params.blendSrc = SrcAlphaFactor\n params.blendDst = OneMinusSrcAlphaFactor\n params.blendSrcAlpha = SrcAlphaFactor\n params.blendDstAlpha = DstAlphaFactor\n\n // side\n\n if (data.metadata.format === 'pmx' && (material.flag & 0x1) === 1) {\n params.side = DoubleSide\n } else {\n params.side = params.opacity === 1.0 ? FrontSide : DoubleSide\n }\n\n if (data.metadata.format === 'pmd') {\n // map, envMap\n\n if (material.fileName) {\n const fileName = material.fileName\n const fileNames = fileName.split('*')\n\n // fileNames[ 0 ]: mapFileName\n // fileNames[ 1 ]: envMapFileName( optional )\n\n params.map = this._loadTexture(fileNames[0], textures)\n\n if (fileNames.length > 1) {\n const extension = fileNames[1].slice(-4).toLowerCase()\n\n params.envMap = this._loadTexture(fileNames[1], textures)\n\n params.combine = extension === '.sph' ? MultiplyOperation : AddOperation\n }\n }\n\n // gradientMap\n\n const toonFileName = material.toonIndex === -1 ? 'toon00.bmp' : data.toonTextures[material.toonIndex].fileName\n\n params.gradientMap = this._loadTexture(toonFileName, textures, {\n isToonTexture: true,\n isDefaultToonTexture: this._isDefaultToonTexture(toonFileName),\n })\n\n // parameters for OutlineEffect\n\n params.userData.outlineParameters = {\n thickness: material.edgeFlag === 1 ? 0.003 : 0.0,\n color: [0, 0, 0],\n alpha: 1.0,\n visible: material.edgeFlag === 1,\n }\n } else {\n // map\n\n if (material.textureIndex !== -1) {\n params.map = this._loadTexture(data.textures[material.textureIndex], textures)\n }\n\n // envMap TODO: support m.envFlag === 3\n\n if (material.envTextureIndex !== -1 && (material.envFlag === 1 || material.envFlag == 2)) {\n params.envMap = this._loadTexture(data.textures[material.envTextureIndex], textures)\n\n params.combine = material.envFlag === 1 ? MultiplyOperation : AddOperation\n }\n\n // gradientMap\n\n let toonFileName, isDefaultToon\n\n if (material.toonIndex === -1 || material.toonFlag !== 0) {\n toonFileName = 'toon' + ('0' + (material.toonIndex + 1)).slice(-2) + '.bmp'\n isDefaultToon = true\n } else {\n toonFileName = data.textures[material.toonIndex]\n isDefaultToon = false\n }\n\n params.gradientMap = this._loadTexture(toonFileName, textures, {\n isToonTexture: true,\n isDefaultToonTexture: isDefaultToon,\n })\n\n // parameters for OutlineEffect\n params.userData.outlineParameters = {\n thickness: material.edgeSize / 300, // TODO: better calculation?\n color: material.edgeColor.slice(0, 3),\n alpha: material.edgeColor[3],\n visible: (material.flag & 0x10) !== 0 && material.edgeSize > 0.0,\n }\n }\n\n if (params.map !== undefined) {\n if (!params.transparent) {\n this._checkImageTransparency(params.map, geometry, i)\n }\n\n params.emissive.multiplyScalar(0.2)\n }\n\n materials.push(new MeshToonMaterial(params))\n }\n\n if (data.metadata.format === 'pmx') {\n // set transparent true if alpha morph is defined.\n\n function checkAlphaMorph(elements, materials) {\n for (let i = 0, il = elements.length; i < il; i++) {\n const element = elements[i]\n\n if (element.index === -1) continue\n\n const material = materials[element.index]\n\n if (material.opacity !== element.diffuse[3]) {\n material.transparent = true\n }\n }\n }\n\n for (let i = 0, il = data.morphs.length; i < il; i++) {\n const morph = data.morphs[i]\n const elements = morph.elements\n\n if (morph.type === 0) {\n for (let j = 0, jl = elements.length; j < jl; j++) {\n const morph2 = data.morphs[elements[j].index]\n\n if (morph2.type !== 8) continue\n\n checkAlphaMorph(morph2.elements, materials)\n }\n } else if (morph.type === 8) {\n checkAlphaMorph(elements, materials)\n }\n }\n }\n\n return materials\n }\n\n // private methods\n\n _getTGALoader() {\n if (this.tgaLoader === null) {\n if (TGALoader === undefined) {\n throw new Error('THREE.MMDLoader: Import TGALoader')\n }\n\n this.tgaLoader = new TGALoader(this.manager)\n }\n\n return this.tgaLoader\n }\n\n _isDefaultToonTexture(name) {\n if (name.length !== 10) return false\n\n return /toon(10|0[0-9])\\.bmp/.test(name)\n }\n\n _loadTexture(filePath, textures, params, onProgress, onError) {\n params = params || {}\n\n const scope = this\n\n let fullPath\n\n if (params.isDefaultToonTexture === true) {\n let index\n\n try {\n index = parseInt(filePath.match(/toon([0-9]{2})\\.bmp$/)[1])\n } catch (e) {\n console.warn(\n 'THREE.MMDLoader: ' +\n filePath +\n ' seems like a ' +\n 'not right default texture path. Using toon00.bmp instead.',\n )\n\n index = 0\n }\n\n fullPath = DEFAULT_TOON_TEXTURES[index]\n } else {\n fullPath = this.resourcePath + filePath\n }\n\n if (textures[fullPath] !== undefined) return textures[fullPath]\n\n let loader = this.manager.getHandler(fullPath)\n\n if (loader === null) {\n loader = filePath.slice(-4).toLowerCase() === '.tga' ? this._getTGALoader() : this.textureLoader\n }\n\n const texture = loader.load(\n fullPath,\n function (t) {\n // MMD toon texture is Axis-Y oriented\n // but Three.js gradient map is Axis-X oriented.\n // So here replaces the toon texture image with the rotated one.\n if (params.isToonTexture === true) {\n t.image = scope._getRotatedImage(t.image)\n\n t.magFilter = NearestFilter\n t.minFilter = NearestFilter\n }\n\n t.flipY = false\n t.wrapS = RepeatWrapping\n t.wrapT = RepeatWrapping\n\n for (let i = 0; i < texture.readyCallbacks.length; i++) {\n texture.readyCallbacks[i](texture)\n }\n\n delete texture.readyCallbacks\n },\n onProgress,\n onError,\n )\n\n texture.readyCallbacks = []\n\n textures[fullPath] = texture\n\n return texture\n }\n\n _getRotatedImage(image) {\n const canvas = document.createElement('canvas')\n const context = canvas.getContext('2d')\n\n const width = image.width\n const height = image.height\n\n canvas.width = width\n canvas.height = height\n\n context.clearRect(0, 0, width, height)\n context.translate(width / 2.0, height / 2.0)\n context.rotate(0.5 * Math.PI) // 90.0 * Math.PI / 180.0\n context.translate(-width / 2.0, -height / 2.0)\n context.drawImage(image, 0, 0)\n\n return context.getImageData(0, 0, width, height)\n }\n\n // Check if the partial image area used by the texture is transparent.\n _checkImageTransparency(map, geometry, groupIndex) {\n map.readyCallbacks.push(function (texture) {\n // Is there any efficient ways?\n function createImageData(image) {\n const canvas = document.createElement('canvas')\n canvas.width = image.width\n canvas.height = image.height\n\n const context = canvas.getContext('2d')\n context.drawImage(image, 0, 0)\n\n return context.getImageData(0, 0, canvas.width, canvas.height)\n }\n\n function detectImageTransparency(image, uvs, indices) {\n const width = image.width\n const height = image.height\n const data = image.data\n const threshold = 253\n\n if (data.length / (width * height) !== 4) return false\n\n for (let i = 0; i < indices.length; i += 3) {\n const centerUV = { x: 0.0, y: 0.0 }\n\n for (let j = 0; j < 3; j++) {\n const index = indices[i * 3 + j]\n const uv = { x: uvs[index * 2 + 0], y: uvs[index * 2 + 1] }\n\n if (getAlphaByUv(image, uv) < threshold) return true\n\n centerUV.x += uv.x\n centerUV.y += uv.y\n }\n\n centerUV.x /= 3\n centerUV.y /= 3\n\n if (getAlphaByUv(image, centerUV) < threshold) return true\n }\n\n return false\n }\n\n /*\n * This method expects\n * texture.flipY = false\n * texture.wrapS = RepeatWrapping\n * texture.wrapT = RepeatWrapping\n * TODO: more precise\n */\n function getAlphaByUv(image, uv) {\n const width = image.width\n const height = image.height\n\n let x = Math.round(uv.x * width) % width\n let y = Math.round(uv.y * height) % height\n\n if (x < 0) x += width\n if (y < 0) y += height\n\n const index = y * width + x\n\n return image.data[index * 4 + 3]\n }\n\n const imageData = texture.image.data !== undefined ? texture.image : createImageData(texture.image)\n\n const group = geometry.groups[groupIndex]\n\n if (\n detectImageTransparency(\n imageData,\n geometry.attributes.uv.array,\n geometry.index.array.slice(group.start, group.start + group.count),\n )\n ) {\n map.transparent = true\n }\n })\n }\n}\n\n//\n\nclass AnimationBuilder {\n /**\n * @param {Object} vmd - parsed VMD data\n * @param {SkinnedMesh} mesh - tracks will be fitting to mesh\n * @return {AnimationClip}\n */\n build(vmd, mesh) {\n // combine skeletal and morph animations\n\n const tracks = this.buildSkeletalAnimation(vmd, mesh).tracks\n const tracks2 = this.buildMorphAnimation(vmd, mesh).tracks\n\n for (let i = 0, il = tracks2.length; i < il; i++) {\n tracks.push(tracks2[i])\n }\n\n return new AnimationClip('', -1, tracks)\n }\n\n /**\n * @param {Object} vmd - parsed VMD data\n * @param {SkinnedMesh} mesh - tracks will be fitting to mesh\n * @return {AnimationClip}\n */\n buildSkeletalAnimation(vmd, mesh) {\n function pushInterpolation(array, interpolation, index) {\n array.push(interpolation[index + 0] / 127) // x1\n array.push(interpolation[index + 8] / 127) // x2\n array.push(interpolation[index + 4] / 127) // y1\n array.push(interpolation[index + 12] / 127) // y2\n }\n\n const tracks = []\n\n const motions = {}\n const bones = mesh.skeleton.bones\n const boneNameDictionary = {}\n\n for (let i = 0, il = bones.length; i < il; i++) {\n boneNameDictionary[bones[i].name] = true\n }\n\n for (let i = 0; i < vmd.metadata.motionCount; i++) {\n const motion = vmd.motions[i]\n const boneName = motion.boneName\n\n if (boneNameDictionary[boneName] === undefined) continue\n\n motions[boneName] = motions[boneName] || []\n motions[boneName].push(motion)\n }\n\n for (const key in motions) {\n const array = motions[key]\n\n array.sort(function (a, b) {\n return a.frameNum - b.frameNum\n })\n\n const times = []\n const positions = []\n const rotations = []\n const pInterpolations = []\n const rInterpolations = []\n\n const basePosition = mesh.skeleton.getBoneByName(key).position.toArray()\n\n for (let i = 0, il = array.length; i < il; i++) {\n const time = array[i].frameNum / 30\n const position = array[i].position\n const rotation = array[i].rotation\n const interpolation = array[i].interpolation\n\n times.push(time)\n\n for (let j = 0; j < 3; j++) positions.push(basePosition[j] + position[j])\n for (let j = 0; j < 4; j++) rotations.push(rotation[j])\n for (let j = 0; j < 3; j++) pushInterpolation(pInterpolations, interpolation, j)\n\n pushInterpolation(rInterpolations, interpolation, 3)\n }\n\n const targetName = '.bones[' + key + ']'\n\n tracks.push(this._createTrack(targetName + '.position', VectorKeyframeTrack, times, positions, pInterpolations))\n tracks.push(\n this._createTrack(targetName + '.quaternion', QuaternionKeyframeTrack, times, rotations, rInterpolations),\n )\n }\n\n return new AnimationClip('', -1, tracks)\n }\n\n /**\n * @param {Object} vmd - parsed VMD data\n * @param {SkinnedMesh} mesh - tracks will be fitting to mesh\n * @return {AnimationClip}\n */\n buildMorphAnimation(vmd, mesh) {\n const tracks = []\n\n const morphs = {}\n const morphTargetDictionary = mesh.morphTargetDictionary\n\n for (let i = 0; i < vmd.metadata.morphCount; i++) {\n const morph = vmd.morphs[i]\n const morphName = morph.morphName\n\n if (morphTargetDictionary[morphName] === undefined) continue\n\n morphs[morphName] = morphs[morphName] || []\n morphs[morphName].push(morph)\n }\n\n for (const key in morphs) {\n const array = morphs[key]\n\n array.sort(function (a, b) {\n return a.frameNum - b.frameNum\n })\n\n const times = []\n const values = []\n\n for (let i = 0, il = array.length; i < il; i++) {\n times.push(array[i].frameNum / 30)\n values.push(array[i].weight)\n }\n\n tracks.push(new NumberKeyframeTrack('.morphTargetInfluences[' + morphTargetDictionary[key] + ']', times, values))\n }\n\n return new AnimationClip('', -1, tracks)\n }\n\n /**\n * @param {Object} vmd - parsed VMD data\n * @return {AnimationClip}\n */\n buildCameraAnimation(vmd) {\n function pushVector3(array, vec) {\n array.push(vec.x)\n array.push(vec.y)\n array.push(vec.z)\n }\n\n function pushQuaternion(array, q) {\n array.push(q.x)\n array.push(q.y)\n array.push(q.z)\n array.push(q.w)\n }\n\n function pushInterpolation(array, interpolation, index) {\n array.push(interpolation[index * 4 + 0] / 127) // x1\n array.push(interpolation[index * 4 + 1] / 127) // x2\n array.push(interpolation[index * 4 + 2] / 127) // y1\n array.push(interpolation[index * 4 + 3] / 127) // y2\n }\n\n const cameras = vmd.cameras === undefined ? [] : vmd.cameras.slice()\n\n cameras.sort(function (a, b) {\n return a.frameNum - b.frameNum\n })\n\n const times = []\n const centers = []\n const quaternions = []\n const positions = []\n const fovs = []\n\n const cInterpolations = []\n const qInterpolations = []\n const pInterpolations = []\n const fInterpolations = []\n\n const quaternion = new Quaternion()\n const euler = new Euler()\n const position = new Vector3()\n const center = new Vector3()\n\n for (let i = 0, il = cameras.length; i < il; i++) {\n const motion = cameras[i]\n\n const time = motion.frameNum / 30\n const pos = motion.position\n const rot = motion.rotation\n const distance = motion.distance\n const fov = motion.fov\n const interpolation = motion.interpolation\n\n times.push(time)\n\n position.set(0, 0, -distance)\n center.set(pos[0], pos[1], pos[2])\n\n euler.set(-rot[0], -rot[1], -rot[2])\n quaternion.setFromEuler(euler)\n\n position.add(center)\n position.applyQuaternion(quaternion)\n\n pushVector3(centers, center)\n pushQuaternion(quaternions, quaternion)\n pushVector3(positions, position)\n\n fovs.push(fov)\n\n for (let j = 0; j < 3; j++) {\n pushInterpolation(cInterpolations, interpolation, j)\n }\n\n pushInterpolation(qInterpolations, interpolation, 3)\n\n // use the same parameter for x, y, z axis.\n for (let j = 0; j < 3; j++) {\n pushInterpolation(pInterpolations, interpolation, 4)\n }\n\n pushInterpolation(fInterpolations, interpolation, 5)\n }\n\n const tracks = []\n\n // I expect an object whose name 'target' exists under THREE.Camera\n tracks.push(this._createTrack('target.position', VectorKeyframeTrack, times, centers, cInterpolations))\n\n tracks.push(this._createTrack('.quaternion', QuaternionKeyframeTrack, times, quaternions, qInterpolations))\n tracks.push(this._createTrack('.position', VectorKeyframeTrack, times, positions, pInterpolations))\n tracks.push(this._createTrack('.fov', NumberKeyframeTrack, times, fovs, fInterpolations))\n\n return new AnimationClip('', -1, tracks)\n }\n\n // private method\n\n _createTrack(node, typedKeyframeTrack, times, values, interpolations) {\n /*\n * optimizes here not to let KeyframeTrackPrototype optimize\n * because KeyframeTrackPrototype optimizes times and values but\n * doesn't optimize interpolations.\n */\n if (times.length > 2) {\n times = times.slice()\n values = values.slice()\n interpolations = interpolations.slice()\n\n const stride = values.length / times.length\n const interpolateStride = interpolations.length / times.length\n\n let index = 1\n\n for (let aheadIndex = 2, endIndex = times.length; aheadIndex < endIndex; aheadIndex++) {\n for (let i = 0; i < stride; i++) {\n if (\n values[index * stride + i] !== values[(index - 1) * stride + i] ||\n values[index * stride + i] !== values[aheadIndex * stride + i]\n ) {\n index++\n break\n }\n }\n\n if (aheadIndex > index) {\n times[index] = times[aheadIndex]\n\n for (let i = 0; i < stride; i++) {\n values[index * stride + i] = values[aheadIndex * stride + i]\n }\n\n for (let i = 0; i < interpolateStride; i++) {\n interpolations[index * interpolateStride + i] = interpolations[aheadIndex * interpolateStride + i]\n }\n }\n }\n\n times.length = index + 1\n values.length = (index + 1) * stride\n interpolations.length = (index + 1) * interpolateStride\n }\n\n const track = new typedKeyframeTrack(node, times, values)\n\n track.createInterpolant = function InterpolantFactoryMethodCubicBezier(result) {\n return new CubicBezierInterpolation(\n this.times,\n this.values,\n this.getValueSize(),\n result,\n new Float32Array(interpolations),\n )\n }\n\n return track\n }\n}\n\n// interpolation\n\nclass CubicBezierInterpolation extends Interpolant {\n constructor(parameterPositions, sampleValues, sampleSize, resultBuffer, params) {\n super(parameterPositions, sampleValues, sampleSize, resultBuffer)\n\n this.interpolationParams = params\n }\n\n interpolate_(i1, t0, t, t1) {\n const result = this.resultBuffer\n const values = this.sampleValues\n const stride = this.valueSize\n const params = this.interpolationParams\n\n const offset1 = i1 * stride\n const offset0 = offset1 - stride\n\n // No interpolation if next key frame is in one frame in 30fps.\n // This is from MMD animation spec.\n // '1.5' is for precision loss. times are Float32 in Three.js Animation system.\n const weight1 = t1 - t0 < (1 / 30) * 1.5 ? 0.0 : (t - t0) / (t1 - t0)\n\n if (stride === 4) {\n // Quaternion\n\n const x1 = params[i1 * 4 + 0]\n const x2 = params[i1 * 4 + 1]\n const y1 = params[i1 * 4 + 2]\n const y2 = params[i1 * 4 + 3]\n\n const ratio = this._calculate(x1, x2, y1, y2, weight1)\n\n Quaternion.slerpFlat(result, 0, values, offset0, values, offset1, ratio)\n } else if (stride === 3) {\n // Vector3\n\n for (let i = 0; i !== stride; ++i) {\n const x1 = params[i1 * 12 + i * 4 + 0]\n const x2 = params[i1 * 12 + i * 4 + 1]\n const y1 = params[i1 * 12 + i * 4 + 2]\n const y2 = params[i1 * 12 + i * 4 + 3]\n\n const ratio = this._calculate(x1, x2, y1, y2, weight1)\n\n result[i] = values[offset0 + i] * (1 - ratio) + values[offset1 + i] * ratio\n }\n } else {\n // Number\n\n const x1 = params[i1 * 4 + 0]\n const x2 = params[i1 * 4 + 1]\n const y1 = params[i1 * 4 + 2]\n const y2 = params[i1 * 4 + 3]\n\n const ratio = this._calculate(x1, x2, y1, y2, weight1)\n\n result[0] = values[offset0] * (1 - ratio) + values[offset1] * ratio\n }\n\n return result\n }\n\n _calculate(x1, x2, y1, y2, x) {\n /*\n * Cubic Bezier curves\n * https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves\n *\n * B(t) = ( 1 - t ) ^ 3 * P0\n * + 3 * ( 1 - t ) ^ 2 * t * P1\n * + 3 * ( 1 - t ) * t^2 * P2\n * + t ^ 3 * P3\n * ( 0 <= t <= 1 )\n *\n * MMD uses Cubic Bezier curves for bone and camera animation interpolation.\n * http://d.hatena.ne.jp/edvakf/20111016/1318716097\n *\n * x = ( 1 - t ) ^ 3 * x0\n * + 3 * ( 1 - t ) ^ 2 * t * x1\n * + 3 * ( 1 - t ) * t^2 * x2\n * + t ^ 3 * x3\n * y = ( 1 - t ) ^ 3 * y0\n * + 3 * ( 1 - t ) ^ 2 * t * y1\n * + 3 * ( 1 - t ) * t^2 * y2\n * + t ^ 3 * y3\n * ( x0 = 0, y0 = 0 )\n * ( x3