Appearance
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:
| Mode | Behaviour |
|---|---|
Cut | Instant switch, no blending |
Lerp | Linear interpolation over DurationMs |
Ease | Eased 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
└── CameraEffectsSlotIf 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.asmdefDependencies
| Dependency | Kind |
|---|---|
kids.kapish | runtime (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 sizeProjection 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 targetPosition 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.signalsto 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 configProjection
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
FollowRigandOrbitRig. SetcollisionMaskto 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 --
SetRigAsyncrespectsCancellationToken. Cancel to abort the await (the transition still completes visually).