From 2d3b7ad555a5ae9f73b82be1e6d0d94efc463028 Mon Sep 17 00:00:00 2001 From: noname <48761044+noname0310@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:03:17 +0900 Subject: [PATCH] make animation runtime as tree-shakeable --- package.json | 2 + src/Runtime/Animation/mmdRuntimeAnimation.ts | 712 +----------------- .../Animation/mmdRuntimeCameraAnimation.ts | 196 +++++ .../Animation/mmdRuntimeModelAnimation.ts | 557 ++++++++++++++ src/Runtime/mmdCamera.ts | 4 +- src/Runtime/mmdModel.ts | 4 +- src/Runtime/mmdRuntime.ts | 3 +- src/Test/Scene/physicsTestScene.ts | 2 + src/index.ts | 6 +- 9 files changed, 769 insertions(+), 717 deletions(-) create mode 100644 src/Runtime/Animation/mmdRuntimeCameraAnimation.ts create mode 100644 src/Runtime/Animation/mmdRuntimeModelAnimation.ts diff --git a/package.json b/package.json index 636c4a25..e6342c06 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "**/mmdOutlineRenderer.*", "**/pmxLoader.*", "**/bpmxLoader.*", + "**/mmdRuntimeCameraAnimation.*", + "**/mmdRuntimeModelAnimation.*", "**/index.*" ], "peerDependencies": { diff --git a/src/Runtime/Animation/mmdRuntimeAnimation.ts b/src/Runtime/Animation/mmdRuntimeAnimation.ts index e353488c..5ac87721 100644 --- a/src/Runtime/Animation/mmdRuntimeAnimation.ts +++ b/src/Runtime/Animation/mmdRuntimeAnimation.ts @@ -1,22 +1,4 @@ -import type { Bone } from "@babylonjs/core/Bones/bone"; -import type { Material } from "@babylonjs/core/Materials/material"; -import { Space } from "@babylonjs/core/Maths/math.axis"; -import { Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector"; -import type { Nullable } from "@babylonjs/core/types"; - -import type { MmdAnimation } from "@/Loader/Animation/mmdAnimation"; -import type { MmdAnimationTrack, MmdCameraAnimationTrack } from "@/Loader/Animation/mmdAnimationTrack"; -import type { MmdStandardMaterial } from "@/Loader/mmdStandardMaterial"; -import { PmxObject } from "@/Loader/Parser/pmxObject"; - -import type { IIkSolver } from "../ikSolver"; -import type { ILogger } from "../ILogger"; -import type { MmdCamera } from "../mmdCamera"; -import type { RuntimeMmdMesh } from "../mmdMesh"; -import type { MmdModel } from "../mmdModel"; -import type { MmdMorphController } from "../mmdMorphController"; - -type MorphIndices = readonly number[]; +import type { MmdAnimationTrack } from "@/Loader/Animation/mmdAnimationTrack"; /** * Mmd runtime animation base class @@ -71,698 +53,6 @@ export abstract class MmdRuntimeAnimation { } } -/** - * Mmd runtime model animation - * - * An object with mmd animation and model binding information - */ -export class MmdRuntimeModelAnimation extends MmdRuntimeAnimation { - /** - * Mmd animation - */ - public readonly animation: MmdAnimation; - - private readonly _boneBindIndexMap: Nullable[]; - private readonly _moveableBoneBindIndexMap: Nullable[]; - private readonly _morphController: MmdMorphController; - private readonly _morphBindIndexMap: Nullable[]; - private readonly _mesh: RuntimeMmdMesh; - private readonly _ikSolverBindIndexMap: Nullable[]; - - private constructor( - animation: MmdAnimation, - boneBindIndexMap: Nullable[], - moveableBoneBindIndexMap: Nullable[], - morphController: MmdMorphController, - morphBindIndexMap: Nullable[], - mesh: RuntimeMmdMesh, - ikSolverBindIndexMap: Nullable[] - ) { - super(); - - this.animation = animation; - - this._boneBindIndexMap = boneBindIndexMap; - this._moveableBoneBindIndexMap = moveableBoneBindIndexMap; - this._morphController = morphController; - this._morphBindIndexMap = morphBindIndexMap; - this._mesh = mesh; - this._ikSolverBindIndexMap = ikSolverBindIndexMap; - } - - private static readonly _BonePositionA = new Vector3(); - private static readonly _BonePositionB = new Vector3(); - private static readonly _BoneOriginalPosition = new Vector3(); - private static readonly _BoneRotationA = new Quaternion(); - private static readonly _BoneRotationB = new Quaternion(); - private static readonly _BoneOriginalRotation = new Quaternion(); - - /** - * Update animation - * @param frameTime frame time in 30fps - */ - public animate(frameTime: number): void { - const animation = this.animation; - - const boneTracks = animation.boneTracks; - if (0 < boneTracks.length) { - const boneBindIndexMap = this._boneBindIndexMap; - for (let i = 0; i < boneTracks.length; ++i) { - const bone = boneBindIndexMap[i]; - if (bone === null) continue; - - const boneTrack = boneTracks[i]; - const clampedFrameTime = Math.max(boneTrack.startFrame, Math.min(boneTrack.endFrame, frameTime)); - const upperBoundIndex = this._upperBoundFrameIndex(clampedFrameTime, boneTrack); - const upperBoundIndexMinusOne = upperBoundIndex - 1; - - const frameNumberB = boneTrack.frameNumbers[upperBoundIndex]; - if (frameNumberB === undefined) { - const rotations = boneTrack.rotations; - bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); - bone.setRotationQuaternion( - MmdRuntimeModelAnimation._BoneOriginalRotation.multiply( - MmdRuntimeModelAnimation._BoneRotationB.set( - rotations[upperBoundIndexMinusOne * 4], - rotations[upperBoundIndexMinusOne * 4 + 1], - rotations[upperBoundIndexMinusOne * 4 + 2], - rotations[upperBoundIndexMinusOne * 4 + 3] - ) - ), - Space.LOCAL - ); - } else { - const frameNumberA = boneTrack.frameNumbers[upperBoundIndexMinusOne]; - const gradient = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); - - const rotations = boneTrack.rotations; - const rotationInterpolations = boneTrack.rotationInterpolations; - - const rotationA = MmdRuntimeModelAnimation._BoneRotationA.set( - rotations[upperBoundIndexMinusOne * 4], - rotations[upperBoundIndexMinusOne * 4 + 1], - rotations[upperBoundIndexMinusOne * 4 + 2], - rotations[upperBoundIndexMinusOne * 4 + 3] - ); - const rotationB = MmdRuntimeModelAnimation._BoneRotationB.set( - rotations[upperBoundIndex * 4], - rotations[upperBoundIndex * 4 + 1], - rotations[upperBoundIndex * 4 + 2], - rotations[upperBoundIndex * 4 + 3] - ); - - const weight = MmdInterpolator.Interpolate( - rotationInterpolations[upperBoundIndex * 4] / 127, // x1 - rotationInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 - rotationInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 - rotationInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 - gradient - ); - - Quaternion.SlerpToRef(rotationA, rotationB, weight, rotationA); - bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); - bone.setRotationQuaternion( - MmdRuntimeModelAnimation._BoneOriginalRotation.multiply(rotationA), - Space.LOCAL - ); - } - } - } - - const moveableBoneTracks = animation.moveableBoneTracks; - if (0 < moveableBoneTracks.length) { - const boneBindIndexMap = this._moveableBoneBindIndexMap; - for (let i = 0; i < moveableBoneTracks.length; ++i) { - const bone = boneBindIndexMap[i]; - if (bone === null) continue; - - const boneTrack = moveableBoneTracks[i]; - const clampedFrameTime = Math.max(boneTrack.startFrame, Math.min(boneTrack.endFrame, frameTime)); - const upperBoundIndex = this._upperBoundFrameIndex(clampedFrameTime, boneTrack); - const upperBoundIndexMinusOne = upperBoundIndex - 1; - - const frameNumberB = boneTrack.frameNumbers[upperBoundIndex]; - if (frameNumberB === undefined) { - const positions = boneTrack.positions; - bone.getPositionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalPosition); - bone.setPosition( - MmdRuntimeModelAnimation._BoneOriginalPosition.add( - MmdRuntimeModelAnimation._BonePositionB.set( - positions[upperBoundIndexMinusOne * 3], - positions[upperBoundIndexMinusOne * 3 + 1], - positions[upperBoundIndexMinusOne * 3 + 2] - ) - ), - Space.LOCAL - ); - - const rotations = boneTrack.rotations; - bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); - bone.setRotationQuaternion( - MmdRuntimeModelAnimation._BoneOriginalRotation.multiply( - MmdRuntimeModelAnimation._BoneRotationB.set( - rotations[upperBoundIndexMinusOne * 4], - rotations[upperBoundIndexMinusOne * 4 + 1], - rotations[upperBoundIndexMinusOne * 4 + 2], - rotations[upperBoundIndexMinusOne * 4 + 3] - ) - ), - Space.LOCAL - ); - } else { - const frameNumberA = boneTrack.frameNumbers[upperBoundIndexMinusOne]; - const gradient = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); - - const positions = boneTrack.positions; - const positionInterpolations = boneTrack.positionInterpolations; - - const positionA = MmdRuntimeModelAnimation._BonePositionA.set( - positions[upperBoundIndexMinusOne * 3], - positions[upperBoundIndexMinusOne * 3 + 1], - positions[upperBoundIndexMinusOne * 3 + 2] - ); - const positionB = MmdRuntimeModelAnimation._BonePositionB.set( - positions[upperBoundIndex * 3], - positions[upperBoundIndex * 3 + 1], - positions[upperBoundIndex * 3 + 2] - ); - - const xWeight = MmdInterpolator.Interpolate( - positionInterpolations[upperBoundIndex * 12] / 127, // x_x1 - positionInterpolations[upperBoundIndex * 12 + 1] / 127, // x_x2 - positionInterpolations[upperBoundIndex * 12 + 2] / 127, // x_y1 - positionInterpolations[upperBoundIndex * 12 + 3] / 127, // x_y2 - gradient - ); - const yWeight = MmdInterpolator.Interpolate( - positionInterpolations[upperBoundIndex * 12 + 4] / 127, // y_x1 - positionInterpolations[upperBoundIndex * 12 + 5] / 127, // y_x2 - positionInterpolations[upperBoundIndex * 12 + 6] / 127, // y_y1 - positionInterpolations[upperBoundIndex * 12 + 7] / 127, // y_y2 - gradient - ); - const zWeight = MmdInterpolator.Interpolate( - positionInterpolations[upperBoundIndex * 12 + 8] / 127, // z_x1 - positionInterpolations[upperBoundIndex * 12 + 9] / 127, // z_x2 - positionInterpolations[upperBoundIndex * 12 + 10] / 127, // z_y1 - positionInterpolations[upperBoundIndex * 12 + 11] / 127, // z_y2 - gradient - ); - - positionA.x += (positionB.x - positionA.x) * xWeight; - positionA.y += (positionB.y - positionA.y) * yWeight; - positionA.z += (positionB.z - positionA.z) * zWeight; - bone.getPositionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalPosition); - bone.setPosition(MmdRuntimeModelAnimation._BoneOriginalPosition.add(positionA), Space.LOCAL); - - const rotations = boneTrack.rotations; - const rotationInterpolations = boneTrack.rotationInterpolations; - - const rotationA = MmdRuntimeModelAnimation._BoneRotationA.set( - rotations[upperBoundIndexMinusOne * 4], - rotations[upperBoundIndexMinusOne * 4 + 1], - rotations[upperBoundIndexMinusOne * 4 + 2], - rotations[upperBoundIndexMinusOne * 4 + 3] - ); - const rotationB = MmdRuntimeModelAnimation._BoneRotationB.set( - rotations[upperBoundIndex * 4], - rotations[upperBoundIndex * 4 + 1], - rotations[upperBoundIndex * 4 + 2], - rotations[upperBoundIndex * 4 + 3] - ); - - const weight = MmdInterpolator.Interpolate( - rotationInterpolations[upperBoundIndex * 4] / 127, // x1 - rotationInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 - rotationInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 - rotationInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 - gradient - ); - - Quaternion.SlerpToRef(rotationA, rotationB, weight, rotationA); - bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); - bone.setRotationQuaternion( - MmdRuntimeModelAnimation._BoneOriginalRotation.multiply(rotationA), - Space.LOCAL - ); - } - } - } - - 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]; - - 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) { - // this clamp will be removed when morph target recompilation problem is solved - // ref: https://github.com/BabylonJS/Babylon.js/issues/14008 - const weight = Math.max(morphTrack.weights[upperBoundIndexMinusOne], 1e-16); - for (let j = 0; j < morphIndices.length; ++j) { - morphController.setMorphWeightFromIndex(morphIndices[j], weight); - } - } else { - const frameNumberA = morphTrack.frameNumbers[upperBoundIndexMinusOne]; - const relativeTime = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); - - const weightA = morphTrack.weights[upperBoundIndexMinusOne]; - const weightB = morphTrack.weights[upperBoundIndex]; - - // this clamp will be removed when morph target recompilation problem is solved - // ref: https://github.com/BabylonJS/Babylon.js/issues/14008 - const weight = Math.max(weightA + (weightB - weightA) * relativeTime, 1e-16); - for (let j = 0; j < morphIndices.length; ++j) { - morphController.setMorphWeightFromIndex(morphIndices[j], weight); - } - } - } - } - - 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; - - this._mesh.visibility = propertyTrack.visibles[stepIndex]; - - const ikSolverBindIndexMap = this._ikSolverBindIndexMap; - const propertyTrackIkStates = propertyTrack.ikStates; - for (let i = 0; i < ikSolverBindIndexMap.length; ++i) { - const ikSolver = ikSolverBindIndexMap[i]; - if (ikSolver === null) continue; - - const ikState = propertyTrackIkStates[i]; - ikSolver.enabled = ikState[stepIndex] !== 0; - } - } - } - - private _materialRecompileInduced = false; - - /** - * 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 logger logger - */ - public induceMaterialRecompile(logger?: ILogger): void { - if (this._materialRecompileInduced) return; - this._materialRecompileInduced = true; - - MmdRuntimeModelAnimation.InduceMaterialRecompile( - this._mesh.material.subMaterials, - this._morphController, - this._morphBindIndexMap, - logger - ); - } - - /** - * Bind animation to model and prepare material for morph animation - * @param animation Animation to bind - * @param model Bind target - * @param retargetingMap Model bone name to animation bone name map - * @param logger Logger - * @return MmdRuntimeModelAnimation instance - */ - public static Create(animation: MmdAnimation, model: MmdModel, retargetingMap?: { [key: string]: string }, logger?: ILogger): MmdRuntimeModelAnimation { - const skeleton = model.mesh.skeleton; - const bones = skeleton.bones; - - const boneIndexMap = new Map(); - if (retargetingMap === undefined) { - for (let i = 0; i < bones.length; ++i) { - boneIndexMap.set(bones[i].name, bones[i]); - } - } else { - for (let i = 0; i < bones.length; ++i) { - boneIndexMap.set(retargetingMap[bones[i].name] ?? bones[i].name, bones[i]); - } - } - - const boneBindIndexMap: Nullable[] = []; - const boneTracks = animation.boneTracks; - for (let i = 0; i < boneTracks.length; ++i) { - const boneTrack = boneTracks[i]; - const bone = boneIndexMap.get(boneTrack.name); - if (bone === undefined) { - logger?.warn(`Binding failed: bone ${boneTrack.name} not found`); - boneBindIndexMap.push(null); - } else { - boneBindIndexMap.push(bone); - } - } - - const moveableBoneBindIndexMap: Nullable[] = []; - const moveableBoneTracks = animation.moveableBoneTracks; - for (let i = 0; i < moveableBoneTracks.length; ++i) { - const moveableBoneTrack = moveableBoneTracks[i]; - const bone = boneIndexMap.get(moveableBoneTrack.name); - if (bone === undefined) { - logger?.warn(`Binding failed: bone ${moveableBoneTrack.name} not found`); - moveableBoneBindIndexMap.push(null); - } else { - moveableBoneBindIndexMap.push(bone); - } - } - - const morphController = model.morph; - const morphBindIndexMap: Nullable[] = []; - 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.push(null); - } else { - morphBindIndexMap.push(morphIndices); - } - } - - const runtimeBones = model.sortedRuntimeBones; - const runtimeBoneIndexMap = new Map(); - if (retargetingMap === undefined) { - for (let i = 0; i < bones.length; ++i) { - runtimeBoneIndexMap.set(runtimeBones[i].name, i); - } - } else { - for (let i = 0; i < bones.length; ++i) { - runtimeBoneIndexMap.set(retargetingMap[runtimeBones[i].name] ?? runtimeBones[i].name, i); - } - } - - const ikSolverBindIndexMap: Nullable[] = []; - 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`); - ikSolverBindIndexMap.push(null); - } else { - const ikSolver = runtimeBones[ikBoneIndex].ikSolver; - if (ikSolver === null) { - logger?.warn(`Binding failed: IK solver for bone ${ikBoneName} not found`); - ikSolverBindIndexMap.push(null); - } else { - ikSolverBindIndexMap.push(ikSolver); - } - } - } - - return new MmdRuntimeModelAnimation( - animation, - boneBindIndexMap, - moveableBoneBindIndexMap, - morphController, - morphBindIndexMap, - model.mesh, - ikSolverBindIndexMap - ); - } - - /** - * 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 - */ - public static InduceMaterialRecompile = ( - materials: Material[], - morphController: MmdMorphController, - morphIndices: Nullable[], - logger?: ILogger - ): void => { - let allTextureColorPropertiesAreRecompiled = false; - let allSphereTextureColorPropertiesAreRecompiled = false; - let allToonTextureColorPropertiesAreRecompiled = false; - const recompiledMaterials = new Set(); - - for (let i = 0; i < morphIndices.length; ++i) { - const morphIndex = morphIndices[i]; - if (morphIndex === null) continue; - - for (let j = 0; j < morphIndex.length; ++j) { - const morph = morphController.morphs[morphIndex[j]]; - if (morph.type === PmxObject.Morph.Type.MaterialMorph) { - const elements = morph.materialElements!; - for (let k = 0; k < elements.length; ++k) { - const element = elements[k]; - if (element.textureColor !== null && !allTextureColorPropertiesAreRecompiled) { - const materialIndex = element.index; - if (element.index === -1) { - for (let l = 0; l < materials.length; ++l) { - (materials[l] as MmdStandardMaterial).textureColor; - } - allTextureColorPropertiesAreRecompiled = true; - } else { - (materials[materialIndex] as MmdStandardMaterial).textureColor; - recompiledMaterials.add(materialIndex.toString()); - } - } - - if (element.sphereTextureColor !== null && !allSphereTextureColorPropertiesAreRecompiled) { - const materialIndex = element.index; - if (element.index === -1) { - for (let l = 0; l < materials.length; ++l) { - (materials[l] as MmdStandardMaterial).sphereTextureColor; - } - allSphereTextureColorPropertiesAreRecompiled = true; - } else { - (materials[materialIndex] as MmdStandardMaterial).sphereTextureColor; - recompiledMaterials.add(materialIndex.toString()); - } - } - - if (element.toonTextureColor !== null && !allToonTextureColorPropertiesAreRecompiled) { - const materialIndex = element.index; - if (element.index === -1) { - for (let l = 0; l < materials.length; ++l) { - (materials[l] as MmdStandardMaterial).toonTextureColor; - } - allToonTextureColorPropertiesAreRecompiled = true; - } else { - (materials[materialIndex] as MmdStandardMaterial).toonTextureColor; - recompiledMaterials.add(materialIndex.toString()); - } - } - } - } - } - - if (allTextureColorPropertiesAreRecompiled - && allSphereTextureColorPropertiesAreRecompiled - && allToonTextureColorPropertiesAreRecompiled) { - break; - } - } - - if (allTextureColorPropertiesAreRecompiled - || allSphereTextureColorPropertiesAreRecompiled - || allToonTextureColorPropertiesAreRecompiled) { - logger?.log("All materials could be recompiled for morph animation"); - } else if (0 < recompiledMaterials.size) { - logger?.log(`Materials ${Array.from(recompiledMaterials).join(", ")} could be recompiled for morph animation`); - } - }; -} - -/** - * Mmd runtime camera animation - * - * An object with mmd animation and camera binding information - */ -export class MmdRuntimeCameraAnimation extends MmdRuntimeAnimation { - public readonly animation: MmdCameraAnimationTrack; - - private readonly _camera: MmdCamera; - - private constructor( - animation: MmdAnimation, - camera: MmdCamera - ) { - super(); - - this.animation = animation.cameraTrack; - this._camera = camera; - } - - private static readonly _CameraPositionA = new Vector3(); - private static readonly _CameraPositionB = new Vector3(); - private static readonly _CameraRotationA = new Vector3(); - private static readonly _CameraRotationB = new Vector3(); - - private static readonly _DegToRad = Math.PI / 180; - - /** - * Update animation - * @param frameTime frame time in 30fps - */ - public animate(frameTime: number): void { - const cameraTrack = this.animation; - if (cameraTrack.frameNumbers.length === 0) return; - - const camera = this._camera; - - const clampedFrameTime = Math.max(cameraTrack.startFrame, Math.min(cameraTrack.endFrame, frameTime)); - const upperBoundIndex = this._upperBoundFrameIndex(clampedFrameTime, cameraTrack); - const upperBoundIndexMinusOne = upperBoundIndex - 1; - - const frameNumberA = cameraTrack.frameNumbers[upperBoundIndexMinusOne]; - const frameNumberB = cameraTrack.frameNumbers[upperBoundIndex]; - - if (frameNumberB === undefined || frameNumberA + 1 === frameNumberB) { - const positions = cameraTrack.positions; - camera.position.set( - positions[upperBoundIndexMinusOne * 3], - positions[upperBoundIndexMinusOne * 3 + 1], - positions[upperBoundIndexMinusOne * 3 + 2] - ); - - const rotations = cameraTrack.rotations; - camera.rotation.set( - rotations[upperBoundIndexMinusOne * 3], - rotations[upperBoundIndexMinusOne * 3 + 1], - rotations[upperBoundIndexMinusOne * 3 + 2] - ); - - camera.distance = cameraTrack.distances[upperBoundIndexMinusOne]; - camera.fov = cameraTrack.fovs[upperBoundIndexMinusOne] * MmdRuntimeCameraAnimation._DegToRad; - } else { - const gradient = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); - - const positions = cameraTrack.positions; - const positionInterpolations = cameraTrack.positionInterpolations; - - const positionA = MmdRuntimeCameraAnimation._CameraPositionA.set( - positions[upperBoundIndexMinusOne * 3], - positions[upperBoundIndexMinusOne * 3 + 1], - positions[upperBoundIndexMinusOne * 3 + 2] - ); - const positionB = MmdRuntimeCameraAnimation._CameraPositionB.set( - positions[upperBoundIndex * 3], - positions[upperBoundIndex * 3 + 1], - positions[upperBoundIndex * 3 + 2] - ); - - const xWeight = MmdInterpolator.Interpolate( - positionInterpolations[upperBoundIndex * 12] / 127, // x_x1 - positionInterpolations[upperBoundIndex * 12 + 1] / 127, // x_x2 - positionInterpolations[upperBoundIndex * 12 + 2] / 127, // x_y1 - positionInterpolations[upperBoundIndex * 12 + 3] / 127, // x_y2 - gradient - ); - const yWeight = MmdInterpolator.Interpolate( - positionInterpolations[upperBoundIndex * 12 + 4] / 127, // y_x1 - positionInterpolations[upperBoundIndex * 12 + 5] / 127, // y_x2 - positionInterpolations[upperBoundIndex * 12 + 6] / 127, // y_y1 - positionInterpolations[upperBoundIndex * 12 + 7] / 127, // y_y2 - gradient - ); - const zWeight = MmdInterpolator.Interpolate( - positionInterpolations[upperBoundIndex * 12 + 8] / 127, // z_x1 - positionInterpolations[upperBoundIndex * 12 + 9] / 127, // z_x2 - positionInterpolations[upperBoundIndex * 12 + 10] / 127, // z_y1 - positionInterpolations[upperBoundIndex * 12 + 11] / 127, // z_y2 - gradient - ); - - camera.position.set( - positionA.x + (positionB.x - positionA.x) * xWeight, - positionA.y + (positionB.y - positionA.y) * yWeight, - positionA.z + (positionB.z - positionA.z) * zWeight - ); - - const rotations = cameraTrack.rotations; - const rotationInterpolations = cameraTrack.rotationInterpolations; - - const rotationA = MmdRuntimeCameraAnimation._CameraRotationA.set( - rotations[upperBoundIndexMinusOne * 3], - rotations[upperBoundIndexMinusOne * 3 + 1], - rotations[upperBoundIndexMinusOne * 3 + 2] - ); - const rotationB = MmdRuntimeCameraAnimation._CameraRotationB.set( - rotations[upperBoundIndex * 3], - rotations[upperBoundIndex * 3 + 1], - rotations[upperBoundIndex * 3 + 2] - ); - - const rotationWeight = MmdInterpolator.Interpolate( - rotationInterpolations[upperBoundIndex * 4] / 127, // x1 - rotationInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 - rotationInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 - rotationInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 - gradient - ); - const oneMinusRotationWeight = 1 - rotationWeight; - - camera.rotation.set( - rotationA.x * oneMinusRotationWeight + rotationB.x * rotationWeight, - rotationA.y * oneMinusRotationWeight + rotationB.y * rotationWeight, - rotationA.z * oneMinusRotationWeight + rotationB.z * rotationWeight - ); - - const distanceA = cameraTrack.distances[upperBoundIndexMinusOne]; - const distanceB = cameraTrack.distances[upperBoundIndex]; - - const distanceWeight = MmdInterpolator.Interpolate( - cameraTrack.distanceInterpolations[upperBoundIndex * 4] / 127, // x1 - cameraTrack.distanceInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 - cameraTrack.distanceInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 - cameraTrack.distanceInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 - gradient - ); - - camera.distance = distanceA + (distanceB - distanceA) * distanceWeight; - - const fovA = cameraTrack.fovs[upperBoundIndexMinusOne]; - const fovB = cameraTrack.fovs[upperBoundIndex]; - - const fovWeight = MmdInterpolator.Interpolate( - cameraTrack.fovInterpolations[upperBoundIndex * 4] / 127, // x1 - cameraTrack.fovInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 - cameraTrack.fovInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 - cameraTrack.fovInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 - gradient - ); - - camera.fov = (fovA + (fovB - fovA) * fovWeight) * MmdRuntimeCameraAnimation._DegToRad; - } - } - - /** - * bind animation to camera - * @param animation animation to bind - * @param camera bind target - * @returns MmdRuntimeCameraAnimation instance - */ - public static Create(animation: MmdAnimation, camera: MmdCamera): MmdRuntimeCameraAnimation { - return new MmdRuntimeCameraAnimation(animation, camera); - } -} - /** * Mmd Interpolator for MMD animation interpolation */ diff --git a/src/Runtime/Animation/mmdRuntimeCameraAnimation.ts b/src/Runtime/Animation/mmdRuntimeCameraAnimation.ts new file mode 100644 index 00000000..cf1f3235 --- /dev/null +++ b/src/Runtime/Animation/mmdRuntimeCameraAnimation.ts @@ -0,0 +1,196 @@ +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; + +import { MmdAnimation } from "@/Loader/Animation/mmdAnimation"; +import type { MmdCameraAnimationTrack } from "@/Loader/Animation/mmdAnimationTrack"; + +import type { MmdCamera } from "../mmdCamera"; +import { MmdInterpolator, MmdRuntimeAnimation } from "./mmdRuntimeAnimation"; + +/** + * Mmd runtime camera animation + * + * An object with mmd animation and camera binding information + */ +export class MmdRuntimeCameraAnimation extends MmdRuntimeAnimation { + public readonly animation: MmdCameraAnimationTrack; + + private readonly _camera: MmdCamera; + + private constructor( + animation: MmdAnimation, + camera: MmdCamera + ) { + super(); + + this.animation = animation.cameraTrack; + this._camera = camera; + } + + private static readonly _CameraPositionA = new Vector3(); + private static readonly _CameraPositionB = new Vector3(); + private static readonly _CameraRotationA = new Vector3(); + private static readonly _CameraRotationB = new Vector3(); + + private static readonly _DegToRad = Math.PI / 180; + + /** + * Update animation + * @param frameTime frame time in 30fps + */ + public animate(frameTime: number): void { + const cameraTrack = this.animation; + if (cameraTrack.frameNumbers.length === 0) return; + + const camera = this._camera; + + const clampedFrameTime = Math.max(cameraTrack.startFrame, Math.min(cameraTrack.endFrame, frameTime)); + const upperBoundIndex = this._upperBoundFrameIndex(clampedFrameTime, cameraTrack); + const upperBoundIndexMinusOne = upperBoundIndex - 1; + + const frameNumberA = cameraTrack.frameNumbers[upperBoundIndexMinusOne]; + const frameNumberB = cameraTrack.frameNumbers[upperBoundIndex]; + + if (frameNumberB === undefined || frameNumberA + 1 === frameNumberB) { + const positions = cameraTrack.positions; + camera.position.set( + positions[upperBoundIndexMinusOne * 3], + positions[upperBoundIndexMinusOne * 3 + 1], + positions[upperBoundIndexMinusOne * 3 + 2] + ); + + const rotations = cameraTrack.rotations; + camera.rotation.set( + rotations[upperBoundIndexMinusOne * 3], + rotations[upperBoundIndexMinusOne * 3 + 1], + rotations[upperBoundIndexMinusOne * 3 + 2] + ); + + camera.distance = cameraTrack.distances[upperBoundIndexMinusOne]; + camera.fov = cameraTrack.fovs[upperBoundIndexMinusOne] * MmdRuntimeCameraAnimation._DegToRad; + } else { + const gradient = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); + + const positions = cameraTrack.positions; + const positionInterpolations = cameraTrack.positionInterpolations; + + const positionA = MmdRuntimeCameraAnimation._CameraPositionA.set( + positions[upperBoundIndexMinusOne * 3], + positions[upperBoundIndexMinusOne * 3 + 1], + positions[upperBoundIndexMinusOne * 3 + 2] + ); + const positionB = MmdRuntimeCameraAnimation._CameraPositionB.set( + positions[upperBoundIndex * 3], + positions[upperBoundIndex * 3 + 1], + positions[upperBoundIndex * 3 + 2] + ); + + const xWeight = MmdInterpolator.Interpolate( + positionInterpolations[upperBoundIndex * 12] / 127, // x_x1 + positionInterpolations[upperBoundIndex * 12 + 1] / 127, // x_x2 + positionInterpolations[upperBoundIndex * 12 + 2] / 127, // x_y1 + positionInterpolations[upperBoundIndex * 12 + 3] / 127, // x_y2 + gradient + ); + const yWeight = MmdInterpolator.Interpolate( + positionInterpolations[upperBoundIndex * 12 + 4] / 127, // y_x1 + positionInterpolations[upperBoundIndex * 12 + 5] / 127, // y_x2 + positionInterpolations[upperBoundIndex * 12 + 6] / 127, // y_y1 + positionInterpolations[upperBoundIndex * 12 + 7] / 127, // y_y2 + gradient + ); + const zWeight = MmdInterpolator.Interpolate( + positionInterpolations[upperBoundIndex * 12 + 8] / 127, // z_x1 + positionInterpolations[upperBoundIndex * 12 + 9] / 127, // z_x2 + positionInterpolations[upperBoundIndex * 12 + 10] / 127, // z_y1 + positionInterpolations[upperBoundIndex * 12 + 11] / 127, // z_y2 + gradient + ); + + camera.position.set( + positionA.x + (positionB.x - positionA.x) * xWeight, + positionA.y + (positionB.y - positionA.y) * yWeight, + positionA.z + (positionB.z - positionA.z) * zWeight + ); + + const rotations = cameraTrack.rotations; + const rotationInterpolations = cameraTrack.rotationInterpolations; + + const rotationA = MmdRuntimeCameraAnimation._CameraRotationA.set( + rotations[upperBoundIndexMinusOne * 3], + rotations[upperBoundIndexMinusOne * 3 + 1], + rotations[upperBoundIndexMinusOne * 3 + 2] + ); + const rotationB = MmdRuntimeCameraAnimation._CameraRotationB.set( + rotations[upperBoundIndex * 3], + rotations[upperBoundIndex * 3 + 1], + rotations[upperBoundIndex * 3 + 2] + ); + + const rotationWeight = MmdInterpolator.Interpolate( + rotationInterpolations[upperBoundIndex * 4] / 127, // x1 + rotationInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 + rotationInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 + rotationInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 + gradient + ); + const oneMinusRotationWeight = 1 - rotationWeight; + + camera.rotation.set( + rotationA.x * oneMinusRotationWeight + rotationB.x * rotationWeight, + rotationA.y * oneMinusRotationWeight + rotationB.y * rotationWeight, + rotationA.z * oneMinusRotationWeight + rotationB.z * rotationWeight + ); + + const distanceA = cameraTrack.distances[upperBoundIndexMinusOne]; + const distanceB = cameraTrack.distances[upperBoundIndex]; + + const distanceWeight = MmdInterpolator.Interpolate( + cameraTrack.distanceInterpolations[upperBoundIndex * 4] / 127, // x1 + cameraTrack.distanceInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 + cameraTrack.distanceInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 + cameraTrack.distanceInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 + gradient + ); + + camera.distance = distanceA + (distanceB - distanceA) * distanceWeight; + + const fovA = cameraTrack.fovs[upperBoundIndexMinusOne]; + const fovB = cameraTrack.fovs[upperBoundIndex]; + + const fovWeight = MmdInterpolator.Interpolate( + cameraTrack.fovInterpolations[upperBoundIndex * 4] / 127, // x1 + cameraTrack.fovInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 + cameraTrack.fovInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 + cameraTrack.fovInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 + gradient + ); + + camera.fov = (fovA + (fovB - fovA) * fovWeight) * MmdRuntimeCameraAnimation._DegToRad; + } + } + + /** + * bind animation to camera + * @param animation animation to bind + * @param camera bind target + * @returns MmdRuntimeCameraAnimation instance + */ + public static Create(animation: MmdAnimation, camera: MmdCamera): MmdRuntimeCameraAnimation { + return new MmdRuntimeCameraAnimation(animation, camera); + } +} + +declare module "../../Loader/Animation/mmdAnimation" { + export interface MmdAnimation { + /** + * Create runtime camera animation + * @param camera bind target + * @returns MmdRuntimeCameraAnimation instance + */ + createRuntimeCameraAnimation(camera: MmdCamera): MmdRuntimeCameraAnimation; + } +} + +MmdAnimation.prototype.createRuntimeCameraAnimation = function(camera: MmdCamera): MmdRuntimeCameraAnimation { + return MmdRuntimeCameraAnimation.Create(this, camera); +}; diff --git a/src/Runtime/Animation/mmdRuntimeModelAnimation.ts b/src/Runtime/Animation/mmdRuntimeModelAnimation.ts new file mode 100644 index 00000000..ac450b6a --- /dev/null +++ b/src/Runtime/Animation/mmdRuntimeModelAnimation.ts @@ -0,0 +1,557 @@ +import type { Bone } from "@babylonjs/core/Bones/bone"; +import type { Material } from "@babylonjs/core/Materials/material"; +import { Space } from "@babylonjs/core/Maths/math.axis"; +import { Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector"; +import type { Nullable } from "@babylonjs/core/types"; + +import { MmdAnimation } from "@/Loader/Animation/mmdAnimation"; +import type { MmdStandardMaterial } from "@/Loader/mmdStandardMaterial"; +import type { ILogger } from "@/Loader/Parser/ILogger"; +import { PmxObject } from "@/Loader/Parser/pmxObject"; + +import type { IIkSolver } from "../ikSolver"; +import type { RuntimeMmdMesh } from "../mmdMesh"; +import type { MmdModel } from "../mmdModel"; +import type { MmdMorphController } from "../mmdMorphController"; +import { MmdInterpolator, MmdRuntimeAnimation } from "./mmdRuntimeAnimation"; + +type MorphIndices = readonly number[]; + +/** + * Mmd runtime model animation + * + * An object with mmd animation and model binding information + */ +export class MmdRuntimeModelAnimation extends MmdRuntimeAnimation { + /** + * Mmd animation + */ + public readonly animation: MmdAnimation; + + private readonly _boneBindIndexMap: Nullable[]; + private readonly _moveableBoneBindIndexMap: Nullable[]; + private readonly _morphController: MmdMorphController; + private readonly _morphBindIndexMap: Nullable[]; + private readonly _mesh: RuntimeMmdMesh; + private readonly _ikSolverBindIndexMap: Nullable[]; + + private constructor( + animation: MmdAnimation, + boneBindIndexMap: Nullable[], + moveableBoneBindIndexMap: Nullable[], + morphController: MmdMorphController, + morphBindIndexMap: Nullable[], + mesh: RuntimeMmdMesh, + ikSolverBindIndexMap: Nullable[] + ) { + super(); + + this.animation = animation; + + this._boneBindIndexMap = boneBindIndexMap; + this._moveableBoneBindIndexMap = moveableBoneBindIndexMap; + this._morphController = morphController; + this._morphBindIndexMap = morphBindIndexMap; + this._mesh = mesh; + this._ikSolverBindIndexMap = ikSolverBindIndexMap; + } + + private static readonly _BonePositionA = new Vector3(); + private static readonly _BonePositionB = new Vector3(); + private static readonly _BoneOriginalPosition = new Vector3(); + private static readonly _BoneRotationA = new Quaternion(); + private static readonly _BoneRotationB = new Quaternion(); + private static readonly _BoneOriginalRotation = new Quaternion(); + + /** + * Update animation + * @param frameTime frame time in 30fps + */ + public animate(frameTime: number): void { + const animation = this.animation; + + const boneTracks = animation.boneTracks; + if (0 < boneTracks.length) { + const boneBindIndexMap = this._boneBindIndexMap; + for (let i = 0; i < boneTracks.length; ++i) { + const bone = boneBindIndexMap[i]; + if (bone === null) continue; + + const boneTrack = boneTracks[i]; + const clampedFrameTime = Math.max(boneTrack.startFrame, Math.min(boneTrack.endFrame, frameTime)); + const upperBoundIndex = this._upperBoundFrameIndex(clampedFrameTime, boneTrack); + const upperBoundIndexMinusOne = upperBoundIndex - 1; + + const frameNumberB = boneTrack.frameNumbers[upperBoundIndex]; + if (frameNumberB === undefined) { + const rotations = boneTrack.rotations; + bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); + bone.setRotationQuaternion( + MmdRuntimeModelAnimation._BoneOriginalRotation.multiply( + MmdRuntimeModelAnimation._BoneRotationB.set( + rotations[upperBoundIndexMinusOne * 4], + rotations[upperBoundIndexMinusOne * 4 + 1], + rotations[upperBoundIndexMinusOne * 4 + 2], + rotations[upperBoundIndexMinusOne * 4 + 3] + ) + ), + Space.LOCAL + ); + } else { + const frameNumberA = boneTrack.frameNumbers[upperBoundIndexMinusOne]; + const gradient = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); + + const rotations = boneTrack.rotations; + const rotationInterpolations = boneTrack.rotationInterpolations; + + const rotationA = MmdRuntimeModelAnimation._BoneRotationA.set( + rotations[upperBoundIndexMinusOne * 4], + rotations[upperBoundIndexMinusOne * 4 + 1], + rotations[upperBoundIndexMinusOne * 4 + 2], + rotations[upperBoundIndexMinusOne * 4 + 3] + ); + const rotationB = MmdRuntimeModelAnimation._BoneRotationB.set( + rotations[upperBoundIndex * 4], + rotations[upperBoundIndex * 4 + 1], + rotations[upperBoundIndex * 4 + 2], + rotations[upperBoundIndex * 4 + 3] + ); + + const weight = MmdInterpolator.Interpolate( + rotationInterpolations[upperBoundIndex * 4] / 127, // x1 + rotationInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 + rotationInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 + rotationInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 + gradient + ); + + Quaternion.SlerpToRef(rotationA, rotationB, weight, rotationA); + bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); + bone.setRotationQuaternion( + MmdRuntimeModelAnimation._BoneOriginalRotation.multiply(rotationA), + Space.LOCAL + ); + } + } + } + + const moveableBoneTracks = animation.moveableBoneTracks; + if (0 < moveableBoneTracks.length) { + const boneBindIndexMap = this._moveableBoneBindIndexMap; + for (let i = 0; i < moveableBoneTracks.length; ++i) { + const bone = boneBindIndexMap[i]; + if (bone === null) continue; + + const boneTrack = moveableBoneTracks[i]; + const clampedFrameTime = Math.max(boneTrack.startFrame, Math.min(boneTrack.endFrame, frameTime)); + const upperBoundIndex = this._upperBoundFrameIndex(clampedFrameTime, boneTrack); + const upperBoundIndexMinusOne = upperBoundIndex - 1; + + const frameNumberB = boneTrack.frameNumbers[upperBoundIndex]; + if (frameNumberB === undefined) { + const positions = boneTrack.positions; + bone.getPositionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalPosition); + bone.setPosition( + MmdRuntimeModelAnimation._BoneOriginalPosition.add( + MmdRuntimeModelAnimation._BonePositionB.set( + positions[upperBoundIndexMinusOne * 3], + positions[upperBoundIndexMinusOne * 3 + 1], + positions[upperBoundIndexMinusOne * 3 + 2] + ) + ), + Space.LOCAL + ); + + const rotations = boneTrack.rotations; + bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); + bone.setRotationQuaternion( + MmdRuntimeModelAnimation._BoneOriginalRotation.multiply( + MmdRuntimeModelAnimation._BoneRotationB.set( + rotations[upperBoundIndexMinusOne * 4], + rotations[upperBoundIndexMinusOne * 4 + 1], + rotations[upperBoundIndexMinusOne * 4 + 2], + rotations[upperBoundIndexMinusOne * 4 + 3] + ) + ), + Space.LOCAL + ); + } else { + const frameNumberA = boneTrack.frameNumbers[upperBoundIndexMinusOne]; + const gradient = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); + + const positions = boneTrack.positions; + const positionInterpolations = boneTrack.positionInterpolations; + + const positionA = MmdRuntimeModelAnimation._BonePositionA.set( + positions[upperBoundIndexMinusOne * 3], + positions[upperBoundIndexMinusOne * 3 + 1], + positions[upperBoundIndexMinusOne * 3 + 2] + ); + const positionB = MmdRuntimeModelAnimation._BonePositionB.set( + positions[upperBoundIndex * 3], + positions[upperBoundIndex * 3 + 1], + positions[upperBoundIndex * 3 + 2] + ); + + const xWeight = MmdInterpolator.Interpolate( + positionInterpolations[upperBoundIndex * 12] / 127, // x_x1 + positionInterpolations[upperBoundIndex * 12 + 1] / 127, // x_x2 + positionInterpolations[upperBoundIndex * 12 + 2] / 127, // x_y1 + positionInterpolations[upperBoundIndex * 12 + 3] / 127, // x_y2 + gradient + ); + const yWeight = MmdInterpolator.Interpolate( + positionInterpolations[upperBoundIndex * 12 + 4] / 127, // y_x1 + positionInterpolations[upperBoundIndex * 12 + 5] / 127, // y_x2 + positionInterpolations[upperBoundIndex * 12 + 6] / 127, // y_y1 + positionInterpolations[upperBoundIndex * 12 + 7] / 127, // y_y2 + gradient + ); + const zWeight = MmdInterpolator.Interpolate( + positionInterpolations[upperBoundIndex * 12 + 8] / 127, // z_x1 + positionInterpolations[upperBoundIndex * 12 + 9] / 127, // z_x2 + positionInterpolations[upperBoundIndex * 12 + 10] / 127, // z_y1 + positionInterpolations[upperBoundIndex * 12 + 11] / 127, // z_y2 + gradient + ); + + positionA.x += (positionB.x - positionA.x) * xWeight; + positionA.y += (positionB.y - positionA.y) * yWeight; + positionA.z += (positionB.z - positionA.z) * zWeight; + bone.getPositionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalPosition); + bone.setPosition(MmdRuntimeModelAnimation._BoneOriginalPosition.add(positionA), Space.LOCAL); + + const rotations = boneTrack.rotations; + const rotationInterpolations = boneTrack.rotationInterpolations; + + const rotationA = MmdRuntimeModelAnimation._BoneRotationA.set( + rotations[upperBoundIndexMinusOne * 4], + rotations[upperBoundIndexMinusOne * 4 + 1], + rotations[upperBoundIndexMinusOne * 4 + 2], + rotations[upperBoundIndexMinusOne * 4 + 3] + ); + const rotationB = MmdRuntimeModelAnimation._BoneRotationB.set( + rotations[upperBoundIndex * 4], + rotations[upperBoundIndex * 4 + 1], + rotations[upperBoundIndex * 4 + 2], + rotations[upperBoundIndex * 4 + 3] + ); + + const weight = MmdInterpolator.Interpolate( + rotationInterpolations[upperBoundIndex * 4] / 127, // x1 + rotationInterpolations[upperBoundIndex * 4 + 1] / 127, // x2 + rotationInterpolations[upperBoundIndex * 4 + 2] / 127, // y1 + rotationInterpolations[upperBoundIndex * 4 + 3] / 127, // y2 + gradient + ); + + Quaternion.SlerpToRef(rotationA, rotationB, weight, rotationA); + bone.getRotationQuaternionToRef(Space.LOCAL, null, MmdRuntimeModelAnimation._BoneOriginalRotation); + bone.setRotationQuaternion( + MmdRuntimeModelAnimation._BoneOriginalRotation.multiply(rotationA), + Space.LOCAL + ); + } + } + } + + 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]; + + 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) { + // this clamp will be removed when morph target recompilation problem is solved + // ref: https://github.com/BabylonJS/Babylon.js/issues/14008 + const weight = Math.max(morphTrack.weights[upperBoundIndexMinusOne], 1e-16); + for (let j = 0; j < morphIndices.length; ++j) { + morphController.setMorphWeightFromIndex(morphIndices[j], weight); + } + } else { + const frameNumberA = morphTrack.frameNumbers[upperBoundIndexMinusOne]; + const relativeTime = (clampedFrameTime - frameNumberA) / (frameNumberB - frameNumberA); + + const weightA = morphTrack.weights[upperBoundIndexMinusOne]; + const weightB = morphTrack.weights[upperBoundIndex]; + + // this clamp will be removed when morph target recompilation problem is solved + // ref: https://github.com/BabylonJS/Babylon.js/issues/14008 + const weight = Math.max(weightA + (weightB - weightA) * relativeTime, 1e-16); + for (let j = 0; j < morphIndices.length; ++j) { + morphController.setMorphWeightFromIndex(morphIndices[j], weight); + } + } + } + } + + 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; + + this._mesh.visibility = propertyTrack.visibles[stepIndex]; + + const ikSolverBindIndexMap = this._ikSolverBindIndexMap; + const propertyTrackIkStates = propertyTrack.ikStates; + for (let i = 0; i < ikSolverBindIndexMap.length; ++i) { + const ikSolver = ikSolverBindIndexMap[i]; + if (ikSolver === null) continue; + + const ikState = propertyTrackIkStates[i]; + ikSolver.enabled = ikState[stepIndex] !== 0; + } + } + } + + private _materialRecompileInduced = false; + + /** + * 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 logger logger + */ + public induceMaterialRecompile(logger?: ILogger): void { + if (this._materialRecompileInduced) return; + this._materialRecompileInduced = true; + + MmdRuntimeModelAnimation.InduceMaterialRecompile( + this._mesh.material.subMaterials, + this._morphController, + this._morphBindIndexMap, + logger + ); + } + + /** + * Bind animation to model and prepare material for morph animation + * @param animation Animation to bind + * @param model Bind target + * @param retargetingMap Model bone name to animation bone name map + * @param logger Logger + * @return MmdRuntimeModelAnimation instance + */ + public static Create(animation: MmdAnimation, model: MmdModel, retargetingMap?: { [key: string]: string }, logger?: ILogger): MmdRuntimeModelAnimation { + const skeleton = model.mesh.skeleton; + const bones = skeleton.bones; + + const boneIndexMap = new Map(); + if (retargetingMap === undefined) { + for (let i = 0; i < bones.length; ++i) { + boneIndexMap.set(bones[i].name, bones[i]); + } + } else { + for (let i = 0; i < bones.length; ++i) { + boneIndexMap.set(retargetingMap[bones[i].name] ?? bones[i].name, bones[i]); + } + } + + const boneBindIndexMap: Nullable[] = []; + const boneTracks = animation.boneTracks; + for (let i = 0; i < boneTracks.length; ++i) { + const boneTrack = boneTracks[i]; + const bone = boneIndexMap.get(boneTrack.name); + if (bone === undefined) { + logger?.warn(`Binding failed: bone ${boneTrack.name} not found`); + boneBindIndexMap.push(null); + } else { + boneBindIndexMap.push(bone); + } + } + + const moveableBoneBindIndexMap: Nullable[] = []; + const moveableBoneTracks = animation.moveableBoneTracks; + for (let i = 0; i < moveableBoneTracks.length; ++i) { + const moveableBoneTrack = moveableBoneTracks[i]; + const bone = boneIndexMap.get(moveableBoneTrack.name); + if (bone === undefined) { + logger?.warn(`Binding failed: bone ${moveableBoneTrack.name} not found`); + moveableBoneBindIndexMap.push(null); + } else { + moveableBoneBindIndexMap.push(bone); + } + } + + const morphController = model.morph; + const morphBindIndexMap: Nullable[] = []; + 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.push(null); + } else { + morphBindIndexMap.push(morphIndices); + } + } + + const runtimeBones = model.sortedRuntimeBones; + const runtimeBoneIndexMap = new Map(); + if (retargetingMap === undefined) { + for (let i = 0; i < bones.length; ++i) { + runtimeBoneIndexMap.set(runtimeBones[i].name, i); + } + } else { + for (let i = 0; i < bones.length; ++i) { + runtimeBoneIndexMap.set(retargetingMap[runtimeBones[i].name] ?? runtimeBones[i].name, i); + } + } + + const ikSolverBindIndexMap: Nullable[] = []; + 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`); + ikSolverBindIndexMap.push(null); + } else { + const ikSolver = runtimeBones[ikBoneIndex].ikSolver; + if (ikSolver === null) { + logger?.warn(`Binding failed: IK solver for bone ${ikBoneName} not found`); + ikSolverBindIndexMap.push(null); + } else { + ikSolverBindIndexMap.push(ikSolver); + } + } + } + + return new MmdRuntimeModelAnimation( + animation, + boneBindIndexMap, + moveableBoneBindIndexMap, + morphController, + morphBindIndexMap, + model.mesh, + ikSolverBindIndexMap + ); + } + + /** + * 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 + */ + public static InduceMaterialRecompile = ( + materials: Material[], + morphController: MmdMorphController, + morphIndices: Nullable[], + logger?: ILogger + ): void => { + let allTextureColorPropertiesAreRecompiled = false; + let allSphereTextureColorPropertiesAreRecompiled = false; + let allToonTextureColorPropertiesAreRecompiled = false; + const recompiledMaterials = new Set(); + + for (let i = 0; i < morphIndices.length; ++i) { + const morphIndex = morphIndices[i]; + if (morphIndex === null) continue; + + for (let j = 0; j < morphIndex.length; ++j) { + const morph = morphController.morphs[morphIndex[j]]; + if (morph.type === PmxObject.Morph.Type.MaterialMorph) { + const elements = morph.materialElements!; + for (let k = 0; k < elements.length; ++k) { + const element = elements[k]; + if (element.textureColor !== null && !allTextureColorPropertiesAreRecompiled) { + const materialIndex = element.index; + if (element.index === -1) { + for (let l = 0; l < materials.length; ++l) { + (materials[l] as MmdStandardMaterial).textureColor; + } + allTextureColorPropertiesAreRecompiled = true; + } else { + (materials[materialIndex] as MmdStandardMaterial).textureColor; + recompiledMaterials.add(materialIndex.toString()); + } + } + + if (element.sphereTextureColor !== null && !allSphereTextureColorPropertiesAreRecompiled) { + const materialIndex = element.index; + if (element.index === -1) { + for (let l = 0; l < materials.length; ++l) { + (materials[l] as MmdStandardMaterial).sphereTextureColor; + } + allSphereTextureColorPropertiesAreRecompiled = true; + } else { + (materials[materialIndex] as MmdStandardMaterial).sphereTextureColor; + recompiledMaterials.add(materialIndex.toString()); + } + } + + if (element.toonTextureColor !== null && !allToonTextureColorPropertiesAreRecompiled) { + const materialIndex = element.index; + if (element.index === -1) { + for (let l = 0; l < materials.length; ++l) { + (materials[l] as MmdStandardMaterial).toonTextureColor; + } + allToonTextureColorPropertiesAreRecompiled = true; + } else { + (materials[materialIndex] as MmdStandardMaterial).toonTextureColor; + recompiledMaterials.add(materialIndex.toString()); + } + } + } + } + } + + if (allTextureColorPropertiesAreRecompiled + && allSphereTextureColorPropertiesAreRecompiled + && allToonTextureColorPropertiesAreRecompiled) { + break; + } + } + + if (allTextureColorPropertiesAreRecompiled + || allSphereTextureColorPropertiesAreRecompiled + || allToonTextureColorPropertiesAreRecompiled) { + logger?.log("All materials could be recompiled for morph animation"); + } else if (0 < recompiledMaterials.size) { + logger?.log(`Materials ${Array.from(recompiledMaterials).join(", ")} could be recompiled for morph animation`); + } + }; +} + +declare module "../../Loader/Animation/mmdAnimation" { + export interface MmdAnimation { + /** + * Create runtime model animation + * @param model Bind target + * @param retargetingMap Model bone name to animation bone name map + * @param logger Logger + * @returns MmdRuntimeModelAnimation instance + */ + createRuntimeModelAnimation(model: MmdModel, retargetingMap?: { [key: string]: string }, logger?: ILogger): MmdRuntimeModelAnimation; + } +} + +MmdAnimation.prototype.createRuntimeModelAnimation = function( + model: MmdModel, + retargetingMap?: { [key: string]: string }, + logger?: ILogger +): MmdRuntimeModelAnimation { + return MmdRuntimeModelAnimation.Create(this, model, retargetingMap, logger); +}; diff --git a/src/Runtime/mmdCamera.ts b/src/Runtime/mmdCamera.ts index 11be03c4..3d52613e 100644 --- a/src/Runtime/mmdCamera.ts +++ b/src/Runtime/mmdCamera.ts @@ -6,7 +6,7 @@ import type { Nullable } from "@babylonjs/core/types"; import type { MmdAnimation } from "@/Loader/Animation/mmdAnimation"; -import { MmdRuntimeCameraAnimation } from "./Animation/mmdRuntimeAnimation"; +import type { MmdRuntimeCameraAnimation } from "./Animation/mmdRuntimeCameraAnimation"; /** * MMD camera @@ -67,7 +67,7 @@ export class MmdCamera extends Camera { * @param animation The animation to add */ public addAnimation(animation: MmdAnimation): void { - const runtimeAnimation = MmdRuntimeCameraAnimation.Create(animation, this); + const runtimeAnimation = animation.createRuntimeCameraAnimation(this); this._animationIndexMap.set(animation.name, this._animations.length); this._animations.push(runtimeAnimation); } diff --git a/src/Runtime/mmdModel.ts b/src/Runtime/mmdModel.ts index 962c208a..f15525b6 100644 --- a/src/Runtime/mmdModel.ts +++ b/src/Runtime/mmdModel.ts @@ -9,7 +9,7 @@ import type { Nullable } from "@babylonjs/core/types"; import type { MmdAnimation } from "@/Loader/Animation/mmdAnimation"; import type { MmdModelMetadata } from "@/Loader/mmdModelMetadata"; -import { MmdRuntimeModelAnimation } from "./Animation/mmdRuntimeAnimation"; +import type { MmdRuntimeModelAnimation } from "./Animation/mmdRuntimeModelAnimation"; import { AppendTransformSolver } from "./appendTransformSolver"; import { IkSolver } from "./ikSolver"; import type { ILogger } from "./ILogger"; @@ -160,7 +160,7 @@ export class MmdModel { * @param retargetingMap Model bone name to animation bone name map */ public addAnimation(animation: MmdAnimation, retargetingMap?: { [key: string]: string }): void { - const runtimeAnimation = MmdRuntimeModelAnimation.Create(animation, this, retargetingMap, this._logger); + const runtimeAnimation = animation.createRuntimeModelAnimation(this, retargetingMap, this._logger); this._animationIndexMap.set(animation.name, this._animations.length); this._animations.push(runtimeAnimation); } diff --git a/src/Runtime/mmdRuntime.ts b/src/Runtime/mmdRuntime.ts index 0cf2fab8..d8bdf2f4 100644 --- a/src/Runtime/mmdRuntime.ts +++ b/src/Runtime/mmdRuntime.ts @@ -5,7 +5,8 @@ import { Observable } from "@babylonjs/core/Misc/observable"; import type { Scene } from "@babylonjs/core/scene"; import type { Nullable } from "@babylonjs/core/types"; -import type { MmdRuntimeCameraAnimation, MmdRuntimeModelAnimation } from "./Animation/mmdRuntimeAnimation"; +import type { MmdRuntimeCameraAnimation } from "./Animation/mmdRuntimeCameraAnimation"; +import type { MmdRuntimeModelAnimation } from "./Animation/mmdRuntimeModelAnimation"; import type { IPlayer } from "./Audio/IAudioPlayer"; import type { ILogger } from "./ILogger"; import type { IMmdMaterialProxyConstructor } from "./IMmdMaterialProxy"; diff --git a/src/Test/Scene/physicsTestScene.ts b/src/Test/Scene/physicsTestScene.ts index 0d39167c..7811ad0b 100644 --- a/src/Test/Scene/physicsTestScene.ts +++ b/src/Test/Scene/physicsTestScene.ts @@ -5,6 +5,8 @@ import "@babylonjs/core/Rendering/prePassRendererSceneComponent"; import "@babylonjs/core/Rendering/depthRendererSceneComponent"; import "@babylonjs/core/Rendering/geometryBufferRendererSceneComponent"; import "@/Loader/Optimized/bpmxLoader"; +import "@/Runtime/Animation/mmdRuntimeCameraAnimation"; +import "@/Runtime/Animation/mmdRuntimeModelAnimation"; import { ArcRotateCamera } from "@babylonjs/core/Cameras/arcRotateCamera"; import { PhysicsViewer } from "@babylonjs/core/Debug/physicsViewer"; diff --git a/src/index.ts b/src/index.ts index be349683..90f06740 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,8 @@ import "@/Loader/mmdOutlineRenderer"; import "@/Loader/pmxLoader"; import "@/Loader/Optimized/bpmxLoader"; +import "@/Runtime/Animation/mmdRuntimeCameraAnimation"; +import "@/Runtime/Animation/mmdRuntimeModelAnimation"; // Loader/Animation export { MmdAnimation } from "@/Loader/Animation/mmdAnimation"; @@ -36,7 +38,9 @@ export { TextureAlphaChecker, TransparencyMode } from "@/Loader/textureAlphaChec export { VmdLoader } from "@/Loader/vmdLoader"; // Runtime/Animation -export { MmdRuntimeAnimation, MmdRuntimeCameraAnimation, MmdRuntimeModelAnimation } from "@/Runtime/Animation/mmdRuntimeAnimation"; +export { MmdRuntimeAnimation } from "@/Runtime/Animation/mmdRuntimeAnimation"; +export { MmdRuntimeCameraAnimation } from "@/Runtime/Animation/mmdRuntimeCameraAnimation"; +export { MmdRuntimeModelAnimation } from "@/Runtime/Animation/mmdRuntimeModelAnimation"; // Runtime/Audio export { IPlayer } from "@/Runtime/Audio/IAudioPlayer";