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

Return to the regular view of this page.

Phantom Cameras

The base virtual camera component — priority, target routing, lens, channel scope, snap and focus state, and the composable Body / Aim / Additive stage pipeline.

A Phantom Camera is a single entity component — GS_PhantomCameraComponent — that publishes a candidate camera pose every tick. It holds priority, target routing, lens, channel scope, and snap / focus / blendingOut state, plus the stage pipeline slot that authors fill in to give the cam its behavior. Phantom cameras do not render anything themselves; they register with the Cam Manager and the Cam Core drives the engine view toward whichever phantom currently wins arbitration.

Where previous versions of GS_PhantomCam shipped separate components for each behavior (clamped-look, static-orbit, spline tracking, etc.), all of those behaviors now live as stage variants on the single base component. An author picks a Body stage, an Aim stage, and zero-or-more Additive stages from the type-picker, and the same GS_PhantomCameraComponent becomes a follow cam, an orbital cam, a tracking dolly, a third-person shoulder cam, or a cinematic stinger depending on which stages are slotted.

For usage guides and setup examples, see The Basics: GS_PhantomCam.

The Phantom Camera component in the Entity Inspector.

 

Contents


How It Works

Priority-Based Selection

Every phantom camera has a base priority. The camera with the highest effective priority (base plus any active influences) wins arbitration within its channel. You can control which camera wins through several strategies:

  • Raise priority — Set the desired camera’s priority above all others via SetCameraPriority.
  • Disable / Enable — Call DisableCamera to drop the camera’s effective priority, EnableCamera to restore it.
  • Change priority directly — Call ChangeCameraPriority on the Cam Manager bus.
  • Influence — Add temporary priority modifiers through Influence Fields without touching base priorities.

Follow and Look-At

Follow and look-at behavior is no longer hardcoded on the component; it is provided by the Body and Aim stages slotted on the pipeline. Each tick the cam threads a CameraState accumulator through:

Body → Aim → Reposition additives → Noise additives → Finalize
  • The Body stage writes state.position — different variants implement spring-damped follow, orbital sweep, spline traversal, etc.
  • The Aim stage writes state.rotation — including angle-clamped look, head-tracking look, etc.
  • Additive stages correct or perturb the pose without poisoning Body / Aim ideals.

See Stage Pipeline for the per-tick contract.

Camera Data

Each phantom camera stores its lens and target configuration in a PhantomCamData structure. This data is read by the Cam Core during blending and by stages during evaluation. The stage list lives separately on the component (m_bodySlot, m_aimSlot, m_additives).


Stage Pipeline

Each phantom camera is a container for stages. The component owns three slots:

A fully-configured Phantom Camera with body, aim, and additive stages slotted

SlotTypeCardinalityExamples
m_bodySlotIBodyStageOne per cam (single-element vector by policy)DefaultFollowBody, OrbitBody, DynamicOrbitBody, LeadingFollowBody, TrackBody.
m_aimSlotIAimStageOne per cam (single-element vector by policy)DefaultAim, ClampedLookAim. Other gems (notably gs_performer) register additional variants.
m_additivesIAdditiveStageZero or more, each self-declares Reposition or Noise phaseNoiseStage (Perlin), ImpulseStage (ADSR), TugAimListener, TugBodyListener, collision / occlusion reposition.

Stages are reflected polymorphic types — derived from IBodyStage / IAimStage / IAdditiveStage, not AZ::Component. The editor type-picker enumerates all registered derivations from the SerializeContext class hierarchy, so other gems can extend the catalog cleanly.

Per-tick orchestration (run by the component each OnTick):

EvaluateCamTick — eligibility check (see Execution States)
   │ tickActive == true
EvaluatePose
   ├─► Build CameraContext (target, deltaTime, lifecycle flags, snapThisFrame)
   ├─► IBodyStage::Evaluate    → writes state.position, state.bodyAnchor
   ├─► IAimStage::Evaluate     → writes state.rotation, state.lookAtPoint
   ├─► snapshot m_desiredPose
   ├─► For each Reposition additive → corrects state
   ├─► snapshot m_stablePose
   ├─► For each Noise additive → perturbs state
   ├─► snapshot m_finalPose
   ├─► Resolve cached pivot (state.lookAtPoint, else target world position)
   └─► Commit m_finalPose to TransformBus + apply lens to engine Camera component

See Stage Pipeline reference for the full per-stage contract, init / asset-fresh-load semantics, and variant catalogs.


Channel Scope

Each phantom camera authors a CamChannelScope that decides how it participates in the channel system:

enum class CamChannelScope : AZ::u8 {
    Local,         // Default. Registers to ancestor stamp's channel, or channel 0 if no stamp.
    AllChannels,   // Stamped: per-rig-instance natural duplicate. Unstamped: warns + falls back.
    TrueUnique,    // Single instance. Either bound to a specific channel or shared across all.
};
FieldVisibilityPurpose
m_channelScopeAlwaysThe scope mode.
m_boundChannelIdWhen m_channelScope == TrueUniqueExplicit channel binding for direct mode.
m_allChannelsShareWhen m_channelScope == TrueUnique AND m_showAdvancedShared mode toggle — appears in every active channel’s priority table.
m_showAdvancedWhen m_channelScope == TrueUniqueReveals advanced TrueUnique fields.

Author recipes:

  • Cam inside a rig prefab — leave as Local. Each spawned rig instance hosts its own copy via the stamp-walk path.
  • Per-player tailored broadcast cam in the levelAllChannels (works naturally for in-rig cams; out-of-rig duplication is deferred).
  • Hero-perspective cam for a specific playerTrueUnique + m_boundChannelId = N.
  • Shared cinematic collapse camTrueUnique + advanced + m_allChannelsShare = true. Paired with a Group Target, this triggers OnAllChannelsActivatedSharedCam when every channel selects the cam.

See Channels & Instancing for the resolution algorithm.


Execution States

A phantom camera carries three boolean states that gate whether it ticks each frame. The tick-eligibility predicate is:

tickActive = m_hasFocus || m_alwaysUpdate || m_blendingOut

When tickActive is false the cam is fully dormant — its stages don’t run, its body doesn’t integrate input, its damping doesn’t advance.

The three flags

FlagOriginMeaning
m_hasFocusRuntime — set by SettingNewCam / SettingNewCamOnChannel notifications.True while this cam is the channel’s currently-driven cam.
m_alwaysUpdateAuthored on the component.If true, the cam ticks every frame regardless of focus. Use for background-tracking cams that should be up-to-date the moment focus arrives, at the cost of constant CPU.
m_blendingOutRuntime — set by Cam Core via SetBlendingOut while this cam is the outgoing source of an in-flight blend.Keeps a !alwaysUpdate outgoing cam ticking through the blend so its body can keep tracking the target — otherwise the blend’s “from” pose would freeze.

snapThisFrame and m_snapNextEval

When a cam should bypass damping for a single tick — to commit its ideal pose directly without spring lag — the component sets m_snapNextEval = true. The next Evaluate consumes the flag, sets ctx.snapThisFrame = true, and resets m_snapNextEval.

m_snapNextEval is set on:

  • Focus gainm_hasFocus flipped from false to true (gated on focus specifically; blend-out wake does NOT trigger snap, which would otherwise yank an outgoing cam back to its initial yaw).
  • SetCameraTarget(validEntity) — new follow target.
  • SetTargetFocusGroup(validEntity) — new group target.
  • QueueSnapCamera() — explicit API call.
  • Mid-spawn channel binding — the Cam Manager pushes a synchronous SnapCameraNow when a channel target is already present at registration time. See Cam Manager.

SnapCameraNow vs QueueSnapCamera

MethodTiming
QueueSnapCamera()Sets m_snapNextEval = true. Honored on next Evaluate. Most code paths use this.
SnapCameraNow()Synchronous. Evaluates Body + Aim with dt = 0 and writes the snapped TM straight to the transform bus. Required when the Cam Core is about to read the cam’s TM on the same call stack (mid-spawn binding). Early-returns if neither follow target nor look-at target is set.

Input integration sub-gate

Body stages that drive pose from input (notably DynamicOrbitBody) integrate input on the same predicate, mirrored into CameraContext:

integrateInput = ctx.hasFocus || ctx.alwaysUpdate || ctx.blendingOut

The body still polls the input reader every tick regardless of integrateInput — the poll drains Delta-mode buffers so they don’t accumulate during dormancy. The integration result is discarded when !integrateInput.


Staged Poses

The pipeline produces three pose snapshots per tick:

SnapshotWhere it landsUse it for…
m_desiredPosePost Body + Aim, pre-Reposition. “Clean” ideal — no collision correction, no shake.Debug visualization of the cam’s ideal trajectory.
m_stablePosePost Reposition, pre-Noise. Collision-safe, shake-free.Gameplay readback — Unit movement input driven by camera facing should use this.
m_finalPosePost Noise. What gets written to the transform bus.Anything that needs the actual rendered pose (audio listener placement, screen-space UI).

Three snapshots exist because consumers vary. A Unit that uses camera-forward to drive movement input should query GetStablePose — querying GetFinalPose would let the noise stage drag the character around.

The component also caches a pivot each tick (m_cachedPivot) resolved from the Aim stage’s state.lookAtPoint, falling back to the target’s world position. The Cam Core queries this via GetCameraPivot to drive orbital blend shapes.


