This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Interrupt Blending

Mid-blend interrupt correction window — m_interruptCorrection, T_start / T_natural derivation, snapshot Y-blend. Prevents velocity discontinuities when a new blend starts before the current one finishes.

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

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:

ApproachBehaviorFailure
Snap to mid-blend pose, start new blend from thereSource pose = currently-rendered TMThe 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 durationSource TM frozen at interruptThe 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

  1. Capture the cam’s currently-rendered TM at the interrupt moment → m_interruptSnapshotTM.
  2. Compute oldBlendFactor = oldCurTime / oldTargetTime (how far through the OLD blend we were).
  3. Compute T_natural = 1 − oldBlendFactor — the curve point on the new blend at which the cam’s velocity matches its current motion.
  4. Compute T_start = max(0, T_natural − m_interruptCorrection) — pull the new blend back by the configurable window width.
  5. Seed curBlendTime = T_start * blendTime so the new blend starts at curve-point T_start.
  6. Over the window [T_start, T_natural], the rendered pose is a Y-blend between m_interruptSnapshotTM and the new blend’s natural pose at the current curve point. The Y-factor sweeps 0 → 1 across the window.
  7. 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:

SettingBehavior
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.
0Degenerates 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

FieldDefaultPurpose
m_interruptCorrection0.03Fraction 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 StartBlend and OnTick.
  • 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 Statesm_blendingOut keeps 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.