using System.Collections.Generic; using System.Text; using LibMMD.Model; using UnityEngine; namespace LibMMD.Motion { /// Retargets standard VMD translations to the target model's measured leg length. internal sealed class MmdMotionTranslationScaler { // Median leg length of the normal-proportion reference models listed in // docs/MOTION_IK_SCALE_MODELS.txt. VMD does not contain its authoring skeleton. internal const float ReferenceLegLength = 9.7337f; private readonly Dictionary _scales = new Dictionary(); internal MmdMotionTranslationScaler(MmdModel model) { BuildScales(model); } internal BonePose Apply(int boneIndex, BonePose pose) { if (_scales.TryGetValue(boneIndex, out var scale)) pose.Translation *= scale; return pose; } private void BuildScales(MmdModel model) { var sideScales = new float[2]; for (var i = 0; i < model.Bones.Length; ++i) { var bone = model.Bones[i]; if (!bone.HasIk || bone.IkInfoVal == null || bone.IkInfoVal.IkLinks == null || bone.IkInfoVal.IkLinks.Length < 2) continue; var side = GetSide(bone.Name, bone.NameEn); var knee = bone.IkInfoVal.IkLinks[0].LinkIndex; var thigh = bone.IkInfoVal.IkLinks[1].LinkIndex; var ankle = bone.IkInfoVal.IkTargetIndex; if (side < 0 || !IsValidBone(model, knee) || !IsValidBone(model, thigh) || !IsValidBone(model, ankle) || !IsLegChain(model, thigh, knee, ankle)) continue; var length = Vector3.Distance(model.Bones[thigh].Position, model.Bones[knee].Position) + Vector3.Distance(model.Bones[knee].Position, model.Bones[ankle].Position); if (length > 1e-5f) sideScales[side] = length / ReferenceLegLength; } for (var i = 0; i < model.Bones.Length; ++i) { var bone = model.Bones[i]; var side = GetSide(bone.Name, bone.NameEn); if (side >= 0 && sideScales[side] > 0f && IsFootOrToeIk(bone)) _scales[i] = sideScales[side]; } var rootScale = 0f; var validSideCount = 0; foreach (var scale in sideScales) { if (scale <= 0f) continue; rootScale += scale; validSideCount++; } if (validSideCount == 0) return; rootScale /= validSideCount; for (var i = 0; i < model.Bones.Length; ++i) if (IsMotionRoot(model.Bones[i])) _scales[i] = rootScale; } private static bool IsMotionRoot(Bone bone) { var name = NormalizeBoneName(bone.Name); var nameEn = NormalizeBoneName(bone.NameEn); return name == "全ての親" || name == "センター" || name == "グルーブ" || nameEn == "master" || nameEn == "root" || nameEn == "center" || nameEn == "groove"; } private static bool IsFootOrToeIk(Bone bone) { if (!bone.HasIk) return false; var name = NormalizeBoneName(bone.Name); var nameEn = NormalizeBoneName(bone.NameEn); return name.Contains("足ik") || name.Contains("つま先ik") || nameEn.Contains("legik") || nameEn.Contains("footik") || nameEn.Contains("toeik"); } private static bool IsLegChain(MmdModel model, int thigh, int knee, int ankle) => HasAnyName(model.Bones[thigh], "足", "leg", "thigh") && HasAnyName(model.Bones[knee], "ひざ", "膝", "knee") && HasAnyName(model.Bones[ankle], "足首", "ankle"); private static bool HasAnyName(Bone bone, params string[] candidates) { var name = NormalizeBoneName(bone.Name); var nameEn = NormalizeBoneName(bone.NameEn); foreach (var candidate in candidates) if (name.Contains(candidate) || nameEn.Contains(candidate)) return true; return false; } private static int GetSide(string name, string nameEn) { var normalized = NormalizeBoneName(name); var normalizedEn = NormalizeBoneName(nameEn); if (normalized.StartsWith("左") || normalizedEn.StartsWith("left")) return 0; if (normalized.StartsWith("右") || normalizedEn.StartsWith("right")) return 1; return -1; } private static string NormalizeBoneName(string name) => string.IsNullOrWhiteSpace(name) ? string.Empty : name.Normalize(NormalizationForm.FormKC).Replace(" ", string.Empty).ToLowerInvariant(); private static bool IsValidBone(MmdModel model, int index) => index >= 0 && index < model.Bones.Length; } }