Orbital Solver
Categories:
The OrbitalSolver namespace (GS_Core::Math) provides a stateless, deterministic, pure position / pose blend primitive. Given two positions (or transforms) and a pivot point, it blends them by a fraction t along an arc that preserves the geometric relationship to the pivot — rather than the straight-line lerp that cuts through the pivot when the start and end sit on opposite sides.
The primitive originated as a fix for PhantomCam cross-cam blends (two orbit cams at ~180° around a shared target produced visible “cuts through the target” mid-blend). It has since been reframed as the standard movement primitive for any motion naturally arranged around a pivot — orbit cams, leading-follow band response, post-config stage transitions. Linear lerp becomes the special case used only when no meaningful pivot exists.
Used by: gs_phantomcam (cross-cam blend execution, DynamicOrbitBody damping, LeadingFollowBody band response).
For usage guides and PhantomCam-specific consumption patterns, see the PhantomCam consumer pages linked under Consumers.
Contents
- BlendShape
- PivotSource
- BlendPositionAroundPivot
- BlendPoseAroundPivot
- ResolveCrossCamPivot
- Reflection
- Per-Tick Damping Pattern
- Consumers
- See Also
BlendShape
The path shape. The same enum is reflected for authoring on consumers like Blend Profiles and DynamicOrbitBody.
enum class BlendShape : AZ::u8 {
Linear,
Cylindrical,
Spherical,
};
| Shape | Behavior | Right for |
|---|---|---|
Linear | Straight-line lerp in world space. Used as the automatic fallback when the pivot or either input is degenerate. | Cams with no meaningful pivot. Short displacements where arcing would be over-engineered. |
Cylindrical | Interpolate in a plane perpendicular to cylinderAxis (default world +Z). Radius and angle lerp in the plane; height along the axis lerps linearly. | Ground cams sweeping horizontally without dipping / rising mid-sweep. PhantomCam LeadingFollowBody band response default. |
Spherical | Full 3D arc on the surface of a sphere centered on the pivot. Radius lerps along the arc. | Cams that change height during the motion (over-shoulder ↔ overhead). Free pitch-aware orbits. PhantomCam DynamicOrbitBody default. |
PivotSource
For two-cam blends, which cam’s pivot drives the arc. Single-pivot consumers don’t need this — they pass the one pivot they already have. The enum exists for cross-cam blend authoring on PhantomBlend.
enum class PivotSource : AZ::u8 {
Source,
Destination,
Shared,
};
| Value | Behavior |
|---|---|
Source | Sweep around the outgoing cam’s pivot. “Leave cam A elegantly, then settle into wherever cam B is.” |
Destination | Sweep around the incoming cam’s pivot. “Arrive at cam B elegantly, regardless of where cam A was.” |
Shared | Midpoint of both pivots. Collapses identically to either single-pivot mode when the pivots coincide. Recommended default — handles same-target and split-target cases gracefully. |
BlendPositionAroundPivot
Position-only blend.
AZ::Vector3 BlendPositionAroundPivot(
const AZ::Vector3& a,
const AZ::Vector3& b,
const AZ::Vector3& pivot,
float t,
BlendShape shape,
const AZ::Vector3& cylinderAxis = AZ::Vector3::CreateAxisZ());
Blend position a toward b around pivot by fraction t along the chosen shape.
Parameters
| Parameter | Purpose |
|---|---|
a | Start position. |
b | End position. |
pivot | World-space pivot point. The geometric center of the arc. |
t | Blend fraction. NOT pre-clamped — values outside [0, 1] extrapolate. |
shape | Linear / Cylindrical / Spherical. |
cylinderAxis | Up axis for cylindrical mode. Default world +Z. Ignored by Linear and Spherical. |
Degenerate cases (automatic Linear fallback)
The function falls back to linear lerp when:
- Either input position is at or very-near the pivot (degenerate radius makes arc direction undefined).
shape == Linearexplicitly.
Extrapolation
Occasionally useful — e.g. t = -0.1 to anti-extrapolate slightly past the source for an overshoot effect.
Cylindrical math sketch
height_a = (a - pivot) ⋅ cylinderAxis
height_b = (b - pivot) ⋅ cylinderAxis
plane_a = a - pivot - height_a * cylinderAxis
plane_b = b - pivot - height_b * cylinderAxis
radius_a = plane_a.length
radius_b = plane_b.length
angle_a = atan2(plane_a.y, plane_a.x)
angle_b = atan2(plane_b.y, plane_b.x)
shortestAngle_b = ShortestArc(angle_a, angle_b)
radius_t = lerp(radius_a, radius_b, t)
angle_t = lerp(angle_a, shortestAngle_b, t)
height_t = lerp(height_a, height_b, t)
result = pivot + height_t * cylinderAxis +
radius_t * (cos(angle_t) * basisX + sin(angle_t) * basisY)
Spherical math sketch
dir_a = (a - pivot).normalized
dir_b = (b - pivot).normalized
radius_a = (a - pivot).length
radius_b = (b - pivot).length
// Slerp the direction on the unit sphere.
angle = acos(clamp(dir_a · dir_b, -1, 1))
if angle ≈ 0: dir_t = dir_a
else:
dir_t = (dir_a * sin((1 - t) * angle) + dir_b * sin(t * angle)) / sin(angle)
radius_t = lerp(radius_a, radius_b, t)
result = pivot + dir_t * radius_t
BlendPoseAroundPivot
Pose-level blend. Position uses BlendPositionAroundPivot. Rotation defaults to “look at pivot from the blended position.”
AZ::Transform BlendPoseAroundPivot(
const AZ::Transform& a,
const AZ::Transform& b,
const AZ::Vector3& pivot,
float t,
BlendShape shape,
const AZ::Vector3& cylinderAxis = AZ::Vector3::CreateAxisZ(),
bool slerpRotations = false);
Rotation modes
slerpRotations | Rotation behavior |
|---|---|
false (default) | Fresh “look at pivot from the blended position” quaternion. The most-often-correct cam semantic — orientation always faces the pivot regardless of either input cam’s authored aim quirks. Uses +Y forward / +Z up (matches AZ::Transform::CreateLookAt). |
true | Slerp a.GetRotation() → b.GetRotation() directly. For UI / VFX consumers that want raw input-rotation interpolation. |
ResolveCrossCamPivot
For consumers that need to pick between two cams’ pivots based on a PivotSource enum.
struct ResolvedPivot {
AZ::Vector3 pivot;
bool valid; // false → no meaningful pivot; use Linear
};
ResolvedPivot ResolveCrossCamPivot(
bool sourceHasPivot,
const AZ::Vector3& sourcePivot,
bool destHasPivot,
const AZ::Vector3& destPivot,
PivotSource source);
Resolution table
source | Both have pivots | Only source has | Only dest has | Neither |
|---|---|---|---|---|
Source | sourcePivot | sourcePivot | destPivot (fallback) | invalid |
Destination | destPivot | sourcePivot (fallback) | destPivot | invalid |
Shared | midpoint(s, d) | sourcePivot | destPivot | invalid |
valid = false signals the consumer should fall back to BlendShape::Linear.
Reflection
void ReflectOrbitalSolverEnums(AZ::ReflectContext* context);
Reflects BlendShape and PivotSource into the SerializeContext so authoring sites (PhantomBlend, body stages) can expose them as ComboBox EnumAttributes.
Guarded internally — safe to call multiple times from independent reflect chains. PhantomBlend and DynamicOrbitBody both call it at the top of their own Reflect.
Per-Tick Damping Pattern
The primitive doubles as a per-tick damping operator. Spring damping toward a pivot-arranged destination is exactly this primitive:
m_idealPosition = GS_Core::Math::BlendPositionAroundPivot(
m_idealPosition,
desiredPos,
pivot,
HalflifeAlpha(halflife, deltaTime),
shape);
No separate spring needed. HalflifeAlpha comes from the standard halflife → per-frame alpha conversion 1 − exp2(-deltaTime / halflife).
This is how DynamicOrbitBody damps the cam’s position toward whatever its target angles resolve to on the orbit surface — the solver call IS the smoothing. Authors tune one halflife and get spring-like response shaped around the pivot.
Consumers
PhantomCam consumers
| Consumer | Use |
|---|---|
| Cam Core — cross-cam blend execution | BlendPositionAroundPivot per-tick during cross-cam blends. Pivot resolved by the matched blend entry’s PivotSource choice (Source / Destination / Shared). |
| Blend Profiles — authoring | Each PhantomBlend entry authors BlendShape and PivotSource. |
| DynamicOrbitBody — per-tick damping | Solver IS the damping primitive — yaw / pitch target produces desiredPos on the orbit surface; solver damps m_idealPosition toward it. |
| LeadingFollowBody — band response | Cylindrical default. Cam arcs around the target during band response rather than cutting through. |
Non-PhantomCam consumers
The same primitive serves any system that needs pivot-aware blending — UI elements orbiting a focal point, world-space VFX paths, unit motion patterns. The primitive is namespaced under GS_Core::Math precisely so other gems can adopt it.
See Also
Related GS_Core utilities:
- Springs — spring-damper primitives. Use Springs for non-pivot smoothing; use Orbital Solver when motion naturally arranges around a pivot.
- Curves —
CurveTypeenum used for blend easing curves. - Angles Helper — yaw wrap / shortest-arc helpers used by orbit body stages.
PhantomCam consumer pages:
Get GS_Core
Explore other GS_Play gems on the product page.