UNPKG

babylon-mmd

Version:
428 lines (427 loc) 20.5 kB
import { InduceMmdStandardMaterialRecompile, SetMorphTargetManagersNumMaxInfluencers } from "../../Animation/Common/induceMmdStandardMaterialRecompile"; import { MmdRuntimeAnimation } from "../../Animation/mmdRuntimeAnimation"; import { MmdWasmAnimation } from "./mmdWasmAnimation"; /** * Mmd WASM runtime model animation * * An object with mmd animation and model binding information */ export class MmdWasmRuntimeModelAnimation extends MmdRuntimeAnimation { /** * Pointer to the animation data in wasm memory */ ptr; _modelPtr; _onDispose; /** * The animation data */ animation; _boneBindIndexMap; /** * Bone bind index map */ get boneBindIndexMap() { return this._boneBindIndexMap.array; } _movableBoneBindIndexMap; /** * Movable bone bind index map */ get movableBoneBindIndexMap() { return this._movableBoneBindIndexMap.array; } _morphController; /** * Morph bind index map */ morphBindIndexMap; _meshes; _ikSolverBindIndexMap; /** * IK solver bind index map */ get ikSolverBindIndexMap() { return this._ikSolverBindIndexMap.array; } _materialRecompileInduceInfo; constructor(ptr, modelPtr, animation, boneBindIndexMap, movableBoneBindIndexMap, morphController, morphBindIndexMap, meshes, ikSolverBindIndexMap, materialRecompileInduceInfo, onDispose) { super(); this.ptr = ptr; this._modelPtr = modelPtr; this._onDispose = onDispose; this.animation = animation; this._boneBindIndexMap = boneBindIndexMap; this._movableBoneBindIndexMap = movableBoneBindIndexMap; this._morphController = morphController; this.morphBindIndexMap = morphBindIndexMap; this._meshes = meshes; this._ikSolverBindIndexMap = ikSolverBindIndexMap; this._materialRecompileInduceInfo = materialRecompileInduceInfo; } /** * Dispose this instance * @param fromAnimation Dispose from animation (default: false) (internal use only) */ dispose(fromAnimation = false) { if (this._onDispose === null) return; if (!fromAnimation) { const runtimeModelAnimations = this.animation._runtimeModelAnimations; if (runtimeModelAnimations !== undefined) { const index = runtimeModelAnimations.indexOf(this); if (index !== -1) runtimeModelAnimations.splice(index, 1); } } this._onDispose(); this._onDispose = null; const animationPool = this.animation._poolWrapper.pool; animationPool.destroyRuntimeAnimation(this.ptr); } /** * Run wasm side animation evaluation * * Update bone / bone morphs / ik solver state * * IMPORTANT: when wasm runtime using buffered evaluation, this method must be called before waiting for the WasmMmdRuntime.lock * otherwise, it can cause a datarace * @param frameTime Frame time in 30fps */ wasmAnimate(frameTime) { this.animation._poolWrapper.pool.animateMmdModel(this.ptr, this._modelPtr, frameTime); } /** * Update vertex / uv morphs and visibility * @param frameTime Frame time in 30fps */ animate(frameTime) { const animation = this.animation; const morphTracks = animation.morphTracks; if (0 < morphTracks.length) { const morphController = this._morphController; const morphBindIndexMap = this.morphBindIndexMap; for (let i = 0; i < morphTracks.length; ++i) { const morphIndices = morphBindIndexMap[i]; if (morphIndices === null) continue; const morphTrack = morphTracks[i]; if (morphTrack.frameNumbers.length === 0) { for (let j = 0; j < morphIndices.length; ++j) { morphController.setMorphWeightFromIndex(morphIndices[j], 0); } continue; } const clampedFrameTime = Math.max(morphTrack.startFrame, Math.min(morphTrack.endFrame, frameTime)); const upperBoundIndex = this._upperBoundFrameIndex(clampedFrameTime, morphTrack); const upperBoundIndexMinusOne = upperBoundIndex - 1; const frameNumberB = morphTrack.frameNumbers[upperBoundIndex]; if (frameNumberB === undefined) { const weight = morphTrack.weights[upperBoundIndexMinusOne]; for (let j = 0; j < morphIndices.length; ++j) { morphController.setMorphWeightFromIndex(morphIndices[j], weight, false); } } else { const frameNumberA = morphTrack.frameNumbers[upperBoundIndexMinusOne]; const relativeTime = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); const weightA = morphTrack.weights[upperBoundIndexMinusOne]; const weightB = morphTrack.weights[upperBoundIndex]; const weight = weightA + (weightB - weightA) * relativeTime; for (let j = 0; j < morphIndices.length; ++j) { morphController.setMorphWeightFromIndex(morphIndices[j], weight, false); } } } } if (0 < animation.propertyTrack.frameNumbers.length) { const propertyTrack = animation.propertyTrack; const clampedFrameTime = Math.max(propertyTrack.startFrame, Math.min(propertyTrack.endFrame, frameTime)); const stepIndex = this._upperBoundFrameIndex(clampedFrameTime, propertyTrack) - 1; const visibility = propertyTrack.visibles[stepIndex]; const meshes = this._meshes; for (let i = 0; i < meshes.length; ++i) { meshes[i].visibility = visibility; } } } /** * 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; MmdWasmRuntimeModelAnimation.InduceMaterialRecompile(this._materialRecompileInduceInfo, this._morphController, this.morphBindIndexMap, logger); if (updateMorphTarget) { SetMorphTargetManagersNumMaxInfluencers(this._morphController, this.morphBindIndexMap); } this._materialRecompileInduceInfo = null; } /** * @internal * Bind animation to model and prepare material for morph animation * @param animation Animation to bind * @param model Bind target * @param onDispose Callback when this instance is disposed * @param retargetingMap Animation bone name to model bone name map * @param logger Logger * @return MmdRuntimeModelAnimation instance */ static Create(animation, model, onDispose, retargetingMap, logger) { const wasmInstance = animation._poolWrapper.instance; const animationPool = animation._poolWrapper.pool; const skeleton = model.skeleton; const runtimeBoneIndexMap = new Map(); { const bones = skeleton.bones; const linkedBoneMap = new Map(); 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 runtimeBones = model.runtimeBones; const linkedBoneToRuntimeBoneIndexMap = new Map(); for (let i = 0; i < runtimeBones.length; ++i) { linkedBoneToRuntimeBoneIndexMap.set(runtimeBones[i].linkedBone, i); } for (const [name, linkedBone] of linkedBoneMap) { const runtimeBoneIndex = linkedBoneToRuntimeBoneIndexMap.get(linkedBone); if (runtimeBoneIndex === undefined) { logger?.warn(`Binding warning: bone ${name} not found in runtime bones`); continue; } runtimeBoneIndexMap.set(name, runtimeBoneIndex); } } const boneBindIndexMapPtr = animationPool.createBoneBindIndexMap(animation.ptr); const boneBindIndexMap = wasmInstance.createTypedArray(Int32Array, boneBindIndexMapPtr, animation.boneTracks.length); { const boneTracks = animation.boneTracks; const boneBindIndexMapArray = boneBindIndexMap.array; for (let i = 0; i < boneTracks.length; ++i) { const boneTrack = boneTracks[i]; const boneIndex = runtimeBoneIndexMap.get(boneTrack.name); if (boneIndex === undefined) { logger?.warn(`Binding failed: bone ${boneTrack.name} not found`); boneBindIndexMapArray[i] = -1; } else { boneBindIndexMapArray[i] = boneIndex; } } } const movableBoneBindIndexMapPtr = animationPool.createMovableBoneBindIndexMap(animation.ptr); const movableBoneBindIndexMap = wasmInstance.createTypedArray(Int32Array, movableBoneBindIndexMapPtr, animation.movableBoneTracks.length); { const movableBoneBindIndexMapArray = movableBoneBindIndexMap.array; const movableBoneTracks = animation.movableBoneTracks; for (let i = 0; i < movableBoneTracks.length; ++i) { const movableBoneTrack = movableBoneTracks[i]; const boneIndex = runtimeBoneIndexMap.get(movableBoneTrack.name); if (boneIndex === undefined) { logger?.warn(`Binding failed: bone ${movableBoneTrack.name} not found`); movableBoneBindIndexMapArray[i] = -1; } else { movableBoneBindIndexMapArray[i] = boneIndex; } } } const morphController = model.morph; const morphBindIndexMap = new Array(animation.morphTracks.length); const morphTracks = animation.morphTracks; for (let i = 0; i < morphTracks.length; ++i) { const morphTrack = morphTracks[i]; const mappedName = retargetingMap?.[morphTrack.name] ?? morphTrack.name; const morphIndices = morphController.getMorphIndices(mappedName); if (morphIndices === undefined) { logger?.warn(`Binding failed: morph ${mappedName} not found`); morphBindIndexMap[i] = null; } else { morphBindIndexMap[i] = morphIndices; } } const morphLengthBufferPtr = animationPool.allocateLengthsBuffer(morphTracks.length); const morphLengthBuffer = wasmInstance.createTypedArray(Uint32Array, morphLengthBufferPtr, morphTracks.length); { const morphLengthBufferArray = morphLengthBuffer.array; const wasmMorphIndexMap = morphController.wasmMorphIndexMap; for (let i = 0; i < morphTracks.length; ++i) { let indicesCount = 0; const morphIndices = morphBindIndexMap[i]; if (morphIndices !== null) { for (let j = 0; j < morphIndices.length; ++j) { const remappedIndex = wasmMorphIndexMap[morphIndices[j]]; if (remappedIndex !== undefined && remappedIndex !== -1) indicesCount += 1; } } morphLengthBufferArray[i] = indicesCount; } } const morphBindIndexMapPtr = animationPool.createMorphBindIndexMap(animation.ptr, morphLengthBufferPtr); { const wasmMorphIndexMap = morphController.wasmMorphIndexMap; for (let i = 0; i < morphTracks.length; ++i) { const nthMorphIndicesPtr = animationPool.getNthMorphBindIndexMap(morphBindIndexMapPtr, i); const nthMorphIndices = wasmInstance.createTypedArray(Int32Array, nthMorphIndicesPtr, morphLengthBuffer.array[i]).array; let indicesCount = 0; const morphIndices = morphBindIndexMap[i]; if (morphIndices !== null) { for (let j = 0; j < morphIndices.length; ++j) { const remappedIndex = wasmMorphIndexMap[morphIndices[j]]; if (remappedIndex !== undefined && remappedIndex !== -1) { nthMorphIndices[indicesCount] = remappedIndex; indicesCount += 1; } } } } } animationPool.deallocateLengthsBuffer(morphLengthBufferPtr, morphTracks.length); const ikSolverBindIndexMapPtr = animationPool.createIkSolverBindIndexMap(animation.ptr); const ikSolverBindIndexMap = wasmInstance.createTypedArray(Int32Array, ikSolverBindIndexMapPtr, animation.propertyTrack.ikBoneNames.length); { const ikSolverBindIndexMapArray = ikSolverBindIndexMap.array; const runtimeBones = model.runtimeBones; const propertyTrackIkBoneNames = animation.propertyTrack.ikBoneNames; for (let i = 0; i < propertyTrackIkBoneNames.length; ++i) { const ikBoneName = propertyTrackIkBoneNames[i]; const ikBoneIndex = runtimeBoneIndexMap.get(ikBoneName); if (ikBoneIndex === undefined) { logger?.warn(`Binding failed: IK bone ${ikBoneName} not found`); ikSolverBindIndexMapArray[i] = -1; } else { const ikSolverIndex = runtimeBones[ikBoneIndex].ikSolverIndex; if (ikSolverIndex === -1) { logger?.warn(`Binding failed: IK solver for bone ${ikBoneName} not found`); ikSolverBindIndexMapArray[i] = -1; } else { ikSolverBindIndexMapArray[i] = ikSolverIndex; } } } } const boneToBodyBindIndexMap = new Array(animation.boneTracks.length + animation.movableBoneTracks.length); { const runtimeBones = model.runtimeBones; { const boneTracks = animation.boneTracks; for (let i = 0; i < boneTracks.length; ++i) { const boneTrack = boneTracks[i]; const runtimeBoneIndex = runtimeBoneIndexMap.get(boneTrack.name); if (runtimeBoneIndex === undefined) { logger?.warn(`Binding failed: runtime bone ${boneTrack.name} not found`); boneToBodyBindIndexMap[i] = null; } else { boneToBodyBindIndexMap[i] = runtimeBones[runtimeBoneIndex].rigidBodyIndices; } } } { const movableBoneTracks = animation.movableBoneTracks; const offset = animation.boneTracks.length; for (let i = 0; i < movableBoneTracks.length; ++i) { const movableBoneTrack = movableBoneTracks[i]; const runtimeBoneIndex = runtimeBoneIndexMap.get(movableBoneTrack.name); if (runtimeBoneIndex === undefined) { logger?.warn(`Binding failed: runtime bone ${movableBoneTrack.name} not found`); boneToBodyBindIndexMap[i + offset] = null; } else { boneToBodyBindIndexMap[i + offset] = runtimeBones[runtimeBoneIndex].rigidBodyIndices; } } } } const bodyLengthBufferPtr = animationPool.allocateLengthsBuffer(boneToBodyBindIndexMap.length); const bodyLengthBuffer = wasmInstance.createTypedArray(Uint32Array, bodyLengthBufferPtr, boneToBodyBindIndexMap.length); { const bodyLengthBufferArray = bodyLengthBuffer.array; for (let i = 0; i < boneToBodyBindIndexMap.length; ++i) { const bodyIndices = boneToBodyBindIndexMap[i]; if (bodyIndices === null) { bodyLengthBufferArray[i] = 0; } else { bodyLengthBufferArray[i] = bodyIndices.length; } } } const boneToBodyBindIndexMapPtr = animationPool.createBoneToBodyBindIndexMap(animation.ptr, bodyLengthBufferPtr); for (let i = 0; i < boneToBodyBindIndexMap.length; ++i) { const nthBodyIndicesPtr = animationPool.getNthBoneToBodyBindIndexMap(boneToBodyBindIndexMapPtr, i); const nthBodyIndices = wasmInstance.createTypedArray(Int32Array, nthBodyIndicesPtr, bodyLengthBuffer.array[i]).array; const bodyIndices = boneToBodyBindIndexMap[i]; if (bodyIndices === null) continue; for (let j = 0; j < bodyIndices.length; ++j) { nthBodyIndices[j] = bodyIndices[j]; } } animationPool.deallocateLengthsBuffer(bodyLengthBufferPtr, boneToBodyBindIndexMap.length); const runtimeAnimationPtr = animationPool.createRuntimeAnimation(animation.ptr, boneBindIndexMapPtr, movableBoneBindIndexMapPtr, morphBindIndexMapPtr, ikSolverBindIndexMapPtr, boneToBodyBindIndexMapPtr); return new MmdWasmRuntimeModelAnimation(runtimeAnimationPtr, model.ptr, animation, boneBindIndexMap, movableBoneBindIndexMap, morphController, morphBindIndexMap, model.mesh.metadata.meshes, ikSolverBindIndexMap, model.mesh.metadata.materials, onDispose); } /** * 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 onDispose Callback when this instance is disposed * @param retargetingMap Animation bone name to model bone name map * @param logger Logger * @returns MmdRuntimeModelAnimation instance */ MmdWasmAnimation.prototype.createWasmRuntimeModelAnimation = function (model, onDispose, retargetingMap, logger) { if (this._runtimeModelAnimations === undefined) this._runtimeModelAnimations = []; const runtimeAnimation = MmdWasmRuntimeModelAnimation.Create(this, model, onDispose, retargetingMap, logger); this._runtimeModelAnimations.push(runtimeAnimation); return runtimeAnimation; }; const MmdWasmAnimationDispose = MmdWasmAnimation.prototype.dispose; MmdWasmAnimation.prototype.dispose = function () { if (this.isDisposed) return; const runtimeModelAnimations = this._runtimeModelAnimations; if (runtimeModelAnimations !== undefined) { for (let i = 0; i < runtimeModelAnimations.length; ++i) { runtimeModelAnimations[i].dispose(true); } this._runtimeModelAnimations = undefined; } MmdWasmAnimationDispose.call(this); };