diff --git a/com.unity.cinemachine/CHANGELOG.md b/com.unity.cinemachine/CHANGELOG.md index 3f9b6425b..68310a138 100644 --- a/com.unity.cinemachine/CHANGELOG.md +++ b/com.unity.cinemachine/CHANGELOG.md @@ -17,7 +17,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - CinemachineGroupFraming now has a compatibility mode so that it can work with CinemachineConfiner2D out of the box. ### Added -- Added Recenter Target setting to CinemachinePanTilt +- New Rotation Control behaviour LookAtDataOnSpline lets you specify LookAt points at desird positions along a Spline Dolly trajectory. +- Added Recenter Target setting to CinemachinePanTilt. ## [3.1.0] - 2024-04-01 diff --git a/com.unity.cinemachine/Documentation~/CinemachineLookAtDataOnSpline.md b/com.unity.cinemachine/Documentation~/CinemachineLookAtDataOnSpline.md new file mode 100644 index 000000000..3d714ad11 --- /dev/null +++ b/com.unity.cinemachine/Documentation~/CinemachineLookAtDataOnSpline.md @@ -0,0 +1,26 @@ +# LookAt Data On Spline + +This CinemachineCamera __Rotation Control__ behaviour lets you assign LookAt targets to points on a spline, so that as the camera arrives at the position on the spline, it looks at the specified place. + +It's useful for creating curated dolly shots with specified aim targets along the way. This behaviour eliminates the need to provide rotation animations for the camera that are synchronized with the spline position animation. LookAt points are anchored to specific spline positions, and because they specify a LookAt target point, the appropriate rotation angles get computed dynamically. As a result, the rotation animation is more robust and less likely to break if the spline is modified. + +To use this behaviour, select it in the Rotation Control section of the CinemachineCamera inspector, or just add it manually to a CinemachineCamera. Note that a CinemachineSplineDolly behaviour is required in the Position Control section of the CinemachineCamera. Then, add Data Points to the array. + +### Scene View Tool + +When the LookAtDataOnSpline is selected in the inspector, a Scene View tool is provided to position the LookAt targets along the spline. The tool lets you add, remove, and reposition LookAt targets. + +![LookAt Data On Spline Tool](images/LookAtDataOnSplineTool.png) + + +### Properties + +| Property | Field | Description | +| --- | --- | --- | +| __Index Unit__ | | 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. | +| __Data Points__ | | The list of markup points on the spline. As the camera approaches these points, the corresponding fields will come into effect. | +| | _Index_ | The position on the Spline where the camera should look at the supplied point. The value is interpreted according to the _Index Unit_ setting. | +| | _Look At Point_ | The point that the camera should look at, in world space co-ordinates. | +| | _Easing_ | Controls how to ease in and out of this target. A value of 0 will linearly interpolate between LookAt points, while a value of 1 will slow down and briefly pause the rotation to look at the target. | + + diff --git a/com.unity.cinemachine/Documentation~/images/LookAtDataOnSplineTool.png b/com.unity.cinemachine/Documentation~/images/LookAtDataOnSplineTool.png new file mode 100644 index 000000000..597e14114 Binary files /dev/null and b/com.unity.cinemachine/Documentation~/images/LookAtDataOnSplineTool.png differ diff --git a/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackLookAt@256.png b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackLookAt@256.png new file mode 100644 index 000000000..9f019bb2e Binary files /dev/null and b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackLookAt@256.png differ diff --git a/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackLookAt@256.png.meta b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackLookAt@256.png.meta new file mode 100644 index 000000000..6d86c8ddd --- /dev/null +++ b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackLookAt@256.png.meta @@ -0,0 +1,114 @@ +fileFormatVersion: 2 +guid: 6a0027bb643b5ec42b50f812226666be +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/CmTrackRoll@256.png b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackRoll@256.png new file mode 100644 index 000000000..3d77e53ab Binary files /dev/null and b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackRoll@256.png differ diff --git a/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackRoll@256.png.meta b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackRoll@256.png.meta new file mode 100644 index 000000000..6a849ec07 --- /dev/null +++ b/com.unity.cinemachine/Editor/EditorResources/Icons/CmTrackRoll@256.png.meta @@ -0,0 +1,114 @@ +fileFormatVersion: 2 +guid: 9202b828e2d1cd549acbf7bb84ed61f7 +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/Editors/CinemachineLookAtDataOnSplineEditor.cs b/com.unity.cinemachine/Editor/Editors/CinemachineLookAtDataOnSplineEditor.cs new file mode 100644 index 000000000..ae49fc2b2 --- /dev/null +++ b/com.unity.cinemachine/Editor/Editors/CinemachineLookAtDataOnSplineEditor.cs @@ -0,0 +1,149 @@ +using UnityEditor; +using UnityEditor.EditorTools; +using UnityEditor.Splines; +using UnityEngine.UIElements; +using UnityEngine; +using UnityEngine.Splines; +using UnityEditor.UIElements; + +namespace Unity.Cinemachine.Editor +{ + [CustomEditor(typeof(CinemachineLookAtDataOnSpline))] + [CanEditMultipleObjects] + class CinemachineLookAtDataOnSplineEditor : CinemachineComponentBaseEditor + { + public override VisualElement CreateInspectorGUI() + { + var ux = new VisualElement(); + this.AddMissingCmCameraHelpBox(ux); + + var splineData = target as CinemachineLookAtDataOnSpline; + var invalidHelp = new HelpBox( + "This component requires a CinemachineSplineDolly component referencing a nonempty Spline", + HelpBoxMessageType.Warning); + ux.Add(invalidHelp); + ux.TrackAnyUserActivity(() => invalidHelp.SetVisible(splineData != null && !splineData.GetTargets(out _, out _))); + + var property = serializedObject.FindProperty(() => splineData.LookAtData); + ux.Add(new PropertyField(property.FindPropertyRelative("m_IndexUnit")) + { 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." }); + ux.Add(new PropertyField(property.FindPropertyRelative("m_DataPoints")) + { tooltip = "The list of markup points on the spline. As the camera approaches these points on the spline, " + + "the corresponding LookAt points will come into effect."}); + + return ux; + } + + [DrawGizmo(GizmoType.Active | GizmoType.NotInSelectionHierarchy + | GizmoType.InSelectionHierarchy | GizmoType.Pickable, typeof(CinemachineLookAtDataOnSpline))] + static void DrawGizmos(CinemachineLookAtDataOnSpline splineData, GizmoType selectionType) + { + // For performance reasons, we only draw a gizmo for the current active game object + if (Selection.activeGameObject == splineData.gameObject && splineData.LookAtData.Count > 0 + && splineData.GetTargets(out var spline, out _) && spline.Spline != null) + { + Gizmos.color = CinemachineCorePrefs.BoundaryObjectGizmoColour.Value; + + var indexUnit = splineData.LookAtData.PathIndexUnit; + for (int i = 0; i < splineData.LookAtData.Count; i++) + { + var t = SplineUtility.GetNormalizedInterpolation(spline.Spline, splineData.LookAtData[i].Index, indexUnit); + spline.Evaluate(t, out var position, out _, out _); + var p = splineData.LookAtData[i].Value.LookAtPoint; + Gizmos.DrawLine(position, p); + Gizmos.DrawSphere(p, HandleUtility.GetHandleSize(p) * 0.1f); + +#if false // Enable this for debugging easing + if (i > 0) + { + var oldColor = Gizmos.color; + Gizmos.color = Color.white; + var it = new CinemachineLookAtDataOnSpline.LerpRotation(); + for (float j = 0; j < 1f; j += 0.05f) + { + var item = it.Interpolate(splineData.LookAtData[i-1].Value, splineData.LookAtData[i].Value, j); + Gizmos.DrawLine(p, item.LookAtPoint); + p = item.LookAtPoint; + Gizmos.DrawSphere(p, HandleUtility.GetHandleSize(p) * 0.05f); + } + Gizmos.color = oldColor; + } +#endif + } + } + } + } + + [CustomPropertyDrawer(typeof(CinemachineLookAtDataOnSpline.Item))] + class CinemachineLookAtDataOnSplineItemPropertyDrawer : PropertyDrawer + { + public override VisualElement CreatePropertyGUI(SerializedProperty property) + { + CinemachineLookAtDataOnSpline.Item def = new (); + var ux = new VisualElement(); + ux.Add(new PropertyField(property.FindPropertyRelative(() => def.LookAtPoint))); + ux.Add(new PropertyField(property.FindPropertyRelative(() => def.Easing))); + return ux; + } + } + + + [EditorTool("LookAt Data On Spline Tool", typeof(CinemachineLookAtDataOnSpline))] + sealed class LookAtDataOnSplineTool : EditorTool + { + GUIContent m_IconContent; + public override GUIContent toolbarIcon => m_IconContent; + + bool GetTargets(out CinemachineLookAtDataOnSpline splineDataTarget, out SplineContainer spline, out CinemachineSplineDolly dolly) + { + splineDataTarget = target as CinemachineLookAtDataOnSpline; + if (splineDataTarget != null && splineDataTarget.GetTargets(out spline, out dolly)) + return true; + spline = null; + dolly = null; + return false; + } + + void OnEnable() + { + m_IconContent = new GUIContent + { + image = AssetDatabase.LoadAssetAtPath( + CinemachineCore.kPackageRoot + "/Editor/EditorResources/Icons/CmTrackLookAt@256.png"), + text = "LookAt Data On Spline Tool", + tooltip = "Assign LookAt points to points on the spline." + }; + } + + public override void OnToolGUI(EditorWindow window) + { + if (!GetTargets(out var splineDataTarget, out var spline, out _)) + return; + + Undo.RecordObject(splineDataTarget, "Modifying CinemachineLookAtDataOnSpline values"); + using (new Handles.DrawingScope(Handles.selectedColor)) + { + DrawDataPoints(splineDataTarget.LookAtData); + var nativeSpline = new NativeSpline(spline.Spline, spline.transform.localToWorldMatrix); + nativeSpline.DataPointHandles(splineDataTarget.LookAtData); + } + } + + void DrawDataPoints(SplineData splineData) + { + for (var r = 0; r < splineData.Count; ++r) + { + var dataPoint = splineData[r]; + var newPos = Handles.PositionHandle(dataPoint.Value.LookAtPoint, Quaternion.identity); + if (newPos != dataPoint.Value.LookAtPoint) + { + var item = dataPoint.Value; + item.LookAtPoint = newPos; + dataPoint.Value = item; + splineData[r] = dataPoint; + } + } + } + } +} diff --git a/com.unity.cinemachine/Editor/Editors/CinemachineLookAtDataOnSplineEditor.cs.meta b/com.unity.cinemachine/Editor/Editors/CinemachineLookAtDataOnSplineEditor.cs.meta new file mode 100644 index 000000000..7385c982f --- /dev/null +++ b/com.unity.cinemachine/Editor/Editors/CinemachineLookAtDataOnSplineEditor.cs.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1f8c114a760bf8d48866c11850dac920 +timeCreated: 1483406727 +licenseType: Pro +MonoImporter: + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.cinemachine/Runtime/Components/CinemachineLookAtDataOnSpline.cs b/com.unity.cinemachine/Runtime/Components/CinemachineLookAtDataOnSpline.cs new file mode 100644 index 000000000..c08776435 --- /dev/null +++ b/com.unity.cinemachine/Runtime/Components/CinemachineLookAtDataOnSpline.cs @@ -0,0 +1,104 @@ +using System; +using UnityEngine; +using UnityEngine.Splines; + +namespace Unity.Cinemachine +{ + /// + /// CinemachineLookAtDataOnSpline is a component that allows the camera to look at + /// specific points in the world as it moves along a spline. + /// + [ExecuteAlways, SaveDuringPlay] + [CameraPipeline(CinemachineCore.Stage.Aim)] + [AddComponentMenu("Cinemachine/Procedural/Rotation Control/Cinemachine Look At Data On Spline")] + [DisallowMultipleComponent] + [HelpURL(Documentation.BaseURL + "manual/CinemachineLookAtDataOnSpline.html")] + public class CinemachineLookAtDataOnSpline : CinemachineComponentBase + { + /// LookAt targets for the camera at specific points on the Spline + [Serializable] + public struct Item + { + /// The worldspace point to look at + [Tooltip("The point that the camera should look at, in world space co-ordinates.")] + public Vector3 LookAtPoint; + + /// Easing value for the Bezier curve. 0 is linear, 1 is smooth. + [Tooltip("Controls how to ease in and out of this data point. A value of 0 will linearly interpolate between " + + "LookAt points, while a value of 1 will slow down and briefly pause the rotation to look at the target.")] + [Range(0, 1)] + public float Easing; + } + + /// Interpolator for the LookAtData + internal struct LerpRotation : IInterpolator + { + public Item Interpolate(Item a, Item b, float t) + { + var p1 = Vector3.Lerp(Vector3.Lerp(a.LookAtPoint, b.LookAtPoint, 0.33f), a.LookAtPoint, a.Easing); + var p2 = Vector3.Lerp(Vector3.Lerp(b.LookAtPoint, a.LookAtPoint, 0.33f), b.LookAtPoint, b.Easing); + return new Item + { + LookAtPoint = SplineHelpers.Bezier3(t, a.LookAtPoint, p1, p2, b.LookAtPoint), + Easing = Mathf.Lerp(a.Easing, b.Easing, t) + }; + } + } + + /// LookAt targets for the camera at specific points on the Spline + [Tooltip("LookAt targets for the camera at specific points on the Spline")] + public SplineData LookAtData = new () { DefaultValue = new Item { Easing = 1 } }; + + void Reset() => LookAtData = new SplineData { DefaultValue = new Item { Easing = 1 } }; + + /// + public override bool IsValid => enabled && LookAtData != null && GetTargets(out _, out _); + + /// + public override CinemachineCore.Stage Stage => CinemachineCore.Stage.Aim; + + /// + public override void MutateCameraState(ref CameraState state, float deltaTime) + { + if (!GetTargets(out var spline, out var dolly)) + return; + + var splinePath = spline.Spline; + if (splinePath == null || splinePath.Count == 0) + return; + + var item = LookAtData.Evaluate(splinePath, dolly.CameraPosition, dolly.PositionUnits, new LerpRotation()); + var dir = item.LookAtPoint - state.RawPosition; + if (dir.sqrMagnitude > UnityVectorExtensions.Epsilon) + { + var up = state.ReferenceUp; + if (Vector3.Cross(dir, up).sqrMagnitude < UnityVectorExtensions.Epsilon) + { + // Look direction is parallel to the up vector + up = state.RawOrientation * Vector3.back; + if (Vector3.Cross(dir, up).sqrMagnitude < UnityVectorExtensions.Epsilon) + up = state.RawOrientation * Vector3.left; + } + state.RawOrientation = Quaternion.LookRotation(dir, up); + } + state.ReferenceLookAt = item.LookAtPoint; + } + + /// + /// API for the inspector: Get the spline and the required CinemachineTrackDolly component. + /// + /// The spline being augmented + /// The associated CinemachineTrackDolly component + /// + internal bool GetTargets(out SplineContainer spline, out CinemachineSplineDolly dolly) + { + if (TryGetComponent(out dolly)) + { + spline = dolly.Spline; + return spline != null && spline.Spline != null; + } + spline = null; + return false; + } + } +} diff --git a/com.unity.cinemachine/Runtime/Components/CinemachineLookAtDataOnSpline.cs.meta b/com.unity.cinemachine/Runtime/Components/CinemachineLookAtDataOnSpline.cs.meta new file mode 100644 index 000000000..7acc42896 --- /dev/null +++ b/com.unity.cinemachine/Runtime/Components/CinemachineLookAtDataOnSpline.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 28e2023ac4c6b204e9bad440d2cea142 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {fileID: 2800000, guid: 26663a5ec9987421996161615506b6c5, type: 3} + userData: + assetBundleName: + assetBundleVariant: