From 5a6fd4f9eb89f57da4cdf86bc1251ae186ab3c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Par=C3=A9-Vogt?= Date: Thu, 12 Feb 2026 14:21:26 -0500 Subject: [PATCH 1/3] Fix for UUM-131870 Core issue was a bugfix for the portals scene (fd5aafd083e92477eb2d711405e0f905dce0d903) to account for camera rotation when warping both a target and camera. The bugfix erroneously changed the OnForceCameraPosition function to rely on the target position (current transform) instead of the camera position. This meant that forcing the camera to look away from the target did not work. This fix corrects the original bugfix by reverting PreviousTargetPosition calculation method, but keeps the m_PreviousOffset calculation method from the bugfix, as this one is needed to account for rotations. --- .../Runtime/Core/TargetTracking.cs | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/com.unity.cinemachine/Runtime/Core/TargetTracking.cs b/com.unity.cinemachine/Runtime/Core/TargetTracking.cs index d30aa7cca..ca012357a 100644 --- a/com.unity.cinemachine/Runtime/Core/TargetTracking.cs +++ b/com.unity.cinemachine/Runtime/Core/TargetTracking.cs @@ -179,7 +179,6 @@ struct Tracker public Quaternion PreviousReferenceOrientation { get; private set; } Vector3 m_PreviousOffset; - Vector3 m_PreviousTargetPositionDampingOffset; Quaternion m_TargetOrientationOnAssign; Transform m_PreviousTarget; @@ -331,7 +330,6 @@ public void TrackTarget( outTargetPosition = PreviousTargetPosition = currentPosition; outTargetOrient = dampedRot; - m_PreviousTargetPositionDampingOffset = currentPosition - targetPosition; } Vector3 GetTargetPositionWithOffset( @@ -405,20 +403,20 @@ public void OnForceCameraPosition( BindingMode bindingMode, Vector3 targetOffset, ref CameraState newState) { - var state = component.VcamState; // old state - var prevOrient = GetReferenceOrientation( - component, bindingMode, targetOffset, newState.ReferenceUp, ref state); - var targetPos = GetTargetPositionWithOffset(component, bindingMode, targetOffset, prevOrient); + var oldState = component.VcamState; - var orient = GetReferenceOrientation( - component, bindingMode, targetOffset, newState.ReferenceUp, ref newState); - m_PreviousOffset = orient * (Quaternion.Inverse(prevOrient) * m_PreviousOffset); - PreviousReferenceOrientation = orient; + var oldOrient = GetReferenceOrientation(component, bindingMode, targetOffset, newState.ReferenceUp, ref oldState); + var newOrient = GetReferenceOrientation(component, bindingMode, targetOffset, newState.ReferenceUp, ref newState); - // Rotate the damping data also, to preserve position damping integrity - var deltaRot = newState.GetFinalOrientation() * Quaternion.Inverse(state.GetFinalOrientation()); - m_PreviousTargetPositionDampingOffset = deltaRot * m_PreviousTargetPositionDampingOffset; - PreviousTargetPosition = targetPos + m_PreviousTargetPositionDampingOffset; + // Recompute the target position based on the camera state and target offset. This has the effect + // of snapping the camera to camera position since the camera posistion will be computed from the target and + // the target offset during the next frame. + var cameraOffset = Quaternion.Inverse(oldState.GetFinalOrientation()) * (PreviousTargetPosition - oldState.GetFinalPosition()); + PreviousTargetPosition = newState.GetFinalOrientation() * cameraOffset + newState.GetFinalPosition(); + PreviousReferenceOrientation = newOrient; + + m_PreviousOffset = newOrient * (Quaternion.Inverse(oldOrient) * m_PreviousOffset); + var deltaRot = newState.GetFinalOrientation() * Quaternion.Inverse(oldState.GetFinalOrientation()); if (bindingMode == BindingMode.WorldSpace) m_PreviousOffset = deltaRot * m_PreviousOffset; } From eab0347cb78bca5bcf8783a30775b2e763d2cb4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Par=C3=A9-Vogt?= Date: Fri, 13 Feb 2026 12:07:23 -0500 Subject: [PATCH 2/3] Add test for UUM-131870 --- .../Runtime/CinemachineRuntimeFixtureBase.cs | 1 + .../Tests/Runtime/FollowForcePositionTests.cs | 46 +++++++++++++++++++ .../Runtime/FollowForcePositionTests.cs.meta | 2 + 3 files changed, 49 insertions(+) create mode 100644 com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs create mode 100644 com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs.meta diff --git a/com.unity.cinemachine/Tests/Runtime/CinemachineRuntimeFixtureBase.cs b/com.unity.cinemachine/Tests/Runtime/CinemachineRuntimeFixtureBase.cs index a5e56cc70..49c7c1e68 100644 --- a/com.unity.cinemachine/Tests/Runtime/CinemachineRuntimeFixtureBase.cs +++ b/com.unity.cinemachine/Tests/Runtime/CinemachineRuntimeFixtureBase.cs @@ -15,6 +15,7 @@ public class CinemachineRuntimeFixtureBase : CinemachineFixtureBase protected CinemachineBrain m_Brain; protected readonly FloatEqualityComparer m_FloatEqualityComparer = new(UnityVectorExtensions.Epsilon); protected readonly Vector3EqualityComparer m_Vector3EqualityComparer = new(UnityVectorExtensions.Epsilon); + protected readonly QuaternionEqualityComparer m_QuaternionComparer = new QuaternionEqualityComparer(1e-5f); [SetUp] public override void SetUp() diff --git a/com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs b/com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs new file mode 100644 index 000000000..29a24f190 --- /dev/null +++ b/com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs @@ -0,0 +1,46 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using Unity.Cinemachine; + +namespace Unity.Cinemachine.Tests +{ + [TestFixture] + public class FollowForcePositionTests : CinemachineRuntimeFixtureBase + { + CinemachineCamera m_Vcam; + GameObject m_FollowObject; + + [SetUp] + public override void SetUp() + { + base.SetUp(); + + m_Vcam = CreateGameObject("CM Vcam", typeof(CinemachineCamera)).GetComponent(); + m_FollowObject = CreateGameObject("Follow Object"); + } + + [UnityTest, Description("UUM-131870: ForceCameraPosition should allow looking away from the target.")] + public IEnumerator LookAwayFromTarget() + { + CinemachineCore.UniformDeltaTimeOverride = 0.0f; // we only test force position, not damping + var follow = m_Vcam.gameObject.AddComponent(); + follow.FollowOffset = new Vector3(0, 0, -10); + m_Vcam.Follow = m_FollowObject.transform; + + // Initial update to settle camera + m_FollowObject.transform.position = Vector3.zero; + yield return null; + Assume.That(m_Vcam.State.GetFinalPosition(), Is.EqualTo(follow.FollowOffset).Using(m_Vector3EqualityComparer)); + + // Move camera away from target + var forcedPos = new Vector3(0, 5, -5); + var forcedRot = Quaternion.identity; + m_Vcam.ForceCameraPosition(forcedPos, forcedRot); + yield return null; + Assert.That(m_Vcam.State.GetFinalPosition(), Is.EqualTo(forcedPos).Using(m_Vector3EqualityComparer)); + Assert.That(m_Vcam.State.GetFinalOrientation(), Is.EqualTo(forcedRot).Using(m_QuaternionComparer)); + } + } +} diff --git a/com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs.meta b/com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs.meta new file mode 100644 index 000000000..948472d7f --- /dev/null +++ b/com.unity.cinemachine/Tests/Runtime/FollowForcePositionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9106470f138959c43a0d029e42c4c217 From 4c7124f16cc89b17898a3dce03d7f6ff67e9d034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20Par=C3=A9-Vogt?= Date: Fri, 13 Feb 2026 14:04:32 -0500 Subject: [PATCH 3/3] Add Changelog entry for UUM-131870 --- com.unity.cinemachine/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/com.unity.cinemachine/CHANGELOG.md b/com.unity.cinemachine/CHANGELOG.md index 4cd12e9c1..f03e26323 100644 --- a/com.unity.cinemachine/CHANGELOG.md +++ b/com.unity.cinemachine/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixed error when both HDRP and URP rendering pipelines are added to a project.\ - Fixed outdated warning on RotationComposerComponent. - Fixed GC Alloc in CinemachineInputAxisController.Update(). +- Fixed ForceCameraPosition regression in CinemachineFollow/CinemachineOrbitalFollow/CinemachineTransposer/CinemachineOrbitalTransposer to allow looking away from targets. ### Changed - Converted code using InstanceID references and API to EntityID.