Setup

  1. Create an entity and add GS_PhantomCameraComponent.
  2. Set the Priority value. Higher values take precedence within the channel.
  3. Configure FOV, near clip, and far clip on the lens fields.
  4. Assign a Cam Target entity (the follow target). Leave empty to inherit the channel target from the Cam Manager.
  5. From the Body type-picker, slot a body stage. Configure its target mode, offset, and damping halflife.
  6. From the Aim type-picker, slot an aim stage. Configure its target mode, offset, and damping halflife.
  7. Optionally add Additive stages — noise, impulse, tug listeners, collision reposition.
  8. Set the Channel Scope (Local is the default and almost always correct).
  9. Place the entity in your scene or inside a rig prefab. The component registers with the Cam Manager automatically on activation.

For a full walkthrough, see the PhantomCam Set Up Guide.


PhantomCamData Structure

The PhantomCamData structure holds the lens and priority configuration for a phantom camera. Stages are stored separately on the component.

FieldTypeDescription
PriorityAZ::s32Base priority used in arbitration.
FOVfloatField of view in degrees.
NearClipfloatNear clipping plane distance.
FarClipfloatFar clipping plane distance.
CamTargetAZ::EntityIdThe follow target. May be left empty to inherit the channel target.

Follow / look-at offsets, damping halflives, and target modes now live on the stages, not on PhantomCamData.


API Reference

Request Bus: PhantomCameraRequestBus

Commands sent to a specific phantom camera. ById bus — addressed by entity ID, single handler per address.

Lifecycle

MethodParametersReturnsDescription
EnableCameravoidRestores the cam’s effective priority and registers it for evaluation.
DisableCameravoidDrops the cam’s effective priority to 0.
IsCamEnabledboolQuery.

Priority and target

MethodParametersReturnsDescription
SetCameraPriorityAZ::s32 newPriorityvoidSets base priority. Triggers Cam Manager re-evaluation.
GetCameraPriorityAZ::s32Query.
SetCameraTargetAZ::EntityId targetEntityvoidSets the follow target. Triggers a snap on next tick when the entity is valid. Clearing (invalid id) does NOT snap — preserves depossess pose-hold.
SetTargetFocusGroupAZ::EntityId targetFocusGroupvoidSets the target to a Group Target entity. Body / aim stages with CamTargetMode::GroupTarget route through this.
GetCameraDataconst PhantomCamData*Returns const pointer to the cam’s lens / priority / target data. Consumed by the Cam Core.

Snap

MethodParametersReturnsDescription
QueueSnapCameravoidSets m_snapNextEval = true. Honored on next Evaluate.
SnapCameraNowvoidSynchronous snap. Evaluates Body + Aim with dt = 0 and writes the snapped TM. Early-returns if neither follow nor look-at target is set.

Staged pose accessors

MethodParametersReturnsDescription
GetDesiredPoseAZ::TransformPost Body + Aim, pre-Reposition.
GetStablePoseAZ::TransformPost Reposition, pre-Noise. Recommended default for gameplay readback.
GetFinalPoseAZ::TransformCommitted pose (post-Noise).
GetCameraPivotAZ::Vector3& outPivot, bool& outHasPivotvoidReturns the cam’s cached pivot. Used by Cam Core to drive non-Linear blend shapes.

Impulse

MethodParametersReturnsDescription
TriggerCameraImpulsefloat strengthvoidFires every ImpulseNoise additive on this cam. strength multiplies each stage’s AmplitudeGain — typically pass distance-falloff values in 0..1.

Inheritance forwarders

MethodParametersReturnsDescription
TryGetPoseSnapshotCamPoseSnapshot& outboolForwards to the body stage’s virtual. The Cam Core invokes this on the outgoing cam during the inheritance handoff. Returns false if the body doesn’t implement the protocol.
TryAdoptPoseSnapshotconst CamPoseSnapshot& inboolForwards to the body stage’s virtual. The Cam Core invokes this on the incoming cam.

Blend-out lifecycle

MethodParametersReturnsDescription
SetBlendingOutbool isBlendingOutvoidCam Core sets this on the outgoing cam during a blend. While true, the cam ticks (and integrates input) even if !alwaysUpdate && !hasFocus. Cleared on blend completion.

Camera Behavior Types

Retired components. ClampedLook_PhantomCamComponent, StaticOrbit_PhantomCamComponent, and Track_PhantomCamComponent no longer exist as separate components. Their behavior is now provided by stage variants on the base GS_PhantomCameraComponent. Legacy URLs for these subpages redirect to the matching stage docs.

Retired componentReplacement
StaticOrbit_PhantomCamComponentOrbitBody Body stage variant. See Stage Pipeline.
ClampedLook_PhantomCamComponentClampedLookAim Aim stage variant. See Stage Pipeline.
Track_PhantomCamComponentTrackBody Body stage variant. See Stage Pipeline.

AlwaysFaceCameraComponent

A billboard helper component. Keeps the attached entity always facing the active camera. This is not a phantom camera type itself but a utility for objects that should always face the viewer (UI elements in world space, sprite-based effects, etc.). Unchanged by the refactor.


Usage Examples

Switching Cameras by Priority

To make a specific phantom camera dominant, raise its priority above all others:

#include <GS_PhantomCam/Cameras/GS_PhantomCameraBus.h>

GS_PhantomCam::PhantomCameraRequestBus::Event(
    myCameraEntityId,
    &GS_PhantomCam::PhantomCameraRequests::SetCameraPriority,
    100);

Disabling the Current Camera

Disable the current dominant camera to let the next-highest priority camera take over:

GS_PhantomCam::PhantomCameraRequestBus::Event(
    currentCameraEntityId,
    &GS_PhantomCam::PhantomCameraRequests::DisableCamera);

Triggering a Camera Impulse

Fire all ImpulseNoise additives on the active cam at a strength scaled by distance:

const float strength = AZ::GetClamp(1.0f - distance / radius, 0.0f, 1.0f);
GS_PhantomCam::PhantomCameraRequestBus::Event(
    activeCameraEntityId,
    &GS_PhantomCam::PhantomCameraRequests::TriggerCameraImpulse,
    strength);

Script Canvas

Enabling and disabling a phantom camera:

Getting a phantom camera’s data:


Extending Phantom Cameras

Most camera customization now happens by authoring a new stage variant rather than subclassing the component. Stages are reflected polymorphic types — derive from IBodyStage, IAimStage, or IAdditiveStage, register with AZ_RTTI + AZ_CLASS_ALLOCATOR, and reflect under the base interface’s class hierarchy. The editor type-picker discovers the new variant automatically.

Use the PhantomCamera ClassWizard template to generate a new camera component when you need component-level customization (rare) — see GS_PhantomCam Templates.

Header (.h)

#pragma once
#include <GS_PhantomCam/Cameras/GS_PhantomCameraBus.h>
#include <Source/Cameras/GS_PhantomCameraComponent.h>

namespace MyProject
{
    class MyCustomCam : public GS_PhantomCam::GS_PhantomCameraComponent
    {
    public:
        AZ_COMPONENT_DECL(MyCustomCam);

        static void Reflect(AZ::ReflectContext* context);
    };
}

Implementation (.cpp)

#include "MyCustomCam.h"
#include <AzCore/Serialization/SerializeContext.h>

namespace MyProject
{
    AZ_COMPONENT_IMPL(MyCustomCam, "MyCustomCam", "{YOUR-UUID-HERE}");

    void MyCustomCam::Reflect(AZ::ReflectContext* context)
    {
        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serializeContext->Class<MyCustomCam, GS_PhantomCam::GS_PhantomCameraComponent>()
                ->Version(0);

            if (AZ::EditContext* editContext = serializeContext->GetEditContext())
            {
                editContext->Class<MyCustomCam>("My Custom Camera", "Custom phantom camera component")
                    ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                        ->Attribute(AZ::Edit::Attributes::Category, "MyProject")
                        ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game"));
            }
        }
    }
}

For stage-level extension (the more common case), see the Stage Pipeline reference.


See Also

For related PhantomCam components:

For conceptual overviews and usage guides:


Get GS_PhantomCam

GS_PhantomCam — Explore this gem on the product page and add it to your project.

1 - Stage Pipeline

The composable per-tick pipeline each Phantom Camera runs — Body, Aim, Reposition additives, Noise additives. Stage interfaces, CameraState accumulator, target routing, decoupled-ideal pattern, init contract.

Every Phantom Camera runs the same fixed pipeline per tick:

Body → Aim → Reposition additives → Noise additives → Finalize

The Body and Aim stages produce the cam’s smoothed ideal pose; Reposition additives correct it for collision / tug volumes; Noise additives perturb it for shake and impulse; Finalize commits the result to the entity transform and the engine camera component.

Stages are not AZ::Components. They are reflected polymorphic types — derived from IBodyStage, IAimStage, or IAdditiveStage. The editor’s type-picker enumerates registered derivations from the SerializeContext class hierarchy, so other gems can extend the catalog cleanly without touching the base component.

For per-variant detail, see the catalog pages:

  • Body Stage VariantsDefaultFollowBody, OrbitBody, DynamicOrbitBody, LeadingFollowBody, TrackBody.
  • Aim Stage VariantsDefaultLookAtAim, ClampedLookAim. Plus extensions from gs_performer.
  • Additive Stage VariantsCollisionReposition, OcclusionReposition, TugAimListener, TugBodyListener, PerlinNoise, ImpulseNoise.

 

Contents


Per-Tick Model

Each tick, the owning Phantom Camera threads a CameraState accumulator through the pipeline:

