Skip to content

kids.kapish.cameras

unity

Camera system with multi-layer stacks, pluggable rigs, smooth transitions, and render-to-texture support.

Installation

Add to your Unity project's package manifest:

json
{
  "kids.kapish.cameras": "file:../../src/unity/kids.kapish.cameras"
}

Requires Unity 6000.0+.

Architecture

Director / Rig Pattern

The system uses a director/rig separation. CameraDirector is a singleton that holds a reference to the currently active CameraRig. Only one rig is active at a time. Switching rigs goes through the director, which handles deactivation of the old rig, activation of the new rig, and optional blended transitions between them.

Rigs are CarrotBehaviour subclasses placed in the scene. They do nothing when inactive -- CameraDirector.SetRig() calls OnActivated() / OnDeactivated() to toggle enabled. All rig update logic runs in LateUpdate via the abstract UpdateRig() method, ensuring it runs after physics and gameplay logic.

Transition System

Transitions blend position, rotation, and projection between the old and new rig over a configurable duration. Three modes:

ModeBehaviour
CutInstant switch, no blending
LerpLinear interpolation over DurationMs
EaseEased interpolation using Carrot's tween system (EaseKind + EaseMode)

The director captures the start state (position, rotation, projection) from the outgoing rig at transition start. Each frame it evaluates t, applies the ease curve if applicable, and blends the active rig's transform and projection toward the target. The new rig's UpdateRig() runs throughout the transition so the destination keeps moving naturally (e.g. a follow rig tracking a moving target).

SetRigAsync returns an Awaitable that completes when the transition finishes, with CancellationToken support.

Camera Stack

Each rig owns a CameraStack -- an ordered list of CameraLayer definitions. On rebuild, the stack creates child GameObjects with Camera components for each layer. Layers are configured independently (depth, culling mask, clear flags, render target, audio listener).

The stack manages FOV separately from the rig's projection. Additive FOV modifiers are keyed by an object owner, allowing multiple systems (e.g. sprint FOV kick, ADS zoom) to contribute independently. The effective FOV is baseFov + sum(additives), smoothed with exponential damping.

CameraRig
├── CameraStack
│   ├── CameraLayer "World"    → Camera (depth 0, skybox, all layers)
│   ├── CameraLayer "UI"       → Camera (depth 1, depth-only, UI layer)
│   └── CameraLayer "Minimap"  → Camera (depth 0, solid, renders to RT)
├── CameraProjection
└── CameraEffectsSlot

If a rig has no layers on Start(), the stack auto-creates a default "World" layer with depth 0, skybox clear, and all culling masks.

Projection Blending

CameraProjection is a value type with a static Lerp method. During transitions, the director blends FOV, orthographic size, and clip planes between the start and end projections. The projection Kind (perspective vs orthographic) snaps at t = 0.5 since there's no meaningful interpolation between projection modes.

Exponential Smoothing

All rigs use 1 - exp(-speed * deltaTime) for framerate-independent damping instead of Mathf.Lerp with raw deltaTime. This produces consistent visual smoothing regardless of framerate and avoids the common pitfall of Lerp(current, target, dt) behaving differently at 30fps vs 144fps.

Pipeline Agnosticism

CameraEffectsSlot holds a ProfileName and Enabled flag but does not reference any pipeline-specific types. The VolumeComponent property is typed as Component -- pipeline integration packages populate it at runtime with URP Volumes, HDRP Volumes, or BiRP post-process layers as appropriate.

Collision Avoidance

FollowRig and OrbitRig use Physics.SphereCast from the target toward the desired camera position. If the cast hits geometry, the camera is pulled forward to the hit distance. This prevents clipping through walls in third-person and orbit cameras. Collision is configurable: enable/disable, layer mask, and sphere radius.

File Structure

