diff --git a/.gitignore b/.gitignore index 09b58d71ef..f3f6954974 100644 --- a/.gitignore +++ b/.gitignore @@ -100,3 +100,4 @@ tools/jsdoc/package-lock.json tools/unity-avatar-exporter/Library tools/unity-avatar-exporter/Packages tools/unity-avatar-exporter/ProjectSettings +tools/unity-avatar-exporter/Temp \ No newline at end of file diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs index b62f51f4e8..2e17b04643 100644 --- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -12,8 +12,8 @@ using System; using System.IO; using System.Collections.Generic; -public class AvatarExporter : MonoBehaviour { - public static readonly Dictionary HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary { +class AvatarExporter : MonoBehaviour { + static readonly Dictionary HUMANOID_TO_HIFI_JOINT_NAME = new Dictionary { {"Chest", "Spine1"}, {"Head", "Head"}, {"Hips", "Hips"}, @@ -70,7 +70,7 @@ public class AvatarExporter : MonoBehaviour { {"UpperChest", "Spine2"}, }; - public static readonly Dictionary referenceAbsoluteRotations = new Dictionary { + static readonly Dictionary referenceAbsoluteRotations = new Dictionary { {"Head", new Quaternion(-2.509889e-9f, -3.379446e-12f, 2.306033e-13f, 1f)}, {"Hips", new Quaternion(-3.043941e-10f, -1.573706e-7f, 5.112975e-6f, 1f)}, {"LeftHandIndex3", new Quaternion(-0.5086057f, 0.4908088f, -0.4912299f, -0.5090388f)}, @@ -127,55 +127,176 @@ public class AvatarExporter : MonoBehaviour { {"Spine2", new Quaternion(-0.0824653f, 1.25274e-7f, -6.75759e-6f, 0.996594f)}, }; - public static Dictionary userBoneToHumanoidMappings = new Dictionary(); - public static Dictionary userParentNames = new Dictionary(); - public static Dictionary userAbsoluteRotations = new Dictionary(); + static Dictionary userBoneToHumanoidMappings = new Dictionary(); + static Dictionary userParentNames = new Dictionary(); + static Dictionary userAbsoluteRotations = new Dictionary(); + + static string assetPath = ""; + static string assetName = ""; + static HumanDescription humanDescription; [MenuItem("High Fidelity/Export New Avatar")] - public static void ExportNewAvatar() { + static void ExportNewAvatar() { ExportSelectedAvatar(false); } - [MenuItem("High Fidelity/Update Avatar")] - public static void UpdateAvatar() { + [MenuItem("High Fidelity/Update Existing Avatar")] + static void UpdateAvatar() { ExportSelectedAvatar(true); } - public static void ExportSelectedAvatar(bool updateAvatar) { + static void ExportSelectedAvatar(bool updateAvatar) { string[] guids = Selection.assetGUIDs; if (guids.Length != 1) { if (guids.Length == 0) { - EditorUtility.DisplayDialog("Error", "Please select an asset to export", "Ok"); + EditorUtility.DisplayDialog("Error", "Please select an asset to export.", "Ok"); } else { - EditorUtility.DisplayDialog("Error", "Please select a single asset to export", "Ok"); + EditorUtility.DisplayDialog("Error", "Please select a single asset to export.", "Ok"); } return; } - string assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); - ModelImporter importer = ModelImporter.GetAtPath(assetPath) as ModelImporter; - if (assetPath.LastIndexOf(".fbx") == -1 || importer == null) { - EditorUtility.DisplayDialog("Error", "Please select an .fbx model asset to export", "Ok"); + assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); + assetName = Path.GetFileNameWithoutExtension(assetPath); + ModelImporter modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; + if (assetPath.LastIndexOf(".fbx") == -1 || modelImporter == null) { + EditorUtility.DisplayDialog("Error", "Please select an .fbx model asset to export.", "Ok"); return; } - if (importer.animationType != ModelImporterAnimationType.Human) { - EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid", "Ok"); + if (modelImporter.animationType != ModelImporterAnimationType.Human) { + EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid in the Rig section of it's Inspector window.", "Ok"); return; } + + humanDescription = modelImporter.humanDescription; + if (!SetJointMappingsAndParentNames()) { + return; + } + + string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); + string hifiFolder = documentsFolder + "\\High Fidelity Projects"; + if (updateAvatar) { // Update Existing Avatar menu option + 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 + 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"; + + 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 + DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); + DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); + if (assetModelWriteTime > targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + assetName + + ".fbx model in the Unity Assets folder is newer than the " + exportModelPath + + " model.\n\nDo you want to replace the older .fbx with the newer .fbx?", + "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; + } else if (assetModelWriteTime < targetModelWriteTime) { + int option = EditorUtility.DisplayDialogComplex("Error", "The " + exportModelPath + + " model is newer than the " + assetName + ".fbx model in the Unity Assets folder." + + "\n\nDo you want to replace the older .fbx with the newer .fbx and re-import it?", + "Yes", "No" , "Cancel"); + if (option == 2) { // Cancel + return; + } else if (option == 0) { // Yes - copy model to Unity project + // delete existing fbx and associated meta file in Unity Assets + File.Delete(assetPath); + File.Delete(assetPath + ".meta"); + AssetDatabase.Refresh(); + + // copy the fbx from the project folder to Unity Assets and import it + File.Copy(exportModelPath, assetPath); + AssetDatabase.ImportAsset(assetPath); + + // set model to Humanoid animation type and force another refresh on it to process Humanoid + modelImporter = ModelImporter.GetAtPath(assetPath) as ModelImporter; + modelImporter.animationType = ModelImporterAnimationType.Human; + EditorUtility.SetDirty(modelImporter); + modelImporter.SaveAndReimport(); + humanDescription = modelImporter.humanDescription; + + // redo joint mappings and parent names due to the fbx change + SetJointMappingsAndParentNames(); + } + } + } else { + // if no matching fbx exists in the target export folder then ask to copy fbx over + int option = EditorUtility.DisplayDialogComplex("Error", "There is no existing " + exportModelPath + + " model.\n\nDo you want to copy over the " + assetName + + ".fbx model from the Unity Assets folder?", "Yes", "No", "Cancel"); + if (option == 2) { // Cancel + return; + } + copyModelToExport = option == 0; + } + + // delete any existing fbx if we agreed to overwrite it, and copy asset fbx over + if (copyModelToExport) { + if (File.Exists(exportModelPath)) { + File.Delete(exportModelPath); + } + 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); + } else { // Export New Avatar menu option + // create High Fidelity folder in user documents folder if it doesn't exist + if (!Directory.Exists(hifiFolder)) { + Directory.CreateDirectory(hifiFolder); + } + + // open a popup window to enter new export project name and project location + ExportProjectWindow window = ScriptableObject.CreateInstance(); + window.Init(hifiFolder, OnExportProjectWindowClose); + } + } + + 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 + string exportModelPath = projectDirectory + assetName + ".fbx"; + string exportFstPath = projectDirectory + "avatar.fst"; + File.Copy(assetPath, exportModelPath); + WriteFST(exportFstPath); - userBoneToHumanoidMappings.Clear(); + // create empty Textures and Scripts folders in the project directory + string texturesDirectory = projectDirectory + "\\textures"; + string scriptsDirectory = projectDirectory + "\\scripts"; + Directory.CreateDirectory(texturesDirectory); + Directory.CreateDirectory(scriptsDirectory); + + // open File Explorer to the project directory once finished + System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath); + } + + static bool SetJointMappingsAndParentNames() { userParentNames.Clear(); - userAbsoluteRotations.Clear(); + userBoneToHumanoidMappings.Clear(); // instantiate a game object of the user avatar to save out bone parents then destroy it UnityEngine.Object avatarResource = AssetDatabase.LoadAssetAtPath(assetPath, typeof(UnityEngine.Object)); - if (avatarResource) { - GameObject assetGameObject = (GameObject)Instantiate(avatarResource); - SetParentNames(assetGameObject.transform, userParentNames); - DestroyImmediate(assetGameObject); + if (!avatarResource) { + return false; } + GameObject assetGameObject = (GameObject)Instantiate(avatarResource); + SetParentNames(assetGameObject.transform, userParentNames); + DestroyImmediate(assetGameObject); // store joint mappings only for joints that exist in hifi and verify missing joints - HumanDescription humanDescription = importer.humanDescription; HumanBone[] boneMap = humanDescription.human; string chestUserBone = ""; string neckUserBone = ""; @@ -195,11 +316,11 @@ public class AvatarExporter : MonoBehaviour { } if (!userBoneToHumanoidMappings.ContainsValue("Hips")) { EditorUtility.DisplayDialog("Error", "There is no Hips bone in selected avatar", "Ok"); - return; + return false; } if (!userBoneToHumanoidMappings.ContainsValue("Spine")) { EditorUtility.DisplayDialog("Error", "There is no Spine bone in selected avatar", "Ok"); - return; + return false; } if (!userBoneToHumanoidMappings.ContainsValue("Chest")) { // check to see if there is a child of Spine that could be mapped to Chest @@ -222,7 +343,7 @@ public class AvatarExporter : MonoBehaviour { chestUserBone = spineChild; } else { EditorUtility.DisplayDialog("Error", "There is no Chest bone in selected avatar", "Ok"); - return; + return false; } } if (!userBoneToHumanoidMappings.ContainsValue("UpperChest")) { @@ -240,66 +361,13 @@ public class AvatarExporter : MonoBehaviour { userBoneToHumanoidMappings[chestUserBone] = "UpperChest"; } } - - bool copyModelToExport = false; - string exportFstPath, exportModelPath; - string assetName = Path.GetFileNameWithoutExtension(assetPath); - string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); - if (updateAvatar) { - // open file explorer defaulting to user documents folder to select target fst to update - exportFstPath = EditorUtility.OpenFilePanel("Select fst to update", documentsFolder, "fst"); - if (exportFstPath.Length == 0) { // file selection cancelled - return; - } - exportModelPath = Path.GetDirectoryName(exportFstPath) + "/" + assetName + ".fbx"; - - if (File.Exists(exportModelPath)) { - // if the fbx in Unity Assets/Resources is newer than the fbx in the - // target export folder or vice-versa then ask to copy fbx over - DateTime assetModelWriteTime = File.GetLastWriteTime(assetPath); - DateTime targetModelWriteTime = File.GetLastWriteTime(exportModelPath); - if (assetModelWriteTime > targetModelWriteTime) { - copyModelToExport = EditorUtility.DisplayDialog("Error", "The " + assetName + - ".fbx model in the Unity Assets/Resources folder is newer than the " + exportModelPath + - " model. Do you want to copy the newer .fbx model over?" , "Yes", "No"); - } else if (assetModelWriteTime < targetModelWriteTime) { - bool copyModelToUnity = EditorUtility.DisplayDialog("Error", "The " + exportModelPath + - " model is newer than the " + assetName + - ".fbx model in the Unity Assets/Resources folder. Do you want to copy the newer .fbx model over?", - "Yes", "No"); - if (copyModelToUnity) { - File.Delete(assetPath); - File.Copy(exportModelPath, assetPath); - } - } - } else { - // if no matching fbx exists in the target export folder then ask to copy fbx over - copyModelToExport = EditorUtility.DisplayDialog("Error", "There is no existing " + exportModelPath + - " model. Do you want to copy over the " + assetName + - ".fbx model from the Unity Assets/Resources folder?" , "Yes", "No"); - } - } else { - // open folder explorer defaulting to user documents folder to select target folder to export fst and fbx to - if (!SelectExportFolder(assetName, documentsFolder, out exportFstPath, out exportModelPath)) { - return; - } - copyModelToExport = true; - } - - // delete any existing fbx since we would have agreed to overwrite it, and copy asset fbx over - if (copyModelToExport) { - if (File.Exists(exportModelPath)) { - File.Delete(exportModelPath); - } - File.Copy(assetPath, exportModelPath); - } - - // delete any existing fst since we agreed to overwrite it or are updating it - // TODO: should updating fst only rewrite joint mappings and joint rotation offsets? - if (File.Exists(exportFstPath)) { - File.Delete(exportFstPath); - } + return true; + } + + static void WriteFST(string exportFstPath) { + 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"); @@ -310,6 +378,7 @@ public class AvatarExporter : MonoBehaviour { File.AppendAllText(exportFstPath, "jointMap = " + hifiJointName + " = " + jointMapping.Key + "\n"); } + // calculate and write out joint rotation offsets to fst file SkeletonBone[] skeletonMap = humanDescription.skeleton; foreach (SkeletonBone userBone in skeletonMap) { string userBoneName = userBone.name; @@ -353,37 +422,8 @@ public class AvatarExporter : MonoBehaviour { } } } - - public static bool SelectExportFolder(string assetName, string initialPath, out string fstPath, out string modelPath) { - string selectedPath = EditorUtility.OpenFolderPanel("Select export location", initialPath, ""); - if (selectedPath.Length == 0) { // folder selection cancelled - fstPath = ""; - modelPath = ""; - return false; - } - fstPath = selectedPath + "/" + assetName + ".fst"; - modelPath = selectedPath + "/" + assetName + ".fbx"; - bool fstExists = File.Exists(fstPath); - bool modelExists = File.Exists(modelPath); - if (fstExists || modelExists) { - string overwriteMessage; - if (fstExists && modelExists) { - overwriteMessage = assetName + ".fst and " + assetName + - ".fbx already exist here, would you like to overwrite them?"; - } else if (fstExists) { - overwriteMessage = assetName + ".fst already exists here, would you like to overwrite it?"; - } else { - overwriteMessage = assetName + ".fbx already exists here, would you like to overwrite it?"; - } - bool overwrite = EditorUtility.DisplayDialog("Error", overwriteMessage, "Yes", "No"); - if (!overwrite) { - return SelectExportFolder(assetName, selectedPath, out fstPath, out modelPath); - } - } - return true; - } - - public static void SetParentNames(Transform modelBone, Dictionary parentNames) { + + static void SetParentNames(Transform modelBone, Dictionary parentNames) { for (int i = 0; i < modelBone.childCount; i++) { SetParentNames(modelBone.GetChild(i), parentNames); } @@ -394,7 +434,7 @@ public class AvatarExporter : MonoBehaviour { } } - public static string FindLastRequiredParentBone(string currentBone) { + static string FindLastRequiredParentBone(string currentBone) { string result = currentBone; while (result != "root" && !userBoneToHumanoidMappings.ContainsKey(result)) { result = userParentNames[result]; @@ -402,3 +442,103 @@ public class AvatarExporter : MonoBehaviour { return result; } } + +class ExportProjectWindow : EditorWindow { + string projectName = ""; + string projectLocation = ""; + string projectDirectory = ""; + string errorLabel = ""; + + public delegate void OnCloseDelegate(string projectDirectory); + OnCloseDelegate onCloseCallback; + + public void Init(string initialPath, OnCloseDelegate closeCallback) { + projectLocation = initialPath; + onCloseCallback = closeCallback; + ShowUtility(); + } + + void OnGUI() { + GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); + buttonStyle.fontSize = 20; + 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; + GUIStyle textStyle = new GUIStyle(GUI.skin.textField); + textStyle.fontSize = 16; + + GUILayout.Space(10); + + GUILayout.Label("Export project name:", labelStyle); + projectName = GUILayout.TextField(projectName, textStyle); + + GUILayout.Space(10); + + GUILayout.Label("Export project location:", labelStyle); + projectLocation = GUILayout.TextField(projectLocation, textStyle); + + if (GUILayout.Button("Browse", buttonStyle)) { + string result = EditorUtility.OpenFolderPanel("Select export location", projectLocation, ""); + if (result.Length > 0) { // folder selection not cancelled + projectLocation = result.Replace('/', '\\'); + } + } + + GUILayout.Label(errorLabel, errorStyle); + + GUILayout.Space(30); + + bool export = false; + if (GUILayout.Button("Export", buttonStyle)) { + export = true; + if (!CheckForErrors(true)) { + Close(); + onCloseCallback(projectDirectory); + } + } + + if (GUILayout.Button("Cancel", buttonStyle)) { + Close(); + } + + if (GUI.changed && !export) { + CheckForErrors(false); + } + } + + bool CheckForErrors(bool exporting) { + errorLabel = ""; + projectDirectory = projectLocation + "\\" + projectName + "\\"; + if (projectName.Length > 0) { + if (Directory.Exists(projectDirectory)) { + 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) { + if (!Char.IsLetter(projectLocation[0]) || projectLocation.Length == 1 || projectLocation[1] != ':') { + errorLabel = "Project location is invalid. Please choose a different project location."; + return true; + } + } + if (exporting) { + if (projectName.Length == 0) { + errorLabel = "Please define a project name."; + return true; + } else if (projectLocation.Length == 0) { + errorLabel = "Please define a project location."; + return true; + } else { + try { + Directory.CreateDirectory(projectDirectory); + } catch { + errorLabel = "Project location is invalid. Please choose a different project location."; + return true; + } + } + } + return false; + } +} diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt new file mode 100644 index 0000000000..7e6c6d4f48 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/README.txt @@ -0,0 +1,11 @@ +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. +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 diff --git a/tools/unity-avatar-exporter/Assets/README.txt.meta b/tools/unity-avatar-exporter/Assets/README.txt.meta new file mode 100644 index 0000000000..d8bc5b9b66 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/README.txt.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 71e72751b2810fc4993ff53291c430b6 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage index 4134d6f09a..d1cd4efe8c 100644 Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ diff --git a/tools/unity-avatar-exporter/packager.bat b/tools/unity-avatar-exporter/packager.bat index 99932f1ead..55b59a9db6 100644 --- a/tools/unity-avatar-exporter/packager.bat +++ b/tools/unity-avatar-exporter/packager.bat @@ -1 +1 @@ -"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets/Editor" "avatarExporter.unitypackage" +"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets" "avatarExporter.unitypackage"