GS_PhantomCameraComponent::EvaluatePose
   ├─► Build CameraContext (target, deltaTime, lifecycle flags, snapThisFrame)
   ├─► CameraState state;  // zero-initialized accumulator
   ├─► IBodyStage::Evaluate(state, ctx, dt)
   │       writes state.position, state.bodyAnchor
   ├─► IAimStage::Evaluate(state, ctx, dt)
   │       writes state.rotation, state.lookAtPoint
   ├─► m_desiredPose = (state.position, state.rotation)
   ├─► For each additive in m_repositionStages:
   │       Evaluate(state, ctx, dt) — may correct state.position/rotation
   ├─► m_stablePose = (state.position, state.rotation)
   ├─► For each additive in m_noiseStages:
   │       Evaluate(state, ctx, dt) — perturbs state.position/rotation
   ├─► m_finalPose = (state.position, state.rotation)
   ├─► Resolve cached pivot from state.lookAtPoint (else fallback to target world pos)
   └─► Commit m_finalPose to TransformBus + apply lens to engine Camera component

The component partitions additives into m_repositionStages and m_noiseStages once at slot-assign time (or whenever the additive list changes) based on each additive’s GetStage() result — per-tick code does not re-check the enum each frame.

CamPipelineStage is a fixed ordered enum:

enum class CamPipelineStage {
    Body,
    Aim,
    Reposition,
    Noise,
    Finalize
};

Reposition and Noise are the values an additive may return from GetStage. Body, Aim, and Finalize exist for symmetry but are not author-selectable.


Stage Interfaces

IBodyStage

Single slot per cam. Computes the smoothed follow position.

class IBodyStage {
public:
    AZ_RTTI(IBodyStage, "{6A78E21A-9A5F-4AB5-8F20-08B4D09E5C01}");
    AZ_CLASS_ALLOCATOR(IBodyStage, AZ::SystemAllocator);

    virtual ~IBodyStage() = default;

    virtual void Init() {}

    virtual void Evaluate(CameraState& state, const CameraContext& ctx, float dt) = 0;
    virtual AZ::EntityId ResolveTarget(AZ::EntityId incoming, TargetSignalKind kind) = 0;
    virtual const char* GetDisplayName() const { return "Body Stage"; }

    // State inheritance — default returns false (no protocol).
    virtual bool TryGetPoseSnapshot(CamPoseSnapshot& out) const  { return false; }
    virtual bool TryAdoptPoseSnapshot(const CamPoseSnapshot& in) { return false; }

    static void Reflect(AZ::ReflectContext* context);
};

See Body Stage Variants for the catalog.

IAimStage

Single slot per cam. Computes the smoothed look rotation.

class IAimStage {
public:
    AZ_RTTI(IAimStage, "{9F37E4C6-04AD-4B1F-A9E2-0D3A4B8F1E02}");
    AZ_CLASS_ALLOCATOR(IAimStage, AZ::SystemAllocator);

    virtual ~IAimStage() = default;

    virtual void Init() {}

    virtual void Evaluate(CameraState& state, const CameraContext& ctx, float dt) = 0;
    virtual AZ::EntityId ResolveTarget(AZ::EntityId incoming, TargetSignalKind kind) = 0;
    virtual const char* GetDisplayName() const { return "Aim Stage"; }

    static void Reflect(AZ::ReflectContext* context);
};

Aim does not implement state inheritance — inheritance is body-only by design. See Aim Stage Variants.

IAdditiveStage

Stackable. Self-declares phase via GetStage.

class IAdditiveStage {
public:
    AZ_RTTI(IAdditiveStage, "{B7D2F618-3E44-4A71-9C21-0F3B7C5E0D04}");
    AZ_CLASS_ALLOCATOR(IAdditiveStage, AZ::SystemAllocator);

    virtual ~IAdditiveStage() = default;

    virtual void Init() {}

    virtual CamPipelineStage GetStage() const = 0;  // returns Reposition or Noise
    virtual void Evaluate(CameraState& state, const CameraContext& ctx, float dt) = 0;
    virtual const char* GetDisplayName() const { return "Additive Stage"; }

    static void Reflect(AZ::ReflectContext* context);
};

See Additive Stage Variants.


CameraState and CameraContext

CameraState

The mutable pose accumulator threaded through every stage. Not serialized.

struct CameraState {
    AZ::Vector3    position    = Vector3::Zero();        // accumulator
    AZ::Quaternion rotation    = Quaternion::Identity();

    // Sidecar hints populated by Body/Aim for downstream stages.
    AZ::Vector3    bodyAnchor  = Vector3::Zero();   // post-offset follow anchor
    AZ::Vector3    lookAtPoint = Vector3::Zero();   // world-space focal point
};

The two sidecar fields exist so Reposition additives can read the “natural” pose Body / Aim intended rather than the live state (which may have been displaced by an earlier additive). Tug listeners, for example, blend toward the source point starting from the Body’s bodyAnchor rather than the cam’s possibly-perturbed state.position — prevents compound feedback across ticks.

CameraContext

Read-only per-tick context. Not serialized.

struct CameraContext {
    GS_PhantomCameraComponent* owner;        // for owner helpers (WriteLensFieldOfView, QueueSnapCamera).

    AZ::EntityId               camEntityId;
    AZ::EntityId               targetEntityId;

    AZ::Transform              camInitialTM;     // cam TM read at start of tick
    AZ::Transform              targetTM;         // resolved target world TM
    AZ::Vector3                targetVelocity;

    bool                       targetIsPhysical;
    bool                       hasFocus;
    bool                       alwaysUpdate;
    bool                       blendingOut;
    bool                       snapThisFrame;    // bypass damping this tick

    float                      deltaTime;
};

hasFocus || alwaysUpdate || blendingOut is the input-integration gate — Body stages that drive pose from input (notably DynamicOrbitBody) integrate input only when any of these is true. See Execution States.

snapThisFrame is set on focus gain, target swap, queued snap, and a few other lifecycle events. Stages honor it by bypassing damping for the one tick it is true.


Target Routing

Every Body and Aim variant authors a CamTargetMode enum and (depending on mode) supporting fields. This is the front-end every variant uses to decide which entity it actually follows.

enum class CamTargetMode : AZ::u8 {
    None               = 0,  // no-op for target resolution
    Transform          = 1,  // follow / look at the dispatched CameraTarget
    TargetOffsetEntity = 2,  // reserved — not yet implemented
    // 3 was FocusGroup — removed; use GroupTarget.
    Head               = 4,  // reserved — not yet implemented
    Override           = 5,  // manually-set m_overrideEntity on this stage
    GroupTarget        = 6   // resolve a named GroupTargetComponent via CamManager
};

TargetSignalKind identifies which bus call dispatched the target signal. Lets stages filter by target mode:

enum class TargetSignalKind {
    CameraTarget,  // SetCameraTarget
    FocusGroup     // SetTargetFocusGroup
};

Legacy scenes storing FocusGroup = 3 deserialize as an unknown value; the author re-picks GroupTarget. The newer mode is the canonical way for a stage to track a named group via Group Targets.


Decoupled Ideal Pattern

A subtle but critical convention: each Body / Aim variant maintains an m_idealPosition / m_idealRotation independent of what gets committed to the entity transform.

Why: Reposition additives (collision, occlusion, tug) and Noise additives mutate state.position and state.rotation AFTER Body / Aim runs. If the Body read the committed transform back next tick, the spring would re-try its advance every frame against a collision clamp — producing shudder. By tracking an internal ideal, the Body’s spring runs in clean kinematic space; the committed transform may differ (clamped by collision, shaken by noise) but the Body’s trajectory stays smooth.

The same applies to Aim — m_idealRotation is the slerp’s working value; downstream rotation-modifying additives can freely displace state.rotation without poisoning next tick’s slerp trajectory.

m_idealPosition / m_idealRotation are also what state inheritance reads for TryGetPoseSnapshot and writes for TryAdoptPoseSnapshot — see State Inheritance.


Damping Convention

There is no separate Smoothing stage. Each stage owns its damping internally:

StageWhat it damps
BodyPursuit of the follow target — halflife on m_idealPosition.
AimSlerp toward the look-at rotation — halflife on m_idealRotation.
Reposition additivesTheir own correction delta — collision pushback, tug-source approach.
Noise additivesOperate in their own time domain (noise time, ADSR envelope).

Different halflives capture different physical slownesses and compose naturally. Damping uses the frame-rate-independent HalflifeAlpha(halflife, dt) = 1 - exp2(-dt / halflife) helper.

ctx.deltaTime is clamped at tick entry (maxEvalDeltaTime = 0.05s default) so editor hitches don’t spike damping.


Init Contract

Each stage’s Init() runs once per Activate, after the slot is populated, before any Evaluate. The default is a no-op.

Critical use case — asset-fresh-load. Stages holding asset references (e.g. DynamicOrbitBody.m_orbitShape, PerlinNoise.m_profile) override Init to force a fresh AssetManager load. The deserialized asset reference only carries an AssetId — without an explicit GetAsset call in Init, in-editor edits to the asset don’t propagate to the runtime instance.

Pattern:

void DynamicOrbitBody::Init()
{
    if (m_orbitShape.GetId().IsValid())
    {
        m_orbitShape = AZ::Data::AssetManager::Instance().GetAsset<CameraOrbitShape>(
            m_orbitShape.GetId(),
            AZ::Data::AssetLoadBehavior::PreLoad);
        m_orbitShape.BlockUntilLoadComplete();
    }
}

Any new stage you author that holds an AZ::Data::Asset<T> should follow this pattern.


Polymorphic-Array Convention

The component’s stage slots hold raw pointers in a vector:

AZStd::vector<IBodyStage*>     m_bodySlot;     // single-element conceptually
AZStd::vector<IAimStage*>      m_aimSlot;      // single-element conceptually
AZStd::vector<IAdditiveStage*> m_additives;

