diff --git a/Editor/Scripts/GLTFImporterInspector.cs b/Editor/Scripts/GLTFImporterInspector.cs index 47a242710..1d8279fa1 100644 --- a/Editor/Scripts/GLTFImporterInspector.cs +++ b/Editor/Scripts/GLTFImporterInspector.cs @@ -23,12 +23,86 @@ namespace UnityGLTF [CanEditMultipleObjects] internal class GLTFImporterInspector : UnityGLTFTabbedEditor { + static Texture BoneAssignmentDotIcon; + static Texture BoneAssignmentDotFrameIcon; + static Texture BoneAssignmentDotFrameDottedIcon; + + static string[] boneGroupTabs = { "Body", "Head", "Left Hand", "Right Hand" }; + static new Dictionary boneGroups = new() + { + { "Hips", "Body" }, + { "Spine", "Body" }, + { "Chest", "Body" }, + { "UpperChest", "Body" }, + { "Neck", "Body" }, + { "LeftUpperLeg", "Body" }, + { "RightUpperLeg", "Body" }, + { "LeftLowerLeg", "Body" }, + { "RightLowerLeg", "Body" }, + { "LeftFoot", "Body" }, + { "RightFoot", "Body" }, + { "LeftToes", "Body" }, + { "RightToes", "Body" }, + { "LeftShoulder", "Body" }, + { "LeftUpperArm", "Body" }, + { "LeftLowerArm", "Body" }, + { "LeftHand", "Body" }, + { "RightShoulder", "Body" }, + { "RightUpperArm", "Body" }, + { "RightLowerArm", "Body" }, + { "RightHand", "Body" }, + + { "Head", "Head" }, + { "Jaw", "Head" }, + { "LeftEye", "Head" }, + { "RightEye", "Head" }, + + { "Left Thumb Proximal", "Left Hand" }, + { "Left Thumb Intermediate", "Left Hand" }, + { "Left Thumb Distal", "Left Hand" }, + { "Left Index Proximal", "Left Hand" }, + { "Left Index Intermediate", "Left Hand" }, + { "Left Index Distal", "Left Hand" }, + { "Left Middle Proximal", "Left Hand" }, + { "Left Middle Intermediate", "Left Hand" }, + { "Left Middle Distal", "Left Hand" }, + { "Left Ring Proximal", "Left Hand" }, + { "Left Ring Intermediate", "Left Hand" }, + { "Left Ring Distal", "Left Hand" }, + { "Left Little Proximal", "Left Hand" }, + { "Left Little Intermediate", "Left Hand" }, + { "Left Little Distal", "Left Hand" }, + + { "Right Thumb Proximal", "Right Hand" }, + { "Right Thumb Intermediate", "Right Hand" }, + { "Right Thumb Distal", "Right Hand" }, + { "Right Index Proximal", "Right Hand" }, + { "Right Index Intermediate", "Right Hand" }, + { "Right Index Distal", "Right Hand" }, + { "Right Middle Proximal", "Right Hand" }, + { "Right Middle Intermediate", "Right Hand" }, + { "Right Middle Distal", "Right Hand" }, + { "Right Ring Proximal", "Right Hand" }, + { "Right Ring Intermediate", "Right Hand" }, + { "Right Ring Distal", "Right Hand" }, + { "Right Little Proximal", "Right Hand" }, + { "Right Little Intermediate", "Right Hand" }, + { "Right Little Distal", "Right Hand" }, + }; + private string[] _importNormalsNames; - + GLTFImporter importer; + readonly Dictionary boneTransforms = new(); + readonly Dictionary assignedBoneNames = new(); + Avatar avatar; + int selectedBoneGroupTab = 0; + public override void OnEnable() { if (!this) return; + importer = target as GLTFImporter; + var m_HasSceneData = serializedObject.FindProperty(nameof(GLTFImporter.m_HasSceneData)); if (m_HasSceneData.boolValue) AddTab(new GLTFAssetImporterTab(this, "Model", ModelInspectorGUI)); @@ -43,34 +117,64 @@ public override void OnEnable() AddTab(new GLTFAssetImporterTab(this, "Extensions", ExtensionInspectorGUI)); AddTab(new GLTFAssetImporterTab(this, "Info", AssetInfoInspectorGUI)); + BoneAssignmentDotIcon = (Texture)EditorGUIUtility.Load("DotFill"); + BoneAssignmentDotFrameIcon = (Texture)EditorGUIUtility.Load("DotFrame"); + BoneAssignmentDotFrameDottedIcon = (Texture)EditorGUIUtility.Load("DotFrameDotted"); + + var animationMethod = serializedObject.FindProperty(nameof(GLTFImporter._importAnimations)); + if (animationMethod.enumValueIndex == (int)AnimationMethod.MecanimHumanoid) + { + avatar = AssetDatabase.LoadAssetAtPath(importer.assetPath); + + if (avatar && avatar.isHuman && avatar.isValid) PopulateBoneInfo(); + } + base.OnEnable(); } - public override void OnInspectorGUI() + void PopulateBoneInfo() + { + var modelAsset = AssetDatabase.LoadAssetAtPath(importer.assetPath); + boneTransforms.Clear(); + assignedBoneNames.Clear(); + + if (modelAsset) + { + foreach (var transform in modelAsset.GetComponentsInChildren(true)) + { + boneTransforms[transform.name] = transform; + } + + foreach (var bone in avatar.humanDescription.human) + { + assignedBoneNames[bone.humanName] = bone.boneName; + } + } + } + + public override void OnInspectorGUI() { - var t = target as GLTFImporter; - TextureWarningsGUI(t); + TextureWarningsGUI(); EditorGUILayout.Space(); base.OnInspectorGUI(); } - private void TextureWarningsGUI(GLTFImporter t) + private void TextureWarningsGUI() { - if (!t) return; - if (!GLTFImporterHelper.TextureImportSettingsAreCorrect(t)) + if (!importer) return; + if (!GLTFImporterHelper.TextureImportSettingsAreCorrect(importer)) { EditorGUILayout.HelpBox("Some Textures have incorrect linear/sRGB settings. Results might be incorrect.", MessageType.Warning); if (GUILayout.Button("Fix All")) { - GLTFImporterHelper.FixTextureImportSettings(t); + GLTFImporterHelper.FixTextureImportSettings(importer); } } } private void ModelInspectorGUI() { - var t = target as GLTFImporter; - if (!t) return; + if (!importer) return; // serializedObject.Update(); if (_importNormalsNames == null) @@ -84,13 +188,13 @@ private void ModelInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(GLTFImporter._scaleFactor))); EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(GLTFImporter._importCamera))); EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(GLTFImporter._deduplicateResources))); - if (t._deduplicatedStatistics != null && t._deduplicateResources != DeduplicateOptions.None) + if (importer._deduplicatedStatistics != null && importer._deduplicateResources != DeduplicateOptions.None) { - var stats = t._deduplicatedStatistics; - var meshText = t._deduplicateResources.HasFlag(DeduplicateOptions.Meshes) + var stats = importer._deduplicatedStatistics; + var meshText = importer._deduplicateResources.HasFlag(DeduplicateOptions.Meshes) ? $"Removed {stats.MeshesRemoved} Meshes ({stats.meshCountAfter}/{stats.meshCountBefore}). " : ""; - var textureText = t._deduplicateResources.HasFlag(DeduplicateOptions.Textures) + var textureText = importer._deduplicateResources.HasFlag(DeduplicateOptions.Textures) ? $"Removed {stats.TexturesRemoved} Textures ({stats.textureCountAfter}/{stats.textureCountBefore}). " : ""; EditorGUILayout.LabelField(" ", meshText+textureText, EditorStyles.miniLabel); @@ -103,7 +207,7 @@ private void ModelInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(GLTFImporter._readWriteEnabled)), new GUIContent("Read/Write")); #pragma warning disable 0618 - if (t._generateColliders) + if (importer._generateColliders) { serializedObject.FindProperty(nameof(GLTFImporter._addColliders)).enumValueIndex = (int)GLTFSceneImporter.ColliderType.Mesh; @@ -199,8 +303,7 @@ private static void CopyLoopSettingsToClips(SerializedProperty animations, bool private void AnimationInspectorGUI() { - var t = target as GLTFImporter; - if (!t) return; + if (!importer) return; var hasAnimationData = serializedObject.FindProperty(nameof(GLTFImporter.m_HasAnimationData)).boolValue; @@ -208,10 +311,10 @@ private void AnimationInspectorGUI() { EditorGUILayout.HelpBox("File doesn't contain animation data.", MessageType.None); } - - var anim = serializedObject.FindProperty(nameof(GLTFImporter._importAnimations)); - EditorGUILayout.PropertyField(anim, new GUIContent("Animation Type")); - if (anim.enumValueIndex == (int)AnimationMethod.MecanimHumanoid) + + var animationMethod = serializedObject.FindProperty(nameof(GLTFImporter._importAnimations)); + EditorGUILayout.PropertyField(animationMethod, new GUIContent("Animation Type")); + if (animationMethod.enumValueIndex == (int)AnimationMethod.MecanimHumanoid) { var flip = serializedObject.FindProperty(nameof(GLTFImporter._mecanimHumanoidFlip)); EditorGUI.indentLevel++; @@ -220,7 +323,7 @@ private void AnimationInspectorGUI() } var animations = serializedObject.FindProperty(GLTFImporter.AnimationsPropertyName); - if (hasAnimationData && anim.enumValueIndex > 0) + if (hasAnimationData && animationMethod.enumValueIndex > 0) { var loopTime = serializedObject.FindProperty(nameof(GLTFImporter._animationLoopTime)); var loopPose = serializedObject.FindProperty(nameof(GLTFImporter._animationLoopPose)); @@ -254,18 +357,73 @@ private void AnimationInspectorGUI() AnimationClipListGUI(animations); } - // warn if Humanoid rig import has failed - if (!HasModified() && anim.enumValueIndex == (int) AnimationMethod.MecanimHumanoid && !AssetDatabase.LoadAssetAtPath(t.assetPath)) + if (animationMethod.enumValueIndex == (int)AnimationMethod.MecanimHumanoid) { - EditorGUILayout.Separator(); - EditorGUILayout.HelpBox("The model doesn't contain a valid Humanoid rig. See the console for more information.", MessageType.Error); + // List all bones and any assigned gameobjects. + if (avatar && avatar.isHuman && avatar.isValid) + { + var humanBones = avatar.humanDescription.human; + var allMecanimBonesCount = HumanTrait.BoneName.Length; + EditorGUILayout.Separator(); + EditorGUILayout.LabelField("Avatar Bones", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Assigned Bones:", humanBones.Length + "/" + allMecanimBonesCount); + + EditorGUILayout.BeginHorizontal(); + var legendIconRect = GUILayoutUtility.GetRect(18, 18, GUILayout.Width(18), GUILayout.Height(18)); + GUI.DrawTexture(legendIconRect, BoneAssignmentDotFrameDottedIcon, ScaleMode.ScaleToFit, true); + GUILayout.Label("Optional Bone", EditorStyles.miniLabel); + EditorGUILayout.EndHorizontal(); + + selectedBoneGroupTab = GUILayout.Toolbar(selectedBoneGroupTab, boneGroupTabs); + + EditorGUI.indentLevel++; + + for (var i = 0; i < allMecanimBonesCount; i++) + { + var mecanimBoneName = HumanTrait.BoneName[i]; + if (boneGroupTabs[selectedBoneGroupTab] != boneGroups[mecanimBoneName]) continue; + + assignedBoneNames.TryGetValue(mecanimBoneName, out string assignedBoneName); + Transform transform = null; + if (!string.IsNullOrEmpty(assignedBoneName)) + { + boneTransforms.TryGetValue(assignedBoneName, out transform); + } + + EditorGUILayout.BeginHorizontal(); + + var iconRect = GUILayoutUtility.GetRect(18, 18, GUILayout.Width(18), GUILayout.Height(18)); + var frameIcon = HumanTrait.RequiredBone(i) ? BoneAssignmentDotFrameIcon : BoneAssignmentDotFrameDottedIcon; + + if (transform) + { + var originalColor = GUI.color; + GUI.color = Color.green; + GUI.DrawTexture(iconRect, frameIcon, ScaleMode.ScaleToFit, true); + GUI.DrawTexture(iconRect, BoneAssignmentDotIcon, ScaleMode.ScaleToFit, true); + GUI.color = originalColor; + } + else GUI.DrawTexture(iconRect, frameIcon, ScaleMode.ScaleToFit, true); + + GUILayout.Label(mecanimBoneName, GUILayout.Width(130)); + + EditorGUI.BeginDisabledGroup(true); + EditorGUILayout.ObjectField(transform, typeof(Transform), true); + EditorGUI.EndDisabledGroup(); + + EditorGUILayout.EndHorizontal(); + } + + EditorGUI.indentLevel--; + } + // warn if Humanoid rig import has failed + else EditorGUILayout.HelpBox("The model doesn't contain a valid Humanoid rig. See the console for more information.", MessageType.Error); } } private void MaterialInspectorGUI() { - var t = target as GLTFImporter; - if (!t) return; + if (!importer) return; var importMaterialsProp = serializedObject.FindProperty(nameof(GLTFImporter._importMaterials)); EditorGUILayout.PropertyField(importMaterialsProp); @@ -280,7 +438,7 @@ private void MaterialInspectorGUI() var importedMaterials = serializedObject.FindProperty("m_Materials"); if (importedMaterials.arraySize > 0) { - RemappingUI(t, importedMaterials, "Materials", ".mat"); + RemappingUI(importedMaterials, "Materials", ".mat"); } // There's a bunch of known edge cases with texture remapping that can go wrong, @@ -291,7 +449,7 @@ private void MaterialInspectorGUI() var importedTextures = serializedObject.FindProperty("m_Textures"); if (EnableTextureRemapping && importedTextures.arraySize > 0) { - RemappingUI(t, importedTextures, "Textures", ".asset"); + RemappingUI(importedTextures, "Textures", ".asset"); } var identifierProp = serializedObject.FindProperty(nameof(GLTFImporter._useSceneNameIdentifier)); @@ -322,14 +480,14 @@ private void MaterialInspectorGUI() EditorGUILayout.Separator(); } - private void RemappingUI(GLTFImporter t, SerializedProperty importedData, string subDirectoryName, string fileExtension) where T: UnityEngine.Object + private void RemappingUI(SerializedProperty importedData, string subDirectoryName, string fileExtension) where T: UnityEngine.Object { // extract and remap materials if (importedData != null && importedData.serializedObject != null) { EditorGUI.indentLevel++; - var externalObjectMap = t.GetExternalObjectMap(); + var externalObjectMap = importer.GetExternalObjectMap(); // TODO this also counts old remaps that are not used anymore var remapCount = externalObjectMap.Values.Count(x => x is T); @@ -341,7 +499,7 @@ void ExtractAssets(T[] subAssets, bool importImmediately) { if (!subAsset) return; var filename = SanitizePath(subAsset.name); - var dirName = Path.GetDirectoryName(t.assetPath) + "/" + subDirectoryName; + var dirName = Path.GetDirectoryName(importer.assetPath) + "/" + subDirectoryName; if (!Directory.Exists(dirName)) Directory.CreateDirectory(dirName); var destinationPath = dirName + "/" + filename + fileExtension; @@ -363,7 +521,7 @@ void ExtractAsset(T subAsset, bool importImmediately) { if (!subAsset) return; var filename = SanitizePath(subAsset.name); - var dirName = Path.GetDirectoryName(t.assetPath) + "/" + subDirectoryName; + var dirName = Path.GetDirectoryName(importer.assetPath) + "/" + subDirectoryName; if (!Directory.Exists(dirName)) Directory.CreateDirectory(dirName); var destinationPath = dirName + "/" + filename + fileExtension; @@ -397,14 +555,14 @@ void ExtractAsset(T subAsset, bool importImmediately) { var mat = importedData.GetArrayElementAtIndex(i).objectReferenceValue as T; if (!mat) continue; - t.RemoveRemap(new AssetImporter.SourceAssetIdentifier(mat)); + importer.RemoveRemap(new AssetImporter.SourceAssetIdentifier(mat)); } // also remove all old remaps var oldRemaps = externalObjectMap.Where(x => x.Value is T).ToList(); foreach (var oldRemap in oldRemaps) { - t.RemoveRemap(oldRemap.Key); + importer.RemoveRemap(oldRemap.Key); } } } @@ -442,9 +600,9 @@ void ExtractAsset(T subAsset, bool importImmediately) if (EditorGUI.EndChangeCheck()) { if (newObj && newObj != mat) - t.AddRemap(id, newObj); + importer.AddRemap(id, newObj); else - t.RemoveRemap(id); + importer.RemoveRemap(id); } if (!remap) @@ -459,7 +617,7 @@ void ExtractAsset(T subAsset, bool importImmediately) { if (GUILayout.Button("Restore", GUILayout.Width(60))) { - t.RemoveRemap(id); + importer.RemoveRemap(id); #if UNITY_2022_2_OR_NEWER SaveChanges(); #else @@ -483,8 +641,7 @@ void ExtractAsset(T subAsset, bool importImmediately) private static GUIStyle _richTextWordWrap; private void AssetInfoInspectorGUI() { - var t = target as GLTFImporter; - if (!t) return; + if (!importer) return; var assetProp = serializedObject.FindProperty(nameof(GLTFImporter._gltfAsset)); if (assetProp == null) return; @@ -497,15 +654,15 @@ private void AssetInfoInspectorGUI() _richTextWordWrap = style; } - if (string.IsNullOrEmpty(t._gltfAsset)) + if (string.IsNullOrEmpty(importer._gltfAsset)) { EditorGUILayout.LabelField("No asset information included in file", _richTextWordWrap); return; } EditorGUILayout.Space(); - var rect = GUILayoutUtility.GetRect(new GUIContent(t._gltfAsset), _richTextWordWrap); - EditorGUI.SelectableLabel(rect, t._gltfAsset, _richTextWordWrap); + var rect = GUILayoutUtility.GetRect(new GUIContent(importer._gltfAsset), _richTextWordWrap); + EditorGUI.SelectableLabel(rect, importer._gltfAsset, _richTextWordWrap); EditorGUILayout.Space(); EditorGUI.BeginDisabledGroup(true); @@ -515,8 +672,7 @@ private void AssetInfoInspectorGUI() private void ExtensionInspectorGUI() { - var t = target as GLTFImporter; - if (!t) return; + if (!importer) return; EditorGUILayout.PropertyField(serializedObject.FindProperty(nameof(GLTFImporter._extensions)), new GUIContent("Extensions in file")); EditorGUI.EndDisabledGroup(); @@ -524,7 +680,7 @@ private void ExtensionInspectorGUI() // TODO add list of supported extensions and links to docs // Gather list of all plugins var registeredPlugins = GLTFSettings.GetDefaultSettings().ImportPlugins; - var overridePlugins = t._importPlugins; + var overridePlugins = importer._importPlugins; EditorGUILayout.Space(); EditorGUILayout.LabelField("Available Import Plugins", EditorStyles.boldLabel); @@ -568,10 +724,10 @@ private void ExtensionInspectorGUI() overrideEnabled = true, }; overridePlugin = newPlugin; - t._importPlugins.Add(newPlugin); + importer._importPlugins.Add(newPlugin); } } - EditorUtility.SetDirty(t); + EditorUtility.SetDirty(importer); } EditorGUI.BeginDisabledGroup(!hasOverride); var currentlyEnabled = (overridePlugin != null && overridePlugin.overrideEnabled) ? overridePlugin.enabled : plugin.EnabledByDefault; @@ -580,7 +736,7 @@ private void ExtensionInspectorGUI() { currentlyEnabled = enabled; overridePlugin.enabled = enabled; - EditorUtility.SetDirty(t); + EditorUtility.SetDirty(importer); } EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(false); @@ -590,7 +746,7 @@ private void ExtensionInspectorGUI() EditorGUILayout.EndHorizontal(); // This assumes that the display name of a Plugin matches what extension it operates on. // It's not correct for Plugins that operate on multiple extensions, or none at all. - if (hasWarning && t._extensions?.Find(x => x.name == plugin.DisplayName) != null) + if (hasWarning && importer._extensions?.Find(x => x.name == plugin.DisplayName) != null) { EditorGUILayout.HelpBox(plugin.Warning, MessageType.Warning); editorCache.TryGetValue(plugin.GetType(), out var editor); diff --git a/Editor/Scripts/Internal/HumanoidSetup.cs b/Editor/Scripts/Internal/HumanoidSetup.cs index 76d950fcc..a93ac2e18 100644 --- a/Editor/Scripts/Internal/HumanoidSetup.cs +++ b/Editor/Scripts/Internal/HumanoidSetup.cs @@ -24,7 +24,7 @@ internal static Avatar AddAvatarToGameObject(GameObject gameObject, bool flipFor gameObject.transform.rotation *= Quaternion.Euler(0,180,0); Avatar avatar = AvatarBuilder.BuildHumanAvatar(gameObject, description); - avatar.name = "Avatar"; + avatar.name = gameObject.name+"Avatar"; if (flipForward) gameObject.transform.rotation = previousRotation;