Interrupt Blending
Categories:
When a blend A → B is in flight and a new blend A’ → C kicks off, snapping the cam’s source pose produces a perceptible velocity jolt — the cam visibly changes direction at the interrupt moment. The Cam Core’s mid-blend correction window prevents this by seeding the new blend at a velocity-matched curve point and Y-blending between the snapshot pose and the new blend’s natural pose across a small window.
This replaces the legacy BlendFromCore hard-anchor approach, which produced a velocity-zero discontinuity at every interrupt.
Contents
- The Problem
- The Mechanism
- Window Width
- Edge Cases
- Cam Core State
- StartBlend Detection
- OnTick Application
- Inspector Field
- What This Replaces
- See Also
The Problem
Consider a blend A → B running at 50% progress. A new winner C is arbitrated; the system needs a new blend A’ → C. Two naive approaches both fail:
| Approach | Behavior | Failure |
|---|---|---|
| Snap to mid-blend pose, start new blend from there | Source pose = currently-rendered TM | The new blend’s start velocity is whatever its curve dictates at t=0 (often zero) — does not match the cam’s current motion vector. Visible direction change at interrupt. |
| Hard-anchor the snapshot as the new blend’s source for the entire duration | Source TM frozen at interrupt | The cam’s velocity drops to zero at t=0 of the new blend — instant velocity discontinuity. |
The legacy BlendFromCore flag implemented the second approach. The new correction window replaces it.
The Mechanism
The Cam Core introduces a small correction window at the start of the new blend during which the rendered TM is a Y-blend between the snapshot pose and the new blend’s natural pose. After the window, pure new blend with live source / destination TMs.
Step by step
- Capture the cam’s currently-rendered TM at the interrupt moment →
m_interruptSnapshotTM. - Compute
oldBlendFactor = oldCurTime / oldTargetTime(how far through the OLD blend we were). - Compute
T_natural = 1 − oldBlendFactor— the curve point on the new blend at which the cam’s velocity matches its current motion. - Compute
T_start = max(0, T_natural − m_interruptCorrection)— pull the new blend back by the configurable window width. - Seed
curBlendTime = T_start * blendTimeso the new blend starts at curve-pointT_start. - Over the window
[T_start, T_natural], the rendered pose is a Y-blend betweenm_interruptSnapshotTMand the new blend’s natural pose at the current curve point. The Y-factor sweeps 0 → 1 across the window. - After
T_natural, the Y-blend yields fully to the new blend; pure new-blend interpolation continues with live source / destination TMs.
Net effect: the rendered cam starts in the snapshot pose at the interrupt, ramps gracefully into the new blend’s natural trajectory across the correction window, and proceeds normally.
Window Width
m_interruptCorrection is authored on the Cam Core, default 0.03 (3% of the new blend’s curve duration). The tradeoff:
| Setting | Behavior |
|---|---|
| Larger window (e.g. 0.08) | Smoother handoff, but the cam is “stuck” in snapshot territory for longer — can feel like lag. |
| Smaller window (e.g. 0.01) | Quicker handoff, less smoothness. |
| 0 | Degenerates to legacy hard-anchor behavior with the perceptible jolt. |
3% is a balance that’s invisibly smooth for typical blend durations (~1s → 30ms correction window).
Edge Cases
Old blend nearly complete
If oldBlendFactor is close to 1 (the old blend was almost done), T_natural ≈ 0 and T_start ≈ −m_interruptCorrection. The window may compress to nothing — T_start clamps at 0:
T_start = max(0.0, T_natural - m_interruptCorrection);
When clamped, the new blend effectively starts from its natural beginning. The cam may still have a small handoff jolt because the correction can’t pull back further.
Old blend just started
If oldBlendFactor is close to 0 (old blend barely started), T_natural ≈ 1 and T_start ≈ 1 − m_interruptCorrection. Most of the new blend happens in pure new mode after a brief correction window. The snapshot influence is short-lived.
Non-blending interrupt (cold start of new blend)
If IsBlending == false when StartBlend is called, no interrupt — set m_inCorrectionWindow = false and run the new blend normally.
Cam Core State
class GS_CamCoreComponent {
// ...
float m_interruptCorrection = 0.03f; // authored
bool m_inCorrectionWindow = false;
float m_correctionStart = 0.0f; // blendFactor where window begins
float m_correctionEnd = 0.0f; // blendFactor where window ends (= T_natural)
AZ::Transform m_interruptSnapshotTM = AZ::Transform::CreateIdentity();
};
StartBlend Detection
void GS_CamCoreComponent::StartBlend(
float blendTime,
GS_Core::CurveType blendType,
GS_Core::Math::BlendShape blendShape,
GS_Core::Math::PivotSource pivotSource)
{
if (IsBlending)
{
// Mid-blend interrupt.
const float oldBlendFactor = curBlendTime / targetBlendTime;
const float T_natural = 1.0f - oldBlendFactor;
const float T_start = AZ::GetMax(0.0f, T_natural - m_interruptCorrection);
// Snapshot current rendered TM.
AZ::TransformBus::EventResult(m_interruptSnapshotTM, m_entityId,
&AZ::TransformInterface::GetWorldTM);
m_inCorrectionWindow = true;
m_correctionStart = T_start;
m_correctionEnd = T_natural;
// Seed new blend's curve position so velocity matches.
curBlendTime = T_start * blendTime;
}
else
{
// Cold start.
m_inCorrectionWindow = false;
curBlendTime = 0.0f;
}
// Capture source / dest poses, blend params.
prevTransform = ... ; // cam's current world TM at this moment
targetBlendTime = blendTime;
currentBlendType = blendType;
currentBlendShape = blendShape;
currentPivotSource = pivotSource;
IsBlending = true;
}
OnTick Application
void GS_CamCoreComponent::OnTick(float deltaTime, AZ::ScriptTimePoint)
{
if (!IsBlending)
{
// Locked phase — read currentCam's TM, write it directly.
return;
}
curBlendTime += deltaTime;
const float blendFactor = AZ::GetMin(1.0f, curBlendTime / targetBlendTime);
const float easedFactor = ApplyCurve(blendFactor, currentBlendType);
// Compute the "natural" blended pose for this curve point.
AZ::Vector3 blendedPos;
if (currentBlendShape == BlendShape::Linear)
{
blendedPos = AZ::Vector3::Lerp(
prevTransform.GetTranslation(),
currentTM.GetTranslation(),
easedFactor);
}
else
{
AZ::Vector3 pivot = ResolvePivot(currentPivotSource, lastCam, currentCam);
blendedPos = GS_Core::Math::BlendPositionAroundPivot(
prevTransform.GetTranslation(),
currentTM.GetTranslation(),
pivot, easedFactor, currentBlendShape, AZ::Vector3::CreateAxisZ());
}
AZ::Quaternion blendedRot = prevTransform.GetRotation().Slerp(
currentTM.GetRotation(), easedFactor);
AZ::Vector3 appliedPos;
AZ::Quaternion appliedRot;
if (m_inCorrectionWindow && blendFactor < m_correctionEnd)
{
// Y-blend the snapshot with the natural pose.
float yFactor = (blendFactor - m_correctionStart) / m_interruptCorrection;
yFactor = AZ::GetClamp(yFactor, 0.0f, 1.0f);
appliedPos = AZ::Vector3::Lerp(
m_interruptSnapshotTM.GetTranslation(), blendedPos, yFactor);
appliedRot = m_interruptSnapshotTM.GetRotation().Slerp(blendedRot, yFactor);
}
else
{
// Past the correction window — pure new blend.
appliedPos = blendedPos;
appliedRot = blendedRot;
}
// Write final TM. Apply lens interpolation similarly.
AZ::TransformBus::Event(m_entityId,
&AZ::TransformInterface::SetWorldTranslation, appliedPos);
AZ::TransformBus::Event(m_entityId,
&AZ::TransformInterface::SetWorldRotationQuaternion, appliedRot);
if (blendFactor >= 1.0f)
{
CompleteBlend();
}
}
Inspector Field
| Field | Default | Purpose |
|---|---|---|
m_interruptCorrection | 0.03 | Fraction of the new blend’s curve over which the cam’s snapshot pose at interrupt blends to the new blend’s natural pose. Smaller = snappier interrupts but more visible jolts. 0 = legacy hard-anchor. |
What This Replaces
Pre-correction, the system used a BlendFromCore flag on the Cam Core. When set, the blend’s source pose was hard-anchored to the cam’s currently-rendered TM at the interrupt moment, for the entire duration of the new blend. The new blend’s velocity at t = 0 was therefore zero — an instant velocity discontinuity at the interrupt point.
The correction-window approach replaces this entirely. BlendFromCore is retained as a field on the Cam Core for back-compat but is not used in the new flow.
See Also
Related PhantomCam pages:
- Cam Core — owns
StartBlendandOnTick. - State Inheritance — runs BEFORE
StartBlend; the destination cam’s pose is already inherited when the correction-window snapshot is captured. - Blend Profiles — author the entries that trigger blends.
- Phantom Cameras — Execution States —
m_blendingOutkeeps the outgoing cam ticking during the blend so its source pose stays live.
Get GS_PhantomCam
GS_PhantomCam — Explore this gem on the product page and add it to your project.