Why vectors instead of single pointers? The vector enables the editor’s type-picker for polymorphic types — the user picks from a dropdown of all registered derived classes. The runtime treats Body and Aim slots as single-element; the dropdown for those slots is constrained to one entry by policy.

The component owns the pointers (newed by the editor’s type instantiation; deleted by the component’s Deactivate / destructor).


StageHelpers Utility

StageHelpers.h provides free inline functions used by multiple variants:

HelperPurpose
ResolveByMode(mode, incoming, override, kind, groupTargetName)Resolves the actual tracked entity from a CamTargetMode. Shared by every Body and Aim variant.
ResolveStageTargetTM(mode, groupTargetName, ctx)Resolves the target world TM. Handles Transform (uses ctx.targetTM), Override (looks up m_overrideEntity), GroupTarget (calls Cam Manager’s group registry).
ApplyFollowOffset(point, targetTM, offset, isRelative)Adds the offset to the point. World axes or target-relative basis.
HalflifeAlpha(halflife, deltaTime)Frame-rate-independent damping alpha: 1 - exp2(-deltaTime / halflife).
WrapYawToPi(yaw)Wraps yaw into (-π, π].
ShortestYawTarget(currentYaw, desiredYaw)Returns the equivalent of desiredYaw that is numerically closest to currentYaw (avoids 2π wraparound jumps for monotonic damping).
FindPitchOnShape(shape, radiusObserved, heightObserved)Inverse search on a CameraOrbitShape — given an observed (radius, height) pair, returns the pitch that produces that point on the curve. Used by DynamicOrbitBody’s POSITION-mode adoption fallback.

Reflection Conventions

Every stage class follows the same Reflect pattern:

void MyStage::Reflect(AZ::ReflectContext* context)
{
    if (auto* sc = azrtti_cast<AZ::SerializeContext*>(context))
    {
        if (sc->FindClassData(azrtti_typeid<MyStage>())) { return; }  // guard

        sc->Class<MyStage, IBodyStage>()   // base = IBodyStage / IAimStage / IAdditiveStage
            ->Version(1)
            ->Field("TargetMode", &MyStage::m_targetMode)
            ->Field("Offset",     &MyStage::m_offset)
            // ...
            ;

        if (auto* ec = sc->GetEditContext())
        {
            ec->Class<MyStage>("Display Name", "Tooltip description")
                ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                    ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
                    ->Attribute(AZ::Edit::Attributes::NameLabelOverride, "<h3>My Stage</h3>")
                ->DataElement(AZ::Edit::UIHandlers::ComboBox, &MyStage::m_targetMode, "Target Mode", "...")
                    ->EnumAttribute(CamTargetMode::Transform, "Transform")
                    // ...
                ;
        }
    }
}

Hard rules for stage authors:

  • The FindClassData guard is mandatory. Multiple stages reflect the shared curve type / common enums chain, and an unguarded reflect call triggers a double-registration crash.
  • EnableForAssetEditor belongs under SerializeContext only, not EditContext. Stages don’t typically need this attribute, but it applies when reflecting asset types.
  • Members are named with the m_ prefix per the O3DE code-style guideline; field strings inside Reflect keep the un-prefixed form for save-data compatibility.

See Also

Stage variant catalogs:

For related PhantomCam pages:

  • Phantom Cameras — the owning component.
  • Cam Core — consumes published poses.
  • State Inheritance — the per-Body TryGet/AdoptPoseSnapshot protocol.
  • Orbit Profiles.camorbit asset consumed by DynamicOrbitBody.
  • Noise Profiles.camnoiseprofile asset consumed by PerlinNoise / ImpulseNoise.
  • Tug Fields — volume / source / proxy model; tug listeners are additive stages.
  • Group TargetsCamTargetMode::GroupTarget resolves through Cam Manager’s registry.

For conceptual overviews and usage guides:


Get GS_PhantomCam

GS_PhantomCam — Explore this gem on the product page and add it to your project.

1.1 - Body Stage Variants

Body stage catalog — DefaultFollowBody, OrbitBody, DynamicOrbitBody, LeadingFollowBody, TrackBody. Each variant’s kinematic model, authored fields, and state-inheritance support.

The Body stage owns state.position for each tick. Five variants ship with GS_PhantomCam — each with its own kinematic model (orbit math, spring damping, path interpolation, band response, etc.). All variants share a target-routing front-end via the CamTargetMode enum documented in Stage Pipeline. Per-variant state inheritance support is summarized below; see State Inheritance for the full protocol.

Body stage slot on the Phantom Camera component

 

Contents


DefaultFollowBody

Simple follow camera. Tracks a target at an authored offset, position-space spring damped.

Kinematic model

destPos        = targetTM.translation + ApplyOffset(offset, targetTM, isRelative)
state.position = SimpleSpringDamperExact(m_idealPosition, m_springVelocity,
                                         destPos, halflife, dt)

Spring runs against an internal m_idealPosition per the decoupled-ideal pattern. On ctx.snapThisFrame, m_idealPosition = destPos directly.

Authored fields

FieldDefaultPurpose
m_targetModeTransformHow to resolve the follow target.
m_overrideEntity(entity slot)Used when m_targetMode == Override.
m_groupTargetName""Used when m_targetMode == GroupTarget.
m_offset(0, 0, 0)Offset applied before damping.
m_offsetIsRelativefalseWorld axes (false) or target-relative basis (true).
m_halflife0.1Spring halflife.

State inheritance

None. Default IBodyStage virtuals return false. Inheritance into a Default Follow falls through to the Blend Profile’s visual bridge.


OrbitBody

Fixed orbital pose around the target — authored yaw, pitch, and radius, no input. The “static orbit cam.” Successor to the retired StaticOrbit_PhantomCamComponent.

Kinematic model

destPos     = targetTM.translation + offset
pitchRad    = clamp(degToRad(m_orbitPitchDeg), -89°, +89°)
yawRad      = degToRad(m_orbitYawDeg)

orbitOffset = m_orbitRadius * (cos(pitch)*cos(yaw), cos(pitch)*sin(yaw), sin(pitch))
destPos    += orbitOffset

state.position = SimpleSpringDamperExact(m_idealPosition, m_springVelocity,
                                         destPos, m_halflife, dt)

The cached pivot m_lastPivot (the post-offset, pre-orbit pivot point) is published through TryGetPoseSnapshot for inheritance into orbit-style adopters.

Authored fields

FieldDefaultPurpose
m_targetModeTransformTarget routing.
m_overrideEntityWhen m_targetMode == Override.
m_groupTargetName""When m_targetMode == GroupTarget.
m_offset(0, 0, 0)Pre-orbit offset.
m_offsetIsRelativefalseBasis.
m_orbitRadius5.0Distance from pivot to cam.
m_orbitYawDeg45.0Horizontal angle around pivot.
m_orbitPitchDeg20.0Vertical angle (clamped −89° to +89°).
m_halflife0.1Spring halflife (0 = snap).

State inheritance

DirectionBehavior
GetPublishes the cleanest snapshot in the framework: worldPos = m_idealPosition, worldFwd toward cached pivot, pivotPos = m_lastPivot, AND direct angular state from authored yaw / pitch (yawRad, pitchRad, angularStateValid = true). Orbit-style adopters receive ANGULAR-mode handoff with no back-derivation.
AdoptNot implemented — left at default false. The authored yaw / pitch define the static shot’s identity; accepting inbound pose would defeat author intent. Blend Profile’s visual bridge handles inbound transitions.

DynamicOrbitBody

Input-driven orbit camera around a target, shaped by a CameraOrbitShape (.camorbit) asset. The current go-to body for orbit-style player cams. Successor to the retired OrbitCam variant.

Unified drive model

The stage holds target angles (m_targetYaw, m_targetPitch) and damps the cam’s position toward whatever those angles imply on the orbit surface via the Orbital Solver Utility. Target angles are written by:

  1. Snap seeding (m_initialYawDeg / m_initialPitchDeg) on first activate or ctx.snapThisFrame.
  2. External scripted calls via DynamicOrbitBodyRequestBus::SetOrbit(yawRad, pitchRad).
  3. Per-tick input integration — when an OrbitInputProvider bus handler is bound to the cam’s entity (the Camera Input Reader component), the body polls input deltas and integrates them. Gated on ctx.hasFocus || ctx.alwaysUpdate || ctx.blendingOut.

No drive-mode enum. Multiple writers compose; last write wins per tick; solver damping shapes the visual response.

Static-orbit behavior is also achievable here by authoring initial angles and not installing an input provider — target angles never change, solver damps once, holds. The separate OrbitBody class is retained for the explicit “static shot” identity case.

Kinematic model

// 1. Resolve pivot (post-offset target).
pivot = targetTM.translation + offset
m_lastResolvedPivot = pivot   // cached for inheritance Get

// 2. Inheritance consume (if pending). Sets m_targetYaw / m_targetPitch /
//    m_idealPosition. See [State Inheritance].

// 3. Snap handling — reset target angles to authored seeds on
//    ctx.snapThisFrame (unless inheritance just consumed).
if (ctx.snapThisFrame && !consumedAdoption):
    m_targetYaw   = degToRad(m_initialYawDeg)
    m_targetPitch = degToRad(m_initialPitchDeg)

// 4. Input integration (if cam has an OrbitInputProvider AND
//    ctx.hasFocus || alwaysUpdate || blendingOut).
yawDelta, pitchDelta = poll OrbitInputProvider
m_targetYaw   += yawDelta   * degToRad(m_yawSpeed)   * dt
m_targetPitch += pitchDelta * degToRad(m_pitchSpeed) * dt
m_targetYaw   = WrapYawToPi(m_targetYaw)
m_targetPitch = clamp(m_targetPitch, kPitchAtLow, kPitchAtHigh)

