using System; using System.Collections; using System.Collections.Generic; using LibMMD.Model; using LibMMD.Motion; using LibMMD.Reader; using LibMMD.Unity3D.BonePose; using LibMMD.Unity3D.Physics; using Unity.Collections; using Unity.Jobs; using UnityEngine; namespace LibMMD.Unity3D.Animation { // Owns VMD/IK/Bullet-physics pose evaluation and vertex-morph BlendShape playback, and // drives bone Transforms through an Animator + PlayableGraph (see MmdAnimationDriver). // MmdGameObject owns model/mesh/bone-hierarchy loading and calls LoadModel() once that's // ready; MmdGameObject's public playback API (Playing/Loop/MotionPos/PhysicsMode/etc.) is // a thin delegate over this component so external callers are unaffected by the split. internal sealed class MmdAnimator : MonoBehaviour { public enum MmdEvent { SlowPoseCalculation } public delegate void MmdEventDelegate(MmdEvent mmdEvent); public bool AutoPhysicsStepLength = true; public bool Playing; public bool Loop; public bool EnableLog = true; [SerializeField] private bool _enableVmdIk = true; public bool EnableVmdIk { get => _enableVmdIk; set { if (_enableVmdIk == value) return; _enableVmdIk = value; if (_poser != null) _poser.EnableIk = value; _bakedPoseController.Clear(); } } public int PhysicsCacheFrameSize = 300; internal MmdPhysicsMode PhysicsMode = MmdPhysicsMode.Bullet; public float PhysicsFps = 120.0f; public MmdEventDelegate OnMmdEvent { get; set; } = mmdEvent => { }; public string MotionPath { get; private set; } public string BonePoseFilePath { get; private set; } public double MotionLength => _motion != null ? _motion.Length / 30.0 : 0.0; public double MotionPos { get { return _playTime; } set { _playTime = value; _motionPlayer.SeekTime(_playTime); _poser.PrePhysicsPosing(); _bakedPoseController.Apply(_poser, _playTime); ApplyRuntimeRigIk(); _physicsController.Reset(); _poser.PostPhysicsPosing(); PushLivePoseToAnimator(); _morphRenderer.Update(_model, _motionPlayer, _playTime); } } private MmdModel _model; private GameObject _boneRootGameObject; private GameObject[] _bones; private bool[] _bonePhysicsControlFlags; private Poser _poser; private bool _modelLoaded; private IMmdPhysicsBackend _physicsController = new NoneMmdPhysicsBackend(); private MmdMotion _motion; private MotionPlayer _motionPlayer; private double _playTime; private BonePoseFileStorage _bonePoseFileStorage; private Animator _animator; private readonly MmdAnimationDriver _animationDriver = new MmdAnimationDriver(); private readonly Dictionary _boneIndexes = new Dictionary(); private NativeArray _ikInputs; private NativeArray _ikOutputs; private readonly List _activeIkBindings = new List(4); private struct RuntimeIkBinding { public int Root, Mid, Tip; } private readonly MmdBakedPoseController _bakedPoseController = new MmdBakedPoseController(); // true when the current pose came from LoadPose (a static VPD pose, no VMD motion) - // Update()'s Bullet branch must not call _motionPlayer.SeekTime in that case (it may // be null, or stale from a previously loaded motion). private bool _staticPoseMode; private const int MaxBulletSubSteps = 10; private readonly MmdMorphRenderer _morphRenderer = new MmdMorphRenderer(); // Called by MmdGameObject once it has built the mesh/bone hierarchy/BlendShapes for a // newly loaded model. Owns Poser/physics/Animator construction and pushes the initial // pose. boneRootGameObject is the "Model" node - the Animator component gets added to // it here, and bones' Position/Rotation are pushed relative to it every frame. public void LoadModel(MmdModel model, GameObject boneRootGameObject, GameObject[] bones, SkinnedMeshRenderer skinnedMeshRenderer, SkinnedMeshRenderer[] partSkinnedMeshRenderers, int[] morphBlendShapeIndex, int[][] partMorphBlendShapeIndex, Poser poser, IMmdPhysicsBackend physicsController, string modelPath = null) { Release(); _model = model; _boneRootGameObject = boneRootGameObject; _bones = bones; _bonePhysicsControlFlags = physicsController.TransformSkipMask; _bakedPoseController.Configure(_model, modelPath, PhysicsMode, _bonePhysicsControlFlags); _morphRenderer.Configure(_model, skinnedMeshRenderer, partSkinnedMeshRenderers, morphBlendShapeIndex, partMorphBlendShapeIndex); _poser = poser; _physicsController = physicsController; // Reuse an Animator supplied by another runtime integration when present. _animator = _boneRootGameObject.GetComponent(); if (_animator == null) { _animator = _boneRootGameObject.AddComponent(); } var boneTransforms = new Transform[_bones.Length]; _boneIndexes.Clear(); for (var i = 0; i < _bones.Length; ++i) { boneTransforms[i] = _bones[i].transform; _boneIndexes[boneTransforms[i]] = i; } _animationDriver.Build(_animator, boneTransforms, _bonePhysicsControlFlags); _staticPoseMode = false; if (_motion != null) { ResetMotionPlayer(); } _playTime = 0.0; PushLivePoseToAnimator(); _modelLoaded = true; } public void LoadMotion(string path) { LoadMotion(MmdMotionLoader.Load(path)); } public void LoadMotion(MmdMotionAsset motionAsset) { PrepareLoadMotion(motionAsset); var hasMotionData = BindMotion(motionAsset); if (hasMotionData) { var key = _bakedPoseController.BuildCacheKey(motionAsset.SourcePath, _enableVmdIk); if (!_bakedPoseController.TryReuse(key)) { var baked = MmdAnimationClipBaker.Bake(_model, _motion, _bakedPoseController.BoneMask, _enableVmdIk); _bakedPoseController.SetFresh(key, baked); } } FinishLoadMotion(); } // Coroutine equivalent of LoadMotion(): identical behavior, but the FK-baking step // (MmdAnimationClipBaker.BakeAsync) runs its IK/FK sampling on a background thread // instead of blocking the main thread for the whole motion length. Costs a few frames // of latency instead of one long stall - drive with `yield return LoadMotionAsync(path)` // from a MonoBehaviour coroutine. public IEnumerator LoadMotionAsync(string path) { var loadTask = MmdMotionLoader.LoadAsync(path); while (!loadTask.IsCompleted) yield return null; var bindCoroutine = LoadMotionAsync(loadTask.Result); while (bindCoroutine.MoveNext()) yield return bindCoroutine.Current; } public IEnumerator LoadMotionAsync(MmdMotionAsset motionAsset) { if (motionAsset == null) throw new ArgumentNullException(nameof(motionAsset)); if (_model == null) { throw new InvalidOperationException("model not loaded yet"); } // Prepare the replacement without touching the currently bound motion. This is // intentionally different from the synchronous path: callers can keep showing // and playing the old motion throughout a potentially long background bake. var key = _bakedPoseController.BuildCacheKey(motionAsset.SourcePath, _enableVmdIk); var baked = motionAsset.Motion.Length > 0 ? MmdBakedClipCache.TryGet(key) : null; if (motionAsset.Motion.Length > 0 && baked == null) { yield return MmdAnimationClipBaker.BakeAsync( _model, motionAsset.Motion, _bakedPoseController.BoneMask, pose => baked = pose, _enableVmdIk); } // The owner may have been destroyed while the background bake was running. // Never commit a completed bake into an animator whose Unity hierarchy has // already been released. if (!_modelLoaded || _boneRootGameObject == null) yield break; // Commit only after every expensive operation has completed. From this point the // switch is synchronous, so Update can never advance the new motion before its // bake is ready and then restart it in FinishLoadMotion. PrepareLoadMotion(motionAsset); var hasMotionData = BindMotion(motionAsset); if (hasMotionData && baked != null) { _bakedPoseController.SetFresh(key, baked); } else if (hasMotionData) { // Defensive fallback for an unexpected baker result. Keeping the same // behavior as the old path is preferable to failing the entire motion swap. if (!_bakedPoseController.TryReuse(key)) { var fallback = MmdAnimationClipBaker.Bake( _model, motionAsset.Motion, _bakedPoseController.BoneMask, _enableVmdIk); _bakedPoseController.SetFresh(key, fallback); } } FinishLoadMotion(); } private void PrepareLoadMotion(MmdMotionAsset motionAsset) { if (motionAsset == null) throw new ArgumentNullException(nameof(motionAsset)); if (_model == null) { throw new InvalidOperationException("model not loaded yet"); } ReleaseBonePoseFile(); MotionPath = motionAsset.SourcePath; _staticPoseMode = false; _bakedPoseController.Clear(); } private void FinishLoadMotion() { _playTime = 0.0; if (_bakedPoseController.Pose != null) { _bakedPoseController.Apply(_poser, _playTime); _physicsController.Reset(); _poser.PostPhysicsPosing(); } PushLivePoseToAnimator(); _morphRenderer.Update(_model, _motionPlayer, _playTime); } public void LoadPose(string path) { MotionPath = path; ReleaseBonePoseFile(); _staticPoseMode = true; _bakedPoseController.Clear(); var pose = VpdReader.Read(path); _poser.ResetPosing(); foreach (var entry in pose.BonePoses) { _poser.SetBonePose(entry.Key, entry.Value); } _playTime = 0.0; _poser.PrePhysicsPosing(); ApplyRuntimeRigIk(); _physicsController.Reset(); _poser.PostPhysicsPosing(); PushLivePoseToAnimator(); _morphRenderer.Reset(); } public void LoadBonePoseFile(string path) { BonePoseFilePath = path; if (_model == null) { Debug.LogWarning("model not loaded yet, skip LoadBonePoseFile"); return; } ReleaseBonePoseFile(); _bonePoseFileStorage = new BonePoseFileStorage(_model, path); } private void ReleaseBonePoseFile() { if (_bonePoseFileStorage == null) return; _bonePoseFileStorage.Release(); _bonePoseFileStorage = null; BonePoseFilePath = null; } public void ResetMotion() { if (_motionPlayer == null) { return; } _playTime = 0.0; _motionPlayer.SeekFrame(0); _poser.PrePhysicsPosing(); _bakedPoseController.Apply(_poser, _playTime); ApplyRuntimeRigIk(); _physicsController.Reset(); _poser.PostPhysicsPosing(); _morphRenderer.Update(_model, _motionPlayer, _playTime); PushLivePoseToAnimator(); } public void ResetPhysics() { if (_poser == null) return; if (!_staticPoseMode && _motionPlayer != null) _motionPlayer.SeekTime(_playTime); _poser.PrePhysicsPosing(_physicsController.CalculateMorphBeforePhysics); _bakedPoseController.Apply(_poser, _playTime); ApplyRuntimeRigIk(); _poser.PostPhysicsPosing(); var poses = BonePosePreCalculator.GetBonePoseImage(_poser); // Unity physics bones have been detached from the Animator hierarchy, so their // TransformStreamHandles cannot be resolved. Restore those Transforms directly // and keep them skipped by the animation job, exactly as during normal playback. if (_physicsController.TransformSkipMask != null) { var root = _boneRootGameObject.transform; for (var i = 0; i < poses.Length; ++i) { if (!_physicsController.TransformSkipMask[i]) continue; _bones[i].transform.SetPositionAndRotation( root.TransformPoint(poses[i].Position), root.rotation * poses[i].Rotation); } } _animationDriver.SetBonePoses(poses, _boneRootGameObject.transform, _physicsController.TransformSkipMask); _animationDriver.Evaluate(); UnityEngine.Physics.SyncTransforms(); // Reset after the target pose has reached both animated and detached physics bones. // Unity's backend resets Rigidbody state in-place, so doing this before restoring // those Transforms only cleared velocity at the old simulated pose. Backends that // own their simulation (Bullet/MagicaCloth) also see the final reset pose here. _physicsController.Reset(); } private void Update() { // MmdAnimator can remain enabled while MmdGameObject is loading or replacing its // model. Do not evaluate references owned by the previous/unfinished model. if (!_modelLoaded) { return; } var deltaTime = Time.deltaTime; if (Playing) _playTime += deltaTime; if (Playing && Loop && MotionLength > 0.0 && _playTime >= MotionLength) { ResetMotion(); Playing = true; return; } if (_bonePoseFileStorage != null) { if (!Playing) return; var poses = _bonePoseFileStorage.GetBonePose(_playTime); PushPoseToAnimator(poses); _morphRenderer.Update(_model, _motionPlayer, _playTime); } else { // _motionPlayer stays null until LoadMotion() is first called (LoadModel only // builds one when a motion was already loaded - see LoadModel's _motion != null // guard), so Playing can turn true before it exists. if (!_staticPoseMode && _motionPlayer != null) { _motionPlayer.SeekTime(_playTime); } // calculateMorph=true only for Bullet: its bone-morph values feed the physics // step below the same frame; other modes leave it to ApplyBakedPoseToPoser / // the live FK walk instead (pre-existing behavior, unrelated to baking). _poser.PrePhysicsPosing(_physicsController.CalculateMorphBeforePhysics); _bakedPoseController.Apply(_poser, _playTime); ApplyRuntimeRigIk(); // Motion pause freezes _playTime, but Bullet still needs explicit stepping so // hair and clothing can settle. Unity and MagicaCloth already simulate outside // this call; their Step implementations are intentionally no-ops. _physicsController.Step(deltaTime, PhysicsFps, MaxBulletSubSteps); _poser.PostPhysicsPosing(); PushLivePoseToAnimator(); _morphRenderer.Update(_model, _motionPlayer, _playTime); } } private void OnDestroy() { Release(); } public void Release() { _modelLoaded = false; _animationDriver.Dispose(); if (_ikInputs.IsCreated) _ikInputs.Dispose(); if (_ikOutputs.IsCreated) _ikOutputs.Dispose(); _activeIkBindings.Clear(); _bakedPoseController.Clear(); if (_bonePoseFileStorage != null) { _bonePoseFileStorage.Release(); _bonePoseFileStorage = null; } _physicsController.Dispose(); _physicsController = new NoneMmdPhysicsBackend(); } private bool BindMotion(MmdMotionAsset motionAsset) { _motion = motionAsset.Motion; if (_motion.Length == 0) { _poser.ResetPosing(); ResetMotionPlayer(); return false; } ResetMotionPlayer(); return true; } private void ApplyRuntimeRigIk() { if (_poser == null || _boneRootGameObject == null) return; var owner = _boneRootGameObject.transform.parent != null ? _boneRootGameObject.transform.parent.GetComponent() : null; var rig = owner != null ? owner.RuntimeRig : null; if (rig == null || rig.Weight <= 0f) return; var modelRoot = _boneRootGameObject.transform; EnsureIkCapacity(rig.Constraints.Count); _activeIkBindings.Clear(); foreach (var constraint in rig.Constraints.Values) { if (constraint == null || constraint.Weight <= 0f || constraint.Root == null || constraint.Mid == null || constraint.Tip == null || constraint.Target == null) continue; if (!_boneIndexes.TryGetValue(constraint.Root, out var rootIndex) || !_boneIndexes.TryGetValue(constraint.Mid, out var midIndex) || !_boneIndexes.TryGetValue(constraint.Tip, out var tipIndex)) continue; Vector3? hintPosition = constraint.Hint != null ? modelRoot.InverseTransformPoint(constraint.Hint.position) : (Vector3?)null; var jobIndex = _activeIkBindings.Count; _ikInputs[jobIndex] = _poser.CreateExternalIkInput(rootIndex, midIndex, tipIndex, modelRoot.InverseTransformPoint(constraint.Target.position), Quaternion.Inverse(modelRoot.rotation) * constraint.Target.rotation, hintPosition, constraint.Weight * rig.Weight, constraint.PositionWeight, constraint.RotationWeight, constraint.HintWeight); _activeIkBindings.Add(new RuntimeIkBinding { Root = rootIndex, Mid = midIndex, Tip = tipIndex }); } var count = _activeIkBindings.Count; if (count == 0) return; new MmdPrePhysicsIkJob { Inputs = _ikInputs, Outputs = _ikOutputs } .Schedule(count, 1).Complete(); for (var i = 0; i < count; ++i) { var binding = _activeIkBindings[i]; _poser.ApplyExternalIkOutput(binding.Root, binding.Mid, binding.Tip, _ikOutputs[i]); } _poser.RefreshExternalIkAppendTransforms(); } private void EnsureIkCapacity(int count) { if (_ikInputs.IsCreated && _ikInputs.Length >= count) return; if (_ikInputs.IsCreated) _ikInputs.Dispose(); if (_ikOutputs.IsCreated) _ikOutputs.Dispose(); var capacity = Mathf.Max(4, count); _ikInputs = new NativeArray(capacity, Allocator.Persistent); _ikOutputs = new NativeArray(capacity, Allocator.Persistent); } private void ResetMotionPlayer() { _motionPlayer = new MotionPlayer(_motion, _poser); _motionPlayer.SeekFrame(0); _poser.PrePhysicsPosing(); ApplyRuntimeRigIk(); _physicsController.Reset(); _poser.PostPhysicsPosing(); } private bool CanNotUpdateBone() { return _boneRootGameObject == null || _bones == null || _poser == null || _model == null || _poser.BoneImages.Length != _bones.Length || _model.Bones.Length != _bones.Length; } // Computes the current Poser pose and pushes it through the Animator/PlayableGraph. private void PushLivePoseToAnimator() { if (CanNotUpdateBone()) { Debug.LogError("illegal argument for PushLivePoseToAnimator"); return; } PushPoseToAnimator(BonePosePreCalculator.GetBonePoseImage(_poser)); } // Pushes an already-computed pose (live, or file storage - normalized to // BonePoseImage[]) through the Animator/PlayableGraph and evaluates synchronously so // callers observe the result immediately. skipMask leaves a bone's Transform untouched // (PhysicsMode.Unity's Rigidbody/ConfigurableJoint-controlled bones). PhysicsMode.Bullet // needs no skip mask - baked bones' Poser.BoneImages were already overwritten by // ApplyBakedPoseToPoser before this pose was computed, so pushing every bone is correct. private void PushPoseToAnimator(BonePoseImage[] poses) { _animationDriver.SetBonePoses(poses, _boneRootGameObject.transform, _physicsController.TransformSkipMask); _animationDriver.Evaluate(); } } }