using System; using LibMMD.Model; using LibMMD.Motion; using UnityEngine; namespace LibMMD.Unity3D.Animation { internal sealed class MmdBakedPoseController { private MmdModel _model; private string _modelPath; private MmdPhysicsMode PhysicsMode; private bool[] _bonePhysicsControlFlags; private MmdAnimationClipBaker.BakedPose _bakedPose; private bool[] _bakedBoneMask; internal MmdAnimationClipBaker.BakedPose Pose => _bakedPose; internal bool[] BoneMask => _bakedBoneMask; internal void Configure(MmdModel model, string modelPath, MmdPhysicsMode physicsMode, bool[] bonePhysicsControlFlags) { _model = model ?? throw new ArgumentNullException(nameof(model)); _modelPath = modelPath; PhysicsMode = physicsMode; _bonePhysicsControlFlags = bonePhysicsControlFlags; _bakedBoneMask = ComputeBakeableBoneMask(); _bakedPose = null; } internal void Clear() { _bakedPose = null; } // Null if the model/motion path is unavailable (eg. no modelPath was passed to // LoadModel) - reuse is skipped in that case since there's nothing stable to key on. // PhysicsMode is folded into the key because ComputeBakeableBoneMask's exclusion set // differs per mode - reusing a bake from a mode with a narrower bakeable set would // silently under-bake after a mode switch (safe, since ApplyBakedPoseToPoser re-checks // the current mask before touching a bone, but it'd quietly lose the perf benefit for // the "toggle PhysicsMode and Load again" workflow the cache exists for). internal string BuildCacheKey(string motionPath, bool enableIk) { var baseKey = MmdBakedClipCache.BuildKey(_modelPath, motionPath); return baseKey == null ? null : $"{baseKey}|{PhysicsMode}|IK:{enableIk}|Bake:{MmdAnimationClipBaker.BakeAlgorithmVersion}"; } // Reuses the previous bake when the model+VMD combination (path and last-write time of // both) matches - see MmdBakedClipCache for why this is safe across a fresh bone // hierarchy. Returns false (nothing assigned) on a cache miss so the caller bakes. internal bool TryReuse(string key) { var pose = MmdBakedClipCache.TryGet(key); if (pose == null) { return false; } _bakedPose = pose; return true; } // Stores a freshly baked pose as the new cache entry (a no-op if key is null - eg. no // modelPath was available). internal void SetFresh(string key, MmdAnimationClipBaker.BakedPose pose) { _bakedPose = pose; if (key == null) { return; } MmdBakedClipCache.Store(key, pose); } // model.Bones.Length long; true for bones this component bakes (kinematic FK, no // live-physics involvement) - see MmdAnimationClipBaker. The excluded set is whatever // PhysicsMode drives live every frame instead: // - Bullet: _postPhysicsBones-equivalent bones (bone.PostPhysics) and bones driven by // a RigidTypePhysics/RigidTypePhysicsStrict rigid body - PoserMotionState overwrites // these live every physics step regardless of what this component bakes. // - Unity: bones a real UnityEngine Rigidbody/ConfigurableJoint controls // (_bonePhysicsControlFlags) - genuinely live PhysX simulation, can't be pre-baked. // - None/MagicaCloth: nothing excluded. None has no physics at all; MagicaCloth // simulates entirely outside this component in LateUpdate() and overwrites whatever // this component wrote regardless, so baking every bone is safe. private bool[] ComputeBakeableBoneMask() { var boneCount = _model.Bones.Length; var mask = new bool[boneCount]; for (var i = 0; i < boneCount; ++i) { mask[i] = true; } if (PhysicsMode == MmdPhysicsMode.Bullet) { for (var i = 0; i < boneCount; ++i) { mask[i] = !_model.Bones[i].PostPhysics; } foreach (var rigidBody in _model.Rigidbodies) { if (rigidBody.Type != MmdRigidBody.RigidBodyType.RigidTypePhysics && rigidBody.Type != MmdRigidBody.RigidBodyType.RigidTypePhysicsStrict) { continue; } var boneIndex = rigidBody.AssociatedBoneIndex; if (boneIndex >= 0 && boneIndex < boneCount) { mask[boneIndex] = false; } } } else if (PhysicsMode == MmdPhysicsMode.Unity && _bonePhysicsControlFlags != null) { for (var i = 0; i < boneCount; ++i) { mask[i] = !_bonePhysicsControlFlags[i]; } } return mask; } // Interpolates the baked kinematic-FK pose (see MmdAnimationClipBaker) at time and // writes it into Poser.BoneImages for baked bones, so the upcoming physics step and // PostPhysicsPosing see the baked pose (mirrors PoserMotionState.Fix()'s // SkinningMatrix/LocalMatrix relationship). No-op if nothing was baked. internal void Apply(Poser poser, double time) { if (_bakedPose == null) { return; } for (var i = 0; i < _bakedBoneMask.Length; ++i) { if (!_bakedBoneMask[i]) { continue; } if (!_bakedPose.TrySample(i, time, out var position, out var rotation)) { continue; } var image = poser.BoneImages[i]; image.SkinningMatrix = Matrix4x4.TRS(position, rotation, Vector3.one) * Matrix4x4.Translate(-_model.Bones[i].Position); image.LocalMatrix = image.SkinningMatrix * image.GlobalOffsetMatrixInv; } } } }