using System; using System.Collections.Generic; using System.Text; using LibMMD.Model; using UnityEngine; namespace LibMMD.Unity3D.Animation { /// Handles generated by the MMD Animation Rigging setup. public sealed class MmdRuntimeRig { internal MmdRuntimeRig(Transform rigRoot, Transform controlsRoot, IDictionary targets, IDictionary constraints) { RigRoot = rigRoot; ControlsRoot = controlsRoot; Targets = targets; Constraints = constraints; } public Transform RigRoot { get; } public float Weight { get; set; } = 1f; public Transform ControlsRoot { get; } public IDictionary Targets { get; } public IDictionary Constraints { get; } public bool SetWeight(string controlName, float weight) { if (!Constraints.TryGetValue(controlName, out var constraint)) return false; constraint.Weight = Mathf.Clamp01(weight); return true; } } /// Builds two-bone IK controls from standard MMD bone names at runtime. public static class MmdRuntimeRigBuilder { public const string LeftLeg = "LeftLeg"; public const string RightLeg = "RightLeg"; public const string LeftArm = "LeftArm"; public const string RightArm = "RightArm"; public static MmdRuntimeRig Build(GameObject animatorRoot, MmdModel model, GameObject[] bones) { if (animatorRoot == null) throw new ArgumentNullException(nameof(animatorRoot)); if (model == null) throw new ArgumentNullException(nameof(model)); if (bones == null || bones.Length != model.Bones.Length) throw new ArgumentException("Bone objects must match the MMD model.", nameof(bones)); var rigObject = new GameObject("MmdIkConstraints"); rigObject.transform.SetParent(animatorRoot.transform, false); // Controls must live outside the Animator hierarchy. If they are children of the // Animator, its animation stream restores them to bind/default values and scene or // script movement appears to have no effect on the IK job. var controls = new GameObject("MmdRigControls"); var controlsParent = animatorRoot.transform.parent; controls.transform.SetParent(controlsParent, false); var lookup = BuildLookup(model, bones); var targets = new Dictionary(); var constraints = new Dictionary(); BuildChain(LeftLeg, rigObject.transform, controls.transform, lookup, // Solve the physical/source chain. Models with *D deform bones already copy // this chain through append transforms; solving *D and then copying its local // rotations back to differently-parented source bones is not equivalent. new[] { "左足", "leftleg", "左足D", "leftlegD" }, new[] { "左ひざ", "左膝", "leftknee", "左ひざD", "左膝D", "leftkneeD" }, new[] { "左足首", "leftankle", "左足首D", "leftankleD" }, new[] { "左足ik", "leftlegik" }, targets, constraints); BuildChain(RightLeg, rigObject.transform, controls.transform, lookup, new[] { "右足", "rightleg", "右足D", "rightlegD" }, new[] { "右ひざ", "右膝", "rightknee", "右ひざD", "右膝D", "rightkneeD" }, new[] { "右足首", "rightankle", "右足首D", "rightankleD" }, new[] { "右足ik", "rightlegik" }, targets, constraints); BuildChain(LeftArm, rigObject.transform, controls.transform, lookup, new[] { "左腕", "leftarm" }, new[] { "左ひじ", "左肘", "leftelbow" }, new[] { "左手首", "leftwrist" }, null, targets, constraints); BuildChain(RightArm, rigObject.transform, controls.transform, lookup, new[] { "右腕", "rightarm" }, new[] { "右ひじ", "右肘", "rightelbow" }, new[] { "右手首", "rightwrist" }, null, targets, constraints); return new MmdRuntimeRig(rigObject.transform, controls.transform, targets, constraints); } private static Dictionary BuildLookup(MmdModel model, GameObject[] bones) { var result = new Dictionary(); for (var i = 0; i < bones.Length; i++) { AddName(result, model.Bones[i].Name, bones[i].transform); AddName(result, model.Bones[i].NameEn, bones[i].transform); } return result; } private static void AddName(IDictionary lookup, string name, Transform bone) { var key = Normalize(name); if (key.Length > 0 && !lookup.ContainsKey(key)) lookup.Add(key, bone); } private static Transform Find(IDictionary lookup, string[] names) { if (names == null) return null; foreach (var name in names) if (lookup.TryGetValue(Normalize(name), out var bone)) return bone; return null; } private static string Normalize(string name) { if (string.IsNullOrWhiteSpace(name)) return string.Empty; return name.Normalize(NormalizationForm.FormKC) .Replace(" ", string.Empty).Replace("_", string.Empty).ToLowerInvariant(); } private static void BuildChain(string controlName, Transform rigRoot, Transform controlsRoot, IDictionary lookup, string[] rootNames, string[] midNames, string[] tipNames, string[] targetNames, IDictionary targets, IDictionary constraints) { var root = Find(lookup, rootNames); var mid = Find(lookup, midNames); var tip = Find(lookup, tipNames); if (root == null || mid == null || tip == null) return; // Never use an animated MMD bone directly as a control: MmdAnimationDriver writes // it every frame, which makes Inspector/script movement appear to do nothing. // An IK bone is still useful as the initial target pose when the model provides one. var targetSource = Find(lookup, targetNames) ?? tip; var target = CreateControl(controlName + "Target", controlsRoot, targetSource.position, targetSource.rotation); var chainDirection = tip.position - root.position; var bendDirection = mid.position - Vector3.Lerp(root.position, tip.position, 0.5f); if (bendDirection.sqrMagnitude < 0.000001f) bendDirection = Vector3.forward * Mathf.Max(chainDirection.magnitude * 0.25f, 0.05f); var hint = CreateControl(controlName + "Hint", controlsRoot, mid.position + bendDirection.normalized * Mathf.Max(chainDirection.magnitude * 0.5f, 0.1f), Quaternion.identity); var constraintObject = new GameObject(controlName + "IK"); constraintObject.transform.SetParent(rigRoot, false); var constraint = constraintObject.AddComponent(); constraint.Root = root; constraint.Mid = mid; constraint.Tip = tip; constraint.Target = target; constraint.Hint = hint; if (!tip.IsChildOf(mid) || !mid.IsChildOf(root)) { Debug.LogError($"[MMD Rig] Invalid {controlName} chain: " + $"root='{root.name}', mid='{mid.name}', tip='{tip.name}', " + $"midUnderRoot={mid.IsChildOf(root)}, tipUnderMid={tip.IsChildOf(mid)}"); UnityEngine.Object.Destroy(constraintObject); return; } // VMD owns the limb until the caller explicitly enables this control. constraint.Weight = 0f; targets[controlName] = target; constraints[controlName] = constraint; } private static Transform CreateControl(string name, Transform parent, Vector3 position, Quaternion rotation) { var control = new GameObject(name).transform; control.SetParent(parent, false); control.SetPositionAndRotation(position, rotation); return control; } } }