Runtime/
├── CameraDirector.cs         # Singleton rig manager + transition driver
├── CameraRig.cs              # Abstract rig base
├── CameraStack.cs            # Multi-layer camera management
├── CameraLayer.cs            # Single camera configuration
├── CameraProjection.cs       # Perspective/orthographic projection struct
├── CameraTransition.cs       # Transition mode/duration/ease config
├── CameraEffectsSlot.cs      # Pipeline-agnostic effects holder
├── Carrot.Cameras.asmdef
└── Rigs/
    ├── FollowRig.cs           # Third-person follow
    ├── FixedRig.cs            # Static with optional look target
    ├── IsometricRig.cs        # Orthographic iso/top-down
    └── OrbitRig.cs            # Orbit with zoom and collision

Editor/
└── Carrot.Cameras.Editor.asmdef

Dependencies

DependencyKind
kids.kapishruntime (Carrot core -- MonoSingleton, CarrotBehaviour, EnsureComponent, tween system)

Build

Import via Unity Package Manager. Requires Unity 6000.0+.


Usage Guide

Camera system with multi-layer stacks, pluggable rigs, smooth transitions, and render-to-texture support.

Setting Up a Camera Rig

Basic rig with the director

csharp
using Carrot.Cameras;
using Carrot.Cameras.Rigs;
using UnityEngine;

public class GameCameraSetup : MonoBehaviour
{
    [SerializeField] private FollowRig followRig;

    private void Start()
    {
        CameraDirector.Instance.SetRig(followRig);
    }
}

Place a CameraDirector singleton in the scene (or let it auto-create). Add rig components to GameObjects. Call SetRig to activate a rig -- the director handles everything else.

Follow rig (third-person)

csharp
using Carrot.Cameras.Rigs;
using UnityEngine;

// Attach FollowRig to a GameObject in the scene
// Configure in the inspector or via code:
FollowRig rig = GetComponent<FollowRig>();
rig.Target = playerTransform;
rig.Offset = new Vector3(0f, 2f, -5f);

The rig smoothly follows the target with exponential damping. Collision avoidance is on by default -- the camera pulls forward when geometry is between it and the target.

Orbit rig (mouse/gamepad orbit)

csharp
using Carrot.Cameras.Rigs;
using UnityEngine;

OrbitRig orbit = GetComponent<OrbitRig>();
orbit.Target = focusTransform;

// Feed input each frame from your input system
void Update()
{
    float h = Input.GetAxis("Mouse X");
    float v = Input.GetAxis("Mouse Y");
    float scroll = Input.GetAxis("Mouse ScrollWheel");

    orbit.RotateInput(h, v);
    orbit.ZoomInput(scroll);
}

The rig handles pitch clamping, distance limits, and collision avoidance internally.

Isometric rig (top-down / iso)

csharp
using Carrot.Cameras.Rigs;

IsometricRig iso = GetComponent<IsometricRig>();
iso.Target = playerTransform;
iso.Angle = 45f;       // Camera pitch (10-89)
iso.Rotation = 45f;    // Compass rotation (0-360)
iso.Zoom = 10f;        // Orthographic size

Projection is automatically set to orthographic. Zoom is smoothed and clamped to a configurable range.

Fixed rig (static / cutscene camera)

csharp
using Carrot.Cameras.Rigs;

FixedRig fixedCam = GetComponent<FixedRig>();
fixedCam.LookTarget = actorTransform; // Optional -- smoothly tracks target

Position the rig's Transform wherever you want the camera. If LookTarget is null, the camera holds its current rotation.

Transitions

Instant cut

csharp
using Carrot.Cameras;

CameraDirector.Instance.SetRig(newRig);
// Or explicitly:
CameraDirector.Instance.SetRig(newRig, CameraTransition.Cut());

Smooth eased transition

csharp
using Carrot.Cameras;

var transition = CameraTransition.Smooth(750f); // 750ms, cubic ease in-out
CameraDirector.Instance.SetRig(newRig, transition);

Await transition completion

csharp
using Carrot.Cameras;
using System.Threading;

async Awaitable PlayCutsceneAsync(CancellationToken ct)
{
    await CameraDirector.Instance.SetRigAsync(cutsceneRig, CameraTransition.Smooth(1000f), ct);
    // Transition complete -- start dialogue, etc.
}

Custom transition

