using System; using System.Collections.Generic; using UnityEngine; namespace FIMSpace.AnimationTools { public static class SkeletonRecognize { public enum EWhatIsIt { Unknown, Humanoidal, Quadroped, Creature } public class SkeletonInfo { public Transform AnimatorTransform; public float LowestVsHighestLen; public float MostLeftVsMostRightLen; public float MostForwVsMostBackLen; public float AverageLen; public Transform ProbablyRootBone; public Transform ProbablyHips; public Transform ProbablyChest; public Transform ProbablyHead; public List TrReachingGround = new List(); public List TrReachingSides = new List(); public List TrEnds = new List(); public List ProbablySpineChain = new List(); public List ProbablySpineChainShort = new List(); public List> ProbablyRightArms = new List>(); public List> ProbablyLeftArms = new List>(); public List> ProbablyLeftLegs = new List>(); public List ProbablyLeftLegRoot = new List(); public List> ProbablyRightLegs = new List>(); public List ProbablyRightLegRoot = new List(); public Vector3 LocalSpaceHighest = Vector3.zero; public Vector3 LocalSpaceMostRight = Vector3.zero; public Vector3 LocalSpaceMostForward = Vector3.zero; public Vector3 LocalSpaceMostBack = Vector3.zero; public Vector3 LocalSpaceMostLeft = Vector3.zero; public Vector3 LocalSpaceLowest = Vector3.zero; public int SpineChainLength { get { return ProbablySpineChain.Count; } } public int LeftArms { get { return ProbablyLeftArms.Count; } } public int LeftLegs { get { return ProbablyLeftLegs.Count; } } public int RightArms { get { return ProbablyRightArms.Count; } } public int RightLegs { get { return ProbablyRightLegs.Count; } } public int Legs { get { return RightLegs + LeftLegs; } } public int Arms { get { return LeftArms + RightArms; } } public EWhatIsIt WhatIsIt = EWhatIsIt.Unknown; public SkeletonInfo(Transform t, List checkOnly = null, Transform pelvisHelp = null) { AnimatorTransform = t; Transform[] childTransforms; if (checkOnly != null) { childTransforms = new Transform[checkOnly.Count]; for (int i = 0; i < checkOnly.Count; i++) { childTransforms[i] = checkOnly[i]; } } else childTransforms = AnimatorTransform.GetComponentsInChildren(true); if (childTransforms.Length > 0) { Vector3 l = AnimatorTransform.InverseTransformPoint(childTransforms[0].position); LocalSpaceHighest = l; LocalSpaceMostRight = l; LocalSpaceMostForward = l; LocalSpaceMostBack = l; LocalSpaceMostLeft = l; LocalSpaceLowest = l; } List childT = new List(); for (int i = 0; i < childTransforms.Length; i++) { Transform c = childTransforms[i]; SkinnedMeshRenderer skin = c.GetComponentInChildren(); if (skin != null) continue; childT.Add(c); } //UnityEngine.Debug.Log("ChildTransforms = " + childTransforms.Length); #region Defining Skeleton Bounds Guides for (int i = 0; i < childT.Count; i++) { Transform c = childT[i]; if (c.GetComponent()) continue; Vector3 local = AnimatorTransform.InverseTransformPoint(c.position); if (local.x > LocalSpaceMostRight.x) LocalSpaceMostRight = local; else if (local.x < LocalSpaceMostLeft.x) LocalSpaceMostLeft = local; if (local.z > LocalSpaceMostForward.z) LocalSpaceMostForward = local; else if (local.z < LocalSpaceMostBack.z) LocalSpaceMostBack = local; if (local.y > LocalSpaceHighest.y) LocalSpaceHighest = local; else if (local.y < LocalSpaceLowest.y) LocalSpaceLowest = local; } #endregion // Helper Measures LowestVsHighestLen = Mathf.Abs(LocalSpaceLowest.y - LocalSpaceHighest.y); MostLeftVsMostRightLen = Mathf.Abs(LocalSpaceMostLeft.x - LocalSpaceMostRight.x); MostForwVsMostBackLen = Mathf.Abs(LocalSpaceMostForward.z - LocalSpaceMostBack.z); AverageLen = (LowestVsHighestLen + MostLeftVsMostRightLen + MostForwVsMostBackLen) / 3f; float limbMinimumLength = LowestVsHighestLen * 0.55f; #region Initial finding name based // arms for (int c = 0; c < childT.Count; c++) { Transform ct = childT[c]; if (NameContains(ct.name, ShouldersNames)) { Transform getCh = GetBottomMostChildTransform(ct); if (NotContainedYetByLimbs(getCh)) TrReachingSides.Add(getCh); } else { if (NameContains(ct.name, ElbowNames)) { Transform getCh = GetBottomMostChildTransform(ct); if (NotContainedYetByLimbs(getCh)) TrReachingSides.Add(getCh); } } } // legs for (int c = 0; c < childT.Count; c++) { Transform ct = childT[c]; if (NameContains(ct.name, UpperLegNames)) { Transform getCh = GetBottomMostChildTransform(ct); if (NotContainedYetByLimbs(getCh)) TrReachingGround.Add(getCh); } else { if (NameContains(ct.name, KneeNames)) { Transform getCh = GetBottomMostChildTransform(ct); if (NotContainedYetByLimbs(getCh)) TrReachingGround.Add(getCh); } } } // pelvis bool hipsByName = false; for (int c = 0; c < childT.Count; c++) { Transform ct = childT[c]; if (NameContains(ct.name, PelvisNames)) { hipsByName = true; ProbablyHips = ct; break; } } // chest bool chestByName = false; for (int c = 0; c < childT.Count; c++) { Transform ct = childT[c]; if (NameContains(ct.name, ChestNames)) { chestByName = true; ProbablyChest = ct; break; } } // head bool headByName = false; for (int c = 0; c < childT.Count; c++) { Transform ct = childT[c]; if (NameContains(ct.name, HeadNames)) { headByName = true; ProbablyHead = ct; break; } } if (ProbablyHead != null) if (ProbablyHips != null) { if (IsChildOf(ProbablyHead, ProbablyHips) == false) { ProbablyHead = null; } } // root for (int c = 0; c < childT.Count; c++) { Transform ct = childT[c]; if (NameContains(ct.name, RootNames)) { ProbablyRootBone = ct; break; } } #endregion #region Defining End Transforms for Arms / Legs / Head if (childT.Count > 2) { for (int i = 1; i < childT.Count; i++) { Transform tr = childT[i]; if (tr.childCount == 0) { TrEnds.Add(tr); Vector3 l = Loc(tr); if (l.y < LocalSpaceLowest.y + LowestVsHighestLen * 0.1f) { if (NotContainedYetByLimbs(tr)) TrReachingGround.Add(tr); } else { if (l.y > LocalSpaceLowest.y + LowestVsHighestLen * 0.2f) { if (l.x < MostLeftVsMostRightLen * -0.1f || l.x > MostLeftVsMostRightLen * 0.1f) { if (NotContainedYetByLimbs(tr)) TrReachingSides.Add(tr); } } } } } } #endregion #region Chest Basing on Left / Right Sides Limbs if (!chestByName) { List probablyChestOnes = new List(); for (int i = 0; i < TrReachingSides.Count; i++) { if (childT[i].GetComponent()) continue; Transform par = TrReachingSides[i].parent; while (par != null) { if (par.childCount > 2) { Vector3 loc = Loc(par); if (loc.x > -MostLeftVsMostRightLen * 0.03f && loc.x < MostLeftVsMostRightLen * 0.03f) { probablyChestOnes.Add(par); break; } } par = par.parent; } } if (probablyChestOnes.Count == 1) ProbablyChest = probablyChestOnes[0]; else if (probablyChestOnes.Count > 1) { if (probablyChestOnes[0] == probablyChestOnes[1]) ProbablyChest = probablyChestOnes[0]; } } #endregion #region Pelvis Basing On Left / Right Low Limbs if (!hipsByName) { List probablyHipsOnes = new List(); for (int i = 0; i < TrReachingGround.Count; i++) { Transform par = TrReachingGround[i].parent; while (par != null) { if (par.childCount > 2) { Vector3 loc = Loc(par); if (loc.y > LocalSpaceLowest.y + LowestVsHighestLen * 0.04f) if (loc.x > -MostLeftVsMostRightLen * 0.02f && loc.x < MostLeftVsMostRightLen * 0.02f) { probablyHipsOnes.Add(par); break; } } par = par.parent; } } if (probablyHipsOnes.Count == 1) ProbablyChest = probablyHipsOnes[0]; else if (probablyHipsOnes.Count > 1) { if (probablyHipsOnes[0] == probablyHipsOnes[1]) ProbablyHips = probablyHipsOnes[0]; } } if (ProbablyHips == null) ProbablyHips = pelvisHelp; #endregion #region correcting chest if required if (ProbablyChest == null || ProbablyChest == ProbablyHips || (ProbablyHips != null && IsChildOf(ProbablyChest, ProbablyHips) == false)) { if (ProbablyHips) if (ProbablyHead) { Transform checkT = ProbablyHead.parent; bool found = false; while (checkT.parent != null && checkT.parent != ProbablyHips) { if (checkT.childCount > 2) { // Check if some side limbs are child bones of chest check bone for (int s = 0; s < TrReachingSides.Count; s++) { Transform side = TrReachingSides[s]; if (IsChildOf(side, checkT)) { found = true; break; } } } if (found) break; checkT = checkT.parent; } if (found) ProbablyChest = checkT; } } if (ProbablyHips == null) ProbablyHips = pelvisHelp; #endregion // Probably correctly detected chest and hips if (ProbablyChest && ProbablyHips) { if (MostForwVsMostBackLen > LowestVsHighestLen * 0.9f) // If forward legth is bigger than model's height { // In most cases chest is more in front than hips if (Loc(ProbablyChest).z < Loc(ProbablyHips).z) // Chest is behind hips - swap! { Transform swap = ProbablyChest; ProbablyChest = ProbablyHips; ProbablyHips = swap; UnityEngine.Debug.Log("Hips - Chest - Reversed Detection Swap!"); } } #region Trying To Detect Head if (!headByName) { Vector3 highestForHead = Vector3.zero; for (int c = 0; c < ProbablyChest.childCount; c++) { // checking all probably chest child transforms Transform ch = ProbablyChest.GetChild(c); Vector3 lc; if (ch.childCount > 0) // Going through { for (int c2 = 0; c2 < ch.childCount; c2++) { Transform ch2 = ch.GetChild(c2); lc = Loc(ch2); if (lc.x > -MostLeftVsMostRightLen * 0.04f && lc.x < MostLeftVsMostRightLen * 0.04f) { if (Loc(ch2).y > highestForHead.y) { highestForHead = Loc(ch2); ProbablyHead = ch2; } } } } lc = Loc(ch); if (lc.x > -MostLeftVsMostRightLen * 0.04f && lc.x < MostLeftVsMostRightLen * 0.04f) if (lc.y > highestForHead.y) { highestForHead = Loc(ch); ProbablyHead = ch; } } if (ProbablyChest && ProbablyHead && ProbablyHips) { float chestToPelvis = Vector3.Distance(Loc(ProbablyChest), Loc(ProbablyHips)); if ((ProbablyChest.childCount < 3 || chestToPelvis < AverageLen * 0.12f) && ProbablyHead.childCount > 1) { ProbablyChest = ProbablyHead; ProbablyHead = GetHighestChild(ProbablyHead, AnimatorTransform, MostLeftVsMostRightLen * 0.05f); if (ProbablyHead == ProbablyChest) ProbablyHead = ProbablyChest.GetChild(0); } } } #endregion #region Eliminating wrong detected arms (it can be ear bones) if (ProbablyHead) { for (int i = TrReachingSides.Count - 1; i >= 0; i--) { if (IsChildOf(TrReachingSides[i], ProbablyHead)) TrReachingSides.RemoveAt(i); } } for (int i = TrReachingSides.Count - 1; i >= 0; i--) { if (GetDepth(TrReachingSides[i], AnimatorTransform) < 5) { TrReachingSides.RemoveAt(i); } } #endregion #region Detecting Spine Chain Transform headC = null; if (ProbablyHead) { //if (ProbablyHead.parent) headC = ProbablyHead.parent; ProbablySpineChain.Add(ProbablyHead); headC = ProbablyHead.parent; } while (headC != null && headC != ProbablyHips) { ProbablySpineChain.Add(headC); headC = headC.parent; } ProbablySpineChain.Reverse(); for (int i = 0; i < Mathf.Min(4, ProbablySpineChain.Count); i++) { ProbablySpineChainShort.Add(ProbablySpineChain[i]); } #endregion #region Detecting Legs List confirmedLegs = new List(); for (int i = 0; i < TrReachingGround.Count; i++) { Transform start = TrReachingGround[i]; Vector3 startLoc = Loc(start); List fullChain = new List(); Transform untilHips = start; while (untilHips != null && (untilHips != ProbablyHips && untilHips != ProbablyChest)) { fullChain.Add(untilHips); untilHips = untilHips.parent; } if (fullChain.Count >= 3) { List legChain = new List(); legChain.Add(fullChain[fullChain.Count - 1]); legChain.Add(fullChain[fullChain.Count - 2]); legChain.Add(fullChain[fullChain.Count - 3]); confirmedLegs.Add(start); if (startLoc.x < MostLeftVsMostRightLen * 0.02f) { ProbablyLeftLegs.Add(legChain); ProbablyLeftLegRoot.Add(untilHips); } else { ProbablyRightLegs.Add(legChain); ProbablyRightLegRoot.Add(untilHips); } } } #endregion #region Detecting Arms for (int i = 0; i < TrReachingSides.Count; i++) { Transform start = TrReachingSides[i]; Vector3 startLoc = Loc(start); List fullChain = new List(); Transform untilChest = start; while (untilChest != null && untilChest != ProbablyChest) { fullChain.Add(untilChest); untilChest = untilChest.parent; } if (fullChain.Count >= 4) { List armChain = new List(); armChain.Add(fullChain[fullChain.Count - 1]); armChain.Add(fullChain[fullChain.Count - 2]); armChain.Add(fullChain[fullChain.Count - 3]); armChain.Add(fullChain[fullChain.Count - 4]); if (startLoc.x < MostLeftVsMostRightLen * 0.02f) ProbablyLeftArms.Add(armChain); else ProbablyRightArms.Add(armChain); } } #endregion #region Removing Duplicates (resulting by fingers counts) ClearDuplicates(ProbablyLeftArms, null); ClearDuplicates(ProbablyRightArms, null); ClearDuplicates(ProbablyLeftLegs, ProbablyLeftLegRoot); ClearDuplicates(ProbablyRightLegs, ProbablyRightLegRoot); #endregion if (Legs == 2 && Arms == 2) { WhatIsIt = EWhatIsIt.Humanoidal; } else if (Legs == 4 && Arms == 0) { WhatIsIt = EWhatIsIt.Quadroped; } else if (Legs > 0 || Arms > 0) { WhatIsIt = EWhatIsIt.Creature; } else { WhatIsIt = EWhatIsIt.Unknown; } } float middleHeight = Mathf.Lerp(LocalSpaceLowest.y, LocalSpaceHighest.y, 0.5f); UnityEngine.Debug.DrawLine(t.TransformPoint(new Vector3(LocalSpaceMostLeft.x, LocalSpaceHighest.y, LocalSpaceMostForward.z)), t.TransformPoint(new Vector3(LocalSpaceMostLeft.x, LocalSpaceLowest.y, LocalSpaceMostForward.z)), Color.green, 12); UnityEngine.Debug.DrawLine(t.TransformPoint(new Vector3(LocalSpaceMostLeft.x, middleHeight, LocalSpaceMostForward.z)), t.TransformPoint(new Vector3(LocalSpaceMostRight.x, middleHeight, LocalSpaceMostForward.z)), Color.red, 12); UnityEngine.Debug.DrawLine(t.TransformPoint(new Vector3(LocalSpaceMostRight.x, middleHeight, LocalSpaceMostForward.z)), t.TransformPoint(new Vector3(LocalSpaceMostRight.x, middleHeight, LocalSpaceMostBack.z)), Color.blue, 12); } bool NotContainedYetByAny(Transform t) { return (!TrReachingSides.Contains(t) && !TrReachingGround.Contains(t) && !TrEnds.Contains(t) && t != ProbablyChest && t != ProbablyHips && t != ProbablyHead && t != ProbablyChest && t != ProbablyRootBone && t != AnimatorTransform); } bool NotContainedYetByLimbs(Transform t) { return (!TrReachingSides.Contains(t) && !TrReachingGround.Contains(t)); } public Transform GetHighestChild(Transform t, Transform root, float inCenterRangeFactor) { if (t == null) return null; Transform highT = t; Vector3 highest = root.InverseTransformPoint(t.position); foreach (var ct in t.GetComponentsInChildren(true)) { Vector3 pos = root.InverseTransformPoint(ct.position); if (pos.x > -inCenterRangeFactor && pos.x < inCenterRangeFactor) if (pos.y > highest.y) { highest.y = pos.y; highT = ct; } } return highT; } //float ComputeLength(Transform p, int parentBack) //{ // float len = 0f; // if (p != null) // for (int i = 0; i < parentBack; i++) // { // if (p.parent != null) // { // len += Vector3.Distance(p.position, p.parent.position); // p = p.parent; // } // else // break; // } // return len; //} void ClearDuplicates(List> limbs, List roots) { if (limbs.Count > 1) { for (int main = 0; main < limbs.Count; main++) // Checking all limb chains { if (main >= limbs.Count) return; var limb = limbs[main]; // Checking if some other limbs contains duplicate bones of each other // It can be caused by finger bones - how many fingers -> that many hands detected for (int i = limbs.Count - 1; i >= 0; i--) { if (i == main) continue; // Don't check self var otherLimb = limbs[i]; bool remove = false; for (int p = 0; p < otherLimb.Count; p++) { if (limb.Contains(otherLimb[p])) { remove = true; break; } } if (remove) { limbs.RemoveAt(i); } } } } } Vector3 Loc(Transform t) { return AnimatorTransform.InverseTransformPoint(t.position); } #region Debug Log Report public string GetLog() { string log = "< " + AnimatorTransform.name + " >\n"; log += "\nGenerate Guides:\n"; log += "Highest: " + LocalSpaceHighest + " "; log += "Lowest: " + LocalSpaceLowest + " "; log += "Left: " + LocalSpaceMostLeft + " "; log += "Right: " + LocalSpaceMostRight + " "; log += "Forward: " + LocalSpaceMostForward + " "; log += "Back: " + LocalSpaceMostBack + " "; log += "\n\nGenerated Helper Measurements: \n"; log += "UpDown: " + LowestVsHighestLen + " "; log += "LeftRight: " + MostLeftVsMostRightLen + " "; log += "ForwBack: " + MostForwVsMostBackLen + " "; log += "Avr: " + AverageLen + " "; log += "\n\nDetected Propabilities: \n"; log += "ProbablyHips: " + ProbablyHips + " "; log += "ProbablyChest: " + ProbablyChest + " "; log += "ProbablyHead: " + ProbablyHead + " "; log += "\n\nLimb End Detections: \n"; log += "Reaching Ground: " + TrReachingGround.Count + " "; log += "Reaching Sides: " + TrReachingSides.Count + " "; log += "Spine Chain Length: " + ProbablySpineChain.Count + " (" + ProbablySpineChainShort.Count + ") "; log += "\n\nDetected Propabilities: \n"; log += "Probably Left Arms: " + ProbablyLeftArms.Count + " "; log += "Probably Right Arms: " + ProbablyRightArms.Count + " "; log += "Probably Left Legs: " + ProbablyLeftLegs.Count + " "; log += "Probably Right Legs: " + ProbablyRightLegs.Count + " "; log += "\n\n\nTr Ends: \n"; for (int i = 0; i < TrEnds.Count; i++) { if (TrEnds[i] == null) continue; log += TrEnds[i].name + " "; } log += "\n\nTr Reaching Ground: \n"; for (int i = 0; i < TrReachingGround.Count; i++) { if (TrReachingGround[i] == null) continue; log += TrReachingGround[i].name + " "; } log += "\n\nTr Reaching Sides: \n"; for (int i = 0; i < TrReachingSides.Count; i++) { if (TrReachingSides[i] == null) continue; log += TrReachingSides[i].name + " "; } if (ProbablyLeftArms.Count > 0) { log += "\n\nDebug Left Arms: \n"; for (int i = 0; i < ProbablyLeftArms.Count; i++) { if (ProbablyLeftArms[i] == null) continue; log += "[" + i + "] "; for (int l = 0; l < ProbablyLeftArms[i].Count; l++) { log += ProbablyLeftArms[i][l].name + " "; } log += "\n"; } } if (ProbablySpineChainShort.Count > 0) { log += "\n\nDebug Spine Chain: \n"; for (int i = 0; i < ProbablySpineChainShort.Count; i++) { if (ProbablySpineChainShort[i] == null) continue; log += ProbablySpineChainShort[i].name + " "; } } log += "\n\n"; return log; } #endregion public static int GetDepth(Transform t, Transform skelRootBone) { int depth = 0; if (t == skelRootBone) return 0; if (t == null) return 0; if (t.parent == null) return 0; while (t != null && t != skelRootBone) { t = t.parent; depth += 1; } return depth; } } #region Transforms Utils public static bool IsChildOf(Transform child, Transform parent) { Transform p = child; while (p != null) { if (p == parent) return true; p = p.parent; } return false; } public static Transform GetBottomMostChildTransform(Transform parent) { var allCh = parent.GetComponentsInChildren(true); int lowest = 0; Transform lowestT = parent; for (int c = 0; c < allCh.Length; c++) { if (allCh[c] == parent) continue; Transform ch = allCh[c]; int depth = 0; while (ch.parent != parent && ch.parent != null) { depth += 1; ch = ch.parent; } if (depth > lowest) { lowest = depth; lowestT = allCh[c]; } } return lowestT; } #endregion #region Name Based Search Utils public static readonly string[] SpineNames = new string[] { "spine" }; public static readonly string[] NeckNames = new string[] { "neck" }; public static readonly string[] HeadNames = new string[] { "head" }; public static readonly string[] RootNames = new string[] { "root", "origin", "skel" }; public static readonly string[] PelvisNames = new string[] { "pelvis", "hips", "pelv" }; public static readonly string[] ChestNames = new string[] { "chest", "upperspine" }; public static readonly string[] ShouldersNames = new string[] { "shoulde", "collarbon", "clavicl" }; public static readonly string[] UpperLegNames = new string[] { "upperleg", "thigh" }; public static readonly string[] KneeNames = new string[] { "knee", "calf", "lowerleg" }; public static readonly string[] ElbowNames = new string[] { "elbow", "lowerarm" }; public static bool NameContains(string name, string[] names) { string nm = name.ToLower(); nm = nm.Replace("-", ""); nm = nm.Replace(" ", ""); nm = nm.Replace("_", ""); nm = nm.Replace("|", ""); nm = nm.Replace("@", ""); for (int n = 0; n < names.Length; n++) { if (nm.Contains(names[n])) return true; } return false; } #endregion } }