Cam Core
Categories:
The Cam Core is the runtime driver of the GS_PhantomCam system. It lives on a camera entity (the main O3DE camera entity for single-player, or a per-channel Cam Core entity for multi-channel projects) and is responsible for one job: making the engine’s camera match the dominant Phantom Camera for its channel. Every frame, the Cam Core reads the target phantom camera’s position, rotation, and field of view, then applies those values to the actual camera entity — either instantly (when locked) or through a timed blend transition.
When the Cam Manager determines a new dominant phantom camera for the Cam Core’s channel, it fires a per-channel notification and routes the new cam to the Cam Core via SetPhantomCam. The Cam Core then looks up the best matching blend in the active Blend Profile, optionally inherits state from the outgoing cam, and begins interpolating toward the new phantom camera over the specified duration, easing curve, and blend shape. If a new transition starts before the current blend completes, a small correction window pulls the new blend back from the rendered TM so the cam does not snap. Once a blend completes, the main camera locks to the phantom camera as a child entity, matching its transforms exactly until the next transition.
For usage guides and setup examples, see The Basics: GS_PhantomCam.

Contents
- How It Works
- Blend Shape and Pivot
- State Inheritance Handoff
- Interrupt Correction
- Channel Addressing
- Setup
- API Reference
- Usage Examples
- Extending the Cam Core
- See Also
How It Works
Blend Transitions
When a camera transition is triggered:
- The Cam Core receives
SetPhantomCam(targetCam)from the Cam Manager, routed to its own entity address. - It queries the assigned Blend Profile for the best matching blend entry between the outgoing and incoming cameras (by entity name).
- The matched entry returns duration, easing curve, blend shape (Linear / Spherical / Cylindrical), an inherit-state flag, and a pivot source. If no entry matches, the Cam Core falls back to its own defaults.
- If the entry’s inherit-state is set, the state inheritance handoff runs before the blend starts so the destination cam’s body adopts a kinematic interpretation of the outgoing cam’s pose.
- The outgoing cam’s
m_blendingOutflag is set so it continues ticking through the transition (keeps the source pose live if the target is moving). - Over the blend duration, the Cam Core interpolates position (using the chosen shape), rotation (slerp), and lens (FOV / near / far) from the outgoing state to the incoming state.
- If a new blend interrupts this one, the interrupt correction window pulls the new start curve back so the cam doesn’t snap.
- On completion, the Cam Core parents the main camera to the new dominant phantom, locking them together until the next transition.
Blend Profile Resolution
The Cam Core holds a reference to a GS_PhantomCamBlendProfile asset. When a transition occurs, it calls GetBestBlend(fromCam, toCam) on the profile to find the most specific matching entry. Resolution order:
- Exact match — From camera name to To camera name.
- Any-to-specific — “Any” to the To camera name.
- Specific-to-any — From camera name to “Any”.
- Default fallback — The Cam Core’s own default blend time, easing, and shape.
See Blend Profiles for full details.
Camera Locking
Once a blend completes, the main camera becomes a child of the dominant phantom camera entity. All position, rotation, and property updates from the phantom camera are reflected immediately on the real camera. This lock persists until the next transition begins.
Blend Shape and Pivot
Each blend entry specifies a shape that controls how position interpolates:
| Shape | Behavior |
|---|---|
Linear | Straight-line lerp from source to destination. The default and the cheapest. No pivot used. |
Cylindrical | Sweeps through a yaw arc around a pivot, preserving the heading change as a rotation rather than a translation. Pitch and radius interpolate independently. |
Spherical | Sweeps through a great-circle arc around a pivot. Both yaw and pitch animate around the pivot, producing an orbital flight path between source and destination. |
The pivot point is resolved by the entry’s pivot source:
| Pivot source | Resolution |
|---|---|
Shared | Midpoint of both cams’ published pivots. Falls back gracefully if either is missing. |
Source | Outgoing cam’s pivot. Falls back to the incoming cam, then to Linear if neither publishes. |
Destination | Incoming cam’s pivot. Falls back to the outgoing cam, then to Linear. |
Cams publish their pivot as the pivotPos field on CamPoseSnapshot (see State Inheritance). Stages that don’t publish a pivot disqualify themselves from being the pivot source.
The math is implemented by the Orbital Solver Utility in GS_Core — BlendPositionAroundPivot(from, to, pivot, alpha, shape, axis).
State Inheritance Handoff
When a blend entry has m_inheritState = true, the Cam Core runs a handoff between the outgoing and incoming cams before starting the blend:
- Pull a
CamPoseSnapshotfrom the outgoing cam viaPhantomCameraRequests::TryGetPoseSnapshot. - If the snapshot is valid, push it to the incoming cam via
PhantomCameraRequests::TryAdoptPoseSnapshot.
The Get / Adopt calls forward to the cam’s Body stage. Bodies that don’t implement the protocol return false; the handoff is a silent no-op and the blend proceeds normally.
The handoff runs before StartBlend, so by the time the blend captures source and destination poses, the destination cam’s body has already adopted the seed pose. This makes inherited blends feel like a continuation of the outgoing cam’s motion rather than a clean start.
See State Inheritance for the full protocol and per-stage adoption semantics.
Interrupt Correction
When a blend A → B is in flight and a new blend A’ → C kicks off, snapping the cam’s source TM produces a perceptible velocity jolt — the cam visibly changes direction at the interrupt moment. The Cam Core’s mid-blend correction window prevents this.
Mechanism
- Capture the cam’s currently-rendered TM at the interrupt moment →
m_interruptSnapshotTM. - 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 correction window. - Seed the new blend at curve point
T_start. - Over the window
[T_start, T_natural], the rendered pose is a Y-blend between the snapshot and the new blend’s natural pose, with the Y factor sweeping 0 → 1. - After
T_natural, the Y-blend yields fully to the new blend; pure new-blend interpolation continues with live source / destination TMs.
The cam starts in the snapshot pose at the interrupt, ramps gracefully into the new 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). Larger windows feel smoother but produce more visible lag in the handoff territory; smaller windows feel snappier but reintroduce the velocity jolt. Setting it to 0 degenerates to legacy hard-anchor behavior.
Channel Addressing
The Cam Core self-registers with the Cam Manager on activate. It walks transform ancestors looking for a ChannelStampComponent to determine its channel:
- Stamp found — Registers via
RegisterCamCoreToChannel(camCore, stampEntity, channelId, token). Listens at its own entity address onCamCoreRequestBusso the Cam Manager can route per-channelSetPhantomCamcalls. - No stamp — Registers via the legacy
RegisterCamCore. Routes through channel 0 in single-player projects.
If the Cam Core’s parent rig is re-stamped (rare; typically during stage transitions or hot-reload), the ChannelStampNotificationBus::OnStamped handler un-registers from the old channel and re-registers to the new one. The Cam Manager’s token validation rejects mismatched stamp tokens to prevent stale messages from overwriting fresh bindings.
One Cam Core per channel. Multi-channel projects (Tier 3) spawn one per active channel; single-player projects use a single Cam Core at channel 0.
Setup
- Add GS_CamCoreComponent to a camera entity. For Tier 1 / Tier 2 (single-player), this is the main O3DE camera entity, parented under the Cam Manager entity. For Tier 3 (multi-channel), include one Cam Core per rig inside the rig prefab.
- Create a Blend Profile data asset in the Asset Editor and assign it to the Cam Core’s Blend Profile slot.
- Configure default blend time, easing, and blend shape on the Cam Core for cases where no profile entry matches. See Curves Utility for available easing types.
- Optionally adjust Interrupt Correction (default
0.03) if your project has unusually short or long blends.
API Reference
Request Bus: CamCoreRequestBus
Internal command bus, Cam Manager → Cam Core. Per-entity — each Cam Core listens at its own entity id so the Cam Manager can route per-channel events; Broadcast hits every connected Cam Core (stage-transition global clear). This is not a cross-gem contract and is not script-reflected — external systems observe the camera through the two GS_Core contracts below.
| Method | Parameters | Returns | Description |
|---|---|---|---|
SetPhantomCam | AZ::EntityId targetCam | void | Sets the phantom camera that the Cam Core should blend toward or lock to. Called by the Cam Manager when arbitration produces a new winner. Triggers the inheritance handoff and blend start. EntityId() clears it. |
Cross-Gem Contract: CamCoreExchangeBus
The query half of the camera’s observable-service trio, housed in GS_Core. Addressed by the Cam Core’s EntityId; single provider.
| Method | Parameters | Returns | Description |
|---|---|---|---|
GetCamCore | — | AZ::EntityId | Resolves the Cam Core’s entity (late-joiner / poll for the current core). Was CamCoreRequestBus::GetCamCore before the interfaces migration. |
Cross-Gem Contract: CamCoreEmissionBus
The emit half of the trio, housed in GS_Core. Global broadcast, multiple handlers — any number of components can subscribe without depending on GS_PhantomCam. Was CamCoreNotificationBus before the interfaces migration; the ScriptCanvas-facing name is preserved by GS_PhantomCam’s binder, so existing graphs keep resolving.
| Event | Parameters | Description |
|---|---|---|
UpdateCameraPosition | camPos, camFacing, deltaTime | Fired each tick with the committed camera position, forward vector, and frame delta. Subscribe to react to camera movement in real time (audio, UI, gameplay readback). |
OnCamCoreRegistered | AZ::EntityId camCore | A Cam Core came online (rig lifetime). Pair with GetCamCore to resolve it, then observe its updates. |
OnCamCoreUnregistered | AZ::EntityId camCore | A Cam Core went away. |
Inspector Fields
| Field | Default | Purpose |
|---|---|---|
defaultBlendType | CurveType::Linear | Easing curve used when no Blend Profile entry matches. |
defaultBlendTime | 1.0 | Fallback blend duration in seconds. |
defaultBlendShape | BlendShape::Linear | Fallback blend shape when no profile entry matches. |
activeBlendProfileAsset | — | The active .camblendprofile asset. |
m_interruptCorrection | 0.03 | Width of the mid-blend interrupt correction window as a fraction of the new blend’s curve. See Interrupt Correction. |
Usage Examples
C++ — Querying the Main Camera Entity
#include <GS_Core/Interfaces/Camera/CamCoreExchangeBus.h>
AZ::EntityId mainCameraId;
GS_Core::CamCoreExchangeBus::BroadcastResult(
mainCameraId,
&GS_Core::CamCoreExchange::GetCamCore);
For a multi-channel project, query the Cam Manager for a specific channel’s Cam Core instead:
#include <GS_PhantomCam/GS_CamManagerBus.h>
AZ::EntityId p1CamCore;
GS_PhantomCam::CamManagerRequestBus::BroadcastResult(
p1CamCore,
&GS_PhantomCam::CamManagerRequests::GetChannelCamCore,
GS_PhantomCam::ChannelId(0));
C++ — Listening for Camera Updates
#include <GS_Core/Interfaces/Camera/CamCoreEmissionBus.h>
class MyCameraListener
: public AZ::Component
, protected GS_Core::CamCoreEmissionBus::Handler
{
protected:
void Activate() override
{
GS_Core::CamCoreEmissionBus::Handler::BusConnect();
}
void Deactivate() override
{
GS_Core::CamCoreEmissionBus::Handler::BusDisconnect();
}
void UpdateCameraPosition(
const AZ::Vector3& camPos,
const AZ::Vector3& camFacing,
float deltaTime) override
{
// React to per-tick camera position / facing changes.
}
};
Script Canvas
Reacting to camera position updates:

Extending the Cam Core
The Cam Core can be extended in C++ to customize blend behavior, add post-processing logic, or integrate with external camera systems.
Header (.h)
#pragma once
#include <GS_PhantomCam/Core/GS_CamCoreBus.h>
#include <Source/Core/GS_CamCoreComponent.h>
namespace MyProject
{
class MyCamCore : public GS_PhantomCam::GS_CamCoreComponent
{
public:
AZ_COMPONENT_DECL(MyCamCore);
static void Reflect(AZ::ReflectContext* context);
};
}
Implementation (.cpp)
#include "MyCamCore.h"
#include <AzCore/Serialization/SerializeContext.h>
namespace MyProject
{
AZ_COMPONENT_IMPL(MyCamCore, "MyCamCore", "{YOUR-UUID-HERE}");
void MyCamCore::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<MyCamCore, GS_PhantomCam::GS_CamCoreComponent>()
->Version(0);
if (AZ::EditContext* editContext = serializeContext->GetEditContext())
{
editContext->Class<MyCamCore>("My Cam Core", "Custom camera core driver")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "MyProject")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game"));
}
}
}
}
See Also
For related PhantomCam components:
For utilities used by the blend math:
For conceptual overviews and usage guides:
Get GS_PhantomCam
GS_PhantomCam — Explore this gem on the product page and add it to your project.