TransitionPreviewSettings.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR && UNITY_IMGUI
  3. #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
  4. using Animancer.Units;
  5. using System;
  6. using System.Collections.Generic;
  7. using UnityEditor;
  8. using UnityEngine;
  9. namespace Animancer.Editor.Previews
  10. {
  11. /// <summary>Persistent settings for the <see cref="TransitionPreviewWindow"/>.</summary>
  12. /// <remarks>
  13. /// <strong>Documentation:</strong>
  14. /// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#previews">
  15. /// Previews</see>
  16. /// </remarks>
  17. /// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/TransitionPreviewSettings
  18. [Serializable, InternalSerializableType]
  19. public class TransitionPreviewSettings : AnimancerSettingsGroup
  20. {
  21. /************************************************************************************************************************/
  22. /// <inheritdoc/>
  23. public override string DisplayName
  24. => "Transition Previews";
  25. /// <inheritdoc/>
  26. public override int Index
  27. => 3;
  28. /************************************************************************************************************************/
  29. private static TransitionPreviewSettings Instance
  30. => AnimancerSettingsGroup<TransitionPreviewSettings>.Instance;
  31. /************************************************************************************************************************/
  32. /// <summary>Draws the Inspector GUI for these settings.</summary>
  33. public static void DoInspectorGUI()
  34. {
  35. AnimancerSettings.SerializedObject.Update();
  36. EditorGUI.indentLevel++;
  37. DoMiscGUI();
  38. DoEnvironmentGUI();
  39. DoModelsGUI();
  40. DoHierarchyGUI();
  41. EditorGUI.indentLevel--;
  42. AnimancerSettings.SerializedObject.ApplyModifiedProperties();
  43. }
  44. /************************************************************************************************************************/
  45. #region Misc
  46. /************************************************************************************************************************/
  47. private static void DoMiscGUI()
  48. {
  49. Instance.DoPropertyField(nameof(_AutoClose));
  50. }
  51. /************************************************************************************************************************/
  52. [SerializeField]
  53. [Tooltip("Should this window automatically close if the target object is destroyed?")]
  54. private bool _AutoClose = true;
  55. /// <summary>Should this window automatically close if the target object is destroyed?</summary>
  56. public static bool AutoClose
  57. => Instance._AutoClose;
  58. /************************************************************************************************************************/
  59. [SerializeField]
  60. [Tooltip("Should the scene lighting be enabled?")]
  61. private bool _SceneLighting = false;
  62. /// <summary>Should the scene lighting be enabled?</summary>
  63. public static bool SceneLighting
  64. {
  65. get => Instance._SceneLighting;
  66. set
  67. {
  68. if (SceneLighting == value)
  69. return;
  70. var property = Instance.GetSerializedProperty(nameof(_SceneLighting));
  71. property.boolValue = value;
  72. property.serializedObject.ApplyModifiedProperties();
  73. }
  74. }
  75. /************************************************************************************************************************/
  76. [SerializeField]
  77. [Tooltip("Should the skybox be visible?")]
  78. private bool _ShowSkybox = false;
  79. /// <summary>Should the skybox be visible?</summary>
  80. public static bool ShowSkybox
  81. {
  82. get => Instance._ShowSkybox;
  83. set
  84. {
  85. if (ShowSkybox == value)
  86. return;
  87. var property = Instance.GetSerializedProperty(nameof(_ShowSkybox));
  88. property.boolValue = value;
  89. property.serializedObject.ApplyModifiedProperties();
  90. }
  91. }
  92. /************************************************************************************************************************/
  93. [SerializeField]
  94. [Seconds(Rule = Validate.Value.IsNotNegative)]
  95. [DefaultValue(0.02f)]
  96. [Tooltip("The amount of time that will be added by a single frame step")]
  97. private float _FrameStep = 0.02f;
  98. /// <summary>The amount of time that will be added by a single frame step (in seconds).</summary>
  99. public static float FrameStep
  100. => AnimancerSettingsGroup<TransitionPreviewSettings>.Instance._FrameStep;
  101. /************************************************************************************************************************/
  102. #endregion
  103. /************************************************************************************************************************/
  104. #region Environment
  105. /************************************************************************************************************************/
  106. [SerializeField]
  107. [Tooltip("If set, the default preview scene lighting will be replaced with this prefab.")]
  108. private GameObject _SceneEnvironment;
  109. /// <summary>If set, the default preview scene lighting will be replaced with this prefab.</summary>
  110. public static GameObject SceneEnvironment
  111. => Instance._SceneEnvironment;
  112. /************************************************************************************************************************/
  113. private static void DoEnvironmentGUI()
  114. {
  115. EditorGUI.BeginChangeCheck();
  116. var property = Instance.DoPropertyField(nameof(_SceneEnvironment));
  117. if (EditorGUI.EndChangeCheck())
  118. {
  119. property.serializedObject.ApplyModifiedProperties();
  120. TransitionPreviewWindow.InstanceScene.OnEnvironmentPrefabChanged();
  121. }
  122. }
  123. /************************************************************************************************************************/
  124. #endregion
  125. /************************************************************************************************************************/
  126. #region Models
  127. /************************************************************************************************************************/
  128. private static void DoModelsGUI()
  129. {
  130. var property = ModelsProperty;
  131. var count = property.arraySize = EditorGUILayout.DelayedIntField(nameof(Models), property.arraySize);
  132. // Drag and Drop to add model.
  133. var area = GUILayoutUtility.GetLastRect();
  134. HandleModelDragAndDrop(area);
  135. if (count == 0)
  136. return;
  137. property.isExpanded = EditorGUI.Foldout(area, property.isExpanded, GUIContent.none, true);
  138. if (!property.isExpanded)
  139. return;
  140. EditorGUI.indentLevel++;
  141. var model = property.GetArrayElementAtIndex(0);
  142. for (int i = 0; i < count; i++)
  143. {
  144. GUILayout.BeginHorizontal();
  145. EditorGUILayout.ObjectField(model);
  146. if (GUILayout.Button(AnimancerIcons.ClearIcon("Remove model"), AnimancerGUI.NoPaddingButtonStyle))
  147. {
  148. Serialization.RemoveArrayElement(property, i);
  149. property.serializedObject.ApplyModifiedProperties();
  150. AnimancerGUI.Deselect();
  151. GUIUtility.ExitGUI();
  152. return;
  153. }
  154. GUILayout.Space(EditorStyles.objectField.margin.right);
  155. GUILayout.EndHorizontal();
  156. model.Next(false);
  157. }
  158. EditorGUI.indentLevel--;
  159. }
  160. /************************************************************************************************************************/
  161. private static DragAndDropHandler<GameObject> _ModelDropHandler;
  162. private static void HandleModelDragAndDrop(Rect area)
  163. {
  164. _ModelDropHandler ??= (gameObject, isDrop) =>
  165. {
  166. if (!EditorUtility.IsPersistent(gameObject) ||
  167. Models.Contains(gameObject) ||
  168. gameObject.GetComponentInChildren<Animator>() == null)
  169. return false;
  170. if (isDrop)
  171. {
  172. var modelsProperty = ModelsProperty;
  173. modelsProperty.serializedObject.Update();
  174. var i = modelsProperty.arraySize;
  175. modelsProperty.arraySize = i + 1;
  176. modelsProperty.GetArrayElementAtIndex(i).objectReferenceValue = gameObject;
  177. modelsProperty.serializedObject.ApplyModifiedProperties();
  178. }
  179. return true;
  180. };
  181. _ModelDropHandler.Handle(area);
  182. }
  183. /************************************************************************************************************************/
  184. [SerializeField]
  185. private List<GameObject> _Models;
  186. /// <summary>The models previously used in the <see cref="TransitionPreviewWindow"/>.</summary>
  187. /// <remarks>Accessing this property removes missing and duplicate models from the list.</remarks>
  188. public static List<GameObject> Models
  189. {
  190. get
  191. {
  192. var instance = Instance;
  193. AnimancerEditorUtilities.RemoveMissingAndDuplicates(ref instance._Models);
  194. return instance._Models;
  195. }
  196. }
  197. private static SerializedProperty ModelsProperty
  198. => Instance.GetSerializedProperty(nameof(_Models));
  199. /************************************************************************************************************************/
  200. /// <summary>Adds a `model` to the list of preview models.</summary>
  201. public static void AddModel(GameObject model)
  202. {
  203. if (model == GetOrCreateDefaultHumanoid(null) ||
  204. model == GetOrCreateDefaultSprite(null))
  205. return;
  206. if (EditorUtility.IsPersistent(model))
  207. {
  208. AddModel(Models, model);
  209. AnimancerSettings.SetDirty();
  210. }
  211. else
  212. {
  213. AddModel(TemporarySettings.PreviewModels, model);
  214. }
  215. }
  216. private static void AddModel(List<GameObject> models, GameObject model)
  217. {
  218. // Remove if it was already there so that when we add it, it will be moved to the end.
  219. var index = models.LastIndexOf(model);// Search backwards because it's more likely to be near the end.
  220. if (index >= 0 && index < models.Count)
  221. models.RemoveAt(index);
  222. models.Add(model);
  223. }
  224. /************************************************************************************************************************/
  225. private static GameObject _DefaultHumanoid;
  226. /// <summary>
  227. /// Returns the default preview object for Humanoid animations
  228. /// if it has already been loaded.
  229. /// </summary>
  230. public static GameObject GetDefaultHumanoidIfAlreadyLoaded()
  231. => _DefaultHumanoid;
  232. /// <summary>Returns the default preview object for Humanoid animations.</summary>
  233. /// <remarks>A `parent` is only required if Animancer's or Unity's default objects fail to load.</remarks>
  234. public static GameObject GetOrCreateDefaultHumanoid(Transform parent)
  235. {
  236. if (_DefaultHumanoid != null)
  237. return _DefaultHumanoid;
  238. // Try to load Animancer Humanoid.
  239. var path = AssetDatabase.GUIDToAssetPath("f976ca0fb1329b44a8bc3dcca706751a");
  240. if (!string.IsNullOrEmpty(path))
  241. {
  242. _DefaultHumanoid = AssetDatabase.LoadAssetAtPath<GameObject>(path);
  243. if (_DefaultHumanoid != null)
  244. return _DefaultHumanoid;
  245. }
  246. // Otherwise try to load Unity's DefaultAvatar.
  247. _DefaultHumanoid = EditorGUIUtility.Load("Avatar/DefaultAvatar.fbx") as GameObject;
  248. if (_DefaultHumanoid != null)
  249. return _DefaultHumanoid;
  250. if (parent == null)
  251. return null;
  252. // Otherwise just create an empty object.
  253. _DefaultHumanoid = EditorUtility.CreateGameObjectWithHideFlags(
  254. "DummyAvatar",
  255. HideFlags.HideAndDontSave,
  256. typeof(Animator));
  257. _DefaultHumanoid.transform.parent = parent;
  258. return _DefaultHumanoid;
  259. }
  260. /************************************************************************************************************************/
  261. private static GameObject _DefaultSprite;
  262. /// <summary>
  263. /// Returns the default preview object for <see cref="Sprite"/> animations
  264. /// if it has already been created.
  265. /// </summary>
  266. public static GameObject GetDefaultSpriteIfAlreadyCreated()
  267. => _DefaultSprite;
  268. /// <summary>Returns the default preview object for <see cref="Sprite"/> animations.</summary>
  269. /// <remarks>A `parent` is required to create the object.</remarks>
  270. public static GameObject GetOrCreateDefaultSprite(Transform parent)
  271. {
  272. if (_DefaultSprite == null && parent != null)
  273. {
  274. _DefaultSprite = EditorUtility.CreateGameObjectWithHideFlags(
  275. "DefaultSprite",
  276. HideFlags.HideAndDontSave,
  277. typeof(Animator),
  278. typeof(SpriteRenderer));
  279. _DefaultSprite.transform.parent = parent;
  280. }
  281. return _DefaultSprite;
  282. }
  283. /************************************************************************************************************************/
  284. /// <summary>
  285. /// Tries to choose the most appropriate model to use
  286. /// based on the properties animated by the `animationClipSource`.
  287. /// </summary>
  288. public static Transform TrySelectBestModel(
  289. object animationClipSource,
  290. Transform parent)
  291. {
  292. if (animationClipSource.IsNullOrDestroyed())
  293. return null;
  294. using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
  295. {
  296. clips.GatherFromSource(animationClipSource);
  297. if (clips.Count == 0)
  298. return null;
  299. var model = TrySelectBestModel(clips, TemporarySettings.PreviewModels);
  300. if (model != null)
  301. return model;
  302. model = TrySelectBestModel(clips, Models);
  303. if (model != null)
  304. return model;
  305. foreach (var clip in clips)
  306. {
  307. var type = AnimationBindings.GetAnimationType(clip);
  308. switch (type)
  309. {
  310. case AnimationType.Humanoid:
  311. return GetOrCreateDefaultHumanoid(parent).transform;
  312. case AnimationType.Sprite:
  313. return GetOrCreateDefaultSprite(parent).transform;
  314. }
  315. }
  316. return null;
  317. }
  318. }
  319. /************************************************************************************************************************/
  320. private static Transform TrySelectBestModel(HashSet<AnimationClip> clips, List<GameObject> models)
  321. {
  322. var animatableBindings = new HashSet<EditorCurveBinding>[models.Count];
  323. for (int i = 0; i < models.Count; i++)
  324. {
  325. animatableBindings[i] = AnimationBindings.GetBindings(models[i]).ObjectBindings;
  326. }
  327. var bestMatchIndex = -1;
  328. var bestMatchCount = 0;
  329. foreach (var clip in clips)
  330. {
  331. var clipBindings = AnimationBindings.GetBindings(clip);
  332. for (int iModel = animatableBindings.Length - 1; iModel >= 0; iModel--)
  333. {
  334. var modelBindings = animatableBindings[iModel];
  335. var matches = 0;
  336. for (int iBinding = 0; iBinding < clipBindings.Length; iBinding++)
  337. {
  338. if (modelBindings.Contains(clipBindings[iBinding]))
  339. matches++;
  340. }
  341. if (bestMatchCount < matches && matches > clipBindings.Length / 2)
  342. {
  343. bestMatchCount = matches;
  344. bestMatchIndex = iModel;
  345. // If it matches all bindings, use it.
  346. if (bestMatchCount == clipBindings.Length)
  347. goto FoundBestMatch;
  348. }
  349. }
  350. }
  351. FoundBestMatch:
  352. if (bestMatchIndex >= 0)
  353. return models[bestMatchIndex].transform;
  354. else
  355. return null;
  356. }
  357. /************************************************************************************************************************/
  358. #endregion
  359. /************************************************************************************************************************/
  360. #region Scene Hierarchy
  361. /************************************************************************************************************************/
  362. private static void DoHierarchyGUI()
  363. {
  364. GUILayout.BeginVertical(GUI.skin.box);
  365. GUILayout.Label("Preview Scene Hierarchy");
  366. DoHierarchyGUI(TransitionPreviewWindow.InstanceScene.PreviewSceneRoot);
  367. GUILayout.EndVertical();
  368. }
  369. /************************************************************************************************************************/
  370. private static GUIStyle _HierarchyButtonStyle;
  371. private static void DoHierarchyGUI(Transform root)
  372. {
  373. var area = AnimancerGUI.LayoutSingleLineRect();
  374. _HierarchyButtonStyle ??= new(EditorStyles.miniButton)
  375. {
  376. alignment = TextAnchor.MiddleLeft,
  377. };
  378. if (GUI.Button(EditorGUI.IndentedRect(area), root.name, _HierarchyButtonStyle))
  379. {
  380. Selection.activeTransform = root;
  381. GUIUtility.ExitGUI();
  382. }
  383. var childCount = root.childCount;
  384. if (childCount == 0)
  385. return;
  386. var expandedHierarchy = TransitionPreviewWindow.InstanceScene.ExpandedHierarchy;
  387. var index = expandedHierarchy != null ? expandedHierarchy.IndexOf(root) : -1;
  388. var isExpanded = EditorGUI.Foldout(area, index >= 0, GUIContent.none);
  389. if (isExpanded)
  390. {
  391. if (index < 0)
  392. expandedHierarchy.Add(root);
  393. EditorGUI.indentLevel++;
  394. for (int i = 0; i < childCount; i++)
  395. DoHierarchyGUI(root.GetChild(i));
  396. EditorGUI.indentLevel--;
  397. }
  398. else if (index >= 0)
  399. {
  400. expandedHierarchy.RemoveAt(index);
  401. }
  402. }
  403. /************************************************************************************************************************/
  404. #endregion
  405. /************************************************************************************************************************/
  406. }
  407. }
  408. #endif