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