three-stdlib
Version:
stand-alone library of threejs examples
690 lines (689 loc) • 23.5 kB
JavaScript
"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<THREE.AnimationClip>} 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