// 5. Orbit shape evaluation.
shapePoint = m_orbitShape->EvaluateAtPitch(m_targetPitch)  // (radius, height)
desiredPos = pivot + (shapePoint.radius * cos(m_targetYaw),
                     shapePoint.radius * sin(m_targetYaw),
                     shapePoint.height)

// 6. Solver damping via BlendPositionAroundPivot.
alpha = HalflifeAlpha(m_blendHalflife, dt)
m_idealPosition = GS_Core::Math::BlendPositionAroundPivot(
                    m_idealPosition, desiredPos, pivot,
                    alpha, m_blendShape, +Z)

state.position = m_idealPosition

Authored fields

FieldDefaultPurpose
m_targetModeTransformTarget routing.
m_overrideEntityWhen m_targetMode == Override.
m_groupTargetName""When m_targetMode == GroupTarget.
m_offset(0, 0, 0)Pivot offset from target.
m_offsetIsRelativefalseBasis.
m_orbitShape(asset slot).camorbit asset defining the surface. See Orbit Profiles.
m_yawSpeed90Degrees / sec per unit input on yaw.
m_pitchSpeed60Degrees / sec per unit input on pitch.
m_blendHalflife0.10Damping halflife — the solver call IS the smoothing.
m_blendShapeSphericalSolver shape (Spherical allows arbitrary surface; Cylindrical constrains height).
m_initialYawDeg180.0Snap seed yaw. 180° = directly behind target.
m_initialPitchDeg0.0Snap seed pitch. 0° = mid band height.
m_debugPrintfalsePer-tick AZ_Printf of inputs / target angles / shape output / committed position.

Companion bus

DynamicOrbitBodyRequestBus (per-entity, addressed by cam EntityId):

virtual void  SetOrbit(float yawRad, float pitchRad) = 0;
virtual float GetCurrentTargetYaw()   const = 0;
virtual float GetCurrentTargetPitch() const = 0;

SetOrbit uses ShortestYawTarget to keep the damping arc bounded — prevents the solver taking the long way around for distant yaw writes.

State inheritance

Full Get + Adopt. Two derivation paths:

PathTriggerBehavior
ANGULAR (preferred)snap.angularStateValid == true (source published authored angles)Inherit raw yawRad / pitchRad directly via ShortestYawTarget + WrapYawToPi + pitch clamp. Preserves angular continuity across different shape assets.
POSITION (fallback)Source has no angular state, just worldPos + optional pivotPosBack-derive yaw from (worldPos − pivot) via atan2; search the shape for the matching pitch via FindPitchOnShape.

Both seed m_idealPosition = snap.worldPos and set consumedAdoption = true so subsequent snap-clobber logic is gated.

Get publishes m_idealPosition, world-forward toward cached pivot, pivotPos = m_lastResolvedPivot, pivotEntity = m_lastResolvedPivotEntity, and direct authored m_targetYaw / m_targetPitch + angularStateValid = true.

Init contract

Init() overrides to force-load m_orbitShape via AssetManager::GetAsset + BlockUntilLoadComplete per the asset-fresh-load pattern. Without this, in-editor .camorbit edits don’t propagate to the runtime body until restart.


LeadingFollowBody

Held-position cam that slides along an arc around the target only when the target leaves an authored distance band. Leading-look / “Gears of War”-style follow — heading-agnostic, so abrupt 180° turns by the target don’t swing the cam.

Kinematic model