csharp
using Carrot.Cameras;
using Carrot.Tween.Easing;

var transition = new CameraTransition
{
    Mode = CameraTransitionMode.Ease,
    DurationMs = 1200f,
    Ease = EaseKind.Elastic,
    EaseMode = EaseMode.Out,
};

CameraDirector.Instance.SetRig(newRig, transition);

Reacting to transitions

csharp
using Carrot.Cameras;

CameraDirector director = CameraDirector.Instance;

var unsub = director.RigChanged.Add(change =>
{
    Debug.Log($"Switched from {change.Previous?.name} to {change.Current.name}");
});
// unsub() later to unsubscribe

director.TransitionStarted.Add(() => Debug.Log("Transition started"));
director.TransitionCompleted.Add(() => Debug.Log("Transition finished"));

Add kids.kapish.signals to your asmdef references if using these signals from your own code.

Camera Stack

Multi-layer setup

csharp
using Carrot.Cameras;
using UnityEngine;

CameraStack stack = rig.Stack;

// Default "World" layer is auto-created if the stack is empty.
// Add more layers for UI, minimap, etc.
stack.AddLayer("UI", depth: 1f, CameraClearFlags.Depth, LayerMask.GetMask("UI"));

Each layer gets its own Camera component on a child GameObject. Layers render in depth order.

Render to texture

csharp
using Carrot.Cameras;
using UnityEngine;

RenderTexture minimapRT = new RenderTexture(512, 512, 16);

CameraStack stack = rig.Stack;
stack.AddLayer("Minimap", depth: 0f, CameraClearFlags.SolidColor, ~0);
stack.SetRenderTarget("Minimap", minimapRT);

// Later:
stack.ClearRenderTarget("Minimap");

Additive FOV

csharp
using Carrot.Cameras;

CameraStack stack = CameraDirector.Instance.ActiveStack;

// Sprint FOV kick
stack.SetAdditiveFov(this, 10f); // +10 degrees while sprinting

// Remove when sprint ends
stack.ClearAdditiveFov(this);

Multiple systems can contribute FOV additives simultaneously. The stack sums them and smooths the result with exponential damping.

Accessing cameras

csharp
using Carrot.Cameras;
using UnityEngine;

CameraStack stack = rig.Stack;

Camera worldCam = stack.PrimaryCamera;         // First layer's camera
Camera uiCam = stack.GetCamera("UI");          // By name
Camera cam = stack.GetCamera(0);               // By index
CameraLayer layer = stack.GetLayer("World");   // Full layer config

Projection

Switching projection at runtime

csharp
using Carrot.Cameras;

// Switch to orthographic
rig.Projection = CameraProjection.Orthographic(5f);

// Switch to perspective
rig.Projection = CameraProjection.Perspective(60f);

// Isometric preset (orthographic with tighter far clip)
rig.Projection = CameraProjection.Isometric(10f);

Setting Projection immediately applies it to the rig's primary camera.

Post-Processing Effects

csharp
using Carrot.Cameras;
using UnityEngine;

CameraEffectsSlot effects = rig.Effects;
effects.ProfileName = "Gameplay";
effects.Enabled = true;

// Pipeline-specific code sets the volume at runtime:
// effects.VolumeComponent = urpVolume;

The effects slot is pipeline-agnostic. Your URP/HDRP integration code is responsible for mapping ProfileName to an actual volume component.

Tips

  • One director, many rigs -- place all your rigs in the scene and switch between them via SetRig. Only the active rig updates.
  • Exponential smoothing -- all rig speeds are in "units per second" fed to 1 - exp(-speed * dt). Higher values = snappier response. A value of 10 settles in roughly 0.3s.
  • Collision avoidance -- enabled by default on FollowRig and OrbitRig. Set collisionMask to exclude triggers, characters, or other layers you don't want the camera to collide with.
  • Default stack -- if a rig has no layers configured, it auto-creates a "World" layer on Start(). You only need to configure the stack if you want multiple layers.
  • Awaitable transitions -- SetRigAsync respects CancellationToken. Cancel to abort the await (the transition still completes visually).

Carrot