CameraKit
Drives the client camera from declarative shots, re-evaluated against live targets every frame, with blends from the real current pose and a render binding held only while it owns the camera.
- Realm
- Client
- Require
ReplicatedStorage.ChloeKernel .CameraKit - Since
- v0.7.0
- Source
- CameraKit.luau
CameraKit turns cutscene camera work into data. A shot is a table that says what the camera does right now: hold a
pose, follow a target, orbit it, or ride a spline. CameraKit evaluates the active shot against live targets every frame,
so a Follow shot tracks a moving boss and an Orbit keeps circling a target that walks away, with no per-frame code
on your side. Nothing about a shot is baked when you set it. CutsceneKit drives CameraKit from
timelines, and Impact layers its shake on top.
Mental model
Section titled “Mental model”The render binding
Section titled “The render binding”While the kit holds the camera it binds one render step with
RunService:BindToRenderStep(RenderName, Enum.RenderPriority.Camera.Value + 1, ...). That runs right after the player
camera script (which runs at Camera), so CameraKit’s write wins for the frame, and before Impact’s shake at
Camera + 2, so the shake lands on top of the shot. The binding is made on the first cut or blend and removed when
the kit releases the camera. A kit that is not holding the camera has no per-frame work.
Each kit instance binds under its own name, CKCamera_<n>, stored in the RenderName field, so one kit’s release never
unbinds another kit’s step. Names starting with CKCamera_ are reserved; do not bind your own steps under them.
SkipLoop = true never binds, and specs call step(dt) by hand.
Which camera
Section titled “Which camera”With no Camera option the kit follows workspace.CurrentCamera. On every capture and every step it checks whether the
current camera changed. If it did while the kit holds the camera, the kit takes over the new one: it records the new
camera’s state as the one to restore and sets it Scriptable. An injected Camera (specs) is used as given.
Static: holdsCFrame(default: the camera’s currentCFrame).Follow:Offset(defaultCFrame.new(0, 6, 14)) is applied in the target’s space to get a goal position. The camera’s position eases toward the goal withSmoothed:Lerp(Goal, 1 - math.exp(-Damping * dt)), whereDamping(default 8) is a rate per second, so the smoothing is the same at any frame rate. The camera looks at the target’s raw position.Orbit: the angle advances bySpeed * dtradians per frame (Speeddefault 0.5,Anglestart default 0). The eye sits attarget + Vector3.new(math.cos(Angle) * Distance, Height, math.sin(Angle) * Distance): a ring of radiusDistance(default 20) in the XZ plane at a flatHeight(default 5).Speed = 0holds a fixed angle.Rail: a Catmull-Rom spline throughPoints, which passes through every point. Progress isease(clamp(age / Duration, 0, 1), Ease)withDurationdefault 5 and theSmoothease by default, so the camera speeds up away from the first point and slows into the last. It holds the last point afterDuration. Only position is splined: facing lerps between the two bracketing points’ rotations, unlessLookAtis set, in which case the camera looks at that target every frame.FOVis optional on any shot. A shot without it leavesFieldOfViewas it is.
Camera.Focus
Section titled “Camera.Focus”A Scriptable camera gets no automatic Focus, and the engine uses Focus to decide where to spend lighting and level
of detail. Every step sets Camera.Focus to what the shot looks at: the target for Follow and Orbit, LookAt for
a Rail that has one, and a point 20 studs ahead of the pose for Static and a Rail without LookAt. The focus from
before capture is restored on release.
Targets are read live
Section titled “Targets are read live”Follow, Orbit and the rail’s LookAt resolve their target every frame:
| Target | Resolves to |
|---|---|
Vector3 |
That fixed point. |
BasePart |
.Position, read each frame. |
Model |
:GetPivot().Position, read each frame. |
() -> Vector3 |
The function’s result, called under pcall each frame. |
A BasePart or Model with no Parent, or a function that errors or returns something other than a Vector3, counts
as dead. A shot whose target is dead has no pose, and the step hands the camera back at once through the same finish
path as release, instead of freezing on the last frame.
Capture, blend, cut, release
Section titled “Capture, blend, cut, release”cut and blend both call capture(), which records the camera’s CameraType, CFrame, FieldOfView and Focus
once and sets CameraType to Scriptable. A second capture while holding does nothing, so a chain of shots never
overwrites the original pose.
cut(shot)starts the shot and steps once withdt = 0: the camera jumps to the shot’s pose this frame.blend(shot, seconds?, ease?)takes the camera’s currentCFrameandFieldOfViewas the start and eases toward the new shot’s live pose overseconds(default 1). The start is the camera’s real pose, so blending away from a shot that is still tracking a moving target never jumps.release(seconds?)blends to the recordedCFrameandFieldOfViewas aStaticshot overseconds, then restoresCameraType,FieldOfView,FocusandCFrameand unbinds the render step. Withsecondsomitted or<= 0it does all of that at once, so the camera lands exactly where capture found it rather than where the last shot pointed. When the restored type isCustom, the player camera re-solves from the character on the next frame anyway.
A cutscene that never releases leaves the camera Scriptable and the player unable to steer it. release, destroy
and the dead-target path all end in the same restore.
CameraKit.ease and CameraKit.rail are pure functions: plain values in, no kernel or camera, so tooling and specs
can test the curves and the spline directly.
A chase camera that blends into a rail reveal when the player reaches a gate, then blends back:
--!strict-- RevealController: the chase camera and the gate reveal.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CameraKit = require(ReplicatedStorage.ChloeKernel.CameraKit)
local RevealController = { Name = "RevealController",}
function RevealController.init(self: any, kernel: any) self.Kernel = kernel self.Camera = CameraKit.attach(kernel)end
function RevealController.chase(self: any, character: Model) self.Camera:cut({ Type = "Follow", Target = character, Offset = CFrame.new(0, 6, 14), -- behind and above, in the target's space Damping = 8, FOV = 70, })end
function RevealController.reveal(self: any, gate: Model) -- 1.5 s from wherever the camera is now into the rail self.Camera:blend({ Type = "Rail", Points = { CFrame.new(-40, 15, 0), CFrame.new(0, 20, -20), CFrame.new(40, 15, 0), }, Duration = 4, Ease = "Smooth", LookAt = gate, FOV = 50, }, 1.5) if self.ReleaseHandle then self.ReleaseHandle:cancel() end self.ReleaseHandle = self.Kernel.Scheduler:after(5.5, function() self.ReleaseHandle = nil self.Camera:release(1.2) -- ease back and restore the player camera end)end
function RevealController.stop(self: any) if self.ReleaseHandle then self.ReleaseHandle:cancel() end if self.Camera then self.Camera:destroy() endend
return RevealControllerAPI reference
Section titled “API reference”CameraKit.attach(kernel, options?) -> kit
Section titled “CameraKit.attach(kernel, options?) -> kit”| Option | Type | Default | Description |
|---|---|---|---|
Camera |
Camera? |
follows workspace.CurrentCamera |
A fixed camera. Specs inject a double. |
Clock |
(() -> number)? |
os.clock |
Time source for blends and rails. |
SkipLoop |
boolean? |
false |
Never bind the render step. Specs drive step(dt). |
Kit members
Section titled “Kit members”| Member | Description |
|---|---|
kit:cut(shot) |
Captures the camera (once) and snaps to the shot’s pose this frame. |
kit:blend(shot, seconds?, ease?) |
Captures the camera (once) and eases from its real current pose to the shot, evaluated live. seconds defaults to 1. |
kit:release(seconds?) |
Blends back to the captured pose, then restores CameraType, FieldOfView, Focus and CFrame and unbinds. Omitted or <= 0 restores at once. Does nothing if the camera was never captured. |
kit:capture() |
Takes Scriptable ownership and binds the render step. cut and blend call it. |
kit:current() -> Shot? |
The active shot, or nil. |
kit:step(dt) |
Evaluates the active shot and writes the camera. The render binding calls it. |
kit:destroy() |
Finishes at once: restores a held camera and unbinds. |
kit.RenderName |
This kit’s render step name, CKCamera_<n>. |
| Field | Type | Applies to | Default | Description |
|---|---|---|---|---|
Type |
string |
all | required | "Static", "Follow", "Orbit" or "Rail". |
CFrame |
CFrame? |
Static |
the camera’s current CFrame |
The held pose. |
Target |
Vector3, BasePart, Model or () -> Vector3 |
Follow, Orbit |
required | Resolved every frame. |
Offset |
CFrame? |
Follow |
CFrame.new(0, 6, 14) |
Goal offset in the target’s space. |
Damping |
number? |
Follow |
8 |
Smoothing rate per second toward the goal. |
Distance |
number? |
Orbit |
20 |
Ring radius in the XZ plane. |
Height |
number? |
Orbit |
5 |
Flat Y offset above the target. |
Speed |
number? |
Orbit |
0.5 |
Radians per second. 0 holds Angle. |
Angle |
number? |
Orbit |
0 |
Starting angle in radians. |
Points |
{ CFrame }? |
Rail |
required | Catmull-Rom control points. |
Duration |
number? |
Rail |
5 |
Seconds to cover the rail. Holds the last point after it. |
Ease |
string? |
Rail |
"Smooth" |
"Linear", "Smooth" (smoothstep), "In" (quadratic) or "Out" (quadratic). blend takes its own ease argument. |
LookAt |
same as Target |
Rail |
nil |
Overrides the rail’s facing. Resolved every frame. |
FOV |
number? |
all | unchanged | Field of view for this shot. |
Pure functions
Section titled “Pure functions”| Member | Description |
|---|---|
CameraKit.ease(alpha, style?) -> number |
Clamps alpha to 0-1 and applies the named curve. |
CameraKit.rail(points, alpha) -> CFrame |
The Catmull-Rom pose at alpha (0 is the first point, 1 the last). One point returns that point. |
Hooks and Bus topics
Section titled “Hooks and Bus topics”CameraKit publishes no Bus topics and defines no hooks. CutsceneKit wraps cut, blend and
release in timeline events and publishes the scene lifecycle.