From 978b305717d393bd864834c244734091e5fa800a Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Wed, 5 Aug 2020 19:24:49 +0200 Subject: [PATCH] [unity] Added frustum culling / update mode parameters `Update When Invisible` and `UpdateMode` to all Skeleton components. Closes #1595. --- CHANGELOG.md | 1 + .../Components/SkeletonGraphicInspector.cs | 5 ++- .../Components/SkeletonRendererInspector.cs | 8 +++- .../Components/SkeletonAnimation.cs | 15 ++++++- .../spine-unity/Components/SkeletonGraphic.cs | 42 +++++++++++++++++-- .../spine-unity/Components/SkeletonMecanim.cs | 18 +++++--- .../Components/SkeletonRenderer.cs | 26 ++++++++++-- .../Runtime/spine-unity/ISkeletonAnimation.cs | 7 ++++ 8 files changed, 105 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aee67e828..d003e19a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -235,6 +235,7 @@ * `SkeletonMecanim` now provides an additional `Custom MixMode` parameter under `Mecanim Translator`. It is enabled by default in version 3.8 to maintain current behaviour, using the set `Mix Mode` for each Mecanim layer. When disabled, `SkeletonMecanim` will use the recommended `MixMode` according to the layer blend mode. Additional information can be found in the [Mecanim Translator section](http://esotericsoftware.com/spine-unity#Parameters-for-animation-blending-control) on the spine-unity documentation pages. * Added **SkeletonGraphic Timeline support**. Added supprot for multi-track Timeline preview in the Editor outside of play mode (multi-track scrubbing). See the [Timeline-Extension-UPM-Package](http://esotericsoftware.com/spine-unity#Timeline-Extension-UPM-Package) section of the spine-unity documentation for more information. * Added support for double-sided lighting at all `SkeletonLit` shaders (including URP and LWRP packages). + * Added frustum culling update mode parameters `Update When Invisible` (Inspector parameter) and `UpdateMode` (available via code) to all Skeleton components. This provides a simple way to disable certain updates when the `Renderer` is no longer visible (outside all cameras, culled in frustum culling). The new `UpdateMode` property allows disabling updates at a finer granularity level than disabling the whole component. Available modes are: `Nothing`, `OnlyAnimationStatus`, `EverythingExceptMesh` and `FullUpdate`. * **Changes of default values** * `SkeletonMecanim`'s `Layer Mix Mode` now defaults to `MixMode.MixNext` instead of `MixMode.MixAlways`. diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs index 5f9c5942a..e1725fd6a 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonGraphicInspector.cs @@ -37,7 +37,6 @@ using UnityEditor; namespace Spine.Unity.Editor { using Icons = SpineEditorUtilities.Icons; - [InitializeOnLoad] [CustomEditor(typeof(SkeletonGraphic))] [CanEditMultipleObjects] public class SkeletonGraphicInspector : UnityEditor.Editor { @@ -51,7 +50,7 @@ namespace Spine.Unity.Editor { SerializedProperty material, color; SerializedProperty skeletonDataAsset, initialSkinName; - SerializedProperty startingAnimation, startingLoop, timeScale, freeze, unscaledTime, tintBlack; + SerializedProperty startingAnimation, startingLoop, timeScale, freeze, updateWhenInvisible, unscaledTime, tintBlack; SerializedProperty initialFlipX, initialFlipY; SerializedProperty meshGeneratorSettings; SerializedProperty allowMultipleCanvasRenderers, separatorSlotNames, enableSeparatorSlots, updateSeparatorPartLocation; @@ -111,6 +110,7 @@ namespace Spine.Unity.Editor { timeScale = so.FindProperty("timeScale"); unscaledTime = so.FindProperty("unscaledTime"); freeze = so.FindProperty("freeze"); + updateWhenInvisible = so.FindProperty("updateWhenInvisible"); meshGeneratorSettings = so.FindProperty("meshGenerator").FindPropertyRelative("settings"); meshGeneratorSettings.isExpanded = SkeletonRendererInspector.advancedFoldout; @@ -233,6 +233,7 @@ namespace Spine.Unity.Editor { EditorGUILayout.PropertyField(unscaledTime, SpineInspectorUtility.TempContent(unscaledTime.displayName, tooltip: "If checked, this will use Time.unscaledDeltaTime to make this update independent of game Time.timeScale. Instance SkeletonGraphic.timeScale will still be applied.")); EditorGUILayout.Space(); EditorGUILayout.PropertyField(freeze); + EditorGUILayout.PropertyField(updateWhenInvisible); EditorGUILayout.Space(); SkeletonRendererInspector.SkeletonRootMotionParameter(targets); EditorGUILayout.Space(); diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs index 62f50ed22..1e8a3e50b 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Components/SkeletonRendererInspector.cs @@ -59,7 +59,7 @@ namespace Spine.Unity.Editor { protected SerializedProperty skeletonDataAsset, initialSkinName; protected SerializedProperty initialFlipX, initialFlipY; - protected SerializedProperty singleSubmesh, separatorSlotNames, clearStateOnDisable, immutableTriangles, fixDrawOrder; + protected SerializedProperty updateWhenInvisible, singleSubmesh, separatorSlotNames, clearStateOnDisable, immutableTriangles, fixDrawOrder; protected SerializedProperty normals, tangents, zSpacing, pmaVertexColors, tintBlack; // MeshGenerator settings protected SerializedProperty maskInteraction; protected SerializedProperty maskMaterialsNone, maskMaterialsInside, maskMaterialsOutside; @@ -74,7 +74,7 @@ namespace Spine.Unity.Editor { protected bool deleteOutsideMaskMaterialsQueued = false; protected GUIContent SkeletonDataAssetLabel, SkeletonUtilityButtonContent; - protected GUIContent PMAVertexColorsLabel, ClearStateOnDisableLabel, ZSpacingLabel, ImmubleTrianglesLabel, TintBlackLabel, SingleSubmeshLabel, FixDrawOrderLabel; + protected GUIContent PMAVertexColorsLabel, ClearStateOnDisableLabel, ZSpacingLabel, ImmubleTrianglesLabel, TintBlackLabel, UpdateWhenInvisibleLabel, SingleSubmeshLabel, FixDrawOrderLabel; protected GUIContent NormalsLabel, TangentsLabel, MaskInteractionLabel; protected GUIContent MaskMaterialsHeadingLabel, MaskMaterialsNoneLabel, MaskMaterialsInsideLabel, MaskMaterialsOutsideLabel; protected GUIContent SetMaterialButtonLabel, ClearMaterialButtonLabel, DeleteMaterialButtonLabel; @@ -119,6 +119,7 @@ namespace Spine.Unity.Editor { TangentsLabel = new GUIContent("Solve Tangents", "Calculates the tangents per frame. Use this if you are using lit shaders (usually with normal maps) that require vertex tangents."); TintBlackLabel = new GUIContent("Tint Black (!)", "Adds black tint vertex data to the mesh as UV2 and UV3. Black tinting requires that the shader interpret UV2 and UV3 as black tint colors for this effect to work. You may also use the default [Spine/Skeleton Tint Black] shader.\n\nIf you only need to tint the whole skeleton and not individual parts, the [Spine/Skeleton Tint] shader is recommended for better efficiency and changing/animating the _Black material property via MaterialPropertyBlock."); SingleSubmeshLabel = new GUIContent("Use Single Submesh", "Simplifies submesh generation by assuming you are only using one Material and need only one submesh. This is will disable multiple materials, render separation, and custom slot materials."); + UpdateWhenInvisibleLabel = new GUIContent("Update When Invisible", "Update mode used when the MeshRenderer becomes invisible. Update mode is automatically reset to UpdateMode.FullUpdate when the mesh becomes visible again."); FixDrawOrderLabel = new GUIContent("Fix Draw Order", "Applies only when 3+ submeshes are used (2+ materials with alternating order, e.g. \"A B A\"). If true, GPU instancing will be disabled at all materials and MaterialPropertyBlocks are assigned at each material to prevent aggressive batching of submeshes by e.g. the LWRP renderer, leading to incorrect draw order (e.g. \"A1 B A2\" changed to \"A1A2 B\"). You can disable this parameter when everything is drawn correctly to save the additional performance cost. Note: the GPU instancing setting will remain disabled at affected material assets after exiting play mode, you have to enable it manually if you accidentally enabled this parameter."); MaskInteractionLabel = new GUIContent("Mask Interaction", "SkeletonRenderer's interaction with a Sprite Mask."); MaskMaterialsHeadingLabel = new GUIContent("Mask Interaction Materials", "Materials used for different interaction with sprite masks."); @@ -140,6 +141,7 @@ namespace Spine.Unity.Editor { pmaVertexColors = so.FindProperty("pmaVertexColors"); clearStateOnDisable = so.FindProperty("clearStateOnDisable"); tintBlack = so.FindProperty("tintBlack"); + updateWhenInvisible = so.FindProperty("updateWhenInvisible"); singleSubmesh = so.FindProperty("singleSubmesh"); fixDrawOrder = so.FindProperty("fixDrawOrder"); maskInteraction = so.FindProperty("maskInteraction"); @@ -319,6 +321,8 @@ namespace Spine.Unity.Editor { EditorGUILayout.LabelField("Renderer Settings", EditorStyles.boldLabel); using (new SpineInspectorUtility.LabelWidthScope()) { // Optimization options + if (updateWhenInvisible != null) EditorGUILayout.PropertyField(updateWhenInvisible, UpdateWhenInvisibleLabel); + if (singleSubmesh != null) EditorGUILayout.PropertyField(singleSubmesh, SingleSubmeshLabel); #if PER_MATERIAL_PROPERTY_BLOCKS if (fixDrawOrder != null) EditorGUILayout.PropertyField(fixDrawOrder, FixDrawOrderLabel); diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs index 69679ff9b..112f231d5 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonAnimation.cs @@ -185,9 +185,23 @@ namespace Spine.Unity { if (!valid || state == null) return; + wasUpdatedAfterInit = true; + if (updateMode < UpdateMode.OnlyAnimationStatus) + return; + UpdateAnimationStatus(deltaTime); + + if (updateMode == UpdateMode.OnlyAnimationStatus) + return; + ApplyAnimation(); + } + + protected void UpdateAnimationStatus (float deltaTime) { deltaTime *= timeScale; skeleton.Update(deltaTime); state.Update(deltaTime); + } + + protected void ApplyAnimation () { state.Apply(skeleton); if (_UpdateLocal != null) @@ -203,7 +217,6 @@ namespace Spine.Unity { if (_UpdateComplete != null) { _UpdateComplete(this); } - wasUpdatedAfterInit = true; } public override void LateUpdate () { diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs index 3ea70fbea..fd6a6f7f6 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonGraphic.cs @@ -58,6 +58,16 @@ namespace Spine.Unity { public bool startingLoop; public float timeScale = 1f; public bool freeze; + + /// Update mode to optionally limit updates to e.g. only apply animations but not update the mesh. + public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } } + [SerializeField] protected UpdateMode updateMode = UpdateMode.FullUpdate; + + /// Update mode used when the MeshRenderer becomes invisible + /// (when OnBecameInvisible() is called). Update mode is automatically + /// reset to UpdateMode.FullUpdate when the mesh becomes visible again. + public UpdateMode updateWhenInvisible = UpdateMode.FullUpdate; + public bool unscaledTime; public bool allowMultipleCanvasRenderers = false; public List canvasRenderers = new List(); @@ -232,12 +242,27 @@ namespace Spine.Unity { public virtual void Update (float deltaTime) { if (!this.IsValid) return; + wasUpdatedAfterInit = true; + if (updateMode < UpdateMode.OnlyAnimationStatus) + return; + UpdateAnimationStatus(deltaTime); + + if (updateMode == UpdateMode.OnlyAnimationStatus) + return; + ApplyAnimation(); + } + + protected void UpdateAnimationStatus (float deltaTime) { deltaTime *= timeScale; skeleton.Update(deltaTime); state.Update(deltaTime); + } + + protected void ApplyAnimation () { state.Apply(skeleton); - if (UpdateLocal != null) UpdateLocal(this); + if (UpdateLocal != null) + UpdateLocal(this); skeleton.UpdateWorldTransform(); @@ -246,18 +271,27 @@ namespace Spine.Unity { skeleton.UpdateWorldTransform(); } - if (UpdateComplete != null) UpdateComplete(this); - wasUpdatedAfterInit = true; + if (UpdateComplete != null) + UpdateComplete(this); } public void LateUpdate () { // instantiation can happen from Update() after this component, leading to a missing Update() call. if (!wasUpdatedAfterInit) Update(0); if (freeze) return; - //this.SetVerticesDirty(); // Which is better? + if (updateMode <= UpdateMode.EverythingExceptMesh) return; + UpdateMesh(); } + public void OnBecameVisible () { + updateMode = UpdateMode.FullUpdate; + } + + public void OnBecameInvisible () { + updateMode = updateWhenInvisible; + } + public void ReapplySeparatorSlotNames () { if (!IsValid) return; diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs index d85390b18..2a28fd46b 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonMecanim.cs @@ -78,14 +78,23 @@ namespace Spine.Unity { public void Update () { if (!valid) return; - #if UNITY_EDITOR + wasUpdatedAfterInit = true; + // animation status is kept by Mecanim Animator component + if (updateMode <= UpdateMode.OnlyAnimationStatus) + return; + ApplyAnimation(); + } + + protected void ApplyAnimation () { + #if UNITY_EDITOR var translatorAnimator = translator.Animator; if (translatorAnimator != null && !translatorAnimator.isInitialized) translatorAnimator.Rebind(); if (Application.isPlaying) { translator.Apply(skeleton); - } else { + } + else { if (translatorAnimator != null && translatorAnimator.isInitialized && translatorAnimator.isActiveAndEnabled && translatorAnimator.runtimeAnimatorController != null) { // Note: Rebind is required to prevent warning "Animator is not playing an AnimatorController" with prefabs @@ -93,9 +102,9 @@ namespace Spine.Unity { translator.Apply(skeleton); } } - #else + #else translator.Apply(skeleton); - #endif + #endif // UpdateWorldTransform and Bone Callbacks { @@ -112,7 +121,6 @@ namespace Spine.Unity { if (_UpdateComplete != null) _UpdateComplete(this); } - wasUpdatedAfterInit = true; } public override void LateUpdate () { diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs index e75c00682..2e7e9b88a 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Components/SkeletonRenderer.cs @@ -55,11 +55,11 @@ namespace Spine.Unity { [RequireComponent(typeof(MeshFilter), typeof(MeshRenderer)), DisallowMultipleComponent] [HelpURL("http://esotericsoftware.com/spine-unity-rendering")] public class SkeletonRenderer : MonoBehaviour, ISkeletonComponent, IHasSkeletonDataAsset { - [SerializeField] public SkeletonDataAsset skeletonDataAsset; + public SkeletonDataAsset skeletonDataAsset; #region Initialization settings /// Skin name to use when the Skeleton is initialized. - [SerializeField] [SpineSkin(defaultAsEmptyString:true)] public string initialSkinName; + [SpineSkin(defaultAsEmptyString:true)] public string initialSkinName; /// Enable this parameter when overwriting the Skeleton's skin from an editor script. /// Otherwise any changes will be overwritten by the next inspector update. @@ -71,10 +71,20 @@ namespace Spine.Unity { protected bool editorSkipSkinSync = false; #endif /// Flip X and Y to use when the Skeleton is initialized. - [SerializeField] public bool initialFlipX, initialFlipY; + public bool initialFlipX, initialFlipY; #endregion #region Advanced Render Settings + + /// Update mode to optionally limit updates to e.g. only apply animations but not update the mesh. + public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } } + [SerializeField] protected UpdateMode updateMode = UpdateMode.FullUpdate; + + /// Update mode used when the MeshRenderer becomes invisible + /// (when OnBecameInvisible() is called). Update mode is automatically + /// reset to UpdateMode.FullUpdate when the mesh becomes visible again. + public UpdateMode updateWhenInvisible = UpdateMode.FullUpdate; + // Submesh Separation /// Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing. [UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators")][SerializeField][SpineSlot] protected string[] separatorSlotNames = new string[0]; @@ -361,6 +371,8 @@ namespace Spine.Unity { } #endif + if (updateMode <= UpdateMode.EverythingExceptMesh) return; + #if SPINE_OPTIONAL_RENDEROVERRIDE bool doMeshOverride = generateMeshOverride != null; if ((!meshRenderer.enabled) && !doMeshOverride) return; @@ -477,6 +489,14 @@ namespace Spine.Unity { OnMeshAndMaterialsUpdated(this); } + public void OnBecameVisible () { + updateMode = UpdateMode.FullUpdate; + } + + public void OnBecameInvisible () { + updateMode = updateWhenInvisible; + } + public void FindAndApplySeparatorSlots (string startsWith, bool clearExistingSeparators = true, bool updateStringArray = false) { if (string.IsNullOrEmpty(startsWith)) return; diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs index 496957574..36e300ec9 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/ISkeletonAnimation.cs @@ -28,6 +28,13 @@ *****************************************************************************/ namespace Spine.Unity { + public enum UpdateMode { + Nothing = 0, + OnlyAnimationStatus, + EverythingExceptMesh, + FullUpdate + }; + public delegate void UpdateBonesDelegate (ISkeletonAnimation animated); /// A Spine-Unity Component that animates a Skeleton but not necessarily with a Spine.AnimationState.