Cameras¶
The camera decides how the shot is framed and whether it moves — where it sits, what it looks at, how close it is, and from what angle. You set it with one line; agentwood works out the actual position from the subject and the shot length when the block closes.
shot.camera(name, **kwargs) — resolved at shot exit against the final actor positions and shot
length. There are nine camera types in three families.
Universal kwargs¶
| kwarg | Meaning |
|---|---|
mode |
framing: closeup / medium / wide |
R |
distance in subject radii (× actor_scale) |
theta |
horizontal orbit, degrees (0 = front, 90 = profile) |
phi |
vertical angle, degrees (− = low/looking up, + = high) |
subject |
None → the actor or group midpoint; "group" → midpoint; a Character → that actor |
Parameter reference & tuning¶
Every shot.camera() dial — what it physically changes and how to push it toward a deliberate
frame. Defaults shown are the DSL defaults dispatched by shot.camera() (they differ from the
raw dsl/camera.py function defaults).
Framing & position (subject-relative cameras)¶
| Dial | Type / unit | Default | What it does |
|---|---|---|---|
subject |
None · "group" · Character |
None |
What the lens frames & aims at. None = the lone actor, or the group midpoint if >1; "group" = always the midpoint; a Character = lock onto that one actor (e.g. push on one speaker in a two-hander). |
mode |
closeup·medium·wide |
medium |
Sets only the look-at height = factor × subject height (≈ 0.81 / 0.77 / 0.60) — where on the body the camera aims (face → full body). It does not set distance. |
R |
distance × actor_scale |
2.5 |
Camera distance from the subject. Smaller = tighter/intimate; larger = wider. ×actor_scale, so the same value ports across sets. |
theta |
degrees | 0 |
Horizontal orbit, added to the actor's facing. 0 = front (their face); ±20–35 = ¾; 90 = profile; 180 = behind. |
phi |
degrees | 0 |
Vertical angle. 0 = eye level; negative = low angle (looks up → powerful/heroic); positive = high angle (looks down → small/vulnerable). |
mode is aim, R is distance — they're independent
A common surprise: mode="closeup" with the default R=2.5 is not a tight shot — it just
aims at the face from 2.5 away. For an actual close-up, lower R (≈ 0.8–1.5) (the legacy
close_up alias felt tight because it also set R=1.2). Think R = how close,
mode = how high it aims.
theta / phi rotate around the actor's facing, not the world
Both are added to the subject's yaw, so theta=20 is a ¾-front view whatever direction the
actor faces or wherever they stand — the composition stays consistent. Use this instead of
guessing world angles.
Working ranges: R ≈ 0.8–1.5 intimate · 2–3 conversation · 6–12 establishing ·
theta ≈ ±15–35 flattering ¾ · 90 profile · 160–200 behind/OTS ·
phi ≈ −10…−20 hero · +20…+40 diminish · ±5 just to break a flat dead-on frame.
Movement (per-frame cameras)¶
| Dial | Used by | Default | What it does |
|---|---|---|---|
R1 → R2 |
push, dolly_zoom |
4.0→2.0 / 5.0→2.0 |
Start/end distance of the move. R1>R2 = push in (subject grows); R1<R2 = pull out. Linear over the shot. |
arc |
arc |
90 |
Total orbit degrees — 90 or 180 only (#7). Sweeps −arc/2 → +arc/2 at distance R. |
phi_start → phi_end |
tilt |
−20 → 10 |
Camera position is fixed; the look-at sweeps vertically between these angles (reveal head-to-toe, or let the gaze fall). |
sample_every |
tracking_* |
6 |
Frames between samples of the actor's path. Lower (1–4) = smoother but slower; higher (8–10) = rougher/faster. |
Lens & world-space cameras¶
| Dial | Used by | Default | What it does |
|---|---|---|---|
f |
global_static, zoom, dolly_zoom |
24 |
Focal length (mm). Smaller = wider FOV & deeper space (18–35); larger = telephoto, compressed & flattering (50–135). In dolly_zoom it's auto-derived from R (f = 50·R/R1) — don't set it there. |
anchor |
global_static, tracking_static |
first camera point | A named camera point from set.json used as the fixed camera position (world-space). |
lookat |
global_static |
default subject | What the fixed world camera aims at — a Character / subject, or None for the default subject. Pair with anchor to fully hand-author an establishing composition. |
Putting these together into looks: see Production-Quality Tuning.
Static — framing & angles¶
| closeup | medium | wide |
|---|---|---|
![]() |
![]() |
![]() |
theta swings around the subject; phi raises/lowers the camera:
| low angle (φ−20) | high angle (φ30) |
|---|---|
![]() |
![]() |
Motion cameras¶
The camera generates one setup per frame, so the shot must last several frames.
name |
Move | Key kwargs |
|---|---|---|
push |
dolly straight in/out | R1, R2 |
arc |
orbit ±arc/2 | arc (⚠️ only 90 or 180) |
tilt |
look-at sweeps vertically | phi_start, phi_end |
rotating |
full 360° orbit | R, phi |
dolly_zoom |
"Vertigo" effect | R1, R2 |
| push — start (far) | push — end (close) |
|---|---|
![]() |
![]() |
See it in motion (fast proxy):
Tracking cameras¶
Follow a moving actor; the fixed variants read from the set's camera points.
name |
Behaviour |
|---|---|
tracking_moving |
camera travels with the actor at a fixed offset |
tracking_static |
fixed camera rotates to keep the actor framed |
global_static |
fixed world camera framing the area |
| tracking_moving — start | tracking_moving — end |
|---|---|
![]() |
![]() |
Legacy aliases
Old names still work: close_up, close_up_low_angle, medium_shot, medium_two_shot,
wide_establishing, tracking.
Not in the public API
pan, jitter_static, jitter_tracking, and zoom exist in dsl/camera.py but aren't wired
into shot.camera(). pan/jitter are exercised by the comparison scripts below; zoom
(a focal-length sweep) currently isn't called by anything.
Lower-level primitives & comparison galleries¶
Under the friendly shot.camera() API sit the raw camera-math functions in dsl/camera.py
(static_camera, push, arc_shot, pan, tracking_*, jitter_*). The repo ships
comparison galleries in tests/ that drive those functions directly and render
full-resolution PNG sweeps — the visual reference layer, and the only place pan/jitter
and the full angle matrix are exercised.
| Script | What it sweeps | Output |
|---|---|---|
render_camera_compare.py |
13 camera moves (static, push, arc, tilt, rotating, dolly-zoom, tracking, pan, jitter) | renders/camera_compare/ |
render_camera_compare2.py |
a 24-shot angle/lens matrix across 3 actor groupings | renders/camera_compare2/ |
render_camera_compare_fullscene.py |
the 13-shot gallery inside the set | renders/camera_compare_fullscene/ |
render_camera_compare2_fullscene.py |
the 24-shot matrix inside the set | renders/camera_compare2_fullscene/ |
Every camera primitive, with a sample frame¶
The functions in dsl/camera.py, each shown with one frame from render_camera_compare.py.
Click any row to expand the sample.
Static & global
static_camera(subject, mode, R, theta, phi) — the fixed workhorse shot
A stationary camera. mode sets framing (closeup / medium / wide), theta orbits, phi tilts.
global_camera(anchor, lookat, mode, f) — fixed world-space camera
Pinned at one of the set's camera-points, framing the area at focal length f.
Motion (one camera setup per frame)
push(R1, R2) — dolly straight in / out
Travels from distance R1 to R2; the subject grows (push-in) or shrinks (push-out).
Tracking & handheld
In the real set — the _fullscene galleries run the same shots inside canal_bridge, with real
geometry and actor_scale:
Two layers
The functions above are the low-level math layer; shot.camera() is the public layer
most scenes use. Full shot-by-shot catalogue (13- and 24-shot galleries, bare and in-set):
Testing → Existing tests.


















