Skip to content

Commit 499137d

Browse files
authored
Fix blend reversal logic and add unit test (#1066)
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.
1 parent 529e59a commit 499137d

File tree

4 files changed

+70
-17
lines changed

4 files changed

+70
-17
lines changed

com.unity.cinemachine/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
88
### Unreleased
99

1010
### Bugfixes
11+
- Ensure that correct blend time is always used when backing out of a blend-in-progress.
1112
- CinemachineVolumeSettings: changes to Focal Length and Aperture settings were not being applied while auto-focus was enabled.
1213
- InheritPosition was not inheriting the camera position in all cases.
1314

com.unity.cinemachine/Runtime/Core/CameraBlendStack.cs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ class StackFrame : NestedBlendSource
7171
ICinemachineCamera m_SnapshotSource;
7272
float m_SnapshotBlendWeight;
7373

74+
// If reversing a blend-in-progress, this will indicate how much of the blend was skipped
75+
public float MidBlendNormalizedStartPoint;
76+
7477
public StackFrame() : base(new ()) {}
7578
public bool Active => Source.IsValid;
7679

@@ -234,7 +237,7 @@ public void UpdateRootFrame(
234237
if (activeCamera != outgoingCamera)
235238
{
236239
bool backingOutOfBlend = false;
237-
float backingOutPercentCompleted = 0;
240+
float normalizedBlendPosition = 0;
238241
float duration = 0;
239242

240243
// Do we need to create a game-play blend?
@@ -248,7 +251,8 @@ public void UpdateRootFrame(
248251
// Are we backing out of a blend-in-progress?
249252
backingOutOfBlend = frame.Source.CamA == activeCamera && frame.Source.CamB == outgoingCamera;
250253
if (backingOutOfBlend && frame.Blend.Duration > kEpsilon)
251-
backingOutPercentCompleted = frame.Blend.TimeInBlend / frame.Blend.Duration;
254+
normalizedBlendPosition = frame.MidBlendNormalizedStartPoint
255+
+ (1 - frame.MidBlendNormalizedStartPoint) * frame.Blend.TimeInBlend / frame.Blend.Duration;
252256

253257
frame.Source.CamA = outgoingCamera;
254258
frame.Source.BlendCurve = blendDef.BlendCurve;
@@ -282,8 +286,7 @@ public void UpdateRootFrame(
282286
snapshot = true;
283287

284288
// Avoid nesting too deeply
285-
var nbs = frame.Blend.CamA as NestedBlendSource;
286-
if (!snapshot && nbs != null && nbs.Blend.CamA is NestedBlendSource nbs2)
289+
if (!snapshot && frame.Blend.CamA is NestedBlendSource nbs && nbs.Blend.CamA is NestedBlendSource nbs2)
287290
nbs2.Blend.CamA = new SnapshotBlendSource(nbs2.Blend.CamA);
288291

289292
// Special case: if backing out of a blend-in-progress
@@ -292,15 +295,8 @@ public void UpdateRootFrame(
292295
if (backingOutOfBlend)
293296
{
294297
snapshot = true; // always use a snapshot for this to prevent pops
295-
var adjustedDuration = frame.Blend.TimeInBlend;
296-
if (nbs != null)
297-
adjustedDuration += nbs.Blend.Duration - nbs.Blend.TimeInBlend;
298-
else if (frame.Blend.CamA is SnapshotBlendSource sbs)
299-
adjustedDuration += sbs.RemainingTimeInBlend;
300-
301-
// In the event that the blend times in the different directions are different,
302-
// don't make the blend longer than it would otherwise have been
303-
duration = Mathf.Min(duration * backingOutPercentCompleted, adjustedDuration);
298+
duration = duration * normalizedBlendPosition; // skip first part of blend
299+
normalizedBlendPosition = 1 - normalizedBlendPosition;
304300
}
305301

306302
// Chain to existing blend
@@ -313,13 +309,14 @@ public void UpdateRootFrame(
313309
camA = new NestedBlendSource(blendCopy);
314310
}
315311
}
316-
// For the event, we use the raw outgoing camera, not the blend source
312+
// For the event, we use the raw outgoing camera, not the actual blend source
317313
frame.Blend.CamA = outgoingCamera;
318314
frame.Blend.CamB = activeCamera;
319315
frame.Blend.BlendCurve = frame.Source.BlendCurve;
320316
frame.Blend.Duration = duration;
321317
frame.Blend.TimeInBlend = 0;
322318
frame.Blend.CustomBlender = frame.Source.CustomBlender;
319+
frame.MidBlendNormalizedStartPoint = normalizedBlendPosition;
323320

324321
// Allow the client to modify the blend
325322
if (duration > 0)

com.unity.cinemachine/Tests/Editor/BlendManagerTests.cs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,21 @@ class FakeCamera : ICinemachineCamera
1212
public FakeCamera(string name) => m_Name = name;
1313
public string Name => m_Name;
1414
public string Description => string.Empty;
15-
public CameraState State => CameraState.Default;
15+
public CameraState State
16+
{
17+
get
18+
{
19+
var state = CameraState.Default;
20+
state.RawPosition = Position;
21+
return state;
22+
}
23+
}
1624
public bool IsValid => true;
1725
public ICinemachineMixer ParentCamera => null;
1826
public void UpdateCameraState(Vector3 worldUp, float deltaTime) {}
1927
public void OnCameraActivated(ICinemachineCamera.ActivationEventParams evt) {}
28+
29+
public Vector3 Position;
2030
}
2131

2232
class FakeMixer : FakeCamera, ICinemachineMixer
@@ -61,7 +71,7 @@ [TearDown] public void TearDown()
6171
void Reset(float blendTime)
6272
{
6373
m_BlendManager.LookupBlendDelegate = (outgoing, incoming)
64-
=> new (CinemachineBlendDefinition.Styles.EaseInOut, blendTime); // constant blend time
74+
=> new (CinemachineBlendDefinition.Styles.Linear, blendTime); // linear blend, constant blend time
6575
m_BlendManager.OnEnable();
6676
ProcessFrame(null, 0.1f);
6777
ResetCounters();
@@ -224,5 +234,50 @@ public void TestEventsBlendToNestedBlend()
224234
Assert.AreEqual(1, m_BlendFinishedCount);
225235
Assert.That(m_BlendManager.IsBlending, Is.False);
226236
}
237+
238+
[Test]
239+
public void TestBlendReversal()
240+
{
241+
Reset(1); // constant blend time of 1
242+
m_Cam1.Position = new Vector3(0, 0, 0);
243+
m_Cam2.Position = new Vector3(1, 0, 0);
244+
245+
// Start with cam1
246+
ProcessFrame(m_Cam1, 0.1f);
247+
Assert.That(m_BlendManager.IsBlending, Is.False);
248+
249+
// Activate cam2
250+
ProcessFrame(m_Cam2, 0.5f);
251+
Assert.That(m_BlendManager.IsBlending, Is.True);
252+
Assert.AreEqual(0.5f, m_BlendManager.CameraState.RawPosition.x, 0.001f);
253+
254+
// Reverse the blend to cam1
255+
ProcessFrame(m_Cam1, 0.2f);
256+
Assert.That(m_BlendManager.IsBlending, Is.True);
257+
Assert.AreEqual(0.3f, m_BlendManager.CameraState.RawPosition.x, 0.001f);
258+
259+
// Reverse the blend again to cam2
260+
ProcessFrame(m_Cam2, 0.1f);
261+
Assert.That(m_BlendManager.IsBlending, Is.True);
262+
Assert.AreEqual(0.4f, m_BlendManager.CameraState.RawPosition.x, 0.001f);
263+
264+
ProcessFrame(m_Cam2, 0.4f);
265+
Assert.That(m_BlendManager.IsBlending, Is.True);
266+
Assert.AreEqual(0.8f, m_BlendManager.CameraState.RawPosition.x, 0.001f);
267+
268+
// Reverse the blend again to cam1
269+
ProcessFrame(m_Cam1, 0.1f);
270+
Assert.That(m_BlendManager.IsBlending, Is.True);
271+
Assert.AreEqual(0.7f, m_BlendManager.CameraState.RawPosition.x, 0.001f);
272+
273+
// And finish the blend on cam2
274+
ProcessFrame(m_Cam2, 0.1f);
275+
Assert.That(m_BlendManager.IsBlending, Is.True);
276+
Assert.AreEqual(0.8f, m_BlendManager.CameraState.RawPosition.x, 0.001f);
277+
278+
ProcessFrame(m_Cam2, 0.201f);
279+
Assert.That(m_BlendManager.IsBlending, Is.False);
280+
Assert.AreEqual(1.0f, m_BlendManager.CameraState.RawPosition.x, 0.001f);
281+
}
227282
}
228283
}

com.unity.cinemachine/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "com.unity.cinemachine",
33
"displayName": "Cinemachine",
4-
"version": "3.1.4",
4+
"version": "3.1.5",
55
"unity": "2022.3",
66
"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.",
77
"keywords": [

0 commit comments

Comments
 (0)