distXY = horizontal distance between cam and target (at offset's height)

if inner < distXY < outer:
    hold position   // cam stays put within the band
else if distXY > outer:
    slide toward target along an arc until just inside outer
else if distXY < inner:
    slide away from target along an arc until just outside inner

// Slides use the orbital solver around the target as pivot — cam arcs
// around the target during band response rather than cutting through.

// Z tracking: independent linear-halflife lowpass on the Z axis, mirroring
// target Z without affecting XY band math.

// Optional center-on-heading: after a configurable stillness delay, cam arcs
// around target to settle behind (or off-axis behind via m_idleYawOffsetDeg)
// at the current radius.

Authored fields

FieldDefaultPurpose
m_targetModeTransformTarget routing.
m_overrideEntity / m_groupTargetNameMode-dependent.
m_offset(0, 0, 1.6)Offset applied before band math. Cam rides at this Z height.
m_offsetIsRelativefalseBasis.
m_innerRadius2.0Min cam-target XY distance.
m_outerRadius5.0Max cam-target XY distance.
m_hardClampEnabledfalseOptional absolute outer cap.
m_hardClampDistance8.0Hard cap (when enabled).
m_radialHalflife0.25Arc-slide halflife when outside the envelope.
m_heightHalflife0.50Independent Z follow halflife.
m_blendShapeCylindricalSolver shape for band response — cam stays in working height plane. Downgradable to Linear for short displacements.
m_centerOnHeadingfalseEnable idle reposition.
m_idleVelocityThreshold0.10 m/sXY speed below which the idle timer accumulates.
m_idleDelay1.50 sStillness duration before reposition engages.
m_reorientHalflife0.60Idle reposition arc halflife.
m_idleYawOffsetDeg0Offset from “directly behind” during reposition. ± shifts off-axis.

State inheritance

DirectionBehavior
GetReturns m_idealPosition, world-forward toward cached target, pivotPos = m_lastTargetPoint. No angular state.
AdoptPure stash. Evaluate consume block takes source snap.worldFwd, flattens to XY, places m_idealPosition = targetPoint − inboundFwd * seedDist at band-natural distance (lerp(inner, outer, 0.6)). Cam inherits source’s facing at this body’s preferred standoff. Z naturally lands at targetPoint.Z (inboundFwd is XY-flattened) — a from-below source produces a band-natural pose, not stuck-below framing.

Authoring gotcha. If the matched Blend Profile entry has m_inheritState unchecked, the snap-on-activation block fires instead of the consume block. Symptom: “cam orients behind the player’s travel direction regardless of source facing.” The fix is data-side — check the inherit flag in the asset.


TrackBody

Spline-bound dolly camera. Slides along a SplineComponent, interpolating two authored TrackBodyData blocks (start + end) by spline position (globalT 0..1). Successor to the retired Track_PhantomCamComponent.

Per-endpoint data blocks carry offset, halflife, and an optional FoV override.

Kinematic model

// Resolve nearest spline point to current target.
splineHit = spline->FindClosestWorldPoint(targetTM.translation)
globalT   = splineHit.normalizedDistance   // 0..1 along spline

// Lerp data blocks.
offset    = lerp(m_startData.offset,   m_endData.offset,   globalT)
halflife  = lerp(m_startData.halflife, m_endData.halflife, globalT)
fovDest   = (m_endData.fieldOfView > 0)
            ? lerp(m_startData.fieldOfView, m_endData.fieldOfView, globalT)
            : 0

// Position = spline point + offset (relative or world basis).
destPos        = splineHit.worldPoint + ApplyBasis(offset, splineTangent, isRelative)
state.position = SpringDamp(m_idealPosition, m_springVelocity, destPos, halflife, dt)

// Push FoV to owner via WriteLensFieldOfView so Cam Core picks it up live.
if (fovDest > 0):
    ctx.owner->WriteLensFieldOfView(fovDest)

Authored fields

FieldDefaultPurpose
m_targetModeTransformUsed to compute the closest spline point. Typically Transform.
m_overrideEntity / m_groupTargetNameMode-dependent.
m_splineTrack(entity slot)Entity carrying a SplineComponent.
m_startDataEndpoint A (TrackBodyData).
m_endDataEndpoint B.
m_adoptionPathThreshold5.0 mInheritance: max distance from spline an inbound pose may sit before adoption is rejected.

TrackBodyData

struct TrackBodyData {
    AZ::Vector3 m_offset            = (0, 0, 0);
    bool        m_offsetIsRelative  = false;
    float       m_halflife          = 0.1f;
    float       m_fieldOfView       = 0.0f;   // 0 = don't push lens
};

State inheritance

DirectionBehavior
GetReturns m_idealPosition + world-forward toward cached m_lastTargetPos. No angular state. No pivot (the path is the kinematic reference, not a point).
AdoptPure stash. Evaluate consume block: EnsureSplineResolved, project snap.worldPos via FindClosestWorldPoint. Reject if Euclidean distance to projected point > m_adoptionPathThreshold — prevents teleport-snap when the source sits well off the dolly’s path. Inside threshold: seed m_idealPosition = snap.worldPos, clear spring velocity.

Spline resolution timing

EnsureSplineResolved is lazy — runs on first Evaluate with a valid target. Caches m_spline and m_splineEntity so subsequent ticks don’t re-look-up.


Cross-Variant Summary

VariantDegrees of FreedomDampingPivotInheritance GetInheritance Adopt
DefaultFollowBodyPosition onlySpringNone
OrbitBodyAuthored yaw / pitch / radiusSpringPost-offset targetFull angularDisabled (Get-only)
DynamicOrbitBodyDynamic yaw / pitchSolverPost-offset targetFull angularFull (ANGULAR / POSITION)
LeadingFollowBodyXY band + Z followSolver (band-arc)Post-offset targetworldPos + fwdFacing-seeded standoff
TrackBodySpline-boundSpringNone (path)worldPos + fwdProject + threshold

See Also


Get GS_PhantomCam

GS_PhantomCam — Explore this gem on the product page and add it to your project.

1.2 - Aim Stage Variants

Aim stage catalog — DefaultLookAtAim, ClampedLookAim. Aim-origin convention, decoupled ideal rotation, extension points.

The Aim stage owns state.rotation for each tick. Two variants ship with GS_PhantomCam; other gems (notably gs_performer) register additional variants through the same IAimStage interface. All variants share a target-routing front-end via the CamTargetMode enum documented in Stage Pipeline.

Aim does not implement state inheritance by design — inheritance is body-only. The destination cam’s aim runs fresh on the inherited body pose.

Aim stage slot on the Phantom Camera component

 

Contents


Aim-Origin Convention

A subtle but important rule: every Aim variant uses ctx.camInitialTM.GetTranslation() as the aim origin — the cam’s current world position at the start of the tick — not state.position (which the Body just wrote this tick).

Aiming from a position the cam hasn’t moved to yet would produce a frame-delayed look-at vector. Using camInitialTM keeps the aim ray geometry consistent with where the cam is actually rendered from.

Author your own aim variants the same way.


Decoupled Ideal Rotation

Same convention as the Body stage. Aim variants hold an internal m_idealRotation slerp working value, separate from state.rotation. Downstream rotation-modifying additives (Noise, Impulse, Tug aim listener) can perturb state.rotation without poisoning next tick’s slerp trajectory.

A m_lastValidRotation field preserves the last non-degenerate rotation for zero-forward recovery (the case where the cam ends up coincident with its look-at target and the forward vector collapses).


DefaultLookAtAim

Straightforward look-at. Aims the camera at a target (with optional offset), slerp-damped.

Kinematic model

lookAtPoint = targetTM.translation + ApplyOffset(offset, targetTM, isRelative)
state.lookAtPoint = lookAtPoint        // sidecar — for downstream Reposition consumers

aimOrigin = ctx.camInitialTM.translation     // NOT state.position
forward   = (lookAtPoint - aimOrigin).GetNormalizedSafe()

if (forward.GetLengthSq() < epsilon):
    targetRot = m_lastValidRotation          // zero-forward recovery
else:
    targetRot = Quaternion::CreateLookAt(forward, +Z)
    m_lastValidRotation = targetRot

if (ctx.snapThisFrame || !m_hasIdeal):
    m_idealRotation = targetRot
    m_hasIdeal      = true
else:
    alpha = HalflifeAlpha(m_halflife, dt)
    m_idealRotation = m_idealRotation.Slerp(targetRot, alpha)

state.rotation = m_idealRotation

Authored fields

FieldDefaultPurpose
m_targetModeTransformLook-at target routing.
m_overrideEntityWhen m_targetMode == Override.
m_groupTargetName""When m_targetMode == GroupTarget.
m_offset(0, 0, 0)Offset applied to look-at point.
m_offsetIsRelativefalseWorld axes (false) or target-relative basis (true).
m_halflife0.1Slerp halflife.

ClampedLookAim

Aims at a target clamped within a pitch / yaw envelope relative to a starting forward captured on first evaluation. Replacement for the retired ClampedLook_PhantomCamComponent.

Used for first-person and constrained-look scenarios — e.g. the cam can rotate ±45° pitch and ±90° yaw from its initial facing, but not beyond.

Two clamp modes

bool m_localSpace;
ModeBehavior
false (world-space, default)Yaw clamps around world +Z, pitch clamps in the world-vertical plane. Yaw / pitch reconstruction from quaternion.
true (local-space pivot)Pivot relative to m_startingForward. Useful when the cam should rotate within a local envelope independent of world axes.

Kinematic model (world-space mode)

On first Evaluate or ctx.snapThisFrame:
    m_startingForward = ctx.camInitialTM.GetRotation()   // capture once

lookAtPoint = targetTM.translation + offset
forward     = (lookAtPoint - ctx.camInitialTM.translation).GetNormalizedSafe()

// Convert forward to yaw / pitch.
yaw   = atan2(forward.y, forward.x)
pitch = asin(forward.z)

// Clamp to envelope (degrees  radians for comparison).
yaw   = clamp(yaw,   degToRad(m_minRelClamp.z), degToRad(m_maxRelClamp.z))
pitch = clamp(pitch, degToRad(m_minRelClamp.x), degToRad(m_maxRelClamp.x))

// Reconstruct quaternion from clamped angles.
clampedForward = (cos(pitch)*cos(yaw), cos(pitch)*sin(yaw), sin(pitch))
targetRot      = LookAtQuaternion(clampedForward, +Z)

// Slerp damping with decoupled m_idealRotation.
state.rotation = Slerp(m_idealRotation, targetRot, HalflifeAlpha(m_halflife, dt))

Authored fields

FieldDefaultPurpose
m_targetModeTransformLook-at target routing.
m_overrideEntity / m_groupTargetNameMode-dependent.
m_offset(0, 0, 0)Look-at offset.
m_offsetIsRelativefalseBasis.
m_minRelClamp(-45, 0, -90)Lower bound — x = pitch min, z = yaw min. (Layout mirrors the legacy component for scene-data migration.)
m_maxRelClamp(45, 0, 90)Upper bound — x = pitch max, z = yaw max.
m_localSpacefalseClamp mode (world-space or local-space pivot).
m_halflife0.1Slerp halflife.

Runtime state

FieldPurpose
m_startingForwardCaptured on first Evaluate. The “origin” relative to which the clamp envelope is anchored.
m_hasStartingForwardTrue after first capture.
m_idealRotationDecoupled slerp working value.
m_hasIdealTrue after first ideal write.
m_lastValidRotationLast non-degenerate rotation, used for zero-forward recovery.

Extension Surface

Other gems can ship additional aim stage variants. The editor’s type-picker on the cam’s m_aimSlot enumerates all derived IAimStage classes registered through SerializeContext. gs_performer, for example, registers aim stages that read performer head / eye tracking state.

Convention for authoring an aim variant in another gem:

  • Header in Code/Include/<GemName>/Stages/Variants/<MyAim>.h.
  • Source in Code/Source/Stages/Variants/<MyAim>.cpp.
  • Use AZ_RTTI(<MyAim>, "{UUID}", IAimStage) so the picker finds it.
  • Reflect with the FindClassData guard.
  • Honor the aim-origin convention — use ctx.camInitialTM.translation, not state.position.
  • Use the decoupled ideal rotation pattern if you want downstream Noise / Impulse / Tug aim listener to behave well.

See Also


Get GS_PhantomCam

GS_PhantomCam — Explore this gem on the product page and add it to your project.

1.3 - Additive Stage Variants

Additive stage catalog — collision and tug listener Reposition stages, Perlin and Impulse Noise stages. Phase semantics, composition rules, per-variant fields.

Additive stages stack — multiple may run per cam. Each declares its phase via GetStage():

  • Reposition — runs BEFORE noise. Corrections (collision, occlusion, tug reposition). State after this phase is captured as m_stablePose.
  • Noise — runs AFTER reposition. Perturbations (camera shake, drift, impulses). State after this phase is captured as m_finalPose.

The Phantom Camera component partitions additives into m_repositionStages and m_noiseStages once at slot-assign time so per-tick code does not re-check the enum each frame.

Additive stages list on the Phantom Camera component

 

Contents


Reposition Additives

CollisionReposition

Soft / hard sphere collision correction.

Header: Stages/StageDefaults.h · Source: Source/Stages/StageDefaults.cpp

Algorithm

Sphere-cast from (target root + anchorOffset) toward the Body’s ideal cam position (state.position). Two radii define behavior:

  • Inner radius — hard clamp. Cam is never permitted closer to the hit surface than this. Correction is immediate.
  • Outer radius — soft buffer. Cam glides toward this preferred distance from the wall over halflife seconds via a damped cached correction delta.

When the Body’s ideal is inside the outer zone (between inner and outer), only the soft correction applies. When it is past the inner zone (closer than inner), hard correction snaps the cam to the inner boundary, and soft continues gliding it toward the outer resting spot.

The cached correction m_appliedCorrection damps cleanly toward zero when the target walks out of collision — no feedback loop fighting the Body’s spring (which runs on its own m_idealPosition per the decoupled-ideal pattern).

Authored fields

FieldDefaultPurpose
m_enabledtrueMaster toggle.
m_anchorOffset(0, 0, 1.5)Added to target root to produce the cast origin. Typically cam-height so the cast sweeps at cam altitude.
m_innerRadius0.15Hard clamp distance. Serialized as "Radius" for back-compat.
m_outerRadius0.45Soft buffer distance. Clamped at evaluate time to be ≥ inner.
m_halflife0.15Soft correction halflife (0 = snap).
m_collisionGroupId(null = All)UUID reference to a PhysX collision-group preset. Rendered as a dropdown by PhysX’s property handler — matches the “Collides With” field on PhysX colliders.

OcclusionReposition

Stub. Math is future work. The damping scaffold (m_halflife, m_appliedCorrection) is in place so authored fields are stable when the real implementation lands.

FieldDefaultPurpose
m_enabledtrueMaster toggle.
m_halflife0.15Correction halflife.

TugAimListener

Reposition-phase consumer of Tug Fields. Slerps state.rotation toward look-at the smoothed tug source point while a TugFieldProxyComponent on the rig is in contact with a tug volume.

Header: Stages/TugListeners.h · Source: Source/Stages/TugListeners.cpp

Derives from the abstract TugListenerBase which holds the channel matching, proxy resolution, active-source cache, and smoothed-influence state.

Authored fields

FieldDefaultPurpose
m_enabledtrueMaster toggle.
m_channels[]String tags the listener matches against tug volume channels. Crc32-cached at activate.
m_blendHalflife0.15Smoothing halflife for influence and source-point.
m_strength1.0Per-cam force multiplier.

Per-tick algorithm

1. ResolveProxy(ctx)
   - If target changed since last tick, walk for proxy descendant.
   - If proxy changed, bind to it and repopulate from volume world.

2. Sum contributions from m_activeSources (weighted by source strength × channel match).
   totalInfluence, weightedSourcePoint = Σ source contributions

3. If totalInfluence == 0 AND m_smoothedInfluence ≈ 0:
     m_dormant = true; early-return.

4. If transitioning out of dormancy:
     m_smoothedSourcePoint = GetSeedSourcePoint(state) — a point along the cam's
     current forward, so the ramp-in produces zero displacement at first engagement.

5. Damp m_smoothedInfluence and m_smoothedSourcePoint toward their target values.

6. ApplyModulation: derive the "natural" rotation from ctx (cam toward primary target)
   rather than reading state.rotation. Avoids compound-feedback issue where prior tug
   modifications would loop into next tick's blend source.

TugBodyListener

Reposition-phase consumer of Tug Fields. Lerps state.position toward the smoothed source point.

Header: Stages/TugListeners.h · Source: Source/Stages/TugListeners.cpp

Same base class, fields, and dormancy semantics as TugAimListener.

Algorithm differences from TugAimListener

StepTugBodyListener
GetSeedSourcePointReturns state.position — first engaged tick produces zero displacement (the cam doesn’t yank).
ApplyModulationDerives the natural pose source from ctx.targetTM and the Body’s published state.bodyAnchor sidecar, not state.position directly. Prevents compound feedback.

A cam can run only the aim listener, only the body listener, both, or neither — each is independent.


Noise Additives

PerlinNoise

Continuous Perlin-noise displacement. Samples a Camera Noise Profile (.camnoiseprofile) through GS_Core::Noise::Perlin1D across six channels (translation X / Y / Z, rotation pitch / yaw / roll).

Header: Stages/NoiseStages.h · Source: Source/Stages/NoiseStages.cpp

Algorithm

m_time += deltaTime

For each of 6 channels in profile:
    sample = Perlin1D(m_time * profile.frequency[i] * m_frequencyGain,
                      profile.octaves[i],
                      profile.persistence[i])
    delta[i] = sample * profile.amplitude[i] * m_amplitudeGain

state.position += (delta[Tx], delta[Ty], delta[Tz])
state.rotation  = state.rotation * Quaternion::CreateFromEuler(
                    delta[Pitch], delta[Yaw], delta[Roll])

Never reads back the committed transform between frames — only adds deltas to state in-flight. Body / Aim ideals stay untouched (decoupled-ideal convention).

Authored fields

FieldDefaultPurpose
m_enabledtrueMaster toggle.
m_profile(asset slot).camnoiseprofile asset.
m_amplitudeGain1.0Multiplier over profile amplitudes.
m_frequencyGain1.0Multiplier over profile frequencies.

Init contract

Init() force-loads m_profile via AssetManager per the asset-fresh-load pattern.

ImpulseNoise

Event-triggered one-shot noise burst. Same profile schema as PerlinNoise, same layered Perlin math — only the time source differs.

Stays silent until Trigger(strength) fires, then plays a time-windowed burst shaped by an ADSR envelope (GS_Core::Envelope::ADSREnvelope).

Triggered externally via PhantomCameraRequestBus::TriggerCameraImpulse(strength) on the cam — see Phantom Cameras. The author’s own impulse-source detection system (explosions, foot-stomps, gun kicks) decides when and with what strength.

Stacks freely with PerlinNoise on the same cam — baseline handheld sway plus event-driven shake is the canonical configuration.

Algorithm

On Trigger(strength):
    m_elapsed         = 0.0
    m_triggerStrength = strength
    m_active          = true       // overrides any in-progress release

Per tick (if m_active):
    m_elapsed += deltaTime
    envelopeGain = m_envelope.Evaluate(m_elapsed)   // ADSR 0..1
    if (envelopeGain == 0 AND past sustain end):
        m_active = false
    else:
        // Same Perlin sampling as PerlinNoise,
        // scaled by envelopeGain × triggerStrength × m_amplitudeGain.
        Apply position + rotation deltas to state.

Authored fields

FieldDefaultPurpose
m_enabledtrueMaster toggle.
m_profile(asset slot).camnoiseprofile.
m_envelope(ADSR struct)Attack / Decay / Sustain level / Sustain duration / Release. See GS_Core::Envelope::ADSREnvelope.
m_amplitudeGain1.0Multiplier over profile amplitudes.
m_frequencyGain1.0Multiplier over profile frequencies.

Trigger entry point

void ImpulseNoise::Trigger(float strength);

Called by GS_PhantomCameraComponent::TriggerCameraImpulse(strength) for every ImpulseNoise additive on the cam. strength multiplies amplitude for that specific burst. Pass 1.0 for full profile intensity, smaller for attenuated distance falloff, larger to boost.

The owning component routes the call by walking m_noiseStages and dispatching to each ImpulseNoise it finds via RTTI check. Other noise additive types ignore.


Cross-Variant Summary

VariantPhaseReads ctx natural pose?Notes
CollisionRepositionRepositionPhysX sphere-cast pushback. Soft buffer + hard clamp.
OcclusionRepositionRepositionStub. Damping scaffold present.
TugAimListenerRepositionYes (cam-to-target forward)Slerps rotation toward source.
TugBodyListenerRepositionYes (target / bodyAnchor)Lerps position toward source.
PerlinNoiseNoiseContinuous Perlin sampled by accumulated time.
ImpulseNoiseNoiseEvent-triggered; ADSR-gated.

See Also


Get GS_PhantomCam

GS_PhantomCam — Explore this gem on the product page and add it to your project.

2 - Noise Profiles

CameraNoiseProfile (.camnoiseprofile) — reusable Perlin-noise shape asset for camera shake, handheld feel, and event-triggered impulses.

A Camera Noise Profile is a .camnoiseprofile data asset that defines a layered Perlin-noise shape. It is consumed by the PerlinNoise (continuous) and ImpulseNoise (event-triggered) additive stages. Profiles are reusable — one “Handheld_subtle” profile can drive both a baseline sway and a triggered impact burst at different intensities by adjusting per-stage gains.

Profiles are registered through PhantomCamDataAssetsSystemComponent alongside .camblendprofile and .camorbit. The asset reflects with EnableForAssetEditor so the Asset Editor opens it directly.

Camera Noise Profile asset in the O3DE Asset Editor

 

Contents


Authoring Model — Six Per-Axis Layer Stacks

Six independent layer lists, one per axis:

using LayerList = AZStd::vector<GS_Core::Noise::NoiseLayer>;

LayerList m_posX, m_posY, m_posZ;   // position (world-space meters)
LayerList m_rotX, m_rotY, m_rotZ;   // rotation (Euler degrees, applied as multiplicative deltas)

Each layer is a GS_Core::Noise::NoiseLayer carrying amplitude, frequency, and phase. At evaluate time, the noise stage sums every layer’s Perlin1D contribution per axis, scaled by amplitude.

Layered noise is the common authoring rhythm — three layers per axis: one slow / low-frequency layer for sway, one mid layer for flutter, one fast / low-amplitude layer for micro-jitter. Each layer composes additively at evaluate time.


NoiseLayer

The primitive lives in GS_Core:

namespace GS_Core::Noise {
    struct NoiseLayer {
        float m_amplitude = 0.0f;
        float m_frequency = 1.0f;
        float m_phase     = 0.0f;
    };

    // Sums layers via GS_Core::Noise::LayeredPerlin1D.
    // Each layer samples Perlin1D(time * freq + phase) * amp.
}

The same NoiseLayer primitive is used elsewhere in the engine — UI wobble, unit idle jitter, etc. — so multiple subsystems share the underlying Noise utility. The full per-axis evaluation goes through GS_Core::Noise::LayeredPerlin1D documented there.


Unit Conventions

Axis groupUnitsTypical amplitude
Position layersmeters0.01 – 0.2
Rotation layersdegrees0.1 – 8
Frequency (any axis)HzHandheld profiles 0.1 – 2 Hz; shake / impact 3 – 60 Hz

Authored Fields

FieldDefaultPurpose
m_posX / m_posY / m_posZ{}Layer stacks for translation noise (meters).
m_rotX / m_rotY / m_rotZ{}Layer stacks for rotation noise (degrees).
m_description""Free-form note for preset identity, intent, tweak history.

Consumption Pattern

A profile is shared across multiple cams / stages. Per-cam intensity dialing happens on the additive stage, not on the profile:

// On a PerlinNoise additive:
m_profile        = (asset slot)            // shared profile reference
m_amplitudeGain  = 1.0                     // per-cam intensity multiplier
m_frequencyGain  = 1.0                     // per-cam speed multiplier

So a single “Handheld_subtle” profile can drive both a subtle baseline sway (m_amplitudeGain = 0.3) and a full-strength impact shake (m_amplitudeGain = 2.0) without re-authoring the profile.

Both consumers honor the same fields:

StageUseTime source
PerlinNoiseContinuous handheld sway, driftm_time += deltaTime, accumulated since activation
ImpulseNoiseEvent-triggered burstm_elapsed += deltaTime since Trigger(strength), gated by ADSR envelope

See Additive Stage Variants for the per-stage kinematic model.


Shipped Presets

The gem ships a starter library of .camnoiseprofile assets in gs_phantomcam/Assets/Noise Profiles/. Drop a preset onto a PerlinNoise or ImpulseNoise stage’s Profile slot for an immediate baseline, then either tune intensity per cam via the stage’s m_amplitudeGain / m_frequencyGain or author project-specific profiles.

Handheld lens family

Three lens characters × two intensities. Rotation-dominant; long-period sway with mid-frequency flutter and a touch of micro-jitter.

AssetUse for
Normal_Mild.camnoiseprofileNormal-lens handheld baseline — balanced position and rotation, moderate frequencies.
Normal_Intense.camnoiseprofileAggressive normal-lens handheld — same shape, larger amplitudes.
Telephoto_Mild.camnoiseprofileTelephoto-lens handheld baseline — tighter angular response, low position amplitude.
Telephoto_Intense.camnoiseprofileAggressive telephoto handheld — same shape, larger amplitudes.
Wide_Mild.camnoiseprofileWide-lens handheld baseline — translation dominates; rotation low.
Wide_Intense.camnoiseprofileAggressive wide-lens handheld — larger translation amplitudes.

All-axes shake family

Position + rotation across all six axes. Use for event-triggered impulses (ImpulseNoise) or aggressive ambient shake.

AssetUse for
6D_Shake.camnoiseprofileEarthquake / impact aftermath — all six axes, high-frequency micro-jitter on top of mid-frequency sway.
6D_Wobble.camnoiseprofileFloaty / dreamlike — all six axes, low-frequency long sway.

Authoring more

The full per-axis layer values are inspectable in the Asset Editor when you open any preset. To author additional characters — fast-handheld for chase sequences, ultra-mild for cutscenes, gunfire impulse profiles — follow the authoring conventions above starting from a copy of the closest shipped preset.


Creating a Noise Profile

  1. Open the Asset Editor in O3DE.
  2. Select New and choose CameraNoiseProfile from the asset type list.
  3. For each axis where you want noise, add layers via the + button. Three layers per axis is the typical authoring rhythm (slow / mid / fast).
  4. For each layer:
    • Set the Amplitude in axis-appropriate units (meters for position, degrees for rotation).
    • Set the Frequency in Hz.
    • Optionally set a Phase offset (decorrelates layers from each other when they share frequency).
  5. Set the Description to record the preset’s intent or tuning history.
  6. Save the asset.
  7. Assign the asset to a PerlinNoise or ImpulseNoise additive stage’s Profile slot.

See Also

Consumers:

Underlying utility:

  • GS_Core Noise — the Perlin primitives, NoiseLayer struct, and LayeredPerlin1D that this asset stores and feeds.

Related assets:

Basics-side authoring guide:


Get GS_PhantomCam

GS_PhantomCam — Explore this gem on the product page and add it to your project.

3 - Orbit Profiles

CameraOrbitShape (.camorbit) — parametric orbit-surface asset for DynamicOrbitBody. Three-band power-bulge family with arc-length reparameterization for constant spatial speed.

A Camera Orbit Profile is a .camorbit data asset that defines a parametric orbit surface around a target. It is consumed by the DynamicOrbitBody Body stage variant. Authors define three bands (low / mid / high) of (radius, height) and a Roundness slider that walks a power-bulge family — diamond → sphere → cube — across them. Arc-length reparameterization keeps spatial speed constant as the cam traces the shape, for any roundness value.

Profiles are registered through PhantomCamDataAssetsSystemComponent alongside .camblendprofile and .camnoiseprofile. The asset reflects with EnableForAssetEditor so the Asset Editor opens it directly.

Camera Orbit Shape asset in the O3DE Asset Editor

 

Contents


Three-Band Authoring Model

Authors define three (height, radius) bands around the target:

struct OrbitBand {
    float m_height = 0.0f;   // local-Z offset from cam target (meters)
    float m_radius = 5.0f;   // XY-plane distance from cam target (meters)
};
BandDefaultSelected at pitch
m_lowBand(-2.0, 4.0)−π/2 (cam below pivot)
m_midBand( 0.0, 5.0)0 (the “centered / level” rest position)
m_highBand( 3.0, 3.0)+π/2 (cam above pivot)

Z-up convention. m_height is the local-Z offset from the cam target; m_radius is the XY-plane distance.


Pitch Convention

Pitch ∈ [-π/2, +π/2] radians. Pitch is decoupled from the bands’ authored geometry:

  • pitch = 0 → mid band exactly (the “centered / level” rest pose).
  • pitch = +π/2 → high band.
  • pitch = −π/2 → low band.

Authors can put m_midBand.m_height at any value — pitch = 0 always selects mid regardless. Positive pitch always moves the cam toward high; negative always toward low. The bands’ actual heights / radii determine the cam’s spatial position; pitch is a band-interpolation parameter, NOT a literal elevation angle.

This decoupling lets authors think in spatial terms (mid is where the cam rests; low / high are the framing extremes) without needing mid.height = 0 for pitch = 0 to mean “level.”


Shape Family — Roundness Power-Bulge

A single m_roundness slider walks a power-bulge family:

pos(t) = chord(t) + p(t) · offset
where:
    chord(t) = lerp(endpoint, opposite endpoint, t)   // straight line low → high
    offset   = mid − midpoint(low, high)              // how far the bulge pokes out at mid
    p(t)     = 1 − |2t − 1|^n                         // bulge envelope
    n        = piecewise-linear in m_roundness         // 0 → 1 → kMaxBulge

p(t) is 0 at endpoints (t = 0 and t = 1) and 1 at midpoint (t = 0.5) for any n.

m_roundnessnShapeDescription
0.01DiamondPiecewise linear. Sharp corner at mid. Bulge envelope is 1 − |2t − 1| — a tent.
0.52SphereSmooth quadratic through low / mid / high. Bulge envelope is 1 − (2t − 1)². Default.
1.0~6 (kMaxBulge)Square / rounded-squareFlat sides, sharp tapers near low / high. Bulge envelope is 1 − |2t − 1|^6 — almost a square wave.

Mid is always the apex of the bulge for any n. pitch = 0 always lands exactly on mid. Angular velocity stays smooth throughout the slider’s range — no derivative discontinuities.


Curve Mode (Advanced)

For non-monotonic shapes (Bounce, Elastic, Back) that the power-bulge family cannot produce, authors can switch to ShapingMode::Curve and pick a GS_Core::CurveType enum value:

enum class ShapingMode : AZ::u8 {
    Roundness,  // power-bulge family (default)
    Curve,      // CurveType enum (advanced, supports overshoot)
};

When m_shapingMode == Curve, m_curve is consulted instead of m_roundness. The asset’s IsRoundnessVisible / IsCurveVisible predicates hide whichever field doesn’t match the current mode.

Curve mode skips arc-length reparameterization. Overshooting curves expressly want temporal shape, not constant spatial speed.


Arc-Length Reparameterization

EvaluateAtPitch treats input pitch as an arc-length fraction along the (radius, height) curve, not the curve’s intrinsic parameter t.

Why: without reparameterization, the cam covers wildly unequal spatial distances per unit pitch input. On a square shape (high roundness), constant pitch would whip past the tapers near low / high and crawl along the flat sides — feels broken.

With reparameterization:

  1. Internally sample the raw curve at 64 t-points, building a cumulative-length table.
  2. For a requested pitch fraction s ∈ [0, 1], resolve the t such that cumulative arc length up to that t equals s · total length.
  3. Evaluate the raw curve at that t.

Result: constant pitch input → constant spatial speed of the cam along the orbit surface, for any roundness value.

Arc-length reparameterization is applied in Roundness mode only. Curve mode skips it.


Evaluation Entry Point

void CameraOrbitShape::EvaluateAtPitch(
    float  pitchRad,
    float& outRadius,
    float& outHeight) const;
  • pitchRad is clamped to [kPitchAtLow, kPitchAtHigh] = [-π/2, +π/2].
  • Returns (radius, height) — XY-plane distance from the cam target plus local-Z offset.

The DynamicOrbitBody consumer wraps the call:

shape->EvaluateAtPitch(m_targetPitch, radius, height);
desiredPos = pivot + Vector3(radius * cos(m_targetYaw),
                             radius * sin(m_targetYaw),
                             height);

Constants

static constexpr float kPitchAtLow  = -π/2;
static constexpr float kPitchAtMid  =  0.0;
static constexpr float kPitchAtHigh =  π/2;

Available for editor visualization, body pitch clamping, and adoption-time pitch clamping.


Authored Fields

FieldDefaultPurpose
m_lowBand(-2.0, 4.0)(height, radius) at pitch = −π/2.
m_midBand( 0.0, 5.0)(height, radius) at pitch = 0. The rest pose.
m_highBand( 3.0, 3.0)(height, radius) at pitch = +π/2.
m_shapingModeRoundnessRoundness or Curve.
m_roundness0.50..1. Only visible when m_shapingMode == Roundness. 0 = diamond, 0.5 = sphere, 1 = square.
m_curveLinearGS_Core::CurveType enum. Only visible when m_shapingMode == Curve.
m_description""Free-form author description.

Creating an Orbit Profile

  1. Open the Asset Editor in O3DE.
  2. Select New and choose CameraOrbitShape from the asset type list.
  3. Set the three bands:
    • Low Band(height, radius) for the cam-below-pivot extreme.
    • Mid Band(height, radius) for the cam’s level / rest pose.
    • High Band(height, radius) for the cam-above-pivot extreme.
  4. Choose a Shaping Mode:
    • Roundness (default) — set the slider between 0 (diamond), 0.5 (sphere), 1 (square).
    • Curve (advanced) — pick a CurveType enum value for overshoot / bounce effects. Note that curve mode skips arc-length reparameterization.
  5. Set the Description to record the preset’s intent.
  6. Save the asset.
  7. Assign the asset to a DynamicOrbitBody stage’s Orbit Shape slot.

See Also

Consumer:

Math primitive:

Related assets:


Get GS_PhantomCam

GS_PhantomCam — Explore this gem on the product page and add it to your project.