"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const THREE = require("three"); const CCDIKSolver = require("./CCDIKSolver.cjs"); const MMDPhysics = require("./MMDPhysics.cjs"); class MMDAnimationHelper { /** * @param {Object} params - (optional) * @param {boolean} params.sync - Whether animation durations of added objects are synched. Default is true. * @param {Number} params.afterglow - Default is 0.0. * @param {boolean} params.resetPhysicsOnLoop - Default is true. */ constructor(params = {}) { this.meshes = []; this.camera = null; this.cameraTarget = new THREE.Object3D(); this.cameraTarget.name = "target"; this.audio = null; this.audioManager = null; this.objects = /* @__PURE__ */ new WeakMap(); this.configuration = { sync: params.sync !== void 0 ? params.sync : true, afterglow: params.afterglow !== void 0 ? params.afterglow : 0, resetPhysicsOnLoop: params.resetPhysicsOnLoop !== void 0 ? params.resetPhysicsOnLoop : true, pmxAnimation: params.pmxAnimation !== void 0 ? params.pmxAnimation : false }; this.enabled = { animation: true, ik: true, grant: true, physics: true, cameraAnimation: true }; this.onBeforePhysics = function() { }; this.sharedPhysics = false; this.masterPhysics = null; } /** * Adds an Three.js Object to helper and setups animation. * The anmation durations of added objects are synched * if this.configuration.sync is true. * * @param {THREE.SkinnedMesh|THREE.Camera|THREE.Audio} object * @param {Object} params - (optional) * @param {THREE.AnimationClip|Array} params.animation - Only for THREE.SkinnedMesh and THREE.Camera. Default is undefined. * @param {boolean} params.physics - Only for THREE.SkinnedMesh. Default is true. * @param {Integer} params.warmup - Only for THREE.SkinnedMesh and physics is true. Default is 60. * @param {Number} params.unitStep - Only for THREE.SkinnedMesh and physics is true. Default is 1 / 65. * @param {Integer} params.maxStepNum - Only for THREE.SkinnedMesh and physics is true. Default is 3. * @param {Vector3} params.gravity - Only for THREE.SkinnedMesh and physics is true. Default ( 0, - 9.8 * 10, 0 ). * @param {Number} params.delayTime - Only for THREE.Audio. Default is 0.0. * @return {MMDAnimationHelper} */ add(object, params = {}) { if (object.isSkinnedMesh) { this._addMesh(object, params); } else if (object.isCamera) { this._setupCamera(object, params); } else if (object.type === "Audio") { this._setupAudio(object, params); } else { throw new Error( "THREE.MMDAnimationHelper.add: accepts only THREE.SkinnedMesh or THREE.Camera or THREE.Audio instance." ); } if (this.configuration.sync) this._syncDuration(); return this; } /** * Removes an Three.js Object from helper. * * @param {THREE.SkinnedMesh|THREE.Camera|THREE.Audio} object * @return {MMDAnimationHelper} */ remove(object) { if (object.isSkinnedMesh) { this._removeMesh(object); } else if (object.isCamera) { this._clearCamera(object); } else if (object.type === "Audio") { this._clearAudio(object); } else { throw new Error( "THREE.MMDAnimationHelper.remove: accepts only THREE.SkinnedMesh or THREE.Camera or THREE.Audio instance." ); } if (this.configuration.sync) this._syncDuration(); return this; } /** * Updates the animation. * * @param {Number} delta * @return {MMDAnimationHelper} */ update(delta) { if (this.audioManager !== null) this.audioManager.control(delta); for (let i = 0; i < this.meshes.length; i++) { this._animateMesh(this.meshes[i], delta); } if (this.sharedPhysics) this._updateSharedPhysics(delta); if (this.camera !== null) this._animateCamera(this.camera, delta); return this; } /** * Changes the pose of SkinnedMesh as VPD specifies. * * @param {THREE.SkinnedMesh} mesh * @param {Object} vpd - VPD content parsed MMDParser * @param {Object} params - (optional) * @param {boolean} params.resetPose - Default is true. * @param {boolean} params.ik - Default is true. * @param {boolean} params.grant - Default is true. * @return {MMDAnimationHelper} */ pose(mesh, vpd, params = {}) { if (params.resetPose !== false) mesh.pose(); const bones = mesh.skeleton.bones; const boneParams = vpd.bones; const boneNameDictionary = {}; for (let i = 0, il = bones.length; i < il; i++) { boneNameDictionary[bones[i].name] = i; } const vector = new THREE.Vector3(); const quaternion = new THREE.Quaternion(); for (let i = 0, il = boneParams.length; i < il; i++) { const boneParam = boneParams[i]; const boneIndex = boneNameDictionary[boneParam.name]; if (boneIndex === void 0) continue; const bone = bones[boneIndex]; bone.position.add(vector.fromArray(boneParam.translation)); bone.quaternion.multiply(quaternion.fromArray(boneParam.quaternion)); } mesh.updateMatrixWorld(true); if (this.configuration.pmxAnimation && mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === "pmx") { const sortedBonesData = this._sortBoneDataArray(mesh.geometry.userData.MMD.bones.slice()); const ikSolver = params.ik !== false ? this._createCCDIKSolver(mesh) : null; const grantSolver = params.grant !== false ? this.createGrantSolver(mesh) : null; this._animatePMXMesh(mesh, sortedBonesData, ikSolver, grantSolver); } else { if (params.ik !== false) { this._createCCDIKSolver(mesh).update(); } if (params.grant !== false) { this.createGrantSolver(mesh).update(); } } return this; } /** * Enabes/Disables an animation feature. * * @param {string} key * @param {boolean} enabled * @return {MMDAnimationHelper} */ enable(key, enabled) { if (this.enabled[key] === void 0) { throw new Error("THREE.MMDAnimationHelper.enable: unknown key " + key); } this.enabled[key] = enabled; if (key === "physics") { for (let i = 0, il = this.meshes.length; i < il; i++) { this._optimizeIK(this.meshes[i], enabled); } } return this; } /** * Creates an GrantSolver instance. * * @param {THREE.SkinnedMesh} mesh * @return {GrantSolver} */ createGrantSolver(mesh) { return new GrantSolver(mesh, mesh.geometry.userData.MMD.grants); } // private methods _addMesh(mesh, params) { if (this.meshes.indexOf(mesh) >= 0) { throw new Error("THREE.MMDAnimationHelper._addMesh: SkinnedMesh '" + mesh.name + "' has already been added."); } this.meshes.push(mesh); this.objects.set(mesh, { looped: false }); this._setupMeshAnimation(mesh, params.animation); if (params.physics !== false) { this._setupMeshPhysics(mesh, params); } return this; } _setupCamera(camera, params) { if (this.camera === camera) { throw new Error("THREE.MMDAnimationHelper._setupCamera: Camera '" + camera.name + "' has already been set."); } if (this.camera) this.clearCamera(this.camera); this.camera = camera; camera.add(this.cameraTarget); this.objects.set(camera, {}); if (params.animation !== void 0) { this._setupCameraAnimation(camera, params.animation); } return this; } _setupAudio(audio, params) { if (this.audio === audio) { throw new Error("THREE.MMDAnimationHelper._setupAudio: Audio '" + audio.name + "' has already been set."); } if (this.audio) this.clearAudio(this.audio); this.audio = audio; this.audioManager = new AudioManager(audio, params); this.objects.set(this.audioManager, { duration: this.audioManager.duration }); return this; } _removeMesh(mesh) { let found = false; let writeIndex = 0; for (let i = 0, il = this.meshes.length; i < il; i++) { if (this.meshes[i] === mesh) { this.objects.delete(mesh); found = true; continue; } this.meshes[writeIndex++] = this.meshes[i]; } if (!found) { throw new Error( "THREE.MMDAnimationHelper._removeMesh: SkinnedMesh '" + mesh.name + "' has not been added yet." ); } this.meshes.length = writeIndex; return this; } _clearCamera(camera) { if (camera !== this.camera) { throw new Error("THREE.MMDAnimationHelper._clearCamera: Camera '" + camera.name + "' has not been set yet."); } this.camera.remove(this.cameraTarget); this.objects.delete(this.camera); this.camera = null; return this; } _clearAudio(audio) { if (audio !== this.audio) { throw new Error("THREE.MMDAnimationHelper._clearAudio: Audio '" + audio.name + "' has not been set yet."); } this.objects.delete(this.audioManager); this.audio = null; this.audioManager = null; return this; } _setupMeshAnimation(mesh, animation) { const objects = this.objects.get(mesh); if (animation !== void 0) { const animations = Array.isArray(animation) ? animation : [animation]; objects.mixer = new THREE.AnimationMixer(mesh); for (let i = 0, il = animations.length; i < il; i++) { objects.mixer.clipAction(animations[i]).play(); } objects.mixer.addEventListener("loop", function(event) { const tracks = event.action._clip.tracks; if (tracks.length > 0 && tracks[0].name.slice(0, 6) !== ".bones") return; objects.looped = true; }); } objects.ikSolver = this._createCCDIKSolver(mesh); objects.grantSolver = this.createGrantSolver(mesh); return this; } _setupCameraAnimation(camera, animation) { const animations = Array.isArray(animation) ? animation : [animation]; const objects = this.objects.get(camera); objects.mixer = new THREE.AnimationMixer(camera); for (let i = 0, il = animations.length; i < il; i++) { objects.mixer.clipAction(animations[i]).play(); } } _setupMeshPhysics(mesh, params) { const objects = this.objects.get(mesh); if (params.world === void 0 && this.sharedPhysics) { const masterPhysics = this._getMasterPhysics(); if (masterPhysics !== null) world = masterPhysics.world; } objects.physics = this._createMMDPhysics(mesh, params); if (objects.mixer && params.animationWarmup !== false) { this._animateMesh(mesh, 0); objects.physics.reset(); } objects.physics.warmup(params.warmup !== void 0 ? params.warmup : 60); this._optimizeIK(mesh, true); } _animateMesh(mesh, delta) { const objects = this.objects.get(mesh); const mixer = objects.mixer; const ikSolver = objects.ikSolver; const grantSolver = objects.grantSolver; const physics = objects.physics; const looped = objects.looped; if (mixer && this.enabled.animation) { this._restoreBones(mesh); mixer.update(delta); this._saveBones(mesh); if (this.configuration.pmxAnimation && mesh.geometry.userData.MMD && mesh.geometry.userData.MMD.format === "pmx") { if (!objects.sortedBonesData) objects.sortedBonesData = this._sortBoneDataArray(mesh.geometry.userData.MMD.bones.slice()); this._animatePMXMesh( mesh, objects.sortedBonesData, ikSolver && this.enabled.ik ? ikSolver : null, grantSolver && this.enabled.grant ? grantSolver : null ); } else { if (ikSolver && this.enabled.ik) { mesh.updateMatrixWorld(true); ikSolver.update(); } if (grantSolver && this.enabled.grant) { grantSolver.update(); } } } if (looped === true && this.enabled.physics) { if (physics && this.configuration.resetPhysicsOnLoop) physics.reset(); objects.looped = false; } if (physics && this.enabled.physics && !this.sharedPhysics) { this.onBeforePhysics(mesh); physics.update(delta); } } // Sort bones in order by 1. transformationClass and 2. bone index. // In PMX animation system, bone transformations should be processed // in this order. _sortBoneDataArray(boneDataArray) { return boneDataArray.sort(function(a, b) { if (a.transformationClass !== b.transformationClass) { return a.transformationClass - b.transformationClass; } else { return a.index - b.index; } }); } // PMX Animation system is a bit too complex and doesn't great match to // Three.js Animation system. This method attempts to simulate it as much as // possible but doesn't perfectly simulate. // This method is more costly than the regular one so // you are recommended to set constructor parameter "pmxAnimation: true" // only if your PMX model animation doesn't work well. // If you need better method you would be required to write your own. _animatePMXMesh(mesh, sortedBonesData, ikSolver, grantSolver) { _quaternionIndex = 0; _grantResultMap.clear(); for (let i = 0, il = sortedBonesData.length; i < il; i++) { updateOne(mesh, sortedBonesData[i].index, ikSolver, grantSolver); } mesh.updateMatrixWorld(true); return this; } _animateCamera(camera, delta) { const mixer = this.objects.get(camera).mixer; if (mixer && this.enabled.cameraAnimation) { mixer.update(delta); camera.updateProjectionMatrix(); camera.up.set(0, 1, 0); camera.up.applyQuaternion(camera.quaternion); camera.lookAt(this.cameraTarget.position); } } _optimizeIK(mesh, physicsEnabled) { const iks = mesh.geometry.userData.MMD.iks; const bones = mesh.geometry.userData.MMD.bones; for (let i = 0, il = iks.length; i < il; i++) { const ik = iks[i]; const links = ik.links; for (let j = 0, jl = links.length; j < jl; j++) { const link = links[j]; if (physicsEnabled === true) { link.enabled = bones[link.index].rigidBodyType > 0 ? false : true; } else { link.enabled = true; } } } } _createCCDIKSolver(mesh) { if (CCDIKSolver.CCDIKSolver === void 0) { throw new Error("THREE.MMDAnimationHelper: Import CCDIKSolver."); } return new CCDIKSolver.CCDIKSolver(mesh, mesh.geometry.userData.MMD.iks); } _createMMDPhysics(mesh, params) { if (MMDPhysics.MMDPhysics === void 0) { throw new Error("THREE.MMDPhysics: Import MMDPhysics."); } return new MMDPhysics.MMDPhysics(mesh, mesh.geometry.userData.MMD.rigidBodies, mesh.geometry.userData.MMD.constraints, params); } /* * Detects the longest duration and then sets it to them to sync. * TODO: Not to access private properties ( ._actions and ._clip ) */ _syncDuration() { let max = 0; const objects = this.objects; const meshes = this.meshes; const camera = this.camera; const audioManager = this.audioManager; for (let i = 0, il = meshes.length; i < il; i++) { const mixer = this.objects.get(meshes[i]).mixer; if (mixer === void 0) continue; for (let j = 0; j < mixer._actions.length; j++) { const clip = mixer._actions[j]._clip; if (!objects.has(clip)) { objects.set(clip, { duration: clip.duration }); } max = Math.max(max, objects.get(clip).duration); } } if (camera !== null) { const mixer = this.objects.get(camera).mixer; if (mixer !== void 0) { for (let i = 0, il = mixer._actions.length; i < il; i++) { const clip = mixer._actions[i]._clip; if (!objects.has(clip)) { objects.set(clip, { duration: clip.duration }); } max = Math.max(max, objects.get(clip).duration); } } } if (audioManager !== null) { max = Math.max(max, objects.get(audioManager).duration); } max += this.configuration.afterglow; for (let i = 0, il = this.meshes.length; i < il; i++) { const mixer = this.objects.get(this.meshes[i]).mixer; if (mixer === void 0) continue; for (let j = 0, jl = mixer._actions.length; j < jl; j++) { mixer._actions[j]._clip.duration = max; } } if (camera !== null) { const mixer = this.objects.get(camera).mixer; if (mixer !== void 0) { for (let i = 0, il = mixer._actions.length; i < il; i++) { mixer._actions[i]._clip.duration = max; } } } if (audioManager !== null) { audioManager.duration = max; } } // workaround _updatePropertyMixersBuffer(mesh) { const mixer = this.objects.get(mesh).mixer; const propertyMixers = mixer._bindings; const accuIndex = mixer._accuIndex; for (let i = 0, il = propertyMixers.length; i < il; i++) { const propertyMixer = propertyMixers[i]; const buffer = propertyMixer.buffer; const stride = propertyMixer.valueSize; const offset = (accuIndex + 1) * stride; propertyMixer.binding.getValue(buffer, offset); } } /* * Avoiding these two issues by restore/save bones before/after mixer animation. * * 1. PropertyMixer used by AnimationMixer holds cache value in .buffer. * Calculating IK, Grant, and Physics after mixer animation can break * the cache coherency. * * 2. Applying Grant two or more times without reset the posing breaks model. */ _saveBones(mesh) { const objects = this.objects.get(mesh); const bones = mesh.skeleton.bones; let backupBones = objects.backupBones; if (backupBones === void 0) { backupBones = new Float32Array(bones.length * 7); objects.backupBones = backupBones; } for (let i = 0, il = bones.length; i < il; i++) { const bone = bones[i]; bone.position.toArray(backupBones, i * 7); bone.quaternion.toArray(backupBones, i * 7 + 3); } } _restoreBones(mesh) { const objects = this.objects.get(mesh); const backupBones = objects.backupBones; if (backupBones === void 0) return; const bones = mesh.skeleton.bones; for (let i = 0, il = bones.length; i < il; i++) { const bone = bones[i]; bone.position.fromArray(backupBones, i * 7); bone.quaternion.fromArray(backupBones, i * 7 + 3); } } // experimental _getMasterPhysics() { if (this.masterPhysics !== null) return this.masterPhysics; for (let i = 0, il = this.meshes.length; i < il; i++) { const physics = this.meshes[i].physics; if (physics !== void 0 && physics !== null) { this.masterPhysics = physics; return this.masterPhysics; } } return null; } _updateSharedPhysics(delta) { if (this.meshes.length === 0 || !this.enabled.physics || !this.sharedPhysics) return; const physics = this._getMasterPhysics(); if (physics === null) return; for (let i = 0, il = this.meshes.length; i < il; i++) { const p = this.meshes[i].physics; if (p !== null && p !== void 0) { p.updateRigidBodies(); } } physics.stepSimulation(delta); for (let i = 0, il = this.meshes.length; i < il; i++) { const p = this.meshes[i].physics; if (p !== null && p !== void 0) { p.updateBones(); } } } } const _quaternions = []; let _quaternionIndex = 0; function getQuaternion() { if (_quaternionIndex >= _quaternions.length) { _quaternions.push(new THREE.Quaternion()); } return _quaternions[_quaternionIndex++]; } const _grantResultMap = /* @__PURE__ */ new Map(); function updateOne(mesh, boneIndex, ikSolver, grantSolver) { const bones = mesh.skeleton.bones; const bonesData = mesh.geometry.userData.MMD.bones; const boneData = bonesData[boneIndex]; const bone = bones[boneIndex]; if (_grantResultMap.has(boneIndex)) return; const quaternion = getQuaternion(); _grantResultMap.set(boneIndex, quaternion.copy(bone.quaternion)); if (grantSolver && boneData.grant && !boneData.grant.isLocal && boneData.grant.affectRotation) { const parentIndex = boneData.grant.parentIndex; const ratio = boneData.grant.ratio; if (!_grantResultMap.has(parentIndex)) { updateOne(mesh, parentIndex, ikSolver, grantSolver); } grantSolver.addGrantRotation(bone, _grantResultMap.get(parentIndex), ratio); } if (ikSolver && boneData.ik) { mesh.updateMatrixWorld(true); ikSolver.updateOne(boneData.ik); const links = boneData.ik.links; for (let i = 0, il = links.length; i < il; i++) { const link = links[i]; if (link.enabled === false) continue; const linkIndex = link.index; if (_grantResultMap.has(linkIndex)) { _grantResultMap.set(linkIndex, _grantResultMap.get(linkIndex).copy(bones[linkIndex].quaternion)); } } } quaternion.copy(bone.quaternion); } class AudioManager { /** * @param {THREE.Audio} audio * @param {Object} params - (optional) * @param {Nuumber} params.delayTime */ constructor(audio, params = {}) { this.audio = audio; this.elapsedTime = 0; this.currentTime = 0; this.delayTime = params.delayTime !== void 0 ? params.delayTime : 0; this.audioDuration = this.audio.buffer.duration; this.duration = this.audioDuration + this.delayTime; } /** * @param {Number} delta * @return {AudioManager} */ control(delta) { this.elapsed += delta; this.currentTime += delta; if (this._shouldStopAudio()) this.audio.stop(); if (this._shouldStartAudio()) this.audio.play(); return this; } // private methods _shouldStartAudio() { if (this.audio.isPlaying) return false; while (this.currentTime >= this.duration) { this.currentTime -= this.duration; } if (this.currentTime < this.delayTime) return false; if (this.currentTime - this.delayTime > this.audioDuration) return false; return true; } _shouldStopAudio() { return this.audio.isPlaying && this.currentTime >= this.duration; } } const _q = /* @__PURE__ */ new THREE.Quaternion(); class GrantSolver { constructor(mesh, grants = []) { this.mesh = mesh; this.grants = grants; } /** * Solve all the grant bones * @return {GrantSolver} */ update() { const grants = this.grants; for (let i = 0, il = grants.length; i < il; i++) { this.updateOne(grants[i]); } return this; } /** * Solve a grant bone * @param {Object} grant - grant parameter * @return {GrantSolver} */ updateOne(grant) { const bones = this.mesh.skeleton.bones; const bone = bones[grant.index]; const parentBone = bones[grant.parentIndex]; if (grant.isLocal) { if (grant.affectPosition) ; if (grant.affectRotation) ; } else { if (grant.affectPosition) ; if (grant.affectRotation) { this.addGrantRotation(bone, parentBone.quaternion, grant.ratio); } } return this; } addGrantRotation(bone, q, ratio) { _q.set(0, 0, 0, 1); _q.slerp(q, ratio); bone.quaternion.multiply(_q); return this; } } exports.MMDAnimationHelper = MMDAnimationHelper; //# sourceMappingURL=MMDAnimationHelper.cjs.map