From cedfb6cb99966c108953a4d99d55f1402b32f0ea Mon Sep 17 00:00:00 2001 From: Gregory Labute Date: Thu, 30 May 2024 08:10:00 -0400 Subject: [PATCH] CMCL-0000: Spline roll cleanup (#997) * Spline roll cleanup * Add button in inspector to engage the scene view tool * Prettier SplineRoll inspector - requires minor API change * Add missing tooltips * Update CinemachineSplineRollEditor.cs * Fix broken roll data list * Add tooltips in scene view * Add SplineDataInspectorUtility * Refactor spline roll to work with scaled splines * Refactor ISplineReferencer * Update SplineSettings.cs * Update CinemachineSplineCart.cs * Fix missing doc and remove finalizer * Remove some infrastructure editor classes from public * New spline roll tool icons * Update CinemachineSplineDolly.cs --- .../Icons/CmSplineRollTool@256.png | Bin 0 -> 7756 bytes .../Icons/CmSplineRollTool@256.png.meta | 114 +++++++++ .../Icons/Dark/CmSplineRollTool@256.png | Bin 0 -> 7112 bytes .../Icons/Dark/CmSplineRollTool@256.png.meta | 114 +++++++++ .../Editors/CinemachineSplineCartEditor.cs | 16 +- .../Editors/CinemachineSplineDollyEditor.cs | 16 +- .../Editors/CinemachineSplineRollEditor.cs | 239 ++++++++++++++++-- com.unity.cinemachine/Editor/Overlays.meta | 8 - .../Editor/Overlays/SplineRollTool.cs | 161 ------------ .../Editor/Overlays/SplineRollTool.cs.meta | 3 - .../FoldoutWithEnabledButtonPropertyDrawer.cs | 2 +- .../InputAxisPropertyDrawer.cs | 4 +- .../SplineSettingsPropertyDrawer.cs | 10 +- .../Vector2AsRangePropertyDrawer.cs | 2 +- .../Editor/Upgrader/UpgradeObjectToCm3.cs | 6 +- .../Editor/Utility/CinemachineSceneTools.cs | 10 +- .../CinemachineToolbarOverlay.cs | 9 +- .../CinemachineToolbarOverlay.cs.meta | 0 .../Editor/Utility/InspectorUtility.cs | 38 ++- .../Utility/SplineDataInspectorUtility.cs | 118 +++++++++ .../SplineDataInspectorUtility.cs.meta | 11 + .../Behaviours/CinemachineSplineCart.cs | 71 ++---- .../Behaviours/CinemachineSplineRoll.cs | 89 +++++-- .../Components/CinemachineSplineDolly.cs | 87 +++---- .../Runtime/Core/SplineContainerExtensions.cs | 47 ++-- .../Runtime/Core/SplineSettings.cs | 153 ++++++++++- .../Deprecated/CinemachineDollyCart.cs | 8 +- 27 files changed, 972 insertions(+), 364 deletions(-) create mode 100644 com.unity.cinemachine/Editor/EditorResources/Icons/CmSplineRollTool@256.png create mode 100644 com.unity.cinemachine/Editor/EditorResources/Icons/CmSplineRollTool@256.png.meta create mode 100644 com.unity.cinemachine/Editor/EditorResources/Icons/Dark/CmSplineRollTool@256.png create mode 100644 com.unity.cinemachine/Editor/EditorResources/Icons/Dark/CmSplineRollTool@256.png.meta delete mode 100644 com.unity.cinemachine/Editor/Overlays.meta delete mode 100644 com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs delete mode 100644 com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs.meta rename com.unity.cinemachine/Editor/{Overlays => Utility}/CinemachineToolbarOverlay.cs (97%) rename com.unity.cinemachine/Editor/{Overlays => Utility}/CinemachineToolbarOverlay.cs.meta (100%) create mode 100644 com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs create mode 100644 com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs.meta diff --git a/com.unity.cinemachine/Editor/EditorResources/Icons/CmSplineRollTool@256.png b/com.unity.cinemachine/Editor/EditorResources/Icons/CmSplineRollTool@256.png new file mode 100644 index 0000000000000000000000000000000000000000..51db5e28877fce8ab97017bf2b3deb8dc3e578a1 GIT binary patch literal 7756 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 literal 0 HcmV?d00001 diff --git a/com.unity.cinemachine/Editor/EditorResources/Icons/CmSplineRollTool@256.png.meta b/com.unity.cinemachine/Editor/EditorResources/Icons/CmSplineRollTool@256.png.meta new file mode 100644 index 000000000..02aac5ba9 --- /dev/null +++ b/com.unity.cinemachine/Editor/EditorResources/Icons/CmSplineRollTool@256.png.meta @@ -0,0 +1,114 @@ +fileFormatVersion: 2 +guid: af98f166dae35a14190b6909eed26efb +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 12 + mipmaps: + mipMapMode: 0 + enableMipMap: 1 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + flipGreenChannel: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + vTOnly: 0 + ignoreMipmapLimit: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 0 + wrapV: 0 + wrapW: 0 + nPOTScale: 1 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 1 + alphaUsage: 1 + alphaIsTransparency: 0 + spriteTessellationDetail: -1 + textureType: 0 + textureShape: 1 + singleChannelComponent: 0 + flipbookRows: 1 + flipbookColumns: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + ignorePngGamma: 0 + applyGammaDecoding: 0 + swizzle: 50462976 + cookieLightType: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + ignorePlatformSupport: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + nameFileIdTable: {} + mipmapLimitGroupName: + pSDRemoveMatte: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.cinemachine/Editor/EditorResources/Icons/Dark/CmSplineRollTool@256.png b/com.unity.cinemachine/Editor/EditorResources/Icons/Dark/CmSplineRollTool@256.png new file mode 100644 index 0000000000000000000000000000000000000000..cd433521c4dd5decfa1e6fb8001e56ba1193d79e GIT binary patch literal 7112 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 Target.SplineSettings))); + + var splineIsChildHelp = ux.AddChild(new HelpBox("Spline should not be a child of this object.", HelpBoxMessageType.Error)); + ux.Add(new PropertyField(serializedObject.FindProperty("m_SplineSettings"))); ux.Add(new PropertyField(serializedObject.FindProperty(() => Target.UpdateMethod))); var autoDollyProp = serializedObject.FindProperty(() => Target.AutomaticDolly); @@ -26,6 +28,18 @@ public override VisualElement CreateInspectorGUI() targetField.SetVisible(autodolly != null && autodolly.RequiresTrackingTarget); }); + ux.TrackAnyUserActivity(() => + { + bool isChild = false; + for (int i = 0; !isChild && i < targets.Length; ++i) + { + var t = targets[i] as CinemachineSplineCart; + if (t != null && t.Spline != null) + isChild = t.transform.IsAncestorOf(t.Spline.transform); + } + splineIsChildHelp.SetVisible(isChild); + }); + return ux; } } diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyEditor.cs index 7cd83a14f..59a7d47b9 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineSplineDollyEditor.cs @@ -1,7 +1,6 @@ using UnityEditor; using UnityEditor.UIElements; using UnityEngine.UIElements; -using UnityEngine.Splines; namespace Unity.Cinemachine.Editor { @@ -17,8 +16,9 @@ public override VisualElement CreateInspectorGUI() this.AddMissingCmCameraHelpBox(ux); var noSplineHelp = ux.AddChild(new HelpBox("A Spline is required.", HelpBoxMessageType.Warning)); + var splineIsChildHelp = ux.AddChild(new HelpBox("Spline should not be a child of this object.", HelpBoxMessageType.Error)); - var splineProp = serializedObject.FindProperty(() => Target.SplineSettings); + var splineProp = serializedObject.FindProperty("m_SplineSettings"); ux.Add(new PropertyField(splineProp)); ux.Add(new PropertyField(serializedObject.FindProperty(() => Target.SplineOffset))); ux.Add(new PropertyField(serializedObject.FindProperty(() => Target.CameraRotation))); @@ -33,6 +33,18 @@ public override VisualElement CreateInspectorGUI() noSplineHelp.SetVisible(noSpline); }); + ux.TrackAnyUserActivity(() => + { + bool isChild = false; + for (int i = 0; !isChild && i < targets.Length; ++i) + { + var t = targets[i] as CinemachineSplineDolly; + if (t != null && t.Spline != null) + isChild = t.transform.IsAncestorOf(t.Spline.transform); + } + splineIsChildHelp.SetVisible(isChild); + }); + return ux; } } diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs index 54dd01de8..0811e3d58 100644 --- a/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs +++ b/com.unity.cinemachine/Editor/Editors/CinemachineSplineRollEditor.cs @@ -1,5 +1,7 @@ using UnityEditor; +using UnityEditor.EditorTools; using UnityEditor.Splines; +using UnityEditor.UIElements; using UnityEngine; using UnityEngine.Splines; using UnityEngine.UIElements; @@ -7,15 +9,41 @@ namespace Unity.Cinemachine.Editor { [CustomEditor(typeof(CinemachineSplineRoll))] - [CanEditMultipleObjects] class CinemachineSplineRollEditor : UnityEditor.Editor { public override VisualElement CreateInspectorGUI() { var ux = new VisualElement(); - var prop = serializedObject.GetIterator(); - if (prop.NextVisible(true)) - InspectorUtility.AddRemainingProperties(ux, prop); + var splineData = target as CinemachineSplineRoll; + + var invalidHelp = new HelpBox( + "This component should be associated with a non-empty spline", + HelpBoxMessageType.Warning); + ux.Add(invalidHelp); + var toolButton = ux.AddChild(new Button(() => ToolManager.SetActiveTool(typeof(SplineRollTool))) + { text = "Edit Data Points in Scene View" }); + ux.AddSpace(); + ux.TrackAnyUserActivity(() => + { + var haveSpline = splineData != null && splineData.SplineContainer != null; + invalidHelp.SetVisible(!haveSpline); + toolButton.SetEnabled(haveSpline); + }); + + var rollProp = serializedObject.FindProperty(() => splineData.Roll); + ux.Add(SplineDataInspectorUtility.CreatePathUnitField(rollProp, () => splineData == null ? null : splineData.SplineContainer)); + + ux.AddHeader("Data Points"); + var list = ux.AddChild(SplineDataInspectorUtility.CreateDataListField( + splineData.Roll, rollProp, () => splineData?.SplineContainer)); + + ux.TrackPropertyValue(rollProp, (p) => + { + // Invalidate the mesh cache when the property changes (SetDirty() not always called!) + SplineGizmoCache.Instance = null; + EditorApplication.delayCall += () => InspectorUtility.RepaintGameView(); + }); + return ux; } @@ -33,26 +61,31 @@ static void DrawGizmos(CinemachineSplineRoll splineRoll, GizmoType selectionType static void DrawSplineGizmo(CinemachineSplineRoll splineRoll, Color pathColor, float width, int resolution) { - var spline = splineRoll == null ? null : splineRoll.Container; - if (spline == null || spline.Spline == null || spline.Spline.Count == 0) + var splineContainer = splineRoll == null ? null : splineRoll.SplineContainer as SplineContainer; + if (!splineContainer.IsValid()) return; + var transform = splineContainer.transform; + var scale = transform.lossyScale; + width = width * 3 / (scale.x + scale.y + scale.z); + // Rebuild the cached mesh if necessary. This can be expensive! if (SplineGizmoCache.Instance == null || SplineGizmoCache.Instance.Mesh == null - || SplineGizmoCache.Instance.Spline != spline.Spline - || SplineGizmoCache.Instance.SplineData != splineRoll.Roll + || SplineGizmoCache.Instance.Spline != splineContainer.Spline + || SplineGizmoCache.Instance.RollData != splineRoll.Roll || SplineGizmoCache.Instance.Width != width || SplineGizmoCache.Instance.Resolution != resolution) { - var numKnots = spline.Spline.Count; + var numKnots = splineContainer.Spline.Count; var numSteps = numKnots * resolution; var stepSize = 1.0f / numSteps; var halfWidth = width * 0.5f; // For efficiency, we create a mesh with the track and draw it in one shot - spline.LocalEvaluateSplineWithRoll(splineRoll, Quaternion.identity, 0, out var p, out var q); - var w = (q * Vector3.right) * halfWidth; + var scaledSpline = new CachedScaledSpline(splineContainer.Spline, transform, Collections.Allocator.Temp); + scaledSpline.LocalEvaluateSplineWithRoll(splineRoll, Quaternion.identity, 0, out var p, out var q); + var w = q * Vector3.right * halfWidth; var indices = new int[2 * 3 * numSteps]; numSteps++; // ceil @@ -67,8 +100,8 @@ static void DrawSplineGizmo(CinemachineSplineRoll splineRoll, Color pathColor, f for (int i = 1; i < numSteps; ++i) { var t = i * stepSize; - spline.LocalEvaluateSplineWithRoll(splineRoll, Quaternion.identity, t, out p, out q); - w = (q * Vector3.right) * halfWidth; + scaledSpline.LocalEvaluateSplineWithRoll(splineRoll, Quaternion.identity, t, out p, out q); + w = q * Vector3.right * halfWidth; indices[iIndex++] = vIndex - 2; indices[iIndex++] = vIndex - 1; @@ -90,8 +123,8 @@ static void DrawSplineGizmo(CinemachineSplineRoll splineRoll, Color pathColor, f SplineGizmoCache.Instance = new SplineGizmoCache { Mesh = mesh, - Spline = spline.Spline, - SplineData = splineRoll.Roll, + Spline = splineContainer.Spline, + RollData = splineRoll.Roll, Width = width, Resolution = resolution }; @@ -99,7 +132,7 @@ static void DrawSplineGizmo(CinemachineSplineRoll splineRoll, Color pathColor, f // Draw the path var colorOld = Gizmos.color; var matrixOld = Gizmos.matrix; - Gizmos.matrix = spline.transform.localToWorldMatrix; + Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one); Gizmos.color = pathColor; Gizmos.DrawWireMesh(SplineGizmoCache.Instance.Mesh); Gizmos.matrix =matrixOld; @@ -110,7 +143,7 @@ static void DrawSplineGizmo(CinemachineSplineRoll splineRoll, Color pathColor, f class SplineGizmoCache { public Mesh Mesh; - public SplineData SplineData; + public SplineData RollData; public Spline Spline; public float Width; public int Resolution; @@ -123,19 +156,183 @@ static SplineGizmoCache() Instance = null; EditorSplineUtility.AfterSplineWasModified -= OnSplineChanged; EditorSplineUtility.AfterSplineWasModified += OnSplineChanged; - EditorSplineUtility.UnregisterSplineDataChanged(OnSplineDataChanged); - EditorSplineUtility.RegisterSplineDataChanged(OnSplineDataChanged); + EditorSplineUtility.UnregisterSplineDataChanged(OnSplineDataChanged); + EditorSplineUtility.RegisterSplineDataChanged(OnSplineDataChanged); } static void OnSplineChanged(Spline spline) { if (Instance != null && spline == Instance.Spline) Instance = null; } - static void OnSplineDataChanged(SplineData data) + static void OnSplineDataChanged(SplineData data) { - if (Instance != null && data == Instance.SplineData) + if (Instance != null && data == Instance.RollData) Instance = null; } } } + + [CustomPropertyDrawer(typeof(DataPoint))] + class CinemachineLookAtDataOnSplineDataPointPropertyDrawer : PropertyDrawer + { + public override VisualElement CreatePropertyGUI(SerializedProperty property) + { + const string indexTooltip = "The position on the Spline at which this data point will take effect. " + + "The value is interpreted according to the Index Unit setting."; + + var def = new CinemachineSplineRoll.RollData(); + var indexProp = property.FindPropertyRelative("m_Index"); + var valueProp = property.FindPropertyRelative("m_Value"); + + var ux = new VisualElement { style = { flexDirection = FlexDirection.Row, flexGrow = 1 }}; + + ux.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { width = 12 }}); // pass-through for selecting row in list + var label = ux.AddChild(new Label(indexProp.displayName) { tooltip = indexTooltip, style = { alignSelf = Align.Center }}); + var indexField = ux.AddChild(new PropertyField(indexProp, "") { style = { flexGrow = 1, flexBasis = 20 } }); + indexField.OnInitialGeometry(() => indexField.SafeSetIsDelayed()); + InspectorUtility.AddDelayedFriendlyPropertyDragger(label, indexProp, indexField, false); + + ux.Add(new VisualElement { pickingMode = PickingMode.Ignore, style = { width = 12 }}); // pass-through for selecting row in list + ux.Add(new InspectorUtility.CompactPropertyField(valueProp.FindPropertyRelative(() => def.Value))); + return ux; + } + } + + [EditorTool("Spline Roll Tool", typeof(CinemachineSplineRoll))] + sealed class SplineRollTool : EditorTool + { + GUIContent m_IconContent; + public override GUIContent toolbarIcon => m_IconContent; + + bool GetTargets(out CinemachineSplineRoll splineData, out SplineContainer spline) + { + splineData = target as CinemachineSplineRoll; + if (splineData != null) + { + spline = splineData.SplineContainer as SplineContainer; + return spline != null && spline.Spline != null; + } + spline = null; + return false; + } + + void OnEnable() + { + m_IconContent = new GUIContent + { + image = AssetDatabase.LoadAssetAtPath(CinemachineSceneToolHelpers.GetIconPath() + "CmSplineRollTool@256.png"), + text = "Roll Tool", + tooltip = "Adjust the roll data points along the spline." + }; + } + + public override void OnToolGUI(EditorWindow window) + { + if (!GetTargets(out var splineData, out var spline)) + return; + + Undo.RecordObject(splineData, "Modifying Roll RollData"); + var color = Handles.selectedColor; + using (new Handles.DrawingScope(color)) + { + var nativeSpline = new NativeSpline(spline.Spline, spline.transform.localToWorldMatrix); + DrawIndexPointHandles(nativeSpline, splineData.Roll); + DrawDataPointHandles(nativeSpline, splineData.Roll); + } + } + + int DrawIndexPointHandles(NativeSpline spline, SplineData splineData) + { + int anchorId = GUIUtility.GetControlID(FocusType.Passive); + spline.DataPointHandles(splineData); + int nearestIndex = ControlIdToIndex(anchorId, HandleUtility.nearestControl, splineData.Count); + if (nearestIndex >= 0) + DrawTooltip(spline, splineData, nearestIndex); + + // Return the index that's being changed, or -1 + return ControlIdToIndex(anchorId, GUIUtility.hotControl, splineData.Count); + + // Local function + static int ControlIdToIndex(int anchorId, int controlId, int targetCount) + { + int index = controlId - anchorId - 2; + return index >= 0 && index < targetCount ? index : -1; + } + } + + // inverse pre-calculation optimization + readonly Quaternion m_DefaultHandleOrientation = Quaternion.Euler(270, 0, 0); + readonly Quaternion m_DefaultHandleOrientationInverse = Quaternion.Euler(90, 0, 0); + + int DrawDataPointHandles(NativeSpline spline, SplineData splineData) + { + int changed = -1; + for (var i = 0; i < splineData.Count; ++i) + { + var dataPoint = splineData[i]; + var t = SplineUtility.GetNormalizedInterpolation(spline, dataPoint.Index, splineData.PathIndexUnit); + spline.Evaluate(t, out var position, out var tangent, out var up); + + var id = GUIUtility.GetControlID(FocusType.Passive); + if (DrawDataPoint(id, position, tangent, up, dataPoint.Value, out var result)) + { + dataPoint.Value = result; + splineData.SetDataPoint(i, dataPoint); + changed = i; + } + if (id == HandleUtility.nearestControl || id == GUIUtility.hotControl) + DrawTooltip(spline, splineData, i); + } + return changed; + + // local function + bool DrawDataPoint(int controlID, Vector3 position, Vector3 tangent, Vector3 up, float rollData, out float result) + { + result = 0; + if (tangent == Vector3.zero) + return false; + + var drawMatrix = Handles.matrix * Matrix4x4.TRS(position, Quaternion.LookRotation(tangent, up), Vector3.one); + using (new Handles.DrawingScope(drawMatrix)) // use draw matrix, so we work in local space + { + var localRot = Quaternion.Euler(0, rollData, 0); + var globalRot = m_DefaultHandleOrientation * localRot; + + var handleSize = HandleUtility.GetHandleSize(Vector3.zero) / 2f; + if (Event.current.type == EventType.Repaint) + Handles.ArrowHandleCap(-1, Vector3.zero, globalRot, handleSize, EventType.Repaint); + + var newGlobalRot = Handles.Disc(controlID, globalRot, Vector3.zero, Vector3.forward, handleSize, false, 0); + if (GUIUtility.hotControl == controlID) + { + // Handles.Disc returns roll values in the [0, 360] range. Therefore, it works only in fixed ranges + // For example, within any of these ..., [-720, -360], [-360, 0], [0, 360], [360, 720], ... + // But we want to be able to rotate through these ranges, and not get stuck. We can detect when to + // move between ranges: when the roll delta is big. e.g. 359 -> 1 (358), instead of 1 -> 2 (1) + var newLocalRot = m_DefaultHandleOrientationInverse * newGlobalRot; + var deltaRoll = newLocalRot.eulerAngles.y - localRot.eulerAngles.y; + if (deltaRoll > 180) + deltaRoll -= 360; // Roll down one range + else if (deltaRoll < -180) + deltaRoll += 360; // Roll up one range + + rollData += deltaRoll; + result = rollData; + return true; + } + } + return false; + } + } + + void DrawTooltip(NativeSpline spline, SplineData splineData, int index) + { + var dataPoint = splineData[index]; + var text = $"Index: {dataPoint.Index}\nRoll: {dataPoint.Value.Value}"; + + var t = SplineUtility.GetNormalizedInterpolation(spline, dataPoint.Index, splineData.PathIndexUnit); + spline.Evaluate(t, out var position, out _, out _); + CinemachineSceneToolHelpers.DrawLabel(position, text); + } + } } diff --git a/com.unity.cinemachine/Editor/Overlays.meta b/com.unity.cinemachine/Editor/Overlays.meta deleted file mode 100644 index 89dc173bc..000000000 --- a/com.unity.cinemachine/Editor/Overlays.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 63479c7bfc24449f6b3404b4cd971413 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs b/com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs deleted file mode 100644 index 52d876bff..000000000 --- a/com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using UnityEditor; -using UnityEditor.EditorTools; -using UnityEditor.Splines; -using UnityEngine; -using UnityEngine.Splines; - -namespace Unity.Cinemachine.Editor -{ - [EditorTool("Roll Tool", typeof(CinemachineSplineRoll))] - sealed class SplineRollTool : EditorTool, IDrawSelectedHandles - { - GUIContent m_IconContent; - public override GUIContent toolbarIcon => m_IconContent; - - bool m_DisableHandles; - bool m_RollInUse; - bool m_IsSelected; - - void OnEnable() - { - m_IconContent = new GUIContent - { - image = AssetDatabase.LoadAssetAtPath( - CinemachineCore.kPackageRoot + "/Editor/EditorResources/Icons/CmTrack@256.png"), - text = "Roll Tool", - tooltip = "Adjust the roll data points along the spline." - }; - } - - /// This is called when the Tool is selected in the editor. - public override void OnActivated() - { - base.OnActivated(); - m_IsSelected = true; - } - - /// This is called when the Tool is deselected in the editor. - public override void OnWillBeDeactivated() - { - base.OnWillBeDeactivated(); - m_IsSelected = false; - } - - const float k_UnselectedAlpha = 0.5f; - public override void OnToolGUI(EditorWindow window) - { - var splineDataTarget = target as CinemachineSplineRoll; - if (splineDataTarget == null || splineDataTarget.Container == null) - return; - - var nativeSpline = new NativeSpline(splineDataTarget.Container.Spline, splineDataTarget.Container.transform.localToWorldMatrix); - - Undo.RecordObject(splineDataTarget, "Modifying Roll SplineData"); - m_DisableHandles = false; - var color = Handles.selectedColor; - if (!m_IsSelected) - color.a = k_UnselectedAlpha; - using (new Handles.DrawingScope(color)) - { - m_RollInUse = DrawRollDataPoints(nativeSpline, splineDataTarget.Roll); - nativeSpline.DataPointHandles(splineDataTarget.Roll); - } - } - - public void OnDrawHandles() - { - var splineDataTarget = target as CinemachineSplineRoll; - if (ToolManager.IsActiveTool(this) || splineDataTarget == null || splineDataTarget.Container == null) - return; - - if (Event.current.type != EventType.Repaint) - return; - - m_DisableHandles = true; - var nativeSpline = new NativeSpline(splineDataTarget.Container.Spline, - splineDataTarget.Container.transform.localToWorldMatrix); - - var color = Handles.selectedColor; - if (!m_IsSelected) - color.a = k_UnselectedAlpha; - using (new Handles.DrawingScope(color)) - { - DrawRollDataPoints(nativeSpline, splineDataTarget.Roll); - } - } - - // inverse pre-calculation optimization - readonly Quaternion m_DefaultHandleOrientation = Quaternion.Euler(270, 0, 0); - readonly Quaternion m_DefaultHandleOrientationInverse = Quaternion.Euler(90, 0, 0); - bool DrawRollDataPoints(NativeSpline spline, SplineData rollSplineData) - { - var inUse = false; - for (var r = 0; r < rollSplineData.Count; ++r) - { - var dataPoint = rollSplineData[r]; - - var normalizedT = - SplineUtility.GetNormalizedInterpolation(spline, dataPoint.Index, rollSplineData.PathIndexUnit); - spline.Evaluate(normalizedT, out var position, out var tangent, out var up); - - var id = m_DisableHandles ? -1 : GUIUtility.GetControlID(FocusType.Passive); - if (DrawDataPoint(id, position, tangent, up, dataPoint.Value, out var result)) - { - dataPoint.Value = result; - rollSplineData[r] = dataPoint; - inUse = true; - } - } - return inUse; - - // local function - bool DrawDataPoint( - int controlID, Vector3 position, Vector3 tangent, Vector3 up, float rollData, out float result) - { - result = 0; - if (tangent == Vector3.zero) - return false; - - var drawMatrix = Matrix4x4.identity; - drawMatrix.SetTRS(position, Quaternion.LookRotation(tangent, up), Vector3.one); - - drawMatrix = Handles.matrix * drawMatrix; - using (new Handles.DrawingScope(drawMatrix)) // use draw matrix, so we work in local space - { - var handleLocalRotation = Quaternion.Euler(0, rollData, 0); - var handleWorldRotation = m_DefaultHandleOrientation * handleLocalRotation; - - var handleSize = Mathf.Max( - HandleUtility.GetHandleSize(Vector3.zero) / 2f, CinemachineSplineDollyPrefs.SplineWidth.Value); - if (Event.current.type == EventType.Repaint) { - using (new Handles.DrawingScope(m_RollInUse ? Handles.selectedColor : Handles.color)) - Handles.ArrowHandleCap(-1, Vector3.zero, handleWorldRotation, handleSize, EventType.Repaint); - } - - var newHandleRotationGlobalSpace = Handles.Disc(controlID, handleWorldRotation, - Vector3.zero, Vector3.forward, handleSize, false, 0); - if (GUIUtility.hotControl == controlID) - { - // Handles.Disc returns roll values in the [0, 360] range. Therefore, it works only in fixed ranges - // For example, within any of these ..., [-720, -360], [-360, 0], [0, 360], [360, 720], ... - // But we want to be able to rotate through these ranges, and not get stuck. We can detect when to - // move between ranges: when the roll delta is big. e.g. 359 -> 1 (358), instead of 1 -> 2 (1) - var newHandleRotationLocalSpace = m_DefaultHandleOrientationInverse * newHandleRotationGlobalSpace; - var deltaRoll = newHandleRotationLocalSpace.eulerAngles.y - handleLocalRotation.eulerAngles.y; - if (deltaRoll > 180) - deltaRoll -= 360; // Roll down one range - else if (deltaRoll < -180) - deltaRoll += 360; // Roll up one range - - rollData += deltaRoll; - result = rollData; - return true; - } - } - - return false; - } - } - } -} diff --git a/com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs.meta b/com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs.meta deleted file mode 100644 index 2d510d84a..000000000 --- a/com.unity.cinemachine/Editor/Overlays/SplineRollTool.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: c983fa0b031f4840826e1725e6c2195a -timeCreated: 1659434764 \ No newline at end of file diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs index 2fe3c9783..954aca550 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/FoldoutWithEnabledButtonPropertyDrawer.cs @@ -87,7 +87,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) { style = { flexGrow = 0, marginLeft = 8, alignSelf = Align.Center }, tooltip = childProperty.tooltip}); var childField = row.Contents.AddChild(new PropertyField(childProperty, "") { style = { flexGrow = 1, marginTop = -1, marginLeft = 5, marginBottom = -1 }}); - childLabel.AddDelayedFriendlyPropertyDragger(childProperty, childField); + childLabel.AddDelayedFriendlyPropertyDragger(childProperty, childField, true); childField.RemoveFromClassList(InspectorUtility.kAlignFieldClass); row.TrackPropertyWithInitialCallback(enabledProp, (p) => diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs index 0dc7a901d..37a55967c 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/InputAxisPropertyDrawer.cs @@ -26,7 +26,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var valueLabel = new Label(" ") { style = { minWidth = InspectorUtility.SingleLineHeight * 2}}; var valueField = new InspectorUtility.CompactPropertyField(valueProp, "") { style = { flexGrow = 1}}; valueField.OnInitialGeometry(() => valueField.SafeSetIsDelayed()); - valueLabel.AddDelayedFriendlyPropertyDragger(valueProp, valueField); + valueLabel.AddDelayedFriendlyPropertyDragger(valueProp, valueField, true); var ux = new InspectorUtility.FoldoutWithOverlay(foldout, valueField, valueLabel); @@ -38,7 +38,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) valueFieldRow.Contents.SafeSetIsDelayed(); valueFieldRow.Contents.Q().style.marginLeft = 0; }); - valueFieldRow.Label.AddDelayedFriendlyPropertyDragger(valueProp, valueFieldRow.Contents); + valueFieldRow.Label.AddDelayedFriendlyPropertyDragger(valueProp, valueFieldRow.Contents, true); var centerField = foldout.AddChild(new PropertyField(property.FindPropertyRelative(() => def.Center))); var rangeContainer = foldout.AddChild(new VisualElement() { style = { flexDirection = FlexDirection.Row }}); diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/SplineSettingsPropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/SplineSettingsPropertyDrawer.cs index 0bc0d32bb..c1fa791ca 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/SplineSettingsPropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/SplineSettingsPropertyDrawer.cs @@ -39,13 +39,13 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) var oldUnits = (PathIndexUnit)pU.enumValueIndex; if (oldUnits != newUnits) { - // Convert the position to the new units - var pS = o.FindProperty(splineProp.propertyPath); - var spline = pS.objectReferenceValue as SplineContainer; - if (spline.IsValid()) + var splineReferencer = targets[i] as ISplineReferencer; + var spline = splineReferencer?.SplineSettings.GetCachedSpline(); + if (spline != null) { + // Convert the position to the new units var pP = o.FindProperty(positionProp.propertyPath); - pP.floatValue = spline.Spline.ConvertIndexUnit(pP.floatValue, oldUnits, newUnits); + pP.floatValue = spline.ConvertIndexUnit(pP.floatValue, oldUnits, newUnits); } pU.enumValueIndex = (int)newUnits; o.ApplyModifiedProperties(); diff --git a/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs b/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs index a0fe66cba..48c933e19 100644 --- a/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs +++ b/com.unity.cinemachine/Editor/PropertyDrawers/Vector2AsRangePropertyDrawer.cs @@ -26,7 +26,7 @@ public override VisualElement CreatePropertyGUI(SerializedProperty property) minField.Q().style.marginLeft = 2; }); - label.AddDelayedFriendlyPropertyDragger(xProp, minField); + label.AddDelayedFriendlyPropertyDragger(xProp, minField, true); ux.Left.Add(label); ux.Right.Add(minField); ux.Right.Add(maxField); diff --git a/com.unity.cinemachine/Editor/Upgrader/UpgradeObjectToCm3.cs b/com.unity.cinemachine/Editor/Upgrader/UpgradeObjectToCm3.cs index 729e89100..8247bad8d 100644 --- a/com.unity.cinemachine/Editor/Upgrader/UpgradeObjectToCm3.cs +++ b/com.unity.cinemachine/Editor/Upgrader/UpgradeObjectToCm3.cs @@ -701,7 +701,7 @@ static void UpgradePath(CinemachinePathBase pathBase) spline = Undo.AddComponent(go); var splineRoll = Undo.AddComponent(go); - splineRoll.Roll = new SplineData(); + splineRoll.Roll = new SplineData(); Undo.RecordObject(pathBase, "Upgrader: disable obsolete"); pathBase.enabled = false; @@ -721,7 +721,7 @@ static void UpgradePath(CinemachinePathBase pathBase) TangentIn = -waypoints[i].tangent, TangentOut = waypoints[i].tangent, }); - splineRoll.Roll.Add(new DataPoint(i, waypoints[i].roll)); + splineRoll.Roll.Add(new DataPoint(i, waypoints[i].roll)); } break; } @@ -740,7 +740,7 @@ static void UpgradePath(CinemachinePathBase pathBase) TangentIn = -tangent, TangentOut = tangent, }); - splineRoll.Roll.Add(new DataPoint(i, waypoints[i].roll)); + splineRoll.Roll.Add(new DataPoint(i, waypoints[i].roll)); } break; } diff --git a/com.unity.cinemachine/Editor/Utility/CinemachineSceneTools.cs b/com.unity.cinemachine/Editor/Utility/CinemachineSceneTools.cs index ac0abfdff..2c1b36442 100644 --- a/com.unity.cinemachine/Editor/Utility/CinemachineSceneTools.cs +++ b/com.unity.cinemachine/Editor/Utility/CinemachineSceneTools.cs @@ -89,7 +89,7 @@ static class CinemachineSceneToolHelpers public static readonly Color HelperLineDefaultColor = new Color(255, 255, 255, 25); const float k_DottedLineSpacing = 4f; - static GUIStyle s_LabelStyle = new GUIStyle + static GUIStyle s_LabelStyle = new () { normal = { @@ -100,6 +100,12 @@ static class CinemachineSceneToolHelpers fontStyle = FontStyle.Bold, padding = new RectOffset(5, 0, 5, 0) }; + + public static string GetIconPath() + { + var skin = EditorGUIUtility.isProSkin ? "/Dark" : ""; + return $"{CinemachineCore.kPackageRoot}/Editor/EditorResources/Icons{skin}/"; + } public static float SliderHandleDelta(Vector3 newPos, Vector3 oldPos, Vector3 forward) { @@ -123,7 +129,7 @@ public static Vector3 PositionHandleDelta(Quaternion rot, Vector3 newPos, Vector public static void DrawLabel(Vector3 position, string text) { - var labelOffset = HandleUtility.GetHandleSize(position) / 5f; + var labelOffset = HandleUtility.GetHandleSize(position) / 2f; Handles.Label(position + new Vector3(0, -labelOffset, 0), text, s_LabelStyle); } diff --git a/com.unity.cinemachine/Editor/Overlays/CinemachineToolbarOverlay.cs b/com.unity.cinemachine/Editor/Utility/CinemachineToolbarOverlay.cs similarity index 97% rename from com.unity.cinemachine/Editor/Overlays/CinemachineToolbarOverlay.cs rename to com.unity.cinemachine/Editor/Utility/CinemachineToolbarOverlay.cs index 9913e79cb..d9eacae5f 100644 --- a/com.unity.cinemachine/Editor/Overlays/CinemachineToolbarOverlay.cs +++ b/com.unity.cinemachine/Editor/Utility/CinemachineToolbarOverlay.cs @@ -1,11 +1,6 @@ -using System; using UnityEditor; using UnityEditor.EditorTools; -using UnityEditor.Overlays; -using UnityEditor.Toolbars; using UnityEngine; -using UnityEngine.UIElements; -using System.Collections.Generic; namespace Unity.Cinemachine.Editor { @@ -25,7 +20,7 @@ namespace Unity.Cinemachine.Editor /// /// To check, if a tool has been enabled/disabled in the editor script, use CinemachineSceneToolUtility.IsToolActive. /// - public abstract class CinemachineTool : EditorTool, IDrawSelectedHandles + abstract class CinemachineTool : EditorTool, IDrawSelectedHandles { GUIContent m_IconContent; @@ -148,7 +143,7 @@ protected override GUIContent GetIcon() => /// [Overlay(typeof(SceneView), "Cinemachine Tool Settings")] [Icon(CinemachineCore.kPackageRoot + "/Editor/EditorResources/Icons/CmCamera@256.png")] - public class CinemachineToolSettingsOverlay : Overlay, ICreateToolbar + class CinemachineToolSettingsOverlay : Overlay, ICreateToolbar { static readonly string[] k_CmToolbarItems = { OrbitalFollowOrbitSelection.id }; diff --git a/com.unity.cinemachine/Editor/Overlays/CinemachineToolbarOverlay.cs.meta b/com.unity.cinemachine/Editor/Utility/CinemachineToolbarOverlay.cs.meta similarity index 100% rename from com.unity.cinemachine/Editor/Overlays/CinemachineToolbarOverlay.cs.meta rename to com.unity.cinemachine/Editor/Utility/CinemachineToolbarOverlay.cs.meta diff --git a/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs b/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs index 1087a861e..e524e0cbb 100644 --- a/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs +++ b/com.unity.cinemachine/Editor/Utility/InspectorUtility.cs @@ -530,7 +530,7 @@ public static void AddSpace(this VisualElement ux) /// /// Add a property dragger to a float or int label, so that dragging it changes the property value. /// - public static void AddDelayedFriendlyPropertyDragger(this Label label, SerializedProperty p, VisualElement field) + public static void AddDelayedFriendlyPropertyDragger(this Label label, SerializedProperty p, VisualElement field, bool noDelayOnDrag) { if (p.propertyType == SerializedPropertyType.Float || p.propertyType == SerializedPropertyType.Integer) @@ -538,10 +538,20 @@ public static void AddDelayedFriendlyPropertyDragger(this Label label, Serialize label.AddToClassList("unity-base-field__label--with-dragger"); label.OnInitialGeometry(() => { - if (p.propertyType == SerializedPropertyType.Float) - new DelayedFriendlyFieldDragger(field.Q()).SetDragZone(label); - else if (p.propertyType == SerializedPropertyType.Integer) - new DelayedFriendlyFieldDragger(field.Q()).SetDragZone(label); + if (noDelayOnDrag) + { + if (p.propertyType == SerializedPropertyType.Float) + new DelayedFriendlyFieldDragger(field.Q()).SetDragZone(label); + else if (p.propertyType == SerializedPropertyType.Integer) + new DelayedFriendlyFieldDragger(field.Q()).SetDragZone(label); + } + else + { + if (p.propertyType == SerializedPropertyType.Float) + new FieldMouseDragger(field.Q()).SetDragZone(label); + else if (p.propertyType == SerializedPropertyType.Integer) + new FieldMouseDragger(field.Q()).SetDragZone(label); + } }); } } @@ -770,12 +780,13 @@ public CompactPropertyField(SerializedProperty property) : this(property, proper public CompactPropertyField(SerializedProperty property, string label, float minLabelWidth = 0) { style.flexDirection = FlexDirection.Row; + style.flexGrow = 1; if (!string.IsNullOrEmpty(label)) Label = AddChild(this, new Label(label) { tooltip = property?.tooltip, style = { alignSelf = Align.Center, minWidth = minLabelWidth }}); - Field = AddChild(this, new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = 10 } }); + Field = AddChild(this, new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = 20 } }); if (Label != null) - AddDelayedFriendlyPropertyDragger(Label, property, Field); + AddDelayedFriendlyPropertyDragger(Label, property, Field, true); } } @@ -788,7 +799,7 @@ public static LabeledRow PropertyRow( var row = new LabeledRow(label ?? property.displayName, property.tooltip); var field = propertyField = row.Contents.AddChild(new PropertyField(property, "") { style = { flexGrow = 1, flexBasis = SingleLineHeight * 5 }}); - AddDelayedFriendlyPropertyDragger(row.Label, property, propertyField); + AddDelayedFriendlyPropertyDragger(row.Label, property, propertyField, true); // Kill any left margin that gets inserted into the property field field.OnInitialGeometry(() => @@ -844,5 +855,16 @@ public static void AddRemainingProperties(VisualElement ux, SerializedProperty p while (p.NextVisible(false)); } } + + public static bool IsAncestorOf(this Transform p, Transform other) + { + while (other != null && p != null) + { + if (other == p) + return true; + other = other.parent; + } + return false; + } } } diff --git a/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs b/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs new file mode 100644 index 000000000..c33d55e55 --- /dev/null +++ b/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs @@ -0,0 +1,118 @@ +using System; +using System.Reflection; +using UnityEditor; +using UnityEditor.UIElements; +using UnityEngine; +using UnityEngine.Splines; +using UnityEngine.UIElements; + +namespace Unity.Cinemachine.Editor +{ + static class SplineDataInspectorUtility + { + public delegate ISplineContainer GetSplineDelegate(); + + public static VisualElement CreatePathUnitField(SerializedProperty splineDataProp, GetSplineDelegate getSpline) + { + var indexUnitProp = splineDataProp.FindPropertyRelative("m_IndexUnit"); + var enumField = new EnumField(indexUnitProp.displayName, (PathIndexUnit)indexUnitProp.enumValueIndex) + { tooltip = "Defines how to interpret the Index field for each data point. " + + "Knot is the recommended value because it remains robust if the spline points change." }; + enumField.RegisterValueChangedCallback((evt) => + { + var newIndexUnit = (PathIndexUnit)evt.newValue; + var spline = getSpline?.Invoke(); + if (spline != null && newIndexUnit != (PathIndexUnit)indexUnitProp.intValue) + { + Undo.RecordObject(splineDataProp.serializedObject.targetObject, "Change Index Unit"); + splineDataProp.serializedObject.Update(); + ConvertPathUnit(splineDataProp, spline, 0, newIndexUnit); + splineDataProp.serializedObject.ApplyModifiedProperties(); + } + }); + enumField.TrackPropertyValue(indexUnitProp, (p) => enumField.value = (PathIndexUnit)indexUnitProp.enumValueIndex); + enumField.TrackAnyUserActivity(() => enumField.SetEnabled(getSpline?.Invoke() != null)); + + return enumField; + } + + static void ConvertPathUnit( + SerializedProperty splineDataProp, + ISplineContainer container, int splineIndex, PathIndexUnit newIndexUnit) + { + if (container == null || container.Splines.Count == 0) + return; + var arrayProp = splineDataProp.FindPropertyRelative("m_DataPoints"); + var pathUnitProp = splineDataProp.FindPropertyRelative("m_IndexUnit"); + var from = (PathIndexUnit)Enum.GetValues(typeof(PathIndexUnit)).GetValue(pathUnitProp.enumValueIndex); + + var spline = container.Splines[splineIndex]; + var transform = container is Component component ? component.transform.localToWorldMatrix : Matrix4x4.identity; + using var native = new NativeSpline(spline, transform); + for (int i = 0, c = arrayProp.arraySize; i < c; ++i) + { + var point = arrayProp.GetArrayElementAtIndex(i); + var index = point.FindPropertyRelative("m_Index"); + index.floatValue = native.ConvertIndexUnit(index.floatValue, from, newIndexUnit); + } + pathUnitProp.enumValueIndex = (int)newIndexUnit; + } + + public static ListView CreateDataListField( + SplineData splineData, + SerializedProperty splineDataProp, + GetSplineDelegate getSpline) + { + var sortMethod = splineData.GetType().GetMethod("ForceSort", BindingFlags.Instance | BindingFlags.NonPublic); + var dirtyMethod = splineData.GetType().GetMethod("SetDirty", BindingFlags.Instance | BindingFlags.NonPublic); + + var pathUnitProp = splineDataProp.FindPropertyRelative("m_IndexUnit"); + var arrayProp = splineDataProp.FindPropertyRelative("m_DataPoints"); + + var list = new ListView() + { + reorderable = false, + showFoldoutHeader = false, + showBoundCollectionSize = false, + showAddRemoveFooter = true + }; + list.BindProperty(arrayProp); + + list.TrackPropertyValue(arrayProp, (p) => + { + Undo.RecordObject(p.serializedObject.targetObject, "Sort Spline Data"); + + // Make sure the indexes are properly wrapped around at the bondaries of a loop + SanitizePathUnit(splineDataProp, getSpline?.Invoke(), 0); + p.serializedObject.ApplyModifiedProperties(); + + // Sort the array + sortMethod?.Invoke(splineData, null); + p.serializedObject.Update(); + dirtyMethod?.Invoke(splineData, null); + p.serializedObject.ApplyModifiedProperties(); + }); + return list; + } + + static void SanitizePathUnit(SerializedProperty splineDataProp, ISplineContainer container, int splineIndex) + { + if (container == null || container.Splines.Count == 0) + return; + + var arrayProp = splineDataProp.FindPropertyRelative("m_DataPoints"); + var pathUnitProp = splineDataProp.FindPropertyRelative("m_IndexUnit"); + var unit = (PathIndexUnit)Enum.GetValues(typeof(PathIndexUnit)).GetValue(pathUnitProp.enumValueIndex); + + var spline = container.Splines[splineIndex]; + var transform = container is Component component ? component.transform : null; + var scaledSpline = new CachedScaledSpline(spline, transform, Collections.Allocator.Temp); + for (int i = 0, c = arrayProp.arraySize; i < c; ++i) + { + var point = arrayProp.GetArrayElementAtIndex(i); + var index = point.FindPropertyRelative("m_Index"); + index.floatValue = scaledSpline.StandardizePosition(index.floatValue, unit, out _); + } + } + } +} diff --git a/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs.meta b/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs.meta new file mode 100644 index 000000000..41e9bbfa3 --- /dev/null +++ b/com.unity.cinemachine/Editor/Utility/SplineDataInspectorUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: add4c76e08cd35f4d973fbe94b8553e8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineCart.cs b/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineCart.cs index 56dc9a117..5bf546eb8 100644 --- a/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineCart.cs +++ b/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineCart.cs @@ -13,12 +13,13 @@ namespace Unity.Cinemachine [DisallowMultipleComponent] [AddComponentMenu("Cinemachine/Helpers/Cinemachine Spline Cart")] [HelpURL(Documentation.BaseURL + "manual/CinemachineSplineCart.html")] - public class CinemachineSplineCart : MonoBehaviour + public class CinemachineSplineCart : MonoBehaviour, ISplineReferencer { /// /// Holds the Spline container, the spline position, and the position unit type /// - public SplineSettings SplineSettings = new () { Units = PathIndexUnit.Normalized }; + [SerializeField, FormerlySerializedAs("SplineSettings")] + SplineSettings m_SplineSettings = new () { Units = PathIndexUnit.Normalized }; /// This enum defines the options available for the update method. public enum UpdateMethods @@ -44,18 +45,23 @@ public enum UpdateMethods [Tooltip("Used only by Automatic Dolly settings that require it")] public Transform TrackingTarget; + CinemachineSplineRoll.RollCache m_RollCache; + + /// + public ref SplineSettings SplineSettings => ref m_SplineSettings; + /// The Spline container to which the cart will be constrained. public SplineContainer Spline { - get => SplineSettings.Spline; - set => SplineSettings.Spline = value; + get => m_SplineSettings.Spline; + set => m_SplineSettings.Spline = value; } /// The cart's current position on the spline, in spline position units public float SplinePosition { - get => SplineSettings.Position; - set => SplineSettings.Position = value; + get => m_SplineSettings.Position; + set => m_SplineSettings.Position = value; } /// How to interpret PositionOnSpline: @@ -65,11 +71,10 @@ public float SplinePosition /// interpolation between the specific knot index and the next knot." public PathIndexUnit PositionUnits { - get => SplineSettings.Units; - set => SplineSettings.ChangeUnitPreservePosition(value); + get => m_SplineSettings.Units; + set => m_SplineSettings.ChangeUnitPreservePosition(value); } - CinemachineSplineRoll m_RollCache; // don't use this directly - use SplineRoll // In-editor only: CM 3.0.x Legacy support ================================= [SerializeField, HideInInspector, FormerlySerializedAs("SplinePosition")] private float m_LegacyPosition = -1; @@ -79,14 +84,14 @@ void PerformLegacyUpgrade() { if (m_LegacyPosition != -1) { - SplineSettings.Position = m_LegacyPosition; - SplineSettings.Units = m_LegacyUnits; + m_SplineSettings.Position = m_LegacyPosition; + m_SplineSettings.Units = m_LegacyUnits; m_LegacyPosition = -1; m_LegacyUnits = 0; } if (m_LegacySpline != null) { - SplineSettings.Spline = m_LegacySpline; + m_SplineSettings.Spline = m_LegacySpline; m_LegacySpline = null; } } @@ -100,7 +105,7 @@ private void OnValidate() void Reset() { - SplineSettings = new SplineSettings { Units = PathIndexUnit.Normalized }; + m_SplineSettings = new SplineSettings { Units = PathIndexUnit.Normalized }; UpdateMethod = UpdateMethods.Update; AutomaticDolly.Method = null; TrackingTarget = null; @@ -108,10 +113,12 @@ void Reset() void OnEnable() { - RefreshRollCache(); + m_RollCache.Refresh(this); AutomaticDolly.Method?.Reset(); } + void OnDisable() => SplineSettings.InvalidateCache(); + void FixedUpdate() { if (UpdateMethod == UpdateMethods.FixedUpdate) @@ -144,39 +151,15 @@ void UpdateCartPosition() void SetCartPosition(float distanceAlongPath) { - if (Spline.IsValid()) + var spline = m_SplineSettings.GetCachedSpline(); + if (spline != null) { - SplinePosition = Spline.Spline.StandardizePosition(distanceAlongPath, PositionUnits, Spline.Spline.GetLength()); - var t = Spline.Spline.ConvertIndexUnit(SplinePosition, PositionUnits, PathIndexUnit.Normalized); - Spline.EvaluateSplineWithRoll(SplineRoll, transform.rotation, t, out var pos, out var rot); + var splinePath = Spline.Splines[0]; + SplinePosition = spline.StandardizePosition(distanceAlongPath, PositionUnits, out _); + var t = splinePath.ConvertIndexUnit(SplinePosition, PositionUnits, PathIndexUnit.Normalized); + spline.EvaluateSplineWithRoll(Spline.transform, m_RollCache.GetSplineRoll(this), transform.rotation, t, out var pos, out var rot); transform.ConservativeSetPositionAndRotation(pos, rot); } } - - CinemachineSplineRoll SplineRoll - { - get - { -#if UNITY_EDITOR - if (!Application.isPlaying) - RefreshRollCache(); -#endif - return m_RollCache; - } - } - - void RefreshRollCache() - { - // check if we have CinemachineSplineRoll - TryGetComponent(out m_RollCache); -#if UNITY_EDITOR - // need to tell CinemachineSplineRoll about its spline for gizmo drawing purposes - if (m_RollCache != null) - m_RollCache.Container = Spline; -#endif - // check if our spline has CinemachineSplineRoll - if (Spline != null && m_RollCache == null) - Spline.TryGetComponent(out m_RollCache); - } } } diff --git a/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineRoll.cs b/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineRoll.cs index 253f9b431..ca761aaf4 100644 --- a/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineRoll.cs +++ b/com.unity.cinemachine/Runtime/Behaviours/CinemachineSplineRoll.cs @@ -5,11 +5,11 @@ namespace Unity.Cinemachine { /// - /// Extension that can be added to a SplineContainer or a vcam that uses a SplineContainer, for example a vcam + /// Extension that can be added to a SplineContainer or a CinemachineCamera that uses a SplineContainer, for example a CinemachineCamera /// that has SplineDolly as Body component. /// - When CinemachineSplineRoll is added to a gameObject that has SplineContainer, - /// then the roll affects any vcam that reads that SplineContainer globally. - /// - When CinemachineSplineRoll is added to a vcam, then roll only affects that vcam locally. + /// then the roll affects any CinemachineCamera that reads that SplineContainer globally. + /// - When CinemachineSplineRoll is added to a CinemachineCamera, then roll only affects that CinemachineCamera locally. /// [ExecuteInEditMode] [DisallowMultipleComponent] @@ -17,32 +17,91 @@ namespace Unity.Cinemachine [HelpURL(Documentation.BaseURL + "manual/CinemachineSplineRoll.html")] public class CinemachineSplineRoll : MonoBehaviour { + /// Structure to hold roll value for a specific location on the track. + [Serializable] + public struct RollData + { + /// + /// Roll (in degrees) around the forward direction for specific location on the track. + /// When placed on a SplineContainer, this is going to be a global override that affects all vcams using the Spline. + /// When placed on a CinemachineCamera, this is going to be a local override that only affects that CinemachineCamera. + /// + [Tooltip("Roll (in degrees) around the forward direction for specific location on the track.\n" + + "- When placed on a SplineContainer, this is going to be a global override that affects all vcams using the Spline.\n" + + "- When placed on a CinemachineCamera, this is going to be a local override that only affects that CinemachineCamera.")] + public float Value; + + /// Implicit conversion to float + /// The RollData setting to convert. + /// The value of the RollData setting. + public static implicit operator float(RollData roll) => roll.Value; + + /// Implicit conversion from float + /// The value with which to initialize the RollData setting. + /// A new RollData setting with the given value. + public static implicit operator RollData(float roll) => new () { Value = roll }; + } + + /// Interpolator for the RollData + public struct LerpRollData : IInterpolator + { + /// + public RollData Interpolate(RollData a, RollData b, float t) => new() { Value = Mathf.Lerp(a.Value, b.Value, t) }; + } + /// /// Roll (in degrees) around the forward direction for specific location on the track. /// When placed on a SplineContainer, this is going to be a global override that affects all vcams using the Spline. - /// When placed on a vcam, this is going to be a local override that only affects that vcam. + /// When placed on a CinemachineCamera, this is going to be a local override that only affects that CinemachineCamera. /// - [Tooltip("Roll (in degrees) around the forward direction for specific location on the track.\n" + - "- When placed on a SplineContainer, this is going to be a global override that affects all vcams using the Spline.\n" + - "- When placed on a vcam, this is going to be a local override that only affects that vcam.")] - public SplineData Roll; + [HideFoldout] + public SplineData Roll; #if UNITY_EDITOR // Only needed for drawing the gizmo - internal SplineContainer Container + internal ISplineContainer SplineContainer { get { // In case behaviour was re-parented in the editor, we check every time - TryGetComponent(out SplineContainer container); - if (container != null) - return container; - return m_SplineContainer; + if (TryGetComponent(out ISplineReferencer referencer)) + return referencer.SplineSettings.Spline == null || referencer.SplineSettings.Spline.Splines.Count == 0 + ? null : referencer.SplineSettings.Spline; + if (TryGetComponent(out ISplineContainer container)) + return container.Splines.Count > 0 ? container : null; + return null; } - set { m_SplineContainer = value; } } - SplineContainer m_SplineContainer; #endif void OnEnable() {} // Needed so we can disable it in the editor + + /// Cache for clients that use CinemachineSplineRoll + internal struct RollCache + { + CinemachineSplineRoll m_RollCache; + + public void Refresh(MonoBehaviour owner) + { + m_RollCache = null; + + // Check if owner has CinemachineSplineRoll + if (!owner.TryGetComponent(out m_RollCache) && owner is ISplineReferencer referencer) + { + // Check if the spline has CinemachineSplineRoll + var spline = referencer.SplineSettings.Spline; + if (spline != null && spline is Component component) + component.TryGetComponent(out m_RollCache); + } + } + + public CinemachineSplineRoll GetSplineRoll(MonoBehaviour owner) + { +#if UNITY_EDITOR + if (!Application.isPlaying) + Refresh(owner); +#endif + return m_RollCache; + } + } } } diff --git a/com.unity.cinemachine/Runtime/Components/CinemachineSplineDolly.cs b/com.unity.cinemachine/Runtime/Components/CinemachineSplineDolly.cs index 2aaa088cc..7c922800f 100644 --- a/com.unity.cinemachine/Runtime/Components/CinemachineSplineDolly.cs +++ b/com.unity.cinemachine/Runtime/Components/CinemachineSplineDolly.cs @@ -19,12 +19,13 @@ namespace Unity.Cinemachine [DisallowMultipleComponent] [CameraPipeline(CinemachineCore.Stage.Body)] [HelpURL(Documentation.BaseURL + "manual/CinemachineSplineDolly.html")] - public class CinemachineSplineDolly : CinemachineComponentBase + public class CinemachineSplineDolly : CinemachineComponentBase, ISplineReferencer { /// /// Holds the Spline container, the spline position, and the position unit type /// - public SplineSettings SplineSettings = new () { Units = PathIndexUnit.Normalized }; + [SerializeField, FormerlySerializedAs("SplineSettings")] + SplineSettings m_SplineSettings = new () { Units = PathIndexUnit.Normalized }; /// Where to put the camera relative to the spline position. X is perpendicular /// to the spline, Y is up, and Z is parallel to the spline. @@ -109,7 +110,7 @@ public struct DampingSettings Quaternion m_PreviousRotation; Vector3 m_PreviousPosition; - CinemachineSplineRoll m_RollCache; // don't use this directly - use SplineRoll + CinemachineSplineRoll.RollCache m_RollCache; // In-editor only: CM 3.0.x Legacy support ================================= [SerializeField, HideInInspector, FormerlySerializedAs("CameraPosition")] private float m_LegacyPosition = -1; @@ -119,24 +120,27 @@ void PerformLegacyUpgrade() { if (m_LegacyPosition != -1) { - SplineSettings.Position = m_LegacyPosition; - SplineSettings.Units = m_LegacyUnits; + m_SplineSettings.Position = m_LegacyPosition; + m_SplineSettings.Units = m_LegacyUnits; m_LegacyPosition = -1; m_LegacyUnits = 0; } if (m_LegacySpline != null) { - SplineSettings.Spline = m_LegacySpline; + m_SplineSettings.Spline = m_LegacySpline; m_LegacySpline = null; } } // ================================= + /// + public ref SplineSettings SplineSettings => ref m_SplineSettings; + /// The Spline container to which the camera will be constrained. public SplineContainer Spline { - get => SplineSettings.Spline; - set => SplineSettings.Spline = value; + get => m_SplineSettings.Spline; + set => m_SplineSettings.Spline = value; } /// The position along the spline at which the camera will be placed. This can be animated directly, @@ -144,8 +148,8 @@ public SplineContainer Spline /// The value is interpreted according to the Position Units setting. public float CameraPosition { - get => SplineSettings.Position; - set => SplineSettings.Position = value; + get => m_SplineSettings.Position; + set => m_SplineSettings.Position = value; } /// How to interpret the Spline Position: @@ -155,8 +159,8 @@ public float CameraPosition /// interpolation between the specific knot index and the next knot." public PathIndexUnit PositionUnits { - get => SplineSettings.Units; - set => SplineSettings.ChangeUnitPreservePosition(value); + get => m_SplineSettings.Units; + set => m_SplineSettings.ChangeUnitPreservePosition(value); } void OnValidate() @@ -171,21 +175,28 @@ void OnValidate() void Reset() { - SplineSettings = new SplineSettings { Units = PathIndexUnit.Normalized }; + m_SplineSettings = new SplineSettings { Units = PathIndexUnit.Normalized }; SplineOffset = Vector3.zero; CameraRotation = RotationMode.Default; Damping = default; AutomaticDolly.Method = null; } - /// Called when the behaviour is enabled. + /// protected override void OnEnable() { base.OnEnable(); - RefreshRollCache(); + m_RollCache.Refresh(this); AutomaticDolly.Method?.Reset(); } + /// + protected override void OnDisable() + { + m_SplineSettings.InvalidateCache(); + base.OnDisable(); + } + /// True if component is enabled and has a spline public override bool IsValid => enabled && Spline != null; @@ -208,12 +219,11 @@ public override void MutateCameraState(ref CameraState curState, float deltaTime if (!IsValid) return; - var splinePath = Spline.Spline; - if (splinePath == null || splinePath.Count == 0) + var spline = m_SplineSettings.GetCachedSpline(); + if (spline == null) return; - var pathLength = splinePath.GetLength(); - var splinePos = splinePath.StandardizePosition(CameraPosition, PositionUnits, pathLength); + var splinePos = spline.StandardizePosition(CameraPosition, PositionUnits, out var maxPos); // Init previous frame state info if (deltaTime < 0 || !VirtualCamera.PreviousStateIsValid) @@ -221,7 +231,7 @@ public override void MutateCameraState(ref CameraState curState, float deltaTime m_PreviousSplinePosition = splinePos; m_PreviousPosition = curState.RawPosition; m_PreviousRotation = curState.RawOrientation; - RefreshRollCache(); + m_RollCache.Refresh(this); } // Invoke AutoDolly algorithm to get new desired spline position @@ -233,19 +243,18 @@ public override void MutateCameraState(ref CameraState curState, float deltaTime if (Damping.Enabled && deltaTime >= 0 && VirtualCamera.PreviousStateIsValid) { // If spline is closed, we choose shortest path for damping - var max = splinePath.GetMaxPosition(PositionUnits, pathLength); var prev = m_PreviousSplinePosition; - if (splinePath.Closed && Mathf.Abs(splinePos - prev) > max * 0.5f) - prev += (splinePos > prev) ? max : -max; + if (spline.Closed && Mathf.Abs(splinePos - prev) > maxPos * 0.5f) + prev += (splinePos > prev) ? maxPos : -maxPos; // Do the damping splinePos = prev + Damper.Damp(splinePos - prev, Damping.Position.z, deltaTime); } m_PreviousSplinePosition = CameraPosition = splinePos; - Spline.EvaluateSplineWithRoll( - SplineRoll, m_PreviousRotation, - splinePath.ConvertIndexUnit(splinePos, PositionUnits, PathIndexUnit.Normalized), + spline.EvaluateSplineWithRoll( + Spline.transform, m_RollCache.GetSplineRoll(this), m_PreviousRotation, + spline.ConvertIndexUnit(splinePos, PositionUnits, PathIndexUnit.Normalized), out var newPos, out var newSplineRotation); // Apply the offset to get the new camera position @@ -304,31 +313,5 @@ Quaternion GetCameraRotationAtSplinePoint(Quaternion splineOrientation, Vector3 } return Quaternion.LookRotation(VirtualCamera.transform.rotation * Vector3.forward, up); } - - CinemachineSplineRoll SplineRoll - { - get - { -#if UNITY_EDITOR - if (!Application.isPlaying) - RefreshRollCache(); -#endif - return m_RollCache; - } - } - - void RefreshRollCache() - { - // check if we have CinemachineSplineRoll - TryGetComponent(out m_RollCache); -#if UNITY_EDITOR - // need to tell CinemachineSplineRoll about its spline for gizmo drawing purposes - if (m_RollCache != null) - m_RollCache.Container = Spline; -#endif - // check if our spline has CinemachineSplineRoll - if (Spline != null && m_RollCache == null) - Spline.TryGetComponent(out m_RollCache); - } } } diff --git a/com.unity.cinemachine/Runtime/Core/SplineContainerExtensions.cs b/com.unity.cinemachine/Runtime/Core/SplineContainerExtensions.cs index 9a179413b..c2401b3c5 100644 --- a/com.unity.cinemachine/Runtime/Core/SplineContainerExtensions.cs +++ b/com.unity.cinemachine/Runtime/Core/SplineContainerExtensions.cs @@ -11,7 +11,7 @@ static class SplineContainerExtensions /// Check spline container and child spline for null /// spline container to check /// true if container holds a non-null spline - public static bool IsValid(this SplineContainer spline) => spline != null && spline.Spline != null; + public static bool IsValid(this ISplineContainer spline) => spline != null && spline.Splines != null && spline.Splines.Count > 0; /// /// Apply to a additional roll from @@ -19,18 +19,19 @@ static class SplineContainerExtensions /// The spline in question /// The additional roll to apply /// The normalized position on the spline + /// Delegate to get the path length including scale /// returned point on the spline, in spline-local coords /// returned rotation at the point on the spline, in spline-local coords /// True if the spline position is valid public static bool LocalEvaluateSplineWithRoll( - this SplineContainer spline, + this CachedScaledSpline spline, CinemachineSplineRoll roll, Quaternion defaultRotation, float tNormalized, out Vector3 position, out Quaternion rotation) { - if (spline.Spline == null || !SplineUtility.Evaluate( - spline.Spline, tNormalized, out var localPosition, out var localTangent, out var localUp)) + if (spline == null || !SplineUtility.Evaluate( + spline, tNormalized, out var localPosition, out var localTangent, out var localUp)) { position = Vector3.zero; rotation = Quaternion.identity; @@ -63,8 +64,8 @@ public static bool LocalEvaluateSplineWithRoll( // Apply extra roll if (roll != null && roll.enabled) { - float rollValue = roll.Roll.Evaluate(spline.Spline, tNormalized, - PathIndexUnit.Normalized, new UnityEngine.Splines.Interpolators.LerpFloat()); + float rollValue = roll.Roll.Evaluate(spline, tNormalized, + PathIndexUnit.Normalized, new CinemachineSplineRoll.LerpRollData()); rotation = Quaternion.AngleAxis(-rollValue, fwd) * rotation; } return true; @@ -76,38 +77,37 @@ public static bool LocalEvaluateSplineWithRoll( /// The spline in question /// The additional roll to apply /// The normalized position on the spline + /// Delegate to get the path length including scale /// returned point on the spline, in world coords /// returned rotation at the point on the spline, in world coords /// True if the spline position is valid public static bool EvaluateSplineWithRoll( - this SplineContainer spline, + this CachedScaledSpline spline, + Transform transform, CinemachineSplineRoll roll, Quaternion defaultRotation, float tNormalized, out Vector3 position, out Quaternion rotation) { var result = LocalEvaluateSplineWithRoll(spline, roll, defaultRotation, tNormalized, out position, out rotation); - position = spline.transform.TransformPoint(position); - rotation = spline.transform.rotation * rotation; + position = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one).MultiplyPoint3x4(position); + rotation = transform.rotation * rotation; return result; } - + /// /// Get the maximum value for the spline position. Minimum value is always 0. /// /// The spline in question /// The spline position is expressed in these units - /// The length of the spline, in distance units. - /// Passed as parameter for efficiency because length calculation is slow. - /// If a negative value is passed, length will be calculated. + /// This is needed because we don't have access to the spline's scale. /// - public static float GetMaxPosition( - this Spline spline, PathIndexUnit unit, float splineLength = -1) + public static float GetMaxPosition(this CachedScaledSpline spline, PathIndexUnit unit) { switch (unit) { case PathIndexUnit.Distance: - return splineLength < 0 ? spline.GetLength() : splineLength; + return spline.GetLength(); case PathIndexUnit.Knot: { var knotCount = spline.Count; @@ -123,19 +123,16 @@ public static float GetMaxPosition( /// The spline in question /// Spline position to sanitize /// The spline position is expressed in these units - /// The length of the spline, in distance units. - /// Passed as parameter for efficiency because length calculation is slow. - /// If a negative value is passed, length will be calculated. + /// This is needed because we don't have access to the spline's scale. /// The clamped position value, respecting the specified units - public static float StandardizePosition( - this Spline spline, float t, PathIndexUnit unit, float splineLength = -1) + public static float StandardizePosition(this CachedScaledSpline spline, float t, PathIndexUnit unit, out float maxPos) { - var max = spline.GetMaxPosition(unit, splineLength); + maxPos = spline.GetMaxPosition(unit); if (!spline.Closed) - return Mathf.Clamp(t, 0, max); - t %= max; + return Mathf.Clamp(t, 0, maxPos); + t %= maxPos; if (t < 0) - t += max; + t += maxPos; return t; } } diff --git a/com.unity.cinemachine/Runtime/Core/SplineSettings.cs b/com.unity.cinemachine/Runtime/Core/SplineSettings.cs index 8c2318dc3..8ddd215e8 100644 --- a/com.unity.cinemachine/Runtime/Core/SplineSettings.cs +++ b/com.unity.cinemachine/Runtime/Core/SplineSettings.cs @@ -1,9 +1,21 @@ using System; +using System.Collections; +using System.Collections.Generic; +using Unity.Collections; +using Unity.Mathematics; using UnityEngine; using UnityEngine.Splines; namespace Unity.Cinemachine { + /// Interface for behaviours that reference a Spline + public interface ISplineReferencer + { + /// Get a reference to the SplineSettings struct contained in this object. + /// A reference to the embedded SplineSettings struct + public ref SplineSettings SplineSettings { get; } + } + /// /// This structure holds the spline reference and the position and position units. /// @@ -16,6 +28,7 @@ public struct SplineSettings /// The position along the spline. The actual value corresponding to a given point /// on the spline will depend on the unity type. + [NoSaveDuringPlay] [Tooltip("The position along the spline. The actual value corresponding to a given point " + "on the spline will depend on the unity type.")] public float Position; @@ -40,8 +53,146 @@ public struct SplineSettings public void ChangeUnitPreservePosition(PathIndexUnit newUnits) { if (Spline.IsValid() && newUnits != Units) - Position = Spline.Spline.ConvertIndexUnit(Position, Units, newUnits); + Position = GetCachedSpline().ConvertIndexUnit(Position, Units, newUnits); Units = newUnits; } + + CachedScaledSpline m_CachedSpline; + int m_CachedFrame; + + /// + /// Computing spline length dynamically is costly. This method computes the length on the first call + /// and caches it for subsequent calls. + /// + /// While we can auto-detect changes to the transform and some changes to the spline's knots, it would be + /// too costly to continually check for subtle changes to the spline's control points. Therefore, if such + /// subtle changes are made to the spline's control points at runtime, client is responsible + /// for calling InvalidateCache(). + /// + /// Cached version of the spline with transform incorporated + public CachedScaledSpline GetCachedSpline() + { + if (!Spline.IsValid()) + { + m_CachedSpline?.Dispose(); + m_CachedSpline = null; + } + else + { + // Only check crude validity once per frame, to keep things efficient + if (m_CachedSpline == null || (Time.frameCount != m_CachedFrame && !m_CachedSpline.IsCrudelyValid(Spline.Spline, Spline.transform))) + { + m_CachedSpline?.Dispose(); + m_CachedSpline = new CachedScaledSpline(Spline.Spline, Spline.transform); + } +#if UNITY_EDITOR + // Deep check only in editor and if not playing + else if (!Application.isPlaying && Time.frameCount != m_CachedFrame && !m_CachedSpline.KnotsAreValid(Spline.Spline, Spline.transform)) + m_CachedSpline = new CachedScaledSpline(Spline.Spline, Spline.transform); +#endif + m_CachedFrame = Time.frameCount; + } + return m_CachedSpline; + } + + /// + /// While we can auto-detect changes to the transform and some changes to the spline's knots, it would be + /// too costly to continually check for subtle changes to the spline's control points. Therefore, if such + /// subtle changes are made to the spline's control points at runtime, client is responsible + /// for calling InvalidateCache(). + /// + public void InvalidateCache() + { + m_CachedSpline?.Dispose(); + m_CachedSpline = null; + } + } + + + /// + /// In order to properly handle the spline scale, we need to cache a spline that incorporates the scale + /// natively. This class does that. + /// + public class CachedScaledSpline : ISpline, IDisposable + { + NativeSpline m_NativeSpline; + readonly Spline m_CachedSource; + readonly float m_CachedRawLength; + readonly Vector3 m_CachedScale; + bool m_IsAllocated; + + /// Construct a CachedScaledSpline + /// The spline to cache + /// The transform to use for the scale, or null + /// The allocator to use for the native spline + public CachedScaledSpline(Spline spline, Transform transform, Allocator allocator = Allocator.Persistent) + { + var scale = transform != null ? transform.lossyScale : Vector3.one; + m_CachedSource = spline; + m_NativeSpline = new NativeSpline(spline, Matrix4x4.Scale(scale), allocator); + m_CachedRawLength = spline.GetLength(); + m_CachedScale = scale; + m_IsAllocated = true; + } + + /// + public void Dispose() + { + if (m_IsAllocated) + m_NativeSpline.Dispose(); + m_IsAllocated = false; + } + + /// Check if the cached spline is still valid, without doing any costly knot checks. + /// The source spline + /// The source spline's transform, or null + /// True if the spline is crudely unchanged + public bool IsCrudelyValid(Spline spline, Transform transform) + { + var scale = transform != null ? transform.lossyScale : Vector3.one; + return spline == m_CachedSource && (m_CachedScale - scale).AlmostZero() + && m_NativeSpline.Count == m_CachedSource.Count + //&& Mathf.Abs(m_CachedRawLength - spline.GetLength()) < 0.001f; // this would catch almost everything but is it too expensive? + ; + } + + /// Performs costly knot check to see if the spline's knots have changed. + /// The source spline + /// The source spline's transform, or null + /// True if the knots have not changed + public bool KnotsAreValid(Spline spline, Transform transform) + { + if (m_NativeSpline.Count != spline.Count) + return false; + + var m = Matrix4x4.Scale(transform != null ? transform.lossyScale : Vector3.one); + var ita = GetEnumerator(); + var itb = spline.GetEnumerator(); + while (ita.MoveNext() && itb.MoveNext()) + if (!ita.Current.Equals(itb.Current.Transform(m))) + return false; + return true; + } + + /// + public BezierKnot this[int index] => m_NativeSpline[index]; + /// + public bool Closed => m_NativeSpline.Closed; + /// + public int Count => m_NativeSpline.Count; + /// + public BezierCurve GetCurve(int index) => m_NativeSpline.GetCurve(index); + /// + public float GetCurveInterpolation(int curveIndex, float curveDistance) => m_NativeSpline.GetCurveInterpolation(curveIndex, curveDistance); + /// + public float GetCurveLength(int index) => m_NativeSpline.GetCurveLength(index); + /// + public float3 GetCurveUpVector(int index, float t) => m_NativeSpline.GetCurveUpVector(index, t); + /// + public IEnumerator GetEnumerator() => m_NativeSpline.GetEnumerator(); + /// + public float GetLength() => m_NativeSpline.GetLength(); + /// + IEnumerator IEnumerable.GetEnumerator() => m_NativeSpline.GetEnumerator(); } } diff --git a/com.unity.cinemachine/Runtime/Deprecated/CinemachineDollyCart.cs b/com.unity.cinemachine/Runtime/Deprecated/CinemachineDollyCart.cs index 9caf7f4fb..2c430ec76 100644 --- a/com.unity.cinemachine/Runtime/Deprecated/CinemachineDollyCart.cs +++ b/com.unity.cinemachine/Runtime/Deprecated/CinemachineDollyCart.cs @@ -34,7 +34,9 @@ public enum UpdateMethod public UpdateMethod m_UpdateMethod = UpdateMethod.Update; /// How to interpret the Path Position - [Tooltip("How to interpret the Path Position. If set to Path Units, values are as follows: 0 represents the first waypoint on the path, 1 is the second, and so on. Values in-between are points on the path in between the waypoints. If set to Distance, then Path Position represents distance along the path.")] + [Tooltip("How to interpret the Path Position. If set to Path Units, values are as follows: 0 represents the " + + "first waypoint on the path, 1 is the second, and so on. Values in-between are points on the path in " + + "between the waypoints. If set to Distance, then Path Position represents distance along the path.")] public CinemachinePathBase.PositionUnits m_PositionUnits = CinemachinePathBase.PositionUnits.Distance; /// Move the cart with this speed @@ -43,7 +45,9 @@ public enum UpdateMethod public float m_Speed; /// The cart's current position on the path, in distance units - [Tooltip("The position along the path at which the cart will be placed. This can be animated directly or, if the velocity is non-zero, will be updated automatically. The value is interpreted according to the Position Units setting.")] + [Tooltip("The position along the path at which the cart will be placed. This can be animated directly or, " + + "if the velocity is non-zero, will be updated automatically. The value is interpreted according to the " + + "Position Units setting.")] [FormerlySerializedAs("m_CurrentDistance")] public float m_Position;