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;
}
}
}