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:

| Slot | Type | Cardinality | Examples |
|---|
m_bodySlot | IBodyStage | One per cam (single-element vector by policy) | DefaultFollowBody, OrbitBody, DynamicOrbitBody, LeadingFollowBody, TrackBody. |
m_aimSlot | IAimStage | One per cam (single-element vector by policy) | DefaultAim, ClampedLookAim. Other gems (notably gs_performer) register additional variants. |
m_additives | IAdditiveStage | Zero or more, each self-declares Reposition or Noise phase | NoiseStage (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.
};
| Field | Visibility | Purpose |
|---|
m_channelScope | Always | The scope mode. |
m_boundChannelId | When m_channelScope == TrueUnique | Explicit channel binding for direct mode. |
m_allChannelsShare | When m_channelScope == TrueUnique AND m_showAdvanced | Shared mode toggle — appears in every active channel’s priority table. |
m_showAdvanced | When m_channelScope == TrueUnique | Reveals 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 level —
AllChannels (works naturally for in-rig cams; out-of-rig duplication is deferred). - Hero-perspective cam for a specific player —
TrueUnique + m_boundChannelId = N. - Shared cinematic collapse cam —
TrueUnique + 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
| Flag | Origin | Meaning |
|---|
m_hasFocus | Runtime — set by SettingNewCam / SettingNewCamOnChannel notifications. | True while this cam is the channel’s currently-driven cam. |
m_alwaysUpdate | Authored 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_blendingOut | Runtime — 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 gain —
m_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
| Method | Timing |
|---|
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. |
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:
| Snapshot | Where it lands | Use it for… |
|---|
m_desiredPose | Post Body + Aim, pre-Reposition. “Clean” ideal — no collision correction, no shake. | Debug visualization of the cam’s ideal trajectory. |
m_stablePose | Post Reposition, pre-Noise. Collision-safe, shake-free. | Gameplay readback — Unit movement input driven by camera facing should use this. |
m_finalPose | Post 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
- Create an entity and add GS_PhantomCameraComponent.
- Set the Priority value. Higher values take precedence within the channel.
- Configure FOV, near clip, and far clip on the lens fields.
- Assign a Cam Target entity (the follow target). Leave empty to inherit the channel target from the Cam Manager.
- From the Body type-picker, slot a body stage. Configure its target mode, offset, and damping halflife.
- From the Aim type-picker, slot an aim stage. Configure its target mode, offset, and damping halflife.
- Optionally add Additive stages — noise, impulse, tug listeners, collision reposition.
- Set the Channel Scope (Local is the default and almost always correct).
- 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.
| Field | Type | Description |
|---|
Priority | AZ::s32 | Base priority used in arbitration. |
FOV | float | Field of view in degrees. |
NearClip | float | Near clipping plane distance. |
FarClip | float | Far clipping plane distance. |
CamTarget | AZ::EntityId | The 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
| Method | Parameters | Returns | Description |
|---|
EnableCamera | — | void | Restores the cam’s effective priority and registers it for evaluation. |
DisableCamera | — | void | Drops the cam’s effective priority to 0. |
IsCamEnabled | — | bool | Query. |
Priority and target
| Method | Parameters | Returns | Description |
|---|
SetCameraPriority | AZ::s32 newPriority | void | Sets base priority. Triggers Cam Manager re-evaluation. |
GetCameraPriority | — | AZ::s32 | Query. |
SetCameraTarget | AZ::EntityId targetEntity | void | Sets the follow target. Triggers a snap on next tick when the entity is valid. Clearing (invalid id) does NOT snap — preserves depossess pose-hold. |
SetTargetFocusGroup | AZ::EntityId targetFocusGroup | void | Sets the target to a Group Target entity. Body / aim stages with CamTargetMode::GroupTarget route through this. |
GetCameraData | — | const PhantomCamData* | Returns const pointer to the cam’s lens / priority / target data. Consumed by the Cam Core. |
Snap
| Method | Parameters | Returns | Description |
|---|
QueueSnapCamera | — | void | Sets m_snapNextEval = true. Honored on next Evaluate. |
SnapCameraNow | — | void | Synchronous 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
| Method | Parameters | Returns | Description |
|---|
GetDesiredPose | — | AZ::Transform | Post Body + Aim, pre-Reposition. |
GetStablePose | — | AZ::Transform | Post Reposition, pre-Noise. Recommended default for gameplay readback. |
GetFinalPose | — | AZ::Transform | Committed pose (post-Noise). |
GetCameraPivot | AZ::Vector3& outPivot, bool& outHasPivot | void | Returns the cam’s cached pivot. Used by Cam Core to drive non-Linear blend shapes. |
Impulse
| Method | Parameters | Returns | Description |
|---|
TriggerCameraImpulse | float strength | void | Fires every ImpulseNoise additive on this cam. strength multiplies each stage’s AmplitudeGain — typically pass distance-falloff values in 0..1. |
Inheritance forwarders
| Method | Parameters | Returns | Description |
|---|
TryGetPoseSnapshot | CamPoseSnapshot& out | bool | Forwards 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. |
TryAdoptPoseSnapshot | const CamPoseSnapshot& in | bool | Forwards to the body stage’s virtual. The Cam Core invokes this on the incoming cam. |
Blend-out lifecycle
| Method | Parameters | Returns | Description |
|---|
SetBlendingOut | bool isBlendingOut | void | Cam 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 component | Replacement |
|---|
StaticOrbit_PhantomCamComponent | OrbitBody Body stage variant. See Stage Pipeline. |
ClampedLook_PhantomCamComponent | ClampedLookAim Aim stage variant. See Stage Pipeline. |
Track_PhantomCamComponent | TrackBody 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.
#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 Variants —
DefaultFollowBody, OrbitBody, DynamicOrbitBody, LeadingFollowBody, TrackBody. - Aim Stage Variants —
DefaultLookAtAim, ClampedLookAim. Plus extensions from gs_performer. - Additive Stage Variants —
CollisionReposition, 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:
| Stage | What it damps |
|---|
| Body | Pursuit of the follow target — halflife on m_idealPosition. |
| Aim | Slerp toward the look-at rotation — halflife on m_idealRotation. |
| Reposition additives | Their own correction delta — collision pushback, tug-source approach. |
| Noise additives | Operate 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:
| Helper | Purpose |
|---|
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:
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.

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
| Field | Default | Purpose |
|---|
m_targetMode | Transform | How 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_offsetIsRelative | false | World axes (false) or target-relative basis (true). |
m_halflife | 0.1 | Spring 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
| Field | Default | Purpose |
|---|
m_targetMode | Transform | Target routing. |
m_overrideEntity | — | When m_targetMode == Override. |
m_groupTargetName | "" | When m_targetMode == GroupTarget. |
m_offset | (0, 0, 0) | Pre-orbit offset. |
m_offsetIsRelative | false | Basis. |
m_orbitRadius | 5.0 | Distance from pivot to cam. |
m_orbitYawDeg | 45.0 | Horizontal angle around pivot. |
m_orbitPitchDeg | 20.0 | Vertical angle (clamped −89° to +89°). |
m_halflife | 0.1 | Spring halflife (0 = snap). |
State inheritance
| Direction | Behavior |
|---|
| Get | Publishes 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. |
| Adopt | Not 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:
- Snap seeding (
m_initialYawDeg / m_initialPitchDeg) on first activate or ctx.snapThisFrame. - External scripted calls via
DynamicOrbitBodyRequestBus::SetOrbit(yawRad, pitchRad). - 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
| Field | Default | Purpose |
|---|
m_targetMode | Transform | Target routing. |
m_overrideEntity | — | When m_targetMode == Override. |
m_groupTargetName | "" | When m_targetMode == GroupTarget. |
m_offset | (0, 0, 0) | Pivot offset from target. |
m_offsetIsRelative | false | Basis. |
m_orbitShape | (asset slot) | .camorbit asset defining the surface. See Orbit Profiles. |
m_yawSpeed | 90 | Degrees / sec per unit input on yaw. |
m_pitchSpeed | 60 | Degrees / sec per unit input on pitch. |
m_blendHalflife | 0.10 | Damping halflife — the solver call IS the smoothing. |
m_blendShape | Spherical | Solver shape (Spherical allows arbitrary surface; Cylindrical constrains height). |
m_initialYawDeg | 180.0 | Snap seed yaw. 180° = directly behind target. |
m_initialPitchDeg | 0.0 | Snap seed pitch. 0° = mid band height. |
m_debugPrint | false | Per-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:
| Path | Trigger | Behavior |
|---|
| 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 pivotPos | Back-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
| Field | Default | Purpose |
|---|
m_targetMode | Transform | Target routing. |
m_overrideEntity / m_groupTargetName | — | Mode-dependent. |
m_offset | (0, 0, 1.6) | Offset applied before band math. Cam rides at this Z height. |
m_offsetIsRelative | false | Basis. |
m_innerRadius | 2.0 | Min cam-target XY distance. |
m_outerRadius | 5.0 | Max cam-target XY distance. |
m_hardClampEnabled | false | Optional absolute outer cap. |
m_hardClampDistance | 8.0 | Hard cap (when enabled). |
m_radialHalflife | 0.25 | Arc-slide halflife when outside the envelope. |
m_heightHalflife | 0.50 | Independent Z follow halflife. |
m_blendShape | Cylindrical | Solver shape for band response — cam stays in working height plane. Downgradable to Linear for short displacements. |
m_centerOnHeading | false | Enable idle reposition. |
m_idleVelocityThreshold | 0.10 m/s | XY speed below which the idle timer accumulates. |
m_idleDelay | 1.50 s | Stillness duration before reposition engages. |
m_reorientHalflife | 0.60 | Idle reposition arc halflife. |
m_idleYawOffsetDeg | 0 | Offset from “directly behind” during reposition. ± shifts off-axis. |
State inheritance
| Direction | Behavior |
|---|
| Get | Returns m_idealPosition, world-forward toward cached target, pivotPos = m_lastTargetPoint. No angular state. |
| Adopt | Pure 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
| Field | Default | Purpose |
|---|
m_targetMode | Transform | Used to compute the closest spline point. Typically Transform. |
m_overrideEntity / m_groupTargetName | — | Mode-dependent. |
m_splineTrack | (entity slot) | Entity carrying a SplineComponent. |
m_startData | — | Endpoint A (TrackBodyData). |
m_endData | — | Endpoint B. |
m_adoptionPathThreshold | 5.0 m | Inheritance: 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
| Direction | Behavior |
|---|
| Get | Returns m_idealPosition + world-forward toward cached m_lastTargetPos. No angular state. No pivot (the path is the kinematic reference, not a point). |
| Adopt | Pure 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
| Variant | Degrees of Freedom | Damping | Pivot | Inheritance Get | Inheritance Adopt |
|---|
DefaultFollowBody | Position only | Spring | None | — | — |
OrbitBody | Authored yaw / pitch / radius | Spring | Post-offset target | Full angular | Disabled (Get-only) |
DynamicOrbitBody | Dynamic yaw / pitch | Solver | Post-offset target | Full angular | Full (ANGULAR / POSITION) |
LeadingFollowBody | XY band + Z follow | Solver (band-arc) | Post-offset target | worldPos + fwd | Facing-seeded standoff |
TrackBody | Spline-bound | Spring | None (path) | worldPos + fwd | Project + 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.

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
| Field | Default | Purpose |
|---|
m_targetMode | Transform | Look-at target routing. |
m_overrideEntity | — | When m_targetMode == Override. |
m_groupTargetName | "" | When m_targetMode == GroupTarget. |
m_offset | (0, 0, 0) | Offset applied to look-at point. |
m_offsetIsRelative | false | World axes (false) or target-relative basis (true). |
m_halflife | 0.1 | Slerp 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
| Mode | Behavior |
|---|
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
| Field | Default | Purpose |
|---|
m_targetMode | Transform | Look-at target routing. |
m_overrideEntity / m_groupTargetName | — | Mode-dependent. |
m_offset | (0, 0, 0) | Look-at offset. |
m_offsetIsRelative | false | Basis. |
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_localSpace | false | Clamp mode (world-space or local-space pivot). |
m_halflife | 0.1 | Slerp halflife. |
Runtime state
| Field | Purpose |
|---|
m_startingForward | Captured on first Evaluate. The “origin” relative to which the clamp envelope is anchored. |
m_hasStartingForward | True after first capture. |
m_idealRotation | Decoupled slerp working value. |
m_hasIdeal | True after first ideal write. |
m_lastValidRotation | Last 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.

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
| Field | Default | Purpose |
|---|
m_enabled | true | Master 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_innerRadius | 0.15 | Hard clamp distance. Serialized as "Radius" for back-compat. |
m_outerRadius | 0.45 | Soft buffer distance. Clamped at evaluate time to be ≥ inner. |
m_halflife | 0.15 | Soft 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.
| Field | Default | Purpose |
|---|
m_enabled | true | Master toggle. |
m_halflife | 0.15 | Correction 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
| Field | Default | Purpose |
|---|
m_enabled | true | Master toggle. |
m_channels | [] | String tags the listener matches against tug volume channels. Crc32-cached at activate. |
m_blendHalflife | 0.15 | Smoothing halflife for influence and source-point. |
m_strength | 1.0 | Per-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
| Step | TugBodyListener |
|---|
GetSeedSourcePoint | Returns state.position — first engaged tick produces zero displacement (the cam doesn’t yank). |
ApplyModulation | Derives 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
| Field | Default | Purpose |
|---|
m_enabled | true | Master toggle. |
m_profile | (asset slot) | .camnoiseprofile asset. |
m_amplitudeGain | 1.0 | Multiplier over profile amplitudes. |
m_frequencyGain | 1.0 | Multiplier 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
| Field | Default | Purpose |
|---|
m_enabled | true | Master toggle. |
m_profile | (asset slot) | .camnoiseprofile. |
m_envelope | (ADSR struct) | Attack / Decay / Sustain level / Sustain duration / Release. See GS_Core::Envelope::ADSREnvelope. |
m_amplitudeGain | 1.0 | Multiplier over profile amplitudes. |
m_frequencyGain | 1.0 | Multiplier 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
| Variant | Phase | Reads ctx natural pose? | Notes |
|---|
CollisionReposition | Reposition | — | PhysX sphere-cast pushback. Soft buffer + hard clamp. |
OcclusionReposition | Reposition | — | Stub. Damping scaffold present. |
TugAimListener | Reposition | Yes (cam-to-target forward) | Slerps rotation toward source. |
TugBodyListener | Reposition | Yes (target / bodyAnchor) | Lerps position toward source. |
PerlinNoise | Noise | — | Continuous Perlin sampled by accumulated time. |
ImpulseNoise | Noise | — | Event-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.

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 group | Units | Typical amplitude |
|---|
| Position layers | meters | 0.01 – 0.2 |
| Rotation layers | degrees | 0.1 – 8 |
| Frequency (any axis) | Hz | Handheld profiles 0.1 – 2 Hz; shake / impact 3 – 60 Hz |
Authored Fields
| Field | Default | Purpose |
|---|
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:
| Stage | Use | Time source |
|---|
PerlinNoise | Continuous handheld sway, drift | m_time += deltaTime, accumulated since activation |
ImpulseNoise | Event-triggered burst | m_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.
| Asset | Use for |
|---|
Normal_Mild.camnoiseprofile | Normal-lens handheld baseline — balanced position and rotation, moderate frequencies. |
Normal_Intense.camnoiseprofile | Aggressive normal-lens handheld — same shape, larger amplitudes. |
Telephoto_Mild.camnoiseprofile | Telephoto-lens handheld baseline — tighter angular response, low position amplitude. |
Telephoto_Intense.camnoiseprofile | Aggressive telephoto handheld — same shape, larger amplitudes. |
Wide_Mild.camnoiseprofile | Wide-lens handheld baseline — translation dominates; rotation low. |
Wide_Intense.camnoiseprofile | Aggressive 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.
| Asset | Use for |
|---|
6D_Shake.camnoiseprofile | Earthquake / impact aftermath — all six axes, high-frequency micro-jitter on top of mid-frequency sway. |
6D_Wobble.camnoiseprofile | Floaty / 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
- Open the Asset Editor in O3DE.
- Select New and choose CameraNoiseProfile from the asset type list.
- For each axis where you want noise, add layers via the + button. Three layers per axis is the typical authoring rhythm (slow / mid / fast).
- 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).
- Set the Description to record the preset’s intent or tuning history.
- Save the asset.
- 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.

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)
};
| Band | Default | Selected 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_roundness | n | Shape | Description |
|---|
0.0 | 1 | Diamond | Piecewise linear. Sharp corner at mid. Bulge envelope is 1 − |2t − 1| — a tent. |
0.5 | 2 | Sphere | Smooth quadratic through low / mid / high. Bulge envelope is 1 − (2t − 1)². Default. |
1.0 | ~6 (kMaxBulge) | Square / rounded-square | Flat 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:
- Internally sample the raw curve at 64 t-points, building a cumulative-length table.
- For a requested pitch fraction
s ∈ [0, 1], resolve the t such that cumulative arc length up to that t equals s · total length. - 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
| Field | Default | Purpose |
|---|
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_shapingMode | Roundness | Roundness or Curve. |
m_roundness | 0.5 | 0..1. Only visible when m_shapingMode == Roundness. 0 = diamond, 0.5 = sphere, 1 = square. |
m_curve | Linear | GS_Core::CurveType enum. Only visible when m_shapingMode == Curve. |
m_description | "" | Free-form author description. |
Creating an Orbit Profile
- Open the Asset Editor in O3DE.
- Select New and choose CameraOrbitShape from the asset type list.
- 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.
- 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.
- Set the Description to record the preset’s intent.
- Save the asset.
- 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.