babylon-mmd
Version:
babylon.js mmd loader and runtime
319 lines (318 loc) • 15.4 kB
JavaScript
import { Space } from "@babylonjs/core/Maths/math.axis";
import { Vector3 } from "@babylonjs/core/Maths/math.vector";
import { MmdModelAnimationContainer } from "../../Loader/Animation/mmdModelAnimationContainer";
import { CreateAnimationState } from "./Common/createAnimationState";
import { InduceMmdStandardMaterialRecompile, SetMorphTargetManagersNumMaxInfluencers } from "./Common/induceMmdStandardMaterialRecompile";
/**
* Mmd runtime model animation that use animation container of babylon.js
*
* An object with mmd animation group and model binding information
*/
export class MmdRuntimeModelAnimationContainer {
/**
* The animation data
*/
animation;
/**
* Bone bind index map
*/
boneBindIndexMap;
/**
* Movable bone bind index map
*/
movableBoneBindIndexMap;
_morphController;
/**
* Morph bind index map
*/
morphBindIndexMap;
_meshes;
/**
* IK solver bind index map
*/
ikSolverBindIndexMap;
_ikSolverStates;
/**
* Bone to body bind index map
*/
boneToBodyBindIndexMap;
_rigidBodyStates;
_materialRecompileInduceInfo;
_bonePositionAnimationStates;
_boneRotationAnimationStates;
_morphAnimationStates;
_propertyAnimationStates;
_visibilityAnimationState;
_bonephysicsToggleAnimationStates;
constructor(animation, boneBindIndexMap, movableBoneBindIndexMap, morphController, morphBindIndexMap, meshes, ikSolverBindIndexMap, ikSolverStates, boneToBodyBindIndexMap, rigidBodyStates, materialRecompileInduceInfo) {
this.animation = animation;
this.boneBindIndexMap = boneBindIndexMap;
this.movableBoneBindIndexMap = movableBoneBindIndexMap;
this._morphController = morphController;
this.morphBindIndexMap = morphBindIndexMap;
this._meshes = meshes;
this.ikSolverBindIndexMap = ikSolverBindIndexMap;
this._ikSolverStates = ikSolverStates;
this.boneToBodyBindIndexMap = boneToBodyBindIndexMap;
this._rigidBodyStates = rigidBodyStates;
this._materialRecompileInduceInfo = materialRecompileInduceInfo;
const bonePositionAnimationStates = this._bonePositionAnimationStates = new Array(animation.bonePositionAnimations.length);
for (let i = 0; i < bonePositionAnimationStates.length; ++i) {
bonePositionAnimationStates[i] = CreateAnimationState();
}
const boneRotationAnimationStates = this._boneRotationAnimationStates = new Array(animation.boneRotationAnimations.length);
for (let i = 0; i < boneRotationAnimationStates.length; ++i) {
boneRotationAnimationStates[i] = CreateAnimationState();
}
const morphAnimationStates = this._morphAnimationStates = new Array(animation.morphAnimations.length);
for (let i = 0; i < morphAnimationStates.length; ++i) {
morphAnimationStates[i] = CreateAnimationState();
}
const propertyAnimationStates = this._propertyAnimationStates = new Array(animation.propertyAnimations.length);
for (let i = 0; i < propertyAnimationStates.length; ++i) {
propertyAnimationStates[i] = CreateAnimationState();
}
this._visibilityAnimationState = CreateAnimationState();
const bonephysicsToggleAnimationStates = this._bonephysicsToggleAnimationStates = new Array(animation.bonePhysicsToggleAnimations.length);
for (let i = 0; i < bonephysicsToggleAnimationStates.length; ++i) {
bonephysicsToggleAnimationStates[i] = CreateAnimationState();
}
}
static _BonePosition = new Vector3();
// private static readonly _BoneRotation = new Quaternion();
/**
* Update animation
* @param frameTime Frame time in 30fps
*/
animate(frameTime) {
const animation = this.animation;
const boneTracks = animation.boneRotationAnimations;
const boneBindIndexMap = this.boneBindIndexMap;
for (let i = 0; i < boneTracks.length; ++i) {
const boneTrack = boneTracks[i];
const bone = boneBindIndexMap[i];
if (bone === null)
continue;
// Since mmd bones all have identity quaternions, we abandon the compatibility for skeletons that don't and improve performance
// Quaternion.FromRotationMatrixToRef(bone.getRestMatrix(), MmdRuntimeModelAnimationContainer._BoneRotation);
// bone.setRotationQuaternion(
// MmdRuntimeModelAnimationContainer._BoneRotation.multiplyInPlace(boneTrack._interpolate(frameTime, this._boneRotationAnimationStates[i])),
// Space.LOCAL
// );
bone.setRotationQuaternion(boneTrack._interpolate(frameTime, this._boneRotationAnimationStates[i]), Space.LOCAL);
}
const movableBoneTracks = animation.bonePositionAnimations;
const movableBoneBindIndexMap = this.movableBoneBindIndexMap;
for (let i = 0; i < movableBoneTracks.length; ++i) {
const movableBoneTrack = movableBoneTracks[i];
const bone = movableBoneBindIndexMap[i];
if (bone === null)
continue;
bone.getRestMatrix().getTranslationToRef(MmdRuntimeModelAnimationContainer._BonePosition);
bone.position = MmdRuntimeModelAnimationContainer._BonePosition.addInPlace(movableBoneTrack._interpolate(frameTime, this._bonePositionAnimationStates[i]));
}
const bonephysicsToggleTracks = animation.bonePhysicsToggleAnimations;
const boneToBodyBindIndexMap = this.boneToBodyBindIndexMap;
const rigidBodyStates = this._rigidBodyStates.rigidBodyStates;
for (let i = 0; i < bonephysicsToggleTracks.length; ++i) {
const bonephysicsToggleTrack = bonephysicsToggleTracks[i];
const bodyIndices = boneToBodyBindIndexMap[i];
if (bodyIndices === null)
continue;
const physicsEnabled = 0 < 1 + bonephysicsToggleTrack._interpolate(frameTime, this._bonephysicsToggleAnimationStates[i]) ? 1 : 0;
for (let j = 0; j < bodyIndices.length; ++j) {
rigidBodyStates[bodyIndices[j]] = physicsEnabled;
}
}
const morphTracks = animation.morphAnimations;
const morphBindIndexMap = this.morphBindIndexMap;
const morphController = this._morphController;
for (let i = 0; i < morphTracks.length; ++i) {
const morphTrack = morphTracks[i];
const morphIndices = morphBindIndexMap[i];
if (morphIndices === null)
continue;
const morphWeight = morphTrack._interpolate(frameTime, this._morphAnimationStates[i]);
for (let j = 0; j < morphIndices.length; ++j) {
morphController.setMorphWeightFromIndex(morphIndices[j], morphWeight);
}
}
if (animation.visibilityAnimation !== null) {
const meshes = this._meshes;
const visibility = 1 + animation.visibilityAnimation._interpolate(frameTime, this._visibilityAnimationState);
for (let i = 0; i < meshes.length; ++i)
meshes[i].visibility = visibility;
}
const propertyTracks = animation.propertyAnimations;
const ikSolverBindIndexMap = this.ikSolverBindIndexMap;
const ikSolverStates = this._ikSolverStates.ikSolverStates;
for (let i = 0; i < propertyTracks.length; ++i) {
const propertyTrack = propertyTracks[i];
const ikSolverIndex = ikSolverBindIndexMap[i];
if (ikSolverIndex === -1)
continue;
ikSolverStates[ikSolverIndex] = (0 < 1 + propertyTrack._interpolate(frameTime, this._propertyAnimationStates[i])) ? 1 : 0;
}
}
/**
* Induce material recompile
*
* This method must run once before the animation runs
*
* This method prevents frame drop during animation by inducing properties to be recompiled that are used in morph animation
* @param updateMorphTarget Whether to update morph target manager numMaxInfluencers
* @param logger logger
*/
induceMaterialRecompile(updateMorphTarget, logger) {
if (this._materialRecompileInduceInfo === null)
return;
MmdRuntimeModelAnimationContainer.InduceMaterialRecompile(this._materialRecompileInduceInfo, this._morphController, this.morphBindIndexMap, logger);
if (updateMorphTarget) {
SetMorphTargetManagersNumMaxInfluencers(this._morphController, this.morphBindIndexMap);
}
this._materialRecompileInduceInfo = null;
}
/**
* Bind animation to model and prepare material for morph animation
* @param animationContainer Animation to bind
* @param model Bind target
* @param retargetingMap Animation bone name to model bone name map
* @param logger Logger
* @return MmdRuntimeModelAnimationContainer instance
*/
static Create(animationContainer, model, retargetingMap, logger) {
const skeleton = model.skeleton;
const linkedBoneMap = new Map();
{
const bones = skeleton.bones;
if (retargetingMap === undefined) {
for (let i = 0; i < bones.length; ++i) {
const linkedBone = bones[i];
linkedBoneMap.set(linkedBone.name, linkedBone);
}
}
else {
for (let i = 0; i < bones.length; ++i) {
const linkedBone = bones[i];
linkedBoneMap.set(retargetingMap[linkedBone.name] ?? linkedBone.name, linkedBone);
}
}
}
const runtimeBoneMap = new Map();
{
const runtimeBones = model.runtimeBones;
const linkedBoneToRuntimeBoneMap = new Map();
for (let i = 0; i < runtimeBones.length; ++i) {
const runtimeBone = runtimeBones[i];
linkedBoneToRuntimeBoneMap.set(runtimeBone.linkedBone, runtimeBone);
}
for (const [name, linkedBone] of linkedBoneMap) {
const runtimeBone = linkedBoneToRuntimeBoneMap.get(linkedBone);
if (runtimeBone === undefined) {
logger?.warn(`Binding warning: bone ${name} not found in runtime bones`);
continue;
}
runtimeBoneMap.set(name, runtimeBone);
}
}
const boneBindIndexMap = new Array(animationContainer.boneRotationAnimations.length);
const boneNameMap = animationContainer.boneRotationAnimationBindMap;
for (let i = 0; i < boneNameMap.length; ++i) {
const linkedBone = linkedBoneMap.get(boneNameMap[i]);
if (linkedBone === undefined) {
logger?.warn(`Binding failed: bone ${boneNameMap[i]} not found`);
boneBindIndexMap[i] = null;
}
else {
boneBindIndexMap[i] = linkedBone;
}
}
const movableBoneBindIndexMap = new Array(animationContainer.bonePositionAnimations.length);
const movableBoneNameMap = animationContainer.bonePositionAnimationBindMap;
for (let i = 0; i < movableBoneNameMap.length; ++i) {
const linkedBone = linkedBoneMap.get(movableBoneNameMap[i]);
if (linkedBone === undefined) {
logger?.warn(`Binding failed: bone ${movableBoneNameMap[i]} not found`);
movableBoneBindIndexMap[i] = null;
}
else {
movableBoneBindIndexMap[i] = linkedBone;
}
}
const morphController = model.morph;
const morphBindIndexMap = new Array(animationContainer.morphAnimations.length);
const morphNameMap = animationContainer.morphAnimationBindMap;
for (let i = 0; i < morphNameMap.length; ++i) {
const morphName = morphNameMap[i];
const mappedName = retargetingMap?.[morphName] ?? morphName;
const morphIndices = morphController.getMorphIndices(mappedName);
if (morphIndices === undefined) {
logger?.warn(`Binding failed: morph ${mappedName} not found`);
morphBindIndexMap[i] = null;
}
else {
morphBindIndexMap[i] = morphIndices;
}
}
const ikSolverBindIndexMap = new Int32Array(animationContainer.propertyAnimations.length);
const propertyTrackIkBoneNames = animationContainer.propertyAnimationBindMap;
for (let i = 0; i < propertyTrackIkBoneNames.length; ++i) {
const ikBoneName = propertyTrackIkBoneNames[i];
const ikBone = runtimeBoneMap.get(ikBoneName);
if (ikBone === undefined) {
logger?.warn(`Binding failed: IK bone ${ikBoneName} not found`);
ikSolverBindIndexMap[i] = -1;
}
else {
const ikSolverIndex = ikBone.ikSolverIndex;
if (ikSolverIndex === -1) {
logger?.warn(`Binding failed: IK solver for bone ${ikBoneName} not found`);
ikSolverBindIndexMap[i] = -1;
}
else {
ikSolverBindIndexMap[i] = ikSolverIndex;
}
}
}
const boneToBodyBindIndexMap = new Array(animationContainer.bonePhysicsToggleAnimations.length);
{
const boneNameMap = animationContainer.bonePhysicsToggleAnimationBindMap;
for (let i = 0; i < boneNameMap.length; ++i) {
const runtimeBone = runtimeBoneMap.get(boneNameMap[i]);
if (runtimeBone === undefined) {
logger?.warn(`Binding failed: runtime bone ${boneNameMap[i]} not found`);
boneToBodyBindIndexMap[i] = null;
}
else {
boneToBodyBindIndexMap[i] = runtimeBone.rigidBodyIndices;
}
}
}
return new MmdRuntimeModelAnimationContainer(animationContainer, boneBindIndexMap, movableBoneBindIndexMap, morphController, morphBindIndexMap, model.mesh.metadata.meshes, ikSolverBindIndexMap, model, boneToBodyBindIndexMap, model, model.mesh.metadata.materials);
}
/**
* Induce material recompile
*
* This method prevents frame drop during animation by inducing properties to be recompiled that are used in morph animation
*
* This method is exposed as public because it must be overrideable
*
* If you are using a material other than `MmdStandardMaterial`, you must implement this method yourself
* @param materials Materials
* @param morphController Morph controller
* @param morphIndices Morph indices to induce recompile
* @param logger logger
*/
static InduceMaterialRecompile = InduceMmdStandardMaterialRecompile;
}
/**
* @internal
* Create runtime model animation
* @param model Bind target
* @param retargetingMap Animation bone name to model bone name map
* @param logger Logger
* @returns MmdRuntimeModelAnimationContainer instance
*/
MmdModelAnimationContainer.prototype.createRuntimeModelAnimation = function (model, retargetingMap, logger) {
return MmdRuntimeModelAnimationContainer.Create(this, model, retargetingMap, logger);
};