diff --git a/Assets/DaddyFrosty/Editor.meta b/Assets/DaddyFrosty/Editor.meta new file mode 100644 index 0000000..ff06556 --- /dev/null +++ b/Assets/DaddyFrosty/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7844f4d67c65f134e941813c38650c07 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX.meta b/Assets/DaddyFrosty/Editor/FBX.meta new file mode 100644 index 0000000..d58104c --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b8e069023e8a06b498c07049662f1c63 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity.meta b/Assets/DaddyFrosty/Editor/FBX/Unity.meta new file mode 100644 index 0000000..772f298 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4731a204ecb13a942aad1252a74b8f71 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelEditorWindow.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelEditorWindow.cs new file mode 100644 index 0000000..f3d68be --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelEditorWindow.cs @@ -0,0 +1,989 @@ +using System.Collections.Generic; +using UnityEngine; +#if UNITY_2018_1_OR_NEWER +using UnityEditor.Presets; +#endif +using System.Linq; +using UnityEditor; +using UnityEngine.Timeline; +using UnityEngine.Playables; +using UnityEditor.Timeline; + +namespace DaddyFrosty.Fbx +{ + public abstract class ExportOptionsEditorWindow : EditorWindow + { + internal const string DefaultWindowTitle = "Export Options"; + protected const float SelectableLabelMinWidth = 120; + protected const float BrowseButtonWidth = 25; + protected const float LabelWidth = 175; + protected const float FieldOffset = 18; + protected const float TextFieldAlignOffset = 3; + protected const float ExportButtonWidth = 100; + protected const float FbxExtOffset = -7; + protected virtual float MinWindowHeight { get { return 300; } } + + protected virtual string ExportButtonName { get { return "Export"; } } + + protected virtual GUIContent WindowTitle { get { return new GUIContent(DefaultWindowTitle); } } + + private string m_exportFileName = ""; + protected string ExportFileName + { + get { return m_exportFileName; } + set { m_exportFileName = value; } + } + + private UnityEditor.Editor m_innerEditor; + protected UnityEditor.Editor InnerEditor + { + get { return m_innerEditor; } + set { m_innerEditor = value; } + } +#if UNITY_2018_1_OR_NEWER + private FbxExportPresetSelectorReceiver m_receiver; + protected FbxExportPresetSelectorReceiver Receiver + { + get { return m_receiver; } + set { m_receiver = value; } + } +#endif + private static GUIContent presetIcon { get { return EditorGUIUtility.IconContent("Preset.Context"); } } + private static GUIStyle presetIconButton { get { return new GUIStyle("IconButton"); } } + + private bool m_showOptions; + + private GUIStyle m_nameTextFieldStyle; + protected GUIStyle NameTextFieldStyle + { + get + { + if (m_nameTextFieldStyle == null) + { + m_nameTextFieldStyle = new GUIStyle(GUIStyle.none); + m_nameTextFieldStyle.alignment = TextAnchor.MiddleCenter; + m_nameTextFieldStyle.clipping = TextClipping.Clip; + m_nameTextFieldStyle.normal.textColor = EditorStyles.textField.normal.textColor; + } + return m_nameTextFieldStyle; + } + set { m_nameTextFieldStyle = value; } + } + + private GUIStyle m_fbxExtLabelStyle; + protected GUIStyle FbxExtLabelStyle + { + get + { + if (m_fbxExtLabelStyle == null) + { + m_fbxExtLabelStyle = new GUIStyle(GUIStyle.none); + m_fbxExtLabelStyle.alignment = TextAnchor.MiddleLeft; + m_fbxExtLabelStyle.richText = true; + m_fbxExtLabelStyle.contentOffset = new Vector2(FbxExtOffset, 0); + } + return m_fbxExtLabelStyle; + } + set { m_fbxExtLabelStyle = value; } + } + + private float m_fbxExtLabelWidth = -1; + protected float FbxExtLabelWidth + { + get + { + if (m_fbxExtLabelWidth < 0) + { + m_fbxExtLabelWidth = FbxExtLabelStyle.CalcSize(new GUIContent(".fbx")).x; + } + return m_fbxExtLabelWidth; + } + set { m_fbxExtLabelWidth = value; } + } + + protected abstract bool DisableTransferAnim { get; } + protected abstract bool DisableNameSelection { get; } + + protected abstract ExportOptionsSettingsSerializeBase SettingsObject { get; } + + // Helper functions for persisting the Export Settings for the session + protected abstract string SessionStoragePrefix { get; } + + public const string k_SessionSettingsName = "Settings"; + public const string k_SessionFbxPathsName = "FbxSavePath"; + public const string k_SessionSelectedFbxPathName = "SelectedFbxPath"; + public const string k_SessionPrefabPathsName = "PrefabSavePath"; + public const string k_SessionSelectedPrefabPathName = "SelectedPrefabPath"; + + protected void StorePathsInSession(string varName, List paths) + { + if (paths == null) + { + return; + } + + var n = paths.Count; + SessionState.SetInt(string.Format(SessionStoragePrefix, varName), n); + for (int i = 0; i < n; i++) + { + SessionState.SetString(string.Format(SessionStoragePrefix + "_{1}", varName, i), paths[i]); + } + } + + protected void RestorePathsFromSession(string varName, List defaultsPaths, out List paths) + { + var n = SessionState.GetInt(string.Format(SessionStoragePrefix, varName), 0); + if (n <= 0) + { + paths = defaultsPaths; + return; + } + + paths = new List(); + for (var i = 0; i < n; i++) + { + var path = SessionState.GetString(string.Format(SessionStoragePrefix + "_{1}", varName, i), null); + if (!string.IsNullOrEmpty(path)) + { + paths.Add(path); + } + } + } + + protected static void ClearPathsFromSession(string varName, string prefix) + { + var n = SessionState.GetInt(string.Format(prefix, varName), 0); + SessionState.EraseInt(string.Format(prefix, varName)); + for (int i = 0; i < n; i++) + { + SessionState.EraseString(string.Format(prefix + "_{1}", varName, i)); + } + } + + protected virtual void StoreSettingsInSession() + { + var settings = SettingsObject; + var json = EditorJsonUtility.ToJson(settings); + SessionState.SetString(string.Format(SessionStoragePrefix, k_SessionSettingsName), json); + + StorePathsInSession(k_SessionFbxPathsName, m_fbxSavePaths); + SessionState.SetInt(string.Format(SessionStoragePrefix, k_SessionSelectedFbxPathName), SelectedFbxPath); + } + + protected virtual void RestoreSettingsFromSession(ExportOptionsSettingsSerializeBase defaults) + { + var settings = SettingsObject; + var json = SessionState.GetString(string.Format(SessionStoragePrefix, k_SessionSettingsName), EditorJsonUtility.ToJson(defaults)); + if (!string.IsNullOrEmpty(json)) + { + EditorJsonUtility.FromJsonOverwrite(json, settings); + } + } + + public static void ResetAllSessionSettings(string prefix, string settingsDefaults = null) + { + SessionState.EraseString(string.Format(prefix, k_SessionSettingsName)); + // Set the defaults of the settings. + // If there exists a Default Preset for the Convert/Export settings, then if the project settings are modified, + // the Default Preset will be reloaded instead of the project settings. Therefore, set them explicitely if projects settings desired. + if (!string.IsNullOrEmpty(settingsDefaults)) + { + SessionState.SetString(string.Format(prefix, k_SessionSettingsName), settingsDefaults); + } + + ClearPathsFromSession(k_SessionFbxPathsName, prefix); + SessionState.EraseInt(string.Format(prefix, k_SessionSelectedFbxPathName)); + + ClearPathsFromSession(k_SessionPrefabPathsName, prefix); + SessionState.EraseInt(string.Format(prefix, k_SessionSelectedPrefabPathName)); + } + + public virtual void ResetSessionSettings(string settingsDefaults = null) + { + ResetAllSessionSettings(SessionStoragePrefix, settingsDefaults); + m_fbxSavePaths = null; + SelectedFbxPath = 0; + } + + private List m_fbxSavePaths; + internal List FbxSavePaths + { + get + { + if (m_fbxSavePaths == null) + { + // Try to restore from session, fall back to Fbx Export Settings + RestorePathsFromSession(k_SessionFbxPathsName, ExportSettings.instance.GetCopyOfFbxSavePaths(), out m_fbxSavePaths); + SelectedFbxPath = SessionState.GetInt(string.Format(SessionStoragePrefix, k_SessionSelectedFbxPathName), ExportSettings.instance.SelectedFbxPath); + } + return m_fbxSavePaths; + } + } + + [SerializeField] + private int m_selectedFbxPath = 0; + internal int SelectedFbxPath + { + get { return m_selectedFbxPath; } + set { m_selectedFbxPath = value; } + } + + /// + /// Caches the result of SelectionContainsPrefabInstanceWithAddedObjects() as it + /// only needs to be updated when ToExport is modified. + /// + private bool m_exportSetContainsPrefabInstanceWithAddedObjects; + + private Object[] m_toExport; + protected Object[] ToExport + { + get + { + return m_toExport; + } + set + { + m_toExport = value; + m_exportSetContainsPrefabInstanceWithAddedObjects = SelectionContainsPrefabInstanceWithAddedObjects(); + } + } + + protected virtual void OnEnable() + { + #if UNITY_2018_1_OR_NEWER + InitializeReceiver(); + #endif + m_showOptions = true; + this.minSize = new Vector2(SelectableLabelMinWidth + LabelWidth + BrowseButtonWidth + ExportButtonWidth, MinWindowHeight); + } + + protected static T CreateWindow() where T : EditorWindow + { + return (T)EditorWindow.GetWindow(DefaultWindowTitle, focus: true); + } + + protected virtual void InitializeWindow(string filename = "") + { + this.titleContent = WindowTitle; + this.SetFilename(filename); + } + + #if UNITY_2018_1_OR_NEWER + protected void InitializeReceiver() + { + if (!Receiver) + { + Receiver = ScriptableObject.CreateInstance() as FbxExportPresetSelectorReceiver; + Receiver.SelectionChanged -= OnPresetSelectionChanged; + Receiver.SelectionChanged += OnPresetSelectionChanged; + Receiver.DialogClosed -= SaveExportSettings; + Receiver.DialogClosed += SaveExportSettings; + } + } + + #endif + + internal void SetFilename(string filename) + { + // remove .fbx from end of filename + int extIndex = filename.LastIndexOf(".fbx"); + if (extIndex < 0) + { + ExportFileName = filename; + return; + } + ExportFileName = filename.Remove(extIndex); + } + + public abstract void SaveExportSettings(); + + public void OnPresetSelectionChanged() + { + this.Repaint(); + } + + protected bool SelectionContainsPrefabInstanceWithAddedObjects() + { + var exportSet = ToExport; + // FBX-60 (fogbug 1307749): + // On Linux OnGUI() sometimes gets called a few times before + // the export set is set and window.show() is called. + // This leads to this function being called from OnGUI() with a + // null or empty export set, and an ArgumentNullException when + // creating the stack. + // Check that the set exists and has values before creating the stack. + if (exportSet == null || exportSet.Length <= 0) + { + return false; + } + + Stack stack = new Stack(exportSet); + while (stack.Count > 0) + { + var go = ModelExporter.GetGameObject(stack.Pop()); + if (!go) + { + continue; + } + + if (PrefabUtility.IsAnyPrefabInstanceRoot(go) && PrefabUtility.GetAddedGameObjects(go).Count > 0) + { + return true; + } + + foreach (Transform child in go.transform) + { + stack.Push(child.gameObject); + } + } + return false; + } + + protected abstract bool Export(); + + /// + /// Function to be used by derived classes to add custom UI between the file path selector and export options. + /// + protected virtual void CreateCustomUI() {} + + #if UNITY_2018_1_OR_NEWER + protected abstract void ShowPresetReceiver(); + + protected void ShowPresetReceiver(UnityEngine.Object target) + { + InitializeReceiver(); + Receiver.SetTarget(target); + Receiver.SetInitialValue(new Preset(target)); + UnityEditor.Presets.PresetSelector.ShowSelector(target, null, true, Receiver); + } + + #endif + + protected Transform TransferAnimationSource + { + get + { + return SettingsObject.AnimationSource; + } + set + { + if (!TransferAnimationSourceIsValid(value)) + { + return; + } + SettingsObject.SetAnimationSource(value); + } + } + + protected Transform TransferAnimationDest + { + get + { + return SettingsObject.AnimationDest; + } + set + { + if (!TransferAnimationDestIsValid(value)) + { + return; + } + SettingsObject.SetAnimationDest(value); + } + } + + //-------Helper functions for determining if Animation source and dest are valid--------- + + /// + /// Determines whether p is an ancestor to t. + /// + /// true if p is ancestor to t; otherwise, false. + /// P. + /// T. + protected bool IsAncestor(Transform p, Transform t) + { + var curr = t; + while (curr != null) + { + if (curr == p) + { + return true; + } + curr = curr.parent; + } + return false; + } + + /// + /// Determines whether t1 and t2 are in the same hierarchy. + /// + /// true if t1 is in same hierarchy as t2; otherwise, false. + /// T1. + /// T2. + protected bool IsInSameHierarchy(Transform t1, Transform t2) + { + return (IsAncestor(t1, t2) || IsAncestor(t2, t1)); + } + + protected GameObject m_firstGameObjectToExport; + protected virtual GameObject FirstGameObjectToExport + { + get + { + if (!m_firstGameObjectToExport) + { + if (ToExport == null || ToExport.Length == 0) + { + return null; + } + m_firstGameObjectToExport = ModelExporter.GetGameObject(ToExport[0]); + } + return m_firstGameObjectToExport; + } + } + + protected bool TransferAnimationSourceIsValid(Transform newValue) + { + if (!newValue) + { + return true; + } + + var selectedGO = FirstGameObjectToExport; + if (!selectedGO) + { + Debug.LogWarning("FbxExportSettings: no Objects selected for export, can't transfer animation"); + return false; + } + + // source must be ancestor to dest + if (TransferAnimationDest && !IsAncestor(newValue, TransferAnimationDest)) + { + Debug.LogWarningFormat("FbxExportSettings: Source {0} must be an ancestor of {1}", newValue.name, TransferAnimationDest.name); + return false; + } + // must be in same hierarchy as selected GO + if (!selectedGO || !IsInSameHierarchy(newValue, selectedGO.transform)) + { + Debug.LogWarningFormat("FbxExportSettings: Source {0} must be in the same hierarchy as {1}", newValue.name, selectedGO ? selectedGO.name : "the selected object"); + return false; + } + return true; + } + + protected bool TransferAnimationDestIsValid(Transform newValue) + { + if (!newValue) + { + return true; + } + + var selectedGO = FirstGameObjectToExport; + if (!selectedGO) + { + Debug.LogWarning("FbxExportSettings: no Objects selected for export, can't transfer animation"); + return false; + } + + // source must be ancestor to dest + if (TransferAnimationSource && !IsAncestor(TransferAnimationSource, newValue)) + { + Debug.LogWarningFormat("FbxExportSettings: Destination {0} must be a descendant of {1}", newValue.name, TransferAnimationSource.name); + return false; + } + // must be in same hierarchy as selected GO + if (!selectedGO || !IsInSameHierarchy(newValue, selectedGO.transform)) + { + Debug.LogWarningFormat("FbxExportSettings: Destination {0} must be in the same hierarchy as {1}", newValue.name, selectedGO ? selectedGO.name : "the selected object"); + return false; + } + return true; + } + + /// + /// Add UI to turn the dialog off next time the user exports + /// + protected virtual void DoNotShowDialogUI() + { + EditorGUI.indentLevel--; + ExportSettings.instance.DisplayOptionsWindow = !EditorGUILayout.Toggle( + new GUIContent("Don't ask me again", "Don't ask me again, use the last used paths and options instead"), + !ExportSettings.instance.DisplayOptionsWindow + ); + } + + // ------------------------------------------------------------------------------------- + + protected void OnGUI() + { + // Increasing the label width so that none of the text gets cut off + EditorGUIUtility.labelWidth = LabelWidth; + + GUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + #if UNITY_2018_1_OR_NEWER + if (EditorGUILayout.DropdownButton(presetIcon, FocusType.Keyboard, presetIconButton)) + { + ShowPresetReceiver(); + } + #endif + + GUILayout.EndHorizontal(); + + EditorGUILayout.LabelField("Naming"); + EditorGUI.indentLevel++; + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent( + "Export Name", + "Filename to save model to."), GUILayout.Width(LabelWidth - TextFieldAlignOffset)); + + EditorGUI.BeginDisabledGroup(DisableNameSelection); + // Show the export name with an uneditable ".fbx" at the end + //------------------------------------- + EditorGUILayout.BeginVertical(); + EditorGUILayout.BeginHorizontal(EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight)); + EditorGUI.indentLevel--; + // continually resize to contents + var textFieldSize = NameTextFieldStyle.CalcSize(new GUIContent(ExportFileName)); + ExportFileName = EditorGUILayout.TextField(ExportFileName, NameTextFieldStyle, GUILayout.Width(textFieldSize.x + 5), GUILayout.MinWidth(5)); + ExportFileName = ModelExporter.ConvertToValidFilename(ExportFileName); + + EditorGUILayout.LabelField(".fbx", FbxExtLabelStyle, GUILayout.Width(FbxExtLabelWidth)); + EditorGUI.indentLevel++; + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + //----------------------------------- + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent( + "Export Path", + "Location where the FBX will be saved."), GUILayout.Width(LabelWidth - FieldOffset)); + + var pathLabels = ExportSettings.GetMixedSavePaths(FbxSavePaths); + // + // if (this is ConvertToPrefabEditorWindow) + // { + // pathLabels = ExportSettings.GetRelativeFbxSavePaths(FbxSavePaths, ref m_selectedFbxPath); + // } + + SelectedFbxPath = EditorGUILayout.Popup(SelectedFbxPath, pathLabels, GUILayout.MinWidth(SelectableLabelMinWidth)); + + // if (!(this is ConvertToPrefabEditorWindow)) + // { + var exportSettingsEditor = InnerEditor as ExportModelSettingsEditor; + // Set export setting for exporting outside the project on choosing a path + var exportOutsideProject = !pathLabels[SelectedFbxPath].Substring(0, 6).Equals("Assets"); + exportSettingsEditor.SetExportingOutsideProject(exportOutsideProject); + // } + + if (GUILayout.Button(new GUIContent("...", "Browse to a new location to export to"), EditorStyles.miniButton, GUILayout.Width(BrowseButtonWidth))) + { + string initialPath = Application.dataPath; + + string fullPath = EditorUtility.SaveFolderPanel( + "Select Export Model Path", initialPath, null + ); + + // Unless the user canceled, save path. + if (!string.IsNullOrEmpty(fullPath)) + { + var relativePath = ExportSettings.ConvertToAssetRelativePath(fullPath); + + // If exporting an fbx for a prefab, not allowed to export outside the Assets folder + // if (this is ConvertToPrefabEditorWindow && string.IsNullOrEmpty(relativePath)) + // { + // Debug.LogWarning("Please select a location in the Assets folder"); + // } + // We're exporting outside Assets folder, so store the absolute path + // else if (string.IsNullOrEmpty(relativePath)) + if (string.IsNullOrEmpty(relativePath)) + { + ExportSettings.AddSavePath(fullPath, FbxSavePaths, exportOutsideProject: true); + SelectedFbxPath = 0; + } + // Store the relative path to the Assets folder + else + { + ExportSettings.AddSavePath(relativePath, FbxSavePaths, exportOutsideProject: false); + SelectedFbxPath = 0; + } + // Make sure focus is removed from the selectable label + // otherwise it won't update + GUIUtility.hotControl = 0; + GUIUtility.keyboardControl = 0; + } + } + GUILayout.EndHorizontal(); + + CreateCustomUI(); + + EditorGUILayout.Space(); + + EditorGUI.BeginDisabledGroup(DisableTransferAnim); + EditorGUI.indentLevel--; + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent( + "Transfer Animation", + "Transfer transform animation from source to destination. Animation on objects between source and destination will also be transferred to destination." + ), GUILayout.Width(LabelWidth - FieldOffset)); + GUILayout.EndHorizontal(); + EditorGUI.indentLevel++; + TransferAnimationSource = EditorGUILayout.ObjectField("Source", TransferAnimationSource, typeof(Transform), allowSceneObjects: true) as Transform; + TransferAnimationDest = EditorGUILayout.ObjectField("Destination", TransferAnimationDest, typeof(Transform), allowSceneObjects: true) as Transform; + EditorGUILayout.Space(); + EditorGUI.EndDisabledGroup(); + + EditorGUI.indentLevel--; + m_showOptions = EditorGUILayout.Foldout(m_showOptions, "Options"); + EditorGUI.indentLevel++; + if (m_showOptions) + { + InnerEditor.OnInspectorGUI(); + } + + // if we are exporting or converting a prefab with overrides, then show a warning + if (m_exportSetContainsPrefabInstanceWithAddedObjects) + { + EditorGUILayout.Space(); + EditorGUILayout.HelpBox("Prefab instance overrides will be exported", MessageType.Warning, true); + } + + GUILayout.FlexibleSpace(); + + GUILayout.BeginHorizontal(); + DoNotShowDialogUI(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Cancel", GUILayout.Width(ExportButtonWidth))) + { + this.Close(); + } + + if (GUILayout.Button(ExportButtonName, GUILayout.Width(ExportButtonWidth))) + { + if (Export()) + { + this.Close(); + } + } + GUILayout.EndHorizontal(); + EditorGUILayout.Space(); // adding a space at bottom of dialog so buttons aren't right at the edge + + if (GUI.changed) + { + SaveExportSettings(); + } + } + + /// + /// Checks whether the file exists and if it does then asks if it should be overwritten. + /// + /// true, if file should be overwritten, false otherwise. + /// File path. + protected bool OverwriteExistingFile(string filePath) + { + // check if file already exists, give a warning if it does + if (System.IO.File.Exists(filePath)) + { + bool overwrite = UnityEditor.EditorUtility.DisplayDialog( + string.Format("{0} Warning", ModelExporter.PACKAGE_UI_NAME), + string.Format("File {0} already exists.\nOverwrite cannot be undone.", filePath), + "Overwrite", "Cancel"); + if (!overwrite) + { + if (GUI.changed) + { + SaveExportSettings(); + } + return false; + } + } + return true; + } + } + + internal class ExportModelEditorWindow : ExportOptionsEditorWindow + { + public const string k_SessionStoragePrefix = "FbxExporterOptions_DaddyFrosty_{0}"; + protected override string SessionStoragePrefix => k_SessionStoragePrefix; + + protected override float MinWindowHeight { get { return 310; } } // determined by trial and error + protected override bool DisableNameSelection + { + get + { + return false; + } + } + + protected override GameObject FirstGameObjectToExport + { + get + { + if (!m_firstGameObjectToExport) + { + if (IsTimelineAnim) + { + m_firstGameObjectToExport = AnimationOnlyExportData.GetGameObjectAndAnimationClip(TimelineClipToExport).Key; + } + else if (ToExport != null && ToExport.Length > 0) + { + m_firstGameObjectToExport = ModelExporter.GetGameObject(ToExport[0]); + } + } + return m_firstGameObjectToExport; + } + } + + protected override bool DisableTransferAnim + { + get + { + // don't transfer animation if we are exporting more than one hierarchy, the timeline clips from + // a playable director, or if only the model is being exported + // if we are on the timeline then export length can be more than 1 + return SettingsObject.ModelAnimIncludeOption == Include.Model || (!IsTimelineAnim && (ToExport == null || ToExport.Length != 1)); + } + } + + protected TimelineClip TimelineClipToExport { get; set; } + protected PlayableDirector PlayableDirector { get; set; } + + private bool m_isTimelineAnim = false; + protected bool IsTimelineAnim + { + get { return m_isTimelineAnim; } + set + { + m_isTimelineAnim = value; + if (m_isTimelineAnim) + { + m_previousInclude = ExportModelSettingsInstance.info.ModelAnimIncludeOption; + ExportModelSettingsInstance.info.SetModelAnimIncludeOption(Include.Anim); + } + if (InnerEditor) + { + var exportModelSettingsEditor = InnerEditor as ExportModelSettingsEditor; + if (exportModelSettingsEditor) + { + exportModelSettingsEditor.DisableIncludeDropdown(m_isTimelineAnim); + } + } + } + } + + private bool m_singleHierarchyExport = true; + protected bool SingleHierarchyExport + { + get { return m_singleHierarchyExport; } + set + { + m_singleHierarchyExport = value; + + if (InnerEditor) + { + var exportModelSettingsEditor = InnerEditor as ExportModelSettingsEditor; + if (exportModelSettingsEditor) + { + exportModelSettingsEditor.SetIsSingleHierarchy(m_singleHierarchyExport); + } + } + } + } + + public override void ResetSessionSettings(string defaultSettings = null) + { + base.ResetSessionSettings(defaultSettings); + + // save the source and dest as these are not serialized + var source = m_exportModelSettingsInstance.info.AnimationSource; + var dest = m_exportModelSettingsInstance.info.AnimationDest; + + m_exportModelSettingsInstance = null; + ExportModelSettingsInstance.info.SetAnimationSource(source); + ExportModelSettingsInstance.info.SetAnimationDest(dest); + InnerEditor = Editor.CreateEditor(ExportModelSettingsInstance); + } + + private ExportModelSettings m_exportModelSettingsInstance; + public ExportModelSettings ExportModelSettingsInstance + { + get + { + if (m_exportModelSettingsInstance == null) + { + // make a copy of the settings + m_exportModelSettingsInstance = ScriptableObject.CreateInstance(typeof(ExportModelSettings)) as ExportModelSettings; + // load settings stored in Unity session, default to DefaultPreset, if none then Export Settings + var defaultPresets = Preset.GetDefaultPresetsForObject(m_exportModelSettingsInstance); + if (defaultPresets.Length <= 0) + { + RestoreSettingsFromSession(ExportSettings.instance.ExportModelSettings.info); + } + else + { + // apply the first default preset + // TODO: figure out what it means to have multiple default presets, when would they be applied? + defaultPresets[0].ApplyTo(m_exportModelSettingsInstance); + RestoreSettingsFromSession(m_exportModelSettingsInstance.info); + } + } + return m_exportModelSettingsInstance; + } + } + + public override void SaveExportSettings() + { + // check if the settings are different from what is in the Project Settings and only store + // if they are. Otherwise we want to keep them updated with changes to the Project Settings. + bool settingsChanged = !(ExportModelSettingsInstance.Equals(ExportSettings.instance.ExportModelSettings)); + var projectSettingsPaths = ExportSettings.instance.GetCopyOfFbxSavePaths(); + settingsChanged |= !projectSettingsPaths.SequenceEqual(FbxSavePaths); + settingsChanged |= SelectedFbxPath != ExportSettings.instance.SelectedFbxPath; + + if (settingsChanged) + { + StoreSettingsInSession(); + } + } + + protected override ExportOptionsSettingsSerializeBase SettingsObject => ExportModelSettingsInstance.info; + + private Include m_previousInclude = Include.ModelAndAnim; + + public static ExportModelEditorWindow Init(IEnumerable toExport, string filename = "", TimelineClip timelineClip = null, PlayableDirector director = null) + { + ExportModelEditorWindow window = CreateWindow(); + window.IsTimelineAnim = (timelineClip != null); + window.TimelineClipToExport = timelineClip; + window.PlayableDirector = director ? director : TimelineEditor.inspectedDirector; + + + int numObjects = window.SetGameObjectsToExport(toExport); + if (string.IsNullOrEmpty(filename)) + { + filename = window.DefaultFilename; + } + window.InitializeWindow(filename); + window.SingleHierarchyExport = (numObjects == 1); + window.Show(); + return window; + } + + protected int SetGameObjectsToExport(IEnumerable toExport) + { + ToExport = toExport?.ToArray(); + if (!IsTimelineAnim && (ToExport == null || ToExport.Length == 0)) return 0; + + TransferAnimationSource = null; + TransferAnimationDest = null; + + // if only one object selected, set transfer source/dest to this object + if (IsTimelineAnim || (ToExport != null && ToExport.Length == 1)) + { + GameObject go = FirstGameObjectToExport; + if (go) + { + TransferAnimationSource = go.transform; + TransferAnimationDest = go.transform; + } + } + + return IsTimelineAnim ? 1 : ToExport.Length; + } + + /// + /// Gets the filename from objects to export. + /// + /// The object's name if one object selected, "Untitled" if multiple + /// objects selected for export. + protected string DefaultFilename + { + get + { + string filename; + if (ToExport.Length == 1) + { + filename = ToExport[0].name; + } + else + { + filename = "Untitled"; + } + return filename; + } + } + + protected override void OnEnable() + { + base.OnEnable(); + if (!InnerEditor) + { + InnerEditor = UnityEditor.Editor.CreateEditor(ExportModelSettingsInstance); + this.SingleHierarchyExport = m_singleHierarchyExport; + this.IsTimelineAnim = m_isTimelineAnim; + } + } + + protected void OnDisable() + { + RestoreSettings(); + } + + /// + /// Restore changed export settings after export + /// + protected virtual void RestoreSettings() + { + if (IsTimelineAnim) + { + ExportModelSettingsInstance.info.SetModelAnimIncludeOption(m_previousInclude); + } + } + + protected override bool Export() + { + if (string.IsNullOrEmpty(ExportFileName)) + { + Debug.LogError("FbxExporter: Please specify an fbx filename"); + return false; + } + var folderPath = ExportSettings.GetAbsoluteSavePath(FbxSavePaths[SelectedFbxPath]); + var filePath = System.IO.Path.Combine(folderPath, ExportFileName + ".fbx"); + + if (!OverwriteExistingFile(filePath)) + { + return false; + } + + string exportResult; + if (IsTimelineAnim) + { + exportResult = ModelExporter.ExportTimelineClip(filePath, TimelineClipToExport, PlayableDirector, SettingsObject); + } + else + { + exportResult = ModelExporter.ExportObjects(filePath, ToExport, SettingsObject); + } + + if (!string.IsNullOrEmpty(exportResult)) + { + // refresh the asset database so that the file appears in the + // asset folder view. + AssetDatabase.Refresh(); + } + return true; + } + + #if UNITY_2018_1_OR_NEWER + protected override void ShowPresetReceiver() + { + ShowPresetReceiver(ExportModelSettingsInstance); + } + + #endif + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelEditorWindow.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelEditorWindow.cs.meta new file mode 100644 index 0000000..b89fdaa --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 954c3dda5c0398a4292e34227b6cebad +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelSettings.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelSettings.cs new file mode 100644 index 0000000..5ed1f57 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelSettings.cs @@ -0,0 +1,696 @@ +using DaddyFrosty.Fbx.Misc; +using UnityEditor; +using UnityEngine; + +namespace DaddyFrosty.Fbx +{ + [CustomEditor(typeof(ExportModelSettings))] + public class ExportModelSettingsEditor : UnityEditor.Editor + { + private const float DefaultLabelWidth = 175; + private const float DefaultFieldOffset = 18; + + public float LabelWidth { get; set; } = DefaultLabelWidth; + public float FieldOffset { get; set; } = DefaultFieldOffset; + + private string[] exportFormatOptions = new string[] { "ASCII", "Binary" }; + private string[] includeOptions = new string[] {"Model(s) Only", "Animation Only", "Model(s) + Animation"}; + private string[] lodOptions = new string[] {"All Levels", "Highest", "Lowest"}; + + public const string singleHierarchyOption = "Local Pivot"; + public const string multiHerarchyOption = "Local Centered"; + private string hierarchyDepOption = singleHierarchyOption; + private string[] objPositionOptions { get { return new string[] {hierarchyDepOption, "World Absolute"}; }} + + private bool disableIncludeDropdown = false; + + private bool m_exportingOutsideProject = false; + public void SetExportingOutsideProject(bool val) + { + m_exportingOutsideProject = val; + } + + public void SetIsSingleHierarchy(bool singleHierarchy) + { + if (singleHierarchy) + { + hierarchyDepOption = singleHierarchyOption; + return; + } + hierarchyDepOption = multiHerarchyOption; + } + + public void DisableIncludeDropdown(bool disable) + { + disableIncludeDropdown = disable; + } + + public override void OnInspectorGUI() + { + var exportSettings = ((ExportModelSettings)target).info; + + EditorGUIUtility.labelWidth = LabelWidth; + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Export Format", "Export the FBX file in the standard binary format." + + " Select ASCII to export the FBX file in ASCII format."), GUILayout.Width(LabelWidth - FieldOffset)); + exportSettings.SetExportFormat((ExportFormat)EditorGUILayout.Popup((int)exportSettings.ExportFormat, exportFormatOptions)); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Include", "Select whether to export models, animation or both."), GUILayout.Width(LabelWidth - FieldOffset)); + EditorGUI.BeginDisabledGroup(disableIncludeDropdown); + exportSettings.SetModelAnimIncludeOption((Include)EditorGUILayout.Popup((int)exportSettings.ModelAnimIncludeOption, includeOptions)); + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("LOD level", "Select which LOD to export."), GUILayout.Width(LabelWidth - FieldOffset)); + // greyed out if animation only + EditorGUI.BeginDisabledGroup(exportSettings.ModelAnimIncludeOption == Include.Anim); + exportSettings.SetLODExportType((LODExportType)EditorGUILayout.Popup((int)exportSettings.LODExportType, lodOptions)); + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Object(s) Position", "Select an option for exporting object's transform."), GUILayout.Width(LabelWidth - FieldOffset)); + // greyed out if animation only + EditorGUI.BeginDisabledGroup(exportSettings.ModelAnimIncludeOption == Include.Anim); + exportSettings.SetObjectPosition((ObjectPosition)EditorGUILayout.Popup((int)exportSettings.ObjectPosition, objPositionOptions)); + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Animated Skinned Mesh", + "If checked, animation on objects with skinned meshes will be exported"), GUILayout.Width(LabelWidth - FieldOffset)); + // greyed out if model + EditorGUI.BeginDisabledGroup(exportSettings.ModelAnimIncludeOption == Include.Model); + exportSettings.SetAnimatedSkinnedMesh(EditorGUILayout.Toggle(exportSettings.AnimateSkinnedMesh)); + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Compatible Naming", + "In Maya some symbols such as spaces and accents get replaced when importing an FBX " + + "(e.g. \"foo bar\" becomes \"fooFBXASC032bar\"). " + + "On export, convert the names of GameObjects so they are Maya compatible." + + (exportSettings.UseMayaCompatibleNames ? "" : + "\n\nWARNING: Disabling this feature may result in lost material connections," + + " and unexpected character replacements in Maya.")), + GUILayout.Width(LabelWidth - FieldOffset)); + exportSettings.SetUseMayaCompatibleNames(EditorGUILayout.Toggle(exportSettings.UseMayaCompatibleNames)); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Export Unrendered", + "If checked, meshes will be exported even if they don't have a Renderer component."), GUILayout.Width(LabelWidth - FieldOffset)); + // greyed out if animation only + EditorGUI.BeginDisabledGroup(exportSettings.ModelAnimIncludeOption == Include.Anim); + exportSettings.SetExportUnrendered(EditorGUILayout.Toggle(exportSettings.ExportUnrendered)); + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Preserve Import Settings", + "If checked, the import settings from the overwritten FBX will be carried over to the new version."), GUILayout.Width(LabelWidth - FieldOffset)); + // greyed out if exporting outside assets folder + EditorGUI.BeginDisabledGroup(m_exportingOutsideProject); + exportSettings.SetPreserveImportSettings(EditorGUILayout.Toggle(exportSettings.PreserveImportSettings)); + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Keep Instances", + "If enabled, instances will be preserved as instances in the FBX file. This can cause issues with e.g. Blender if different instances have different materials assigned."), + GUILayout.Width(LabelWidth - FieldOffset)); + exportSettings.SetKeepInstances(EditorGUILayout.Toggle(exportSettings.KeepInstances)); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent("Embed Textures", + "If enabled, textures are embedded into the resulting FBX file instead of referenced."), GUILayout.Width(LabelWidth - FieldOffset)); + exportSettings.SetEmbedTextures(EditorGUILayout.Toggle(exportSettings.EmbedTextures)); + GUILayout.EndHorizontal(); + + // CUSTOM SETTINGS. + CustomFBX.AddSettings( exportSettings, GUILayout.Width( LabelWidth - FieldOffset ) ); + // GUILayout.BeginHorizontal(); + // EditorGUILayout.LabelField( new GUIContent( "Use Maya Cordinate Conversion", + // "If enabled axis will be properly converted." ), GUILayout.Width( LabelWidth - FieldOffset ) ); + // exportSettings.SetConvertCordinateSpace( EditorGUILayout.Toggle( exportSettings.ConvertCordinateSpace ) ); + // GUILayout.EndHorizontal(); + } + } + + /// + /// Interface of export options that you can set when exporting to FBX. + /// + public interface IExportOptions + { + /// + /// The export format (binary or ascii). + /// + ExportFormat ExportFormat { get; } + + /// + /// Option to export the model only, the animation only, or both the model and the animation. + /// + Include ModelAnimIncludeOption { get; } + + /// + /// The type of LOD to export (All, Highest or Lowest). + /// + LODExportType LODExportType { get; } + + /// + /// The position to export the object to (Local centered, World absolute, or Reset). Use Reset for converting to a Prefab. + /// + ObjectPosition ObjectPosition { get; } + + /// + /// Option to export the animation on GameObjects that have a skinned mesh. + /// + bool AnimateSkinnedMesh { get; } + + /// + /// Option to convert the GameObject and material names to Maya compatible names. + /// + bool UseMayaCompatibleNames { get; } + + /// + /// Option to change the GameObjects and material names in the scene to keep them + /// Maya compatible after the export. Only works if UseMayaCompatibleNames is also enabled. + /// + bool AllowSceneModification { get; } + + /// + /// Option to export GameObjects that don't have a renderer. + /// + bool ExportUnrendered { get; } + + /// + /// Option to preserve the previous import settings after the export when overwriting an existing FBX file. + /// + bool PreserveImportSettings { get; } + + /// + /// Option to keep multiple instances of the same mesh as separate instances on export. + /// + bool KeepInstances { get; } + + /// + /// Option to embed textures in the exported FBX file. + /// + /// + /// To embed textures, you must set the file ExportFormat to binary. + /// + bool EmbedTextures { get; } + + /// + /// The transform to transfer the animation from. The animation is transferred to AnimationDest. + /// + /// + /// Transform must be an ancestor of AnimationDest, and may be an ancestor of the selected GameObject. + /// + Transform AnimationSource { get; } + + /// + /// The transform to transfer the animation to. + /// This GameObject receives the transform animation on GameObjects between Source + /// and Destination as well as the animation on the Source itself. + /// + Transform AnimationDest { get; } + + // Custom + bool ConvertCordinateSpace { get; } + float UnitScaleFactor { get; } + } + + public abstract class ExportOptionsSettingsBase : ScriptableObject where T : ExportOptionsSettingsSerializeBase, new() + { + [SerializeField] + private T m_info = new T(); + public T info + { + get { return m_info; } + set { m_info = value; } + } + + public override bool Equals(object e) + { + var expOptions = e as ExportOptionsSettingsBase; + if (expOptions == null) + { + return false; + } + return this.info.Equals(expOptions.info); + } + + public override int GetHashCode() + { + return this.info.GetHashCode(); + } + } + + public class ExportModelSettings : ExportOptionsSettingsBase + {} + + /// + /// Base class for the export model settings and convert to prefab settings. + /// + [System.Serializable] + public abstract class ExportOptionsSettingsSerializeBase : IExportOptions + { + [SerializeField] + private ExportFormat exportFormat = ExportFormat.ASCII; + [SerializeField] + private bool animatedSkinnedMesh = false; + [SerializeField] + private bool mayaCompatibleNaming = true; + + [System.NonSerialized] + private Transform animSource; + [System.NonSerialized] + private Transform animDest; + + // CUSTOM START ====================== + [SerializeField] private bool convertCordinateSpace = true; + public void SetConvertCordinateSpace( bool v ) => convertCordinateSpace = v; + public bool ConvertCordinateSpace => convertCordinateSpace; + + [SerializeField] private float unitScaleFactor = 100f; + public void SetUnitScaleFactor( float v ) => unitScaleFactor = v; + + public float UnitScaleFactor => unitScaleFactor; + // CUSTOM END ======================== + + /// + public ExportFormat ExportFormat { get { return exportFormat; } } + + /// + /// Specifies the export format to binary or ascii. + /// + /// Binary or ASCII + public void SetExportFormat(ExportFormat format) { this.exportFormat = format; } + + /// + public bool AnimateSkinnedMesh { get { return animatedSkinnedMesh; } } + + /// + /// Specifies whether to export animation on GameObjects containing a skinned mesh. + /// + /// True to export animation on skinned meshes, false otherwise. + public void SetAnimatedSkinnedMesh(bool animatedSkinnedMesh) { this.animatedSkinnedMesh = animatedSkinnedMesh; } + + /// + public bool UseMayaCompatibleNames { get { return mayaCompatibleNaming; } } + + /// + /// Specifies whether to rename the exported GameObjects to Maya compatible names. + /// + /// True to have export Maya compatible names, false otherwise. + public void SetUseMayaCompatibleNames(bool useMayaCompNames) { this.mayaCompatibleNaming = useMayaCompNames; } + + /// + public Transform AnimationSource { get { return animSource; } } + + /// + /// Specifies the transform to transfer the animation from. + /// + /// The transform to transfer the animation from. + public void SetAnimationSource(Transform source) { this.animSource = source; } + + /// + public Transform AnimationDest { get { return animDest; } } + + /// + /// Specifies the transform to transfer the source animation to. + /// + /// The transform to transfer the animation to. + public void SetAnimationDest(Transform dest) { this.animDest = dest; } + + /// + public abstract Include ModelAnimIncludeOption { get; } + + /// + public abstract LODExportType LODExportType { get; } + + /// + public abstract ObjectPosition ObjectPosition { get; } + + /// + public abstract bool ExportUnrendered { get; } + + /// + public virtual bool PreserveImportSettings { get { return false; } } + + /// + public abstract bool AllowSceneModification { get; } + + /// + public virtual bool KeepInstances { get { return true; } } + + /// + public virtual bool EmbedTextures { get { return false; } } + + /// + /// Checks if two instances of the export settings are equal. + /// + /// The other export setting object to check. + /// True if equal, false otherwise. + public override bool Equals(object e) + { + var expOptions = e as ExportOptionsSettingsSerializeBase; + if (expOptions == null) + { + return false; + } + return animatedSkinnedMesh == expOptions.animatedSkinnedMesh && + mayaCompatibleNaming == expOptions.mayaCompatibleNaming && + exportFormat == expOptions.exportFormat + + // Custom + && convertCordinateSpace == expOptions.convertCordinateSpace + && unitScaleFactor == expOptions.UnitScaleFactor; + } + + /// + /// Gets the hash code for this instance of the export model settings. + /// + /// Unique hash code for the export model settings. + public override int GetHashCode() + { + return (animatedSkinnedMesh ? 1 : 0) | ((mayaCompatibleNaming ? 1 : 0) << 1) | ((int)exportFormat << 2) + + // Custom + | ( ( convertCordinateSpace ? 1 : 0 ) << 3 ) + | unitScaleFactor.GetHashCode(); + } + } + + /// + /// Class specifying the settings for exporting to FBX. + /// + [System.Serializable] + public class ExportModelSettingsSerialize : ExportOptionsSettingsSerializeBase + { + [SerializeField] + private Include include = Include.ModelAndAnim; + [SerializeField] + private LODExportType lodLevel = LODExportType.All; + [SerializeField] + private ObjectPosition objectPosition = ObjectPosition.LocalCentered; + [SerializeField] + private bool exportUnrendered = true; + [SerializeField] + private bool preserveImportSettings = false; + [SerializeField] + private bool keepInstances = true; + [SerializeField] + private bool embedTextures = false; + + /// + public override Include ModelAnimIncludeOption { get { return include; } } + + /// + /// Specifies to export the model only, the animation only, or both the model and the animation. + /// + /// Model, animation, or model and animation + public void SetModelAnimIncludeOption(Include include) { this.include = include; } + + /// + public override LODExportType LODExportType { get { return lodLevel; } } + + /// + /// Specifies the type of LOD to export (All, Highest or Lowest). + /// + /// All, Highest, or Lowest + public void SetLODExportType(LODExportType lodLevel) { this.lodLevel = lodLevel; } + + /// + public override ObjectPosition ObjectPosition { get { return objectPosition; } } + + /// + /// Specifies the position to export the object to (Local centered, World absolute, or Reset). Use Reset for converting to a Prefab). + /// + /// Local centered, World absolute, or Reset + public void SetObjectPosition(ObjectPosition objectPosition) { this.objectPosition = objectPosition; } + + /// + public override bool ExportUnrendered { get { return exportUnrendered; } } + + /// + /// Specifies whether to export GameObjects that don't have a renderer. + /// + /// True to export unrendered, false otherwise. + public void SetExportUnrendered(bool exportUnrendered) { this.exportUnrendered = exportUnrendered; } + + /// + public override bool PreserveImportSettings { get { return preserveImportSettings; } } + + /// + /// Specifies whether to preserve the previous import settings after the export when overwriting + /// an existing FBX file. + /// + /// True to preserve the previous import settings, false otherwise. + public void SetPreserveImportSettings(bool preserveImportSettings) { this.preserveImportSettings = preserveImportSettings; } + + /// + public override bool AllowSceneModification { get { return false; } } + + /// + public override bool KeepInstances { get { return keepInstances; } } + + /// + /// Specifies whether to keep multiple instances of the same mesh as separate instances on export. + /// + /// True to export as separate instances, false otherwise. + public void SetKeepInstances(bool keepInstances) { this.keepInstances = keepInstances; } + + /// + public override bool EmbedTextures { get { return embedTextures; } } + + /// + /// Specifies whether to embed textures in the exported FBX file. + /// + /// True to embed textures, false otherwise. + public void SetEmbedTextures(bool embedTextures) { this.embedTextures = embedTextures; } + + /// + public override bool Equals(object e) + { + var expOptions = e as ExportModelSettingsSerialize; + if (expOptions == null) + { + return false; + } + return base.Equals(e) && + include == expOptions.include && + lodLevel == expOptions.lodLevel && + objectPosition == expOptions.objectPosition && + exportUnrendered == expOptions.exportUnrendered && + preserveImportSettings == expOptions.preserveImportSettings; + } + + /// + public override int GetHashCode() + { + var bitmask = base.GetHashCode(); + bitmask = (bitmask << 2) ^ (int)include; + bitmask = (bitmask << 2) ^ (int)lodLevel; + bitmask = (bitmask << 2) ^ (int)objectPosition; + bitmask = (bitmask << 1) | (exportUnrendered ? 1 : 0); + bitmask = (bitmask << 1) | (preserveImportSettings ? 1 : 0); + return bitmask; + } + } + + // /// + // /// Class specifying the settings for exporting to FBX. + // /// + // [System.Serializable] + // public class ExportModelOptions : IExportOptions + // { + // [SerializeField] + // private ExportFormat exportFormat = ExportFormat.ASCII; + // [SerializeField] + // private bool animatedSkinnedMesh = false; + // [SerializeField] + // private bool mayaCompatibleNaming = true; + // + // // CUSTOM START + // [SerializeField] private bool convertCordinateSpace = true; + // public void SetConvertCordinateSpace( bool v ) => convertCordinateSpace = v; + // public bool ConvertCordinateSpace => convertCordinateSpace; + // + // [SerializeField] private float unitScaleFactor = 100f; + // public void SetUnitScaleFactor( float v ) => unitScaleFactor = v; + // + // public float UnitScaleFactor => unitScaleFactor; + // // CUSTOM END + // + // [System.NonSerialized] + // private Transform animSource; + // [System.NonSerialized] + // private Transform animDest; + // + // [SerializeField] + // private Include include = Include.ModelAndAnim; + // [SerializeField] + // private LODExportType lodLevel = LODExportType.All; + // [SerializeField] + // private ObjectPosition objectPosition = ObjectPosition.LocalCentered; + // [SerializeField] + // private bool exportUnrendered = true; + // [SerializeField] + // private bool preserveImportSettings = false; + // [SerializeField] + // private bool keepInstances = true; + // [SerializeField] + // private bool embedTextures = false; + // + // /// + // /// The export format (binary or ascii). + // /// + // public ExportFormat ExportFormat + // { + // get { return exportFormat; } + // set { exportFormat = value; } + // } + // + // /// + // /// Option to export the model only, the animation only, or both the model and the animation. + // /// + // public Include ModelAnimIncludeOption + // { + // get { return include; } + // set { include = value; } + // } + // + // /// + // /// The type of LOD to export (All, Highest or Lowest). + // /// + // public LODExportType LODExportType + // { + // get { return lodLevel; } + // set { lodLevel = value; } + // } + // + // /// + // /// The position to export the object to (Local centered, World absolute, or Reset). Use Reset for converting to a Prefab. + // /// + // public ObjectPosition ObjectPosition + // { + // get { return objectPosition; } + // set { objectPosition = value; } + // } + // + // /// + // /// Option to export the animation on GameObjects that have a skinned mesh. + // /// + // public bool AnimateSkinnedMesh + // { + // get { return animatedSkinnedMesh; } + // set { animatedSkinnedMesh = value; } + // } + // + // /// + // /// Option to convert the GameObject and material names to Maya compatible names. + // /// + // public bool UseMayaCompatibleNames + // { + // get { return mayaCompatibleNaming; } + // set { mayaCompatibleNaming = value; } + // } + // + // /// + // /// Option to change the GameObjects and material names in the scene to keep them + // /// Maya compatible after the export. Only works if UseMayaCompatibleNames is also enabled. + // /// + // bool IExportOptions.AllowSceneModification + // { + // get { return false; } + // } + // + // /// + // /// Option to export GameObjects that don't have a renderer. + // /// + // public bool ExportUnrendered + // { + // get { return exportUnrendered; } + // set { exportUnrendered = value; } + // } + // + // /// + // /// Option to preserve the previous import settings after the export when overwriting an existing FBX file. + // /// + // public bool PreserveImportSettings + // { + // get { return preserveImportSettings; } + // set { preserveImportSettings = value; } + // } + // + // /// + // /// Option to keep multiple instances of the same mesh as separate instances on export. + // /// + // public bool KeepInstances + // { + // get { return keepInstances; } + // set { keepInstances = value; } + // } + // + // /// + // /// Option to embed textures in the exported FBX file. + // /// + // /// + // /// To embed textures, you must set the file ExportFormat to binary. + // /// + // public bool EmbedTextures + // { + // get { return embedTextures; } + // set { embedTextures = value; } + // } + // + // /// + // /// The transform to transfer the animation from. The animation is transferred to AnimationDest. + // /// + // /// + // /// Transform must be an ancestor of AnimationDest, and may be an ancestor of the selected GameObject. + // /// + // public Transform AnimationSource + // { + // get { return animSource; } + // set { animSource = value; } + // } + // + // /// + // /// The transform to transfer the animation to. + // /// This GameObject receives the transform animation on GameObjects between Source + // /// and Destination as well as the animation on the Source itself. + // /// + // public Transform AnimationDest + // { + // get { return animDest; } + // set { animDest = value; } + // } + // + // internal ExportModelSettingsSerialize ConvertToModelSettingsSerialize() + // { + // var exportSettings = new ExportModelSettingsSerialize(); + // exportSettings.SetAnimatedSkinnedMesh(animatedSkinnedMesh); + // exportSettings.SetAnimationDest(animDest); + // exportSettings.SetAnimationSource(animSource); + // exportSettings.SetEmbedTextures(embedTextures); + // exportSettings.SetExportFormat(exportFormat); + // exportSettings.SetExportUnrendered(exportUnrendered); + // exportSettings.SetKeepInstances(keepInstances); + // exportSettings.SetLODExportType(lodLevel); + // exportSettings.SetModelAnimIncludeOption(include); + // exportSettings.SetObjectPosition(objectPosition); + // exportSettings.SetPreserveImportSettings(preserveImportSettings); + // exportSettings.SetUseMayaCompatibleNames(mayaCompatibleNaming); + // + // return exportSettings; + // } + // } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelSettings.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelSettings.cs.meta new file mode 100644 index 0000000..74a2c59 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportModelSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63180e849b84d344d81d213c1e4e944b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/ExportSettings.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportSettings.cs new file mode 100644 index 0000000..0ee5606 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportSettings.cs @@ -0,0 +1,1959 @@ +using System; +using System.IO; +using UnityEditorInternal; +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using UnityEditor; +using UnityEditor.Presets; +using DaddyFrosty; + +namespace DaddyFrosty.Fbx +{ + /// + /// FBX export format options. + /// + public enum ExportFormat + { + /// + /// Output the FBX in ASCII format. + /// + ASCII = 0, + /// + /// Output the FBX in Binary format. + /// + Binary = 1 + } + + /// + /// Options for the type of data to include in the export + /// (Model only, animation only, or model and animation). + /// + public enum Include + { + /// + /// Export the model without animation. + /// + Model = 0, + /// + /// Export the animation only. + /// + Anim = 1, + /// + /// Export both the model and animation. + /// + ModelAndAnim = 2 + } + + /// + /// Options for the position to use for the root GameObject. + /// + public enum ObjectPosition + { + /// + /// For a single root, uses the local transform information. + /// If you select multiple GameObjects for export, the FBX Exporter centers GameObjects + /// around a shared root while keeping their relative placement unchanged. + /// + LocalCentered = 0, + /// + /// Uses the world position of the GameObjects. + /// + WorldAbsolute = 1, + /// + /// Exports the GameObject to (0,0,0). + /// For convert to FBX prefab variant only, no UI option. + /// + Reset = 2 + } + + /// + /// LODs to export for LOD groups. + /// + /// + /// Notes: + /// - The FBX Exporter ignores LODs outside of selected hierarchy. + /// - The FBX Exporter does not filter out objects that are used as LODs and doesn't + /// export them if they aren’t direct descendants of their respective LOD Group + /// + public enum LODExportType + { + /// + /// Export all LODs. + /// + All = 0, + /// + /// Export only the highest LOD. + /// + Highest = 1, + /// + /// Export only the lowest LOD. + /// + Lowest = 2 + } + + /// + /// Exception class for FBX export settings. + /// + [System.Serializable] + public class FbxExportSettingsException : System.Exception + { + internal FbxExportSettingsException() {} + + internal FbxExportSettingsException(string message) + : base(message) {} + + internal FbxExportSettingsException(string message, System.Exception inner) + : base(message, inner) {} + + internal FbxExportSettingsException(SerializationInfo info, StreamingContext context) + : base(info, context) {} + } + + [CustomEditor(typeof(ExportSettings))] + internal class ExportSettingsEditor : UnityEditor.Editor + { + Vector2 scrollPos = Vector2.zero; + const float LabelWidth = 180; + const float SelectableLabelMinWidth = 90; + const float BrowseButtonWidth = 25; + const float FieldOffset = 18; + const float BrowseButtonOffset = 5; + + const float ExportOptionsLabelWidth = 205; + const float ExportOptionsFieldOffset = 18; + + private bool m_showExportSettingsOptions = true; + private bool m_showConvertSettingsOptions = true; + + private ExportModelSettingsEditor m_exportModelEditor; + // private ConvertToPrefabSettingsEditor m_convertEditor; + + private void OnEnable() + { + ExportSettings exportSettings = (ExportSettings)target; + m_exportModelEditor = UnityEditor.Editor.CreateEditor(exportSettings.ExportModelSettings) as ExportModelSettingsEditor; + // m_convertEditor = UnityEditor.Editor.CreateEditor(exportSettings.ConvertToPrefabSettings) as ConvertToPrefabSettingsEditor; + } + + static class Style + { + public static GUIContent Application3D = new GUIContent( + "3D Application", + "Select the 3D Application for which you would like to install the Unity integration."); + public static GUIContent KeepOpen = new GUIContent("Keep Open", + "Keep the selected 3D application open after Unity integration install has completed."); + public static GUIContent HideNativeMenu = new GUIContent("Hide Native Menu", + "Replace Maya's native 'Send to Unity' menu with the Unity Integration's menu"); + public static GUIContent InstallIntegrationContent = new GUIContent( + "Install Unity Integration", + "Install and configure the Unity integration for the selected 3D application so that you can import and export directly with this project."); + public static GUIContent RepairMissingScripts = new GUIContent( + "Run Component Updater", + "If FBX exporter version 1.3.0f1 or earlier was previously installed, then links to the FbxPrefab component will need updating.\n" + + "Run this to update all FbxPrefab references in text serialized prefabs and scene files."); + public static GUIContent DisplayOptionsWindow = new GUIContent( + "Display Options Window", + "Show the Convert dialog when converting to an FBX Prefab Variant"); + } + + private void ClearExportWindowSettings(string prefix) where T : ExportOptionsEditorWindow + { + string defaultSettings = null; + if (typeof(T) == typeof(ExportModelEditorWindow)) + { + defaultSettings = EditorJsonUtility.ToJson(ExportSettings.instance.ExportModelSettings.info); + } + else + { + // defaultSettings = EditorJsonUtility.ToJson(ExportSettings.instance.ConvertToPrefabSettings.info); + Log.Error( "typeof(T) == typeof(ExportModelEditorWindow) should not be true" ); + } + + if (EditorWindow.HasOpenInstances()) + { + // clear settings on the window and update the UI + var win = EditorWindow.GetWindow(ExportOptionsEditorWindow.DefaultWindowTitle, focus: false); + win.ResetSessionSettings(defaultSettings); + win.Repaint(); + } + else + { + // Clear what is stored in the session. + // Window will update next time it is opened + ExportOptionsEditorWindow.ResetAllSessionSettings(prefix, defaultSettings); + } + } + + private void ShowExportPathUI(string label, string tooltip, string openFolderPanelTitle, bool isSingletonInstance, bool isConvertToPrefabOptions = false) + { + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(new GUIContent(label, tooltip), GUILayout.Width(ExportOptionsLabelWidth - ExportOptionsFieldOffset)); + + var pathLabels = ExportSettings.GetMixedFbxSavePaths(); + if (isConvertToPrefabOptions) + { + pathLabels = ExportSettings.GetRelativePrefabSavePaths(); + } + + EditorGUI.BeginChangeCheck(); + if (isConvertToPrefabOptions) + { + ExportSettings.instance.SelectedPrefabPath = EditorGUILayout.Popup(ExportSettings.instance.SelectedPrefabPath, pathLabels, GUILayout.MinWidth(SelectableLabelMinWidth)); + } + else + { + ExportSettings.instance.SelectedFbxPath = EditorGUILayout.Popup(ExportSettings.instance.SelectedFbxPath, pathLabels, GUILayout.MinWidth(SelectableLabelMinWidth)); + } + if (EditorGUI.EndChangeCheck() && isSingletonInstance) + { + if (isConvertToPrefabOptions) + { + // ClearExportWindowSettings(ConvertToPrefabEditorWindow.k_SessionStoragePrefix); + Log.Error( "isConvertToPrefabOptions should not be true." ); + } + else + { + ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); + } + } + + // Set export setting for exporting outside the project on choosing a path + if (!isConvertToPrefabOptions) + { + // Set export setting for exporting outside the project on choosing a path + var exportOutsideProject = !pathLabels[ExportSettings.instance.SelectedFbxPath].Substring(0, 6).Equals("Assets"); + m_exportModelEditor.SetExportingOutsideProject(exportOutsideProject); + } + + EditorGUI.BeginDisabledGroup(!isSingletonInstance); + var str = isConvertToPrefabOptions ? "save prefab" : "export"; + var buttonTooltip = string.Format("Browse to a new location to {0} to", str); + if (GUILayout.Button(new GUIContent("...", buttonTooltip), EditorStyles.miniButton, GUILayout.Width(BrowseButtonWidth))) + { + string initialPath = Application.dataPath; + + string fullPath = EditorUtility.SaveFolderPanel( + openFolderPanelTitle, initialPath, null + ); + + // Unless the user canceled, save path. + if (!string.IsNullOrEmpty(fullPath)) + { + var relativePath = ExportSettings.ConvertToAssetRelativePath(fullPath); + + // We're exporting outside Assets folder, so store the absolute path + if (string.IsNullOrEmpty(relativePath)) + { + if (isConvertToPrefabOptions) + { + Debug.LogWarning("Please select a location in the Assets folder"); + } + else + { + ExportSettings.AddFbxSavePath(fullPath, exportOutsideProject: true); + ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); + } + } + // Store the relative path to the Assets folder + else + { + if (isConvertToPrefabOptions) + { + ExportSettings.AddPrefabSavePath(relativePath); + Log.Error( "isConvertToPrefabOptions should not be true" ); + // ClearExportWindowSettings(ConvertToPrefabEditorWindow.k_SessionStoragePrefix); + } + else + { + ExportSettings.AddFbxSavePath(relativePath); + ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); + } + } + // Make sure focus is removed from the selectable label + // otherwise it won't update + GUIUtility.hotControl = 0; + GUIUtility.keyboardControl = 0; + } + } + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + } + + public override void OnInspectorGUI() + { + ExportSettings exportSettings = (ExportSettings)target; + bool isSingletonInstance = this.targets.Length == 1 && this.target == ExportSettings.instance; + + // Increasing the label width so that none of the text gets cut off + EditorGUIUtility.labelWidth = LabelWidth; + + scrollPos = GUILayout.BeginScrollView(scrollPos); + + var version = ModelExporter.GetVersionFromReadme(); + if (!string.IsNullOrEmpty(version)) + { + GUILayout.Label("Version: " + version, EditorStyles.centeredGreyMiniLabel); + EditorGUILayout.Space(); + } + + GUILayout.BeginVertical(); + + EditorGUILayout.LabelField("Export Options", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(Style.DisplayOptionsWindow, GUILayout.Width(LabelWidth)); + exportSettings.DisplayOptionsWindow = EditorGUILayout.Toggle( + exportSettings.DisplayOptionsWindow + ); + EditorGUILayout.EndHorizontal(); + EditorGUILayout.Space(); + + // EXPORT SETTINGS + m_showExportSettingsOptions = EditorGUILayout.Foldout(m_showExportSettingsOptions, "FBX File Options", EditorStyles.foldoutHeader); + if (m_showExportSettingsOptions) + { + EditorGUI.indentLevel++; + ShowExportPathUI("Export Path", "Location where the FBX will be saved.", "Select Export Model Path", isSingletonInstance, isConvertToPrefabOptions: false); + + EditorGUI.BeginChangeCheck(); + m_exportModelEditor.LabelWidth = ExportOptionsLabelWidth; + m_exportModelEditor.FieldOffset = ExportOptionsFieldOffset; + m_exportModelEditor.OnInspectorGUI(); + if (EditorGUI.EndChangeCheck() && isSingletonInstance) + { + ClearExportWindowSettings(ExportModelEditorWindow.k_SessionStoragePrefix); + } + EditorGUI.indentLevel--; + } + // -------------------------- + + // CONVERT TO PREFAB SETTINGS + m_showConvertSettingsOptions = EditorGUILayout.Foldout(m_showConvertSettingsOptions, "Convert to Prefab Options", EditorStyles.foldoutHeader); + if (m_showConvertSettingsOptions) + { + // EditorGUI.indentLevel++; + // ShowExportPathUI("Prefab Path", "Relative path for saving FBX Prefab Variants.", "Select FBX Prefab Variant Save Path", isSingletonInstance, isConvertToPrefabOptions: true); + // + // EditorGUI.BeginChangeCheck(); + // m_convertEditor.LabelWidth = ExportOptionsLabelWidth; + // m_convertEditor.FieldOffset = ExportOptionsFieldOffset; + // m_convertEditor.OnInspectorGUI(); + // if (EditorGUI.EndChangeCheck() && isSingletonInstance) + // { + // ClearExportWindowSettings(ConvertToPrefabEditorWindow.k_SessionStoragePrefix); + // } + // EditorGUI.indentLevel--; + + Log.Error( "m_showConvertSettingsOptions should not be true" ); + } + // -------------------------- + + EditorGUI.indentLevel--; + + EditorGUILayout.LabelField("Integration", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(Style.Application3D, GUILayout.Width(LabelWidth)); + + // dropdown to select Maya version to use + var options = ExportSettings.GetDCCOptions(); + + exportSettings.SelectedDCCApp = EditorGUILayout.Popup(exportSettings.SelectedDCCApp, options); + + EditorGUI.BeginDisabledGroup(!isSingletonInstance); + if (GUILayout.Button(new GUIContent("...", "Browse to a 3D application in a non-default location"), EditorStyles.miniButton, GUILayout.Width(BrowseButtonWidth))) + { + var ext = ""; + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + ext = "exe"; + break; + case RuntimePlatform.OSXEditor: + ext = "app"; + break; + default: + throw new System.NotImplementedException(); + } + + string dccPath = EditorUtility.OpenFilePanel("Select Digital Content Creation Application", ExportSettings.FirstValidVendorLocation, ext); + + // check that the path is valid and references the maya executable + if (!string.IsNullOrEmpty(dccPath)) + { + ExportSettings.DCCType foundDCC = ExportSettings.DCCType.Maya; + var foundDCCPath = TryFindDCC(dccPath, ext, ExportSettings.DCCType.Maya); + if (foundDCCPath == null && Application.platform == RuntimePlatform.WindowsEditor) + { + foundDCCPath = TryFindDCC(dccPath, ext, ExportSettings.DCCType.Max); + foundDCC = ExportSettings.DCCType.Max; + } + if (foundDCCPath == null) + { + Debug.LogError(string.Format("Could not find supported 3D application at: \"{0}\"", Path.GetDirectoryName(dccPath))); + } + else + { + dccPath = foundDCCPath; + ExportSettings.AddDCCOption(dccPath, foundDCC); + } + Repaint(); + } + } + EditorGUI.EndDisabledGroup(); + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(Style.KeepOpen, GUILayout.Width(LabelWidth)); + exportSettings.LaunchAfterInstallation = EditorGUILayout.Toggle( + exportSettings.LaunchAfterInstallation + ); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(Style.HideNativeMenu, GUILayout.Width(LabelWidth)); + exportSettings.HideSendToUnityMenuProperty = EditorGUILayout.Toggle( + exportSettings.HideSendToUnityMenuProperty + ); + GUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // disable button if no 3D application is available + EditorGUI.BeginDisabledGroup(!isSingletonInstance || !ExportSettings.CanInstall()); + if (GUILayout.Button(Style.InstallIntegrationContent)) + { + EditorApplication.delayCall += IntegrationsUI.InstallDCCIntegration; + } + EditorGUI.EndDisabledGroup(); + + EditorGUILayout.Space(); + + EditorGUI.indentLevel--; + EditorGUILayout.LabelField("FBX Prefab Component Updater", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + + EditorGUILayout.Space(); + + EditorGUI.BeginDisabledGroup(!isSingletonInstance); + if (GUILayout.Button(Style.RepairMissingScripts)) + { + var componentUpdater = new RepairMissingScripts(); + var filesToRepairCount = componentUpdater.AssetsToRepairCount; + var dialogTitle = "FBX Prefab Component Updater"; + if (filesToRepairCount > 0) + { + bool result = UnityEditor.EditorUtility.DisplayDialog(dialogTitle, + string.Format("Found {0} prefab(s) and/or scene(s) with components requiring update.\n\n" + + "If you choose 'Go Ahead', the FbxPrefab components in these assets " + + "will be automatically updated to work with the latest FBX exporter.\n" + + "You should make a backup before proceeding.", filesToRepairCount), + "I Made a Backup. Go Ahead!", "No Thanks"); + if (result) + { + componentUpdater.ReplaceGUIDInTextAssets(); + } + else + { + var assetsToRepair = componentUpdater.GetAssetsToRepair(); + Debug.LogFormat("Failed to update the FbxPrefab components in the following files:\n{0}", string.Join("\n", assetsToRepair)); + } + } + else + { + UnityEditor.EditorUtility.DisplayDialog(dialogTitle, + "Couldn't find any prefabs or scenes that require updating", "Ok"); + } + } + EditorGUI.EndDisabledGroup(); + EditorGUI.indentLevel--; + + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + GUILayout.EndScrollView(); + + if (GUI.changed) + { + // Only save the settings if we are in the Singleton instance. + // Otherwise, the user is editing a preset and we don't need to save. + if (isSingletonInstance) + { + exportSettings.Save(); + } + } + } + + private static string TryFindDCC(string dccPath, string ext, ExportSettings.DCCType dccType) + { + string dccName = ""; + switch (dccType) + { + case ExportSettings.DCCType.Maya: + dccName = "maya"; + break; + case ExportSettings.DCCType.Max: + dccName = "3dsmax"; + break; + default: + throw new System.NotImplementedException(); + } + + if (Path.GetFileNameWithoutExtension(dccPath).ToLower().Equals(dccName)) + { + return dccPath; + } + + // clicked on the wrong application, try to see if we can still find + // a dcc in this directory. + var dccDir = new DirectoryInfo(Path.GetDirectoryName(dccPath)); + FileSystemInfo[] files = {}; + switch (Application.platform) + { + case RuntimePlatform.OSXEditor: + files = dccDir.GetDirectories("*." + ext); + break; + case RuntimePlatform.WindowsEditor: + files = dccDir.GetFiles("*." + ext); + break; + default: + throw new System.NotImplementedException(); + } + + string newDccPath = null; + foreach (var file in files) + { + var filename = Path.GetFileNameWithoutExtension(file.Name).ToLower(); + if (filename.Equals(dccName)) + { + newDccPath = file.FullName.Replace("\\", "/"); + break; + } + } + return newDccPath; + } + + [SettingsProvider] + static SettingsProvider CreateFbxExportSettingsProvider() + { + ExportSettings.instance.name = "FBX Export Settings"; + ExportSettings.instance.Load(); + + var provider = AssetSettingsProvider.CreateProviderFromObject( + "Project/Fbx Export [ DaddyFrosty ]", ExportSettings.instance, GetSearchKeywordsFromGUIContentProperties(typeof(Style))); +#if UNITY_2019_1_OR_NEWER + provider.inspectorUpdateHandler += () => + { + if (provider.settingsEditor != null && + provider.settingsEditor.serializedObject.UpdateIfRequiredOrScript()) + { + provider.Repaint(); + } + }; +#else + provider.activateHandler += (searchContext, rootElement) => + { + if (provider.settingsEditor != null && + provider.settingsEditor.serializedObject.UpdateIfRequiredOrScript()) + { + provider.Repaint(); + } + }; +#endif // UNITY_2019_1_OR_NEWER + return provider; + } + + static IEnumerable GetSearchKeywordsFromGUIContentProperties(Type type) + { + return type.GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(field => typeof(GUIContent).IsAssignableFrom(field.FieldType)) + .Select(field => ((GUIContent)field.GetValue(null)).text) + .Concat(type.GetProperties(BindingFlags.Static | BindingFlags.Public) + .Where(prop => typeof(GUIContent).IsAssignableFrom(prop.PropertyType)) + .Select(prop => ((GUIContent)prop.GetValue(null, null)).text)) + .Where(content => content != null) + .Select(content => content.ToLowerInvariant()) + .Distinct(); + } + } + + [FilePath("ProjectSettings/FbxExportSettings.asset", FilePathAttribute.Location.ProjectFolder)] + internal class ExportSettings : ScriptableObject + { + internal const string kDefaultSavePath = "."; + private static List s_PreferenceList = new List() {kMayaOptionName, kMayaLtOptionName, kMaxOptionName}; + //Any additional names require a space after the name + internal const string kMaxOptionName = "3ds Max "; + internal const string kMayaOptionName = "Maya "; + internal const string kMayaLtOptionName = "Maya LT"; + + private static ExportSettings s_Instance; + public static ExportSettings instance + { + get + { + if (s_Instance == null) + { + s_Instance = ScriptableObject.CreateInstance(); + s_Instance.Load(); + // don't save the export settings to the scene, + // otherwise they could be deleted on new scene, breaking + // the UI if the settings are being inspected. + s_Instance.hideFlags = HideFlags.DontSaveInEditor; + } + return s_Instance; + } + } + + internal ExportSettings() + { + if (s_Instance != null) + { + // The user has most likely created a preset. + } + } + + internal void SaveToFile() + { + if (s_Instance == null) + { + Debug.Log("Cannot save ScriptableSingleton: no instance!"); + return; + } + string filePath = GetFilePath(); + if (!string.IsNullOrEmpty(filePath)) + { + string directoryName = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + System.IO.File.WriteAllText(filePath, EditorJsonUtility.ToJson(s_Instance, true)); + } + } + + private static string GetFilePath() + { + foreach (var attr in typeof(ExportSettings).GetCustomAttributes(true)) + { + FilePathAttribute filePathAttribute = attr as FilePathAttribute; + if (filePathAttribute != null) + { + return filePathAttribute.filepath; + } + } + return null; + } + + // NOTE: using "Verbose" and "VerboseProperty" to handle backwards compatibility with older FbxExportSettings.asset files. + // The variable name is used when serializing, so changing the variable name would prevent older FbxExportSettings.asset files + // from loading this property. + [SerializeField] + private bool Verbose = false; + public bool VerboseProperty + { + get { return Verbose; } + set { Verbose = value; } + } + + private static string DefaultIntegrationSavePath + { + get + { + return Path.GetDirectoryName(Application.dataPath); + } + } + + private static string GetMayaLocationFromEnvironmentVariable(string env) + { + string result = null; + + if (string.IsNullOrEmpty(env)) + return null; + + string location = Environment.GetEnvironmentVariable(env); + + if (string.IsNullOrEmpty(location)) + return null; + + //Remove any extra slashes on the end + //Maya would accept a single slash in either direction, so we should be able to + location = location.Replace("\\", "/"); + location = location.TrimEnd('/'); + + if (!Directory.Exists(location)) + return null; + + if (Application.platform == RuntimePlatform.WindowsEditor) + { + //If we are on Windows, we need only go up one location to get to the "Autodesk" folder. + result = Directory.GetParent(location).ToString(); + } + else if (Application.platform == RuntimePlatform.OSXEditor) + { + //We can assume our path is: /Applications/Autodesk/maya2017/Maya.app/Contents + //So we need to go up three folders. + + var appFolder = Directory.GetParent(location); + if (appFolder != null) + { + var versionFolder = Directory.GetParent(appFolder.ToString()); + if (versionFolder != null) + { + var autoDeskFolder = Directory.GetParent(versionFolder.ToString()); + if (autoDeskFolder != null) + { + result = autoDeskFolder.ToString(); + } + } + } + } + return NormalizePath(result, false); + } + + /// + /// Returns a set of valid vendor folder paths with no trailing '/' + /// + private static HashSet GetCustomVendorLocations() + { + HashSet result = null; + + var environmentVariable = Environment.GetEnvironmentVariable("UNITY_3DAPP_VENDOR_LOCATIONS"); + + if (!string.IsNullOrEmpty(environmentVariable)) + { + result = new HashSet(); + string[] locations = environmentVariable.Split(';'); + foreach (var location in locations) + { + if (Directory.Exists(location)) + { + result.Add(NormalizePath(location, false)); + } + } + } + return result; + } + + private static HashSet GetDefaultVendorLocations() + { + string platformDefault; + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + platformDefault = "C:/Program Files/Autodesk"; + break; + case RuntimePlatform.OSXEditor: + platformDefault = "/Applications/Autodesk"; + break; + case RuntimePlatform.LinuxEditor: + platformDefault = "/usr/autodesk"; + break; + default: + throw new NotImplementedException(); + } + + HashSet existingDirectories = new HashSet(); + if (!string.IsNullOrEmpty(platformDefault) && Directory.Exists(platformDefault)) + { + existingDirectories.Add(platformDefault); + } + return existingDirectories; + } + + /// + /// Retrieve available vendor locations. + /// If there is valid alternative vendor locations, do not use defaults + /// always use MAYA_LOCATION when available + /// + internal static List DCCVendorLocations + { + get + { + HashSet result = GetCustomVendorLocations(); + + if (result == null) + { + result = GetDefaultVendorLocations(); + } + + var additionalLocation = GetMayaLocationFromEnvironmentVariable("MAYA_LOCATION"); + + if (!string.IsNullOrEmpty(additionalLocation)) + { + result.Add(additionalLocation); + } + + return result.ToList(); + } + } + + [SerializeField] + private bool launchAfterInstallation = true; + public bool LaunchAfterInstallation + { + get { return launchAfterInstallation; } + set { launchAfterInstallation = value; } + } + + [SerializeField] + private bool HideSendToUnityMenu = true; + public bool HideSendToUnityMenuProperty + { + get { return HideSendToUnityMenu; } + set { HideSendToUnityMenu = value; } + } + + [SerializeField] + private bool BakeAnimation = false; + internal bool BakeAnimationProperty + { + get { return BakeAnimation; } + set { BakeAnimation = value; } + } + + [SerializeField] + private bool showConvertToPrefabDialog = true; + public bool DisplayOptionsWindow + { + get { return showConvertToPrefabDialog; } + set { showConvertToPrefabDialog = value; } + } + + [SerializeField] + private string integrationSavePath; + internal static string IntegrationSavePath + { + get + { + //If the save path gets messed up and ends up not being valid, just use the project folder as the default + if (string.IsNullOrEmpty(instance.integrationSavePath) || + !Directory.Exists(instance.integrationSavePath)) + { + //The project folder, above the asset folder + instance.integrationSavePath = DefaultIntegrationSavePath; + } + return instance.integrationSavePath; + } + set + { + instance.integrationSavePath = value; + } + } + + [SerializeField] + private int selectedDCCApp = 0; + internal int SelectedDCCApp + { + get { return selectedDCCApp; } + set { selectedDCCApp = value; } + } + + /// + /// The path where Convert To Model will save the new fbx and prefab. + /// + /// To help teams work together, this is stored to be relative to the + /// Application.dataPath, and the path separator is the forward-slash + /// (e.g. unix and http, not windows). + /// + /// Use GetRelativeSavePath / SetRelativeSavePath to get/set this + /// value, properly interpreted for the current platform. + /// + [SerializeField] + private List prefabSavePaths = new List(); + internal List GetCopyOfPrefabSavePaths() + { + return new List(prefabSavePaths); + } + + [SerializeField] + private List fbxSavePaths = new List(); + internal List GetCopyOfFbxSavePaths() + { + return new List(fbxSavePaths); + } + + [SerializeField] + private int selectedFbxPath = 0; + public int SelectedFbxPath + { + get { return selectedFbxPath; } + set { selectedFbxPath = value; } + } + + [SerializeField] + private int selectedPrefabPath = 0; + public int SelectedPrefabPath + { + get { return selectedPrefabPath; } + set { selectedPrefabPath = value; } + } + + private int maxStoredSavePaths = 5; + + // List of names in order that they appear in option list + [SerializeField] + private List dccOptionNames = new List(); + // List of paths in order that they appear in the option list + [SerializeField] + private List dccOptionPaths; + + // don't serialize as ScriptableObject does not get properly serialized on export + [System.NonSerialized] + private ExportModelSettings m_exportModelSettings; + internal ExportModelSettings ExportModelSettings + { + get + { + if (!m_exportModelSettings) + { + m_exportModelSettings = ScriptableObject.CreateInstance(typeof(ExportModelSettings)) as ExportModelSettings; + } + if (this.exportModelSettingsSerialize != null) + { + m_exportModelSettings.info = this.exportModelSettingsSerialize; + } + else + { + this.exportModelSettingsSerialize = m_exportModelSettings.info; + } + return m_exportModelSettings; + } + set { m_exportModelSettings = value; } + } + + // store contents of export model settings for serialization + [SerializeField] + private ExportModelSettingsSerialize exportModelSettingsSerialize; + + // [System.NonSerialized] + // private ConvertToPrefabSettings m_convertToPrefabSettings; + // internal ConvertToPrefabSettings ConvertToPrefabSettings + // { + // get + // { + // if (!m_convertToPrefabSettings) + // { + // m_convertToPrefabSettings = ScriptableObject.CreateInstance(typeof(ConvertToPrefabSettings)) as ConvertToPrefabSettings; + // } + // if (this.convertToPrefabSettingsSerialize != null) + // { + // m_convertToPrefabSettings.info = this.convertToPrefabSettingsSerialize; + // } + // else + // { + // this.convertToPrefabSettingsSerialize = m_convertToPrefabSettings.info; + // } + // return m_convertToPrefabSettings; + // } + // set { m_convertToPrefabSettings = value; } + // } + // + // [SerializeField] + // private ConvertToPrefabSettingsSerialize convertToPrefabSettingsSerialize; + + internal void LoadDefaults() + { + LaunchAfterInstallation = true; + HideSendToUnityMenuProperty = true; + prefabSavePaths = new List(){ kDefaultSavePath }; + fbxSavePaths = new List(){ kDefaultSavePath }; + integrationSavePath = DefaultIntegrationSavePath; + dccOptionPaths = new List(); + dccOptionNames = new List(); + BakeAnimationProperty = false; + exportModelSettingsSerialize = new ExportModelSettingsSerialize(); + ExportModelSettings.info = exportModelSettingsSerialize; + DisplayOptionsWindow = true; + // convertToPrefabSettingsSerialize = new ConvertToPrefabSettingsSerialize(); + // ConvertToPrefabSettings.info = convertToPrefabSettingsSerialize; + } + + /// + /// Increments the name if there is a duplicate in dccAppOptions. + /// + /// The unique name. + /// Name. + internal static string GetUniqueDCCOptionName(string name) + { + Debug.Assert(instance != null); + if (name == null) + { + return null; + } + if (!instance.dccOptionNames.Contains(name)) + { + return name; + } + var format = "{1} ({0})"; + int index = 1; + // try extracting the current index from the name and incrementing it + var result = System.Text.RegularExpressions.Regex.Match(name, @"\((?\d+?)\)$"); + if (result != null) + { + var number = result.Groups["number"].Value; + int tempIndex; + if (int.TryParse(number, out tempIndex)) + { + var indexOfNumber = name.LastIndexOf(number); + format = name.Remove(indexOfNumber, number.Length).Insert(indexOfNumber, "{0}"); + index = tempIndex + 1; + } + } + + string uniqueName = null; + do + { + uniqueName = string.Format(format, index, name); + index++; + } + while (instance.dccOptionNames.Contains(uniqueName)); + + return uniqueName; + } + + internal void SetDCCOptionNames(List newList) + { + dccOptionNames = newList; + } + + internal void SetDCCOptionPaths(List newList) + { + dccOptionPaths = newList; + } + + internal void ClearDCCOptionNames() + { + dccOptionNames.Clear(); + } + + internal void ClearDCCOptions() + { + SetDCCOptionNames(null); + SetDCCOptionPaths(null); + } + + /// + /// + /// Find the latest program available and make that the default choice. + /// Will always take any Maya version over any 3ds Max version. + /// + /// Returns the index of the most recent program in the list of dccOptionNames + /// Returns -1 on error. + /// + internal int PreferredDCCApp + { + get + { + if (dccOptionNames == null) + { + return -1; + } + + int result = -1; + int newestDCCVersionNumber = -1; + + for (int i = 0; i < dccOptionNames.Count; i++) + { + int versionToCheck = FindDCCVersion(dccOptionNames[i]); + if (versionToCheck == -1) + { + if (dccOptionNames[i] == "MAYA_LOCATION") + return i; + + continue; + } + if (versionToCheck > newestDCCVersionNumber) + { + result = i; + newestDCCVersionNumber = versionToCheck; + } + else if (versionToCheck == newestDCCVersionNumber) + { + int selection = ChoosePreferredDCCApp(result, i); + if (selection == i) + { + result = i; + newestDCCVersionNumber = FindDCCVersion(dccOptionNames[i]); + } + } + } + + return result; + } + } + /// + /// Takes the index of two program names from dccOptionNames and chooses our preferred one based on the preference list + /// This happens in case of a tie between two programs with the same release year / version + /// + /// + /// + /// + private int ChoosePreferredDCCApp(int optionA, int optionB) + { + Debug.Assert(optionA >= 0 && optionB >= 0 && optionA < dccOptionNames.Count && optionB < dccOptionNames.Count); + if (dccOptionNames.Count == 0) + { + return -1; + } + var appA = dccOptionNames[optionA]; + var appB = dccOptionNames[optionB]; + if (appA == null || appB == null || appA.Length <= 0 || appB.Length <= 0) + { + return -1; + } + + int scoreA = s_PreferenceList.FindIndex(app => RemoveSpacesAndNumbers(app).Equals(RemoveSpacesAndNumbers(appA))); + + int scoreB = s_PreferenceList.FindIndex(app => RemoveSpacesAndNumbers(app).Equals(RemoveSpacesAndNumbers(appB))); + + return scoreA < scoreB ? optionA : optionB; + } + + /// + /// Takes a given string and removes any spaces or numbers from it + /// + /// + internal static string RemoveSpacesAndNumbers(string s) + { + return System.Text.RegularExpressions.Regex.Replace(s, @"[\s^0-9]", ""); + } + + /// + /// Finds the version based off of the title of the application + /// + /// + /// the year/version OR -1 if the year could not be parsed + private static int FindDCCVersion(string AppName) + { + if (string.IsNullOrEmpty(AppName)) + { + return -1; + } + AppName = AppName.Trim(); + if (string.IsNullOrEmpty(AppName)) + return -1; + + string[] piecesArray = AppName.Split(' '); + if (piecesArray.Length < 2) + { + return -1; + } + //Get the number, which is always the last chunk separated by a space. + string number = piecesArray[piecesArray.Length - 1]; + + int version; + if (int.TryParse(number, out version)) + { + return version; + } + else + { + //remove any letters in the string in a final attempt to extract an int from it (this will happen with MayaLT, for example) + string AppNameCopy = AppName; + string stringWithoutLetters = System.Text.RegularExpressions.Regex.Replace(AppNameCopy, "[^0-9]", ""); + + if (int.TryParse(stringWithoutLetters, out version)) + { + return version; + } + + float fVersion; + //In case we are looking at something with a decimal based version- the int parse will fail so we'll need to parse it as a float. + if (float.TryParse(number, out fVersion)) + { + return (int)fVersion; + } + return -1; + } + } + + /// + /// Find Maya and 3DsMax installations at default install path. + /// Add results to given dictionary. + /// + /// If MAYA_LOCATION is set, add this to the list as well. + /// + private static void FindDCCInstalls() + { + var dccOptionNames = instance.dccOptionNames; + var dccOptionPaths = instance.dccOptionPaths; + + // find dcc installation from vendor locations + foreach (var vendorLocation in DCCVendorLocations) + { + if (!Directory.Exists(vendorLocation)) + { + // no autodesk products installed + continue; + } + // List that directory and find the right version: + // either the newest version, or the exact version we wanted. + var adskRoot = new System.IO.DirectoryInfo(vendorLocation); + foreach (var productDir in adskRoot.GetDirectories()) + { + var product = productDir.Name; + + // Only accept those that start with 'maya' in either case. + if (product.StartsWith("maya", StringComparison.InvariantCultureIgnoreCase)) + { + string version = product.Substring("maya".Length); + dccOptionPaths.Add(GetMayaExePathFromLocation(productDir.FullName.Replace("\\", "/"))); + dccOptionNames.Add(GetUniqueDCCOptionName(kMayaOptionName + version)); + continue; + } + + if (product.StartsWith("3ds max", StringComparison.InvariantCultureIgnoreCase)) + { + var exePath = string.Format("{0}/{1}", productDir.FullName.Replace("\\", "/"), "3dsmax.exe"); + + string version = product.Substring("3ds max ".Length); + var maxOptionName = GetUniqueDCCOptionName(kMaxOptionName + version); + + if (IsEarlierThanMax2017(maxOptionName)) + { + continue; + } + + dccOptionPaths.Add(exePath); + dccOptionNames.Add(maxOptionName); + } + } + } + + // add extra locations defined by special environment variables + string location = GetMayaLocationFromEnvironmentVariable("MAYA_LOCATION"); + + if (!string.IsNullOrEmpty(location)) + { + dccOptionPaths.Add(GetMayaExePathFromLocation(location)); + dccOptionNames.Add("MAYA_LOCATION"); + } + + instance.SelectedDCCApp = instance.PreferredDCCApp; + } + + /// + /// Returns the first valid folder in our list of vendor locations + /// + /// The first valid vendor location + internal static string FirstValidVendorLocation + { + get + { + List locations = DCCVendorLocations; + for (int i = 0; i < locations.Count; i++) + { + //Look through the list of locations we have and take the first valid one + if (Directory.Exists(locations[i])) + { + return locations[i]; + } + } + //if no valid locations exist, just take us to the project folder + return Directory.GetCurrentDirectory(); + } + } + + /// + /// Gets the maya exe at Maya install location. + /// + /// The maya exe path. + /// Location of Maya install. + private static string GetMayaExePathFromLocation(string location) + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + return $"{location}/bin/maya.exe"; + case RuntimePlatform.OSXEditor: + // MAYA_LOCATION on mac is set by Autodesk to be the + // Contents directory. But let's make it easier on people + // and allow just having it be the app bundle or a + // directory that holds the app bundle. + if (location.EndsWith(".app/Contents")) + { + return $"{location}/MacOS/Maya"; + } + else if (location.EndsWith(".app")) + { + return $"{location}/Contents/MacOS/Maya"; + } + else + { + return $"{location}/Maya.app/Contents/MacOS/Maya"; + } + case RuntimePlatform.LinuxEditor: + return $"{location}/bin/maya"; + default: + throw new NotImplementedException(); + } + } + + internal static GUIContent[] GetDCCOptions() + { + if (instance.dccOptionNames == null || + instance.dccOptionNames.Count != instance.dccOptionPaths.Count || + instance.dccOptionNames.Count == 0) + { + instance.dccOptionPaths = new List(); + instance.dccOptionNames = new List(); + FindDCCInstalls(); + } + // store the selected app if any + string prevSelection = SelectedDCCPath; + + // remove options that no longer exist + List pathsToDelete = new List(); + List namesToDelete = new List(); + for (int i = 0; i < instance.dccOptionPaths.Count; i++) + { + var dccPath = instance.dccOptionPaths[i]; + if (!File.Exists(dccPath)) + { + namesToDelete.Add(instance.dccOptionNames[i]); + pathsToDelete.Add(dccPath); + } + } + foreach (var str in pathsToDelete) + { + instance.dccOptionPaths.Remove(str); + } + foreach (var str in namesToDelete) + { + instance.dccOptionNames.Remove(str); + } + + // set the selected DCC app to the previous selection + instance.SelectedDCCApp = instance.dccOptionPaths.IndexOf(prevSelection); + if (instance.SelectedDCCApp < 0) + { + // find preferred app if previous selection no longer exists + instance.SelectedDCCApp = instance.PreferredDCCApp; + } + + if (instance.dccOptionPaths.Count <= 0) + { + instance.SelectedDCCApp = 0; + return new GUIContent[] + { + new GUIContent("") + }; + } + + GUIContent[] optionArray = new GUIContent[instance.dccOptionPaths.Count]; + for (int i = 0; i < instance.dccOptionPaths.Count; i++) + { + optionArray[i] = new GUIContent( + instance.dccOptionNames[i], + instance.dccOptionPaths[i] + ); + } + return optionArray; + } + + internal enum DCCType { Maya, Max }; + + internal static void AddDCCOption(string newOption, DCCType dcc) + { + if (Application.platform == RuntimePlatform.OSXEditor && dcc == DCCType.Maya) + { + // on OSX we get a path ending in .app, which is not quite the exe + newOption = GetMayaExePathFromLocation(newOption); + } + + var dccOptionPaths = instance.dccOptionPaths; + if (dccOptionPaths.Contains(newOption)) + { + instance.SelectedDCCApp = dccOptionPaths.IndexOf(newOption); + return; + } + + string optionName = ""; + switch (dcc) + { + case DCCType.Maya: + var version = AskMayaVersion(newOption); + if (version == null) + { + Debug.LogError("This version of Maya could not be launched properly"); + UnityEditor.EditorUtility.DisplayDialog("Error Loading 3D Application", + "Failed to add Maya option, could not get version number from maya.exe", + "Ok"); + return; + } + optionName = GetUniqueDCCOptionName("Maya " + version); + break; + case DCCType.Max: + optionName = GetMaxOptionName(newOption); + if (ExportSettings.IsEarlierThanMax2017(optionName)) + { + Debug.LogError("Earlier than 3ds Max 2017 is not supported"); + UnityEditor.EditorUtility.DisplayDialog( + "Error adding 3D Application", + "Unity Integration only supports 3ds Max 2017 or later", + "Ok"); + return; + } + break; + default: + throw new System.NotImplementedException(); + } + + instance.dccOptionNames.Add(optionName); + dccOptionPaths.Add(newOption); + instance.SelectedDCCApp = dccOptionPaths.Count - 1; + } + + /// + /// Ask the version number by running maya. + /// + internal static string AskMayaVersion(string exePath) + { + System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); + myProcess.StartInfo.FileName = exePath; + myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; + myProcess.StartInfo.CreateNoWindow = true; + myProcess.StartInfo.UseShellExecute = false; + myProcess.StartInfo.RedirectStandardOutput = true; + myProcess.StartInfo.Arguments = "-v"; + myProcess.EnableRaisingEvents = true; + myProcess.Start(); + string resultString = myProcess.StandardOutput.ReadToEnd(); + myProcess.WaitForExit(); + + // Output is like: Maya 2018, Cut Number 201706261615 + // We want the stuff after 'Maya ' and before the comma. + // (Uni-31601) less brittle! Consider also the mel command "about -version". + if (string.IsNullOrEmpty(resultString)) + { + return null; + } + + resultString = resultString.Trim(); + var commaIndex = resultString.IndexOf(','); + + if (commaIndex != -1) + { + const int versionStart = 5; // length of "Maya " + return resultString.Length > versionStart ? resultString.Substring(0, commaIndex).Substring(versionStart) : null; + } + else + { + //This probably means we tried to launch Maya to check the version but it was some sort of broken maya. + //We'll just return null and throw an error for it. + return null; + } + } + + /// + /// Gets the unique label for a new 3DsMax dropdown option. + /// + /// The 3DsMax dropdown option label. + /// Exe path. + internal static string GetMaxOptionName(string exePath) + { + return GetUniqueDCCOptionName(Path.GetFileName(Path.GetDirectoryName(exePath))); + } + + internal static bool IsEarlierThanMax2017(string AppName) + { + int version = FindDCCVersion(AppName); + return version != -1 && version < 2017; + } + + internal static string SelectedDCCPath + { + get + { + return (instance.dccOptionPaths.Count > 0 && + instance.SelectedDCCApp >= 0 && + instance.SelectedDCCApp < instance.dccOptionPaths.Count) ? instance.dccOptionPaths[instance.SelectedDCCApp] : ""; + } + } + + internal static string SelectedDCCName + { + get + { + return (instance.dccOptionNames.Count > 0 && + instance.SelectedDCCApp >= 0 && + instance.SelectedDCCApp < instance.dccOptionNames.Count) ? instance.dccOptionNames[instance.SelectedDCCApp] : ""; + } + } + + internal static bool CanInstall() + { + return instance.dccOptionPaths.Count > 0; + } + + internal static string GetProjectRelativePath(string fullPath) + { + var assetRelativePath = ConvertToAssetRelativePath(fullPath); + var projectRelativePath = "Assets/" + assetRelativePath; + if (string.IsNullOrEmpty(assetRelativePath)) + { + throw new FbxExportSettingsException("Path " + fullPath + " must be in the Assets folder."); + } + return projectRelativePath; + } + + /// + /// The relative save paths for given absolute paths. + /// This is relative to the Application.dataPath ; it uses '/' as the + /// separator on all platforms. + /// + internal static string[] GetRelativeSavePaths(List exportSavePaths) + { + if (exportSavePaths == null) + { + return null; + } + + if (exportSavePaths.Count == 0) + { + exportSavePaths.Add(kDefaultSavePath); + } + string[] relSavePaths = new string[exportSavePaths.Count]; + // use special forward slash unicode char as "/" is a special character + // that affects the dropdown layout. + string forwardslash = " \u2044 "; + for (int i = 0; i < relSavePaths.Length; i++) + { + relSavePaths[i] = string.Format("Assets{0}{1}", forwardslash, exportSavePaths[i] == "." ? "" : NormalizePath(exportSavePaths[i], isRelative: true).Replace("/", forwardslash)); + } + return relSavePaths; + } + + /// + /// Returns the paths for display in the menu. + /// Paths inside the Assets folder are relative, while those outside are kept absolute. + /// + internal static string[] GetMixedSavePaths(List exportSavePaths) + { + string[] displayPaths = new string[exportSavePaths.Count]; + string forwardslash = " \u2044 "; + for (int i = 0; i < displayPaths.Length; i++) + { + // if path is in Assets folder, shorten it + if (!Path.IsPathRooted(exportSavePaths[i])) + { + displayPaths[i] = string.Format("Assets{0}{1}", forwardslash, exportSavePaths[i] == "." ? "" : NormalizePath(exportSavePaths[i], isRelative: true).Replace("/", forwardslash)); + } + else + { + displayPaths[i] = exportSavePaths[i].Replace("/", forwardslash); + } + } + + return displayPaths; + } + + /// + /// The path where Export model will save the new fbx. + /// This is relative to the Application.dataPath ; it uses '/' as the + /// separator on all platforms. + /// Only returns the paths within the Assets folder of the project. + /// + internal static string[] GetRelativeFbxSavePaths() + { + return GetRelativeFbxSavePaths(instance.fbxSavePaths, ref instance.selectedFbxPath); + } + + /// + /// The path where Export model will save the new fbx. + /// This is relative to the Application.dataPath ; it uses '/' as the + /// separator on all platforms. + /// Only returns the paths within the Assets folder of the project. + /// + internal static string[] GetRelativeFbxSavePaths(List fbxSavePaths, ref int pathIndex) + { + // sort the list of paths, putting project paths first + fbxSavePaths.Sort((x, y) => Path.IsPathRooted(x).CompareTo(Path.IsPathRooted(y))); + var relPathCount = fbxSavePaths.FindAll(x => !Path.IsPathRooted(x)).Count; + + // reset selected path if it's out of range + if (pathIndex > relPathCount - 1) + { + pathIndex = 0; + } + + return GetRelativeSavePaths(fbxSavePaths.GetRange(0, relPathCount)); + } + + /// + /// The path where Convert to Prefab will save the new prefab. + /// This is relative to the Application.dataPath ; it uses '/' as the + /// separator on all platforms. + /// + internal static string[] GetRelativePrefabSavePaths() + { + return GetRelativeSavePaths(instance.prefabSavePaths); + } + + /// + /// The paths formatted for display in the menu. + /// Paths outside the Assets folder are kept as they are and ones inside are shortened. + /// + internal static string[] GetMixedFbxSavePaths() + { + return GetMixedSavePaths(instance.fbxSavePaths); + } + + /// + /// Adds the save path to given save path list. + /// + /// Save path. + /// Export save paths. + internal static void AddSavePath(string savePath, List exportSavePaths, bool exportOutsideProject = false) + { + if (exportSavePaths == null) + { + return; + } + + if (exportOutsideProject) + { + savePath = NormalizePath(savePath, isRelative: false); + } + else + { + savePath = NormalizePath(savePath, isRelative: true); + } + + if (exportSavePaths.Contains(savePath)) + { + // move to first place if it isn't already + if (exportSavePaths[0] == savePath) + { + return; + } + exportSavePaths.Remove(savePath); + } + + if (exportSavePaths.Count >= instance.maxStoredSavePaths) + { + // remove last used path + exportSavePaths.RemoveAt(exportSavePaths.Count - 1); + } + + exportSavePaths.Insert(0, savePath); + } + + internal static void AddFbxSavePath(string savePath, bool exportOutsideProject = false) + { + AddSavePath(savePath, instance.fbxSavePaths, exportOutsideProject); + instance.SelectedFbxPath = 0; + } + + internal static void AddPrefabSavePath(string savePath) + { + AddSavePath(savePath, instance.prefabSavePaths); + instance.SelectedPrefabPath = 0; + } + + internal static string GetAbsoluteSavePath(string savePath) + { + var projectAbsolutePath = Path.Combine(Application.dataPath, savePath); + projectAbsolutePath = NormalizePath(projectAbsolutePath, isRelative: false, separator: Path.DirectorySeparatorChar); + + // if path is outside Assets folder, it's already absolute so return the original path + if (string.IsNullOrEmpty(ExportSettings.ConvertToAssetRelativePath(projectAbsolutePath))) + { + return savePath; + } + + return projectAbsolutePath; + } + + internal static string FbxAbsoluteSavePath + { + get + { + if (instance.fbxSavePaths.Count <= 0) + { + instance.fbxSavePaths.Add(kDefaultSavePath); + } + return GetAbsoluteSavePath(instance.fbxSavePaths[instance.SelectedFbxPath]); + } + } + + internal static string PrefabAbsoluteSavePath + { + get + { + if (instance.prefabSavePaths.Count <= 0) + { + instance.prefabSavePaths.Add(kDefaultSavePath); + } + return GetAbsoluteSavePath(instance.prefabSavePaths[instance.SelectedPrefabPath]); + } + } + + /// + /// Convert an absolute path into a relative path like what you would + /// get from GetRelativeSavePath. + /// + /// This uses '/' as the path separator. + /// + /// If 'requireSubdirectory' is the default on, return empty-string if the full + /// path is not in a subdirectory of assets. + /// + internal static string ConvertToAssetRelativePath(string fullPathInAssets, bool requireSubdirectory = true) + { + if (!Path.IsPathRooted(fullPathInAssets)) + { + fullPathInAssets = Path.GetFullPath(fullPathInAssets); + } + var relativePath = GetRelativePath(Application.dataPath, fullPathInAssets); + if (requireSubdirectory && relativePath.StartsWith("..")) + { + if (relativePath.Length == 2 || relativePath[2] == '/') + { + // The relative path has us pop out to another directory, + // so return an empty string as requested. + return ""; + } + } + return relativePath; + } + + /// + /// Compute how to get from 'fromDir' to 'toDir' via a relative path. + /// + internal static string GetRelativePath(string fromDir, string toDir, + char separator = '/') + { + // https://stackoverflow.com/questions/275689/how-to-get-relative-path-from-absolute-path + // Except... the MakeRelativeUri that ships with Unity is buggy. + // e.g. https://bugzilla.xamarin.com/show_bug.cgi?id=5921 + // among other bugs. So we roll our own. + + // Normalize the paths, assuming they're absolute paths (if they + // aren't, they get normalized as relative paths) + fromDir = NormalizePath(fromDir, isRelative: false); + toDir = NormalizePath(toDir, isRelative: false); + + // Break them into path components. + var fromDirs = fromDir.Split('/'); + var toDirs = toDir.Split('/'); + + // Find the least common ancestor + int lca = -1; + for (int i = 0, n = System.Math.Min(fromDirs.Length, toDirs.Length); i < n; ++i) + { + if (fromDirs[i] != toDirs[i]) { break; } + lca = i; + } + + // Step up from the fromDir to the lca, then down from lca to the toDir. + // If from = /a/b/c/d + // and to = /a/b/e/f/g + // Then we need to go up 2 and down 3. + var nStepsUp = (fromDirs.Length - 1) - lca; + var nStepsDown = (toDirs.Length - 1) - lca; + if (nStepsUp + nStepsDown == 0) + { + return "."; + } + + var relDirs = new string[nStepsUp + nStepsDown]; + for (int i = 0; i < nStepsUp; ++i) + { + relDirs[i] = ".."; + } + for (int i = 0; i < nStepsDown; ++i) + { + relDirs[nStepsUp + i] = toDirs[lca + 1 + i]; + } + + return string.Join("" + separator, relDirs); + } + + /// + /// Normalize a path, cleaning up path separators, resolving '.' and + /// '..', removing duplicate and trailing path separators, etc. + /// + /// If the path passed in is a relative path, we remove leading path separators. + /// If it's an absolute path we don't. + /// + /// If you claim the path is absolute but actually it's relative, we + /// treat it as a relative path. + /// + internal static string NormalizePath(string path, bool isRelative, + char separator = '/') + { + if (path == null) + { + return null; + } + + // Use slashes to simplify the code (we're going to clobber them all anyway). + path = path.Replace('\\', '/'); + + // If we're supposed to be an absolute path, but we're actually a + // relative path, ignore the 'isRelative' flag. + if (!isRelative && !Path.IsPathRooted(path)) + { + isRelative = true; + } + + // Build up a list of directory items. + var dirs = path.Split('/'); + + // Modify dirs in-place, reading from readIndex and remembering + // what index we've written to. + int lastWriteIndex = -1; + for (int readIndex = 0, n = dirs.Length; readIndex < n; ++readIndex) + { + var dir = dirs[readIndex]; + + // Skip duplicate path separators. + if (string.IsNullOrEmpty(dir)) + { + // Skip if it's not a leading path separator. + if (lastWriteIndex >= 0) + { + continue; + } + + // Also skip if it's leading and we have a relative path. + if (isRelative) + { + continue; + } + } + + // Skip '.' + if (dir == ".") + { + continue; + } + + // Erase the previous directory we read on '..'. + // Exception: we can start with '..' + // Exception: we can have multiple '..' in a row. + // + // Note: this ignores the actual file system and the funny + // results you see when there are symlinks. + if (dir == "..") + { + if (lastWriteIndex == -1) + { + // Leading '..' => handle like a normal directory. + } + else if (dirs[lastWriteIndex] == "..") + { + // Multiple ".." => handle like a normal directory. + } + else + { + // Usual case: delete the previous directory. + lastWriteIndex--; + continue; + } + } + + // Copy anything else to the next index. + ++lastWriteIndex; + dirs[lastWriteIndex] = dirs[readIndex]; + } + + if (lastWriteIndex == -1 || (lastWriteIndex == 0 && string.IsNullOrEmpty(dirs[lastWriteIndex]))) + { + // If we didn't keep anything, we have the empty path. + // For an absolute path that's / ; for a relative path it's . + if (isRelative) + { + return "."; + } + else + { + return "" + separator; + } + } + else + { + // Otherwise print out the path with the proper separator. + return String.Join("" + separator, dirs, 0, lastWriteIndex + 1); + } + } + + internal void Load() + { + string filePath = GetFilePath(); + if (!System.IO.File.Exists(filePath)) + { + LoadDefaults(); + } + else + { + try + { + var fileData = System.IO.File.ReadAllText(filePath); + EditorJsonUtility.FromJsonOverwrite(fileData, s_Instance); + } + catch (Exception xcp) + { + // Quash the exception and take the default settings. + Debug.LogException(xcp); + LoadDefaults(); + } + } + } + + internal void Save() + { + exportModelSettingsSerialize = ExportModelSettings.info; + // convertToPrefabSettingsSerialize = ConvertToPrefabSettings.info; + this.SaveToFile(); + } + + // Called on creation and whenever the reset button is clicked + internal void Reset() + { + // apply default settings + LoadDefaults(); + } + } + + [AttributeUsage(AttributeTargets.Class)] + internal sealed class FilePathAttribute : Attribute + { + public enum Location + { + PreferencesFolder, + ProjectFolder + } + public string filepath + { + get; + set; + } + public FilePathAttribute(string relativePath, FilePathAttribute.Location location) + { + if (string.IsNullOrEmpty(relativePath)) + { + Debug.LogError("Invalid relative path! (its null or empty)"); + return; + } + if (relativePath[0] == '/') + { + relativePath = relativePath.Substring(1); + } + if (location == FilePathAttribute.Location.PreferencesFolder) + { + this.filepath = InternalEditorUtility.unityPreferencesFolder + "/" + relativePath; + } + else + { + this.filepath = relativePath; + } + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/ExportSettings.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportSettings.cs.meta new file mode 100644 index 0000000..fe3c9f9 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/ExportSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 76a11434abcb460489b23b95aa26ee71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/FbxExporter.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/FbxExporter.cs new file mode 100644 index 0000000..0aa13d3 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/FbxExporter.cs @@ -0,0 +1,5079 @@ +using System.IO; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Animations; +using UnityEngine.Timeline; +using System.Linq; +using Autodesk.Fbx; +using System.Runtime.CompilerServices; +using System.Runtime.Serialization; +using DaddyFrosty.Fbx.Misc.AssetsMenu; +using DaddyFrosty.Fbx.Visitors; +// using DaddyFrosty.Fbx.Visitors; +using UnityEditor; +using UnityEditor.VersionControl; +using UnityEngine.Playables; + +[assembly: InternalsVisibleTo("Unity.Formats.Fbx.Editor.Tests")] +[assembly: InternalsVisibleTo("Unity.ProBuilder.AddOns.Editor")] + +namespace DaddyFrosty.Fbx +{ + /// + /// If your MonoBehaviour knows about some custom geometry that + /// isn't in a MeshFilter or SkinnedMeshRenderer, use + /// RegisterMeshCallback to get a callback when the exporter tries + /// to export your component. + /// + /// The callback should return true, and output the mesh you want. + /// + /// Return false if you don't want to drive this game object. + /// + /// Return true and output a null mesh if you don't want the + /// exporter to output anything. + /// + internal delegate bool GetMeshForComponent(ModelExporter exporter, T component, FbxNode fbxNode) where T : MonoBehaviour; + internal delegate bool GetMeshForComponent(ModelExporter exporter, MonoBehaviour component, FbxNode fbxNode); + + /// + /// Delegate used to convert a GameObject into a mesh. + /// + /// This is useful if you want to have broader control over + /// the export process than the GetMeshForComponent callbacks + /// provide. But it's less efficient because you'll get a callback + /// on every single GameObject. + /// + internal delegate bool GetMeshForObject(ModelExporter exporter, GameObject gameObject, FbxNode fbxNode); + + [System.Serializable] + internal class ModelExportException : System.Exception + { + public ModelExportException() {} + + public ModelExportException(string message) + : base(message) {} + + public ModelExportException(string message, System.Exception inner) + : base(message, inner) {} + + protected ModelExportException(SerializationInfo info, StreamingContext context) + : base(info, context) {} + } + + /// + /// Use the ModelExporter class to export Unity GameObjects to an FBX file. + /// + /// Use the ExportObject and ExportObjects methods. The default export + /// options are used when exporting the objects to the FBX file. + /// + /// For information on using the ModelExporter class, see the Developer's Guide. + /// + public sealed class ModelExporter + { + const string Title = + "Created by FBX Exporter from Unity Technologies"; + + const string Subject = + ""; + + const string Keywords = + "Nodes Meshes Materials Textures Cameras Lights Skins Animation"; + + const string Comments = + @""; + + /// + /// Path to the CHANGELOG file in Unity's virtual file system. Used to get the version number. + /// + const string ChangeLogPath = "Packages/com.unity.formats.fbx/CHANGELOG.md"; + + // NOTE: The ellipsis at the end of the Menu Item name prevents the context + // from being passed to command, thus resulting in OnContextItem() + // being called only once regardless of what is selected. + const string MenuItemName = "GameObject/Export To FBX [ DaddyFrosty ]..."; + + const string ProgressBarTitle = "FBX Export"; + + const char MayaNamespaceSeparator = ':'; + + // replace invalid chars with this one + const char InvalidCharReplacement = '_'; + + const string RegexCharStart = "["; + const string RegexCharEnd = "]"; + + public static float UnitScaleFactor => ExportSettings.instance.ExportModelSettings.info.UnitScaleFactor; + + internal const string PACKAGE_UI_NAME = "FBX Exporter"; + + /// + /// name of the scene's default camera + /// + private static string DefaultCamera = ""; + + private const string SkeletonPrefix = "_Skel"; + + private const string SkinPrefix = "_Skin"; + + /// + /// name prefix for custom properties + /// + const string NamePrefix = "Unity_"; + + private static string MakeName(string basename) + { + return NamePrefix + basename; + } + + /// + /// Create instance of exporter. + /// + static ModelExporter Create() + { + return new ModelExporter(); + } + + /// + /// Which components map from Unity Object to Fbx Object + /// + internal enum FbxNodeRelationType + { + NodeAttribute, + Property, + Material + } + + internal static Dictionary> MapsToFbxObject = new Dictionary>() + { + { typeof(Transform), new KeyValuePair(typeof(FbxProperty), FbxNodeRelationType.Property) }, + { typeof(MeshFilter), new KeyValuePair(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) }, + { typeof(SkinnedMeshRenderer), new KeyValuePair(typeof(FbxMesh), FbxNodeRelationType.NodeAttribute) }, + { typeof(Light), new KeyValuePair(typeof(FbxLight), FbxNodeRelationType.NodeAttribute) }, + { typeof(Camera), new KeyValuePair(typeof(FbxCamera), FbxNodeRelationType.NodeAttribute) }, + { typeof(Material), new KeyValuePair(typeof(FbxSurfaceMaterial), FbxNodeRelationType.Material) }, + }; + + /// + /// keep a map between GameObject and FbxNode for quick lookup when we export + /// animation. + /// + Dictionary MapUnityObjectToFbxNode = new Dictionary(); + + /// + /// keep a map between the constrained FbxNode (in Unity this is the GameObject with constraint component) + /// and its FbxConstraints for quick lookup when exporting constraint animations. + /// + Dictionary> MapConstrainedObjectToConstraints = new Dictionary>(); + + /// + /// keep a map between the FbxNode and its blendshape channels for quick lookup when exporting blendshapes. + /// + Dictionary> MapUnityObjectToBlendShapes = new Dictionary>(); + + /// + /// Map Unity material ID to FBX material object + /// + Dictionary MaterialMap = new Dictionary(); + + /// + /// Map texture properties to FBX texture object + /// + Dictionary<(Texture unityTexture, Vector2 offset, Vector2 scale, TextureWrapMode wrapModeU, TextureWrapMode wrapModeV), FbxFileTexture> TextureMap = + new Dictionary<(Texture unityTexture, Vector2 offset, Vector2 scale, TextureWrapMode wrapModeU, TextureWrapMode wrapModeV), FbxFileTexture>(); + + /// + /// Map a Unity mesh to an fbx node (for preserving instances) + /// + Dictionary SharedMeshes = new Dictionary(); + + /// + /// Map for the Name of an Object to number of objects with this name. + /// Used for enforcing unique names on export. + /// + Dictionary NameToIndexMap = new Dictionary(); + + /// + /// Map for the Material Name to number of materials with this name. + /// Used for enforcing unique names on export. + /// + Dictionary MaterialNameToIndexMap = new Dictionary(); + + /// + /// Map for the Texture Name to number of textures with this name. + /// Used for enforcing unique names on export. + /// + Dictionary TextureNameToIndexMap = new Dictionary(); + + /// + /// Format for creating unique names + /// + const string UniqueNameFormat = "{0}_{1}"; + + /// + /// The animation fbx file format. + /// + const string AnimFbxFileFormat = "{0}/{1}@{2}.fbx"; + + /// + /// Gets the export settings. + /// + internal static ExportSettings ExportSettings + { + get { return ExportSettings.instance; } + } + + internal static IExportOptions DefaultOptions + { + get { return new ExportModelSettingsSerialize(); } + } + + private IExportOptions m_exportOptions; + private IExportOptions ExportOptions + { + get + { + if (m_exportOptions == null) + { + // get default settings; + m_exportOptions = DefaultOptions; + } + return m_exportOptions; + } + set { m_exportOptions = value; } + } + + /// + /// Gets the Unity default material. + /// + internal static Material DefaultMaterial + { + get + { + if (!s_defaultMaterial) + { + var obj = GameObject.CreatePrimitive(PrimitiveType.Quad); + s_defaultMaterial = obj.GetComponent().sharedMaterial; + Object.DestroyImmediate(obj); + } + return s_defaultMaterial; + } + } + + static Material s_defaultMaterial = null; + + static Dictionary MapLightType = new Dictionary() + { + { UnityEngine.LightType.Directional, FbxLight.EType.eDirectional }, + { UnityEngine.LightType.Spot, FbxLight.EType.eSpot }, + { UnityEngine.LightType.Point, FbxLight.EType.ePoint }, + { UnityEngine.LightType.Area, FbxLight.EType.eArea }, + }; + + /// + /// Gets the version number of the FbxExporters plugin from the readme. + /// + internal static string GetVersionFromReadme() + { + if (!File.Exists(ChangeLogPath)) + { + Debug.LogWarning(string.Format("Could not find version number, the ChangeLog file is missing from: {0}", ChangeLogPath)); + return null; + } + + try + { + // The standard format is: + // ## [a.b.c-whatever] - yyyy-mm-dd + // Another format is: + // **Version**: a.b.c-whatever + // we handle either one and read out the version + var lines = File.ReadAllLines(ChangeLogPath); + var regexes = new string[] + { + @"^\s*##\s*\[(.*)\]", + @"^\s*\*\*Version\*\*:\s*(.*)\s*" + }; + foreach (var line in lines) + { + foreach (var regex in regexes) + { + var match = System.Text.RegularExpressions.Regex.Match(line, regex); + if (match.Success) + { + var version = match.Groups[1].Value; + return version.Trim(); + } + } + } + + // If we're here, we didn't find any match. + Debug.LogWarning(string.Format("Could not find most recent version number in {0}", ChangeLogPath)); + return null; + } + catch (IOException e) + { + Debug.LogException(e); + Debug.LogWarning(string.Format("Error reading file {0} ({1})", ChangeLogPath, e)); + return null; + } + } + + /// + /// Get a layer (to store UVs, normals, etc) on the mesh. + /// If it doesn't exist yet, create it. + /// + internal static FbxLayer GetOrCreateLayer(FbxMesh fbxMesh, int layer = 0 /* default layer */) + { + var maxLayerIndex = fbxMesh.GetLayerCount() - 1; + while (layer > maxLayerIndex) + { + // We'll have to create the layer (potentially several). + // Make sure to avoid infinite loops even if there's an + // FbxSdk bug. + var newLayerIndex = fbxMesh.CreateLayer(); + if (newLayerIndex <= maxLayerIndex) + { + // Error! + throw new ModelExportException( + "Internal error: Unable to create mesh layer " + + (maxLayerIndex + 1) + + " on mesh " + fbxMesh.GetName()); + } + maxLayerIndex = newLayerIndex; + } + return fbxMesh.GetLayer(layer); + } + + /// + /// Export the mesh's attributes using layer 0. + /// + private bool ExportComponentAttributes(MeshInfo mesh, FbxMesh fbxMesh, int[] unmergedTriangles) + { + // return true if any attribute was exported + var exportedAttribute = false; + + // Set the normals on Layer 0. + var fbxLayer = GetOrCreateLayer(fbxMesh); + + if (mesh.HasValidNormals()) + { + using (var fbxLayerElement = FbxLayerElementNormal.Create(fbxMesh, "Normals")) + { + fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex); + fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect); + + // Add one normal per each vertex face index (3 per triangle) + FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray(); + + for (var n = 0; n < unmergedTriangles.Length; n++) + { + var unityTriangle = unmergedTriangles[n]; + fbxElementArray.Add(ConvertToFbxVector4(mesh.Normals[unityTriangle])); + } + + fbxLayer.SetNormals(fbxLayerElement); + } + exportedAttribute = true; + } + + /// Set the binormals on Layer 0. + if (mesh.HasValidBinormals()) + { + using (var fbxLayerElement = FbxLayerElementBinormal.Create(fbxMesh, "Binormals")) + { + fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex); + fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect); + + // Add one normal per each vertex face index (3 per triangle) + FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray(); + + for (var n = 0; n < unmergedTriangles.Length; n++) + { + var unityTriangle = unmergedTriangles[n]; + fbxElementArray.Add(ConvertToFbxVector4(mesh.Binormals[unityTriangle])); + } + fbxLayer.SetBinormals(fbxLayerElement); + } + exportedAttribute = true; + } + + /// Set the tangents on Layer 0. + if (mesh.HasValidTangents()) + { + using (var fbxLayerElement = FbxLayerElementTangent.Create(fbxMesh, "Tangents")) + { + fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex); + fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect); + + // Add one normal per each vertex face index (3 per triangle) + FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray(); + + for (var n = 0; n < unmergedTriangles.Length; n++) + { + var unityTriangle = unmergedTriangles[n]; + fbxElementArray.Add(ConvertToFbxVector4( + new Vector3( + mesh.Tangents[unityTriangle][0], + mesh.Tangents[unityTriangle][1], + mesh.Tangents[unityTriangle][2] + ) + )); + } + fbxLayer.SetTangents(fbxLayerElement); + } + exportedAttribute = true; + } + + exportedAttribute |= ExportUVs(fbxMesh, mesh, unmergedTriangles); + + if (mesh.HasValidVertexColors()) + { + using (var fbxLayerElement = FbxLayerElementVertexColor.Create(fbxMesh, "VertexColors")) + { + fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex); + fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect); + + // set texture coordinates per vertex + FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray(); + + // (Uni-31596) only copy unique UVs into this array, and index appropriately + for (var n = 0; n < mesh.VertexColors.Length; n++) + { + // Converting to Color from Color32, as Color32 stores the colors + // as ints between 0-255, while FbxColor and Color + // use doubles between 0-1 + Color color = mesh.VertexColors[n]; + fbxElementArray.Add(new FbxColor(color.r, + color.g, + color.b, + color.a)); + } + + // For each face index, point to a texture uv + FbxLayerElementArray fbxIndexArray = fbxLayerElement.GetIndexArray(); + fbxIndexArray.SetCount(unmergedTriangles.Length); + + for (var i = 0; i < unmergedTriangles.Length; i++) + { + fbxIndexArray.SetAt(i, unmergedTriangles[i]); + } + fbxLayer.SetVertexColors(fbxLayerElement); + } + exportedAttribute = true; + } + return exportedAttribute; + } + + /// + /// Unity has up to 4 uv sets per mesh. Export all the ones that exist. + /// + /// Fbx mesh. + /// Mesh. + /// Unmerged triangles. + private static bool ExportUVs(FbxMesh fbxMesh, MeshInfo meshInfo, int[] unmergedTriangles) + { + var mesh = meshInfo.mesh; + + var uvs = new List(); + + var k = 0; + for (var i = 0; i < 8; i++) + { + mesh.GetUVs(i, uvs); + + if (uvs == null || uvs.Count == 0) + { + continue; // don't have these UV's, so skip + } + + var fbxLayer = GetOrCreateLayer(fbxMesh, k); + using (var fbxLayerElement = FbxLayerElementUV.Create(fbxMesh, "UVSet" + i)) + { + fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex); + fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect); + + // set texture coordinates per vertex + FbxLayerElementArray fbxElementArray = fbxLayerElement.GetDirectArray(); + + // (Uni-31596) only copy unique UVs into this array, and index appropriately + for (var n = 0; n < uvs.Count; n++) + { + fbxElementArray.Add(new FbxVector2(uvs[n][0], + uvs[n][1])); + } + + // For each face index, point to a texture uv + FbxLayerElementArray fbxIndexArray = fbxLayerElement.GetIndexArray(); + fbxIndexArray.SetCount(unmergedTriangles.Length); + + for (var j = 0; j < unmergedTriangles.Length; j++) + { + fbxIndexArray.SetAt(j, unmergedTriangles[j]); + } + fbxLayer.SetUVs(fbxLayerElement, FbxLayerElement.EType.eTextureDiffuse); + } + k++; + uvs.Clear(); + } + + // if we incremented k, then at least on set of UV's were exported + return k > 0; + } + + /// + /// Export the mesh's blend shapes. + /// + private FbxBlendShape ExportBlendShapes(MeshInfo mesh, FbxMesh fbxMesh, FbxScene fbxScene, int[] unmergedTriangles) + { + var umesh = mesh.mesh; + if (umesh.blendShapeCount == 0) + return null; + + var fbxBlendShape = FbxBlendShape.Create(fbxScene, umesh.name + "_BlendShape"); + fbxMesh.AddDeformer(fbxBlendShape); + + var numVertices = umesh.vertexCount; + var basePoints = umesh.vertices; + var baseNormals = umesh.normals; + var baseTangents = umesh.tangents; + var deltaPoints = new Vector3[numVertices]; + var deltaNormals = new Vector3[numVertices]; + var deltaTangents = new Vector3[numVertices]; + + for (var bi = 0; bi < umesh.blendShapeCount; ++bi) + { + var bsName = umesh.GetBlendShapeName(bi); + var numFrames = umesh.GetBlendShapeFrameCount(bi); + var fbxChannel = FbxBlendShapeChannel.Create(fbxScene, bsName); + fbxBlendShape.AddBlendShapeChannel(fbxChannel); + + for (var fi = 0; fi < numFrames; ++fi) + { + var weight = umesh.GetBlendShapeFrameWeight(bi, fi); + umesh.GetBlendShapeFrameVertices(bi, fi, deltaPoints, deltaNormals, deltaTangents); + + var fbxShapeName = bsName; + + if (numFrames > 1) + { + fbxShapeName += "_" + fi; + } + + var fbxShape = FbxShape.Create(fbxScene, fbxShapeName); + fbxChannel.AddTargetShape(fbxShape, weight); + + // control points + fbxShape.InitControlPoints(ControlPointToIndex.Count); + for (var vi = 0; vi < numVertices; ++vi) + { + var ni = ControlPointToIndex[basePoints[vi]]; + var v = basePoints[vi] + deltaPoints[vi]; + fbxShape.SetControlPointAt(ConvertToFbxVector4(v, UnitScaleFactor), ni); + } + + // normals + if (mesh.HasValidNormals()) + { + var elemNormals = fbxShape.CreateElementNormal(); + elemNormals.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex); + elemNormals.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect); + var dstNormals = elemNormals.GetDirectArray(); + dstNormals.SetCount(unmergedTriangles.Length); + for (var ii = 0; ii < unmergedTriangles.Length; ++ii) + { + var vi = unmergedTriangles[ii]; + var n = baseNormals[vi] + deltaNormals[vi]; + dstNormals.SetAt(ii, ConvertToFbxVector4(n)); + } + } + + // tangents + if (mesh.HasValidTangents()) + { + var elemTangents = fbxShape.CreateElementTangent(); + elemTangents.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygonVertex); + elemTangents.SetReferenceMode(FbxLayerElement.EReferenceMode.eDirect); + var dstTangents = elemTangents.GetDirectArray(); + dstTangents.SetCount(unmergedTriangles.Length); + for (var ii = 0; ii < unmergedTriangles.Length; ++ii) + { + var vi = unmergedTriangles[ii]; + var t = (Vector3)baseTangents[vi] + deltaTangents[vi]; + dstTangents.SetAt(ii, ConvertToFbxVector4(t)); + } + } + } + } + return fbxBlendShape; + } + + /// + /// Takes in a left-handed UnityEngine.Vector3 denoting a normal, + /// returns a right-handed FbxVector4. + /// + /// Unity is left-handed, Maya and Max are right-handed. + /// The FbxSdk conversion routines can't handle changing handedness. + /// + /// Remember you also need to flip the winding order on your polygons. + /// + internal static FbxVector4 ConvertToFbxVector4(Vector3 leftHandedVector, float unitScale = 1f) + { + // negating the x component of the vector converts it from left to right handed coordinates + return unitScale * new FbxVector4( + leftHandedVector[0], + leftHandedVector[1], + leftHandedVector[2]); + } + + /// + /// Exports a texture from Unity to FBX. + /// The texture must be a property on the unityMaterial; it gets + /// linked to the FBX via a property on the fbxMaterial. + /// + /// The texture file must be a file on disk; it is not embedded within the FBX. + /// + /// Unity material. + /// Unity property name, e.g. "_MainTex". + /// Fbx material. + /// Fbx property name, e.g. FbxSurfaceMaterial.sDiffuse. + internal bool ExportTexture(Material unityMaterial, string unityPropName, + FbxSurfaceMaterial fbxMaterial, string fbxPropName) + { + if (!unityMaterial) + { + return false; + } + + // Get the texture on this property, if any. + if (!unityMaterial.HasProperty(unityPropName)) + { + return false; + } + var unityTexture = unityMaterial.GetTexture(unityPropName); + if (!unityTexture) + { + return false; + } + + // Find its filename + var textureSourceFullPath = AssetDatabase.GetAssetPath(unityTexture); + if (string.IsNullOrEmpty(textureSourceFullPath)) + { + return false; + } + + // get absolute filepath to texture + textureSourceFullPath = Path.GetFullPath(textureSourceFullPath); + + if (Verbose) + { + Debug.Log(string.Format("{2}.{1} setting texture path {0}", textureSourceFullPath, fbxPropName, fbxMaterial.GetName())); + } + + // Find the corresponding property on the fbx material. + var fbxMaterialProperty = fbxMaterial.FindProperty(fbxPropName); + if (fbxMaterialProperty == null || !fbxMaterialProperty.IsValid()) + { + Debug.Log("property not found"); + return false; + } + + var offset = unityMaterial.GetTextureOffset(unityPropName); + var scale = unityMaterial.GetTextureScale(unityPropName); + var wrapModeU = unityTexture.wrapModeU; + var wrapModeV = unityTexture.wrapModeV; + + var tuple = (unityTexture, offset, scale, wrapModeU, wrapModeV); + + FbxFileTexture fbxTexture; + // Find or create an fbx texture and link it up to the fbx material. + if (!TextureMap.TryGetValue(tuple, out fbxTexture)) + { + var textureName = GetUniqueTextureName(fbxPropName + "_Texture"); + fbxTexture = FbxFileTexture.Create(fbxMaterial, textureName); + fbxTexture.SetFileName(textureSourceFullPath); + fbxTexture.SetTextureUse(FbxTexture.ETextureUse.eStandard); + fbxTexture.SetMappingType(FbxTexture.EMappingType.eUV); + fbxTexture.SetScale(scale.x, scale.y); + fbxTexture.SetTranslation(offset.x, offset.y); + fbxTexture.SetWrapMode(GetWrapModeFromUnityWrapMode(wrapModeU, unityMaterial.name, unityPropName), + GetWrapModeFromUnityWrapMode(wrapModeV, unityMaterial.name, unityPropName)); + TextureMap.Add(tuple, fbxTexture); + } + fbxTexture.ConnectDstProperty(fbxMaterialProperty); + + return true; + } + + private FbxTexture.EWrapMode GetWrapModeFromUnityWrapMode(TextureWrapMode wrapMode, string materialName, string textureName) + { + switch (wrapMode) + { + case TextureWrapMode.Clamp: return FbxTexture.EWrapMode.eClamp; + case TextureWrapMode.Repeat: return FbxTexture.EWrapMode.eRepeat; + default: + Debug.LogWarning($"Texture {textureName} on material {materialName} uses wrap mode {wrapMode} which is not supported by FBX. Will be exported as \"Repeat\"."); + return FbxTexture.EWrapMode.eRepeat; + } + } + + /// + /// Get the color of a material, or grey if we can't find it. + /// + internal FbxDouble3 GetMaterialColor(Material unityMaterial, string unityPropName, float defaultValue = 1) + { + if (!unityMaterial) + { + return new FbxDouble3(defaultValue); + } + if (!unityMaterial.HasProperty(unityPropName)) + { + return new FbxDouble3(defaultValue); + } + var unityColor = unityMaterial.GetColor(unityPropName); + return new FbxDouble3(unityColor.r, unityColor.g, unityColor.b); + } + + /// + /// Export (and map) a Unity PBS material to FBX classic material + /// + internal bool ExportMaterial(Material unityMaterial, FbxScene fbxScene, FbxNode fbxNode) + { + if (!unityMaterial) + { + unityMaterial = DefaultMaterial; + } + + var unityID = unityMaterial.GetInstanceID(); + FbxSurfaceMaterial mappedMaterial; + if (MaterialMap.TryGetValue(unityID, out mappedMaterial)) + { + fbxNode.AddMaterial(mappedMaterial); + return true; + } + + var unityName = unityMaterial.name; + var fbxName = ExportOptions.UseMayaCompatibleNames + ? ConvertToMayaCompatibleName(unityName) : unityName; + + fbxName = GetUniqueMaterialName(fbxName); + + if (Verbose) + { + if (unityName != fbxName) + { + Debug.Log(string.Format("exporting material {0} as {1}", unityName, fbxName)); + } + else + { + Debug.Log(string.Format("exporting material {0}", unityName)); + } + } + + // We'll export either Phong or Lambert. Phong if it calls + // itself specular, Lambert otherwise. + var stringComparison = System.StringComparison.OrdinalIgnoreCase; + var shader = unityMaterial.shader; + var specular = shader.name.IndexOf("specular", stringComparison) >= 0; + var hdrp = shader.name.IndexOf("hdrp", stringComparison) >= 0; + + var fbxMaterial = specular + ? FbxSurfacePhong.Create(fbxScene, fbxName) + : FbxSurfaceLambert.Create(fbxScene, fbxName); + + // Copy the flat colours over from Unity standard materials to FBX. + fbxMaterial.Diffuse.Set(GetMaterialColor(unityMaterial, "_Color")); + fbxMaterial.Emissive.Set(GetMaterialColor(unityMaterial, "_EmissionColor", 0)); + // hdrp materials dont export emission properly, so default to 0 + if (hdrp) + { + fbxMaterial.Emissive.Set(new FbxDouble3(0, 0, 0)); + } + fbxMaterial.Ambient.Set(new FbxDouble3()); + + fbxMaterial.BumpFactor.Set(unityMaterial.HasProperty("_BumpScale") ? unityMaterial.GetFloat("_BumpScale") : 0); + + if (specular) + { + (fbxMaterial as FbxSurfacePhong).Specular.Set(GetMaterialColor(unityMaterial, "_SpecColor")); + } + + // Export the textures from Unity standard materials to FBX. + ExportTexture(unityMaterial, "_MainTex", fbxMaterial, FbxSurfaceMaterial.sDiffuse); + ExportTexture(unityMaterial, "_EmissionMap", fbxMaterial, FbxSurfaceMaterial.sEmissive); + ExportTexture(unityMaterial, "_BumpMap", fbxMaterial, FbxSurfaceMaterial.sNormalMap); + if (specular) + { + ExportTexture(unityMaterial, "_SpecGlossMap", fbxMaterial, FbxSurfaceMaterial.sSpecular); + } + + MaterialMap.Add(unityID, fbxMaterial); + fbxNode.AddMaterial(fbxMaterial); + return true; + } + + /// + /// Sets up the material to polygon mapping for fbxMesh. + /// To determine which part of the mesh uses which material, look at the submeshes + /// and which polygons they represent. + /// Assuming equal number of materials as submeshes, and that they are in the same order. + /// (i.e. submesh 1 uses material 1) + /// + /// Fbx mesh. + /// Mesh. + /// Materials. + private void AssignLayerElementMaterial(FbxMesh fbxMesh, Mesh mesh, int materialCount) + { + // Add FbxLayerElementMaterial to layer 0 of the node + var fbxLayer = fbxMesh.GetLayer(0 /* default layer */); + if (fbxLayer == null) + { + fbxMesh.CreateLayer(); + fbxLayer = fbxMesh.GetLayer(0 /* default layer */); + } + + using (var fbxLayerElement = FbxLayerElementMaterial.Create(fbxMesh, "Material")) + { + // if there is only one material then set everything to that material + if (materialCount == 1) + { + fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eAllSame); + fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect); + + FbxLayerElementArray fbxElementArray = fbxLayerElement.GetIndexArray(); + fbxElementArray.Add(0); + } + else + { + fbxLayerElement.SetMappingMode(FbxLayerElement.EMappingMode.eByPolygon); + fbxLayerElement.SetReferenceMode(FbxLayerElement.EReferenceMode.eIndexToDirect); + + FbxLayerElementArray fbxElementArray = fbxLayerElement.GetIndexArray(); + + for (var subMeshIndex = 0; subMeshIndex < mesh.subMeshCount; subMeshIndex++) + { + var topology = mesh.GetTopology(subMeshIndex); + int polySize; + + switch (topology) + { + case MeshTopology.Triangles: + polySize = 3; + break; + case MeshTopology.Quads: + polySize = 4; + break; + case MeshTopology.Lines: + throw new System.NotImplementedException(); + case MeshTopology.Points: + throw new System.NotImplementedException(); + case MeshTopology.LineStrip: + throw new System.NotImplementedException(); + default: + throw new System.NotImplementedException(); + } + + // Specify the material index for each polygon. + // Material index should match subMeshIndex. + var indices = mesh.GetIndices(subMeshIndex); + for (int j = 0, n = indices.Length / polySize; j < n; j++) + { + fbxElementArray.Add(subMeshIndex); + } + } + } + fbxLayer.SetMaterials(fbxLayerElement); + } + } + + /// + /// Exports a unity mesh and attaches it to the node as an FbxMesh. + /// + /// Able to export materials per sub-mesh as well (by default, exports with the default material). + /// + /// Use fbxNode.GetMesh() to access the exported mesh. + /// + internal bool ExportMesh(Mesh mesh, FbxNode fbxNode, Material[] materials = null) + { + var meshInfo = new MeshInfo(mesh, materials); + return ExportMesh(meshInfo, fbxNode); + } + + /// + /// Keeps track of the index of each point in the exported vertex array. + /// + private Dictionary ControlPointToIndex = new Dictionary(); + + /// + /// Exports a unity mesh and attaches it to the node as an FbxMesh. + /// + bool ExportMesh(MeshInfo meshInfo, FbxNode fbxNode) + { + if (!meshInfo.IsValid) + { + return false; + } + + NumMeshes++; + NumTriangles += meshInfo.Triangles.Length / 3; + + // create the mesh structure. + var fbxScene = fbxNode.GetScene(); + var fbxMesh = FbxMesh.Create(fbxScene, "Scene"); + + // Create control points. + ControlPointToIndex.Clear(); + { + var vertices = meshInfo.Vertices; + for (int v = 0, n = meshInfo.VertexCount; v < n; v++) + { + if (ControlPointToIndex.ContainsKey(vertices[v])) + { + continue; + } + ControlPointToIndex[vertices[v]] = ControlPointToIndex.Count; + } + fbxMesh.InitControlPoints(ControlPointToIndex.Count); + + foreach (var kvp in ControlPointToIndex) + { + var controlPoint = kvp.Key; + var index = kvp.Value; + fbxMesh.SetControlPointAt(ConvertToFbxVector4(controlPoint, UnitScaleFactor), index); + } + } + + var unmergedPolygons = new List(); + var mesh = meshInfo.mesh; + for (var s = 0; s < mesh.subMeshCount; s++) + { + var topology = mesh.GetTopology(s); + var indices = mesh.GetIndices(s); + + int polySize; + int[] vertOrder; + + switch (topology) + { + case MeshTopology.Triangles: + polySize = 3; + vertOrder = new int[] { 0, 1, 2 }; + break; + case MeshTopology.Quads: + polySize = 4; + vertOrder = new int[] { 0, 1, 2, 3 }; + break; + case MeshTopology.Lines: + throw new System.NotImplementedException(); + case MeshTopology.Points: + throw new System.NotImplementedException(); + case MeshTopology.LineStrip: + throw new System.NotImplementedException(); + default: + throw new System.NotImplementedException(); + } + + for (var f = 0; f < indices.Length / polySize; f++) + { + fbxMesh.BeginPolygon(); + + foreach (var val in vertOrder) + { + var polyVert = indices[polySize * f + val]; + + // Save the polygon order (without merging vertices) so we + // properly export UVs, normals, binormals, etc. + unmergedPolygons.Add(polyVert); + + polyVert = ControlPointToIndex[meshInfo.Vertices[polyVert]]; + fbxMesh.AddPolygon(polyVert); + } + fbxMesh.EndPolygon(); + } + } + + // Set up materials per submesh. + foreach (var mat in meshInfo.Materials) + { + ExportMaterial(mat, fbxScene, fbxNode); + } + AssignLayerElementMaterial(fbxMesh, meshInfo.mesh, meshInfo.Materials.Length); + + // Set up normals, etc. + ExportComponentAttributes(meshInfo, fbxMesh, unmergedPolygons.ToArray()); + + // Set up blend shapes. + var fbxBlendShape = ExportBlendShapes(meshInfo, fbxMesh, fbxScene, unmergedPolygons.ToArray()); + + if (fbxBlendShape != null && fbxBlendShape.GetBlendShapeChannelCount() > 0) + { + // Populate mapping for faster lookup when exporting blendshape animations + List blendshapeChannels; + if (!MapUnityObjectToBlendShapes.TryGetValue(fbxNode, out blendshapeChannels)) + { + blendshapeChannels = new List(); + MapUnityObjectToBlendShapes.Add(fbxNode, blendshapeChannels); + } + + for (var i = 0; i < fbxBlendShape.GetBlendShapeChannelCount(); i++) + { + var bsChannel = fbxBlendShape.GetBlendShapeChannel(i); + blendshapeChannels.Add(bsChannel); + } + } + + // set the fbxNode containing the mesh + fbxNode.SetNodeAttribute(fbxMesh); + fbxNode.SetShadingMode(FbxNode.EShadingMode.eWireFrame); + return true; + } + + /// + /// Export GameObject as a skinned mesh with material, bones, a skin and, a bind pose. + /// + private bool ExportSkinnedMesh(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode) + { + if (!unityGo || fbxNode == null) + { + return false; + } + + if (!unityGo.TryGetComponent(out var unitySkin)) + { + return false; + } + + var mesh = unitySkin.sharedMesh; + if (!mesh) + { + return false; + } + + if (Verbose) + Debug.Log(string.Format("exporting {0} {1}", "Skin", fbxNode.GetName())); + + + var meshInfo = new MeshInfo(unitySkin.sharedMesh, unitySkin.sharedMaterials); + + FbxMesh fbxMesh = null; + if (ExportMesh(meshInfo, fbxNode)) + { + fbxMesh = fbxNode.GetMesh(); + } + if (fbxMesh == null) + { + Debug.LogError("Could not find mesh"); + return false; + } + + Dictionary skinnedMeshToBonesMap; + // export skeleton + if (ExportSkeleton(unitySkin, fbxScene, out skinnedMeshToBonesMap)) + { + // bind mesh to skeleton + ExportSkin(unitySkin, meshInfo, fbxScene, fbxMesh, fbxNode); + + // add bind pose + ExportBindPose(unitySkin, fbxNode, fbxScene, skinnedMeshToBonesMap); + + // now that the skin and bindpose are set, make sure that each of the bones + // is set to its original position + var bones = unitySkin.bones; + foreach (var bone in bones) + { + // ignore null bones + if (bone != null) + { + var fbxBone = MapUnityObjectToFbxNode[bone.gameObject]; + ExportTransform(bone, fbxBone, newCenter: Vector3.zero, TransformExportType.Local); + + // Cancel out the pre-rotation from the exported rotation + + // Get prerotation + var fbxPreRotationEuler = fbxBone.GetPreRotation(FbxNode.EPivotSet.eSourcePivot); + // Convert the prerotation to a Quaternion + var fbxPreRotationQuaternion = EulerToQuaternionXYZ(fbxPreRotationEuler); + // Inverse of the prerotation + fbxPreRotationQuaternion.Inverse(); + + // Multiply LclRotation by pre-rotation inverse to get the LclRotation without pre-rotation applied + var finalLclRotationQuat = fbxPreRotationQuaternion * EulerToQuaternionZXY(bone.localEulerAngles); + + // Convert to Euler with Unity axis system and update LclRotation + var finalUnityQuat = new Quaternion((float)finalLclRotationQuat.X, (float)finalLclRotationQuat.Y, (float)finalLclRotationQuat.Z, (float)finalLclRotationQuat.W); + fbxBone.LclRotation.Set(ToFbxDouble3(finalUnityQuat.eulerAngles)); + } + else + { + Debug.Log("Warning: One or more bones are null. Skeleton may not export correctly."); + } + } + } + + return true; + } + + /// + /// Gets the bind pose for the Unity bone. + /// + /// The bind pose. + /// Unity bone. + /// Bind poses. + /// Contains information about bones and skinned mesh. + private Matrix4x4 GetBindPose( + Transform unityBone, Matrix4x4[] bindPoses, + ref SkinnedMeshBoneInfo boneInfo + ) + { + var boneDict = boneInfo.boneDict; + var skinnedMesh = boneInfo.skinnedMesh; + var boneToBindPose = boneInfo.boneToBindPose; + + // If we have already retrieved the bindpose for this bone before + // it will be present in the boneToBindPose dictionary, + // simply return this bindpose. + Matrix4x4 bindPose; + if (boneToBindPose.TryGetValue(unityBone, out bindPose)) + { + return bindPose; + } + + // Check if unityBone is a bone registered in the bone list of the skinned mesh. + // If it is, then simply retrieve the bindpose from the boneDict (maps bone to index in bone/bindpose list). + // Make sure to update the boneToBindPose list in case the bindpose for this bone needs to be retrieved again. + int index; + if (boneDict.TryGetValue(unityBone, out index)) + { + bindPose = bindPoses[index]; + boneToBindPose.Add(unityBone, bindPose); + return bindPose; + } + + // unityBone is not registered as a bone in the skinned mesh, therefore there is no bindpose + // associated with it, we need to calculate one. + + // If this is the rootbone of the mesh or an object without a parent, use the global matrix relative to the skinned mesh + // as the bindpose. + if (unityBone == skinnedMesh.rootBone || unityBone.parent == null) + { + // there is no bone above this object with a bindpose, calculate bindpose relative to skinned mesh + bindPose = (unityBone.worldToLocalMatrix * skinnedMesh.transform.localToWorldMatrix); + boneToBindPose.Add(unityBone, bindPose); + return bindPose; + } + + // If this object has a parent that could be a bone, then it is not enough to use the worldToLocalMatrix, + // as this will give an incorrect global transform if the parents are not already in the bindpose in the scene. + // Instead calculate what the bindpose would be based on the bindpose of the parent object. + + // get the bindpose of the parent + var parentBindPose = GetBindPose(unityBone.parent, bindPoses, ref boneInfo); + // Get the local transformation matrix of the bone, then transform it into + // the global transformation matrix with the parent in the bind pose. + // Formula to get the global transformation matrix: + // (parentBindPose.inverse * boneLocalTRSMatrix) + // The bindpose is then the inverse of this matrix: + // (parentBindPose.inverse * boneLocalTRSMatrix).inverse + // This can be simplified with (AB)^{-1} = B^{-1}A^{-1} rule as follows: + // (parentBindPose.inverse * boneLocalTRSMatrix).inverse + // = boneLocalTRSMatrix.inverse * parentBindPose.inverse.inverse + // = boneLocalTRSMatrix.inverse * parentBindPose + var boneLocalTRSMatrix = Matrix4x4.TRS(unityBone.localPosition, unityBone.localRotation, unityBone.localScale); + bindPose = boneLocalTRSMatrix.inverse * parentBindPose; + boneToBindPose.Add(unityBone, bindPose); + return bindPose; + } + + /// + /// Export bones of skinned mesh, if this is a skinned mesh with + /// bones and bind poses. + /// + private bool ExportSkeleton(SkinnedMeshRenderer skinnedMesh, FbxScene fbxScene, out Dictionary skinnedMeshToBonesMap) + { + skinnedMeshToBonesMap = new Dictionary(); + + if (!skinnedMesh) + { + return false; + } + var bones = skinnedMesh.bones; + if (bones == null || bones.Length == 0) + { + return false; + } + var mesh = skinnedMesh.sharedMesh; + if (!mesh) + { + return false; + } + + var bindPoses = mesh.bindposes; + if (bindPoses == null || bindPoses.Length != bones.Length) + { + return false; + } + + // Two steps: + // 0. Set up the map from bone to index. + // 1. Set the transforms. + + // Step 0: map transform to index so we can look up index by bone. + var index = new Dictionary(); + for (var boneIndex = 0; boneIndex < bones.Length; boneIndex++) + { + var unityBoneTransform = bones[boneIndex]; + + // ignore null bones + if (unityBoneTransform != null) + { + index[unityBoneTransform] = boneIndex; + } + } + + skinnedMeshToBonesMap.Add(skinnedMesh, bones); + + // Step 1: Set transforms + var boneInfo = new SkinnedMeshBoneInfo(skinnedMesh, index); + foreach (var bone in bones) + { + // ignore null bones + if (bone != null) + { + var fbxBone = MapUnityObjectToFbxNode[bone.gameObject]; + ExportBoneTransform(fbxBone, fbxScene, bone, boneInfo); + } + } + return true; + } + + /// + /// Export binding of mesh to skeleton + /// + private bool ExportSkin(SkinnedMeshRenderer skinnedMesh, + MeshInfo meshInfo, FbxScene fbxScene, FbxMesh fbxMesh, + FbxNode fbxRootNode) + { + var fbxSkin = FbxSkin.Create(fbxScene, (skinnedMesh.name + SkinPrefix)); + + var fbxMeshMatrix = fbxRootNode.EvaluateGlobalTransform(); + + // keep track of the bone index -> fbx cluster mapping, so that we can add the bone weights afterwards + var boneCluster = new Dictionary(); + + for (var i = 0; i < skinnedMesh.bones.Length; i++) + { + // ignore null bones + if (skinnedMesh.bones[i] != null) + { + var fbxBoneNode = MapUnityObjectToFbxNode[skinnedMesh.bones[i].gameObject]; + + // Create the deforming cluster + var fbxCluster = FbxCluster.Create(fbxScene, "BoneWeightCluster"); + + fbxCluster.SetLink(fbxBoneNode); + fbxCluster.SetLinkMode(FbxCluster.ELinkMode.eNormalize); + + boneCluster.Add(i, fbxCluster); + + // set the Transform and TransformLink matrix + fbxCluster.SetTransformMatrix(fbxMeshMatrix); + + var fbxLinkMatrix = fbxBoneNode.EvaluateGlobalTransform(); + fbxCluster.SetTransformLinkMatrix(fbxLinkMatrix); + + // add the cluster to the skin + fbxSkin.AddCluster(fbxCluster); + } + } + + // set the vertex weights for each bone + SetVertexWeights(meshInfo, boneCluster); + + // Add the skin to the mesh after the clusters have been added + fbxMesh.AddDeformer(fbxSkin); + + return true; + } + + /// + /// set vertex weights in cluster + /// + private void SetVertexWeights(MeshInfo meshInfo, Dictionary boneIndexToCluster) + { + var mesh = meshInfo.mesh; + // Get the number of bone weights per vertex + var bonesPerVertex = mesh.GetBonesPerVertex(); + if (bonesPerVertex.Length == 0) + { + // no bone weights to set + return; + } + + var visitedVertices = new HashSet(); + + // Get all the bone weights, in vertex index order + // Note: this contains all the bone weights for all vertices. + // Use number of bonesPerVertex to determine where weights end + // for one vertex and begin for another. + var boneWeights1 = mesh.GetAllBoneWeights(); + + // Keep track of where we are in the array of BoneWeights, as we iterate over the vertices + var boneWeightIndex = 0; + + for (var vertIndex = 0; vertIndex < meshInfo.VertexCount; vertIndex++) + { + // Get the index into the list of vertices without duplicates + var actualIndex = ControlPointToIndex[meshInfo.Vertices[vertIndex]]; + + var numberOfBonesForThisVertex = bonesPerVertex[vertIndex]; + + if (visitedVertices.Contains(actualIndex)) + { + // skip duplicate vertex + boneWeightIndex += numberOfBonesForThisVertex; + continue; + } + visitedVertices.Add(actualIndex); + + // For each vertex, iterate over its BoneWeights + for (var i = 0; i < numberOfBonesForThisVertex; i++) + { + var currentBoneWeight = boneWeights1[boneWeightIndex]; + + // bone index is index into skinnedmesh.bones[] + var boneIndex = currentBoneWeight.boneIndex; + // how much influence does this bone have on vertex at vertIndex + var weight = currentBoneWeight.weight; + + if (weight <= 0) + { + continue; + } + + // get the right cluster + FbxCluster boneCluster; + if (!boneIndexToCluster.TryGetValue(boneIndex, out boneCluster)) + { + continue; + } + // add vertex and weighting on vertex to this bone's cluster + boneCluster.AddControlPointIndex(actualIndex, weight); + + boneWeightIndex++; + } + } + } + + /// + /// Export bind pose of mesh to skeleton + /// + private bool ExportBindPose(SkinnedMeshRenderer skinnedMesh, FbxNode fbxMeshNode, + FbxScene fbxScene, Dictionary skinnedMeshToBonesMap) + { + if (fbxMeshNode == null || skinnedMeshToBonesMap == null || fbxScene == null) + { + return false; + } + + var fbxPose = FbxPose.Create(fbxScene, fbxMeshNode.GetName()); + + // set as bind pose + fbxPose.SetIsBindPose(true); + + // assume each bone node has one weighted vertex cluster + Transform[] bones; + if (!skinnedMeshToBonesMap.TryGetValue(skinnedMesh, out bones)) + { + return false; + } + for (var i = 0; i < bones.Length; i++) + { + // ignore null bones + if (bones[i] != null) + { + var fbxBoneNode = MapUnityObjectToFbxNode[bones[i].gameObject]; + + // EvaluateGlobalTransform returns an FbxAMatrix (affine matrix) + // which has to be converted to an FbxMatrix so that it can be passed to fbxPose.Add(). + // The hierarchy for FbxMatrix and FbxAMatrix is as follows: + // + // FbxDouble4x4 + // / \ + // FbxMatrix FbxAMatrix + // + // Therefore we can't convert directly from FbxAMatrix to FbxMatrix, + // however FbxMatrix has a constructor that takes an FbxAMatrix. + var fbxBindMatrix = new FbxMatrix(fbxBoneNode.EvaluateGlobalTransform()); + + fbxPose.Add(fbxBoneNode, fbxBindMatrix); + } + } + + fbxPose.Add(fbxMeshNode, new FbxMatrix(fbxMeshNode.EvaluateGlobalTransform())); + + // add the pose to the scene + fbxScene.AddPose(fbxPose); + + return true; + } + + internal static FbxDouble3 ToFbxDouble3(Vector3 v) + { + return new FbxDouble3(v.x, v.y, v.z); + } + + internal static FbxDouble3 ToFbxDouble3(FbxVector4 v) + { + return new FbxDouble3(v.X, v.Y, v.Z); + } + + /// + /// Euler (roll/pitch/yaw (ZXY rotation order) to quaternion. + /// + /// a quaternion. + /// ZXY Euler. + internal static FbxQuaternion EulerToQuaternionZXY(Vector3 euler) + { + var unityQuat = Quaternion.Euler(euler); + return new FbxQuaternion(unityQuat.x, unityQuat.y, unityQuat.z, unityQuat.w); + } + + /// + /// Euler X/Y/Z rotation order to quaternion. + /// + /// XYZ Euler. + /// a quaternion + internal static FbxQuaternion EulerToQuaternionXYZ(FbxVector4 euler) + { + var m = new FbxAMatrix(); + m.SetR(euler); + return m.GetQ(); + } + + // get a fbxNode's global default position. + internal bool ExportTransform(UnityEngine.Transform unityTransform, FbxNode fbxNode, Vector3 newCenter, TransformExportType exportType) + { + UnityEngine.Vector3 unityTranslate; + FbxDouble3 fbxRotate; + UnityEngine.Vector3 unityScale; + + switch (exportType) + { + case TransformExportType.Reset: + unityTranslate = Vector3.zero; + fbxRotate = new FbxDouble3(0); + unityScale = Vector3.one; + break; + case TransformExportType.Global: + unityTranslate = GetRecenteredTranslation(unityTransform, newCenter); + fbxRotate = ToFbxDouble3(unityTransform.eulerAngles); + unityScale = unityTransform.lossyScale; + break; + default: /*case TransformExportType.Local*/ + unityTranslate = unityTransform.localPosition; + fbxRotate = ToFbxDouble3(unityTransform.localEulerAngles); + unityScale = unityTransform.localScale; + break; + } + + // Transfer transform data from Unity to Fbx + var fbxTranslate = ConvertToFbxVector4(unityTranslate, UnitScaleFactor); + var fbxScale = new FbxDouble3(unityScale.x, unityScale.y, unityScale.z); + + // Zero scale causes issues in 3ds Max (child of object with zero scale will end up with a much larger scale, e.g. >9000). + // When exporting 0 scale from Maya, the FBX contains 1e-12 instead of 0, + // which doesn't cause issues in Max. Do the same here. + if (fbxScale.X == 0) + { + fbxScale.X = 1e-12; + } + if (fbxScale.Y == 0) + { + fbxScale.Y = 1e-12; + } + if (fbxScale.Z == 0) + { + fbxScale.Z = 1e-12; + } + + // set the local position of fbxNode + fbxNode.LclTranslation.Set(new FbxDouble3(fbxTranslate.X, fbxTranslate.Y, fbxTranslate.Z)); + fbxNode.LclRotation.Set(fbxRotate); + fbxNode.LclScaling.Set(fbxScale); + + return true; + } + + /// + /// if this game object is a model prefab or the model has already been exported, then export with shared components + /// + private bool ExportInstance(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode) + { + if (!unityGo || fbxNode == null) + { + return false; + } + + // where the fbx mesh is stored on a successful export + FbxMesh fbxMesh = null; + // store the shared mesh of the game object + Mesh unityGoMesh = null; + + // get the mesh of the game object + if (unityGo.TryGetComponent(out var meshFilter)) + { + unityGoMesh = meshFilter.sharedMesh; + } + + if (!unityGoMesh) + { + return false; + } + // export mesh as an instance if it is a duplicate mesh or a prefab + else if (SharedMeshes.TryGetValue(unityGoMesh, out var node)) + { + if (Verbose) + { + Debug.Log(string.Format("exporting instance {0}", unityGo.name)); + } + + fbxMesh = node.GetMesh(); + } + // unique mesh, so save it to find future duplicates + else + { + SharedMeshes.Add(unityGoMesh, fbxNode); + return false; + } + + // mesh doesn't exist or wasn't exported successfully + if (fbxMesh == null) + { + return false; + } + + // We don't export the mesh because we already have it from the parent, but we still need to assign the material + var renderer = unityGo.GetComponent(); + var materials = renderer ? renderer.sharedMaterials : null; + + Autodesk.Fbx.FbxSurfaceMaterial newMaterial = null; + if (materials != null) + { + foreach (var mat in materials) + { + if (mat != null && MaterialMap.TryGetValue(mat.GetInstanceID(), out newMaterial)) + { + fbxNode.AddMaterial(newMaterial); + } + else + { + // create new material + ExportMaterial(mat, fbxScene, fbxNode); + } + } + } + + // set the fbxNode containing the mesh + fbxNode.SetNodeAttribute(fbxMesh); + fbxNode.SetShadingMode(FbxNode.EShadingMode.eWireFrame); + + return true; + } + + /// + /// Exports camera component + /// + private bool ExportCamera(GameObject unityGO, FbxScene fbxScene, FbxNode fbxNode) + { + if (!unityGO || fbxScene == null || fbxNode == null) + { + return false; + } + + if (!unityGO.TryGetComponent(out var unityCamera)) + { + return false; + } + + var fbxCamera = FbxCamera.Create(fbxScene.GetFbxManager(), unityCamera.name); + if (fbxCamera == null) + { + return false; + } + + // UnityEditor.Formats.Fbx.Exporter.Visitors.CameraVisitor.ConfigureCamera(unityCamera, fbxCamera); + CameraVisitor.ConfigureCamera( unityCamera, fbxCamera ); + + fbxNode.SetNodeAttribute(fbxCamera); + + // set +90 post rotation to counteract for FBX camera's facing +X direction by default + fbxNode.SetPostRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(0, 90, 0)); + // have to set rotation active to true in order for post rotation to be applied + fbxNode.SetRotationActive(true); + + // make the last camera exported the default camera + DefaultCamera = fbxNode.GetName(); + + return true; + } + + /// + /// Exports light component. + /// Supported types: point, spot and directional + /// Cookie => Gobo + /// + private bool ExportLight(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode) + { + if (!unityGo || fbxScene == null || fbxNode == null) + { + return false; + } + + if (!unityGo.TryGetComponent(out var unityLight)) + return false; + + FbxLight.EType fbxLightType; + + // Is light type supported? + if (!MapLightType.TryGetValue(unityLight.type, out fbxLightType)) + return false; + + var fbxLight = FbxLight.Create(fbxScene.GetFbxManager(), unityLight.name); + + // Set the type of the light. + fbxLight.LightType.Set(fbxLightType); + + switch (unityLight.type) + { + case LightType.Directional: + { + break; + } + case LightType.Spot: + { + // Set the angle of the light's spotlight cone in degrees. + fbxLight.InnerAngle.Set(unityLight.spotAngle); + fbxLight.OuterAngle.Set(unityLight.spotAngle); + break; + } + case LightType.Point: + { + break; + } + case LightType.Area: + { + // TODO: areaSize: The size of the area light by scaling the node XY + break; + } + } + // The color of the light. + var unityLightColor = unityLight.color; + fbxLight.Color.Set(new FbxDouble3(unityLightColor.r, unityLightColor.g, unityLightColor.b)); + + // Set the Intensity of a light is multiplied with the Light color. + fbxLight.Intensity.Set(unityLight.intensity * UnitScaleFactor /*compensate for Maya scaling by system units*/); + + // Set the range of the light. + // applies-to: Point & Spot + // => FarAttenuationStart, FarAttenuationEnd + fbxLight.FarAttenuationStart.Set(0.01f /* none zero start */); + fbxLight.FarAttenuationEnd.Set(unityLight.range * UnitScaleFactor); + + // shadows Set how this light casts shadows + // applies-to: Point & Spot + var unityLightCastShadows = unityLight.shadows != LightShadows.None; + fbxLight.CastShadows.Set(unityLightCastShadows); + + fbxNode.SetNodeAttribute(fbxLight); + + // set +90 post rotation on x to counteract for FBX light's facing -Y direction by default + fbxNode.SetPostRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(90, 0, 0)); + // have to set rotation active to true in order for post rotation to be applied + fbxNode.SetRotationActive(true); + + return true; + } + + private bool ExportCommonConstraintProperties(TUnityConstraint uniConstraint, TFbxConstraint fbxConstraint, FbxNode fbxNode) + where TUnityConstraint : IConstraint where TFbxConstraint : FbxConstraint + { + fbxConstraint.Active.Set(uniConstraint.constraintActive); + fbxConstraint.Lock.Set(uniConstraint.locked); + fbxConstraint.Weight.Set(uniConstraint.weight * UnitScaleFactor); + + AddFbxNodeToConstraintsMapping(fbxNode, fbxConstraint, typeof(TUnityConstraint)); + return true; + } + + private struct ExpConstraintSource + { + private FbxNode m_node; + public FbxNode node + { + get { return m_node; } + set { m_node = value; } + } + + private float m_weight; + public float weight + { + get { return m_weight; } + set { m_weight = value; } + } + + public ExpConstraintSource(FbxNode node, float weight) + { + this.m_node = node; + this.m_weight = weight; + } + } + + private List GetConstraintSources(IConstraint unityConstraint) + { + if (unityConstraint == null) + { + return null; + } + + var fbxSources = new List(); + var sources = new List(); + unityConstraint.GetSources(sources); + foreach (var source in sources) + { + if (!source.sourceTransform) + { + continue; + } + + // ignore any sources that are not getting exported + FbxNode sourceNode; + if (!MapUnityObjectToFbxNode.TryGetValue(source.sourceTransform.gameObject, out sourceNode)) + { + continue; + } + fbxSources.Add(new ExpConstraintSource(sourceNode, source.weight * UnitScaleFactor)); + } + return fbxSources; + } + + private void AddFbxNodeToConstraintsMapping(FbxNode fbxNode, T fbxConstraint, System.Type uniConstraintType) where T : FbxConstraint + { + Dictionary constraintMapping; + if (!MapConstrainedObjectToConstraints.TryGetValue(fbxNode, out constraintMapping)) + { + constraintMapping = new Dictionary(); + MapConstrainedObjectToConstraints.Add(fbxNode, constraintMapping); + } + constraintMapping.Add(fbxConstraint, uniConstraintType); + } + + private bool ExportPositionConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode) + { + if (fbxNode == null) + { + return false; + } + + var uniPosConstraint = uniConstraint as PositionConstraint; + Debug.Assert(uniPosConstraint != null); + + var fbxPosConstraint = FbxConstraintPosition.Create(fbxScene, fbxNode.GetName() + "_positionConstraint"); + fbxPosConstraint.SetConstrainedObject(fbxNode); + var uniSources = GetConstraintSources(uniPosConstraint); + uniSources.ForEach(uniSource => fbxPosConstraint.AddConstraintSource(uniSource.node, uniSource.weight)); + ExportCommonConstraintProperties(uniPosConstraint, fbxPosConstraint, fbxNode); + + var uniAffectedAxes = uniPosConstraint.translationAxis; + fbxPosConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X); + fbxPosConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y); + fbxPosConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z); + + var fbxTranslationOffset = ConvertToFbxVector4(uniPosConstraint.translationOffset, UnitScaleFactor); + fbxPosConstraint.Translation.Set(ToFbxDouble3(fbxTranslationOffset)); + + // rest position is the position of the fbx node + var fbxRestTranslation = ConvertToFbxVector4(uniPosConstraint.translationAtRest, UnitScaleFactor); + // set the local position of fbxNode + fbxNode.LclTranslation.Set(ToFbxDouble3(fbxRestTranslation)); + return true; + } + + private bool ExportRotationConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode) + { + if (fbxNode == null) + { + return false; + } + + var uniRotConstraint = uniConstraint as RotationConstraint; + Debug.Assert(uniRotConstraint != null); + + var fbxRotConstraint = FbxConstraintRotation.Create(fbxScene, fbxNode.GetName() + "_rotationConstraint"); + fbxRotConstraint.SetConstrainedObject(fbxNode); + var uniSources = GetConstraintSources(uniRotConstraint); + uniSources.ForEach(uniSource => fbxRotConstraint.AddConstraintSource(uniSource.node, uniSource.weight)); + ExportCommonConstraintProperties(uniRotConstraint, fbxRotConstraint, fbxNode); + + var uniAffectedAxes = uniRotConstraint.rotationAxis; + fbxRotConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X); + fbxRotConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y); + fbxRotConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z); + + // Not converting rotation offset to XYZ euler as it gives the incorrect result in both Maya and Unity. + var uniRotationOffset = uniRotConstraint.rotationOffset; + var fbxRotationOffset = ToFbxDouble3(uniRotationOffset); + + fbxRotConstraint.Rotation.Set(fbxRotationOffset); + + // rest rotation is the rotation of the fbx node + var fbxRestRotation = ToFbxDouble3(uniRotConstraint.rotationAtRest); + // set the local rotation of fbxNode + fbxNode.LclRotation.Set(fbxRestRotation); + return true; + } + + private bool ExportScaleConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode) + { + if (fbxNode == null) + { + return false; + } + + var uniScaleConstraint = uniConstraint as ScaleConstraint; + Debug.Assert(uniScaleConstraint != null); + + var fbxScaleConstraint = FbxConstraintScale.Create(fbxScene, fbxNode.GetName() + "_scaleConstraint"); + fbxScaleConstraint.SetConstrainedObject(fbxNode); + var uniSources = GetConstraintSources(uniScaleConstraint); + uniSources.ForEach(uniSource => fbxScaleConstraint.AddConstraintSource(uniSource.node, uniSource.weight)); + ExportCommonConstraintProperties(uniScaleConstraint, fbxScaleConstraint, fbxNode); + + var uniAffectedAxes = uniScaleConstraint.scalingAxis; + fbxScaleConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X); + fbxScaleConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y); + fbxScaleConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z); + + var uniScaleOffset = uniScaleConstraint.scaleOffset; + var fbxScalingOffset = ToFbxDouble3(uniScaleOffset); + fbxScaleConstraint.Scaling.Set(fbxScalingOffset); + + // rest rotation is the rotation of the fbx node + var uniRestScale = uniScaleConstraint.scaleAtRest; + var fbxRestScale = ToFbxDouble3(uniRestScale); + // set the local rotation of fbxNode + fbxNode.LclScaling.Set(fbxRestScale); + return true; + } + + private bool ExportAimConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode) + { + if (fbxNode == null) + { + return false; + } + + var uniAimConstraint = uniConstraint as AimConstraint; + Debug.Assert(uniAimConstraint != null); + + var fbxAimConstraint = FbxConstraintAim.Create(fbxScene, fbxNode.GetName() + "_aimConstraint"); + fbxAimConstraint.SetConstrainedObject(fbxNode); + var uniSources = GetConstraintSources(uniAimConstraint); + uniSources.ForEach(uniSource => fbxAimConstraint.AddConstraintSource(uniSource.node, uniSource.weight)); + ExportCommonConstraintProperties(uniAimConstraint, fbxAimConstraint, fbxNode); + + var uniAffectedAxes = uniAimConstraint.rotationAxis; + fbxAimConstraint.AffectX.Set((uniAffectedAxes & Axis.X) == Axis.X); + fbxAimConstraint.AffectY.Set((uniAffectedAxes & Axis.Y) == Axis.Y); + fbxAimConstraint.AffectZ.Set((uniAffectedAxes & Axis.Z) == Axis.Z); + + var uniRotationOffset = uniAimConstraint.rotationOffset; + var fbxRotationOffset = ToFbxDouble3(uniRotationOffset); + fbxAimConstraint.RotationOffset.Set(fbxRotationOffset); + + // rest rotation is the rotation of the fbx node + var fbxRestRotation = ToFbxDouble3(uniAimConstraint.rotationAtRest); + // set the local rotation of fbxNode + fbxNode.LclRotation.Set(fbxRestRotation); + + var fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtNone; + switch (uniAimConstraint.worldUpType) + { + case AimConstraint.WorldUpType.None: + fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtNone; + break; + case AimConstraint.WorldUpType.ObjectRotationUp: + fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtObjectRotationUp; + break; + case AimConstraint.WorldUpType.ObjectUp: + fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtObjectUp; + break; + case AimConstraint.WorldUpType.SceneUp: + fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtSceneUp; + break; + case AimConstraint.WorldUpType.Vector: + fbxWorldUpType = FbxConstraintAim.EWorldUp.eAimAtVector; + break; + default: + throw new System.NotImplementedException(); + } + fbxAimConstraint.WorldUpType.Set((int)fbxWorldUpType); + + var uniAimVector = ConvertToFbxVector4(uniAimConstraint.aimVector); + fbxAimConstraint.AimVector.Set(ToFbxDouble3(uniAimVector)); + fbxAimConstraint.UpVector.Set(ToFbxDouble3(uniAimConstraint.upVector)); + fbxAimConstraint.WorldUpVector.Set(ToFbxDouble3(uniAimConstraint.worldUpVector)); + + if (uniAimConstraint.worldUpObject && MapUnityObjectToFbxNode.ContainsKey(uniAimConstraint.worldUpObject.gameObject)) + { + fbxAimConstraint.SetWorldUpObject(MapUnityObjectToFbxNode[uniAimConstraint.worldUpObject.gameObject]); + } + return true; + } + + private bool ExportParentConstraint(IConstraint uniConstraint, FbxScene fbxScene, FbxNode fbxNode) + { + if (fbxNode == null) + { + return false; + } + + var uniParentConstraint = uniConstraint as ParentConstraint; + Debug.Assert(uniParentConstraint != null); + + var fbxParentConstraint = FbxConstraintParent.Create(fbxScene, fbxNode.GetName() + "_parentConstraint"); + fbxParentConstraint.SetConstrainedObject(fbxNode); + var uniSources = GetConstraintSources(uniParentConstraint); + var uniTranslationOffsets = uniParentConstraint.translationOffsets; + var uniRotationOffsets = uniParentConstraint.rotationOffsets; + for (var i = 0; i < uniSources.Count; i++) + { + var uniSource = uniSources[i]; + var uniTranslationOffset = uniTranslationOffsets[i]; + var uniRotationOffset = uniRotationOffsets[i]; + + fbxParentConstraint.AddConstraintSource(uniSource.node, uniSource.weight); + + var fbxTranslationOffset = ConvertToFbxVector4(uniTranslationOffset, UnitScaleFactor); + fbxParentConstraint.SetTranslationOffset(uniSource.node, fbxTranslationOffset); + + var fbxRotationOffset = ConvertToFbxVector4(uniRotationOffset); + fbxParentConstraint.SetRotationOffset(uniSource.node, fbxRotationOffset); + } + ExportCommonConstraintProperties(uniParentConstraint, fbxParentConstraint, fbxNode); + + var uniTranslationAxes = uniParentConstraint.translationAxis; + fbxParentConstraint.AffectTranslationX.Set((uniTranslationAxes & Axis.X) == Axis.X); + fbxParentConstraint.AffectTranslationY.Set((uniTranslationAxes & Axis.Y) == Axis.Y); + fbxParentConstraint.AffectTranslationZ.Set((uniTranslationAxes & Axis.Z) == Axis.Z); + + var uniRotationAxes = uniParentConstraint.rotationAxis; + fbxParentConstraint.AffectRotationX.Set((uniRotationAxes & Axis.X) == Axis.X); + fbxParentConstraint.AffectRotationY.Set((uniRotationAxes & Axis.Y) == Axis.Y); + fbxParentConstraint.AffectRotationZ.Set((uniRotationAxes & Axis.Z) == Axis.Z); + + // rest position is the position of the fbx node + var fbxRestTranslation = ConvertToFbxVector4(uniParentConstraint.translationAtRest, UnitScaleFactor); + // set the local position of fbxNode + fbxNode.LclTranslation.Set(ToFbxDouble3(fbxRestTranslation)); + + // rest rotation is the rotation of the fbx node + var fbxRestRotation = ToFbxDouble3(uniParentConstraint.rotationAtRest); + // set the local rotation of fbxNode + fbxNode.LclRotation.Set(fbxRestRotation); + return true; + } + + private delegate bool ExportConstraintDelegate(IConstraint c, FbxScene fs, FbxNode fn); + + private bool ExportConstraints(GameObject unityGo, FbxScene fbxScene, FbxNode fbxNode) + { + if (!unityGo) + { + return false; + } + + var mapConstraintTypeToExportFunction = new Dictionary() + { + { typeof(PositionConstraint), ExportPositionConstraint }, + { typeof(RotationConstraint), ExportRotationConstraint }, + { typeof(ScaleConstraint), ExportScaleConstraint }, + { typeof(AimConstraint), ExportAimConstraint }, + { typeof(ParentConstraint), ExportParentConstraint } + }; + + // check if GameObject has one of the 5 supported constraints: aim, parent, position, rotation, scale + var uniConstraints = unityGo.GetComponents(); + + foreach (var uniConstraint in uniConstraints) + { + var uniConstraintType = uniConstraint.GetType(); + ExportConstraintDelegate constraintDelegate; + if (!mapConstraintTypeToExportFunction.TryGetValue(uniConstraintType, out constraintDelegate)) + { + Debug.LogWarningFormat("FbxExporter: Missing function to export constraint of type {0}", uniConstraintType.Name); + continue; + } + constraintDelegate(uniConstraint, fbxScene, fbxNode); + } + + return true; + } + + /// + /// Return set of sample times to cover all keys on animation curves + /// + internal static HashSet GetSampleTimes(AnimationCurve[] animCurves, double sampleRate) + { + var keyTimes = new HashSet(); + var fs = 1.0 / sampleRate; + + double firstTime = double.MaxValue, lastTime = double.MinValue; + + foreach (var ac in animCurves) + { + if (ac == null || ac.length <= 0) continue; + + firstTime = System.Math.Min(firstTime, ac[0].time); + lastTime = System.Math.Max(lastTime, ac[ac.length - 1].time); + } + + // if these values didn't get set there were no valid anim curves, + // so don't return any keys + if (firstTime == double.MaxValue || lastTime == double.MinValue) + { + return keyTimes; + } + + var firstframe = (int)System.Math.Floor(firstTime * sampleRate); + var lastframe = (int)System.Math.Ceiling(lastTime * sampleRate); + for (var i = firstframe; i <= lastframe; i++) + { + keyTimes.Add((float)(i * fs)); + } + + return keyTimes; + } + + /// + /// Return set of all keys times on animation curves + /// + internal static HashSet GetKeyTimes(AnimationCurve[] animCurves) + { + var keyTimes = new HashSet(); + + foreach (var ac in animCurves) + { + if (ac != null) foreach (var key in ac.keys) { keyTimes.Add(key.time); } + } + + return keyTimes; + } + + /// + /// Export animation curve key frames with key tangents + /// NOTE : This is a work in progress (WIP). We only export the key time and value on + /// a Cubic curve using the default tangents. + /// + internal static void ExportAnimationKeys(AnimationCurve uniAnimCurve, FbxAnimCurve fbxAnimCurve, + UnityToMayaConvertSceneHelper convertSceneHelper) + { + // Copy Unity AnimCurve to FBX AnimCurve. + // NOTE: only cubic keys are supported by the FbxImporter + using (new FbxAnimCurveModifyHelper(new List {fbxAnimCurve})) + { + for (var keyIndex = 0; keyIndex < uniAnimCurve.length; ++keyIndex) + { + var uniKeyFrame = uniAnimCurve[keyIndex]; + var fbxTime = FbxTime.FromSecondDouble(uniKeyFrame.time); + + var fbxKeyIndex = fbxAnimCurve.KeyAdd(fbxTime); + + // configure tangents + var lTangent = AnimationUtility.GetKeyLeftTangentMode(uniAnimCurve, keyIndex); + var rTangent = AnimationUtility.GetKeyRightTangentMode(uniAnimCurve, keyIndex); + + // Always set tangent mode to eTangentBreak, as other modes are not handled the same in FBX as in + // Unity, thus leading to discrepancies in animation curves. + var tanMode = FbxAnimCurveDef.ETangentMode.eTangentBreak; + + // Default to cubic interpolation, which is the default for KeySet + var interpMode = FbxAnimCurveDef.EInterpolationType.eInterpolationCubic; + switch (rTangent) + { + case AnimationUtility.TangentMode.Linear: + interpMode = FbxAnimCurveDef.EInterpolationType.eInterpolationLinear; + break; + case AnimationUtility.TangentMode.Constant: + interpMode = FbxAnimCurveDef.EInterpolationType.eInterpolationConstant; + break; + default: + break; + } + + var vvv = convertSceneHelper.Convert( uniKeyFrame.value ); + // var vvv = convertSceneHelper.Convert( uniKeyFrame.value ); + + var outTangent = convertSceneHelper.ConvertAnim( uniKeyFrame.outTangent ); + var nextInTangent = keyIndex < uniAnimCurve.length - 1 + ? convertSceneHelper.ConvertAnim( uniAnimCurve[keyIndex + 1].inTangent ) + : 0; + + fbxAnimCurve.KeySet(fbxKeyIndex, + fbxTime, + vvv, + interpMode, + tanMode, + // value of right slope + outTangent, + // value of next left slope + nextInTangent, + FbxAnimCurveDef.EWeightedMode.eWeightedAll, + // weight for right slope + uniKeyFrame.outWeight, + // weight for next left slope + keyIndex < uniAnimCurve.length - 1 ? uniAnimCurve[keyIndex + 1].inWeight : 0 + ); + } + } + } + + /// + /// Export animation curve key samples + /// + internal void ExportAnimationSamples(AnimationCurve uniAnimCurve, FbxAnimCurve fbxAnimCurve, + double sampleRate, + UnityToMayaConvertSceneHelper convertSceneHelper) + { + using (new FbxAnimCurveModifyHelper(new List {fbxAnimCurve})) + { + foreach (var currSampleTime in GetSampleTimes(new AnimationCurve[] {uniAnimCurve}, sampleRate)) + { + var currSampleValue = uniAnimCurve.Evaluate((float)currSampleTime); + + var fbxTime = FbxTime.FromSecondDouble(currSampleTime); + + var fbxKeyIndex = fbxAnimCurve.KeyAdd(fbxTime); + + var pVal = convertSceneHelper.Convert( currSampleValue ); + // var pVal = convertSceneHelper.ConvertAnim( currSampleValue ); + // var pVal2 = convertSceneHelper.Convert( currSampleValue ); + // if ( pVal2.AlmostEqual( 84.7f, 1 ) ) + if ( pVal > 100f ) + { + Log.Info( $"PVal: {pVal} ( {currSampleValue} ) - {currSampleTime} " ); + } + + fbxAnimCurve.KeySet( fbxKeyIndex, + fbxTime, + pVal + ); + } + } + } + + /// + /// Get the FbxConstraint associated with the constrained node. + /// + /// + /// + /// + private FbxConstraint GetFbxConstraint(FbxNode constrainedNode, System.Type uniConstraintType) + { + if (uniConstraintType == null || !uniConstraintType.GetInterfaces().Contains(typeof(IConstraint))) + { + // not actually a constraint + return null; + } + + Dictionary constraints; + if (MapConstrainedObjectToConstraints.TryGetValue(constrainedNode, out constraints)) + { + var targetConstraint = constraints.FirstOrDefault(constraint => (constraint.Value == uniConstraintType)); + if (!targetConstraint.Equals(default(KeyValuePair))) + { + return targetConstraint.Key; + } + } + + return null; + } + + /// + /// Get the FbxBlendshape with the given name associated with the FbxNode. + /// + /// + /// + /// + private FbxBlendShapeChannel GetFbxBlendShape(FbxNode blendshapeNode, string uniPropertyName) + { + List blendshapeChannels; + if (MapUnityObjectToBlendShapes.TryGetValue(blendshapeNode, out blendshapeChannels)) + { + var match = System.Text.RegularExpressions.Regex.Match(uniPropertyName, @"blendShape\.(\S+)"); + if (match.Success && match.Groups.Count > 0) + { + var blendshapeName = match.Groups[1].Value; + + var targetChannel = blendshapeChannels.FirstOrDefault(channel => (channel.GetName() == blendshapeName)); + if (targetChannel != null) + { + return targetChannel; + } + } + } + return null; + } + + private FbxProperty GetFbxProperty(FbxNode fbxNode, string fbxPropertyName, System.Type uniPropertyType, string uniPropertyName) + { + if (fbxNode == null) + { + return null; + } + + // check if property maps to a constraint + // check this first because both constraints and FbxNodes can contain a RotationOffset property, + // but only the constraint one is animatable. + var fbxConstraint = GetFbxConstraint(fbxNode, uniPropertyType); + if (fbxConstraint != null) + { + var prop = fbxConstraint.FindProperty(fbxPropertyName, false); + if (prop.IsValid()) + { + return prop; + } + } + + // check if the property maps to a blendshape + var fbxBlendShape = GetFbxBlendShape(fbxNode, uniPropertyName); + if (fbxBlendShape != null) + { + var prop = fbxBlendShape.FindProperty(fbxPropertyName, false); + if (prop.IsValid()) + { + return prop; + } + } + + // map unity property name to fbx property + var fbxProperty = fbxNode.FindProperty(fbxPropertyName, false); + if (fbxProperty.IsValid()) + { + return fbxProperty; + } + + var fbxNodeAttribute = fbxNode.GetNodeAttribute(); + if (fbxNodeAttribute != null) + { + fbxProperty = fbxNodeAttribute.FindProperty(fbxPropertyName, false); + } + return fbxProperty; + } + + /// + /// Export an AnimationCurve. + /// NOTE: This is not used for rotations, because we need to convert from + /// quaternion to euler and various other stuff. + /// + private void ExportAnimationCurve(FbxNode fbxNode, + AnimationCurve uniAnimCurve, + float frameRate, + string uniPropertyName, + System.Type uniPropertyType, + FbxAnimLayer fbxAnimLayer) + { + if (fbxNode == null) + { + return; + } + + if (Verbose) + { + // Debug.Log("Exporting animation for " + fbxNode.GetName() + " (" + uniPropertyName + ")"); + Debug.Log( $"Exporting animation for {fbxNode.GetName()} ( {uniPropertyName} ) | {fbxAnimLayer.GetName()}" ); + } + + var fbxConstraint = GetFbxConstraint(fbxNode, uniPropertyType); + FbxPropertyChannelPair[] fbxPropertyChannelPairs; + if (!FbxPropertyChannelPair.TryGetValue(uniPropertyName, out fbxPropertyChannelPairs, fbxConstraint)) + { + Debug.LogWarning(string.Format("no mapping from Unity '{0}' to fbx property", uniPropertyName)); + return; + } + + foreach (var fbxPropertyChannelPair in fbxPropertyChannelPairs) + { + // map unity property name to fbx property + var fbxProperty = GetFbxProperty(fbxNode, fbxPropertyChannelPair.Property, uniPropertyType, uniPropertyName); + if (!fbxProperty.IsValid()) + { + Debug.LogError(string.Format("no fbx property {0} found on {1} node or nodeAttribute ", fbxPropertyChannelPair.Property, fbxNode.GetName())); + return; + } + if (!fbxProperty.GetFlag(FbxPropertyFlags.EFlags.eAnimatable)) + { + Debug.LogErrorFormat("fbx property {0} found on node {1} is not animatable", fbxPropertyChannelPair.Property, fbxNode.GetName()); + } + + // Create the AnimCurve on the channel + var fbxAnimCurve = fbxProperty.GetCurve(fbxAnimLayer, fbxPropertyChannelPair.Channel, true); + if (fbxAnimCurve == null) + { + return; + } + + // create a convert scene helper so that we can convert from Unity to Maya + // AxisSystem (LeftHanded to RightHanded) and FBX's default units + // (Meters to Centimetres) + var convertSceneHelper = new UnityToMayaConvertSceneHelper(uniPropertyName, fbxNode); + + if ( ModelExporter.ExportSettings.BakeAnimationProperty ) + { + ExportAnimationSamples(uniAnimCurve, fbxAnimCurve, frameRate, convertSceneHelper); + } + else + { + ExportAnimationKeys(uniAnimCurve, fbxAnimCurve, convertSceneHelper); + } + } + } + + internal class UnityToMayaConvertSceneHelper + { + bool convertDistance = false; + bool convertToRadian = false; + bool convertLensShiftX = false; + bool convertLensShiftY = false; + + FbxCamera camera = null; + + float unitScaleFactor = 1f; + + public UnityToMayaConvertSceneHelper(string uniPropertyName, FbxNode fbxNode) + { + var cc = System.StringComparison.CurrentCulture; + + var partT = uniPropertyName.StartsWith("m_LocalPosition.", cc) || uniPropertyName.StartsWith("m_TranslationOffset", cc); + + convertDistance |= partT; + convertDistance |= uniPropertyName.StartsWith("m_Intensity", cc); + convertDistance |= uniPropertyName.ToLower().EndsWith("weight", cc); + convertLensShiftX |= uniPropertyName.StartsWith("m_LensShift.x", cc); + convertLensShiftY |= uniPropertyName.StartsWith("m_LensShift.y", cc); + if (convertLensShiftX || convertLensShiftY) + { + camera = fbxNode.GetCamera(); + } + + // The ParentConstraint's source Rotation Offsets are read in as radians, so make sure they are exported as radians + convertToRadian = uniPropertyName.StartsWith("m_RotationOffsets.Array.data", cc); + + if (convertDistance) + unitScaleFactor = ModelExporter.UnitScaleFactor; + + if (convertToRadian) + { + unitScaleFactor *= (Mathf.PI / 180); + } + } + + public float Convert(float value) + { + var convertedValue = value; + if (convertLensShiftX || convertLensShiftY) + { + convertedValue = Mathf.Clamp(Mathf.Abs(value), 0f, 1f) * Mathf.Sign(value); + } + if (camera != null) + { + if (convertLensShiftX) + { + convertedValue *= (float)camera.GetApertureWidth(); + } + else if (convertLensShiftY) + { + convertedValue *= (float)camera.GetApertureHeight(); + } + } + + // left handed to right handed conversion + // meters to centimetres conversion + // Log.Info( $"unitScaleFactor: {unitScaleFactor}" ); + return unitScaleFactor * convertedValue; + } + + public float ConvertAnim( float value ) + { + var convertedValue = value; + if ( convertLensShiftX || convertLensShiftY ) + { + convertedValue = Mathf.Clamp( Mathf.Abs( value ), 0f, 1f ) * Mathf.Sign( value ); + } + + if ( camera != null ) + { + if ( convertLensShiftX ) + { + convertedValue *= ( float )camera.GetApertureWidth(); + } + else if ( convertLensShiftY ) + { + convertedValue *= ( float )camera.GetApertureHeight(); + } + } + + return convertedValue; + } + } + + /// + /// Export an AnimationClip as a single take + /// + private void ExportAnimationClip( AnimationClip uniAnimClip, GameObject uniRoot, FbxScene fbxScene ) + { + if ( !uniAnimClip || !uniRoot || fbxScene == null ) return; + + if ( Verbose ) + Debug.Log(string.Format("Exporting animation clip ({1}) for {0}", uniRoot.name, uniAnimClip.name)); + + // setup anim stack + var fbxAnimStack = FbxAnimStack.Create(fbxScene, uniAnimClip.name); + fbxAnimStack.Description.Set("Animation Take: " + uniAnimClip.name); + + // add one mandatory animation layer + var fbxAnimLayer = FbxAnimLayer.Create(fbxScene, "Animation Base Layer"); + fbxAnimStack.AddMember(fbxAnimLayer); + + // Set up the FPS so our frame-relative math later works out + // Custom frame rate isn't really supported in FBX SDK (there's + // a bug), so try hard to find the nearest time mode. + var timeMode = FbxTime.EMode.eCustom; + var precision = 1e-6; + while (timeMode == FbxTime.EMode.eCustom && precision < 1000) + { + timeMode = FbxTime.ConvertFrameRateToTimeMode(uniAnimClip.frameRate, precision); + precision *= 10; + } + if (timeMode == FbxTime.EMode.eCustom) + { + timeMode = FbxTime.EMode.eFrames30; + } + + fbxScene.GetGlobalSettings().SetTimeMode(timeMode); + + // set time correctly + var fbxStartTime = FbxTime.FromSecondDouble(0); + var fbxStopTime = FbxTime.FromSecondDouble(uniAnimClip.length); + + fbxAnimStack.SetLocalTimeSpan(new FbxTimeSpan(fbxStartTime, fbxStopTime)); + + var unityCurves = new Dictionary>(); + + // extract and store all necessary information from the curve bindings, namely the animation curves + // and their corresponding property names for each GameObject. + foreach (var uniCurveBinding in AnimationUtility.GetCurveBindings(uniAnimClip)) + { + var uniObj = AnimationUtility.GetAnimatedObject(uniRoot, uniCurveBinding); + if (!uniObj) + { + continue; + } + + var uniAnimCurve = AnimationUtility.GetEditorCurve(uniAnimClip, uniCurveBinding); + if (uniAnimCurve == null) + { + continue; + } + + var uniGO = GetGameObject( uniObj ); + // Check if the GameObject has an FBX node to the animation. It might be null because the LOD selected doesn't match the one on the gameobject. + if ( !uniGO || MapUnityObjectToFbxNode.ContainsKey( uniGO ) == false ) + { + continue; + } + + if ( unityCurves.ContainsKey( uniGO ) ) + { + unityCurves[uniGO].Add( new UnityCurve( uniCurveBinding.propertyName, uniAnimCurve, uniCurveBinding.type ) ); + continue; + } + + unityCurves.Add( uniGO, new List() { new UnityCurve( uniCurveBinding.propertyName, uniAnimCurve, uniCurveBinding.type ) } ); + } + + // transfer root motion + var animSource = ExportOptions.AnimationSource; + var animDest = ExportOptions.AnimationDest; + if ( animSource && animDest && animSource != animDest ) + { + // list of all transforms between source and dest, including source and dest + var transformsFromSourceToDest = new List(); + var curr = animDest; + while ( curr != animSource ) + { + transformsFromSourceToDest.Add(curr); + curr = curr.parent; + } + transformsFromSourceToDest.Add(animSource); + transformsFromSourceToDest.Reverse(); + + // while there are 2 transforms in the list, transfer the animation from the + // first to the next transform. + // Then remove the first transform from the list. + while (transformsFromSourceToDest.Count >= 2) + { + var source = transformsFromSourceToDest[0]; + transformsFromSourceToDest.RemoveAt(0); + var dest = transformsFromSourceToDest[0]; + + TransferMotion(source, dest, uniAnimClip.frameRate, ref unityCurves); + } + } + + /* The major difficulty: Unity uses quaternions for rotation + * (which is how it should be) but FBX uses Euler angles. So we + * need to gather up the list of transform curves per object. + * + * For euler angles, Unity uses ZXY rotation order while Maya uses XYZ. + * Maya doesn't import files with ZXY rotation correctly, so have to convert to XYZ. + * Need all 3 curves in order to convert. + * + * Also, in both cases, prerotation has to be removed from the animated rotation if + * there are bones being exported. + */ + var rotations = new Dictionary(); + + // export the animation curves for each GameObject that has animation + foreach (var kvp in unityCurves) + { + var uniGO = kvp.Key; + foreach (var uniCurve in kvp.Value) + { + var propertyName = uniCurve.propertyName; + var uniAnimCurve = uniCurve.uniAnimCurve; + + // Do not create the curves if the component is a SkinnedMeshRenderer and if the option in FBX Export settings is toggled on. + if (!ExportOptions.AnimateSkinnedMesh && (uniGO.GetComponent() != null)) + { + continue; + } + + FbxNode fbxNode; + if (!MapUnityObjectToFbxNode.TryGetValue(uniGO, out fbxNode)) + { + Debug.LogError(string.Format("no FbxNode found for {0}", uniGO.name)); + continue; + } + + var index = QuaternionCurve.GetQuaternionIndex(propertyName); + if (index >= 0) + { + // Rotation property; save it to convert quaternion -> euler later. + var rotCurve = GetRotationCurve(uniGO, uniAnimClip.frameRate, ref rotations); + rotCurve.SetCurve(index, uniAnimCurve); + continue; + } + + // If this is an euler curve with a prerotation, then need to sample animations to remove the prerotation. + // Otherwise can export normally with tangents. + index = EulerCurve.GetEulerIndex(propertyName); + if (index >= 0 && + // still need to sample euler curves if baking is specified + (ModelExporter.ExportSettings.BakeAnimationProperty || + // also need to make sure to sample if there is a prerotation, as this is baked into the Unity curves + fbxNode.GetPreRotation(FbxNode.EPivotSet.eSourcePivot).Distance(new FbxVector4()) > 0)) + { + var rotCurve = GetRotationCurve(uniGO, uniAnimClip.frameRate, ref rotations); + rotCurve.SetCurve(index, uniAnimCurve); + continue; + } + + // simple property (e.g. intensity), export right away + ExportAnimationCurve(fbxNode, uniAnimCurve, uniAnimClip.frameRate, + propertyName, uniCurve.propertyType, + fbxAnimLayer); + } + } + + // now export all the quaternion curves + foreach (var kvp in rotations) + { + var unityGo = kvp.Key; + var rot = kvp.Value; + + FbxNode fbxNode; + if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode)) + { + Debug.LogError(string.Format("no FbxNode found for {0}", unityGo.name)); + continue; + } + rot.Animate(unityGo.transform, fbxNode, fbxAnimLayer, Verbose); + } + } + + /// + /// Transfers transform animation from source to dest. Replaces dest's Unity Animation Curves with updated animations. + /// NOTE: Source must be the parent of dest. + /// + /// Source animated object. + /// Destination, child of the source. + /// Sample rate. + /// Unity curves. + private void TransferMotion(Transform source, Transform dest, float sampleRate, ref Dictionary> unityCurves) + { + // get sample times for curves in dest + source + // at each sample time, evaluate all 18 transfom anim curves, creating 2 transform matrices + // combine the matrices, get the new values, apply to the 9 new anim curves for dest + if (dest.parent != source) + { + Debug.LogError("dest must be a child of source"); + return; + } + + List sourceUnityCurves; + if (!unityCurves.TryGetValue(source.gameObject, out sourceUnityCurves)) + { + return; // nothing to do, source has no animation + } + + List destUnityCurves; + if (!unityCurves.TryGetValue(dest.gameObject, out destUnityCurves)) + { + destUnityCurves = new List(); + } + + var animCurves = new List(); + foreach (var curve in sourceUnityCurves) + { + // TODO: check if curve is anim related + animCurves.Add(curve.uniAnimCurve); + } + foreach (var curve in destUnityCurves) + { + animCurves.Add(curve.uniAnimCurve); + } + + var sampleTimes = GetSampleTimes(animCurves.ToArray(), sampleRate); + // need to create 9 new UnityCurves, one for each property + var posKeyFrames = new Keyframe[3][]; + var rotKeyFrames = new Keyframe[3][]; + var scaleKeyFrames = new Keyframe[3][]; + + for (var k = 0; k < posKeyFrames.Length; k++) + { + posKeyFrames[k] = new Keyframe[sampleTimes.Count]; + rotKeyFrames[k] = new Keyframe[sampleTimes.Count]; + scaleKeyFrames[k] = new Keyframe[sampleTimes.Count]; + } + + // If we have a point in local coords represented as a column-vector x, the equation of x in coordinates relative to source's parent is: + // x_grandparent = source * dest * x + // Now we're going to change dest to dest' which has the animation from source. And we're going to change + // source to source' which has no animation. The equation of x will become: + // x_grandparent = source' * dest' * x + // We're not changing x_grandparent and x, so we need that: + // source * dest = source' * dest' + // We know dest and source (both animated) and source' (static). Solve for dest': + // dest' = (source')^-1 * source * dest + var keyIndex = 0; + var sourceStaticMatrixInverse = Matrix4x4.TRS(source.localPosition, source.localRotation, source.localScale).inverse; + foreach (var currSampleTime in sampleTimes) + { + var sourceLocalMatrix = GetTransformMatrix(currSampleTime, source, sourceUnityCurves); + var destLocalMatrix = GetTransformMatrix(currSampleTime, dest, destUnityCurves); + + var newLocalMatrix = sourceStaticMatrixInverse * sourceLocalMatrix * destLocalMatrix; + + FbxVector4 translation, rotation, scale; + GetTRSFromMatrix(newLocalMatrix, out translation, out rotation, out scale); + + // get rotation directly from matrix, as otherwise causes issues + // with negative rotations. + var rot = newLocalMatrix.rotation.eulerAngles; + + for (var k = 0; k < 3; k++) + { + posKeyFrames[k][keyIndex] = new Keyframe(currSampleTime, (float)translation[k]); + rotKeyFrames[k][keyIndex] = new Keyframe(currSampleTime, rot[k] ); + scaleKeyFrames[k][keyIndex] = new Keyframe( currSampleTime, ( float )scale[k] ); + } + keyIndex++; + } + + // create the new list of unity curves, and add it to dest's curves + var newUnityCurves = new List(); + var posPropName = "m_LocalPosition."; + var rotPropName = "localEulerAnglesRaw."; + var scalePropName = "m_LocalScale."; + var xyz = "xyz"; + for (var k = 0; k < 3; k++) + { + var posUniCurve = new UnityCurve(posPropName + xyz[k], new AnimationCurve(posKeyFrames[k]), typeof(Transform)); + newUnityCurves.Add(posUniCurve); + + var rotUniCurve = new UnityCurve(rotPropName + xyz[k], new AnimationCurve(rotKeyFrames[k]), typeof(Transform)); + newUnityCurves.Add(rotUniCurve); + + var scaleUniCurve = new UnityCurve(scalePropName + xyz[k], new AnimationCurve(scaleKeyFrames[k]), typeof(Transform)); + newUnityCurves.Add(scaleUniCurve); + } + + // remove old transform curves + RemoveTransformCurves(ref sourceUnityCurves); + RemoveTransformCurves(ref destUnityCurves); + + unityCurves[source.gameObject] = sourceUnityCurves; + if (!unityCurves.ContainsKey(dest.gameObject)) + { + unityCurves.Add(dest.gameObject, newUnityCurves); + return; + } + unityCurves[dest.gameObject].AddRange(newUnityCurves); + } + + private void RemoveTransformCurves(ref List curves) + { + var transformCurves = new List(); + var transformPropNames = new string[] {"m_LocalPosition.", "m_LocalRotation", "localEulerAnglesRaw.", "m_LocalScale."}; + foreach (var curve in curves) + { + foreach (var prop in transformPropNames) + { + if (curve.propertyName.StartsWith(prop)) + { + transformCurves.Add(curve); + break; + } + } + } + foreach (var curve in transformCurves) + { + curves.Remove(curve); + } + } + + private Matrix4x4 GetTransformMatrix(float currSampleTime, Transform orig, List unityCurves) + { + var sourcePos = orig.localPosition; + var sourceRot = orig.localRotation; + var sourceScale = orig.localScale; + + foreach (var uniCurve in unityCurves) + { + var currSampleValue = uniCurve.uniAnimCurve.Evaluate(currSampleTime); + var propName = uniCurve.propertyName; + // try position, scale, quat then euler + var temp = QuaternionCurve.GetQuaternionIndex(propName); + if (temp >= 0) + { + sourceRot[temp] = currSampleValue; + continue; + } + temp = EulerCurve.GetEulerIndex(propName); + if (temp >= 0) + { + var euler = sourceRot.eulerAngles; + euler[temp] = currSampleValue; + sourceRot.eulerAngles = euler; + continue; + } + temp = GetPositionIndex(propName); + if (temp >= 0) + { + sourcePos[temp] = currSampleValue; + continue; + } + temp = GetScaleIndex(propName); + if (temp >= 0) + { + sourceScale[temp] = currSampleValue; + } + } + + sourceRot = Quaternion.Euler(sourceRot.eulerAngles.x, sourceRot.eulerAngles.y, sourceRot.eulerAngles.z); + return Matrix4x4.TRS(sourcePos, sourceRot, sourceScale); + } + + internal struct UnityCurve + { + public string propertyName; + public AnimationCurve uniAnimCurve; + public System.Type propertyType; + + public UnityCurve(string propertyName, AnimationCurve uniAnimCurve, System.Type propertyType) + { + this.propertyName = propertyName; + this.uniAnimCurve = uniAnimCurve; + this.propertyType = propertyType; + } + } + + private int GetPositionIndex(string uniPropertyName) + { + var ct = System.StringComparison.CurrentCulture; + var isPositionComponent = uniPropertyName.StartsWith("m_LocalPosition.", ct); + + if (!isPositionComponent) { return -1; } + + switch (uniPropertyName[uniPropertyName.Length - 1]) + { + case 'x': + return 0; + case 'y': + return 1; + case 'z': + return 2; + default: + return -1; + } + } + + private int GetScaleIndex(string uniPropertyName) + { + var ct = System.StringComparison.CurrentCulture; + var isScaleComponent = uniPropertyName.StartsWith("m_LocalScale.", ct); + + if (!isScaleComponent) { return -1; } + + switch (uniPropertyName[uniPropertyName.Length - 1]) + { + case 'x': + return 0; + case 'y': + return 1; + case 'z': + return 2; + default: + return -1; + } + } + + /// + /// Gets or creates the rotation curve for GameObject uniGO. + /// + /// The rotation curve. + /// Unity GameObject. + /// Frame rate. + /// Rotations. + /// RotationCurve is abstract so specify type of RotationCurve to create. + private RotationCurve GetRotationCurve( + GameObject uniGO, float frameRate, + ref Dictionary rotations + ) where T : RotationCurve, new() + { + RotationCurve rotCurve; + if (!rotations.TryGetValue(uniGO, out rotCurve)) + { + rotCurve = new T { SampleRate = frameRate }; + rotations.Add(uniGO, rotCurve); + } + return rotCurve; + } + + /// + /// Export the Animator component on this game object + /// + private void ExportAnimation( GameObject uniRoot, FbxScene fbxScene ) + { + if (!uniRoot) + { + return; + } + + var exportedClips = new HashSet(); + + var uniAnimator = uniRoot.GetComponent(); + if (uniAnimator) + { + // Try the animator controller (mecanim) + var controller = uniAnimator.runtimeAnimatorController; + + if (controller) + { + // Only export each clip once per game object. + foreach (var clip in controller.animationClips) + { + if (exportedClips.Add(clip)) + { + ExportAnimationClip(clip, uniRoot, fbxScene); + } + } + } + } + + // Try the playable director + var director = uniRoot.GetComponent(); + if (director) + { + Debug.LogWarning(string.Format("Exporting animation from PlayableDirector on {0} not supported", uniRoot.name)); + // TODO: export animationclips from playabledirector + } + + // Try the animation (legacy) + var uniAnimation = uniRoot.GetComponent(); + if (uniAnimation) + { + // Only export each clip once per game object. + foreach ( var uniAnimObj in uniAnimation ) + { + var uniAnimState = uniAnimObj as AnimationState; + if (uniAnimState) + { + var uniAnimClip = uniAnimState.clip; + if (exportedClips.Add(uniAnimClip)) + { + ExportAnimationClip(uniAnimClip, uniRoot, fbxScene); + } + } + } + } + } + + /// + /// configures default camera for the scene + /// + private void SetDefaultCamera(FbxScene fbxScene) + { + if (fbxScene == null) { return; } + + if (string.IsNullOrEmpty(DefaultCamera)) + DefaultCamera = Globals.FBXSDK_CAMERA_PERSPECTIVE; + + fbxScene.GetGlobalSettings().SetDefaultCamera(DefaultCamera); + } + + /// + /// Ensures that the inputted name is unique. + /// If a duplicate name is found, then it is incremented. + /// e.g. Sphere becomes Sphere_1 + /// + /// Unique name + /// Name + /// The dictionary to use to map name to # of occurences + private string GetUniqueName(string name, Dictionary nameToCountMap) + { + var uniqueName = name; + int count; + if (nameToCountMap.TryGetValue(name, out count)) + { + uniqueName = string.Format(UniqueNameFormat, name, count); + } + else + { + count = 0; + } + nameToCountMap[name] = count + 1; + return uniqueName; + } + + /// + /// Ensures that the inputted name is unique. + /// If a duplicate name is found, then it is incremented. + /// e.g. Sphere becomes Sphere_1 + /// + /// Unique name + /// Name + private string GetUniqueFbxNodeName(string name) + { + return GetUniqueName(name, NameToIndexMap); + } + + /// + /// Ensures that the inputted material name is unique. + /// If a duplicate name is found, then it is incremented. + /// e.g. mat becomes mat_1 + /// + /// Name + /// Unique material name + private string GetUniqueMaterialName(string name) + { + return GetUniqueName(name, MaterialNameToIndexMap); + } + + /// + /// Ensures that the inputted texture name is unique. + /// If a duplicate name is found, then it is incremented. + /// e.g. tex becomes tex_1 + /// + /// Name + /// Unique texture name + private string GetUniqueTextureName(string name) + { + return GetUniqueName(name, TextureNameToIndexMap); + } + + /// + /// Create a fbxNode from unityGo. + /// + /// + /// + /// the created FbxNode + private FbxNode CreateFbxNode(GameObject unityGo, FbxScene fbxScene) + { + var fbxName = unityGo.name; + if (ExportOptions.UseMayaCompatibleNames) + { + fbxName = ConvertToMayaCompatibleName(unityGo.name); + if (ExportOptions.AllowSceneModification) + { + Undo.RecordObject(unityGo, "rename " + fbxName); + unityGo.name = fbxName; + } + } + + var fbxNode = FbxNode.Create(fbxScene, GetUniqueFbxNodeName(fbxName)); + + // Default inheritance type in FBX is RrSs, which causes scaling issues in Maya as + // both Maya and Unity use RSrs inheritance by default. + // Note: MotionBuilder uses RrSs inheritance by default as well, though it is possible + // to select a different inheritance type in the UI. + // Use RSrs as the scaling inheritance instead. + fbxNode.SetTransformationInheritType(FbxTransform.EInheritType.eInheritRSrs); + + // Fbx rotation order is XYZ, but Unity rotation order is ZXY. + // Also, DeepConvert does not convert the rotation order (assumes XYZ), unless RotationActive is true. + fbxNode.SetRotationOrder(FbxNode.EPivotSet.eSourcePivot, FbxEuler.EOrder.eOrderZXY); + fbxNode.SetRotationActive(true); + + MapUnityObjectToFbxNode[unityGo] = fbxNode; + + return fbxNode; + } + + /// + /// Creates an FbxNode for each GameObject. + /// + /// The number of nodes exported. + internal int ExportTransformHierarchy( + GameObject unityGo, FbxScene fbxScene, FbxNode fbxNodeParent, + int exportProgress, int objectCount, Vector3 newCenter, + TransformExportType exportType = TransformExportType.Local, + LODExportType lodExportType = LODExportType.All + ) + { + var numObjectsExported = exportProgress; + + var fbxNode = CreateFbxNode(unityGo, fbxScene); + + if (Verbose) + Debug.Log(string.Format("exporting {0}", fbxNode.GetName())); + + numObjectsExported++; + if (EditorUtility.DisplayCancelableProgressBar( + ProgressBarTitle, + string.Format("Creating FbxNode {0}/{1}", numObjectsExported, objectCount), + (numObjectsExported / (float)objectCount) * 0.25f)) + { + // cancel silently + return -1; + } + + ExportTransform(unityGo.transform, fbxNode, newCenter, exportType); + + fbxNodeParent.AddChild(fbxNode); + + // if this object has an LOD group, then export according to the LOD preference setting + var lodGroup = unityGo.GetComponent(); + if (lodGroup && lodExportType != LODExportType.All) + { + var lods = lodGroup.GetLODs(); + + // LODs are ordered from highest to lowest. + // If exporting lowest LOD, reverse the array + if (lodExportType == LODExportType.Lowest) + { + // reverse the array + var tempLods = new LOD[lods.Length]; + System.Array.Copy(lods, tempLods, lods.Length); + System.Array.Reverse(tempLods); + lods = tempLods; + } + + for (var i = 0; i < lods.Length; i++) + { + var lod = lods[i]; + var exportedRenderer = false; + foreach (var renderer in lod.renderers) + { + // only export if parented under LOD group + if (renderer.transform.parent == unityGo.transform) + { + numObjectsExported = ExportTransformHierarchy(renderer.gameObject, fbxScene, fbxNode, numObjectsExported, objectCount, newCenter, lodExportType: lodExportType); + exportedRenderer = true; + } + else if (Verbose) + { + Debug.LogFormat("FbxExporter: Not exporting LOD {0}: {1}", i, renderer.name); + } + } + + // if at least one renderer for this LOD was exported, then we succeeded + // so stop exporting. + if (exportedRenderer) + { + return numObjectsExported; + } + } + } + + // now unityGo through our children and recurse + foreach (Transform childT in unityGo.transform) + { + numObjectsExported = ExportTransformHierarchy(childT.gameObject, fbxScene, fbxNode, numObjectsExported, objectCount, newCenter, lodExportType: lodExportType); + } + + return numObjectsExported; + } + + /// + /// Exports all animation clips in the hierarchy along with + /// the minimum required GameObject information. + /// i.e. Animated GameObjects, their ancestors, and their transforms are exported, + /// but components are only exported if explicitly animated. Meshes are not exported. + /// + /// The number of nodes exported. + internal int ExportAnimationOnly( + GameObject unityGO, + FbxScene fbxScene, + int exportProgress, + int objectCount, + Vector3 newCenter, + IExportData data, + TransformExportType exportType = TransformExportType.Local + ) + { + var exportData = (AnimationOnlyExportData)data; + var numObjectsExported = exportProgress; + + // make sure anim destination node is exported as well + var exportSet = exportData.Objects; + if (ExportOptions.AnimationDest && ExportOptions.AnimationSource) + { + exportSet.Add(ExportOptions.AnimationDest.gameObject); + } + + // first export all the animated bones that are in the export set + // as only a subset of bones are exported, but we still need to make sure the bone transforms are correct + if (!ExportAnimatedBones(unityGO, fbxScene, ref numObjectsExported, objectCount, exportData)) + { + // export cancelled + return -1; + } + + // export everything else and make sure all nodes are connected + foreach (var go in exportSet) + { + FbxNode node; + if (!ExportGameObjectAndParents( + go, unityGO, fbxScene, out node, newCenter, exportType, ref numObjectsExported, objectCount + )) + { + // export cancelled + return -1; + } + + ExportConstraints(go, fbxScene, node); + + System.Type compType; + if (exportData.exportComponent.TryGetValue(go, out compType)) + { + if (compType == typeof(Light)) + { + ExportLight(go, fbxScene, node); + } + else if (compType == typeof(Camera)) + { + ExportCamera(go, fbxScene, node); + } + else if (compType == typeof(SkinnedMeshRenderer)) + { + // export only what is necessary for exporting blendshape animation + var unitySkin = go.GetComponent(); + var meshInfo = new MeshInfo(unitySkin.sharedMesh, unitySkin.sharedMaterials); + ExportMesh(meshInfo, node); + } + } + } + + return numObjectsExported; + } + + internal class SkinnedMeshBoneInfo + { + public SkinnedMeshRenderer skinnedMesh; + public Dictionary boneDict; + public Dictionary boneToBindPose; + + public SkinnedMeshBoneInfo(SkinnedMeshRenderer skinnedMesh, Dictionary boneDict) + { + this.skinnedMesh = skinnedMesh; + this.boneDict = boneDict; + this.boneToBindPose = new Dictionary(); + } + } + + private bool ExportAnimatedBones( + GameObject unityGo, + FbxScene fbxScene, + ref int exportProgress, + int objectCount, + AnimationOnlyExportData exportData + ) + { + var skinnedMeshRenderers = unityGo.GetComponentsInChildren(); + foreach (var skinnedMesh in skinnedMeshRenderers) + { + var boneArray = skinnedMesh.bones; + var bones = new HashSet(); + var boneDict = new Dictionary(); + + for (var i = 0; i < boneArray.Length; i++) + { + bones.Add(boneArray[i].gameObject); + boneDict.Add(boneArray[i], i); + } + + // get the bones that are also in the export set + bones.IntersectWith(exportData.Objects); + + var boneInfo = new SkinnedMeshBoneInfo(skinnedMesh, boneDict); + foreach (var bone in bones) + { + FbxNode fbxNode; + // bone already exported + if (MapUnityObjectToFbxNode.TryGetValue(bone, out fbxNode)) + { + continue; + } + fbxNode = CreateFbxNode(bone, fbxScene); + + exportProgress++; + if (EditorUtility.DisplayCancelableProgressBar( + ProgressBarTitle, + string.Format("Creating FbxNode {0}/{1}", exportProgress, objectCount), + (exportProgress / (float)objectCount) * 0.5f)) + { + // cancel silently + return false; + } + ExportBoneTransform(fbxNode, fbxScene, bone.transform, boneInfo); + } + } + return true; + } + + /// + /// Exports the Gameobject and its ancestors. + /// + /// true, if game object and parents were exported, + /// false if export cancelled. + private bool ExportGameObjectAndParents( + GameObject unityGo, + GameObject rootObject, + FbxScene fbxScene, + out FbxNode fbxNode, + Vector3 newCenter, + TransformExportType exportType, + ref int exportProgress, + int objectCount + ) + { + // node doesn't exist so create it + if (!MapUnityObjectToFbxNode.TryGetValue(unityGo, out fbxNode)) + { + fbxNode = CreateFbxNode(unityGo, fbxScene); + + exportProgress++; + if (EditorUtility.DisplayCancelableProgressBar( + ProgressBarTitle, + string.Format("Creating FbxNode {0}/{1}", exportProgress, objectCount), + (exportProgress / (float)objectCount) * 0.5f)) + { + // cancel silently + return false; + } + + ExportTransform(unityGo.transform, fbxNode, newCenter, exportType); + } + + if (unityGo == rootObject || unityGo.transform.parent == null) + { + fbxScene.GetRootNode().AddChild(fbxNode); + return true; + } + + // make sure all the nodes are connected and exported + FbxNode fbxNodeParent; + if (!ExportGameObjectAndParents( + unityGo.transform.parent.gameObject, + rootObject, + fbxScene, + out fbxNodeParent, + newCenter, + TransformExportType.Local, + ref exportProgress, + objectCount + )) + { + // export cancelled + return false; + } + fbxNodeParent.AddChild(fbxNode); + + return true; + } + + /// + /// Exports the bone transform. + /// + /// true, if bone transform was exported, false otherwise. + /// Fbx node. + /// Fbx scene. + /// Unity bone. + /// Bone info. + private bool ExportBoneTransform( + FbxNode fbxNode, FbxScene fbxScene, Transform unityBone, SkinnedMeshBoneInfo boneInfo + ) + { + if (boneInfo == null || boneInfo.skinnedMesh == null || boneInfo.boneDict == null || unityBone == null) + { + return false; + } + + var skinnedMesh = boneInfo.skinnedMesh; + var boneDict = boneInfo.boneDict; + var rootBone = skinnedMesh.rootBone; + + // setup the skeleton + var fbxSkeleton = fbxNode.GetSkeleton(); + if (fbxSkeleton == null) + { + fbxSkeleton = FbxSkeleton.Create(fbxScene, unityBone.name + SkeletonPrefix); + + fbxSkeleton.Size.Set(1.0f * UnitScaleFactor); + fbxNode.SetNodeAttribute(fbxSkeleton); + } + var fbxSkeletonType = FbxSkeleton.EType.eLimbNode; + + // Only set the rootbone's skeleton type to FbxSkeleton.EType.eRoot + // if it has at least one child that is also a bone. + // Otherwise if it is marked as Root but has no bones underneath, + // Maya will import it as a Null object instead of a bone. + if (rootBone == unityBone && rootBone.childCount > 0) + { + var hasChildBone = false; + foreach (Transform child in unityBone) + { + if (boneDict.ContainsKey(child)) + { + hasChildBone = true; + break; + } + } + if (hasChildBone) + { + fbxSkeletonType = FbxSkeleton.EType.eRoot; + } + } + fbxSkeleton.SetSkeletonType(fbxSkeletonType); + + var bindPoses = skinnedMesh.sharedMesh.bindposes; + + // get bind pose + var bindPose = GetBindPose(unityBone, bindPoses, ref boneInfo); + + Matrix4x4 pose; + // get parent's bind pose + var parentBindPose = GetBindPose(unityBone.parent, bindPoses, ref boneInfo); + pose = parentBindPose * bindPose.inverse; + + FbxVector4 translation, rotation, scale; + GetTRSFromMatrix(pose, out translation, out rotation, out scale); + + // Export bones with zero rotation, using a pivot instead to set the rotation + // so that the bones are easier to animate and the rotation shows up as the "joint orientation" in Maya. + fbxNode.LclTranslation.Set(new FbxDouble3(translation.X * UnitScaleFactor, translation.Y * UnitScaleFactor, translation.Z * UnitScaleFactor)); + fbxNode.LclRotation.Set(new FbxDouble3(0, 0, 0)); + fbxNode.LclScaling.Set(new FbxDouble3(scale.X, scale.Y, scale.Z)); + + // TODO (UNI-34294): add detailed comment about why we export rotation as pre-rotation + fbxNode.SetRotationActive(true); + fbxNode.SetPivotState(FbxNode.EPivotSet.eSourcePivot, FbxNode.EPivotState.ePivotReference); + fbxNode.SetPreRotation(FbxNode.EPivotSet.eSourcePivot, new FbxVector4(rotation.X, rotation.Y, rotation.Z)); + + return true; + } + + private void GetTRSFromMatrix(Matrix4x4 unityMatrix, out FbxVector4 translation, out FbxVector4 rotation, out FbxVector4 scale) + { + // FBX is transposed relative to Unity: transpose as we convert. + var matrix = new FbxMatrix(); + matrix.SetColumn(0, new FbxVector4(unityMatrix.GetRow(0).x, unityMatrix.GetRow(0).y, unityMatrix.GetRow(0).z, unityMatrix.GetRow(0).w)); + matrix.SetColumn(1, new FbxVector4(unityMatrix.GetRow(1).x, unityMatrix.GetRow(1).y, unityMatrix.GetRow(1).z, unityMatrix.GetRow(1).w)); + matrix.SetColumn(2, new FbxVector4(unityMatrix.GetRow(2).x, unityMatrix.GetRow(2).y, unityMatrix.GetRow(2).z, unityMatrix.GetRow(2).w)); + matrix.SetColumn(3, new FbxVector4(unityMatrix.GetRow(3).x, unityMatrix.GetRow(3).y, unityMatrix.GetRow(3).z, unityMatrix.GetRow(3).w)); + + // FBX wants translation, rotation (in euler angles) and scale. + // We assume there's no real shear, just rounding error. + FbxVector4 shear; + double sign; + matrix.GetElements(out translation, out rotation, out shear, out scale, out sign); + } + + /// + /// Counts how many objects are between this object and the root (exclusive). + /// + /// The object to root count. + /// Start object. + /// Root object. + private static int GetObjectToRootDepth(Transform startObject, Transform root) + { + if (startObject == null) + { + return 0; + } + + var count = 0; + var parent = startObject.parent; + while (parent != null && parent != root) + { + count++; + parent = parent.parent; + } + return count; + } + + /// + /// Gets the count of animated objects to be exported. + /// + /// In addition, collects the minimum set of what needs to be exported for each GameObject hierarchy. + /// This contains all the animated GameObjects, their ancestors, their transforms, as well as any animated + /// components and the animation clips. Also, the first animation to export, if any. + /// + /// The animation only hierarchy count. + /// Map from GameObject hierarchy to animation export data. + internal int GetAnimOnlyHierarchyCount(Dictionary hierarchyToExportData) + { + // including any parents of animated objects that are exported + var completeExpSet = new HashSet(); + foreach (var data in hierarchyToExportData.Values) + { + if (data == null || data.Objects == null || data.Objects.Count == 0) + { + continue; + } + foreach (var go in data.Objects) + { + completeExpSet.Add(go); + + var parent = go.transform.parent; + while (parent != null && completeExpSet.Add(parent.gameObject)) + { + parent = parent.parent; + } + } + } + + return completeExpSet.Count; + } + + internal static Dictionary GetExportData(TimelineClip timelineClip, PlayableDirector director = null, IExportOptions exportOptions = null) + { + if (timelineClip == null) + { + return null; + } + + if (exportOptions == null) + exportOptions = DefaultOptions; + Debug.Assert(exportOptions != null); + + // only support anim only export for timeline clips + if (exportOptions.ModelAnimIncludeOption != Include.Anim) + { + Debug.LogWarning("Timeline clips must be exported with Anim only include option"); + return null; + } + + var exportData = new Dictionary(); + var pair = AnimationOnlyExportData.GetGameObjectAndAnimationClip(timelineClip, director); + var boundGo = pair.Key; + if (boundGo == null) + { + return null; + } + exportData[boundGo] = GetExportData(boundGo, pair.Value, exportOptions); + + return exportData; + } + + internal static Dictionary GetExportData( + Object[] objects, IExportOptions exportOptions = null, AnimationCover targetAnimation = null ) + { + if (exportOptions == null) + exportOptions = DefaultOptions; + Debug.Assert(exportOptions != null); + + if (exportOptions.ModelAnimIncludeOption == Include.Model) + { + return null; + } + + var exportData = new Dictionary(); + foreach (var obj in objects) + { + var go = ModelExporter.GetGameObject(obj); + if (go) + { + exportData[go] = GetExportData( go, exportOptions, targetAnimation ); + } + } + + return exportData.Count == 0 ? null : exportData; + } + + internal static IExportData GetExportData(GameObject rootObject, AnimationClip animationClip, IExportOptions exportOptions = null) + { + if (rootObject == null || animationClip == null) + { + return null; + } + + if (exportOptions == null) + exportOptions = DefaultOptions; + Debug.Assert(exportOptions != null); + + var exportData = new AnimationOnlyExportData(); + exportData.CollectDependencies(animationClip, rootObject, exportOptions); + + // could not find any dependencies, return null + if (exportData.Objects.Count == 0) + { + return null; + } + return exportData; + } + + internal static IExportData GetExportData( GameObject go, IExportOptions exportOptions = null, AnimationCover targetAnimation = null ) + { + if (exportOptions == null) + exportOptions = DefaultOptions; + Debug.Assert(exportOptions != null); + + // gather all animation clips + var legacyAnim = go.GetComponentsInChildren(); + var genericAnim = go.GetComponentsInChildren(); + + var exportData = new AnimationOnlyExportData(); + var depthFromRootAnimation = int.MaxValue; + Animation rootAnimation = null; + + if ( targetAnimation == null ) + { + foreach (var anim in legacyAnim) + { + var count = GetObjectToRootDepth(anim.transform, go.transform); + + if (count < depthFromRootAnimation) + { + depthFromRootAnimation = count; + rootAnimation = anim; + } + + var animClips = AnimationUtility.GetAnimationClips(anim.gameObject); + exportData.CollectDependencies(animClips, anim.gameObject, exportOptions); + } + } + else + { + exportData.CollectDependencies( targetAnimation.Clips, targetAnimation.Animator.gameObject, exportOptions ); + // exportData.defaultClip = targetAnimation.Animator.clip; + exportData.defaultClip = targetAnimation.FirstClip; + } + + var depthFromRootAnimator = int.MaxValue; + Animator rootAnimator = null; + foreach (var anim in genericAnim) + { + var count = GetObjectToRootDepth(anim.transform, go.transform); + + if (count < depthFromRootAnimator) + { + depthFromRootAnimator = count; + rootAnimator = anim; + } + + // Try the animator controller (mecanim) + var controller = anim.runtimeAnimatorController; + if (controller) + { + exportData.CollectDependencies(controller.animationClips, anim.gameObject, exportOptions); + } + } + + // set the first clip to export + if (depthFromRootAnimation < depthFromRootAnimator && rootAnimation) + { + exportData.defaultClip = rootAnimation.clip; + } + else if (rootAnimator) + { + // Try the animator controller (mecanim) + var controller = rootAnimator.runtimeAnimatorController; + if (controller) + { + var dController = controller as UnityEditor.Animations.AnimatorController; + var controllerLayers = dController != null ? dController.layers : null; + if (controllerLayers != null && controllerLayers.Length > 0) + { + var motion = controllerLayers[0].stateMachine.defaultState.motion; + var defaultClip = motion as AnimationClip; + if (defaultClip) + { + exportData.defaultClip = defaultClip; + } + else + { + if (motion != null) + { + Debug.LogWarningFormat("Couldn't export motion {0}", motion.name); + } + // missing animation + else + { + Debug.LogWarningFormat("Couldn't export motion. Motion is missing."); + } + } + } + } + } + return exportData; + } + + /// + /// Export components on this game object. + /// Transform components have already been exported. + /// This function exports the other components and animation. + /// + private bool ExportComponents(FbxScene fbxScene) + { + var numObjectsExported = 0; + var objectCount = MapUnityObjectToFbxNode.Count; + foreach (var entry in MapUnityObjectToFbxNode) + { + numObjectsExported++; + if (EditorUtility.DisplayCancelableProgressBar( + ProgressBarTitle, + string.Format("Exporting Components for GameObject {0}/{1}", numObjectsExported, objectCount), + ((numObjectsExported / (float)objectCount) * 0.25f) + 0.25f)) + { + // cancel silently + return false; + } + + var unityGo = entry.Key; + var fbxNode = entry.Value; + + // try export mesh + var exportedMesh = false; + if (ExportOptions.KeepInstances) + { + exportedMesh = ExportInstance(unityGo, fbxScene, fbxNode); + } + + if (!exportedMesh) + { + exportedMesh = ExportMesh(unityGo, fbxNode); + } + + // export camera, but only if no mesh was exported + var exportedCamera = false; + if (!exportedMesh) + { + exportedCamera = ExportCamera(unityGo, fbxScene, fbxNode); + } + + // export light, but only if no mesh or camera was exported + if (!exportedMesh && !exportedCamera) + { + ExportLight(unityGo, fbxScene, fbxNode); + } + + ExportConstraints(unityGo, fbxScene, fbxNode); + } + return true; + } + + /// + /// Checks if the GameObject has animation. + /// + /// true, if object has animation, false otherwise. + /// Go. + private bool GameObjectHasAnimation(GameObject go) + { + return go != null && + (go.GetComponent() || + go.GetComponent() || + go.GetComponent()); + } + + /// + /// A count of how many GameObjects we are exporting, to have a rough + /// idea of how long creating the scene will take. + /// + /// The hierarchy count. + /// Export set. + internal int GetHierarchyCount(HashSet exportSet) + { + var count = 0; + var queue = new Queue(exportSet); + while (queue.Count > 0) + { + var obj = queue.Dequeue(); + var objTransform = obj.transform; + foreach (Transform child in objTransform) + { + queue.Enqueue(child.gameObject); + } + count++; + } + return count; + } + + /// + /// Removes objects that will already be exported anyway. + /// E.g. if a parent and its child are both selected, then the child + /// will be removed from the export set. + /// + /// The revised export set + /// Unity export set. + internal static HashSet RemoveRedundantObjects(IEnumerable unityExportSet) + { + // basically just remove the descendents from the unity export set + var toExport = new HashSet(); + var hashedExportSet = new HashSet(unityExportSet); + + foreach (var obj in unityExportSet) + { + var unityGo = GetGameObject(obj); + + if (unityGo) + { + // if any of this nodes ancestors is already in the export set, + // then ignore it, it will get exported already + var parentInSet = false; + var parent = unityGo.transform.parent; + while (parent != null) + { + if (hashedExportSet.Contains(parent.gameObject)) + { + parentInSet = true; + break; + } + parent = parent.parent; + } + + if (!parentInSet) + { + toExport.Add(unityGo); + } + } + } + return toExport; + } + + /// + /// Recursively go through the hierarchy, unioning the bounding box centers + /// of all the children, to find the combined bounds. + /// + /// Transform. + /// The Bounds that is the Union of all the bounds on this transform's hierarchy. + private static void EncapsulateBounds(Transform t, ref Bounds boundsUnion) + { + var bounds = GetBounds(t); + boundsUnion.Encapsulate(bounds); + + foreach (Transform child in t) + { + EncapsulateBounds(child, ref boundsUnion); + } + } + + /// + /// Gets the bounds of a transform. + /// Looks first at the Renderer, then Mesh, then Collider. + /// Default to a bounds with center transform.position and size zero. + /// + /// The bounds. + /// Transform. + private static Bounds GetBounds(Transform t) + { + if (t.TryGetComponent(out var renderer)) + { + return renderer.bounds; + } + + if (t.TryGetComponent(out var meshFilter)) + { + return meshFilter.mesh.bounds; + } + + if (t.TryGetComponent(out var collider)) + { + return collider.bounds; + } + return new Bounds(t.position, Vector3.zero); + } + + /// + /// Finds the center of a group of GameObjects. + /// + /// Center of gameObjects. + /// Game objects. + internal static Vector3 FindCenter(IEnumerable gameObjects) + { + var bounds = new Bounds(); + // Assign the initial bounds to first GameObject's bounds + // (if we initialize the bounds to 0, then 0 will be part of the bounds) + foreach (var go in gameObjects) + { + var tempBounds = GetBounds(go.transform); + bounds = new Bounds(tempBounds.center, tempBounds.size); + break; + } + foreach (var go in gameObjects) + { + EncapsulateBounds(go.transform, ref bounds); + } + return bounds.center; + } + + /// + /// Gets the recentered translation. + /// + /// The recentered translation. + /// Transform. + /// Center point. + internal static Vector3 GetRecenteredTranslation(Transform t, Vector3 center) + { + return t.position - center; + } + + internal enum TransformExportType { Local, Global, Reset }; + + /// + /// Export all the objects in the set. + /// Return the number of objects in the set that we exported. + /// + /// This refreshes the asset database. + /// + internal int ExportAll( + IEnumerable unityExportSet, + Dictionary exportData) + { + exportCancelled = false; + + m_lastFilePath = LastFilePath; + + // Export first to a temporary file + // in case the export is cancelled. + // This way we won't overwrite existing files. + try + { + // create a temp file in the same directory where the fbx will be exported + var exportDir = Path.GetDirectoryName(m_lastFilePath); + var lastFileName = Path.GetFileName(m_lastFilePath); + var tempFileName = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + "_" + lastFileName; + m_tempFilePath = Path.Combine(new string[] { exportDir, tempFileName }); + } + catch (IOException) + { + return 0; + } + + if (string.IsNullOrEmpty(m_tempFilePath)) + { + return 0; + } + + try + { + var animOnly = exportData != null && ExportOptions.ModelAnimIncludeOption == Include.Anim; + var status = false; + // Create the FBX manager + using (var fbxManager = FbxManager.Create()) + { + // Configure fbx IO settings. + var settings = FbxIOSettings.Create(fbxManager, Globals.IOSROOT); + if (ExportOptions.EmbedTextures) + settings.SetBoolProp(Globals.EXP_FBX_EMBEDDED, true); + fbxManager.SetIOSettings(settings); + + // Create the exporter + var fbxExporter = FbxExporter.Create(fbxManager, "Exporter"); + + // Initialize the exporter. + // fileFormat must be binary if we are embedding textures + var fileFormat = -1; + if (ExportOptions.ExportFormat == ExportFormat.ASCII) + { + fileFormat = fbxManager.GetIOPluginRegistry().FindWriterIDByDescription("FBX ascii (*.fbx)"); + } + + status = fbxExporter.Initialize(m_tempFilePath, fileFormat, fbxManager.GetIOSettings()); + // Check that initialization of the fbxExporter was successful + if (!status) + return 0; + + // Set the progress callback. + fbxExporter.SetProgressCallback(ExportProgressCallback); + + // Create a scene + var fbxScene = FbxScene.Create(fbxManager, "Scene"); + + // set up the scene info + var fbxSceneInfo = FbxDocumentInfo.Create(fbxManager, "SceneInfo"); + fbxSceneInfo.mTitle = Title; + fbxSceneInfo.mSubject = Subject; + fbxSceneInfo.mAuthor = "Unity Technologies"; + fbxSceneInfo.mRevision = "1.0"; + fbxSceneInfo.mKeywords = Keywords; + fbxSceneInfo.mComment = Comments; + fbxSceneInfo.Original_ApplicationName.Set(string.Format("Unity {0}", PACKAGE_UI_NAME)); + // set last saved to be the same as original, as this is a new file. + fbxSceneInfo.LastSaved_ApplicationName.Set(fbxSceneInfo.Original_ApplicationName.Get()); + + var version = GetVersionFromReadme(); + if (version != null) + { + fbxSceneInfo.Original_ApplicationVersion.Set(version); + fbxSceneInfo.LastSaved_ApplicationVersion.Set(fbxSceneInfo.Original_ApplicationVersion.Get()); + } + fbxScene.SetSceneInfo(fbxSceneInfo); + + // Set up the axes (Y up, Z forward, X to the right) and units (centimeters) + // Exporting in centimeters as this is the default unit for FBX files, and easiest + // to work with when importing into Maya or Max + var fbxSettings = fbxScene.GetGlobalSettings(); + fbxSettings.SetSystemUnit(FbxSystemUnit.cm); + + // The Unity axis system has Y up, Z forward, X to the right (left handed system with odd parity). + // DirectX has the same axis system, so use this constant. + var unityAxisSystem = FbxAxisSystem.DirectX; + fbxSettings.SetAxisSystem(unityAxisSystem); + + // export set of object + var fbxRootNode = fbxScene.GetRootNode(); + // stores how many objects we have exported, -1 if export was cancelled + var exportProgress = 0; + IEnumerable revisedExportSet = null; + + // Total # of objects to be exported + // Used by progress bar to show how many objects will be exported in total + // i.e. exporting x/count... + var count = 0; + + // number of object hierarchies being exported. + // Used to figure out exported transforms for root objects. + // i.e. if we are exporting a single hierarchy at local position, then it's root is set to zero, + // but if we are exporting multiple hierarchies at local position, then each hierarchy will be recentered according + // to the center of the bounding box. + var rootObjCount = 0; + + if (animOnly) + { + count = GetAnimOnlyHierarchyCount(exportData); + revisedExportSet = from entry in exportData select entry.Key; + rootObjCount = exportData.Keys.Count; + } + else + { + var revisedGOSet = RemoveRedundantObjects(unityExportSet); + count = GetHierarchyCount(revisedGOSet); + rootObjCount = revisedGOSet.Count; + revisedExportSet = revisedGOSet; + } + + if (count <= 0) + { + // nothing to export + Debug.LogWarning("Nothing to Export"); + return 0; + } + + var center = Vector3.zero; + var transformExportType = TransformExportType.Global; + switch (ExportOptions.ObjectPosition) + { + case ObjectPosition.LocalCentered: + // one object to export -> move to (0,0,0) + if (rootObjCount == 1) + { + var tempList = new List(revisedExportSet); + center = tempList[0].transform.position; + break; + } + // more than one object to export -> get bounding center + center = FindCenter(revisedExportSet); + break; + case ObjectPosition.Reset: + transformExportType = TransformExportType.Reset; + break; + // absolute center -> don't do anything + default: + center = Vector3.zero; + break; + } + + foreach (var unityGo in revisedExportSet) + { + IExportData data; + if (animOnly && exportData.TryGetValue(unityGo, out data)) + { + exportProgress = this.ExportAnimationOnly(unityGo, fbxScene, exportProgress, count, center, data, transformExportType); + } + else + { + exportProgress = this.ExportTransformHierarchy(unityGo, fbxScene, fbxRootNode, + exportProgress, count, center, transformExportType, ExportOptions.LODExportType); + } + if (exportCancelled || exportProgress < 0) + { + Debug.LogWarning("Export Cancelled"); + return 0; + } + } + + if (!animOnly) + { + if (!ExportComponents(fbxScene)) + { + Debug.LogWarning("Export Cancelled"); + return 0; + } + } + + // Export animation if any + if (exportData != null) + { + foreach (var unityGo in revisedExportSet) + { + IExportData iData; + if (!exportData.TryGetValue(unityGo, out iData)) + { + continue; + } + var data = iData as AnimationOnlyExportData; + if (data == null) + { + Debug.LogWarningFormat("FBX Exporter: no animation export data found for {0}", unityGo.name); + continue; + } + // export animation + // export default clip first + if (data.defaultClip != null) + { + var defaultClip = data.defaultClip; + ExportAnimationClip(defaultClip, data.animationClips[defaultClip], fbxScene); + data.animationClips.Remove(defaultClip); + } + + foreach (var animClip in data.animationClips) + { + ExportAnimationClip(animClip.Key, animClip.Value, fbxScene); + } + } + } + // Set the scene's default camera. + SetDefaultCamera(fbxScene); + + // if ( ExportSettings.ExportModelSettings.info.ConvertCordinateSpace ) + if ( ExportOptions.ConvertCordinateSpace ) + { + // The Maya axis system has Y up, Z forward, X to the left (right handed system with odd parity). + // We need to export right-handed for Maya because ConvertScene (used by Maya and Max importers) can't switch handedness: + // https://forums.autodesk.com/t5/fbx-forum/get-confused-with-fbxaxissystem-convertscene/td-p/4265472 + // This needs to be done last so that everything is converted properly. + FbxAxisSystem.MayaYUp.DeepConvertScene( fbxScene ); + Log.Info( "Converting scene to MayaY." ); + } + else + { + Log.Info( "Scene is not being converted." ); + } + + // Export the scene to the file. + status = fbxExporter.Export(fbxScene); + + // cleanup + fbxScene.Destroy(); + fbxExporter.Destroy(); + } + + if (exportCancelled) + { + Debug.LogWarning("Export Cancelled"); + return 0; + } + + // make a temporary copy of the original metafile + var originalMetafilePath = ""; + if (ExportOptions.PreserveImportSettings && File.Exists(m_lastFilePath)) + { + originalMetafilePath = SaveMetafile(); + } + + // delete old file, move temp file + ReplaceFile(); + + // refresh the database so Unity knows the file's been deleted + AssetDatabase.Refresh(); + + // replace with original metafile if specified to + if (ExportOptions.PreserveImportSettings && !string.IsNullOrEmpty(originalMetafilePath)) + { + ReplaceMetafile(originalMetafilePath); + } + + return status == true ? NumNodes : 0; + } + finally + { + // You must clear the progress bar when you're done, + // otherwise it never goes away and many actions in Unity + // are blocked (e.g. you can't quit). + EditorUtility.ClearProgressBar(); + + // make sure the temp file is deleted, no matter + // when we return + DeleteTempFile(); + } + } + + static bool exportCancelled = false; + + static bool ExportProgressCallback(float percentage, string status) + { + // Convert from percentage to [0,1]. + // Then convert from that to [0.5,1] because the first half of + // the progress bar was for creating the scene. + var progress01 = 0.5f * (1f + (percentage / 100.0f)); + + var cancel = EditorUtility.DisplayCancelableProgressBar(ProgressBarTitle, "Exporting Scene...", progress01); + + if (cancel) + { + exportCancelled = true; + } + + // Unity says "true" for "cancel"; FBX wants "true" for "continue" + return !cancel; + } + + /// + /// Deletes the file that got created while exporting. + /// + private void DeleteTempFile() + { + if (!File.Exists(m_tempFilePath)) + { + return; + } + + try + { + File.Delete(m_tempFilePath); + } + catch (IOException) + { + } + + if (File.Exists(m_tempFilePath)) + { + Debug.LogWarning("Failed to delete file: " + m_tempFilePath); + } + } + + /// + /// Replaces the file we are overwriting with + /// the temp file that was exported to. + /// + private void ReplaceFile() + { + if (m_tempFilePath.Equals(m_lastFilePath) || !File.Exists(m_tempFilePath)) + { + return; + } + // delete old file + try + { + File.Delete(m_lastFilePath); + // delete meta file also + File.Delete(m_lastFilePath + ".meta"); + } + catch (IOException) + { + } + + if (File.Exists(m_lastFilePath)) + { + Debug.LogWarning("Failed to delete file: " + m_lastFilePath); + } + + // rename the new file + try + { + File.Move(m_tempFilePath, m_lastFilePath); + } + catch (IOException) + { + Debug.LogWarning(string.Format("Failed to move file {0} to {1}", m_tempFilePath, m_lastFilePath)); + } + } + + private string SaveMetafile() + { + var tempMetafilePath = Path.GetTempFileName(); + + // get relative path + var fbxPath = "Assets/" + ExportSettings.ConvertToAssetRelativePath(m_lastFilePath); + if (AssetDatabase.LoadAssetAtPath(fbxPath, typeof(Object)) == null) + { + Debug.LogWarning(string.Format("Failed to find a valid asset at {0}. Import settings will be reset to default values.", m_lastFilePath)); + return ""; + } + + // get metafile for original fbx file + var metafile = fbxPath + ".meta"; + +#if UNITY_2019_1_OR_NEWER + metafile = Provider.GetAssetByPath(fbxPath).metaPath; +#endif + + // save it to a temp file + try + { + File.Copy(metafile, tempMetafilePath, true); + } + catch (IOException) + { + Debug.LogWarning(string.Format("Failed to copy file {0} to {1}. Import settings will be reset to default values.", metafile, tempMetafilePath)); + return ""; + } + + return tempMetafilePath; + } + + private void ReplaceMetafile(string metafilePath) + { + // get relative path + var fbxPath = "Assets/" + ExportSettings.ConvertToAssetRelativePath(m_lastFilePath); + if (AssetDatabase.LoadAssetAtPath(fbxPath, typeof(Object)) == null) + { + Debug.LogWarning(string.Format("Failed to find a valid asset at {0}. Import settings will be reset to default values.", m_lastFilePath)); + return; + } + + // get metafile for new fbx file + var metafile = fbxPath + ".meta"; + +#if UNITY_2019_1_OR_NEWER + metafile = Provider.GetAssetByPath(fbxPath).metaPath; +#endif + + // replace metafile with original one in temp file + try + { + File.Copy(metafilePath, metafile, true); + } + catch (IOException) + { + Debug.LogWarning(string.Format("Failed to copy file {0} to {1}. Import settings will be reset to default values.", metafilePath, m_lastFilePath)); + } + } + + internal static void ExportSingleTimelineClip(TimelineClip timelineClip, PlayableDirector director = null) + { + var filename = AnimationOnlyExportData.GetFileName(timelineClip); + if (ExportSettings.DisplayOptionsWindow) + { + ExportModelEditorWindow.Init(null, filename, timelineClip, director); + return; + } + + var folderPath = ExportSettings.FbxAbsoluteSavePath; + var filePath = System.IO.Path.Combine(folderPath, filename + ".fbx"); + + if (System.IO.File.Exists(filePath)) + { + Debug.LogErrorFormat("{0}: Failed to export to {1}, file already exists", PACKAGE_UI_NAME, filePath); + return; + } + + var previousInclude = ExportSettings.instance.ExportModelSettings.info.ModelAnimIncludeOption; + ExportSettings.instance.ExportModelSettings.info.SetModelAnimIncludeOption(Include.Anim); + + if (ExportTimelineClip(filePath, timelineClip, director, ExportSettings.instance.ExportModelSettings.info) != null) + { + // refresh the asset database so that the file appears in the + // asset folder view. + AssetDatabase.Refresh(); + } + + ExportSettings.instance.ExportModelSettings.info.SetModelAnimIncludeOption(previousInclude); + } + + /// + /// Add a menu item "Export Model..." to a GameObject's context menu. + /// + /// Command. + [MenuItem(MenuItemName, false, 30)] + internal static void OnContextItem(MenuCommand command) + { + if (Selection.objects.Length == 0) + { + DisplayNoSelectionDialog(); + return; + } + OnExport(); + } + + /// + /// Validate the menu item defined by the function OnContextItem. + /// + [MenuItem(MenuItemName, true, 30)] + internal static bool OnValidateMenuItem() + { + return true; + } + + internal static void DisplayNoSelectionDialog() + { + UnityEditor.EditorUtility.DisplayDialog( + string.Format("{0} Warning", PACKAGE_UI_NAME), + "No GameObjects selected for export.", + "Ok"); + } + + // + // export mesh info from Unity + // + /// + ///Information about the mesh that is important for exporting. + /// + internal class MeshInfo + { + public Mesh mesh; + + /// + /// Return true if there's a valid mesh information + /// + public bool IsValid { get { return mesh; } } + + /// + /// Gets the vertex count. + /// + /// The vertex count. + public int VertexCount { get { return Vertices.Length; } } + + /// + /// Gets the triangles. Each triangle is represented as 3 indices from the vertices array. + /// Ex: if triangles = [3,4,2], then we have one triangle with vertices vertices[3], vertices[4], and vertices[2] + /// + /// The triangles. + private int[] m_triangles; + public int[] Triangles + { + get + { + if (m_triangles == null) { m_triangles = mesh.triangles; } + return m_triangles; + } + } + + /// + /// Gets the vertices, represented in local coordinates. + /// + /// The vertices. + private Vector3[] m_vertices; + public Vector3[] Vertices + { + get + { + if (m_vertices == null) { m_vertices = mesh.vertices; } + return m_vertices; + } + } + + /// + /// Gets the normals for the vertices. + /// + /// The normals. + private Vector3[] m_normals; + public Vector3[] Normals + { + get + { + if (m_normals == null) + { + m_normals = mesh.normals; + } + return m_normals; + } + } + + /// + /// Gets the binormals for the vertices. + /// + /// The normals. + private Vector3[] m_Binormals; + + public Vector3[] Binormals + { + get + { + /// NOTE: LINQ + /// return mesh.normals.Zip (mesh.tangents, (first, second) + /// => Math.cross (normal, tangent.xyz) * tangent.w + if (m_Binormals == null || m_Binormals.Length == 0) + { + var normals = Normals; + var tangents = Tangents; + + if (HasValidNormals() && HasValidTangents()) + { + m_Binormals = new Vector3[normals.Length]; + + for (var i = 0; i < normals.Length; i++) + m_Binormals[i] = Vector3.Cross(normals[i], + tangents[i]) + * tangents[i].w; + } + } + return m_Binormals; + } + } + + /// + /// Gets the tangents for the vertices. + /// + /// The tangents. + private Vector4[] m_tangents; + public Vector4[] Tangents + { + get + { + if (m_tangents == null) + { + m_tangents = mesh.tangents; + } + return m_tangents; + } + } + + /// + /// Gets the vertex colors for the vertices. + /// + /// The vertex colors. + private Color32[] m_vertexColors; + public Color32[] VertexColors + { + get + { + if (m_vertexColors == null) + { + m_vertexColors = mesh.colors32; + } + return m_vertexColors; + } + } + + /// + /// Gets the uvs. + /// + /// The uv. + private Vector2[] m_UVs; + public Vector2[] UV + { + get + { + if (m_UVs == null) + { + m_UVs = mesh.uv; + } + return m_UVs; + } + } + + /// + /// The material(s) used. + /// Always at least one. + /// None are missing materials (we replace missing materials with the default material). + /// + public Material[] Materials { get; private set; } + + /// + /// Set up the MeshInfo with the given mesh and materials. + /// + public MeshInfo(Mesh mesh, Material[] materials) + { + this.mesh = mesh; + + this.m_Binormals = null; + this.m_vertices = null; + this.m_triangles = null; + this.m_normals = null; + this.m_UVs = null; + this.m_vertexColors = null; + this.m_tangents = null; + + if (materials == null) + { + this.Materials = new Material[] { DefaultMaterial }; + } + else + { + this.Materials = materials.Select(mat => mat ? mat : DefaultMaterial).ToArray(); + if (this.Materials.Length == 0) + { + this.Materials = new Material[] { DefaultMaterial }; + } + } + } + + public bool HasValidNormals() + { + return Normals != null && Normals.Length > 0; + } + + public bool HasValidBinormals() + { + return HasValidNormals() && + HasValidTangents() && + Binormals != null; + } + + public bool HasValidTangents() + { + return Tangents != null && Tangents.Length > 0; + } + + public bool HasValidVertexColors() + { + return VertexColors != null && VertexColors.Length > 0; + } + } + + /// + /// Get the GameObject + /// + internal static GameObject GetGameObject(Object obj) + { + if (obj is UnityEngine.Transform) + { + var xform = obj as UnityEngine.Transform; + return xform.gameObject; + } + else if (obj is UnityEngine.SkinnedMeshRenderer) + { + var skinnedMeshRenderer = obj as UnityEngine.SkinnedMeshRenderer; + return skinnedMeshRenderer.gameObject; + } + else if (obj is UnityEngine.GameObject) + { + return obj as UnityEngine.GameObject; + } + else if (obj is Behaviour) + { + var behaviour = obj as Behaviour; + return behaviour.gameObject; + } + + return null; + } + + /// + /// Map from type (must be a MonoBehaviour) to callback. + /// The type safety is lost; the caller must ensure it at run-time. + /// + static Dictionary MeshForComponentCallbacks + = new Dictionary(); + + /// + /// Register a callback to invoke if the object has a component of type T. + /// + /// This function is prefered over the other mesh callback + /// registration methods because it's type-safe, efficient, and + /// invocation order between types can be controlled in the UI by + /// reordering the components. + /// + /// It's an error to register a callback for a component that + /// already has one, unless 'replace' is set to true. + /// + internal static void RegisterMeshCallback(GetMeshForComponent callback, bool replace = false) + where T : UnityEngine.MonoBehaviour + { + // Under the hood we lose type safety, but don't let the user notice! + RegisterMeshCallback(typeof(T), + (ModelExporter exporter, MonoBehaviour component, FbxNode fbxNode) => + callback(exporter, (T)component, fbxNode), + replace); + } + + /// + /// Register a callback to invoke if the object has a component of type T. + /// + /// The callback will be invoked with an argument of type T, it's + /// safe to downcast. + /// + /// Normally you'll want to use the generic form, but this one is + /// easier to use with reflection. + /// + internal static void RegisterMeshCallback(System.Type t, + GetMeshForComponent callback, + bool replace = false) + { + if (!t.IsSubclassOf(typeof(MonoBehaviour))) + { + throw new ModelExportException("Registering a callback for a type that isn't derived from MonoBehaviour: " + t); + } + if (!replace && MeshForComponentCallbacks.ContainsKey(t)) + { + throw new ModelExportException("Replacing a callback for type " + t); + } + MeshForComponentCallbacks[t] = callback; + } + + /// + /// Forget the callback linked to a component of type T. + /// + internal static void UnRegisterMeshCallback() + { + MeshForComponentCallbacks.Remove(typeof(T)); + } + + /// + /// Forget the callback linked to a component of type T. + /// + internal static void UnRegisterMeshCallback(System.Type t) + { + MeshForComponentCallbacks.Remove(t); + } + + /// + /// Forget the callbacks linked to components. + /// + internal static void UnRegisterAllMeshCallbacks() + { + MeshForComponentCallbacks.Clear(); + } + + static List MeshForObjectCallbacks = new List(); + + /// + /// Register a callback to invoke on every GameObject we export. + /// + /// Avoid doing this if you can use a callback that depends on type. + /// + /// The GameObject-based callbacks are checked before the + /// component-based ones. + /// + /// Multiple GameObject-based callbacks can be registered; they are + /// checked in order of registration. + /// + internal static void RegisterMeshObjectCallback(GetMeshForObject callback) + { + MeshForObjectCallbacks.Add(callback); + } + + /// + /// Forget a GameObject-based callback. + /// + internal static void UnRegisterMeshObjectCallback(GetMeshForObject callback) + { + MeshForObjectCallbacks.Remove(callback); + } + + /// + /// Forget all GameObject-based callbacks. + /// + internal static void UnRegisterAllMeshObjectCallbacks() + { + MeshForObjectCallbacks.Clear(); + } + + /// + /// Exports a mesh for a unity gameObject. + /// + /// This goes through the callback system to find the right mesh and + /// allow plugins to substitute their own meshes. + /// + bool ExportMesh(GameObject gameObject, FbxNode fbxNode) + { + // First allow the object-based callbacks to have a hack at it. + foreach (var callback in MeshForObjectCallbacks) + { + if (callback(this, gameObject, fbxNode)) + { + return true; + } + } + + // Next iterate over components and allow the component-based + // callbacks to have a hack at it. This is complicated by the + // potential of subclassing. While we're iterating we keep the + // first MeshFilter or SkinnedMeshRenderer we find. + Component defaultComponent = null; + foreach (var component in gameObject.GetComponents()) + { + if (!component) + { + continue; + } + var monoBehaviour = component as MonoBehaviour; + if (!monoBehaviour) + { + // Check for default handling. But don't commit yet. + if (defaultComponent) + { + continue; + } + else if (component is MeshFilter) + { + defaultComponent = component; + } + else if (component is SkinnedMeshRenderer) + { + defaultComponent = component; + } + } + else + { + // Check if we have custom behaviour for this component type, or + // one of its base classes. + if (!monoBehaviour.enabled) + { + continue; + } + var componentType = monoBehaviour.GetType(); + do + { + GetMeshForComponent callback; + if (MeshForComponentCallbacks.TryGetValue(componentType, out callback)) + { + if (callback(this, monoBehaviour, fbxNode)) + { + return true; + } + } + componentType = componentType.BaseType; + } + while (componentType.IsSubclassOf(typeof(MonoBehaviour))); + } + } + + // If we're here, custom handling didn't work. + // Revert to default handling. + + // if user doesn't want to export mesh colliders, and this gameobject doesn't have a renderer + // then don't export it. + if (!ExportOptions.ExportUnrendered && (!gameObject.GetComponent() || !gameObject.GetComponent().enabled)) + { + return false; + } + + var meshFilter = defaultComponent as MeshFilter; + if (meshFilter) + { + var renderer = gameObject.GetComponent(); + var materials = renderer ? renderer.sharedMaterials : null; + return ExportMesh(new MeshInfo(meshFilter.sharedMesh, materials), fbxNode); + } + else + { + var smr = defaultComponent as SkinnedMeshRenderer; + if (smr) + { + var result = ExportSkinnedMesh(gameObject, fbxNode.GetScene(), fbxNode); + if (!result) + { + // fall back to exporting as a static mesh + var mesh = new Mesh(); + smr.BakeMesh(mesh); + var materials = smr.sharedMaterials; + result = ExportMesh(new MeshInfo(mesh, materials), fbxNode); + Object.DestroyImmediate(mesh); + } + return result; + } + } + + return false; + } + + /// + /// Number of nodes exported including siblings and decendents + /// + internal int NumNodes { get { return MapUnityObjectToFbxNode.Count; } } + + /// + /// Number of meshes exported + /// + internal int NumMeshes { set; get; } + + /// + /// Number of triangles exported + /// + internal int NumTriangles { set; get; } + + internal bool Verbose { get { return ExportSettings.instance.VerboseProperty; } } + + /// + /// manage the selection of a filename + /// + static string LastFilePath { get; set; } + private string m_tempFilePath { get; set; } + private string m_lastFilePath { get; set; } + + const string kFBXFileExtension = "fbx"; + + private static string MakeFileName(string basename = "test", string extension = kFBXFileExtension) + { + return basename + "." + extension; + } + + private static void OnExport() + { + var selectedGOs = Selection.GetFiltered(SelectionMode.TopLevel); + + var toExport = ModelExporter.RemoveRedundantObjects(selectedGOs); + if (ExportSettings.instance.DisplayOptionsWindow) + { + ExportModelEditorWindow.Init(Enumerable.Cast(toExport)); + return; + } + + string filename; + if (toExport.Count == 1) + { + filename = toExport.ToArray()[0].name; + } + else + { + filename = "Untitled"; + } + + var folderPath = ExportSettings.FbxAbsoluteSavePath; + var filePath = System.IO.Path.Combine(folderPath, filename + ".fbx"); + + if (System.IO.File.Exists(filePath)) + { + Debug.LogErrorFormat("{0}: Failed to export to {1}, file already exists", PACKAGE_UI_NAME, filePath); + return; + } + + if (ExportObjects(filePath, toExport.ToArray(), ExportSettings.instance.ExportModelSettings.info) != null) + { + // refresh the asset database so that the file appears in the + // asset folder view. + AssetDatabase.Refresh(); + } + } + + // /// + // /// Exports a single Unity GameObject to an FBX file, + // /// with the specified export settings. + // /// + // /// + // /// The FBX file path if successful; otherwise null. + // /// + // /// Absolute file path to use for the FBX file. + // /// The Unity GameObject to export. + // /// The export options to use. + // public static string ExportObject( + // string filePath, + // UnityEngine.Object singleObject, + // ExportModelOptions exportOptions = null + // ) + // { + // return ExportObjects(filePath, new Object[] { singleObject }, exportOptions?.ConvertToModelSettingsSerialize(), exportData: null); + // } + + internal static string ExportObject( + string filePath, + UnityEngine.Object singleObject, + IExportOptions exportOptions = null + ) + { + return ExportObjects(filePath, new Object[] { singleObject }, exportOptions, exportData: null); + } + + // /// + // /// Exports an array of Unity GameObjects to an FBX file, + // /// with the specified export settings. + // /// + // /// + // /// The FBX file path if successful; otherwise null. + // /// + // /// Absolute file path to use for the FBX file. + // /// Array of Unity GameObjects to export. + // /// The export options to use. + // public static string ExportObjects( + // string filePath, + // UnityEngine.Object[] objects = null, + // ExportModelOptions exportOptions = null) + // { + // return ExportObjects(filePath, objects, exportOptions?.ConvertToModelSettingsSerialize(), exportData: null); + // } + + /// + /// Exports an array of Unity GameObjects to an FBX file. + /// + /// + /// The FBX file path if successful; otherwise returns null. + /// + /// Absolute file path to use for the FBX file. + /// Array of Unity GameObjects to export. + public static string ExportObjects(string filePath, UnityEngine.Object[] objects = null) + { + return ExportObjects(filePath, objects, exportOptions: null, exportData: null); + } + + /// + /// Exports a single Unity GameObject to an FBX file. + /// + /// + /// The FBX file path if successful; otherwise null. + /// + /// Absolute file path to use for the FBX file. + /// The Unity GameObject to export. + public static string ExportObject(string filePath, UnityEngine.Object singleObject) + { + return ExportObjects(filePath, new Object[] { singleObject }, exportOptions: null); + } + + /// + /// Exports the animation from a single TimelineClip to an FBX file. + /// + /// Absolute file path to use for the FBX file. + /// The TimelineClip to export. + /// The export options to use. + /// The FBX file path if successful; otherwise null. + internal static string ExportTimelineClip(string filePath, TimelineClip timelineClip, PlayableDirector director = null, IExportOptions exportOptions = null) + { + var exportData = ModelExporter.GetExportData(timelineClip, director, exportOptions); + return ExportObjects(filePath, null, exportOptions: exportOptions, exportData: exportData); + } + + /// + /// Exports a list of GameObjects to an FBX file. + /// + /// Use the SaveFile panel to allow the user to enter a file name. + /// + /// + internal static string ExportObjects( + string filePath, + UnityEngine.Object[] objects = null, + IExportOptions exportOptions = null, + Dictionary exportData = null, + AnimationCover targetAnimation = null + ) + { + LastFilePath = filePath; + + var fbxExporter = Create(); + // ensure output directory exists + EnsureDirectory(filePath); + fbxExporter.ExportOptions = exportOptions; + + if (objects == null) + { + objects = Selection.objects; + } + + if (exportData == null) + { + exportData = GetExportData(objects, exportOptions, targetAnimation); + } + + if (fbxExporter.ExportAll(objects, exportData) > 0) + { + var message = string.Format("Successfully exported: {0}", filePath); + Debug.Log(message); + + return filePath; + } + return null; + } + + private static void EnsureDirectory(string path) + { + //check to make sure the path exists, and if it doesn't then + //create all the missing directories. + var fileInfo = new FileInfo(path); + + if (!fileInfo.Exists) + { + Directory.CreateDirectory(fileInfo.Directory.FullName); + } + } + + /// + /// Removes the diacritics (i.e. accents) from letters. + /// e.g. é becomes e + /// + /// Text with accents removed. + /// Text. + private static string RemoveDiacritics(string text) + { + var normalizedString = text.Normalize(System.Text.NormalizationForm.FormD); + var stringBuilder = new System.Text.StringBuilder(); + + foreach (var c in normalizedString) + { + var unicodeCategory = System.Globalization.CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != System.Globalization.UnicodeCategory.NonSpacingMark) + { + stringBuilder.Append(c); + } + } + + return stringBuilder.ToString().Normalize(System.Text.NormalizationForm.FormC); + } + + private static string ConvertToMayaCompatibleName(string name) + { + if (string.IsNullOrEmpty(name)) + { + return InvalidCharReplacement.ToString(); + } + var newName = RemoveDiacritics(name); + + if (char.IsDigit(newName[0])) + { + newName = newName.Insert(0, InvalidCharReplacement.ToString()); + } + + for (var i = 0; i < newName.Length; i++) + { + if (!char.IsLetterOrDigit(newName, i)) + { + if (i < newName.Length - 1 && newName[i] == MayaNamespaceSeparator) + { + continue; + } + newName = newName.Replace(newName[i], InvalidCharReplacement); + } + } + return newName; + } + + internal static string ConvertToValidFilename(string filename) + { + return System.Text.RegularExpressions.Regex.Replace(filename, + RegexCharStart + new string(Path.GetInvalidFileNameChars()) + RegexCharEnd, + InvalidCharReplacement.ToString() + ); + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/FbxExporter.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/FbxExporter.cs.meta new file mode 100644 index 0000000..e4c1bfd --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/FbxExporter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 849017d7ef6c589448dcbf335cda940f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/IExportData.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/IExportData.cs new file mode 100644 index 0000000..525d1e2 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/IExportData.cs @@ -0,0 +1,210 @@ +using UnityEngine; +using UnityEngine.Timeline; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine.Playables; + +namespace DaddyFrosty.Fbx +{ + /// + /// Export data containing extra information required to export + /// + internal interface IExportData + { + HashSet Objects { get; } + } + + /// + /// Export data containing what to export when + /// exporting animation only. + /// + internal class AnimationOnlyExportData : IExportData + { + // map from animation clip to GameObject that has Animation/Animator + // component containing clip + public Dictionary animationClips; + + // set of all GameObjects to export + public HashSet goExportSet; + public HashSet Objects { get { return goExportSet; } } + + // map from GameObject to component type to export + public Dictionary exportComponent; + + // first clip to export + public AnimationClip defaultClip; + + public AnimationOnlyExportData( + Dictionary animClips, + HashSet exportSet, + Dictionary exportComponent + ) + { + this.animationClips = animClips; + this.goExportSet = exportSet; + this.exportComponent = exportComponent; + this.defaultClip = null; + } + + public AnimationOnlyExportData() + { + this.animationClips = new Dictionary(); + this.goExportSet = new HashSet(); + this.exportComponent = new Dictionary(); + this.defaultClip = null; + } + + /// + /// collect all object dependencies for given animation clip + /// + public void CollectDependencies( + AnimationClip animClip, + GameObject rootObject, + IExportOptions exportOptions + ) + { + Debug.Assert(rootObject != null); + Debug.Assert(exportOptions != null); + + if (this.animationClips.ContainsKey(animClip)) + { + // we have already exported gameobjects for this clip + return; + } + + // NOTE: the object (animationRootObject) containing the animation is not necessarily animated + // when driven by an animator or animation component. + this.animationClips.Add(animClip, rootObject); + + foreach (EditorCurveBinding uniCurveBinding in AnimationUtility.GetCurveBindings(animClip)) + { + Object uniObj = AnimationUtility.GetAnimatedObject(rootObject, uniCurveBinding); + if (!uniObj) + { + continue; + } + + GameObject unityGo = ModelExporter.GetGameObject(uniObj); + if (!unityGo) + { + continue; + } + + if (!exportOptions.AnimateSkinnedMesh && unityGo.GetComponent()) + { + continue; + } + + // If we have a clip driving a camera or light then force the export of FbxNodeAttribute + // so that they point the right way when imported into Maya. + if (unityGo.GetComponent()) + this.exportComponent[unityGo] = typeof(Light); + else if (unityGo.GetComponent()) + this.exportComponent[unityGo] = typeof(Camera); + else if ((uniCurveBinding.type == typeof(SkinnedMeshRenderer)) && unityGo.GetComponent()) + { + // only export mesh if there are animation keys for it (e.g. for blendshapes) + if (FbxPropertyChannelPair.TryGetValue(uniCurveBinding.propertyName, out FbxPropertyChannelPair[] channelPairs)) + { + this.exportComponent[unityGo] = typeof(SkinnedMeshRenderer); + } + } + + this.goExportSet.Add(unityGo); + } + } + + /// + /// collect all objects dependencies for animation clips. + /// + public void CollectDependencies( + AnimationClip[] animClips, + GameObject rootObject, + IExportOptions exportOptions + ) + { + Debug.Assert(rootObject != null); + Debug.Assert(exportOptions != null); + + foreach (var animClip in animClips) + { + CollectDependencies(animClip, rootObject, exportOptions); + } + } + + /// + /// Get the GameObject that the clip is bound to in the timeline. + /// + /// + /// The GameObject bound to the timeline clip or null if none. + private static GameObject GetGameObjectBoundToTimelineClip(TimelineClip timelineClip, PlayableDirector director = null) + { + object parentTrack = timelineClip.GetParentTrack(); + AnimationTrack animTrack = parentTrack as AnimationTrack; + + var inspectedDirector = director ? director : UnityEditor.Timeline.TimelineEditor.inspectedDirector; + if (!inspectedDirector) + { + Debug.LogWarning("No Timeline selected in inspector, cannot retrieve GameObject bound to track"); + return null; + } + + Object animationTrackObject = inspectedDirector.GetGenericBinding(animTrack); + + GameObject animationTrackGO = null; + if (animationTrackObject is GameObject) + { + animationTrackGO = animationTrackObject as GameObject; + } + else if (animationTrackObject is Animator) + { + animationTrackGO = (animationTrackObject as Animator).gameObject; + } + + if (animationTrackGO == null) + { + Debug.LogErrorFormat("Could not export animation track object of type {0}", animationTrackObject.GetType().Name); + return null; + } + return animationTrackGO; + } + + /// + /// Get the GameObject and it's corresponding animation clip from the given timeline clip. + /// + /// + /// KeyValuePair containing GameObject and corresponding AnimationClip + public static KeyValuePair GetGameObjectAndAnimationClip(TimelineClip timelineClip, PlayableDirector director = null) + { + var animationTrackGO = GetGameObjectBoundToTimelineClip(timelineClip, director); + if (!animationTrackGO) + { + return new KeyValuePair(); + } + + return new KeyValuePair(animationTrackGO, timelineClip.animationClip); + } + + /// + /// Get the filename of the format {model}@{anim}.fbx from the given timeline clip + /// + /// + /// filename for use for exporting animation clip + public static string GetFileName(TimelineClip timelineClip) + { + // if the timeline clip name already contains an @, then take this as the + // filename to avoid duplicate @ + if (timelineClip.displayName.Contains("@")) + { + return timelineClip.displayName; + } + + var goBound = GetGameObjectBoundToTimelineClip(timelineClip); + if (goBound == null) + { + return timelineClip.displayName; + } + return string.Format("{0}@{1}", goBound.name, timelineClip.displayName); + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/IExportData.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/IExportData.cs.meta new file mode 100644 index 0000000..49b78c0 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/IExportData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 020f058b66da64746830aa60391539dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc.meta new file mode 100644 index 0000000..aa5c0f1 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: eeebc6a06d294fb494ef7a9697faba57 +timeCreated: 1671143407 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu.meta new file mode 100644 index 0000000..a75a3d8 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: cae2b7a32f684525875a7ef9f0edf5ce +timeCreated: 1671154957 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims.meta new file mode 100644 index 0000000..b1ee3d4 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3f227242526649c4a3668a2adfc1e184 +timeCreated: 1671162404 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/AnimationCover.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/AnimationCover.cs new file mode 100644 index 0000000..4b80d1a --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/AnimationCover.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using UnityEngine; + +namespace DaddyFrosty.Fbx.Misc.AssetsMenu +{ + public class AnimationCover + { + public Animation Animator { get; set; } + public AnimationClip[] Clips { get; set; } + [CanBeNull] public AnimationClip FirstClip { get; set; } + + public AnimationCover() + { + + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/AnimationCover.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/AnimationCover.cs.meta new file mode 100644 index 0000000..7b5f9ba --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/AnimationCover.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 94d85be9434e481896f4c461319d54cf +timeCreated: 1671163247 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Export.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Export.cs new file mode 100644 index 0000000..753a068 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Export.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.IO; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; + +namespace DaddyFrosty.Fbx.Misc.AssetsMenu +{ + public partial class ExportAnimations + { + // public GameObject GameObject { get; private set; } + // public Animation Animator { get; private set; } + // protected ExportModelSettingsEditor InnerEditor { get; private set; } + + private bool Export() + { + var clips = AnimationUtility.GetAnimationClips( Animator.gameObject ); + var folderPath = ExportSettings.GetAbsoluteSavePath( FbxSavePaths[SelectedFbxPath] ); + var finalPath = $"{folderPath}{Path.DirectorySeparatorChar}{GameObject.name}@"; + + foreach ( var clip in clips ) + { + // Log.Info( $"Clip: {clip.name}, {clip.clip.frameRate}. | {GameObject.name}." ); + if ( ExportClip( finalPath, ExportModelSettingsInstance, clip ) ) + continue; + + Log.Error( $"Export failed: {clip.name} ( {clip} ) | {GameObject.name}." ); + } + + AssetDatabase.Refresh(); + SaveExportSettings(); + return true; + } + + private bool ExportClip( string finalPath, ExportModelSettings settings, AnimationClip clip ) + { + var savePath = finalPath + clip.name + ".fbx"; + if ( !OverwriteExistingFile( savePath ) ) + return false; + + var targetAnimation = new AnimationCover() + { + Animator = Animator, + Clips = new[] { clip }, + FirstClip = null + }; + var exportResult = ModelExporter.ExportObjects( savePath, new Object[]{ GameObject }, settings.info, targetAnimation: targetAnimation ); + if ( string.IsNullOrEmpty( exportResult ) ) + { + Log.Error( $"exportResult = null. {finalPath}, {clip}." ); + return false; + } + + return true; + } + + private bool OverwriteExistingFile( string filePath ) + { + // check if file already exists, give a warning if it does + if ( _overwriteAll || !System.IO.File.Exists( filePath ) ) + return true; + + var shouldOverwrite = UnityEditor.EditorUtility.DisplayDialogComplex( + $"{ModelExporter.PACKAGE_UI_NAME} Warning", + $"File {filePath} already exists.\nOverwrite cannot be undone.", + "Overwrite", "Overwrite All", "Cancel" ); + if ( shouldOverwrite == 2 ) + { + if ( GUI.changed ) + SaveExportSettings(); + + return false; + } + + if ( shouldOverwrite == 1 ) + _overwriteAll = true; + + return true; + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Export.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Export.cs.meta new file mode 100644 index 0000000..8e8f62e --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Export.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 500cbd797b024292aa9e25661f50abb9 +timeCreated: 1671160494 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Path.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Path.cs new file mode 100644 index 0000000..d0bb5db --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Path.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEditor; +using UnityEngine; + +namespace DaddyFrosty.Fbx.Misc.AssetsMenu +{ + public partial class ExportAnimations + { + private static void RestorePathsFromSession( string varName, List defaultsPaths, out List paths ) + { + var n = SessionState.GetInt( string.Format( ExportModelEditorWindow.k_SessionStoragePrefix, varName ), 0 ); + if ( n <= 0 ) + { + paths = defaultsPaths; + return; + } + + paths = new(); + for ( var i = 0; i < n; i++ ) + { + var path = SessionState.GetString( string.Format( ExportModelEditorWindow.k_SessionStoragePrefix + "_{1}", varName, i ), null ); + if ( !string.IsNullOrEmpty( path ) ) + paths.Add( path ); + } + } + + private const string k_SessionFbxPathsName = "FbxAnimAllPath"; + private const string k_SessionSelectedFbxPathName = "SelectedFbxAnimAllPath"; + + // FBX Paths. + private List m_fbxSavePaths; + + public List FbxSavePaths + { + get + { + if ( m_fbxSavePaths == null ) + { + // Try to restore from session, fall back to Fbx Export Settings + RestorePathsFromSession( k_SessionFbxPathsName, ExportSettings.instance.GetCopyOfFbxSavePaths(), out m_fbxSavePaths ); + SelectedFbxPath = SessionState.GetInt( string.Format( ExportModelEditorWindow.k_SessionStoragePrefix, + k_SessionSelectedFbxPathName ), ExportSettings.instance.SelectedFbxPath ); + } + + return m_fbxSavePaths; + } + } + + [SerializeField] private int _selectedFbxPath = 0; + + public int SelectedFbxPath + { + get => _selectedFbxPath; + set => _selectedFbxPath = value; + } + + private static void StorePathsInSession( string varName, [CanBeNull] List paths ) + { + if ( paths == null ) + return; + + var n = paths.Count; + SessionState.SetInt( string.Format( SessionStoragePrefix, varName ), n ); + for ( var i = 0; i < n; i++ ) + SessionState.SetString( string.Format( SessionStoragePrefix + "_{1}", varName, i ), paths[i] ); + } + + private void SaveExportSettings() + { + var settings = ExportModelSettingsInstance.info; + var json = EditorJsonUtility.ToJson( settings ); + SessionState.SetString( string.Format( SessionStoragePrefix, SessionSettingsName ), json ); + + StorePathsInSession( k_SessionFbxPathsName, m_fbxSavePaths ); + SessionState.SetInt( string.Format( SessionStoragePrefix, k_SessionSelectedFbxPathName ), SelectedFbxPath ); + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Path.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Path.cs.meta new file mode 100644 index 0000000..3358528 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Path.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1f6b5c316ea646e5b75b2c473998168c +timeCreated: 1671160149 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Settings.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Settings.cs new file mode 100644 index 0000000..59bc024 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Settings.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEditor; +using UnityEditor.Presets; +using UnityEngine; + +namespace DaddyFrosty.Fbx.Misc.AssetsMenu +{ + public partial class ExportAnimations + { + private static string SessionStoragePrefix => ExportModelEditorWindow.k_SessionStoragePrefix; + private static string SessionSettingsName => ExportOptionsEditorWindow.k_SessionSettingsName; + private ExportModelSettings _exportModelSettingsInstance; + + public ExportModelSettings ExportModelSettingsInstance + { + get + { + if ( _exportModelSettingsInstance != null ) + return _exportModelSettingsInstance; + + // make a copy of the settings + _exportModelSettingsInstance = CreateInstance( typeof( ExportModelSettings ) ) as ExportModelSettings; + // load settings stored in Unity session, default to DefaultPreset, if none then Export Settings + var defaultPresets = Preset.GetDefaultPresetsForObject( _exportModelSettingsInstance ); + if ( defaultPresets.Length <= 0 ) + { + RestoreSettingsFromSession( ExportSettings.instance.ExportModelSettings.info ); + return _exportModelSettingsInstance; + } + + // apply the first default preset + // TODO: figure out what it means to have multiple default presets, when would they be applied? + defaultPresets[0].ApplyTo( _exportModelSettingsInstance ); + RestoreSettingsFromSession( _exportModelSettingsInstance!.info ); + return _exportModelSettingsInstance; + } + } + + private void RestoreSettingsFromSession( ExportOptionsSettingsSerializeBase defaults ) + { + var settings = ExportModelSettingsInstance.info; + var key = string.Format( SessionStoragePrefix, SessionSettingsName ); + var json = SessionState.GetString( key, EditorJsonUtility.ToJson( defaults ) ); + if ( string.IsNullOrEmpty( json ) ) + { + return; + } + + EditorJsonUtility.FromJsonOverwrite( json, settings ); + } + + private void AddPathUI() + { + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField( new GUIContent( + "Export Path", + "Location where the FBXs will be saved." ), GUILayout.Width( LabelWidth - FieldOffset ) ); + var pathLabels = ExportSettings.GetMixedSavePaths( FbxSavePaths ); + + SelectedFbxPath = EditorGUILayout.Popup( SelectedFbxPath, pathLabels, GUILayout.MinWidth( SelectableLabelMinWidth ) ); + var exportSettingsEditor = InnerEditor as ExportModelSettingsEditor; + // Set export setting for exporting outside the project on choosing a path + var exportOutsideProject = !pathLabels[SelectedFbxPath].Substring( 0, 6 ).Equals( "Assets" ); + exportSettingsEditor.SetExportingOutsideProject( exportOutsideProject ); + + if ( GUILayout.Button( new GUIContent( "...", "Browse to a new location to export to" ), EditorStyles.miniButton, GUILayout.Width( BrowseButtonWidth ) ) ) + { + var initialPath = Application.dataPath; + + var fullPath = EditorUtility.SaveFolderPanel( + "Select Export Model Path", initialPath, null + ); + + // Unless the user canceled, save path. + if ( !string.IsNullOrEmpty( fullPath ) ) + { + var relativePath = ExportSettings.ConvertToAssetRelativePath( fullPath ); + + if ( string.IsNullOrEmpty( relativePath ) ) + { + ExportSettings.AddSavePath( fullPath, FbxSavePaths, exportOutsideProject: true ); + SelectedFbxPath = 0; + } + // Store the relative path to the Assets folder + else + { + ExportSettings.AddSavePath( relativePath, FbxSavePaths, exportOutsideProject: false ); + SelectedFbxPath = 0; + } + + GUIUtility.hotControl = 0; + GUIUtility.keyboardControl = 0; + } + } + + GUILayout.EndHorizontal(); + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Settings.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Settings.cs.meta new file mode 100644 index 0000000..6bcea22 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.Settings.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 572d5e1936574f0182ffc3896882391d +timeCreated: 1671159589 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.cs new file mode 100644 index 0000000..c39d039 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.cs @@ -0,0 +1,97 @@ +using System; +using JetBrains.Annotations; +using UnityEditor; +using UnityEngine; + +namespace DaddyFrosty.Fbx.Misc.AssetsMenu +{ + public partial class ExportAnimations : EditorWindow + { + public const string kMenuPrefix = RemoveAllScripts.kMenuPrefix; + public const string kMenuName = kMenuPrefix + "Export Animations"; + + [MenuItem( kMenuName, true, 100 )] + private static bool CanExportAnimations() => Selection.activeObject is GameObject go && go.TryGetComponent( out _ ); + + [MenuItem( kMenuName, false, 100 )] + private static void ExportAnimation() + { + if ( Selection.activeObject is not GameObject go || !go.TryGetComponent( out var animator ) ) + return; + + var window = CreateWindow(); + window.Setup( go, animator ); + window.Show(); + } + + public GameObject GameObject { get; private set; } + public Animation Animator { get; private set; } + protected ExportModelSettingsEditor InnerEditor { get; private set; } + private bool _showOptions; + private bool _overwriteAll; + private void Setup( GameObject go, Animation animator ) + { + titleContent = new( "Animation Exporter" ); + GameObject = go; + Animator = animator; + } + + protected const float SelectableLabelMinWidth = 120; + protected const float BrowseButtonWidth = 25; + protected const float LabelWidth = 175; + protected const float FieldOffset = 18; + protected const float TextFieldAlignOffset = 3; + protected const float ExportButtonWidth = 100; + protected const float FbxExtOffset = -7; + + private void OnEnable() + { + _showOptions = true; + _overwriteAll = false; + if ( !InnerEditor ) + InnerEditor = Editor.CreateEditor( ExportModelSettingsInstance ) as ExportModelSettingsEditor; + + InnerEditor!.DisableIncludeDropdown( true ); + ExportModelSettingsInstance.info.SetModelAnimIncludeOption( Include.Anim ); + } + + private void OnGUI() + { + if ( GameObject == null || Animator == null ) + { + return; + } + + // Path. + AddPathUI(); + + // Spacer + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + + // Show Options. + EditorGUI.indentLevel++; + _showOptions = EditorGUILayout.Foldout( _showOptions, "Options" ); + if ( _showOptions ) + { + InnerEditor.OnInspectorGUI(); + } + EditorGUI.indentLevel--; + + // Spacer + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + + if ( GUILayout.Button( "Cancel", GUILayout.Width( ExportButtonWidth ) ) ) + Close(); + + if ( GUILayout.Button( "Export all", GUILayout.Width( ExportButtonWidth ) ) ) + { + if ( Export() ) + Close(); + } + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.cs.meta new file mode 100644 index 0000000..c0b8fb5 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/Anims/ExportAnimations.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fbc05f6a5648492b91c9799cfbf77423 +timeCreated: 1671158894 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/RemoveAllScripts.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/RemoveAllScripts.cs new file mode 100644 index 0000000..1427284 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/RemoveAllScripts.cs @@ -0,0 +1,72 @@ +using UnityEditor; +using UnityEngine; + +namespace DaddyFrosty.Fbx.Misc.AssetsMenu +{ + public class RemoveAllScripts + { + public const string kMenuPrefix = "GameObject/DaddyFrosty/"; + public const string kRemoveAllMenu = kMenuPrefix + "Remove All Scripts"; + public const string kRemoveAllMenuChildren = kMenuPrefix + "Remove All Scripts ( Traverse Children )"; + + [MenuItem( kRemoveAllMenu, false, 1 )] + private static void RemoveAllMonoBehvaiours() + { + // var go = Selection.activeObject; + // Log.Info( $"go: {go}, {go?.GetType()} {go?.name}" ); + if ( Selection.activeObject is not GameObject go ) + return; + + var destroyed = RemoveOn( go, false ); + Log.Info( $"Destroyed: {destroyed} script(s). With Children: False." ); + } + + [MenuItem( kRemoveAllMenuChildren, false, 10 )] + private static void RemoveAllMonoBehvaioursChildren() + { + if ( Selection.activeObject is not GameObject go ) + return; + + var destroyed = RemoveOn( go, true ); + Log.Info( $"Destroyed: {destroyed} script(s). With Children: True." ); + } + + private static int RemoveOn( GameObject go, bool traverseChildren ) + { + // var scripts = go.GetComponents(); + // var scripts = go.GetComponents( typeof( MonoBehaviour ) ); + // Log.Info( $"Found: {scripts.Length} Script(s) on {go.name}." ); + + var destroyed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript( go ); + // for ( var i = 0; i < scripts.Length; i++ ) + // { + // // Log.Info( $"script: {script} | {script?.GetInstanceID()} | {(bool)script} | {script == null}" ); + // var script = scripts[i]; + // if ( script != null ) + // continue; + // + // Log.Info( $"script: {script} | PrefabUtility.IsPartOfPrefabInstance:." ); + // Object.DestroyImmediate( script ); + // GameObjectUtility.RemoveMonoBehavioursWithMissingScript( go ) + // destroyed++; + // } + + if ( !traverseChildren ) + return destroyed; + + foreach ( var child in go.transform ) + { + if ( child is not Transform cT ) + { + // Log.Info( $"Skipping {child}, {child?.ToString()}" ); + continue; + } + + // Log.Info( "Found child." ); + destroyed += RemoveOn( cT.gameObject, true ); + } + + return destroyed; + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/RemoveAllScripts.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/RemoveAllScripts.cs.meta new file mode 100644 index 0000000..d792e22 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/AssetsMenu/RemoveAllScripts.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e7d9c7fd478c45cda37a8e4078ab8821 +timeCreated: 1671154962 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/CustomFBX.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/CustomFBX.cs new file mode 100644 index 0000000..9b33d09 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/CustomFBX.cs @@ -0,0 +1,46 @@ +using UnityEditor; +using UnityEngine; + +namespace DaddyFrosty.Fbx.Misc +{ + public partial class CustomFBX + { + public static void AddSettings( ExportModelSettingsSerialize options, GUILayoutOption guiWidth ) + { + GUILayout.BeginVertical(); + GUILayout.FlexibleSpace(); + GUILayout.EndVertical(); + GUILayout.BeginHorizontal(); + GUILayout.Label( "------ Custom ------", guiWidth ); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField( new GUIContent( "Verbose", + "Extra debug info." ), guiWidth ); + ExportSettings.instance.VerboseProperty = EditorGUILayout.Toggle( ExportSettings.instance.VerboseProperty ); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField( new GUIContent( "Use Maya Cordinate Conversion", + "If enabled axis will be properly converted." ), guiWidth ); + options.SetConvertCordinateSpace( EditorGUILayout.Toggle( options.ConvertCordinateSpace ) ); + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField( new GUIContent( "Bake Animation", + "If enabled BakeAnimationProperty will be true." ), guiWidth ); + ModelExporter.ExportSettings.BakeAnimationProperty = EditorGUILayout.Toggle( ModelExporter.ExportSettings.BakeAnimationProperty ); + GUILayout.EndHorizontal(); + + + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField( new GUIContent( "Unit Scale Factor", + "Scaling applied to export." ), guiWidth ); + + var val = EditorGUILayout.FloatField( options.UnitScaleFactor ); + options.SetUnitScaleFactor( val ); + ExportSettings.instance.ExportModelSettings.info.SetUnitScaleFactor( val ); + GUILayout.EndHorizontal(); + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/CustomFBX.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/CustomFBX.cs.meta new file mode 100644 index 0000000..30fc1e6 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/CustomFBX.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 8b09b952f5cb40789169ec3d2ffe71da +timeCreated: 1671143548 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark.meta new file mode 100644 index 0000000..9c4f457 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 736e57e7cca54e95b0447f83e419380c +timeCreated: 1671142838 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/Log.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/Log.cs new file mode 100644 index 0000000..08b8256 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/Log.cs @@ -0,0 +1,33 @@ +#nullable enable +using UnityEngine; +using UnityEditor; + +namespace DaddyFrosty +{ + public static class Log + { + public static void Info( object? str ) + { + Debug.Log( str ); + } + + public static void Error( object? str ) + { + Debug.LogError( $"[ERROR] {str}" ); + } + + public static void Warning( object? str ) + { + Debug.LogWarning( $"[Warning] {str}" ); + } + + public static string LineS => "-------------"; + + public static void Line() + => Log.Info( LineS ); + + // "VARIDAIC" + private static string VarInternal( string? varName, object? obj ) + => varName == null ? ( obj?.ToString() ?? string.Empty ) : $"{varName}: {obj}"; + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/Log.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/Log.cs.meta new file mode 100644 index 0000000..70f5558 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/Log.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3ff8397ea8124438a6e486833291fe11 +timeCreated: 1671142843 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/MathX.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/MathX.cs new file mode 100644 index 0000000..019cab4 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/MathX.cs @@ -0,0 +1,214 @@ +using System; + +namespace DaddyFrosty +{ + public static class MathX + { + /// Returns a double between min and max + public static double Double( this Random self, double min, double max ) + { + return min + ( max - min ) * self.NextDouble(); + } + + /// Returns a random float between min and max + public static float Float( this Random self, float min, float max ) + { + return min + ( max - min ) * (float)self.NextDouble(); + } + + /// Returns a random float between 0 and max (or 1) + public static float Float( this Random self, float max = 1f ) + { + return self.Float( 0.0f, max ); + } + + /// Returns a random double between 0 and max (or 1) + public static double Double( Random self, double max = 1.0 ) + { + return self.Double( 0.0, max ); + } + + /// Returns a random int between min and max (inclusive) + public static int Int( this Random self, int min, int max ) + { + return self.Next( min, max + 1 ); + } + + /// Returns a random int between 0 and max (inclusive) + public static int Int( this Random self, int max ) + { + return self.Int( 0, max ); + } + + internal const float toDegrees = 57.2958f; + internal const float toRadians = 0.0174533f; + internal const float toGradiansDegrees = 0.9f; + internal const float toGradiansRadians = 0.01570796f; + internal const float toMeters = 0.0254f; + internal const float toInches = 39.37008f; + internal const float toMilimeters = 0.0393701f; + + public static float DegreeToRadian( this float f ) + { + return f * 0.0174533f; + } + + public static float RadianToDegree( this float f ) + { + return f * 57.2958f; + } + + public static float GradiansToDegrees( this float f ) + { + return f * 0.9f; + } + + public static float GradiansToRadians( this float f ) + { + return f * 0.01570796f; + } + + public static float MeterToInch( this float f ) + { + return f * 39.37008f; + } + + public static float InchToMeter( this float f ) + { + return f * 0.0254f; + } + + public static float InchToMilimeter( this float f ) + { + return f * 0.0393701f; + } + + /// Snap number to grid + public static float SnapToGrid( this float f, float gridSize ) + { + return MathF.Round( f / gridSize ) * gridSize; + } + + /// + /// Remove the fractional part and return the float as an integer. + /// + public static int FloorToInt( this float f ) + { + return (int)MathF.Floor( f ); + } + + public static float Floor( this float f ) + { + return MathF.Floor( f ); + } + + public static int CeilToInt( this float f ) + { + return (int)MathF.Ceiling( f ); + } + + private static void Order( ref float a, ref float b ) + { + if ( a <= (double)b ) + return; + ( a, b ) = ( b, a ); + } + + public static float Clamp( this float v, float min, float max ) + { + Order( ref min, ref max ); + return v < (double)min ? min : v < (double)max ? v : max; + } + + public static float Lerp( float from, float to, float delta, bool clamp = true ) + { + if ( clamp ) + delta = delta.Clamp( 0.0f, 1f ); + return from + delta * ( to - from ); + } + + public static float LerpTo( this float from, float to, float delta, bool clamp = true ) + { + if ( clamp ) + delta = delta.Clamp( 0.0f, 1f ); + return from + delta * ( to - from ); + } + + public static float[] LerpTo( this float[] from, float[] to, float delta, bool clamp = true ) + { + if ( from == null ) + return null; + if ( to == null ) + return from; + var numArray = new float[Math.Min( from.Length, to.Length )]; + for ( var index = 0; index < numArray.Length; ++index ) + numArray[index] = from[index].LerpTo( to[index], delta, clamp ); + return numArray; + } + + public static float Approach( this float f, float target, float delta ) + { + if ( f > (double)target ) + { + f -= delta; + if ( f < (double)target ) + return target; + } + else + { + f += delta; + if ( f > (double)target ) + return target; + } + + return f; + } + + public static float LerpInverse( this float value, float a, float b, bool clamp = true ) + { + if ( clamp ) + value = value.Clamp( a, b ); + value -= a; + b -= a; + return value / b; + } + + public static bool AlmostEqual( this float value, float b, float within = 0.0001f ) + { + return MathF.Abs( value - b ) <= (double)within; + } + + /// Does what you expected to happen when you did "a % b" + public static float UnsignedMod( this float a, float b ) + { + return a - b * ( a / b ).Floor(); + } + + /// Convert angle to between 0 - 360 + public static float NormalizeDegrees( this float degree ) + { + degree %= 360f; + if ( degree < 0.0 ) + degree += 360f; + return degree; + } + + /// Remap a float value from a one range to another + public static float Remap( + this float value, + float oldLow, + float oldHigh, + float newLow = 0.0f, + float newHigh = 1f ) + { + return newLow + + (float)( ( value - (double)oldLow ) * ( newHigh - (double)newLow ) / ( oldHigh - (double)oldLow ) ); + } + + /// Remap an integer value from a one range to another + public static int Remap( this int value, int oldLow, int oldHigh, int newLow, int newHigh ) + { + return newLow + ( value - oldLow ) * ( newHigh - newLow ) / ( oldHigh - oldLow ); + } + } +} \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/MathX.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/MathX.cs.meta new file mode 100644 index 0000000..81a0e11 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Misc/PixelBenchmark/MathX.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 45978a3e2c074d2a8a7129d9ccbfb1c5 +timeCreated: 1671150414 \ No newline at end of file diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources.meta new file mode 100644 index 0000000..d20e9d5 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e6000a9455111214ebda68b014dba7fa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CameraVisitor.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CameraVisitor.cs new file mode 100644 index 0000000..1cab03f --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CameraVisitor.cs @@ -0,0 +1,135 @@ +using UnityEngine; +using Autodesk.Fbx; +using DaddyFrosty.Fbx.CustomExtensions; +using System.Collections.Generic; +using UnityEditor; + +namespace DaddyFrosty.Fbx +{ + namespace Visitors + { + public static class CameraVisitor + { + private static Dictionary s_mapGateFit = new Dictionary() + { + { Camera.GateFitMode.Fill, FbxCamera.EGateFit.eFitFill }, + { Camera.GateFitMode.Horizontal, FbxCamera.EGateFit.eFitHorizontal }, + { Camera.GateFitMode.None, FbxCamera.EGateFit.eFitNone }, + { Camera.GateFitMode.Overscan, FbxCamera.EGateFit.eFitOverscan }, + { Camera.GateFitMode.Vertical, FbxCamera.EGateFit.eFitVertical } + }; + + /// + /// Visit Object and configure FbxCamera + /// + public static void ConfigureCamera(Camera unityCamera, FbxCamera fbxCamera) + { + if (unityCamera.usePhysicalProperties) + ConfigurePhysicalCamera(fbxCamera, unityCamera); + else + ConfigureGameCamera(fbxCamera, unityCamera); + } + + /// + /// Configure FbxCameras from GameCamera + /// + private static void ConfigureGameCamera(FbxCamera fbxCamera, Camera unityCamera) + { + // Configure FilmBack settings as a 35mm TV Projection (0.816 x 0.612) + float aspectRatio = unityCamera.aspect; + + float apertureHeightInInches = 0.612f; + float apertureWidthInInches = aspectRatio * apertureHeightInInches; + + FbxCamera.EProjectionType projectionType = + unityCamera.orthographic ? FbxCamera.EProjectionType.eOrthogonal : FbxCamera.EProjectionType.ePerspective; + + fbxCamera.ProjectionType.Set(projectionType); + fbxCamera.FilmAspectRatio.Set(aspectRatio); + fbxCamera.SetApertureWidth(apertureWidthInInches); + fbxCamera.SetApertureHeight(apertureHeightInInches); + fbxCamera.SetApertureMode(FbxCamera.EApertureMode.eVertical); + + // Focal Length + double focalLength = fbxCamera.ComputeFocalLength(unityCamera.fieldOfView); + + fbxCamera.FocalLength.Set(focalLength); + + // Field of View + fbxCamera.FieldOfView.Set(unityCamera.fieldOfView); + + // NearPlane + fbxCamera.SetNearPlane(unityCamera.nearClipPlane.Meters().ToCentimeters()); + + // FarPlane + fbxCamera.SetFarPlane(unityCamera.farClipPlane.Meters().ToCentimeters()); + + return; + } + + public static Vector2 GetSizeOfMainGameView() + { +#if UNITY_2020_1_OR_NEWER + return Handles.GetMainGameViewSize(); +#else + System.Type T = System.Type.GetType("UnityEditor.GameView,UnityEditor"); + System.Reflection.MethodInfo GetSizeOfMainGameView = T.GetMethod("GetSizeOfMainGameView", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + System.Object Res = GetSizeOfMainGameView.Invoke(null, null); + return (Vector2)Res; +#endif // UNITY_2020_1_OR_NEWER + } + + /// + /// Configure FbxCameras from a Physical Camera + /// + private static void ConfigurePhysicalCamera(FbxCamera fbxCamera, Camera unityCamera) + { + Debug.Assert(unityCamera.usePhysicalProperties); + + // Configure FilmBack settings + float apertureHeightInInches = unityCamera.sensorSize.y.Millimeters().ToInches(); + float apertureWidthInInches = unityCamera.sensorSize.x.Millimeters().ToInches(); + float aspectRatio = apertureWidthInInches / apertureHeightInInches; + + FbxCamera.EProjectionType projectionType = unityCamera.orthographic + ? FbxCamera.EProjectionType.eOrthogonal + : FbxCamera.EProjectionType.ePerspective; + + // NOTE: it is possible to match some of the sensor sizes to the + // predefined EApertureFormats : e16mmTheatrical, eSuper16mm, + // e35mmFullAperture, eIMAX. However the round in the sizes is not + // consistent between Unity and FBX so we choose + // to leave the values as a eCustomAperture setting. + + fbxCamera.ProjectionType.Set(projectionType); + fbxCamera.FilmAspectRatio.Set(aspectRatio); + + Vector2 gameViewSize = GetSizeOfMainGameView(); + fbxCamera.SetAspect(FbxCamera.EAspectRatioMode.eFixedRatio, gameViewSize.x / gameViewSize.y, 1.0); + fbxCamera.SetApertureWidth(apertureWidthInInches); + fbxCamera.SetApertureHeight(apertureHeightInInches); + + // Fit the resolution gate horizontally within the film gate. + fbxCamera.GateFit.Set(s_mapGateFit[unityCamera.gateFit]); + + // Lens Shift ( Film Offset ) as a percentage 0..1 + // FBX FilmOffset is in inches + fbxCamera.FilmOffsetX.Set(apertureWidthInInches * Mathf.Clamp(Mathf.Abs(unityCamera.lensShift.x), 0f, 1f) * Mathf.Sign(unityCamera.lensShift.x)); + fbxCamera.FilmOffsetY.Set(apertureHeightInInches * Mathf.Clamp(Mathf.Abs(unityCamera.lensShift.y), 0f, 1f) * Mathf.Sign(unityCamera.lensShift.y)); + + // Focal Length + fbxCamera.SetApertureMode(FbxCamera.EApertureMode.eFocalLength); + + double focalLength = (double)unityCamera.focalLength; + fbxCamera.FocalLength.Set(focalLength); /* in millimeters */ + + // NearPlane + fbxCamera.SetNearPlane((double)unityCamera.nearClipPlane.Meters().ToCentimeters()); + + // FarPlane + fbxCamera.SetFarPlane((float)unityCamera.farClipPlane.Meters().ToCentimeters()); + return; + } + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CameraVisitor.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CameraVisitor.cs.meta new file mode 100644 index 0000000..bd7e5f4 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CameraVisitor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f112f025b26d64345936e9d9c6752f50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CustomExtensions.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CustomExtensions.cs new file mode 100644 index 0000000..d7a012a --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CustomExtensions.cs @@ -0,0 +1,310 @@ +using System; +using UnityEngine; +using Autodesk.Fbx; + +namespace DaddyFrosty.Fbx +{ + namespace CustomExtensions + { + public class MetricDistance : object + { + public static readonly MetricDistance Millimeter = new MetricDistance(0.001f); + public static readonly MetricDistance Centimeter = new MetricDistance(0.01f); + public static readonly MetricDistance Meter = new MetricDistance(1.0f); + + private float _meters; + + public MetricDistance(float m) + { + this._meters = m; + } + + public float ToMeters() + { + return this._meters; + } + + public float ToCentimeters() + { + return this._meters / Centimeter._meters; + } + + public float ToMillimeters() + { + return this._meters / Millimeter._meters; + } + + public ImperialDistance ToImperial() + { + return new ImperialDistance(this._meters * 39.3701f); + } + + public float ToInches() + { + return ToImperial().ToInches(); + } + + public override int GetHashCode() + { + return _meters.GetHashCode(); + } + + public override bool Equals(object obj) + { + var o = obj as MetricDistance; + if (o == null) return false; + return _meters.Equals(o._meters); + } + + public static bool operator==(MetricDistance a, MetricDistance b) + { + // If both are null, or both are same instance, return true + if (ReferenceEquals(a, b)) return true; + + // if either one or the other are null, return false + if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false; + + return a._meters == b._meters; + } + + public static bool operator!=(MetricDistance a, MetricDistance b) + { + return !(a == b); + } + + public static MetricDistance operator+(MetricDistance a, MetricDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new MetricDistance(a._meters + b._meters); + } + + public static MetricDistance Add(MetricDistance a, MetricDistance b) + { + return a + b; + } + + public static MetricDistance operator-(MetricDistance a, MetricDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new MetricDistance(a._meters - b._meters); + } + + public static MetricDistance Subtract(MetricDistance a, MetricDistance b) + { + return a - b; + } + + public static MetricDistance operator*(MetricDistance a, MetricDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new MetricDistance(a._meters * b._meters); + } + + public static MetricDistance Multiply(MetricDistance a, MetricDistance b) + { + return a * b; + } + + public static MetricDistance operator/(MetricDistance a, MetricDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new MetricDistance(a._meters / b._meters); + } + + public static MetricDistance Divide(MetricDistance a, MetricDistance b) + { + return a / b; + } + } + + public class ImperialDistance + { + public static readonly ImperialDistance Inch = new ImperialDistance(1.0f); + public static readonly ImperialDistance Foot = new ImperialDistance(12.0f); + + private float _inches; + + public ImperialDistance(float m) + { + this._inches = m; + } + + public MetricDistance ToMetric() + { + return new MetricDistance(this._inches * 0.0254f); + } + + public float ToMeters() + { + return this.ToMetric().ToMeters(); + } + + public float ToInches() + { + return _inches; + } + + public override int GetHashCode() + { + return _inches.GetHashCode(); + } + + public override bool Equals(object obj) + { + var o = obj as ImperialDistance; + if (o == null) return false; + return _inches.Equals(o._inches); + } + + public static bool operator==(ImperialDistance a, ImperialDistance b) + { + // If both are null, or both are same instance, return true + if (ReferenceEquals(a, b)) return true; + + // if either one or the other are null, return false + if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false; + + return a._inches == b._inches; + } + + public static bool operator!=(ImperialDistance a, ImperialDistance b) + { + return !(a == b); + } + + public static ImperialDistance operator+(ImperialDistance a, ImperialDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new ImperialDistance(a._inches + b._inches); + } + + public static ImperialDistance Add(ImperialDistance a, ImperialDistance b) + { + return a + b; + } + + public static ImperialDistance operator-(ImperialDistance a, ImperialDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new ImperialDistance(a._inches - b._inches); + } + + public static ImperialDistance Subtract(ImperialDistance a, ImperialDistance b) + { + return a - b; + } + + public static ImperialDistance operator*(ImperialDistance a, ImperialDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new ImperialDistance(a._inches * b._inches); + } + + public static ImperialDistance Multiply(ImperialDistance a, ImperialDistance b) + { + return a * b; + } + + public static ImperialDistance operator/(ImperialDistance a, ImperialDistance b) + { + if (a == null) throw new ArgumentNullException("a"); + if (b == null) throw new ArgumentNullException("b"); + return new ImperialDistance(a._inches / b._inches); + } + + public static ImperialDistance Divide(ImperialDistance a, ImperialDistance b) + { + return a / b; + } + } + + //Extension methods must be defined in a static class + internal static class FloatExtension + { + public static MetricDistance Meters(this float that) + { + return new MetricDistance(that); + } + + public static MetricDistance Millimeters(this float that) + { + return new MetricDistance(MetricDistance.Millimeter.ToMeters() * that); + } + + public static MetricDistance Centimeters(this float that) + { + return new MetricDistance(MetricDistance.Centimeter.ToMeters() * that); + } + + public static ImperialDistance Inches(this float that) + { + return new ImperialDistance(that); + } + + public static ImperialDistance Feet(this float that) + { + return new ImperialDistance(ImperialDistance.Foot.ToInches() * that); + } + } + + //Extension methods must be defined in a static class + internal static class Vector3Extension + { + public static Vector3 RightHanded(this Vector3 leftHandedVector) + { + // negating the x component of the vector converts it from left to right handed coordinates + return new Vector3( + -leftHandedVector[0], + leftHandedVector[1], + leftHandedVector[2]); + } + + public static FbxVector4 FbxVector4(this Vector3 uniVector) + { + return new FbxVector4( + uniVector[0], + uniVector[1], + uniVector[2]); + } + } + + //Extension methods must be defined in a static class + internal static class AnimationCurveExtension + { + // This is an extension method for the AnimationCurve class + // The first parameter takes the "this" modifier + // and specifies the type for which the method is defined. + public static void Dump(this AnimationCurve animCurve, string message = "", float[] keyTimesExpected = null, float[] keyValuesExpected = null) + { + if (animCurve == null) + { + throw new System.ArgumentNullException("animCurve"); + } + + int idx = 0; + foreach (var key in animCurve.keys) + { + if (keyTimesExpected != null && keyValuesExpected != null && keyTimesExpected.Length == keyValuesExpected.Length) + { + Debug.Log(string.Format("{5} keys[{0}] {1}({3}) {2} ({4})", + idx, key.time, key.value, + keyTimesExpected[idx], keyValuesExpected[idx], + message)); + } + else + { + Debug.Log(string.Format("{3} keys[{0}] {1} {2}", idx, key.time, key.value, message)); + } + idx++; + } + } + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CustomExtensions.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CustomExtensions.cs.meta new file mode 100644 index 0000000..198ee27 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/CustomExtensions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1719f38fb4e4691439146f2d8d66cc22 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportPresetSelectorReceiver.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportPresetSelectorReceiver.cs new file mode 100644 index 0000000..f395d44 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportPresetSelectorReceiver.cs @@ -0,0 +1,55 @@ +#if UNITY_2018_1_OR_NEWER +using UnityEditor.Presets; + +namespace DaddyFrosty.Fbx +{ + public delegate void SelectionChangedDelegate(); + + public delegate void DialogClosedDelegate(); + + public class FbxExportPresetSelectorReceiver : PresetSelectorReceiver + { + UnityEngine.Object m_Target; + Preset m_InitialValue; + + public event SelectionChangedDelegate SelectionChanged; + public event DialogClosedDelegate DialogClosed; + + public override void OnSelectionClosed(Preset selection) + { + OnSelectionChanged(selection); + if (DialogClosed != null) + { + DialogClosed(); + } + DestroyImmediate(this); + } + + public override void OnSelectionChanged(Preset selection) + { + if (selection != null) + { + selection.ApplyTo(m_Target); + } + else + { + m_InitialValue.ApplyTo(m_Target); + } + if (SelectionChanged != null) + { + SelectionChanged(); + } + } + + public void SetTarget(UnityEngine.Object target) + { + m_Target = target; + } + + public void SetInitialValue(Preset initialValue) + { + m_InitialValue = initialValue; + } + } +} +#endif diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportPresetSelectorReceiver.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportPresetSelectorReceiver.cs.meta new file mode 100644 index 0000000..3242f28 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportPresetSelectorReceiver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d6d24eae8578a914a8169f974b2cc03c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportTimelineAction.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportTimelineAction.cs new file mode 100644 index 0000000..efac11c --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportTimelineAction.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using UnityEditor.Timeline.Actions; +using UnityEngine.Timeline; +using System.Linq; +using UnityEngine.Playables; +using UnityEditor.Timeline; + +namespace DaddyFrosty.Fbx +{ + [MenuEntry("Export Clip To FBX...", MenuPriority.CustomClipActionSection.start + MenuPriority.separatorAt), UsedImplicitly] + class FbxExportTimelineAction : ClipAction + { + public override bool Execute(IEnumerable clips) + { + PlayableDirector director = TimelineEditor.inspectedDirector; + ModelExporter.ExportSingleTimelineClip(clips.First(), director); + return true; + } + + public override ActionValidity Validate(IEnumerable clips) + { + if (clips.Count() != 1) + { + return ActionValidity.NotApplicable; + } + + // has to be an animation clip + if (clips.Any((clip) => { return clip.animationClip == null; })) + { + return ActionValidity.NotApplicable; + } + + return ActionValidity.Valid; + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportTimelineAction.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportTimelineAction.cs.meta new file mode 100644 index 0000000..bbddb5c --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExportTimelineAction.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 573e216414ac007408e419f5bd6f79b4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExporterRepairMissingScripts.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExporterRepairMissingScripts.cs new file mode 100644 index 0000000..4614183 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExporterRepairMissingScripts.cs @@ -0,0 +1,276 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using System.IO; +using UnityEditor; + +namespace DaddyFrosty.Fbx +{ + internal class RepairMissingScripts + { + private const string ForumPackageGUID = "2d81c55c4d9d85146b1d2de96e084b63"; + private const string AssetStorePackageGUID = "628ffbda3fdf4df4588770785d91a698"; + + private const string FbxPrefabDLLFileId = "69888640"; + + private const string IdFormat = "{{fileID: {0}, guid: {1}, type:"; + + private static List s_searchIDsToReplace; + private static List SearchIDsToReplace + { + get + { + if (s_searchIDsToReplace == null || s_searchIDsToReplace.Count <= 0) + { + s_searchIDsToReplace = new List() + { + string.Format(IdFormat, FbxPrefabDLLFileId, ForumPackageGUID), + string.Format(IdFormat, FbxPrefabDLLFileId, AssetStorePackageGUID) + }; + } + return s_searchIDsToReplace; + } + } + + private string[] m_assetsToRepair; + private string[] AssetsToRepair + { + get + { + if (m_assetsToRepair == null) + { + m_assetsToRepair = FindAssetsToRepair(); + } + return m_assetsToRepair; + } + } + + public static string SourceCodeSearchID + { + get + { + var fbxPrefabObj = AssetDatabase.LoadMainAssetAtPath(FindFbxPrefabAssetPath()); + string searchID = null; + string guid; + long fileId; + if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier(fbxPrefabObj, out guid, out fileId)) + { + searchID = string.Format(IdFormat, fileId, guid); + } + return searchID; + } + } + +#if COM_UNITY_FORMATS_FBX_AS_ASSET + public const string FbxPrefabFile = "/UnityFbxPrefab.dll"; +#else + public const string FbxPrefabFile = "Packages/com.unity.formats.fbx/Runtime/FbxPrefab.cs"; +#endif + public static string FindFbxPrefabAssetPath() + { +#if COM_UNITY_FORMATS_FBX_AS_ASSET + // Find guids that are scripts that look like FbxPrefab. + // That catches FbxPrefabTest too, so we have to make sure. + var allGuids = AssetDatabase.FindAssets("FbxPrefab t:MonoScript"); + string foundPath = ""; + foreach (var guid in allGuids) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + if (path.EndsWith(FbxPrefabFile)) + { + if (!string.IsNullOrEmpty(foundPath)) + { + // How did this happen? Anyway, just don't try. + Debug.LogWarning(string.Format("{0} found in multiple places; did you forget to delete one of these?\n{1}\n{2}", + FbxPrefabFile.Substring(1), foundPath, path)); + return ""; + } + foundPath = path; + } + } + if (string.IsNullOrEmpty(foundPath)) + { + Debug.LogWarning(string.Format("{0} not found; are you trying to uninstall {1}?", FbxPrefabFile.Substring(1), ModelExporter.PACKAGE_UI_NAME)); + } + return foundPath; +#else + // In Unity 2018.1 and 2018.2.0b7, FindAssets can't find FbxPrefab.cs in a package. + // So we hardcode the path. + var path = FbxPrefabFile; + if (System.IO.File.Exists(System.IO.Path.GetFullPath(path))) + { + return path; + } + else + { + Debug.LogWarningFormat("{0} not found; update FbxPrefabFile variable in FbxExporterRepairMissingScripts.cs to point to FbxPrefab.cs path.", FbxPrefabFile); + return ""; + } +#endif + } + + public int AssetsToRepairCount + { + get + { + return AssetsToRepair.Length; + } + } + + public string[] GetAssetsToRepair() + { + return AssetsToRepair; + } + + public static string[] FindAssetsToRepair() + { + // search project for assets containing old GUID + + // ignore if forced binary + if (UnityEditor.EditorSettings.serializationMode == SerializationMode.ForceBinary) + { + return new string[] {}; + } + + // check all scenes and prefabs + string[] searchFilePatterns = new string[] { "*.prefab", "*.unity" }; + + List assetsToRepair = new List(); + foreach (string searchPattern in searchFilePatterns) + { + foreach (string file in Directory.GetFiles(Application.dataPath, searchPattern, SearchOption.AllDirectories)) + { + if (AssetNeedsRepair(file)) + { + assetsToRepair.Add(file); + } + } + } + return assetsToRepair.ToArray(); + } + + private static bool AssetNeedsRepair(string filePath) + { + try + { + using (var sr = new StreamReader(filePath)) + { + if (sr.Peek() > -1) + { + var firstLine = sr.ReadLine(); + if (!firstLine.StartsWith("%YAML")) + { + return false; + } + } + + var contents = sr.ReadToEnd(); + if (SearchIDsToReplace.Exists(searchId => contents.Contains(searchId))) + { + return true; + } + } + } + catch (IOException e) + { + Debug.LogError(string.Format("Failed to check file for component update: {0} (error={1})", filePath, e)); + } + return false; + } + + public bool ReplaceGUIDInTextAssets() + { + string sourceCodeSearchID = SourceCodeSearchID; + if (string.IsNullOrEmpty(sourceCodeSearchID)) + { + return false; + } + bool replacedGUID = false; + foreach (string file in AssetsToRepair) + { + replacedGUID |= ReplaceGUIDInFile(file, sourceCodeSearchID); + } + if (replacedGUID) + { + AssetDatabase.Refresh(); + } + return replacedGUID; + } + + private static bool ReplaceID(string searchId, string replacementId, ref string line) + { + if (line.Contains(searchId)) + { + line = line.Replace(searchId, replacementId); + return true; + } + return false; + } + + private static bool ReplaceGUIDInFile(string path, string replacementSearchID) + { + // try to read file, assume it's a text file for now + bool modified = false; + + try + { + var tmpFile = Path.GetTempFileName(); + if (string.IsNullOrEmpty(tmpFile)) + { + return false; + } + + using (var sr = new StreamReader(path)) + { + // verify that this is a text file + var firstLine = ""; + if (sr.Peek() > -1) + { + firstLine = sr.ReadLine(); + if (!firstLine.StartsWith("%YAML")) + { + return false; + } + } + + using (var sw = new StreamWriter(tmpFile, false)) + { + if (!string.IsNullOrEmpty(firstLine)) + { + sw.WriteLine(firstLine); + } + + while (sr.Peek() > -1) + { + var line = sr.ReadLine(); + SearchIDsToReplace.ForEach(searchId => + modified |= ReplaceID(searchId, replacementSearchID, ref line) + ); + + sw.WriteLine(line); + } + } + } + + if (modified) + { + File.Delete(path); + File.Move(tmpFile, path); + + Debug.LogFormat("Updated FbxPrefab components in file {0}", path); + return true; + } + else + { + File.Delete(tmpFile); + } + } + catch (IOException e) + { + Debug.LogError(string.Format("Failed to replace GUID in file {0} (error={1})", path, e)); + } + + return false; + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExporterRepairMissingScripts.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExporterRepairMissingScripts.cs.meta new file mode 100644 index 0000000..58efe4e --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxExporterRepairMissingScripts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1dc7ecef19dfd6a4d84f3e2e890dfe19 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxPropertyChannelPair.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxPropertyChannelPair.cs new file mode 100644 index 0000000..27893ce --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxPropertyChannelPair.cs @@ -0,0 +1,375 @@ +using Autodesk.Fbx; +using System.Collections.Generic; + +namespace DaddyFrosty.Fbx +{ + /// + /// Store FBX property name and channel name + /// Default constructor added because it needs to be called before autoimplemented properties can be assigned. Otherwise we get build errors + /// + struct FbxPropertyChannelPair + { + public string Property { get; private set; } + public string Channel { get; private set; } + + public FbxPropertyChannelPair(string p, string c) : this() + { + Property = p; + Channel = c; + } + + struct UnityPropertyChannelPair + { + public string property; + public string channel; + + public UnityPropertyChannelPair(string p, string c) + { + property = p; + channel = c; + } + } + + /// + /// Contains the two lists that map Unity property to FBX property and Unity channel to Fbx channel + /// for a set of properties. + /// + struct PropertyChannelMap + { + public List<(string, string)> MapUnityPropToFbxProp; + public List<(string, string)> MapUnityChannelToFbxChannel; + + public PropertyChannelMap(List<(string, string)> propertyMap, List<(string, string)> channelMap) + { + MapUnityPropToFbxProp = propertyMap; + MapUnityChannelToFbxChannel = channelMap; + } + + private string GetFbxValue(string uniValue, List<(string, string)> list) + { + return list.Find(x => x.Item1 == uniValue).Item2; + } + + /// + /// Get the Fbx property name for the given Unity property name from the given list. + /// + /// + /// + /// The Fbx property name or null if there was no match in the list + public string GetFbxProperty(string uniProperty) + { + return GetFbxValue(uniProperty, MapUnityPropToFbxProp); + } + + /// + /// Get the Fbx channel name for the given Unity channel from the given list. + /// + /// + /// + /// The Fbx channel name or null if there was no match in the list + public string GetFbxChannel(string uniChannel) + { + return GetFbxValue(uniChannel, MapUnityChannelToFbxChannel); + } + } + + // =========== Property Maps ================ + // These are lists that map a Unity property name to it's corresponding Fbx property name. + // Split up into multiple lists as some are channel and object dependant. + + /// + /// Map of Unity transform properties to their FBX equivalent. + /// + private static List<(string, string)> MapTransformPropToFbxProp = new List<(string, string)>() + { + ("m_LocalScale", "Lcl Scaling"), + ("Motion S", "Lcl Scaling"), + ("m_LocalPosition", "Lcl Translation"), + ("Motion T", "Lcl Translation"), + ("m_TranslationOffset", "Translation"), + ("m_ScaleOffset", "Scaling"), + ("m_RotationOffset", "Rotation"), + ("localEulerAnglesRaw", "Lcl Rotation") + }; + + /// + /// Map of Unity Aim constraint properties to their FBX equivalent. + /// + private static List<(string, string)> MapAimConstraintPropToFbxProp = new List<(string, string)>() + { + ("m_AimVector", "AimVector"), + ("m_UpVector", "UpVector"), + ("m_WorldUpVector", "WorldUpVector"), + ("m_RotationOffset", "RotationOffset") + }; + + /// + /// Map of Unity color properties to their FBX equivalent. + /// + private static List<(string, string)> MapColorPropToFbxProp = new List<(string, string)>() + { + ("m_Color", "Color") + }; + + /// + /// Map of Unity properties to their FBX equivalent. + /// + private static List<(string, string)> MapPropToFbxProp = new List<(string, string)>() + { + ("m_Intensity", "Intensity"), + ("field of view", "FieldOfView"), + ("m_Weight", "Weight"), + ("m_FocalLength", "FocalLength"), + ("m_LensShift.x", "FilmOffsetX"), + ("m_LensShift.y", "FilmOffsetY") + }; + + /// + /// Map of Unity constraint source property name as a regular expression to the FBX property as a string format. + /// This is necessary because the Unity property contains an index in to an array, and the FBX property contains + /// the name of the source object. + /// + private static List<(string, string)> MapConstraintSourcePropToFbxProp = new List<(string, string)>() + { + (@"m_Sources\.Array\.data\[(\d+)\]\.weight", "{0}.Weight") + }; + + /// + /// Map of Unity constraint source transform property name as a regular expression to the FBX property as a string format. + /// This is necessary because the Unity property contains an index in to an array, and the FBX property contains + /// the name of the source object. + /// + private static List<(string, string)> MapConstraintSourceTransformPropToFbxProp = new List<(string, string)>() + { + (@"m_TranslationOffsets\.Array\.data\[(\d+)\]", "{0}.Offset T"), + (@"m_RotationOffsets\.Array\.data\[(\d+)\]", "{0}.Offset R") + }; + + /// + /// Map of Unity blendshape property name as a regular expression to the FBX property. + /// This is necessary because the Unity property contains the name of the target object. + /// + private static List<(string, string)> MapBlendshapesPropToFbxProp = new List<(string, string)>() + { + (@"blendShape\.(\S+)", "DeformPercent") + }; + + // ================== Channel Maps ====================== + + /// + /// Map of Unity transform channels to their FBX equivalent. + /// + private static List<(string, string)> MapTransformChannelToFbxChannel = new List<(string, string)>() + { + ("x", Globals.FBXSDK_CURVENODE_COMPONENT_X), + ("y", Globals.FBXSDK_CURVENODE_COMPONENT_Y), + ("z", Globals.FBXSDK_CURVENODE_COMPONENT_Z) + }; + + /// + /// Map of Unity color channels to their FBX equivalent. + /// + private static List<(string, string)> MapColorChannelToFbxChannel = new List<(string, string)>() + { + ("b", Globals.FBXSDK_CURVENODE_COLOR_BLUE), + ("g", Globals.FBXSDK_CURVENODE_COLOR_GREEN), + ("r", Globals.FBXSDK_CURVENODE_COLOR_RED) + }; + + // ======================================================= + + private static PropertyChannelMap TransformPropertyMap = new PropertyChannelMap(MapTransformPropToFbxProp, MapTransformChannelToFbxChannel); + private static PropertyChannelMap AimConstraintPropertyMap = new PropertyChannelMap(MapAimConstraintPropToFbxProp, MapTransformChannelToFbxChannel); + private static PropertyChannelMap ColorPropertyMap = new PropertyChannelMap(MapColorPropToFbxProp, MapColorChannelToFbxChannel); + private static PropertyChannelMap ConstraintSourcePropertyMap = new PropertyChannelMap(MapConstraintSourcePropToFbxProp, null); + private static PropertyChannelMap ConstraintSourceTransformPropertyMap = new PropertyChannelMap(MapConstraintSourceTransformPropToFbxProp, MapTransformChannelToFbxChannel); + private static PropertyChannelMap BlendshapeMap = new PropertyChannelMap(MapBlendshapesPropToFbxProp, null); + + private static PropertyChannelMap OtherPropertyMap = new PropertyChannelMap(MapPropToFbxProp, null); + + /// + /// Separates and returns the property and channel from the full Unity property name. + /// + /// Takes what is after the last period as the channel. + /// In order to use this have to be certain that there are channels, as there are cases where what is after + /// the last period is still the property name. E.g. m_Sources.Array.data[0].weight has no channel. + /// + /// + /// + private static UnityPropertyChannelPair GetUnityPropertyChannelPair(string fullPropertyName) + { + int index = fullPropertyName.LastIndexOf('.'); + if (index < 0) + { + return new UnityPropertyChannelPair(fullPropertyName, null); + } + + var property = fullPropertyName.Substring(0, index); + var channel = fullPropertyName.Substring(index + 1); + return new UnityPropertyChannelPair(property, channel); + } + + /// + /// Get the Fbx property name for the given Unity constraint source property name from the given list. + /// + /// This is different from GetFbxProperty() because the Unity constraint source properties contain indices, and + /// the Fbx constraint source property contains the name of the source object. + /// + /// + /// + /// + /// The Fbx property name or null if there was no match in the list + private static string GetFbxConstraintSourceProperty(string uniProperty, FbxConstraint constraint, List<(string, string)> propertyMap) + { + foreach (var prop in propertyMap) + { + var match = System.Text.RegularExpressions.Regex.Match(uniProperty, prop.Item1); + if (match.Success && match.Groups.Count > 0) + { + var matchedStr = match.Groups[1].Value; + int index; + if (!int.TryParse(matchedStr, out index)) + { + continue; + } + var source = constraint.GetConstraintSource(index); + return string.Format(prop.Item2, source.GetName()); + } + } + return null; + } + + /// + /// Get the Fbx property name for the given Unity blendshape property name from the given list. + /// + /// This is different from GetFbxProperty() because the Unity blendshape properties contain the name + /// of the target object. + /// + /// + /// + /// The Fbx property name or null if there was no match in the list + private static string GetFbxBlendshapeProperty(string uniProperty, List<(string, string)> propertyMap) + { + foreach (var prop in propertyMap) + { + var match = System.Text.RegularExpressions.Regex.Match(uniProperty, prop.Item1); + if (match.Success) + { + return prop.Item2; + } + } + return null; + } + + /// + /// Try to get the property channel pairs for the given Unity property from the given property channel mapping. + /// + /// + /// + /// + /// The property channel pairs or null if there was no match + private static FbxPropertyChannelPair[] GetChannelPairs(string uniPropertyName, PropertyChannelMap propertyChannelMap, FbxConstraint constraint = null) + { + // Unity property name is of the format "property.channel" or "property". Handle both cases. + var possibleUniPropChannelPairs = new List(); + + // could give same result as already in the list, avoid checking this case twice + var propChannelPair = GetUnityPropertyChannelPair(uniPropertyName); + possibleUniPropChannelPairs.Add(propChannelPair); + if (propChannelPair.property != uniPropertyName) + { + possibleUniPropChannelPairs.Add(new UnityPropertyChannelPair(uniPropertyName, null)); + } + + foreach (var uniPropChannelPair in possibleUniPropChannelPairs) + { + // try to match property + var fbxProperty = propertyChannelMap.GetFbxProperty(uniPropChannelPair.property); + if (string.IsNullOrEmpty(fbxProperty)) + { + if (constraint != null) + { + // check if it's a constraint source property + fbxProperty = GetFbxConstraintSourceProperty(uniPropChannelPair.property, constraint, propertyChannelMap.MapUnityPropToFbxProp); + } + else if (propertyChannelMap.MapUnityPropToFbxProp == MapBlendshapesPropToFbxProp) + { + // check if it's a blendshape property + fbxProperty = GetFbxBlendshapeProperty(uniPropChannelPair.property, propertyChannelMap.MapUnityPropToFbxProp); + } + } + + if (string.IsNullOrEmpty(fbxProperty)) + { + continue; + } + + // matched property, now try to match channel + string fbxChannel = null; + if (!string.IsNullOrEmpty(uniPropChannelPair.channel) && propertyChannelMap.MapUnityChannelToFbxChannel != null) + { + fbxChannel = propertyChannelMap.GetFbxChannel(uniPropChannelPair.channel); + if (string.IsNullOrEmpty(fbxChannel)) + { + // couldn't match the Unity channel to the fbx channel + continue; + } + } + return new FbxPropertyChannelPair[] { new FbxPropertyChannelPair(fbxProperty, fbxChannel) }; + } + return null; + } + + /// + /// Map a Unity property name to the corresponding FBX property and + /// channel names. + /// + public static bool TryGetValue(string uniPropertyName, out FbxPropertyChannelPair[] prop, FbxConstraint constraint = null) + { + prop = new FbxPropertyChannelPair[] {}; + + // spot angle is a special case as it returns two channel pairs instead of one + System.StringComparison ct = System.StringComparison.CurrentCulture; + if (uniPropertyName.StartsWith("m_SpotAngle", ct)) + { + prop = new FbxPropertyChannelPair[] + { + new FbxPropertyChannelPair("OuterAngle", null), + new FbxPropertyChannelPair("InnerAngle", null) + }; + return true; + } + + var propertyMaps = new List(); + + // Try get constraint specific channel pairs first as we know this is a constraint + if (constraint != null) + { + // Aim constraint shares the RotationOffset property with RotationConstraint, so make sure that the correct FBX property is returned + if (constraint.GetConstraintType() == FbxConstraint.EType.eAim) + { + propertyMaps.Add(AimConstraintPropertyMap); + } + + propertyMaps.Add(ConstraintSourcePropertyMap); + propertyMaps.Add(ConstraintSourceTransformPropertyMap); + } + + // Check if this is a transform, color, or other property and return the channel pairs if they match. + propertyMaps.Add(TransformPropertyMap); + propertyMaps.Add(ColorPropertyMap); + propertyMaps.Add(OtherPropertyMap); + propertyMaps.Add(BlendshapeMap); + + foreach (var propMap in propertyMaps) + { + prop = GetChannelPairs(uniPropertyName, propMap, constraint); + if (prop != null) + { + return true; + } + } + return false; + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxPropertyChannelPair.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxPropertyChannelPair.cs.meta new file mode 100644 index 0000000..3529a4a --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxPropertyChannelPair.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a6b42abf99d4bdf4c8798184fb5c936d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxRotationCurve.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxRotationCurve.cs new file mode 100644 index 0000000..d036110 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxRotationCurve.cs @@ -0,0 +1,276 @@ +using Autodesk.Fbx; +using UnityEngine; +using System.Collections.Generic; + +namespace DaddyFrosty.Fbx +{ + /// + /// Base class for QuaternionCurve and EulerCurve. + /// Provides implementation for computing keys and generating FbxAnimCurves + /// for euler rotation. + /// + internal abstract class RotationCurve + { + private double m_sampleRate; + public double SampleRate + { + get { return m_sampleRate; } + set { m_sampleRate = value; } + } + + private AnimationCurve[] m_curves; + public AnimationCurve[] GetCurves() { return m_curves; } + public void SetCurves(AnimationCurve[] value) { m_curves = value; } + + protected struct Key + { + private FbxTime m_time; + public FbxTime time + { + get { return m_time; } + set { m_time = value; } + } + private FbxVector4 m_euler; + public FbxVector4 euler + { + get { return m_euler; } + set { m_euler = value; } + } + } + + protected RotationCurve() {} + + public void SetCurve(int i, AnimationCurve curve) + { + GetCurves()[i] = curve; + } + + protected abstract FbxQuaternion GetConvertedQuaternionRotation(float seconds, UnityEngine.Quaternion restRotation); + + private Key[] ComputeKeys(UnityEngine.Quaternion restRotation, FbxNode node) + { + // Get the source pivot pre-rotation if any, so we can + // remove it from the animation we get from Unity. + var fbxPreRotationEuler = node.GetRotationActive() + ? node.GetPreRotation(FbxNode.EPivotSet.eSourcePivot) + : new FbxVector4(); + + // Get the inverse of the prerotation + var fbxPreRotationInverse = ModelExporter.EulerToQuaternionXYZ(fbxPreRotationEuler); + fbxPreRotationInverse.Inverse(); + + // Find when we have keys set. + var keyTimes = ModelExporter.GetSampleTimes(GetCurves(), SampleRate); + + // Convert to the Key type. + var keys = new Key[keyTimes.Count]; + int i = 0; + foreach (var seconds in keyTimes) + { + var fbxFinalAnimation = GetConvertedQuaternionRotation(seconds, restRotation); + + // Cancel out the pre-rotation. Order matters. FBX reads left-to-right. + // When we run animation we will apply: + // pre-rotation + // then pre-rotation inverse + // then animation. + var fbxFinalQuat = fbxPreRotationInverse * fbxFinalAnimation; + + var finalUnityQuat = new Quaternion((float)fbxFinalQuat.X, (float)fbxFinalQuat.Y, (float)fbxFinalQuat.Z, (float)fbxFinalQuat.W); + + // Store the key so we can sort them later. + Key key = new Key(); + key.time = FbxTime.FromSecondDouble(seconds); + key.euler = ModelExporter.ConvertToFbxVector4(finalUnityQuat.eulerAngles); + keys[i++] = key; + } + + // Sort the keys by time + System.Array.Sort(keys, (Key a, Key b) => a.time.CompareTo(b.time)); + + return keys; + } + + public void Animate(Transform unityTransform, FbxNode fbxNode, FbxAnimLayer fbxAnimLayer, bool Verbose) + { + if (!unityTransform || fbxNode == null) + { + return; + } + + /* Find or create the three curves. */ + var fbxAnimCurveX = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_X, true); + var fbxAnimCurveY = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_Y, true); + var fbxAnimCurveZ = fbxNode.LclRotation.GetCurve(fbxAnimLayer, Globals.FBXSDK_CURVENODE_COMPONENT_Z, true); + + /* set the keys */ + using (new FbxAnimCurveModifyHelper(new List {fbxAnimCurveX, fbxAnimCurveY, fbxAnimCurveZ})) + { + foreach (var key in ComputeKeys(unityTransform.localRotation, fbxNode)) + { + int i = fbxAnimCurveX.KeyAdd(key.time); + fbxAnimCurveX.KeySet(i, key.time, (float)key.euler.X); + + i = fbxAnimCurveY.KeyAdd(key.time); + fbxAnimCurveY.KeySet(i, key.time, (float)key.euler.Y); + + i = fbxAnimCurveZ.KeyAdd(key.time); + fbxAnimCurveZ.KeySet(i, key.time, (float)key.euler.Z); + } + } + + if (Verbose) + { + Debug.Log("Exported rotation animation for " + fbxNode.GetName()); + } + } + } + + /// + /// Convert from ZXY to XYZ euler, and remove + /// prerotation from animated rotation. + /// + internal class EulerCurve : RotationCurve + { + public EulerCurve() { SetCurves(new AnimationCurve[3]); } + + /// + /// Gets the index of the euler curve by property name. + /// x = 0, y = 1, z = 2 + /// + /// The index of the curve, or -1 if property doesn't map to Euler curve. + /// Unity property name. + public static int GetEulerIndex(string uniPropertyName) + { + if (string.IsNullOrEmpty(uniPropertyName)) + { + return -1; + } + + System.StringComparison ct = System.StringComparison.CurrentCulture; + bool isEulerComponent = uniPropertyName.StartsWith("localEulerAnglesRaw.", ct); + + if (!isEulerComponent) { return -1; } + + switch (uniPropertyName[uniPropertyName.Length - 1]) + { + case 'x': return 0; + case 'y': return 1; + case 'z': return 2; + default: return -1; + } + } + + protected override FbxQuaternion GetConvertedQuaternionRotation(float seconds, Quaternion restRotation) + { + var eulerRest = restRotation.eulerAngles; + AnimationCurve x = GetCurves()[0], y = GetCurves()[1], z = GetCurves()[2]; + + // The final animation, including the effect of pre-rotation. + // If we have no curve, assume the node has the correct rotation right now. + // We need to evaluate since we might only have keys in one of the axes. + var unityFinalAnimation = new Vector3( + (x == null) ? eulerRest[0] : x.Evaluate(seconds), + (y == null) ? eulerRest[1] : y.Evaluate(seconds), + (z == null) ? eulerRest[2] : z.Evaluate(seconds) + ); + + // convert the final animation to righthanded coords + + return ModelExporter.EulerToQuaternionZXY(unityFinalAnimation); + } + } + + /// + /// Exporting rotations is more complicated. We need to convert + /// from quaternion to euler. We use this class to help. + /// + internal class QuaternionCurve : RotationCurve + { + public QuaternionCurve() { SetCurves(new AnimationCurve[4]); } + + /// + /// Gets the index of the curve by property name. + /// x = 0, y = 1, z = 2, w = 3 + /// + /// The index of the curve, or -1 if property doesn't map to Quaternion curve. + /// Unity property name. + public static int GetQuaternionIndex(string uniPropertyName) + { + if (string.IsNullOrEmpty(uniPropertyName)) + { + return -1; + } + + System.StringComparison ct = System.StringComparison.CurrentCulture; + bool isQuaternionComponent = false; + + isQuaternionComponent |= uniPropertyName.StartsWith("m_LocalRotation.", ct); + isQuaternionComponent |= uniPropertyName.EndsWith("Q.x", ct); + isQuaternionComponent |= uniPropertyName.EndsWith("Q.y", ct); + isQuaternionComponent |= uniPropertyName.EndsWith("Q.z", ct); + isQuaternionComponent |= uniPropertyName.EndsWith("Q.w", ct); + + if (!isQuaternionComponent) { return -1; } + + switch (uniPropertyName[uniPropertyName.Length - 1]) + { + case 'x': return 0; + case 'y': return 1; + case 'z': return 2; + case 'w': return 3; + default: return -1; + } + } + + protected override FbxQuaternion GetConvertedQuaternionRotation(float seconds, Quaternion restRotation) + { + AnimationCurve x = GetCurves()[0], y = GetCurves()[1], z = GetCurves()[2], w = GetCurves()[3]; + + // The final animation, including the effect of pre-rotation. + // If we have no curve, assume the node has the correct rotation right now. + // We need to evaluate since we might only have keys in one of the axes. + var fbxFinalAnimation = new FbxQuaternion( + (x == null) ? restRotation[0] : x.Evaluate(seconds), + (y == null) ? restRotation[1] : y.Evaluate(seconds), + (z == null) ? restRotation[2] : z.Evaluate(seconds), + (w == null) ? restRotation[3] : w.Evaluate(seconds)); + + return fbxFinalAnimation; + } + } + + /// + /// Exporting rotations is more complicated. We need to convert + /// from quaternion to euler. We use this class to help. + /// + internal class FbxAnimCurveModifyHelper : System.IDisposable + { + public List Curves { get; private set; } + + public FbxAnimCurveModifyHelper(List list) + { + Curves = list; + + foreach (var curve in Curves) + curve.KeyModifyBegin(); + } + + ~FbxAnimCurveModifyHelper() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + System.GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool cleanUpManaged) + { + foreach (var curve in Curves) + curve.KeyModifyEnd(); + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxRotationCurve.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxRotationCurve.cs.meta new file mode 100644 index 0000000..88f95d2 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/FbxRotationCurve.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1df2cf4a1fa06b64bac747b1d3730d6c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/InstallIntegration.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/InstallIntegration.cs new file mode 100644 index 0000000..2bcbe3a --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/InstallIntegration.cs @@ -0,0 +1,991 @@ +using UnityEngine; +using System; +using System.Collections.Generic; +using System.Security.Permissions; +using UnityEditor; + +namespace DaddyFrosty.Fbx +{ + internal abstract class DCCIntegration + { + public abstract string DccDisplayName { get; } + public abstract string IntegrationZipPath { get; } + + private static string s_integrationFolderPath = null; + public static string IntegrationFolderPath + { + get + { + if (string.IsNullOrEmpty(s_integrationFolderPath)) + { + s_integrationFolderPath = Application.dataPath; + } + return s_integrationFolderPath; + } + set + { + if (!string.IsNullOrEmpty(value) && System.IO.Directory.Exists(value)) + { + s_integrationFolderPath = value; + } + else + { + Debug.LogError(string.Format("Failed to set integration folder path, invalid directory \"{0}\"", value)); + } + } + } + + public void SetIntegrationFolderPath(string path) + { + IntegrationFolderPath = path; + } + + /// + /// Gets the integration zip full path as an absolute Unity-style path. + /// + /// The integration zip full path. + public string IntegrationZipFullPath + { + get + { + return System.IO.Path.GetFullPath("Packages/com.unity.formats.fbx/Editor/Integrations~").Replace("\\", "/") + "/" + IntegrationZipPath; + } + } + + /// + /// Gets the project path. + /// + /// The project path. + public static string ProjectPath + { + get + { + return System.IO.Directory.GetParent(Application.dataPath).FullName.Replace("\\", "/"); + } + } + + /// + /// Installs the integration using the provided executable. + /// + /// The integration. + /// Exe. + [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public abstract int InstallIntegration(string exe); + + /// + /// Determines if folder is already unzipped at the specified path. + /// + /// true if folder is already unzipped at the specified path; otherwise, false. + /// Path. + public abstract bool FolderAlreadyUnzippedAtPath(string path); + + /// + /// Launches application at given path + /// + /// + [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public static void LaunchDCCApplication(string AppPath) + { + System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); + myProcess.StartInfo.FileName = AppPath; + myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; + myProcess.StartInfo.CreateNoWindow = false; + myProcess.StartInfo.UseShellExecute = false; + + myProcess.EnableRaisingEvents = false; + myProcess.Start(); + } + } + + + internal class MayaIntegration : DCCIntegration + { + public override string DccDisplayName { get { return "Maya"; } } + + public override string IntegrationZipPath { get { return "UnityFbxForMaya.7z"; } } + + private string FBX_EXPORT_SETTINGS_PATH { get { return "/Integrations/Autodesk/maya/scripts/unityFbxExportSettings.mel"; } } + + private string FBX_IMPORT_SETTINGS_PATH { get { return "/Integrations/Autodesk/maya/scripts/unityFbxImportSettings.mel"; } } + + private string MODULE_TEMPLATE_PATH { get { return "Integrations/Autodesk/maya/" + MODULE_FILENAME + ".txt"; } } + private string MODULE_FILENAME { get { return "UnityFbxForMaya"; } } + + private const string PACKAGE_NAME = "com.unity.formats.fbx"; + private const string VERSION_FIELD = "VERSION"; + private const string VERSION_TAG = "{Version}"; + private const string PROJECT_TAG = "{UnityProject}"; + private const string INTEGRATION_TAG = "{UnityIntegrationsPath}"; + + private const string MAYA_USER_STARTUP_SCRIPT = "userSetup.mel"; + + private const string UI_SETUP_FUNCTION = "unitySetupUI"; + private string USER_STARTUP_CALL { get { return string.Format("if(`exists {0}`){{ {0}; }}", UI_SETUP_FUNCTION); } } + + private static string MAYA_DOCUMENTS_PATH + { + get + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + return "maya"; + case RuntimePlatform.OSXEditor: + return "Library/Preferences/Autodesk/Maya"; + default: + throw new NotImplementedException(); + } + } + } + + private static string MAYA_MODULES_PATH + { + get + { + return System.IO.Path.Combine(UserFolder, MAYA_DOCUMENTS_PATH + "/modules"); + } + } + + private static string MAYA_SCRIPTS_PATH + { + get + { + return System.IO.Path.Combine(UserFolder, MAYA_DOCUMENTS_PATH + "/scripts"); + } + } + + // Use string to define escaped quote + // Windows needs the backslash + protected static string EscapedQuote + { + get + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + return "\\\""; + case RuntimePlatform.OSXEditor: + return "\""; + default: + throw new NotSupportedException(); + } + } + } + + protected string MayaConfigCommand + { + get + { + return string.Format("unityConfigure {0}{1}{0} {0}{2}{0} {0}{3}{0} {4} {5};", + EscapedQuote, ProjectPath, ExportSettingsPath, ImportSettingsPath, (IsHeadlessInstall()), (HideSendToUnityMenu)); + } + } + + private string MAYA_CLOSE_COMMAND + { + get + { + return string.Format("scriptJob -idleEvent quit;"); + } + } + + protected static string UserFolder + { + get + { + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + return System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal); + case RuntimePlatform.OSXEditor: + return System.Environment.GetEnvironmentVariable("HOME"); + default: + throw new NotSupportedException(); + } + } + } + + public static int IsHeadlessInstall() + { + return 0; + } + + public static int HideSendToUnityMenu + { + get + { + return ExportSettings.instance.HideSendToUnityMenuProperty ? 1 : 0; + } + } + + public string ModuleTemplatePath + { + get + { + return System.IO.Path.Combine(IntegrationFolderPath, MODULE_TEMPLATE_PATH); + } + } + + public static string PackagePath + { + get + { + return System.IO.Path.Combine(Application.dataPath, PACKAGE_NAME); + } + } + + /// + /// Gets the path to the export settings file. + /// Returns a relative path with forward slashes as path separators. + /// + /// The export settings path. + public string ExportSettingsPath + { + get + { + return IntegrationFolderPath + FBX_EXPORT_SETTINGS_PATH; + } + } + + /// + /// Gets the path to the import settings file. + /// Returns a relative path with forward slashes as path separators. + /// + /// The import settings path. + public string ImportSettingsPath + { + get + { + return IntegrationFolderPath + FBX_IMPORT_SETTINGS_PATH; + } + } + + /// + /// Gets the user startup script path. + /// Returns a relative path with forward slashes as path separators. + /// + /// The user startup script path. + private static string GetUserStartupScriptPath() + { + return MAYA_SCRIPTS_PATH + "/" + MAYA_USER_STARTUP_SCRIPT; + } + + public static string PackageVersion + { + get + { + return ModelExporter.GetVersionFromReadme(); + } + } + + private static List ParseTemplateFile(string FileName, Dictionary Tokens) + { + List lines = new List(); + + try + { + // Pass the file path and file name to the StreamReader constructor + System.IO.StreamReader sr = new System.IO.StreamReader(FileName); + + // Read the first line of text + string line = sr.ReadLine(); + + // Continue to read until you reach end of file + while (line != null) + { + foreach (KeyValuePair entry in Tokens) + { + line = line.Replace(entry.Key, entry.Value); + } + lines.Add(line); + + //Read the next line + line = sr.ReadLine(); + } + + //close the file + sr.Close(); + } + catch (Exception e) + { + Debug.LogError(string.Format("Exception reading module file template ({0})", e.Message)); + } + + return lines; + } + + private static void WriteFile(string FileName, List Lines) + { + try + { + //Pass the filepath and filename to the StreamWriter Constructor + System.IO.StreamWriter sw = new System.IO.StreamWriter(FileName); + + foreach (string line in Lines) + { + //Write a line of text + sw.WriteLine(line); + } + + //Close the file + sw.Close(); + } + catch (Exception e) + { + Debug.LogException(e); + Debug.LogError(string.Format("Exception while writing module file ({0})", e.Message)); + } + } + + /// + /// Creates the missing directories in path. + /// + /// true, if directory was created, false otherwise. + /// Path to create. + protected static bool CreateDirectory(string path) + { + try + { + System.IO.Directory.CreateDirectory(path); + } + catch (Exception xcp) + { + Debug.LogException(xcp); + return false; + } + + if (!System.IO.Directory.Exists(path)) + { + return false; + } + return true; + } + + [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public int ConfigureMaya(string mayaPath) + { + int ExitCode = 0; + + try + { + if (!System.IO.File.Exists(mayaPath)) + { + Debug.LogError(string.Format("No maya installation found at {0}", mayaPath)); + return -1; + } + + System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); + myProcess.StartInfo.FileName = mayaPath; + myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; + myProcess.StartInfo.CreateNoWindow = true; + myProcess.StartInfo.UseShellExecute = false; + + if (!ExportSettings.instance.LaunchAfterInstallation) + { + myProcess.StartInfo.RedirectStandardError = true; + } + + string commandString; + + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + commandString = "-command \"{0}\""; + break; + case RuntimePlatform.OSXEditor: + commandString = @"-command '{0}'"; + break; + default: + throw new NotImplementedException(); + } + + if (ExportSettings.instance.LaunchAfterInstallation) + { + myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal; + myProcess.StartInfo.CreateNoWindow = false; + myProcess.StartInfo.Arguments = string.Format(commandString, MayaConfigCommand); + } + else + { + myProcess.StartInfo.Arguments = string.Format(commandString, MayaConfigCommand + MAYA_CLOSE_COMMAND); + } + + myProcess.EnableRaisingEvents = true; + myProcess.Start(); + + if (!ExportSettings.instance.LaunchAfterInstallation) + { + string stderr = myProcess.StandardError.ReadToEnd(); + myProcess.WaitForExit(); + ExitCode = myProcess.ExitCode; + Debug.Log(string.Format("Ran maya: [{0}]\nWith args [{1}]\nResult {2}", + mayaPath, myProcess.StartInfo.Arguments, ExitCode)); + + // see if we got any error messages + if (ExitCode != 0) + { + if (!string.IsNullOrEmpty(stderr)) + { + Debug.LogError(string.Format("Maya installation error (exit code: {0}): {1}", ExitCode, stderr)); + } + } + } + else + { + ExitCode = 0; + } + } + catch (Exception e) + { + UnityEngine.Debug.LogError(string.Format("Exception failed to start Maya ({0})", e.Message)); + ExitCode = -1; + } + return ExitCode; + } + + public bool InstallMaya(bool verbose = false) + { + // What's happening here is that we copy the module template to + // the module path, basically: + // - copy the template to the user Maya module path + // - search-and-replace its tags + // - done. + // But it's complicated because we can't trust any files actually exist. + + string moduleTemplatePath = ModuleTemplatePath; + if (!System.IO.File.Exists(moduleTemplatePath)) + { + Debug.LogError(string.Format("Missing Maya module file at: \"{0}\"", moduleTemplatePath)); + return false; + } + + // Create the {USER} modules folder and empty it so it's ready to set up. + string modulePath = MAYA_MODULES_PATH; + string moduleFilePath = System.IO.Path.Combine(modulePath, MODULE_FILENAME + ".mod"); + bool installed = false; + + if (!System.IO.Directory.Exists(modulePath)) + { + if (verbose) { Debug.Log(string.Format("Creating Maya Modules Folder {0}", modulePath)); } + if (!CreateDirectory(modulePath)) + { + Debug.LogError(string.Format("Failed to create Maya Modules Folder {0}", modulePath)); + return false; + } + installed = false; + } + else + { + // detect if UnityFbxForMaya.mod is installed + installed = System.IO.File.Exists(moduleFilePath); + + if (installed) + { + // (Uni-31606): remove this when we support parsing existing .mod files + try + { + if (verbose) { Debug.Log(string.Format("Deleting module file {0}", moduleFilePath)); } + System.IO.File.Delete(moduleFilePath); + installed = false; + } + catch (Exception xcp) + { + Debug.LogException(xcp); + Debug.LogWarning(string.Format("Failed to delete plugin module file {0}", moduleFilePath)); + } + } + } + + // if not installed + if (!installed) + { + Dictionary Tokens = new Dictionary() + { + {VERSION_TAG, PackageVersion }, + {PROJECT_TAG, ProjectPath }, + {INTEGRATION_TAG, IntegrationFolderPath }, + }; + + // parse template, replace "{UnityProject}" with project path + List lines = ParseTemplateFile(moduleTemplatePath, Tokens); + + if (verbose) Debug.Log(string.Format("Copying plugin module file to {0}", moduleFilePath)); + + // write out .mod file + WriteFile(moduleFilePath, lines); + } + else + { + throw new NotImplementedException(); + + // (Uni-31606) Parse maya mod file during installation and find location + } + + return SetupUserStartupScript(verbose); + } + + private bool SetupUserStartupScript(bool verbose = false) + { + // setup user startup script + string mayaStartupScript = GetUserStartupScriptPath(); + string fileContents = string.Format("\n{0}", USER_STARTUP_CALL); + + // make sure scripts directory exists + if (!System.IO.Directory.Exists(MAYA_SCRIPTS_PATH)) + { + if (verbose) { Debug.Log(string.Format("Creating Maya Scripts Folder {0}", MAYA_SCRIPTS_PATH)); } + if (!CreateDirectory(MAYA_SCRIPTS_PATH)) + { + Debug.LogError(string.Format("Failed to create Maya Scripts Folder {0}", MAYA_SCRIPTS_PATH)); + return false; + } + } + else if (System.IO.File.Exists(mayaStartupScript)) + { + // script exists, check that the UI setup is being called + try + { + using (System.IO.StreamReader sr = new System.IO.StreamReader(mayaStartupScript)) + { + while (sr.Peek() >= 0) + { + string line = sr.ReadLine(); + if (line.Trim().Contains(UI_SETUP_FUNCTION)) + { + // startup call already in the file, nothing to do + return true; + } + } + } + } + catch (Exception e) + { + Debug.LogException(e); + Debug.LogError(string.Format("Exception while reading user startup file ({0})", e.Message)); + return false; + } + } + + // append text to file + try + { + System.IO.File.AppendAllText(mayaStartupScript, fileContents); + } + catch (Exception e) + { + Debug.LogException(e); + Debug.LogError(string.Format("Exception while writing to user startup file ({0})", e.Message)); + return false; + } + return true; + } + + [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public override int InstallIntegration(string exe) + { + if (!InstallMaya(verbose: true)) + { + return -1; + } + + return ConfigureMaya(exe); + } + + /// + /// Determines if folder is already unzipped at the specified path + /// by checking if UnityFbxForMaya.mod exists at expected location. + /// + /// true if folder is already unzipped at the specified path; otherwise, false. + /// Path. + public override bool FolderAlreadyUnzippedAtPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + return System.IO.File.Exists(System.IO.Path.Combine(path, MODULE_TEMPLATE_PATH)); + } + } + + internal class MayaLTIntegration : MayaIntegration + { + public override string DccDisplayName { get { return "Maya LT"; } } + } + + internal class MaxIntegration : DCCIntegration + { + public override string DccDisplayName { get { return "3Ds Max"; } } + + private const string MaxScriptsPath = "Integrations/Autodesk/max/scripts/"; + + private const string PluginName = "UnityFbxForMaxPlugin.ms"; + public const string PluginPath = MaxScriptsPath + PluginName; + + private const string ConfigureMaxScript = MaxScriptsPath + "configureUnityFbxForMax.ms"; + + private const string ExportSettingsFile = MaxScriptsPath + "unityFbxExportSettings.ms"; + private const string ImportSettingsFile = MaxScriptsPath + "unityFbxImportSettings.ms"; + + private const string PluginSourceTag = "UnityPluginScript_Source"; + private const string PluginNameTag = "UnityPluginScript_Name"; + private const string ProjectTag = "UnityProject"; + private const string ExportSettingsTag = "UnityFbxExportSettings"; + private const string ImportSettingsTag = "UnityFbxImportSettings"; + + public override string IntegrationZipPath { get { return "UnityFbxForMax.7z"; } } + + /// + /// Gets the absolute Unity path for relative path in Integrations folder. + /// + /// The absolute path. + /// Relative path. + public static string GetAbsPath(string relPath) + { + return MayaIntegration.IntegrationFolderPath + "/" + relPath; + } + + private static string GetInstallScript() + { + Dictionary Tokens = new Dictionary() + { + {PluginSourceTag, GetAbsPath(PluginPath) }, + {PluginNameTag, PluginName }, + {ProjectTag, ProjectPath }, + {ExportSettingsTag, GetAbsPath(ExportSettingsFile) }, + {ImportSettingsTag, GetAbsPath(ImportSettingsFile) } + }; + + var installScript = ""; + // setup the variables to be used in the configure max script + foreach (var t in Tokens) + { + installScript += string.Format(@"global {0} = @\""{1}\"";", t.Key, t.Value); + } + installScript += string.Format(@"filein \""{0}\""", GetAbsPath(ConfigureMaxScript)); + return installScript; + } + + [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public static int InstallMaxPlugin(string maxExe) + { + if (Application.platform != RuntimePlatform.WindowsEditor) + { + Debug.LogError("The 3DsMax Unity plugin is Windows only, please try installing a Maya plugin instead"); + return -1; + } + + var installScript = GetInstallScript(); + + int ExitCode = 0; + + try + { + if (!System.IO.File.Exists(maxExe)) + { + Debug.LogError(string.Format("No 3DsMax installation found at {0}", maxExe)); + return -1; + } + + System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); + myProcess.StartInfo.FileName = maxExe; + myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; + myProcess.StartInfo.CreateNoWindow = true; + myProcess.StartInfo.UseShellExecute = false; + myProcess.StartInfo.RedirectStandardOutput = true; + + myProcess.StartInfo.Arguments = string.Format("-q -silent -mxs \"{0}\"", installScript); + + myProcess.EnableRaisingEvents = true; + myProcess.Start(); + string stderr = myProcess.StandardOutput.ReadToEnd(); + myProcess.WaitForExit(); + ExitCode = myProcess.ExitCode; + + if (ExportSettings.instance.LaunchAfterInstallation) + { + LaunchDCCApplication(maxExe); + } + + // TODO (UNI-29910): figure out what exactly causes this exit code + how to resolve + if (ExitCode == -1073740791) + { + Debug.Log(string.Format("Detected 3ds max exitcode {0} -- safe to ignore", ExitCode)); + ExitCode = 0; + } + + // print any errors + if (ExitCode != 0) + { + if (!string.IsNullOrEmpty(stderr)) + { + Debug.LogError(string.Format("3ds Max installation error (exit code: {0}): {1}", ExitCode, stderr)); + } + } + + Debug.Log(string.Format("Ran max: [{0}]\nWith args [{1}]\nResult {2}", + maxExe, myProcess.StartInfo.Arguments, ExitCode)); + } + catch (Exception e) + { + UnityEngine.Debug.LogError(string.Format("Exception failed to start Max ({0})", e.Message)); + ExitCode = -1; + } + return ExitCode; + } + + [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)] + public override int InstallIntegration(string exe) + { + return MaxIntegration.InstallMaxPlugin(exe); + } + + /// + /// Determines if folder is already unzipped at the specified path + /// by checking if plugin exists at expected location. + /// + /// true if folder is already unzipped at the specified path; otherwise, false. + /// Path. + public override bool FolderAlreadyUnzippedAtPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + return System.IO.File.Exists(System.IO.Path.Combine(path, MaxIntegration.PluginPath)); + } + } + + static class IntegrationsUI + { + /// + /// The path of the DCC executable. + /// + public static string GetDCCExe() + { + return ExportSettings.SelectedDCCPath; + } + + /// + /// Gets the name of the selected DCC. + /// + /// The DCC name. + public static string GetDCCName() + { + return ExportSettings.SelectedDCCName; + } + + /// + /// Opens a dialog showing whether the installation succeeded. + /// + /// Dcc name. + private static void ShowSuccessDialog(string dcc, int exitCode) + { + string title, message, customMessage; + if (exitCode != 0) + { + title = string.Format("Failed to install {0} Integration.", dcc); + message = string.Format("Failed to configure {0}, please check logs (exitcode={1}).", dcc, exitCode); + } + else + { + if (ExportSettings.instance.LaunchAfterInstallation) + { + customMessage = "Installing Unity menu in {0}, application will open once installation is complete"; + } + else + { + customMessage = "Enjoy the new Unity menu in {0}."; + } + title = string.Format("Completing installation of {0} Integration.", dcc); + message = string.Format(customMessage, dcc); + } + UnityEditor.EditorUtility.DisplayDialog(title, message, "Ok"); + } + + public static void InstallDCCIntegration() + { + var dccExe = GetDCCExe(); + if (string.IsNullOrEmpty(dccExe)) + { + return; + } + + string dccType = System.IO.Path.GetFileNameWithoutExtension(dccExe).ToLower(); + DCCIntegration dccIntegration; + if (dccType.Equals("maya")) + { + // could be Maya or Maya LT + if (GetDCCName().ToLower().Contains("lt")) + { + dccIntegration = new MayaLTIntegration(); + } + else + { + dccIntegration = new MayaIntegration(); + } + } + else if (dccType.Equals("3dsmax")) + { + dccIntegration = new MaxIntegration(); + } + else + { + throw new System.NotImplementedException(); + } + + if (!GetIntegrationFolder(dccIntegration)) + { + // failed to get integration folder + return; + } + int exitCode = dccIntegration.InstallIntegration(dccExe); + ShowSuccessDialog(dccIntegration.DccDisplayName, exitCode); + } + + private static bool GetIntegrationFolder(DCCIntegration dcc) + { + // decompress zip file if it exists, otherwise try using default location + var zipPath = dcc.IntegrationZipFullPath; + if (System.IO.File.Exists(zipPath)) + { + return DecompressIntegrationZipFile(zipPath, dcc); + } + dcc.SetIntegrationFolderPath(ExportSettings.IntegrationSavePath); + return true; + } + + private static bool DecompressIntegrationZipFile(string zipPath, DCCIntegration dcc) + { + // prompt user to enter location to unzip file + var unzipFolder = EditorUtility.OpenFolderPanel(string.Format("Select Location to Save {0} Integration", dcc.DccDisplayName), ExportSettings.IntegrationSavePath, ""); + if (string.IsNullOrEmpty(unzipFolder)) + { + // user has cancelled, do nothing + return false; + } + + ExportSettings.IntegrationSavePath = unzipFolder; + + // check that this is a valid location to unzip the file + if (!DirectoryHasWritePermission(unzipFolder)) + { + // display dialog to try again or cancel + var result = UnityEditor.EditorUtility.DisplayDialog("No Write Permission", + string.Format("Directory \"{0}\" does not have write access", unzipFolder), + "Select another Directory", + "Cancel" + ); + + if (result) + { + InstallDCCIntegration(); + } + else + { + return false; + } + } + + // if file already unzipped in this location, then prompt user + // if they would like to continue unzipping or use what is there + if (dcc.FolderAlreadyUnzippedAtPath(unzipFolder)) + { + var result = UnityEditor.EditorUtility.DisplayDialogComplex("Integrations Exist at Path", + string.Format("Directory \"{0}\" already contains the decompressed integration", unzipFolder), + "Overwrite", + "Use Existing", + "Cancel" + ); + + if (result == 0) + { + DecompressZip(zipPath, unzipFolder); + } + else if (result == 2) + { + return false; + } + } + else + { + // unzip Integration folder + DecompressZip(zipPath, unzipFolder); + } + + dcc.SetIntegrationFolderPath(unzipFolder); + + return true; + } + + /// + /// Make sure we can write to this directory. + /// Try creating a file in path directory, if it raises an error, then we can't + /// write here. + /// TODO: find a more reliable way to check this + /// + /// true, if possible to write to path, false otherwise. + /// Path. + public static bool DirectoryHasWritePermission(string path) + { + try + { + using (System.IO.FileStream fs = System.IO.File.Create( + System.IO.Path.Combine( + path, + System.IO.Path.GetRandomFileName() + ), + 1, + System.IO.FileOptions.DeleteOnClose) + ) + {} + return true; + } + catch (Exception) + { + return false; + } + } + + public static void DecompressZip(string zipPath, string destPath) + { + System.Diagnostics.Process myProcess = new System.Diagnostics.Process(); + string zipApp; + switch (Application.platform) + { + case RuntimePlatform.WindowsEditor: + zipApp = "7z.exe"; + break; + case RuntimePlatform.OSXEditor: + case RuntimePlatform.LinuxEditor: + zipApp = "7za"; + break; + default: + throw new NotImplementedException(); + } + + myProcess.StartInfo.FileName = EditorApplication.applicationContentsPath + "/Tools/" + zipApp; + myProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; + myProcess.StartInfo.CreateNoWindow = true; + myProcess.StartInfo.UseShellExecute = false; + + // Command line flags used: + // x : extract the zip contents so that they maintain the file hierarchy + // -o : specify where to extract contents + // -r : recurse subdirectories + // -y : auto yes to all questions (without this Unity freezes as the process waits for a response) + myProcess.StartInfo.Arguments = string.Format("x \"{0}\" -o\"{1}\" -r -y", zipPath, destPath); + myProcess.EnableRaisingEvents = true; + myProcess.Start(); + myProcess.WaitForExit(); + + // in case we unzip inside the Assets folder, make sure it updates + AssetDatabase.Refresh(); + } + } +} diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/InstallIntegration.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/InstallIntegration.cs.meta new file mode 100644 index 0000000..531fbc1 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/InstallIntegration.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d9e9d20e0d3cf8143aabac612af5c880 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/ManualUpdateEditorWindow.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/ManualUpdateEditorWindow.cs new file mode 100644 index 0000000..b166319 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/ManualUpdateEditorWindow.cs @@ -0,0 +1,197 @@ +#if !UNITY_2018_3_OR_NEWER +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using UnityEngine.Formats.Fbx.Exporter; +using System.Linq; + +namespace DaddyFrosty.Fbx +{ + internal class ManualUpdateEditorWindow : EditorWindow + { + int[] selectedNodesToDestroy; + int[] selectedNodesToRename; + + FbxPrefabUtility m_fbxPrefabUtility; + FbxPrefab m_fbxPrefab; + GUIContent[] options; + List m_nodesToCreate; + List m_nodesToDestroy; + List m_nodesToRename; + + List m_nodeNameToSuggest; + + public bool Verbose { get { return UnityEditor.Formats.Fbx.Exporter.ExportSettings.instance.VerboseProperty; } } + + public void Init(FbxPrefabUtility fbxPrefabUtility, FbxPrefab fbxPrefab) + { + if (fbxPrefab == null) + { + return; + } + + FbxPrefabUtility.UpdateList updates = new FbxPrefabUtility.UpdateList(new FbxRepresentation(fbxPrefab.FbxHistory), fbxPrefab.FbxModel.transform, fbxPrefab); + + m_fbxPrefabUtility = fbxPrefabUtility; + m_fbxPrefab = fbxPrefab; + // Convert Hashset into List + m_nodesToCreate = updates.NodesToCreate.ToList(); + m_nodesToDestroy = updates.NodesToDestroy.ToList(); + m_nodesToRename = updates.NodesToRename.ToList(); + // Create the dropdown list + m_nodeNameToSuggest = new List(); + m_nodeNameToSuggest.AddRange(m_nodesToCreate); + m_nodeNameToSuggest.AddRange(m_nodesToRename); + + // Keep track of the selected combo option in each type + selectedNodesToDestroy = new int[m_nodesToDestroy.Count]; + selectedNodesToRename = new int[m_nodesToRename.Count]; + + // Default option for nodes to rename. Shows the current name mapping + for (int i = 0; i < m_nodesToRename.Count; i++) + { + for (int j = 0; j < m_nodeNameToSuggest.Count; j++) + { + if (m_nodeNameToSuggest[j] == m_nodesToRename[i]) + { + // Add extra 1 for the [Delete] option + selectedNodesToRename[i] = j + 1; + } + } + } + } + + void OnGUI() + { + // If there is nothing to map, sync prefab automatically and close the window + if (m_nodesToDestroy.Count == 0 && m_nodesToRename.Count == 0) + { + m_fbxPrefabUtility.SyncPrefab(); + Close(); + } + + //Titles of the columns + GUILayout.BeginHorizontal(); + GUILayout.Label("Unity Names", EditorStyles.boldLabel); + GUILayout.Label("FBX Names", EditorStyles.boldLabel); + GUILayout.EndHorizontal(); + + // List of nodes that will be destroyed on the Unity object, unless the user wants to map them + for (int i = 0; i < m_nodesToDestroy.Count; i++) + { + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(m_nodesToDestroy[i]); + + List listFbxNames = new List(); + listFbxNames.Add(new GUIContent("[Delete]")); + + for (int j = 0; j < m_nodeNameToSuggest.Count; j++) + { + listFbxNames.Add(new GUIContent(m_fbxPrefabUtility.GetFBXObjectName(m_nodeNameToSuggest[j]))); + } + + options = listFbxNames.ToArray(); + selectedNodesToDestroy[i] = EditorGUILayout.Popup(selectedNodesToDestroy[i], options); + + GUILayout.EndHorizontal(); + } + + // List of nodes that will be renamed on the Unity object, unless the user wants to map them or delete them + for (int i = 0; i < m_nodesToRename.Count; i++) + { + GUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(m_fbxPrefabUtility.GetUnityObjectName(m_nodesToRename[i])); + + List listFbxNames = new List(); + listFbxNames.Add(new GUIContent("[Delete]")); + + for (int j = 0; j < m_nodeNameToSuggest.Count; j++) + { + listFbxNames.Add(new GUIContent(m_fbxPrefabUtility.GetFBXObjectName(m_nodeNameToSuggest[j]))); + } + + options = listFbxNames.ToArray(); + + selectedNodesToRename[i] = EditorGUILayout.Popup(selectedNodesToRename[i], options); + + GUILayout.EndHorizontal(); + } + + GUILayout.BeginHorizontal(); + + if (GUILayout.Button("Apply Changes")) + { + ApplyChanges(); + //Close editor window + Close(); + } + + if (GUILayout.Button("Cancel")) + { + //Close editor window + Close(); + } + GUILayout.EndHorizontal(); + } + + void ApplyChanges() + { + // Nodes to Destroy have Unity names + for (int i = 0; i < m_nodesToDestroy.Count; i++) + { + // != [Delete] + if (selectedNodesToDestroy[i] != 0) + { + StringPair stringpair = new StringPair(); + stringpair.FBXObjectName = options[selectedNodesToDestroy[i]].text; + stringpair.UnityObjectName = m_nodesToDestroy[i]; + + m_fbxPrefab.NameMapping.Add(stringpair); + + if (Verbose) + { + Debug.Log("Mapped Unity: " + stringpair.UnityObjectName + " to FBX: " + stringpair.FBXObjectName); + } + } + } + + // Nodes to Rename have FBX names + for (int i = 0; i < m_nodesToRename.Count; i++) + { + string currentUnityNodeName = m_fbxPrefabUtility.GetUnityObjectName(m_nodesToRename[i]); + // == [Delete] + if (selectedNodesToRename[i] == 0) + { + // Remove previous mapping + m_fbxPrefabUtility.RemoveMappingUnityObjectName(currentUnityNodeName); + } + else + { + if (currentUnityNodeName != m_fbxPrefabUtility.GetUnityObjectName(options[selectedNodesToRename[i]].text)) + { + m_fbxPrefabUtility.RemoveMappingUnityObjectName(currentUnityNodeName); + StringPair stringpair = new StringPair(); + stringpair.FBXObjectName = options[selectedNodesToRename[i]].text; + stringpair.UnityObjectName = currentUnityNodeName; + m_fbxPrefab.NameMapping.Add(stringpair); + + if (Verbose) + { + Debug.Log("Mapped Unity: " + stringpair.UnityObjectName + " to FBX: " + stringpair.FBXObjectName); + } + } + else + { + if (Verbose) + { + Debug.Log("ALREADY Mapped Unity: " + currentUnityNodeName + " to FBX: " + options[selectedNodesToRename[i]].text); + } + } + } + } + + m_fbxPrefabUtility.SyncPrefab(); + } + } +} +#endif // !UNITY_2018_3_OR_NEWER diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/ManualUpdateEditorWindow.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/ManualUpdateEditorWindow.cs.meta new file mode 100644 index 0000000..c0392c0 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/ManualUpdateEditorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c65c04234d53a1748bf2c0838e4ec66b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders.meta new file mode 100644 index 0000000..ef31b86 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dbb835d424d3b1b47b0e81c4394ceb50 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder.meta new file mode 100644 index 0000000..a55aa86 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0adf7beba2647444d937ef44201d8f7c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorder.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorder.cs new file mode 100644 index 0000000..6ef5183 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorder.cs @@ -0,0 +1,100 @@ +// #if ENABLE_FBX_RECORDER +// using System.Collections.Generic; +// using UnityEngine; +// using UnityEditor.Recorder; +// using UnityEditor.Recorder.Input; +// +// namespace DaddyFrosty.Fbx +// { +// class FbxRecorder : GenericRecorder +// { +// protected override void RecordFrame(RecordingSession ctx) +// { +// } +// +// private void EndRecordingInternal(RecordingSession session) +// { +// var settings = (FbxRecorderSettings)session.settings; +// +// foreach (var input in m_Inputs) +// { +// var aInput = (AnimationInput)input; +// +// if (aInput.GameObjectRecorder == null) +// continue; +// +// var clip = new AnimationClip(); +// +// settings.FileNameGenerator.CreateDirectory(session); +// +// var absolutePath = FileNameGenerator.SanitizePath(settings.FileNameGenerator.BuildAbsolutePath(session)); +// var clipName = absolutePath.Replace(FileNameGenerator.SanitizePath(Application.dataPath), "Assets"); +// +// #if UNITY_2019_3_OR_NEWER +// var options = settings.GetCurveFilterOptions(settings.AnimationInputSettings.SimplyCurves); +// aInput.GameObjectRecorder.SaveToClip(clip, settings.FrameRate, options); +// #else +// aInput.GameObjectRecorder.SaveToClip(clip, settings.FrameRate); +// #endif +// if (settings.AnimationInputSettings.ClampedTangents) +// { +// FilterClip(clip); +// } +// +// var root = ((AnimationInputSettings)aInput.settings).gameObject; +// clip.name = "recorded_clip"; +// +// var exportSettings = new ExportModelSettingsSerialize(); +// exportSettings.SetAnimationSource(settings.TransferAnimationSource); +// exportSettings.SetAnimationDest(settings.TransferAnimationDest); +// exportSettings.SetObjectPosition(ObjectPosition.WorldAbsolute); +// // export animated skinned meshes so that blendshape animation will export +// exportSettings.SetAnimatedSkinnedMesh(true); +// var toInclude = Include.ModelAndAnim; +// if (!settings.ExportGeometry) +// { +// toInclude = Include.Anim; +// } +// exportSettings.SetModelAnimIncludeOption(toInclude); +// +// var exportData = new AnimationOnlyExportData(); +// exportData.CollectDependencies(clip, root, exportSettings); +// var exportDataContainer = new Dictionary(); +// exportDataContainer.Add(root, exportData); +// +// ModelExporter.ExportObjects(clipName, new UnityEngine.Object[] { root }, exportSettings, exportDataContainer); +// +// aInput.GameObjectRecorder.ResetRecording(); +// } +// } +// +// protected override void EndRecording(RecordingSession session) +// { +// if (session == null) +// { +// throw new System.ArgumentNullException("session"); +// } +// +// if (Recording) +// { +// EndRecordingInternal(session); +// } +// base.EndRecording(session); +// } +// +// void FilterClip(AnimationClip clip) +// { +// foreach (var bind in AnimationUtility.GetCurveBindings(clip)) +// { +// var curve = AnimationUtility.GetEditorCurve(clip, bind); +// for (var i = 0; i < curve.keys.Length; ++i) +// { +// AnimationUtility.SetKeyLeftTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); +// AnimationUtility.SetKeyRightTangentMode(curve, i, AnimationUtility.TangentMode.ClampedAuto); +// } +// AnimationUtility.SetEditorCurve(clip, bind, curve); +// } +// } +// } +// } +// #endif // ENABLE_FBX_RECORDER diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorder.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorder.cs.meta new file mode 100644 index 0000000..f175865 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4c5070c8d7ee03d42bd534f93b4fc347 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettings.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettings.cs new file mode 100644 index 0000000..234f633 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettings.cs @@ -0,0 +1,352 @@ +// #if ENABLE_FBX_RECORDER +// using System.Collections.Generic; +// using UnityEngine; +// using UnityEditor.Recorder; +// using UnityEditor.Recorder.Input; +// using UnityEditor.Animations; +// using System; +// +// namespace DaddyFrosty.Fbx +// { +// /// +// /// Class describing the settings for the FBX Recorder. +// /// +// [RecorderSettings(typeof(FbxRecorder), "FBX")] +// public class FbxRecorderSettings : RecorderSettings +// { +// [SerializeField] bool m_exportGeometry = true; +// +// /// +// /// Option to export the geometry/meshes of the recorded hierarchy to FBX. +// /// +// public bool ExportGeometry +// { +// get +// { +// return m_exportGeometry; +// } +// set +// { +// m_exportGeometry = value; +// } +// } +// +// [SerializeField] +// private string m_animSourceBindingId; +// [SerializeField] +// private string m_animDestBindingId; +// +// /// +// /// Option to transfer the transform animation from this transform to the destination. +// /// This also transfers to the destination any animation on GameObjects between the source and the destination. +// /// +// public Transform TransferAnimationSource +// { +// get +// { +// if (string.IsNullOrEmpty(m_animSourceBindingId)) +// return null; +// +// return GetBinding(m_animSourceBindingId); +// } +// +// set +// { +// if (!TransferAnimationSourceIsValid(value)) +// { +// return; +// } +// if (string.IsNullOrEmpty(m_animSourceBindingId)) +// m_animSourceBindingId = GenerateBindingId(); +// +// SetBinding(m_animSourceBindingId, value); +// } +// } +// +// /// +// /// Option to transfer the transform animation from the source to this transform. +// /// This also transfers to the destination any animation on GameObjects between the source and the destination. +// /// +// public Transform TransferAnimationDest +// { +// get +// { +// if (string.IsNullOrEmpty(m_animDestBindingId)) +// return null; +// +// return GetBinding(m_animDestBindingId); +// } +// +// set +// { +// if (!TransferAnimationDestIsValid(value)) +// { +// return; +// } +// if (string.IsNullOrEmpty(m_animDestBindingId)) +// m_animDestBindingId = GenerateBindingId(); +// +// SetBinding(m_animDestBindingId, value); +// } +// } +// +// static readonly CurveFilterOptions DefaultCurveFilterOptions = new CurveFilterOptions() +// { +// keyframeReduction = true, +// positionError = 0.5f, +// rotationError = 0.5f, +// scaleError = 0.5f, +// floatError = 0.5f +// }; +// +// static readonly CurveFilterOptions RegularCurveFilterOptions = new CurveFilterOptions +// { +// keyframeReduction = true +// }; +// +// static readonly CurveFilterOptions NoCurveFilterOptions = new CurveFilterOptions +// { +// keyframeReduction = false +// }; +// +// internal CurveFilterOptions GetCurveFilterOptions(AnimationInputSettings.CurveSimplificationOptions options) +// { +// switch (options) +// { +// case AnimationInputSettings.CurveSimplificationOptions.Lossy: +// return DefaultCurveFilterOptions; +// case AnimationInputSettings.CurveSimplificationOptions.Lossless: +// return RegularCurveFilterOptions; +// case AnimationInputSettings.CurveSimplificationOptions.Disabled: +// return NoCurveFilterOptions; +// default: +// throw new NotImplementedException(); +// } +// } +// +// /// +// /// Get a binding ID for the transform, so that the reference survives domain reload. Maintaining a direct reference would not work +// /// as all scene objects are destroyed and recreated on reload (e.g. when entering/exiting playmode). +// /// +// /// Binding ID +// static string GenerateBindingId() +// { +// return GUID.Generate().ToString(); +// } +// +// /// +// /// Get the Unity object (in this case transform), associated with the given binding ID. +// /// +// /// Binding ID +// /// Transform associated with binding ID +// static Transform GetBinding(string id) +// { +// return BindingManager.Get(id) as Transform; +// } +// +// /// +// /// Set the binding ID to be associated with the given Unity object. +// /// This information will be saved on domain reload, so that the object can still be found +// /// with the binding ID. +// /// +// /// Binding ID +// /// Unity Object +// static void SetBinding(string id, UnityEngine.Object obj) +// { +// BindingManager.Set(id, obj); +// } +// +// /// +// /// Determines whether p is an ancestor to t. +// /// +// /// true if p is ancestor to t; otherwise, false. +// /// P. +// /// T. +// internal bool IsAncestor(Transform p, Transform t) +// { +// var curr = t; +// while (curr != null) +// { +// if (curr == p) +// { +// return true; +// } +// curr = curr.parent; +// } +// return false; +// } +// +// /// +// /// Determines whether t1 and t2 are in the same hierarchy. +// /// +// /// true if t1 is in same hierarchy as t2; otherwise, false. +// /// T1. +// /// T2. +// internal bool IsInSameHierarchy(Transform t1, Transform t2) +// { +// return (IsAncestor(t1, t2) || IsAncestor(t2, t1)); +// } +// +// internal bool TransferAnimationSourceIsValid(Transform newValue) +// { +// if (!newValue) +// { +// return true; +// } +// +// var selectedGO = m_AnimationInputSettings.gameObject; +// +// if (!selectedGO) +// { +// Debug.LogWarning("FbxRecorderSettings: no Objects selected for export, can't transfer animation"); +// return false; +// } +// +// // source must be ancestor to dest +// if (TransferAnimationDest && !IsAncestor(newValue, TransferAnimationDest)) +// { +// Debug.LogWarningFormat("FbxRecorderSettings: Source {0} must be an ancestor of {1}", newValue.name, TransferAnimationDest.name); +// return false; +// } +// // must be in same hierarchy as selected GO +// if (!selectedGO || !IsInSameHierarchy(newValue, selectedGO.transform)) +// { +// Debug.LogWarningFormat("FbxRecorderSettings: Source {0} must be in the same hierarchy as {1}", newValue.name, selectedGO ? selectedGO.name : "the selected object"); +// return false; +// } +// return true; +// } +// +// internal bool TransferAnimationDestIsValid(Transform newValue) +// { +// if (!newValue) +// { +// return true; +// } +// +// var selectedGO = m_AnimationInputSettings.gameObject; +// +// if (!selectedGO) +// { +// Debug.LogWarning("FbxRecorderSettings: no Objects selected for export, can't transfer animation"); +// return false; +// } +// +// // source must be ancestor to dest +// if (TransferAnimationSource && !IsAncestor(TransferAnimationSource, newValue)) +// { +// Debug.LogWarningFormat("FbxRecorderSettings: Destination {0} must be a descendant of {1}", newValue.name, TransferAnimationSource.name); +// return false; +// } +// // must be in same hierarchy as selected GO +// if (!selectedGO || !IsInSameHierarchy(newValue, selectedGO.transform)) +// { +// Debug.LogWarningFormat("FbxRecorderSettings: Destination {0} must be in the same hierarchy as {1}", newValue.name, selectedGO ? selectedGO.name : "the selected object"); +// return false; +// } +// return true; +// } +// +// [SerializeField] AnimationInputSettings m_AnimationInputSettings = new AnimationInputSettings(); +// +// /// +// /// Stores the reference to the current FBX Recorder's input settings. +// /// +// public AnimationInputSettings AnimationInputSettings +// { +// get { return m_AnimationInputSettings; } +// set { m_AnimationInputSettings = value; } +// } +// +// /// +// /// Default constructor for FbxRecorderSettings. +// /// +// public FbxRecorderSettings() +// { +// var goWildcard = DefaultWildcard.GeneratePattern("GameObject"); +// +// FileNameGenerator.AddWildcard(goWildcard, GameObjectNameResolver); +// FileNameGenerator.AddWildcard(DefaultWildcard.GeneratePattern("GameObjectScene"), GameObjectSceneNameResolver); +// +// FileNameGenerator.ForceAssetsFolder = false; +// FileNameGenerator.Root = OutputPath.Root.AssetsFolder; +// FileNameGenerator.FileName = "animation_" + goWildcard + "_" + DefaultWildcard.Take; +// } +// +// string GameObjectNameResolver(RecordingSession session) +// { +// var go = m_AnimationInputSettings.gameObject; +// return go != null ? go.name : "None"; +// } +// +// string GameObjectSceneNameResolver(RecordingSession session) +// { +// var go = m_AnimationInputSettings.gameObject; +// return go != null ? go.scene.name : "None"; +// } +// +// /// +// /// Indicates if the current platform is supported (True) or not (False). +// /// +// /// +// /// FBX Recorder currently supports the following platforms: LinuxEditor, OSXEditor, WindowsEditor. +// /// +// public override bool IsPlatformSupported +// { +// get +// { +// return Application.platform == RuntimePlatform.LinuxEditor || +// Application.platform == RuntimePlatform.OSXEditor || +// Application.platform == RuntimePlatform.WindowsEditor; +// } +// } +// +// /// +// /// Stores the list of Input settings required by this Recorder. +// /// +// public override IEnumerable InputsSettings +// { +// get { yield return m_AnimationInputSettings; } +// } +// +// /// +// /// Tests if the Recorder has any errors. +// /// +// /// List of errors encountered. +// protected override void GetErrors(List errors) +// { +// base.GetErrors(errors); +// +// if (m_AnimationInputSettings.gameObject == null) +// { +// if (errors == null) +// { +// throw new System.ArgumentNullException("errors"); +// } +// errors.Add("No input object set"); +// } +// } +// +// /// +// /// Override this method if you need to do any post treatment after duplicating this Recorder in the Recorder Window. +// /// +// public override void OnAfterDuplicate() +// { +// m_AnimationInputSettings.DuplicateExposedReference(); +// } +// +// void OnDestroy() +// { +// m_AnimationInputSettings.ClearExposedReference(); +// } +// +// /// +// /// Stores the file extension this Recorder uses (without the dot). +// /// +// protected override string Extension +// { +// get { return "fbx"; } +// } +// } +// } +// #endif // ENABLE_FBX_RECORDER diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettings.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettings.cs.meta new file mode 100644 index 0000000..ebf9419 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettings.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c66f1cb43a625494b931ece4280e5b23 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettingsEditor.cs b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettingsEditor.cs new file mode 100644 index 0000000..8c7d060 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettingsEditor.cs @@ -0,0 +1,40 @@ +// #if ENABLE_FBX_RECORDER +// using System.Collections; +// using System.Collections.Generic; +// using UnityEngine; +// using UnityEditor.Recorder; +// using UnityEditor; +// +// namespace DaddyFrosty.Fbx +// { +// [CustomEditor(typeof(FbxRecorderSettings))] +// class FbxRecorderSettingsEditor : RecorderEditor +// { +// protected override void FileTypeAndFormatGUI() +// { +// EditorGUILayout.LabelField("Format", "FBX"); +// +// FbxRecorderSettings settings = target as FbxRecorderSettings; +// +// settings.ExportGeometry = EditorGUILayout.Toggle("Export Geometry", settings.ExportGeometry); +// } +// +// protected override void OnEncodingGui() +// { +// base.OnEncodingGui(); +// +// DrawSeparator(); +// +// EditorGUILayout.LabelField(new GUIContent( +// "Transfer Animation", +// "Transfer transform animation from source to destination. Animation on objects between source and destination will also be transferred to destination." +// )); +// +// FbxRecorderSettings settings = target as FbxRecorderSettings; +// +// settings.TransferAnimationSource = EditorGUILayout.ObjectField("Source", settings.TransferAnimationSource, typeof(Transform), allowSceneObjects: true) as Transform; +// settings.TransferAnimationDest = EditorGUILayout.ObjectField("Destination", settings.TransferAnimationDest, typeof(Transform), allowSceneObjects: true) as Transform; +// } +// } +// } +// #endif // ENABLE_FBX_RECORDER diff --git a/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettingsEditor.cs.meta b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettingsEditor.cs.meta new file mode 100644 index 0000000..9c581cd --- /dev/null +++ b/Assets/DaddyFrosty/Editor/FBX/Unity/Sources/Recorders/FbxRecorder/FbxRecorderSettingsEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d34f7bd290a32e041869e7e04815ee26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DaddyFrosty/Editor/TextureExport.cs b/Assets/DaddyFrosty/Editor/TextureExport.cs new file mode 100644 index 0000000..06610cf --- /dev/null +++ b/Assets/DaddyFrosty/Editor/TextureExport.cs @@ -0,0 +1,16 @@ +using System.IO; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class TextureExport : MonoBehaviour +{ + public Texture2D tex; + + [ContextMenu("Export")] + public void Export() + { + byte[] bytes = tex.EncodeToPNG(); + File.WriteAllBytes(Application.dataPath + "/savedScreen.png", bytes); + } +} diff --git a/Assets/DaddyFrosty/Editor/TextureExport.cs.meta b/Assets/DaddyFrosty/Editor/TextureExport.cs.meta new file mode 100644 index 0000000..3e36af4 --- /dev/null +++ b/Assets/DaddyFrosty/Editor/TextureExport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7524d6c15639ee04f83432f108698644 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md index 4d2050a..58840f7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # FrostyToolz A collection of unity editor tools I've made / improved + +# FBX Exporter +I spent countless nights fixing Unity's FBX Exporter. The code sucks, the exporter sucks. +
+With my version you can actually export skinned meshes properly with animations etc. +
+It's been really long since I made it so honestly don't remember exactly what it helped with but it was bones and skinned mesh renderer exporting to FBX. +## Pro Tip +FBX is the most supported file type by the Exporter, even Unity's. \ No newline at end of file