using System; using System.Collections.Generic; using System.Linq; using LibMMD.Model; using LibMMD.Util; using UnityEngine; using Tools = LibMMD.Util.Tools; namespace LibMMD.Motion { public class Poser { public Poser(MmdModel model) { Model = model; /***** Create Pose Image *****/ var vertexNum = model.Vertices.Length; //VertexImages = new Vector3[vertexNum]; var boneNum = model.Bones.Length; BoneImages = new BoneImage[boneNum]; for (var i = 0; i < boneNum; ++i) { BoneImages[i] = new BoneImage(); } for (var i = 0; i < boneNum; ++i) { var bone = model.Bones[i]; _boneNameMap[bone.Name] = i; var image = BoneImages[i]; image.GlobalOffsetMatrix.m03 = -bone.Position[0]; image.GlobalOffsetMatrix.m13 = -bone.Position[1]; image.GlobalOffsetMatrix.m23 = -bone.Position[2]; image.GlobalOffsetMatrixInv.m03 = bone.Position[0]; image.GlobalOffsetMatrixInv.m13 = bone.Position[1]; image.GlobalOffsetMatrixInv.m23 = bone.Position[2]; image.Parent = bone.ParentIndex; if (image.Parent < boneNum && image.Parent >= 0) { image.HasParent = true; image.LocalOffset = bone.Position - model.Bones[image.Parent].Position; } else { image.HasParent = false; image.LocalOffset = bone.Position; } image.AppendRotate = bone.AppendRotate; image.AppendTranslate = bone.AppendTranslate; image.HasAppend = false; if (image.AppendRotate || image.AppendTranslate) { image.AppendParent = bone.AppendBoneVal.Index; if (image.AppendParent < boneNum) { image.HasAppend = true; image.AppendRatio = bone.AppendBoneVal.Ratio; } } image.HasIk = bone.HasIk; if (image.HasIk) { (bone.PostPhysics ? _postPhysicsIkBones : _prePhysicsIkBones).Add(i); var ikLinkNum = bone.IkInfoVal.IkLinks.Length; image.IkLinks = new int[ikLinkNum]; image.IkFixTypes = new BoneImage.AxisFixType[ikLinkNum]; image.IkTransformOrders = Enumerable.Repeat(BoneImage.AxisTransformOrder.OrderYzx, ikLinkNum) .ToArray(); image.IkLinkLimited = new bool [ikLinkNum]; image.IkLinkLimitsMin = new Vector3[ikLinkNum]; image.IkLinkLimitsMax = new Vector3[ikLinkNum]; for (var j = 0; j < ikLinkNum; ++j) { var ikLink = bone.IkInfoVal.IkLinks[j]; image.IkLinks[j] = ikLink.LinkIndex; image.IkLinkLimited[j] = ikLink.HasLimit; if (image.IkLinkLimited[j]) { for (var k = 0; k < 3; ++k) { var lo = ikLink.LoLimit[k]; var hi = ikLink.HiLimit[k]; image.IkLinkLimitsMin[j][k] = Math.Min(lo, hi); image.IkLinkLimitsMax[j][k] = Math.Max(lo, hi); } if (image.IkLinkLimitsMin[j].x > -Math.PI * 0.5 && image.IkLinkLimitsMax[j].x < Math.PI * 0.5) { image.IkTransformOrders[j] = BoneImage.AxisTransformOrder.OrderZxy; } else if (image.IkLinkLimitsMin[j].y > -Math.PI * 0.5 && image.IkLinkLimitsMax[j].y < Math.PI * 0.5) { image.IkTransformOrders[j] = BoneImage.AxisTransformOrder.OrderXyz; } if (Math.Abs(image.IkLinkLimitsMin[j].x) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].x) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMin[j].y) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].y) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMin[j].z) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].z) < Tools.MmdMathConstEps) { image.IkFixTypes[j] = BoneImage.AxisFixType.FixAll; } else if (Math.Abs(image.IkLinkLimitsMin[j].y) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].y) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMin[j].z) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].z) < Tools.MmdMathConstEps) { image.IkFixTypes[j] = BoneImage.AxisFixType.FixX; } else if (Math.Abs(image.IkLinkLimitsMin[j].x) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].x) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMin[j].z) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].z) < Tools.MmdMathConstEps) { image.IkFixTypes[j] = BoneImage.AxisFixType.FixY; } else if (Math.Abs(image.IkLinkLimitsMin[j].x) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].x) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMin[j].y) < Tools.MmdMathConstEps && Math.Abs(image.IkLinkLimitsMax[j].y) < Tools.MmdMathConstEps) { image.IkFixTypes[j] = BoneImage.AxisFixType.FixZ; } } BoneImages[image.IkLinks[j]].IkLink = true; } image.CcdAngleLimit = bone.IkInfoVal.CcdAngleLimit; image.CcdIterateLimit = Math.Min(bone.IkInfoVal.CcdIterateLimit, 256); image.IkTarget = bone.IkInfoVal.IkTargetIndex; } if (bone.PostPhysics) { _postPhysicsBones.Add(i); } else { _prePhysicsBones.Add(i); } } BoneImage.TransformOrder order = new BoneImage.TransformOrder(model); _prePhysicsBones.Sort(order); _postPhysicsBones.Sort(order); var materialNum = model.Parts.Length; MaterialMulImages = new MaterialImage[materialNum]; MaterialAddImages = new MaterialImage[materialNum]; for (var i = 0; i < materialNum; ++i) { MaterialMulImages[i] = new MaterialImage(1.0f); MaterialAddImages[i] = new MaterialImage(0.0f); } var morphNum = model.Morphs.Length; MorphRates = new float[morphNum]; for (var i = 0; i < morphNum; ++i) { var morph = model.Morphs[i]; _morphNameMap[morph.Name] = i; } _ikSolver = new MmdIkSolver(Model, BoneImages, UpdateLocalMatrix, UpdateBoneSelfTransform); ConfigureDefaultFootIkPoles(); ResetPosing(); //Debug.LogFormat("morphIndex count = {0}, vertex count = {1}", morphIndex.Count, model.Vertices.Length); //Debug.LogFormat("morphIndex count = {0}, vertex count = {1}", morphIndex.Count, model.Vertices.Length); } private static List GetMorphIndexes(MmdModel model) { var morphs = model.Morphs; var indexSet = new HashSet(); foreach (var morph in morphs) { if (morph.Type != Morph.MorphType.MorphTypeVertex) { continue; } foreach (var morphData in morph.MorphDatas) { var data = (Morph.VertexMorph) morphData; indexSet.Add(data.VertexIndex); } } var ret = new List(indexSet); ret.Sort(); return ret; } public void SetBonePose(int index, BonePose bonePose) { BoneImages[index].Translation = bonePose.Translation; BoneImages[index].Rotation = bonePose.Rotation; } public void SetBonePose(string name, BonePose bonePose) { int index; if (_boneNameMap.TryGetValue(name, out index)) { SetBonePose(index, bonePose); } } public void SetMorphPose(int index, MorphPose morphPose) { MorphRates[index] = morphPose.Weight; } public void SetMorphPose(string name, MorphPose morphPose) { int index; if (_morphNameMap.TryGetValue(name, out index)) { SetMorphPose(index, morphPose); } } private void UpdateBoneAppendTransform(int index) { var image = BoneImages[index]; if (!image.HasAppend) return; if (image.AppendRotate) { image.TotalRotation = image.TotalRotation * Quaternion.SlerpUnclamped(Quaternion.identity, BoneImages[image.AppendParent].TotalRotation, image.AppendRatio); } if (image.AppendTranslate) { image.TotalTranslation = image.TotalTranslation + image.AppendRatio * BoneImages[image.AppendParent].TotalTranslation; } UpdateLocalMatrix(image); } private void UpdateLocalMatrix(BoneImage image) { image.LocalMatrix = MathUtil.QuaternionToMatrix4X4(image.TotalRotation); MathUtil.SetTransToMatrix4X4(image.TotalTranslation + image.LocalOffset, ref image.LocalMatrix); if (image.HasParent) { image.LocalMatrix = BoneImages[image.Parent].LocalMatrix * image.LocalMatrix; } } private void UpdateBoneSelfTransform(int index) { var image = BoneImages[index]; image.TotalRotation = image.MorphRotation * image.Rotation; image.TotalTranslation = image.MorphTranslation + image.Translation; if (image.IkLink) { image.PreIkRotation = image.TotalRotation; image.TotalRotation = image.IkRotation * image.TotalRotation; } UpdateLocalMatrix(image); } public void ResetPosing() { for (var i = 0; i < MorphRates.Length; ++i) { MorphRates[i] = 0.0f; } foreach (var boneImage in BoneImages) { boneImage.Rotation = new Quaternion(0.0f, 0.0f, 0.0f, 1.0f); boneImage.Translation = Vector4.zero; } PrePhysicsPosing(); PostPhysicsPosing(); } public void PrePhysicsPosing(bool calculateMorph = true) { foreach (var boneImage in BoneImages) { boneImage.MorphTranslation = Vector3.zero; boneImage.MorphRotation = Quaternion.identity; boneImage.LocalMatrix = Matrix4x4.identity; boneImage.PreIkRotation = Quaternion.identity; boneImage.IkRotation = Quaternion.identity; boneImage.TotalRotation = Quaternion.identity; boneImage.TotalTranslation = Vector3.zero; } foreach (var materialMulImage in MaterialMulImages) { materialMulImage.Init(1.0f); } foreach (var materialAddImage in MaterialAddImages) { materialAddImage.Init(0.0f); } if (calculateMorph) { for (var i = 0; i < MorphRates.Length; ++i) { UpdateMorphTransform(i, MorphRates[i]); } } UpdateBoneTransform(_prePhysicsBones, _prePhysicsIkBones); UpdateBoneSkinningMatrix(_prePhysicsBones); } private void UpdateBoneSkinningMatrix(List indexList) { foreach (var index in indexList) { var image = BoneImages[index]; image.SkinningMatrix = image.LocalMatrix * image.GlobalOffsetMatrix; } } private void UpdateBoneTransform(List indexList, List ikBones) { foreach (var index in indexList) { UpdateBoneSelfTransform(index); } foreach (var index in indexList) { UpdateBoneAppendTransform(index); } foreach (var index in indexList) { UpdateLocalMatrix(BoneImages[index]); } if (EnableIk) { foreach (var index in ikBones) _ikSolver.Solve(index); // IK updates its links and target directly, but append/deform bones were // evaluated before IK. A matrix-only propagation leaves *D chains holding // their pre-IK append rotations, so models skinned to those bones appear to // ignore IK. Re-evaluate append bones from their authored pose and updated // source, while preserving the solved transforms of ordinary IK links. ReevaluateAppendAndPropagate(indexList); } } private void ReevaluateAppendAndPropagate(List indexList) { foreach (var index in indexList) { var image = BoneImages[index]; if (image.HasAppend) { UpdateBoneSelfTransform(index); UpdateBoneAppendTransform(index); } else { UpdateLocalMatrix(image); } } } private void UpdateMorphTransform(int index, float rate) { if (rate < Tools.MmdMathConstEps) { return; } var morph = Model.Morphs[index]; switch (morph.Type) { case Morph.MorphType.MorphTypeGroup: foreach (var morphData in morph.MorphDatas) { var data = (Morph.GroupMorph) morphData; UpdateMorphTransform(data.MorphIndex, data.MorphRate * rate); } break; case Morph.MorphType.MorphTypeVertex: //顶点Morph分开计算 break; case Morph.MorphType.MorphTypeBone: foreach (var morphData in morph.MorphDatas) { var data = (Morph.BoneMorph) morphData; var boneImage = BoneImages[data.BoneIndex]; boneImage.MorphTranslation = boneImage.MorphTranslation + data.Translation * rate; boneImage.MorphRotation = boneImage.MorphRotation * Quaternion.Slerp(Quaternion.identity, data.Rotation, rate); } break; case Morph.MorphType.MorphTypeUv: break; case Morph.MorphType.MorphTypeExtUv1: break; case Morph.MorphType.MorphTypeExtUv2: break; case Morph.MorphType.MorphTypeExtUv3: break; case Morph.MorphType.MorphTypeExtUv4: break; case Morph.MorphType.MorphTypeMaterial: break; default: throw new ArgumentOutOfRangeException(); } } public void PostPhysicsPosing() { UpdateBoneTransform(_postPhysicsBones, _postPhysicsIkBones); UpdateBoneSkinningMatrix(_postPhysicsBones); } //public Vector3[] VertexImages { get; set; } public BoneImage[] BoneImages { get; set; } public MaterialImage[] MaterialMulImages { get; set; } public MaterialImage[] MaterialAddImages { get; set; } public float[] MorphRates { get; set; } public MmdModel Model { get; set; } public bool EnableIk { get; set; } = true; // Keeps the public posing API stable while the solver owns IK-specific state. public void SetIkPoleVector(int ikBoneIndex, Vector3 poleDirection) { _ikSolver.SetPoleVector(ikBoneIndex, poleDirection); } private void ConfigureDefaultFootIkPoles() { for (var i = 0; i < BoneImages.Length; ++i) { var image = BoneImages[i]; if (!image.HasIk) continue; var thighIndex = -1; var kneeIndex = -1; for (var j = 0; j < image.IkFixTypes.Length; ++j) { if (image.IkFixTypes[j] == BoneImage.AxisFixType.FixX && kneeIndex < 0) kneeIndex = image.IkLinks[j]; if (image.IkFixTypes[j] == BoneImage.AxisFixType.FixNone) thighIndex = image.IkLinks[j]; } if (thighIndex < 0 || kneeIndex < 0 || image.IkTarget < 0 || image.IkTarget >= Model.Bones.Length) continue; // Models do not agree on whether their natural knee/foot-forward direction // is +Z or -Z. Derive the pole from the authored bind chain instead of forcing // every PMX leg toward the same global Vector3.back direction. var hip = Model.Bones[thighIndex].Position; var kneeOffset = Model.Bones[kneeIndex].Position - hip; var legAxis = Model.Bones[image.IkTarget].Position - hip; var pole = kneeOffset; if (legAxis.sqrMagnitude > 1e-10f) pole -= Vector3.Dot(pole, legAxis) / legAxis.sqrMagnitude * legAxis; var scale = kneeOffset.sqrMagnitude + legAxis.sqrMagnitude; if (pole.sqrMagnitude < Mathf.Max(scale * 1e-8f, 1e-10f)) pole = Vector3.back; else if (Mathf.Abs(pole.z) > Mathf.Sqrt(Mathf.Max(scale, 1e-10f)) * 1e-4f) { // Small bind-pose X offsets are common modeling asymmetry, not an // instruction to point the knee inward or outward. MMD legs conventionally // bend along local Z; keep only that sign and project it against the live // leg axis later in the solver. pole = pole.z < 0f ? Vector3.back : Vector3.forward; } _ikSolver.SetPoleVector(i, pole.normalized); } } public void SetIkEnabled(int boneIndex, bool enabled) { if (boneIndex >= 0 && boneIndex < BoneImages.Length && BoneImages[boneIndex].HasIk) BoneImages[boneIndex].IkEnabled = enabled; } public void ResetIkEnabled() { foreach (var image in BoneImages) if (image.HasIk) image.IkEnabled = true; } public MmdPrePhysicsIkInput CreateExternalIkInput(int rootIndex, int midIndex, int tipIndex, Vector3 targetPosition, Quaternion targetRotation, Vector3? hintPosition, float weight, float positionWeight, float rotationWeight, float hintWeight) { return new MmdPrePhysicsIkInput { RootPosition = GetPosition(rootIndex), MidPosition = GetPosition(midIndex), TipPosition = GetPosition(tipIndex), RootRotation = GetRotation(rootIndex), MidRotation = GetRotation(midIndex), TipRotation = GetRotation(tipIndex), TargetPosition = targetPosition, TargetRotation = targetRotation, HintPosition = hintPosition ?? Vector3.zero, HasHint = hintPosition.HasValue, Weight = weight, PositionWeight = positionWeight, RotationWeight = rotationWeight, HintWeight = hintWeight }; } public void ApplyExternalIkOutput(int rootIndex, int midIndex, int tipIndex, MmdPrePhysicsIkOutput output) { SetGlobalRotation(rootIndex, output.RootRotation); RebuildPrePhysicsMatrices(); SetGlobalRotation(midIndex, output.MidRotation); RebuildPrePhysicsMatrices(); SetGlobalRotation(tipIndex, output.TipRotation); RebuildPrePhysicsMatrices(); } /// /// Re-evaluates append/deform bones after an external IK control changed its source /// chain. Rebuilding matrices alone leaves *D bones with the append rotation captured /// before IK, so meshes weighted to those bones appear not to follow the control. /// public void RefreshExternalIkAppendTransforms() { ReevaluateAppendAndPropagate(_prePhysicsBones); UpdateBoneSkinningMatrix(_prePhysicsBones); } private Vector3 GetPosition(int index) => MathUtil.GetTransFromMatrix4X4(BoneImages[index].LocalMatrix); private Quaternion GetRotation(int index) => BoneImages[index].LocalMatrix.rotation; private void SetGlobalRotation(int index, Quaternion rotation) { var image = BoneImages[index]; var parentRotation = image.HasParent ? BoneImages[image.Parent].LocalMatrix.rotation : Quaternion.identity; image.TotalRotation = Quaternion.Inverse(parentRotation) * rotation; } private void RebuildPrePhysicsMatrices() { foreach (var index in _prePhysicsBones) UpdateLocalMatrix(BoneImages[index]); UpdateBoneSkinningMatrix(_prePhysicsBones); } private readonly Dictionary _boneNameMap = new Dictionary(); private readonly Dictionary _morphNameMap = new Dictionary(); private readonly MmdIkSolver _ikSolver; private readonly List _prePhysicsBones = new List(); private readonly List _postPhysicsBones = new List(); private readonly List _prePhysicsIkBones = new List(); private readonly List _postPhysicsIkBones = new List(); } }