From c16f58bdc7bbd4f3939ab53587654b89a4dcd473 Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 19 Dec 2018 18:03:17 -0800 Subject: [PATCH] UX feedback changes --- .../Assets/Editor/AvatarExporter.cs | 162 +++++++++++++----- tools/unity-avatar-exporter/Assets/README.txt | 10 +- .../avatarExporter.unitypackage | Bin 7579 -> 8795 bytes 3 files changed, 126 insertions(+), 46 deletions(-) diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index 2e17b04643..1f3c11fc03 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -171,6 +171,13 @@ class AvatarExporter : MonoBehaviour { if (!SetJointMappingsAndParentNames()) { return; } + + //var textures = AssetDatabase.LoadAllAssetsAtPath(assetPath).Where(x => x.GetType() == typeof(Texture)); + var tests = AssetDatabase.LoadAllAssetsAtPath(assetPath); + Debug.Log("assetPath " + assetPath); + foreach (var test in tests) { + Debug.Log("test " + test.GetType()); + } string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); string hifiFolder = documentsFolder + "\\High Fidelity Projects"; @@ -178,13 +185,40 @@ class AvatarExporter : MonoBehaviour { bool copyModelToExport = false; string initialPath = Directory.Exists(hifiFolder) ? hifiFolder : documentsFolder; - // open file explorer defaulting to hifi folder in user documents to select target fst to update + // open file explorer defaulting to hifi projects folder in user documents to select target fst to update string exportFstPath = EditorUtility.OpenFilePanel("Select fst to update", initialPath, "fst"); if (exportFstPath.Length == 0) { // file selection cancelled return; } - string exportModelPath = Path.GetDirectoryName(exportFstPath) + "/" + assetName + ".fbx"; + exportFstPath = exportFstPath.Replace('/', '\\'); + // lookup the project name field from the fst file to update + string projectName = ""; + try { + string[] lines = File.ReadAllLines(exportFstPath); + foreach (string line in lines) { + if (line.StartsWith("name")) { + projectName = line.Substring(line.IndexOf("=") + 2); + break; + } + } + } catch { + EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + // delete existing fst file since we will write a new file + // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file + try { + File.Delete(exportFstPath); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath + + ". Please check the file and try again.", "Ok"); + return; + } + + string exportModelPath = Path.GetDirectoryName(exportFstPath) + "/" + assetName + ".fbx"; if (File.Exists(exportModelPath)) { // if the fbx in Unity Assets is newer than the fbx in the target export // folder or vice-versa then ask to replace the older fbx with the newer fbx @@ -198,7 +232,7 @@ class AvatarExporter : MonoBehaviour { if (option == 2) { // Cancel return; } - copyModelToExport = option == 0; + copyModelToExport = option == 0; // Yes } else if (assetModelWriteTime < targetModelWriteTime) { int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + @@ -235,26 +269,33 @@ class AvatarExporter : MonoBehaviour { if (option == 2) { // Cancel return; } - copyModelToExport = option == 0; + copyModelToExport = option == 0; // Yes } // delete any existing fbx if we agreed to overwrite it, and copy asset fbx over if (copyModelToExport) { if (File.Exists(exportModelPath)) { - File.Delete(exportModelPath); + try { + File.Delete(exportModelPath); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportModelPath + + ". Please check the file and try again.", "Ok"); + return; + } + } + try { + File.Copy(assetPath, exportModelPath); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to copy existing file " + assetPath + " to " + exportModelPath + + ". Please check the location and try again.", "Ok"); + return; } - File.Copy(assetPath, exportModelPath); } - - // delete any existing fst since we are re-exporting it - // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file - if (File.Exists(exportFstPath)) { - File.Delete(exportFstPath); - } - - WriteFST(exportFstPath); + + // write out a new fst file in place of the old file + WriteFST(exportFstPath, projectName); } else { // Export New Avatar menu option - // create High Fidelity folder in user documents folder if it doesn't exist + // create High Fidelity Projects folder in user documents folder if it doesn't exist if (!Directory.Exists(hifiFolder)) { Directory.CreateDirectory(hifiFolder); } @@ -265,13 +306,10 @@ class AvatarExporter : MonoBehaviour { } } - static void OnExportProjectWindowClose(string projectDirectory) { - // copy the fbx from the Unity Assets folder to the project directory, - // and then write out the fst file to the project directory + static void OnExportProjectWindowClose(string projectDirectory, string projectName) { + // copy the fbx from the Unity Assets folder to the project directory string exportModelPath = projectDirectory + assetName + ".fbx"; - string exportFstPath = projectDirectory + "avatar.fst"; File.Copy(assetPath, exportModelPath); - WriteFST(exportFstPath); // create empty Textures and Scripts folders in the project directory string texturesDirectory = projectDirectory + "\\textures"; @@ -279,8 +317,14 @@ class AvatarExporter : MonoBehaviour { Directory.CreateDirectory(texturesDirectory); Directory.CreateDirectory(scriptsDirectory); - // open File Explorer to the project directory once finished - System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); + // write out the avatar.fst file to the project directory + string exportFstPath = projectDirectory + "avatar.fst"; + WriteFST(exportFstPath, projectName); + + // remove any double slashes in texture directory path and warn user to copy external textures over + texturesDirectory = texturesDirectory.Replace("\\\\", "\\"); + EditorUtility.DisplayDialog("Warning", "If you are using any external textures with your model, " + + "please copy those textures to " + texturesDirectory, "Ok"); } static bool SetJointMappingsAndParentNames() { @@ -296,7 +340,7 @@ class AvatarExporter : MonoBehaviour { SetParentNames(assetGameObject.transform, userParentNames); DestroyImmediate(assetGameObject); - // store joint mappings only for joints that exist in hifi and verify missing joints + // store joint mappings only for joints that exist in hifi and verify missing required joints HumanBone[] boneMap = humanDescription.human; string chestUserBone = ""; string neckUserBone = ""; @@ -365,12 +409,18 @@ class AvatarExporter : MonoBehaviour { return true; } - static void WriteFST(string exportFstPath) { + static void WriteFST(string exportFstPath, string projectName) { userAbsoluteRotations.Clear(); - + // write out core fields to top of fst file - File.WriteAllText(exportFstPath, "name = " + assetName + "\ntype = body+head\nscale = 1\nfilename = " + - assetName + ".fbx\n" + "texdir = textures\n"); + try { + File.WriteAllText(exportFstPath, "name = " + projectName + "\ntype = body+head\nscale = 1\nfilename = " + + assetName + ".fbx\n" + "texdir = textures\n"); + } catch { + EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath + + ". Please check the location and try again.", "Ok"); + return; + } // write out joint mappings to fst file foreach (var jointMapping in userBoneToHumanoidMappings) { @@ -414,13 +464,16 @@ class AvatarExporter : MonoBehaviour { } } - // swap from left-handed (Unity) to right-handed (HiFi) coordinate system and write out joint rotation offset to fst + // swap from left-handed (Unity) to right-handed (HiFi) coordinates and write out joint rotation offset to fst if (outputJointName != "") { jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w); File.AppendAllText(exportFstPath, "jointRotationOffset = " + outputJointName + " = (" + jointOffset.x + ", " + jointOffset.y + ", " + jointOffset.z + ", " + jointOffset.w + ")\n"); } } + + // open File Explorer to the project directory once finished + System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); } static void SetParentNames(Transform modelBone, Dictionary parentNames) { @@ -443,42 +496,55 @@ class AvatarExporter : MonoBehaviour { } } -class ExportProjectWindow : EditorWindow { +class ExportProjectWindow : EditorWindow { + const int MIN_WIDTH = 450; + const int MIN_HEIGHT = 260; + const int BUTTON_FONT_SIZE = 16; + const int LABEL_FONT_SIZE = 16; + const int TEXT_FIELD_FONT_SIZE = 14; + const int ERROR_FONT_SIZE = 12; + string projectName = ""; string projectLocation = ""; string projectDirectory = ""; - string errorLabel = ""; + string errorLabel = "\n"; - public delegate void OnCloseDelegate(string projectDirectory); + public delegate void OnCloseDelegate(string projectDirectory, string projectName); OnCloseDelegate onCloseCallback; public void Init(string initialPath, OnCloseDelegate closeCallback) { + minSize = new Vector2(MIN_WIDTH, MIN_HEIGHT); + titleContent.text = "Export New Avatar"; projectLocation = initialPath; onCloseCallback = closeCallback; ShowUtility(); } void OnGUI() { + // define UI styles for all GUI elements to be created GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); - buttonStyle.fontSize = 20; + buttonStyle.fontSize = BUTTON_FONT_SIZE; GUIStyle labelStyle = new GUIStyle(GUI.skin.label); - labelStyle.fontSize = 16; - GUIStyle errorStyle = new GUIStyle(GUI.skin.label); - errorStyle.fontSize = 12; - errorStyle.normal.textColor = Color.red; + labelStyle.fontSize = LABEL_FONT_SIZE; GUIStyle textStyle = new GUIStyle(GUI.skin.textField); - textStyle.fontSize = 16; + textStyle.fontSize = TEXT_FIELD_FONT_SIZE; + GUIStyle errorStyle = new GUIStyle(GUI.skin.label); + errorStyle.fontSize = ERROR_FONT_SIZE; + errorStyle.normal.textColor = Color.red; GUILayout.Space(10); + // Project name label and input text field GUILayout.Label("Export project name:", labelStyle); projectName = GUILayout.TextField(projectName, textStyle); GUILayout.Space(10); + // Project location label and input text field GUILayout.Label("Export project location:", labelStyle); projectLocation = GUILayout.TextField(projectLocation, textStyle); + // Browse button to open folder explorer that starts at project location path and then updates project location if (GUILayout.Button("Browse", buttonStyle)) { string result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); if (result.Length > 0) { // folder selection not cancelled @@ -486,44 +552,54 @@ class ExportProjectWindow : EditorWindow { } } + // Red error label text to display any issues under text fields and Browse button GUILayout.Label(errorLabel, errorStyle); - GUILayout.Space(30); + GUILayout.Space(25); + // Export button which will verify project folder can actually be created + // before closing popup window and calling back to initiate the export bool export = false; if (GUILayout.Button("Export", buttonStyle)) { export = true; if (!CheckForErrors(true)) { Close(); - onCloseCallback(projectDirectory); + onCloseCallback(projectDirectory, projectName); } } + // Cancel button just closes the popup window without callback if (GUILayout.Button("Cancel", buttonStyle)) { Close(); } + // When either text field changes check for any errors if we didn't just check errors from clicking Export above if (GUI.changed && !export) { CheckForErrors(false); } } bool CheckForErrors(bool exporting) { - errorLabel = ""; + errorLabel = "\n"; // default to no error projectDirectory = projectLocation + "\\" + projectName + "\\"; if (projectName.Length > 0) { + // new project must have a unique folder name since the folder will be created for it if (Directory.Exists(projectDirectory)) { - errorLabel = "A folder with the name " + projectName + " already exists at that location.\nPlease choose a different project name or location."; + errorLabel = "A folder with the name " + projectName + + " already exists at that location.\nPlease choose a different project name or location."; return true; } } if (projectLocation.Length > 0) { + // before clicking Export we can verify that the project location at least starts with a drive if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { - errorLabel = "Project location is invalid. Please choose a different project location."; + errorLabel = "Project location is invalid. Please choose a different project location.\n"; return true; } } if (exporting) { + // when exporting, project name and location must both be defined, and project location must + // be valid and accessible (we attempt to create the project folder at this time to verify this) if (projectName.Length == 0) { errorLabel = "Please define a project name."; return true; @@ -534,7 +610,7 @@ class ExportProjectWindow : EditorWindow { try { Directory.CreateDirectory(projectDirectory); } catch { - errorLabel = "Project location is invalid. Please choose a different project location."; + errorLabel = "Project location is invalid. Please choose a different project location.\n"; return true; } } diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt index 7e6c6d4f48..034ec23982 100644 --- a/tools/unity-avatar-exporter/Assets/README.txt +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -1,11 +1,15 @@ To create a new avatar project: 1. Import your .fbx avatar model into Unity Assets (drag and drop file into Assets window or use Assets menu > Import New Assets). -2. Select the .fbx avatar that you imported in the Assets window, and in the Inspector window set the Animation Type to Humanoid and choose Apply. +2. Select the .fbx avatar that you imported in the Assets window, and in the Rig section of the Inspector window set the Animation Type to Humanoid and choose Apply. 3. With the .fbx avatar still selected, select High Fidelity menu > Export New Avatar. 4. Select a name for your avatar project (this will be used to create a directory with that name), as well as the target location for your project folder. 5. Once it is exported, your project directory will open in File Explorer. To update an existing avatar project: 1. Select the existing .fbx avatar in the Assets window that you would like to re-export. -2. Select High Fidelity menu > Update Avatar and choose the .fst file you would like to update. -3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file. \ No newline at end of file +2. Select High Fidelity menu > Update Existing Avatar and choose the .fst file you would like to update. +3. If the .fbx file in your Unity Assets folder is newer than the existing .fbx file in your avatar project or vice-versa, you will be prompted if you wish to replace the older file with the newer file. +4. Once it is updated, your project directory will open in File Explorer. + +* WARNING * +If you are using any external textures as part of your .fbx model, be sure they are copied into the textures folder that is created in the project folder after exporting a new avatar. diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 5327c0beed0952d24fb2b86cef3e06922058ed60..dfebaa096138bda98ddad5720b4bc3f718294d2e 100644 GIT binary patch literal 8795 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 RJUsu+=MVRhi9!Ih007!G+#LV_ literal 7579 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 xw-dB}{}+C5<^K1-?vKLKD12$TS#000tMlal}d