From f81e6b9ac07e5e7b2b056d7495c359f712730f7f Mon Sep 17 00:00:00 2001 From: Gregory Labute Date: Tue, 15 Jul 2025 11:46:33 -0400 Subject: [PATCH] Fix blend reversal logic and add unit test Improves the handling of reversing a blend-in-progress in CameraBlendStack by tracking the normalized blend position and adjusting the blend duration accordingly. Adds a comprehensive unit test in BlendManagerTests to verify correct behavior when repeatedly reversing blends between cameras. --- com.unity.cinemachine/CHANGELOG.md | 7 +++ .../Runtime/Core/CameraBlendStack.cs | 25 ++++---- .../Tests/Editor/BlendManagerTests.cs | 59 ++++++++++++++++++- com.unity.cinemachine/package.json | 2 +- 4 files changed, 76 insertions(+), 17 deletions(-) diff --git a/com.unity.cinemachine/CHANGELOG.md b/com.unity.cinemachine/CHANGELOG.md index d0847fc59..bd5ea9df9 100644 --- a/com.unity.cinemachine/CHANGELOG.md +++ b/com.unity.cinemachine/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [3.1.5] - 2025-012-31 +### Unreleased + +### Bugfixes +- Ensure that correct blend time is always used when backing out of a blend-in-progress. + + ## [3.1.4] - 2025-06-10 ### Bugfixes diff --git a/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs b/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs index 35f2f2dbd..a6431f861 100644 --- a/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs +++ b/com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs @@ -71,6 +71,9 @@ class StackFrame : NestedBlendSource ICinemachineCamera m_SnapshotSource; float m_SnapshotBlendWeight; + // If reversing a blend-in-progress, this will indicate how much of the blend was skipped + public float MidBlendNormalizedStartPoint; + public StackFrame() : base(new ()) {} public bool Active => Source.IsValid; @@ -234,7 +237,7 @@ public void UpdateRootFrame( if (activeCamera != outgoingCamera) { bool backingOutOfBlend = false; - float backingOutPercentCompleted = 0; + float normalizedBlendPosition = 0; float duration = 0; // Do we need to create a game-play blend? @@ -248,7 +251,8 @@ public void UpdateRootFrame( // Are we backing out of a blend-in-progress? backingOutOfBlend = frame.Source.CamA == activeCamera && frame.Source.CamB == outgoingCamera; if (backingOutOfBlend && frame.Blend.Duration > kEpsilon) - backingOutPercentCompleted = frame.Blend.TimeInBlend / frame.Blend.Duration; + normalizedBlendPosition = frame.MidBlendNormalizedStartPoint + + (1 - frame.MidBlendNormalizedStartPoint) * frame.Blend.TimeInBlend / frame.Blend.Duration; frame.Source.CamA = outgoingCamera; frame.Source.BlendCurve = blendDef.BlendCurve; @@ -282,8 +286,7 @@ public void UpdateRootFrame( snapshot = true; // Avoid nesting too deeply - var nbs = frame.Blend.CamA as NestedBlendSource; - if (!snapshot && nbs != null && nbs.Blend.CamA is NestedBlendSource nbs2) + if (!snapshot && frame.Blend.CamA is NestedBlendSource nbs && nbs.Blend.CamA is NestedBlendSource nbs2) nbs2.Blend.CamA = new SnapshotBlendSource(nbs2.Blend.CamA); // Special case: if backing out of a blend-in-progress @@ -292,15 +295,8 @@ public void UpdateRootFrame( if (backingOutOfBlend) { snapshot = true; // always use a snapshot for this to prevent pops - var adjustedDuration = frame.Blend.TimeInBlend; - if (nbs != null) - adjustedDuration += nbs.Blend.Duration - nbs.Blend.TimeInBlend; - else if (frame.Blend.CamA is SnapshotBlendSource sbs) - adjustedDuration += sbs.RemainingTimeInBlend; - - // In the event that the blend times in the different directions are different, - // don't make the blend longer than it would otherwise have been - duration = Mathf.Min(duration * backingOutPercentCompleted, adjustedDuration); + duration = duration * normalizedBlendPosition; // skip first part of blend + normalizedBlendPosition = 1 - normalizedBlendPosition; } // Chain to existing blend @@ -313,13 +309,14 @@ public void UpdateRootFrame( camA = new NestedBlendSource(blendCopy); } } - // For the event, we use the raw outgoing camera, not the blend source + // For the event, we use the raw outgoing camera, not the actual blend source frame.Blend.CamA = outgoingCamera; frame.Blend.CamB = activeCamera; frame.Blend.BlendCurve = frame.Source.BlendCurve; frame.Blend.Duration = duration; frame.Blend.TimeInBlend = 0; frame.Blend.CustomBlender = frame.Source.CustomBlender; + frame.MidBlendNormalizedStartPoint = normalizedBlendPosition; // Allow the client to modify the blend if (duration > 0) diff --git a/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs b/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs index 6e3b0629e..989ef031b 100644 --- a/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs +++ b/com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs @@ -12,11 +12,21 @@ class FakeCamera : ICinemachineCamera public FakeCamera(string name) => m_Name = name; public string Name => m_Name; public string Description => string.Empty; - public CameraState State => CameraState.Default; + public CameraState State + { + get + { + var state = CameraState.Default; + state.RawPosition = Position; + return state; + } + } public bool IsValid => true; public ICinemachineMixer ParentCamera => null; public void UpdateCameraState(Vector3 worldUp, float deltaTime) {} public void OnCameraActivated(ICinemachineCamera.ActivationEventParams evt) {} + + public Vector3 Position; } class FakeMixer : FakeCamera, ICinemachineMixer @@ -61,7 +71,7 @@ [TearDown] public void TearDown() void Reset(float blendTime) { m_BlendManager.LookupBlendDelegate = (outgoing, incoming) - => new (CinemachineBlendDefinition.Styles.EaseInOut, blendTime); // constant blend time + => new (CinemachineBlendDefinition.Styles.Linear, blendTime); // linear blend, constant blend time m_BlendManager.OnEnable(); ProcessFrame(null, 0.1f); ResetCounters(); @@ -224,5 +234,50 @@ public void TestEventsBlendToNestedBlend() Assert.AreEqual(1, m_BlendFinishedCount); Assert.That(m_BlendManager.IsBlending, Is.False); } + + [Test] + public void TestBlendReversal() + { + Reset(1); // constant blend time of 1 + m_Cam1.Position = new Vector3(0, 0, 0); + m_Cam2.Position = new Vector3(1, 0, 0); + + // Start with cam1 + ProcessFrame(m_Cam1, 0.1f); + Assert.That(m_BlendManager.IsBlending, Is.False); + + // Activate cam2 + ProcessFrame(m_Cam2, 0.5f); + Assert.That(m_BlendManager.IsBlending, Is.True); + Assert.AreEqual(0.5f, m_BlendManager.CameraState.RawPosition.x, 0.001f); + + // Reverse the blend to cam1 + ProcessFrame(m_Cam1, 0.2f); + Assert.That(m_BlendManager.IsBlending, Is.True); + Assert.AreEqual(0.3f, m_BlendManager.CameraState.RawPosition.x, 0.001f); + + // Reverse the blend again to cam2 + ProcessFrame(m_Cam2, 0.1f); + Assert.That(m_BlendManager.IsBlending, Is.True); + Assert.AreEqual(0.4f, m_BlendManager.CameraState.RawPosition.x, 0.001f); + + ProcessFrame(m_Cam2, 0.4f); + Assert.That(m_BlendManager.IsBlending, Is.True); + Assert.AreEqual(0.8f, m_BlendManager.CameraState.RawPosition.x, 0.001f); + + // Reverse the blend again to cam1 + ProcessFrame(m_Cam1, 0.1f); + Assert.That(m_BlendManager.IsBlending, Is.True); + Assert.AreEqual(0.7f, m_BlendManager.CameraState.RawPosition.x, 0.001f); + + // And finish the blend on cam2 + ProcessFrame(m_Cam2, 0.1f); + Assert.That(m_BlendManager.IsBlending, Is.True); + Assert.AreEqual(0.8f, m_BlendManager.CameraState.RawPosition.x, 0.001f); + + ProcessFrame(m_Cam2, 0.201f); + Assert.That(m_BlendManager.IsBlending, Is.False); + Assert.AreEqual(1.0f, m_BlendManager.CameraState.RawPosition.x, 0.001f); + } } } \ No newline at end of file diff --git a/com.unity.cinemachine/package.json b/com.unity.cinemachine/package.json index 8d51dd12a..92f958eb9 100644 --- a/com.unity.cinemachine/package.json +++ b/com.unity.cinemachine/package.json @@ -1,7 +1,7 @@ { "name": "com.unity.cinemachine", "displayName": "Cinemachine", - "version": "3.1.4", + "version": "3.1.5", "unity": "2022.3", "description": "Smart camera tools for passionate creators. \n\nCinemachine 3 is a newer and better version of Cinemachine, but upgrading an existing project from 2.X will likely require some effort. If you're considering upgrading an older project, please see our upgrade guide in the user manual.", "keywords": [