diff --git a/.gitignore b/.gitignore index 8d92fe770b..ef1a7b215e 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,8 @@ interface/resources/GPUCache/* # package lock file for JSDoc tool tools/jsdoc/package-lock.json + +# ignore unneeded unity project files for avatar exporter +tools/unity-avatar-exporter/Library +tools/unity-avatar-exporter/Packages +tools/unity-avatar-exporter/ProjectSettings diff --git a/tools/unity-avatar-exporter/Assets/Editor.meta b/tools/unity-avatar-exporter/Assets/Editor.meta new file mode 100644 index 0000000000..aac82b4258 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 51b3237a2992bd449a58ade16e52d0e0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs new file mode 100644 index 0000000000..60b5e0e643 --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs @@ -0,0 +1,207 @@ +// AvatarExporter.cs +// +// Created by David Back on 28 Nov 2018 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + +using UnityEngine; +using UnityEditor; +using System; +using System.IO; +using System.Collections.Generic; + +public class AvatarExporter : MonoBehaviour { + public static Dictionary UNITY_TO_HIFI_JOINT_NAME = new Dictionary { + {"Chest", "Spine1"}, + {"Head", "Head"}, + {"Hips", "Hips"}, + {"Left Index Distal", "LeftHandIndex3"}, + {"Left Index Intermediate", "LeftHandIndex2"}, + {"Left Index Proximal", "LeftHandIndex1"}, + {"Left Little Distal", "LeftHandPinky3"}, + {"Left Little Intermediate", "LeftHandPinky2"}, + {"Left Little Proximal", "LeftHandPinky1"}, + {"Left Middle Distal", "LeftHandMiddle3"}, + {"Left Middle Intermediate", "LeftHandMiddle2"}, + {"Left Middle Proximal", "LeftHandMiddle1"}, + {"Left Ring Distal", "LeftHandRing3"}, + {"Left Ring Intermediate", "LeftHandRing2"}, + {"Left Ring Proximal", "LeftHandRing1"}, + {"Left Thumb Distal", "LeftHandThumb3"}, + {"Left Thumb Intermediate", "LeftHandThumb2"}, + {"Left Thumb Proximal", "LeftHandThumb1"}, + {"LeftEye", "LeftEye"}, + {"LeftFoot", "LeftFoot"}, + {"LeftHand", "LeftHand"}, + {"LeftLowerArm", "LeftForeArm"}, + {"LeftLowerLeg", "LeftLeg"}, + {"LeftShoulder", "LeftShoulder"}, + {"LeftToes", "LeftToeBase"}, + {"LeftUpperArm", "LeftArm"}, + {"LeftUpperLeg", "LeftUpLeg"}, + {"Neck", "Neck"}, + {"Right Index Distal", "RightHandIndex3"}, + {"Right Index Intermediate", "RightHandIndex2"}, + {"Right Index Proximal", "RightHandIndex1"}, + {"Right Little Distal", "RightHandPinky3"}, + {"Right Little Intermediate", "RightHandPinky2"}, + {"Right Little Proximal", "RightHandPinky1"}, + {"Right Middle Distal", "RightHandMiddle3"}, + {"Right Middle Intermediate", "RightHandMiddle2"}, + {"Right Middle Proximal", "RightHandMiddle1"}, + {"Right Ring Distal", "RightHandRing3"}, + {"Right Ring Intermediate", "RightHandRing2"}, + {"Right Ring Proximal", "RightHandRing1"}, + {"Right Thumb Distal", "RightHandThumb3"}, + {"Right Thumb Intermediate", "RightHandThumb2"}, + {"Right Thumb Proximal", "RightHandThumb1"}, + {"RightEye", "RightEye"}, + {"RightFoot", "RightFoot"}, + {"RightHand", "RightHand"}, + {"RightLowerArm", "RightForeArm"}, + {"RightLowerLeg", "RightLeg"}, + {"RightShoulder", "RightShoulder"}, + {"RightToes", "RightToeBase"}, + {"RightUpperArm", "RightArm"}, + {"RightUpperLeg", "RightUpLeg"}, + {"Spine", "Spine"}, + {"UpperChest", "Spine2"}, + }; + + public static string exportedPath = String.Empty; + + [MenuItem("High Fidelity/Export New Avatar")] + public static void ExportNewAvatar() { + ExportSelectedAvatar(false); + } + + [MenuItem("High Fidelity/Export New Avatar", true)] + private static bool ExportNewAvatarValidator() { + // only enable Export New Avatar option if we have an asset selected + string[] guids = Selection.assetGUIDs; + return guids.Length > 0; + } + + [MenuItem("High Fidelity/Update Avatar")] + public static void UpdateAvatar() { + ExportSelectedAvatar(true); + } + + [MenuItem("High Fidelity/Update Avatar", true)] + private static bool UpdateAvatarValidation() { + // only enable Update Avatar option if the selected avatar is the last one that was exported + if (exportedPath != String.Empty) { + string[] guids = Selection.assetGUIDs; + if (guids.Length > 0) { + string selectedAssetPath = AssetDatabase.GUIDToAssetPath(guids[0]); + string selectedAsset = Path.GetFileNameWithoutExtension(selectedAssetPath); + string exportedAsset = Path.GetFileNameWithoutExtension(exportedPath); + return exportedAsset == selectedAsset; + } + } + return false; + } + + public static void ExportSelectedAvatar(bool usePreviousPath) { + string assetPath = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); + if (assetPath.LastIndexOf(".fbx") == -1) { + EditorUtility.DisplayDialog("Error", "Please select an fbx avatar to export", "Ok"); + return; + } + ModelImporter importer = ModelImporter.GetAtPath(assetPath) as ModelImporter; + if (importer == null) { + EditorUtility.DisplayDialog("Error", "Please select a model", "Ok"); + return; + } + if (importer.animationType != ModelImporterAnimationType.Human) { + EditorUtility.DisplayDialog("Error", "Please set model's Animation Type to Humanoid", "Ok"); + return; + } + + // store joint mappings only for joints that exist in hifi and verify missing joints + HumanDescription humanDescription = importer.humanDescription; + HumanBone[] boneMap = humanDescription.human; + Dictionary jointMappings = new Dictionary(); + foreach (HumanBone bone in boneMap) { + string humanBone = bone.humanName; + string hifiJointName; + if (UNITY_TO_HIFI_JOINT_NAME.TryGetValue(humanBone, out hifiJointName)) { + jointMappings.Add(hifiJointName, bone.boneName); + } + } + if (!jointMappings.ContainsKey("Hips")) { + EditorUtility.DisplayDialog("Error", "There is no Hips bone in selected avatar", "Ok"); + return; + } + if (!jointMappings.ContainsKey("Spine")) { + EditorUtility.DisplayDialog("Error", "There is no Spine bone in selected avatar", "Ok"); + return; + } + if (!jointMappings.ContainsKey("Spine1")) { + EditorUtility.DisplayDialog("Error", "There is no Chest bone in selected avatar", "Ok"); + return; + } + if (!jointMappings.ContainsKey("Spine2")) { + // if there is no UpperChest (Spine2) bone then we remap Chest (Spine1) to Spine2 in hifi and skip Spine1 + jointMappings["Spine2"] = jointMappings["Spine1"]; + jointMappings.Remove("Spine1"); + } + + // open folder explorer defaulting to user documents folder to select target path if exporting new avatar, + // otherwise use previously exported path if updating avatar + string directoryPath; + string assetName = Path.GetFileNameWithoutExtension(assetPath); + if (!usePreviousPath || exportedPath == String.Empty) { + string documentsFolder = System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments); + if (!SelectExportFolder(assetName, documentsFolder, out directoryPath)) { + return; + } + } else { + directoryPath = Path.GetDirectoryName(exportedPath) + "/"; + } + Directory.CreateDirectory(directoryPath); + + // delete any existing fst since we agreed to overwrite it + string fstPath = directoryPath + assetName + ".fst"; + if (File.Exists(fstPath)) { + File.Delete(fstPath); + } + + // write out core fields to top of fst file + File.WriteAllText(fstPath, "name = " + assetName + "\ntype = body+head\nscale = 1\nfilename = " + + assetName + ".fbx\n" + "texdir = textures\n"); + + // write out joint mappings to fst file + foreach (var jointMapping in jointMappings) { + File.AppendAllText(fstPath, "jointMap = " + jointMapping.Key + " = " + jointMapping.Value + "\n"); + } + + // delete any existing fbx since we agreed to overwrite it, and copy fbx over + string targetAssetPath = directoryPath + assetName + ".fbx"; + if (File.Exists(targetAssetPath)) { + File.Delete(targetAssetPath); + } + File.Copy(assetPath, targetAssetPath); + + exportedPath = targetAssetPath; + } + + public static bool SelectExportFolder(string assetName, string initialPath, out string directoryPath) { + string selectedPath = EditorUtility.OpenFolderPanel("Select export location", initialPath, ""); + if (selectedPath.Length == 0) { // folder selection cancelled + directoryPath = ""; + return false; + } + directoryPath = selectedPath + "/" + assetName + "/"; + if (Directory.Exists(directoryPath)) { + bool overwrite = EditorUtility.DisplayDialog("Error", "Directory " + assetName + + " already exists here, would you like to overwrite it?", "Yes", "No"); + if (!overwrite) { + SelectExportFolder(assetName, selectedPath, out directoryPath); + } + } + return true; + } +} diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta new file mode 100644 index 0000000000..c71e4c396d --- /dev/null +++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7a34be82b3ae554ea097963914b083f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage new file mode 100644 index 0000000000..bb25cb4072 Binary files /dev/null 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 new file mode 100644 index 0000000000..99932f1ead --- /dev/null +++ b/tools/unity-avatar-exporter/packager.bat @@ -0,0 +1 @@ +"C:\Program Files\Unity\Editor\Unity.exe" -quit -batchmode -projectPath %CD% -exportPackage "Assets/Editor" "avatarExporter.unitypackage"