Last active
December 11, 2023 16:34
-
-
Save JLChnToZ/2e449d73f2a28dd3bfb8a0d06013c854 to your computer and use it in GitHub Desktop.
Unity's missing Skinned Mesh Bone Reference Editor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using UnityEngine.Rendering; | |
using UnityEditor; | |
using UnityEditorInternal; | |
public class BoneEditorWindow : EditorWindow { | |
const string BASE_MENU_PATH = "CONTEXT/" + nameof(SkinnedMeshRenderer) + "/"; | |
const string MENU_PATH = BASE_MENU_PATH + "Edit Bone References"; | |
const string COPY_MENU_PATH = BASE_MENU_PATH + "Copy Bone References"; | |
const string PASTE_MENU_PATH = BASE_MENU_PATH + "Paste Bone References"; | |
const string EDITOR_PREFS_AUTOAPPLY = "BoneEditorWindow.AutoApply"; | |
public static float boneSize = 0.06F; | |
public static Color minCoverageBoneColor = new Color(0, 0, 0.5F, 0.5F); | |
public static Color maxCoverageBoneColor = new Color(0, 0.8F, 1); | |
static GUIContent tempContent, warningIcon, infoIcon, filterIcon, plusIcon, avatarIcon, rootIcon, notAssignedIcon, noWeightIcon; | |
static readonly Queue<Transform> walker = new Queue<Transform>(); | |
static readonly HashSet<Transform> walkedBones = new HashSet<Transform>(); | |
static readonly HashSet<string> walkedNames = new HashSet<string>(); | |
static bool autoApply; | |
static Transform[] copiedBones; | |
HashSet<Transform> humanoidBones; | |
SkinnedMeshRenderer target; | |
static readonly Dictionary<Transform, float> boneCoverageMap = new Dictionary<Transform, float>(); | |
Mesh mesh; | |
Transform[] bones; | |
float totalCoverage; | |
static float maxCoverage; | |
static bool shouldRefreshBoneCoverageMap; | |
static Transform selectedBone; | |
(int refCount, float coverage)[] boneInfos; | |
ReorderableList list; | |
Vector2 scrollPos; | |
static Action refreshBoneCoverageMap; | |
static BoneEditorWindow() { | |
SceneView.duringSceneGui += OnSceneGUI; | |
} | |
[MenuItem(MENU_PATH)] | |
static void BoneEditorMenu(MenuCommand command) { | |
if (!BoneEditorMenuEnabled(command)) return; | |
var instance = CreateInstance<BoneEditorWindow>(); | |
instance.target = command.context as SkinnedMeshRenderer; | |
instance.ShowUtility(); | |
instance.OnEnable(); | |
} | |
[MenuItem(COPY_MENU_PATH)] | |
static void CopyBones(MenuCommand command) { | |
if (!BoneEditorMenuEnabled(command)) return; | |
copiedBones = (command.context as SkinnedMeshRenderer).bones; | |
} | |
[MenuItem(PASTE_MENU_PATH)] | |
static void PasteBones(MenuCommand command) { | |
if (!PasteBonesEnabled(command)) return; | |
var smr = command.context as SkinnedMeshRenderer; | |
Undo.RecordObject(smr, "Paste Bones"); | |
smr.bones = copiedBones; | |
} | |
[MenuItem(MENU_PATH, true)] | |
[MenuItem(COPY_MENU_PATH, true)] | |
static bool BoneEditorMenuEnabled(MenuCommand command) { | |
var smr = command.context as SkinnedMeshRenderer; | |
if (smr == null) return false; | |
var mesh = smr.sharedMesh; | |
if (mesh == null || !mesh.HasVertexAttribute(VertexAttribute.BlendIndices)) return false; | |
return true; | |
} | |
[MenuItem(PASTE_MENU_PATH, true)] | |
static bool PasteBonesEnabled(MenuCommand command) => | |
BoneEditorMenuEnabled(command) && | |
copiedBones != null && | |
copiedBones.Length == (command.context as SkinnedMeshRenderer).sharedMesh.bindposes.Length; | |
void OnEnable() { | |
if (filterIcon == null) filterIcon = EditorGUIUtility.IconContent("d_Animation.FilterBySelection"); | |
if (plusIcon == null) plusIcon = EditorGUIUtility.IconContent("Toolbar Plus"); | |
if (filterIcon == null) filterIcon = EditorGUIUtility.IconContent("Animation.FilterBySelection"); | |
if (target == null) return; | |
minSize = new Vector2(500, 100); | |
mesh = target.sharedMesh; | |
RefreshBones(); | |
Undo.undoRedoPerformed += OnUndoRedo; | |
autoApply = EditorPrefs.GetBool(EDITOR_PREFS_AUTOAPPLY, true); | |
refreshBoneCoverageMap += RefreshBoneCoverageMap; | |
} | |
void OnLostFocus() { | |
selectedBone = null; | |
} | |
void OnDisable() { | |
Undo.undoRedoPerformed -= OnUndoRedo; | |
refreshBoneCoverageMap -= RefreshBoneCoverageMap; | |
shouldRefreshBoneCoverageMap = true; | |
} | |
void UpdateDirty(bool isDirty) { | |
titleContent = new GUIContent($"Bone Editor{(isDirty ? "*" : "")}"); | |
} | |
void OnGUI() { | |
if (target == null) { | |
Close(); | |
return; | |
} | |
if (list == null || mesh != target.sharedMesh) { | |
mesh = target.sharedMesh; | |
if (mesh == null || !mesh.HasVertexAttribute(VertexAttribute.BlendIndices)) { | |
Close(); | |
return; | |
} | |
RefreshBones(); | |
} | |
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) { | |
if (GUILayout.Button(GetGUIContent("Fill Empty", "Fill empty bone references."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) { | |
Undo.IncrementCurrentGroup(); | |
var undoGroup = Undo.GetCurrentGroup(); | |
for (int i = 0; i < bones.Length; i++) | |
if (bones[i] == null && boneInfos[i].coverage > 0) AutoSetBone(i, false); | |
ApplyBones("Fill Empty Bones"); | |
Undo.SetCurrentGroupName("Fill Empty Bones"); | |
Undo.CollapseUndoOperations(undoGroup); | |
} | |
if (GUILayout.Button(GetGUIContent("Set Bindpose", "Move all bones to bindpose."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) { | |
Undo.IncrementCurrentGroup(); | |
var undoGroup = Undo.GetCurrentGroup(); | |
walkedBones.Clear(); | |
foreach (var i in GetSortedBoneIndecesByDepth()) | |
if (bones[i] != null && !walkedBones.Add(bones[i])) AutoSetBone(i, true); | |
ApplyBones("Move Bones To Bindpose"); | |
Undo.SetCurrentGroupName("Move Bones To Bindpose"); | |
Undo.CollapseUndoOperations(undoGroup); | |
} | |
if (GUILayout.Button(GetGUIContent("Reassign Dup.", "Reassign duplicated bones."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) { | |
Undo.IncrementCurrentGroup(); | |
var undoGroup = Undo.GetCurrentGroup(); | |
walkedBones.Clear(); | |
foreach (var i in GetSortedBoneIndecesByDepth()) | |
if (bones[i] != null && !walkedBones.Add(bones[i])) ReassignBone(i); | |
ApplyBones("Reassign Bones"); | |
Undo.SetCurrentGroupName("Reassign Bones"); | |
Undo.CollapseUndoOperations(undoGroup); | |
} | |
if (GUILayout.Button(GetGUIContent("Rename Dup.", "Rename duplicated bones."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) { | |
Undo.IncrementCurrentGroup(); | |
var undoGroup = Undo.GetCurrentGroup(); | |
walkedNames.Clear(); | |
walkedBones.Clear(); | |
foreach (var i in GetSortedBoneIndecesByDepth()) { | |
var bone = bones[i]; | |
if (bone == null || walkedBones.Add(bone)) continue; | |
var name = bone.name; | |
if (walkedNames.Add(name)) continue; | |
name = GetUniqueNameForBone(bone); | |
walkedNames.Add(name); | |
Undo.RecordObject(bone.gameObject, "Rename Bone"); | |
bone.name = name; | |
} | |
ApplyBones("Rename Bones"); | |
Undo.SetCurrentGroupName("Rename Bones"); | |
Undo.CollapseUndoOperations(undoGroup); | |
} | |
GUILayout.FlexibleSpace(); | |
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) | |
RefreshBones(); | |
using (var changed = new EditorGUI.ChangeCheckScope()) { | |
autoApply = GUILayout.Toggle(autoApply, GetGUIContent(autoApply ? "Auto Apply" : "Auto", "Auto apply changes to target skinned mesh renderer."), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)); | |
if (changed.changed) EditorPrefs.SetBool(EDITOR_PREFS_AUTOAPPLY, autoApply); | |
} | |
if (!autoApply && GUILayout.Button("Apply", EditorStyles.toolbarButton, GUILayout.ExpandWidth(false))) | |
ApplyBones(); | |
} | |
using (new EditorGUILayout.HorizontalScope()) | |
using (new EditorGUI.DisabledScope(true)) { | |
EditorGUILayout.ObjectField(mesh, typeof(Mesh), false, GUILayout.Width(EditorGUIUtility.labelWidth + ReorderableList.Defaults.dragHandleWidth - 4)); | |
EditorGUILayout.ObjectField(target, typeof(SkinnedMeshRenderer), true, GUILayout.ExpandWidth(true)); | |
} | |
using (var scroll = new EditorGUILayout.ScrollViewScope(scrollPos)) { | |
scrollPos = scroll.scrollPosition; | |
walkedBones.Clear(); | |
walkedNames.Clear(); | |
list.DoLayoutList(); | |
} | |
if (focusedWindow == this) selectedBone = list.index >= 0 ? bones[list.index] : null; | |
} | |
void OnHierarchyChange() { | |
if (target == null) { | |
Close(); | |
return; | |
} | |
var root = target.rootBone; | |
if (root == null) root = target.transform; | |
if (humanoidBones != null) humanoidBones.Clear(); | |
var animator = root.GetComponentInParent<Animator>(); | |
if (animator == null) return; | |
var avatar = animator.avatar; | |
if (avatar == null || !avatar.isValid || !avatar.isHuman) return; | |
if (humanoidBones == null) humanoidBones = new HashSet<Transform>(); | |
for (var i = HumanBodyBones.Hips; i < HumanBodyBones.LastBone; i++) { | |
var bone = animator.GetBoneTransform(i); | |
if (bone != null) humanoidBones.Add(bone); | |
} | |
} | |
static void OnSceneGUI(SceneView sceneView) { | |
if (Event.current.type != EventType.Repaint || !sceneView.drawGizmos) return; | |
RefreshBoneCoverageMapIfNeeded(); | |
walker.Clear(); | |
walkedBones.Clear(); | |
foreach (var bone in boneCoverageMap.Keys) walker.Enqueue(bone); | |
while (walker.Count > 0) { | |
var current = walker.Dequeue(); | |
if (current == null || !walkedBones.Add(current)) continue; | |
if (boneCoverageMap.TryGetValue(current, out float coverage)) { | |
var parent = current.parent; | |
Color parentColor = default; | |
if (boneCoverageMap.TryGetValue(parent, out float parentCoverage)) | |
parentColor = GetBoneColor(parent, parentCoverage); | |
else parent = null; | |
DrawBoneGizmos(sceneView, parent, current, GetBoneColor(current, coverage), parentColor); | |
} | |
foreach (Transform child in current) walker.Enqueue(child); | |
} | |
} | |
static void RefreshBoneCoverageMapIfNeeded() { | |
if (!shouldRefreshBoneCoverageMap && boneCoverageMap != null) return; | |
shouldRefreshBoneCoverageMap = false; | |
boneCoverageMap.Clear(); | |
maxCoverage = 0; | |
refreshBoneCoverageMap?.Invoke(); | |
} | |
void RefreshBoneCoverageMap() { | |
for (int i = 0; i < boneInfos.Length; i++) { | |
var bone = bones[i]; | |
if (bone == null) continue; | |
boneCoverageMap.TryGetValue(bone, out float coverage); | |
coverage += boneInfos[i].coverage; | |
boneCoverageMap[bone] = coverage; | |
maxCoverage = Mathf.Max(maxCoverage, coverage); | |
} | |
} | |
void OnUndoRedo() { | |
if (autoApply) { | |
RefreshBones(); | |
Repaint(); | |
} | |
} | |
void RefreshBones() { | |
if (target == null) return; | |
bones = target.bones; | |
int boneCount = mesh.bindposes.Length; | |
if (bones == null || bones.Length != boneCount) { | |
var newBones = new Transform[boneCount]; | |
if (bones != null) Array.Copy(bones, newBones, Mathf.Min(bones.Length, boneCount)); | |
bones = newBones; | |
ApplyBones("Initialize Bones"); | |
} else | |
UpdateDirty(false); | |
boneInfos = new (int, float)[boneCount]; | |
totalCoverage = 0; | |
foreach (var boneWeight in mesh.GetAllBoneWeights()) { | |
var boneInfo = boneInfos[boneWeight.boneIndex]; | |
boneInfo.refCount++; | |
boneInfo.coverage += boneWeight.weight; | |
totalCoverage += boneWeight.weight; | |
boneInfos[boneWeight.boneIndex] = boneInfo; | |
} | |
list = new ReorderableList(bones, typeof(Transform), true, true, false, false) { | |
drawElementCallback = OnListDrawElement, | |
drawHeaderCallback = DoNotDraw, | |
drawFooterCallback = DoNotDraw, | |
onReorderCallback = OnListReorder, | |
elementHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing, | |
showDefaultBackground = false, | |
headerHeight = 0, | |
footerHeight = 0, | |
}; | |
OnHierarchyChange(); | |
shouldRefreshBoneCoverageMap = true; | |
} | |
void OnListDrawElement(Rect rect, int index, bool isActive, bool isFocused) { | |
var bone = bones[index]; | |
var (refCount, coverage) = boneInfos[index]; | |
rect.y += EditorGUIUtility.standardVerticalSpacing; | |
var rect2 = rect; | |
rect2.width -= EditorGUIUtility.singleLineHeight * 2; | |
rect2.height = EditorGUIUtility.singleLineHeight; | |
var contentColor = GUI.contentColor; | |
if (coverage == 0) { | |
var newContentColor = contentColor; | |
newContentColor.a *= 0.5F; | |
GUI.contentColor = newContentColor; | |
} | |
var iconRect = rect2; | |
iconRect.x = rect2.xMin + EditorGUIUtility.labelWidth; | |
iconRect.width = 16; | |
rect2 = EditorGUI.PrefixLabel(rect2, GetGUIContent($"{index} ({refCount}, {coverage / totalCoverage:0.##%})")); | |
if (coverage <= 0) { | |
if (noWeightIcon == null) | |
noWeightIcon = new GUIContent(EditorGUIUtility.IconContent("UnLinked")) { | |
tooltip = "This bone has no weights. In most cases you can safely remove this bone reference.", | |
}; | |
iconRect.x -= iconRect.width; | |
GUI.Label(iconRect, noWeightIcon); | |
} else if (bone == null) { | |
if (notAssignedIcon == null) | |
notAssignedIcon = new GUIContent(EditorGUIUtility.IconContent("Invalid")) { | |
tooltip = "This bone is not assigned and it has weights. Parts of the mesh binded to this bone will breaks.", | |
}; | |
iconRect.x -= iconRect.width; | |
GUI.Label(iconRect, notAssignedIcon); | |
} | |
using (var changed = new EditorGUI.ChangeCheckScope()) { | |
bone = EditorGUI.ObjectField(rect2, bone, typeof(Transform), true) as Transform; | |
if (changed.changed) { | |
ReplaceBone(index, bone); | |
ApplyBones("Replace Bone Transform"); | |
} | |
} | |
iconRect = rect2; | |
iconRect.x = rect2.xMax - EditorGUIUtility.singleLineHeight - 4; | |
iconRect.width = EditorGUIUtility.singleLineHeight; | |
if (bone != null) { | |
if (humanoidBones != null && humanoidBones.Contains(bone)) { | |
if (avatarIcon == null) | |
avatarIcon = new GUIContent(EditorGUIUtility.IconContent("Avatar Icon")) { | |
tooltip = "This bone is a humanoid bone.", | |
}; | |
iconRect.x -= iconRect.width; | |
GUI.Label(iconRect, avatarIcon); | |
} | |
if (target.rootBone == bone) { | |
if (rootIcon == null) | |
rootIcon = new GUIContent(EditorGUIUtility.IconContent("AvatarPivot")) { | |
tooltip = "This bone is the root bone.", | |
}; | |
iconRect.x -= iconRect.width; | |
GUI.Label(iconRect, rootIcon); | |
} | |
} | |
GUI.contentColor = contentColor; | |
rect2.xMin = rect2.xMax + 2; | |
rect2.width = EditorGUIUtility.singleLineHeight; | |
#if UNITY_2021_2_OR_NEWER | |
var iconButtonStyle = EditorStyles.iconButton; | |
#else | |
var iconButtonStyle = EditorStyles.label; | |
#endif | |
if (GUI.Button( | |
rect2, | |
bone == null ? | |
GetGUIContent(tooltip: "Create new bone transform at bindpose.", iconContent: plusIcon) : | |
GetGUIContent(tooltip: "Try move bone transform to bindpose.", iconContent: filterIcon), | |
iconButtonStyle | |
)) { | |
Undo.IncrementCurrentGroup(); | |
var undoGroup = Undo.GetCurrentGroup(); | |
AutoSetBone(index, true); | |
ApplyBones("Update Bones"); | |
Undo.SetCurrentGroupName(bone == null ? "Create Bone Transform" : "Move Bone Transform"); | |
Undo.CollapseUndoOperations(undoGroup); | |
} | |
rect2.x += 16; | |
if (bone != null) { | |
bool isDuplicateName = !walkedNames.Add(bone.name); | |
bool isDuplicateBone = !walkedBones.Add(bone); | |
var rootBone = target.rootBone; | |
if (rootBone == null) rootBone = target.transform; | |
if (!bone.IsChildOf(rootBone)) | |
EditorGUI.LabelField(rect2, GetWarningContent(tooltip: "This bone is not under root bone. This is not recommend unless it is intentional."), EditorStyles.label); | |
else if (isDuplicateBone) { | |
if (GUI.Button(rect2, GetInfoContent(tooltip: "This bone is duplicated. You may need to reassign a new bone in some cases."), iconButtonStyle) && | |
EditorUtility.DisplayDialog("Reassign Bone", $"Reassign bone \"{bone.name}\"?", "Yes", "No")) { | |
ReassignBone(index); | |
ApplyBones("Replace Bone Transform"); | |
} | |
} else if (isDuplicateName) { | |
if (GUI.Button(rect2, GetInfoContent(tooltip: "This bone has the same name with another bone. You may need to rename it in some cases."), iconButtonStyle)) { | |
var newName = GetUniqueNameForBone(bone); | |
if (EditorUtility.DisplayDialog("Rename Bone", $"Rename bone \"{bone.name}\" to \"{newName}\"? This may break some animation clips.", "Rename", "Cancel")) { | |
Undo.RecordObject(bone.gameObject, "Rename Bone"); | |
bone.name = newName; | |
} | |
} | |
} | |
} | |
} | |
static void DoNotDraw(Rect rect) {} | |
void OnListReorder(ReorderableList list) => ApplyBones("Reorder Bones"); | |
bool AutoSetBone(int index, bool move) { | |
var bone = bones[index]; | |
var rootBone = target.rootBone; | |
if (rootBone == null) rootBone = target.transform; | |
var bindposes = mesh.bindposes; | |
GameObject go = null; | |
Matrix4x4 offset; | |
if (bone == null) { | |
go = new GameObject($"{target.name}.Bone {index}"); | |
bone = go.transform; | |
bone.SetParent(rootBone, false); | |
go.name = GetUniqueNameForBone(bone, rootBone); | |
ReplaceBone(index, bone); | |
offset = rootBone.localToWorldMatrix; | |
} else if (!move) return false; | |
else if (bone == rootBone) { | |
var parentBone = bone.parent; | |
if (parentBone == null) return false; | |
offset = parentBone.localToWorldMatrix; | |
} else { | |
RefreshBoneCoverageMapIfNeeded(); | |
var parentBone = bone.parent; | |
while (parentBone != null) { | |
if (boneCoverageMap.ContainsKey(parentBone)) break; | |
parentBone = parentBone.parent; | |
} | |
if (parentBone == null) offset = rootBone.localToWorldMatrix; | |
else { | |
int parentIndex = Array.LastIndexOf(bones, parentBone, index); | |
if (parentIndex < 0) parentIndex = Array.IndexOf(bones, parentBone, index); | |
offset = parentBone.localToWorldMatrix * bindposes[parentIndex]; | |
} | |
} | |
offset *= bindposes[index].inverse; | |
if (go == null) Undo.RecordObject(bone, "Move Bone Transform To Bindpose"); | |
bone.SetPositionAndRotation(offset.MultiplyPoint(Vector3.zero), offset.rotation); | |
var scale = offset.lossyScale; | |
var boneParent = bone.parent; | |
if (boneParent != null) { | |
var boneParentScale = boneParent.lossyScale; | |
scale.x /= boneParentScale.x; | |
scale.y /= boneParentScale.y; | |
scale.z /= boneParentScale.z; | |
} | |
bone.localScale = scale; | |
if (go != null) Undo.RegisterCreatedObjectUndo(go, "Create Bone"); | |
return true; | |
} | |
void ApplyBones(string customUndoName = null) { | |
bool forced = string.IsNullOrEmpty(customUndoName); | |
if (autoApply || forced) { | |
using (var so = new SerializedObject(target)) { | |
so.Update(); | |
var bonesProperty = so.FindProperty("m_Bones"); | |
bonesProperty.arraySize = bones.Length; | |
for (int i = 0; i < bones.Length; i++) { | |
var boneProperty = bonesProperty.GetArrayElementAtIndex(i); | |
boneProperty.objectReferenceValue = bones[i]; | |
} | |
so.ApplyModifiedProperties(); | |
} | |
UpdateDirty(false); | |
} else UpdateDirty(true); | |
shouldRefreshBoneCoverageMap = true; | |
} | |
void ReplaceBone(int index, Transform bone) { | |
bones[index] = bone; | |
shouldRefreshBoneCoverageMap = true; | |
} | |
static void DrawBoneGizmos(SceneView sceneView, Transform from, Transform to, Color color, Color parentColor) { | |
Handles.color = color; | |
var cameraPos = Handles.inverseMatrix.MultiplyPoint(sceneView.camera.transform.position); | |
var toPos = to.position; | |
var vector = cameraPos - toPos; | |
float scale = vector.magnitude * (from != null ? Vector3.Distance(from.position, toPos) : 0.5F) * 0.1F; | |
Handles.DrawWireDisc(toPos, vector.normalized, scale); | |
if (from == null) return; | |
Handles.color = parentColor; | |
var fromPos = from.position; | |
Handles.DrawLine(fromPos, toPos); | |
} | |
int[] GetSortedBoneIndecesByDepth() { | |
var depthMap = new Dictionary<Transform, int>(); | |
foreach (var bone in bones) { | |
if (bone == null) continue; | |
var depth = 0; | |
var parent = bone.parent; | |
while (parent != null) { | |
if (depthMap.TryGetValue(parent, out var parentDepth)) { | |
depth = parentDepth + 1; | |
break; | |
} | |
parent = parent.parent; | |
depth++; | |
} | |
depthMap[bone] = depth; | |
} | |
var boneWithIndex = new (Transform t, int i)[bones.Length]; | |
for (int i = 0; i < bones.Length; i++) boneWithIndex[i] = (bones[i], i); | |
Array.Sort(boneWithIndex, (l, r) => { | |
if (l.t == null || !depthMap.TryGetValue(l.t, out var lDepth)) lDepth = -1; | |
if (r.t == null || !depthMap.TryGetValue(r.t, out var rDepth)) rDepth = -1; | |
return lDepth != rDepth ? lDepth - rDepth : l.i - r.i; | |
}); | |
return Array.ConvertAll(boneWithIndex, x => x.i); | |
} | |
static Color GetBoneColor(Transform bone, float coverage) => | |
selectedBone == bone ? Handles.selectedColor : | |
coverage > 0 ? Color.Lerp(minCoverageBoneColor, maxCoverageBoneColor, Mathf.Sqrt(coverage / maxCoverage)) : | |
Handles.secondaryColor; | |
static GUIContent GetGUIContent(string title = null, string tooltip = null, GUIContent iconContent = null) { | |
if (tempContent == null) tempContent = new GUIContent(); | |
tempContent.text = title ?? string.Empty; | |
tempContent.tooltip = tooltip ?? string.Empty; | |
tempContent.image = iconContent?.image; | |
return tempContent; | |
} | |
static GUIContent GetWarningContent(string title = null, string tooltip = null) { | |
if (warningIcon == null) warningIcon = EditorGUIUtility.IconContent("console.warnicon.sml"); | |
return GetGUIContent(title, tooltip, warningIcon); | |
} | |
static GUIContent GetInfoContent(string title = null, string tooltip = null) { | |
if (infoIcon == null) infoIcon = EditorGUIUtility.IconContent("console.infoicon.sml"); | |
return GetGUIContent(title, tooltip, infoIcon); | |
} | |
string GetUniqueNameForBone(Transform bone, Transform parent = null) { | |
var newName = bone.name; | |
if (parent == null) parent = bone.parent; | |
if (parent != null) newName = GameObjectUtility.GetUniqueNameForSibling(parent, newName); | |
var existNames = new HashSet<string>(); | |
foreach (var otherBone in bones) | |
if (otherBone != null && otherBone != bone) | |
existNames.Add(otherBone.name); | |
if (existNames.Contains(newName)) { | |
var existNameArray = new string[existNames.Count]; | |
existNames.CopyTo(existNameArray); | |
newName = ObjectNames.GetUniqueName(existNameArray, newName); | |
} | |
return newName; | |
} | |
void ReassignBone(int index) { | |
var bone = bones[index]; | |
var go = new GameObject(bone.name); | |
var newBone = go.transform; | |
go.name = GetUniqueNameForBone(newBone, bone); | |
newBone.SetParent(bone, false); | |
Undo.RegisterCreatedObjectUndo(go, "Create Bone"); | |
ReplaceBone(index, newBone); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment