Camera (the follow camera)¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). Runtime claims were made in
PCSX2 2.9.94 (2026-10-04) by reading the camera object over PINE in level99, checkpoint 1, and say so; those of
In the street (2026-10-05) in the street saves of level99's world, read every update while a scripted
pad played (Feel comparison); those of 2026-10-06 from the level99 checkpoint 1 state files' memory and a
copy of slot 6 driven over PINE (L1 held, no stick).
Purpose¶
The camera behind the player in normal play: how a level script creates it, what it is configured with, how it is
made current, and what its update does as far as it has been read. It is what the first playable milestone needs to
show the player walking. The lens (field of view, clip planes, view window) is on
The streamed world. The other camera kinds (Types) are covered from
Death camera on, and every function of Camera/ is in the function map.
In one paragraph: there is one Cam_Follow object per player, a singleton made on first use. level99.lua creates
the follow camera with the global.lua helper CameraCreateFollow("follow", player), which sets it up on the player
(CamSetupFollow) and configures it (CfgFollowCamera): distance 3 to 6.6 m (4.8 by default), a pitch of 13°, a 65°
field of view, a near plane of 0.1 and a look-at point 1.4 m above the player's feet. CameraMakeActive makes it
current with no blend. It is a leash camera: each update it keeps its look-at point on the player, is dragged back into
a band 0.5 m deep (3.0-3.5 m after CfgFollowCamera, which starts it at the minimum; 4.8-5.3 m from checkpoint 2, where
CamSetFollowZoom(1) moves it to the default, Script calls) when the player moves away, covers 22% of
its wanted move per update, swings round toward the player's facing once the angle passes 22.5° (seen in the street, not
at checkpoint 1, Runtime checks), holds a 13° pitch, pulls in to 3.0-3.5 m and lowers its pitch to 7°
while the player sprints, turns with the right stick at 60-150°/s, and swings or pulls in when the world is in the way,
using ray casts and sphere pushes against the collision mesh. While the player holds a lock-on in a fight it pulls in to
2.4-2.9 m and frames the enemy 27° off centre (Combat camera). The tutorial's cut-aways are locked
cameras reached and left with 1 s blends (Blends).
Original structure¶
Camera/ (0x0011b770-0x00143ea0, Source map): the script helpers, then the camera manager and
each camera class in turn, in the order of their vtables (0x00535250-0x005378d0, 0x2c0 bytes apart, 8-byte
{delta, function} slots). Function names are ours unless a class string gives them; every address below has the
same name and a plate comment in the Ghidra project. "Vtable +0xNN" is the slot whose function word is at that
offset (the call reads its delta at +0xNN − 4). Unless a row says otherwise its evidence is the code at that address.
Outside Camera/, the functions this page relies on:
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00367580 → 0x00353818 |
ScenePlayCinematic (binding) |
starts a scene; stores BlendCam at the scene's +0x94 |
confirmed (code) |
0x0039d870 / 0x0039f450 |
scene start / scene end | take the camera over, give it back | confirmed (code) |
0x001562c8 |
cameras to the device | the lens and draw distance each frame (The streamed world) | confirmed (code) |
0x00233c50 |
combat camera test | Combat camera | confirmed (code) |
0x0041ab30 / 0x0041ab60 |
slow motion on / off | the characters' step 0x005102cc |
confirmed (code) |
Script helpers¶
The functions the camera bindings call (camera bindings), 0x0011b770-0x0011e1b0.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0011b770 |
Camera_MakeActiveByHandle |
CameraMakeActive binding helper: resolves the camera handle (must be a camera object), reads the player index (+0x1b0) of up to two optional humans and calls Camera_MakeActive (0x0011ee08) with the blend time. See blends. |
confirmed (code) |
0x0011b838 |
Camera_GetActiveHandle |
CameraGetActive: handle (vtable +0x2c) of the player's current camera, or the null handle 0x006ebd30. |
confirmed (code) |
0x0011b888 |
Camera_DeleteScripted |
CamDelete: releases (Camera_Release 0x0011e440) a camera by handle, but only a fixed (type 0) or locked (type 1) camera; shared kinds are kept. |
confirmed (code) |
0x0011b920 |
Camera_GetPositionByHandle |
CamGetPos: a camera's position (vtable +0x21c) by handle, or the zero vector 0x005116c0. |
confirmed (code) |
0x0011b970 |
Camera_TransformPoint |
CameraTransformPoint: transforms a point (w=1) by the camera's matrix (vtable +0xac, 0x00335a78) and writes it back. No script calls it. |
confirmed (code) |
0x0011ba20 |
Camera_AttachObjectToView |
CameraTransform: object matrix = camera matrix × object matrix (0x003359b0), set back on the object (vtable +0xb4). No script calls it. |
confirmed (code) |
0x0011bad8 |
Camera_ResetByHandle |
CameraReset: vtable +0x13c reset; for a follow camera the other player's follow camera (if made) is reset too. |
confirmed (code) |
0x0011bb98 |
Camera_SetClipping |
CameraSetClipping: near plane (not for a follow camera) and far clip capped at 150. |
confirmed (code) |
0x0011bc48 |
Camera_CreateLocked |
CameraCreateLocked: factory type 1; position at +0x1e0 and +0x10; fov (vtable +0x194); aim from three values (0x001353b8); near; far capped at 150; lens to default (vtable +0x20c -> +0x1b4); returns the handle. |
confirmed (code) |
0x0011bdc0 |
Camera_LockLocked |
CamLockLocked: adds/removes a human in a locked camera's kept-in-view list (0x001358d0). |
confirmed (code) |
0x0011be18 |
Camera_CreateThird |
CameraCreateThird: factory type 0x10 on a target object (+0x1e0); fov (vtable +0x194); distance/height at +0x210/+0x214, angle (0x001207b0), offset +0x220; near; far capped at 150; returns the handle. |
confirmed (code) |
0x0011bfa8 |
Camera_SetupFollow |
CamSetupFollow: puts the player's Cam_Follow on a target (vtable +0x1bc), places it at the target plus the offset +0x200, Cam_Follow_PlaceBehind(180, -1), returns its handle. setting up. |
confirmed (code) |
0x0011c0b8 |
Cam_ConfigureFollow |
CfgFollowCamera (player 1's follow camera): distances, pitch, fov, near plane, offset, slow-motion factor 0x005148a0; band to the minimum with one player camera. setting up. |
confirmed (code) |
0x0011c270 |
Camera_TargetList |
CamTarget: adds or removes a human in the shared target list 0x005d91a8. script calls. |
confirmed (code) |
0x0011c2f0 |
Camera_SetFollowHeading |
CamSetFollowHeading: snaps every player's follow camera to a heading round its target (Cam_Follow_SetHeading), no blend. |
confirmed (code) |
0x0011c3b8 |
Camera_SetFollowPitch |
CamSetFollowAngle: every follow camera's target pitch, clamped, reached at once (Cam_Follow_SetTargetPitch). script calls. |
confirmed (code) |
0x0011c470 |
Camera_SetFollowZoom |
CamSetFollowZoom: band to the minimum, default or maximum, zoom step and upper pitch limit. script calls. |
confirmed (code) |
0x0011c638 |
Camera_SetFollowPosition |
CamSetFollowPos: puts a player's follow camera at a world position at once (Cam_Follow_SetPosition). |
confirmed (code) |
0x0011c6b8 |
Camera_CreateFixed |
CameraCreateFixed: factory type 0 at a position, target put on the shared target list (CameraTargets_Add), offset, lens; returns the handle. |
confirmed (code) |
0x0011c858 |
Camera_CreateWin |
CameraCreateWin: sets up the shared Cam_Win (0x00120188): name (vtable +0x18c), target human +0x1e0, fov (vtable +0x194), distance, angle, height and speed (+0x1e4..+0x1f0), direction (0x00143c58); near 0.1; far capped at 150; returns the handle. |
confirmed (code) |
0x0011c9e0 |
Camera_SetupPoizo |
CamSetupPoizo: starts the path camera from an existing camera's view, clearing its points; time to the first point, end callback, fov, far. path cameras. |
confirmed (code) |
0x0011cb70 |
Camera_ReversePoizo |
CamReversePoizo: reverses the path camera (PoizoCam_Reverse 0x00142578) if it exists; returns whether it does. |
confirmed (code) |
0x0011cbb0 |
Camera_AddPoizoPoint |
CamAddPoizoPoint: appends a path point (position; orientation from heading, pitch, roll) with its time and callback. |
confirmed (code) |
0x0011cc68 |
Camera_AddPoizoPointCam |
CamAddPoizoPointCam: appends a path point taken from another camera's view. |
confirmed (code) |
0x0011cce8 |
Camera_SetupRail |
CamSetupRail: a player's rail camera: name, target on the target list, fov, offset, near, far; CamRail_Reset. rail. |
confirmed (code) |
0x0011cf70 |
Camera_SetRailLead |
CamLeadRail: puts a player's rail camera into leading mode 1 or 2 with a lead distance. rail. |
confirmed (code) |
0x0011d098 |
Camera_AddRailPoint |
CamAddRailPoint: appends a rail point (CamRail_AppendPoint). rail. |
confirmed (code) |
0x0011d180 |
Camera_LockRail |
CamLockRail: holds or releases a player's rail camera (+0x3e4). rail. |
confirmed (code) |
0x0011d228 |
Camera_ModifyRail |
CamModifyRail: a rail setting's target and ease time. rail. |
confirmed (code) |
0x0011d808 |
HoodCam_Setup |
CamSetupHood: every player's hood camera mounted on a vehicle with mount offset and lens values. |
confirmed (code) |
0x0011daa8 |
Camera_UseDeathCamera |
CamUseDeathCamera: makes player 1's Cam_Failed (0x001200e0) current with no blend on a human; saves the previous camera (+0x1e4), sets flags 0x0050b1e8=1/0x0050b1ec=0, fades the screen over ms (0x0018c988/0x0018d058), hides the HUD (0x001b1f38) and removes the overhead icons of every player and the human (Human_RemoveOverheadIcon, 0x00227388); it stops no one. Death camera. |
confirmed (code) |
0x0011dcf0 |
Camera_SetFollowSecondary |
CamSetSecondary: a human/object for a player's follow camera to keep in view (+0x320) and its range (+0x3fc). |
confirmed (code) |
0x0011dd78 |
Camera_CanSeeObject |
CamCanSee: is a point 0.3 m (1 m for class flag 0x40) above the object seen by any player's camera within range (Camera_AnyPlayerCanSeePoint). |
confirmed (code) |
0x0011de58 |
Camera_EnableFeature |
CamEnable(switch, on, player): writes one of the 14 camera switches (switches). | confirmed (code) |
0x0011e0a8 |
Camera_SetSplitMode |
CamSetSplitMode: split layout 0 or 1 into 0x0050b1a0; when it changes, re-lays out the views (0x00122ed0). |
confirmed (code) |
0x0011e0e0 |
Camera_SetGameAspect |
CamSetGameAspect: Cameras_SetAspectAll (0x00122ca0). No script calls it. |
confirmed (code) |
0x0011e100 |
Camera_SetHudAspect |
CamSetHUDAspect: stores the overlay camera aspect 0x0050b208 (0x00122d38). No script calls it. |
confirmed (code) |
0x0011e120 |
Camera_SetHudScale |
CamSetHUDScale: overlay view-window scale 0x0050b20c. No script calls it. |
confirmed (code) |
0x0011e130 |
Camera_RegisterObject |
CamRegisterObject(handle, on): adds (0x001233c0) or removes (0x001233e8) an object handle in the 4-entry list 0x005d9198. No script calls it. |
confirmed (code) |
0x0011e188 |
Camera_GetLastTarget |
CamGetLastTarget: returns the word 0x005d91b8. No script calls it. |
confirmed (code) |
0x0011e198 |
Camera_AssignReverseButton |
CamAssignRevCamButton: the reverse-camera button 0x0050b230. |
confirmed (code) |
The camera manager¶
Cam_ICamera.cpp: the factory, the per-player slots (Globals), the update, the camera stack and the
visibility tests, 0x0011e1b0-0x001204f0.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0011e1b0 |
Camera_Create |
Camera factory (type, name, player): allocates types 0 Cam_Fixed, 1 Cam_Locked, 4 Cam_Scene, 0x10 Cam_3rdPerson; the per-player getters for 2,3,5,7,8 (made on first use); only existing ones for 0xc/0xd. Sets the name (vtable +0x18c) and, if it has no player yet, player 0 (vtable +0x274). |
confirmed (code) |
0x0011e440 |
Camera_Release |
Deletes a camera: clears it from a blend (+0x210 from / +0x214 to; a scene camera is replaced by the top of the camera stack), from its singleton slot, from the players' base and current slots (a player whose current camera it was falls back to his base follow/rail camera, which is reset), then frees it. param 2 = 1 at level end. |
confirmed (code) |
0x0011e878 |
Cameras_Update |
Cameras' update (dt): counts the target list 0x005d91a8 into 0x0050b194; if player 1 has no current and no base camera but a follow camera, makes it both; runs each player's current camera update (vtable +0x134). |
confirmed (code) |
0x0011e978 |
Camera_FromObject |
Returns the object if its class flags (vtable +0x24) have 0x200 (a camera), else null. Every handle-taking camera binding uses it. |
confirmed (code) |
0x0011e9c0 |
Cameras_ClearSlots |
Clears every per-player camera slot (0x005d9148-0x005d918c) and the single ones (0x0050b16c path, 0x0050b170 death, 0x0050b174 win), then re-lays out the views (0x00122ed0). |
confirmed (code) |
0x0011ea80 |
Cameras_ReleaseAll |
Releases every camera in handle slots 0x3c-0x6b (Camera_Release(cam,1)), then Cameras_ClearSlots. | confirmed (code) |
0x0011eae0 |
Cameras_GetViewCount |
Returns the number of views/players 0x0050b198. |
confirmed (code) |
0x0011eaf0 |
Cameras_AddPlayer |
Gives a new player (index) the kinds of game camera player 1 has (follow, rail, hood), copying their settings (0x00124c28, 0x0013b358, 0x00134ca8); his base and current camera become the same kind as player 1's base (for a blend, its destination's), activated (vtable +0x144). |
confirmed (code) |
0x0011ed60 |
Cameras_RemovePlayer |
Releases a player's follow, rail and hood cameras and clears his base and current camera. | confirmed (code) |
0x0011ee08 |
Camera_MakeActive |
Makes a camera current for a player, optionally blending (Cam_Transition 0x0011fac8) over the given time; while a scene camera is current it replaces the camera the scene returns to instead. blends. |
confirmed (code) |
0x0011f9b0 |
Camera_GetPlayerCamera |
A player's current camera (0x005d9150[player]). |
confirmed (code) |
0x0011f9c8 |
Camera_GetPlayerBase |
A player's base camera, the one returned to (0x005d9148[player]). |
confirmed (code) |
0x0011f9e0 |
Cam_GetFollow |
A player's Cam_Follow (0x005d9158[player]), made on first use when asked (0x480 bytes). |
confirmed (code) |
0x0011fac8 |
Cam_GetTransition |
A player's blend camera Cam_Transition (0x005d9180[player], 0x240 bytes), made on first use when asked. |
confirmed (code) |
0x0011fbb0 |
Camera_GetPlayerRail |
A player's Cam_Rail (0x005d9160[player], 0x3f0 bytes), made on first use when asked. |
confirmed (code) |
0x0011fc98 |
Cam_GetPower |
A player's power-move camera Cam_Power (0x005d9188[player], 0x340 bytes), made on first use when asked (only the begin event 0x39 asks; the animation events always pass player 0). |
confirmed (code) |
0x0011fd80 |
Cam_GetMug |
A player's mugging camera Cam_Mug (0x005d9170[player], 0x250 bytes), made on first use when asked. |
confirmed (code) |
0x0011fe68 |
Cam_GetMini |
A player's mini-game camera Cam_Mini (0x005d9178[player], 0x260 bytes), made on first use when asked. |
confirmed (code) |
0x0011ff50 |
HoodCam_GetForPlayer |
A player's hood camera Cam_Hood (0x005d9168[player]), made on first use when asked. |
confirmed (code) |
0x00120038 |
Cam_GetPoizo |
The one path camera Cam_Spline (0x0050b16c), made on first use when asked. |
confirmed (code) |
0x001200e0 |
Cam_GetFailed |
The one death camera Cam_Failed (0x0050b170, 0x200 bytes), made on first use when asked. |
confirmed (code) |
0x00120188 |
Cam_GetWin |
The one win camera Cam_Win (0x0050b174, 0x200 bytes), made on first use when asked. |
confirmed (code) |
0x00120230 |
Cameras_MinDistanceSq |
Smallest squared distance from a point to any player's current camera (FLT_MAX with no views). | confirmed (code) |
0x001202e8 |
Camera_AnyPlayerCanSeePoint |
Is a point within range of and seen by any player's current camera (Camera_CanSeePoint). | confirmed (code) |
0x00120380 |
Cameras_CanSeePointExcept |
As Camera_AnyPlayerCanSeePoint, but a player whose human has +0x1d8 == 1.0 does not count. |
confirmed (code) |
0x00120450 |
CameraStack_Push |
Pushes a camera on the 2-deep stack 0x005d9190 (index 0x0050b180): the camera to return to after a scene. |
confirmed (code) |
0x00120488 |
CameraStack_Pop |
Pops the camera stack 0x005d9190 (null when empty). |
confirmed (code) |
0x001204c0 |
CameraStack_Top |
Top of the camera stack 0x005d9190 without popping (null when empty). |
confirmed (code) |
Cam_3rdPerson (type 16)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x001204f0 |
Cam3rdPerson_Construct |
Cam_3rdPerson constructor (0x230 bytes, vtable 0x00535250): no target, distance 5.1 (+0x210), height 1.8 (+0x214), fov 50, offset (0,0,1.5), near 0.5, far 75. |
confirmed (code) |
0x001205c0 |
Cam3rdPerson_Destroy |
Cam_3rdPerson destructor (vtable +0xc): restores the vtable and runs the base destructor 0x001209d0. |
confirmed (code) |
0x001205e8 |
Cam3rdPerson_Update |
Cam_3rdPerson update (vtable +0x134): look-at = target position + offset (+0x220); orientation (+0x200) slerps 10% to the target's; camera = look-at + orientation × (0,-distance(+0x210),0), height (+0x214) added, swung by the angle quaternion (+0x1f0); shake; look-at matrix (0x00337028) to +0x20/+0x10; base finish 0x00120a30. |
confirmed (code) |
0x001207b0 |
Cam3rdPerson_SetAngle |
Cam_3rdPerson: angle in°rees -> quaternion about z at +0x1f0. |
confirmed (code) |
The base class Cam_ICamera¶
The base camera's own methods, the frustum, the views and the shake, 0x00120868-0x00123630. Its fields are in
The base camera object.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00120868 |
ICamera_Construct |
Cam_ICamera base constructor (vtable 0x00535510): fov 60 (+0x44), aspect 0x0050b204 (+0x4c), far 60 (+0x54), near 0.3 (+0x50), draw distance 60 (0x00122128), identity orientation/position, shake cleared, flags +0x1d6/+0x1d7 = 1, player byte +0x1d8 = -1; registers a handle (+0x40, 0x0038ffd0). |
confirmed (code) |
0x001209d0 |
ICamera_Destroy |
Cam_ICamera destructor: frees the handle (+0x40, 0x00390038); deletes when flag bit 1. |
confirmed (code) |
0x00120a30 |
ICamera_FinishUpdate |
Base update tail (vtable +0x134 of the base and called by every class's update): stores dt (+0x1a0), rebuilds the frustum (0x00120c50), probes the ground under the view (0x00121720) and decides the views (0x00121888). |
confirmed (code) |
0x00120a68 |
Camera_UpdateViewState |
Runs the ground probe 0x00121720 and the view decision 0x00121888. |
confirmed (code) |
0x00120a98 |
Camera_UpdateViewWindow |
View window from the field of view: +0x48 fov used, +0x5c/+0x60 tan(half) × aspect × 0.75 / tan(half) × 0.75, copies at +0x64/+0x68; split screen (0x0050b1a0) halves or doubles them. The streamed world. |
confirmed (code) |
0x00120c50 |
Camera_BuildFrustum |
Builds the 6 frustum planes (+0x70..+0xc0) from the near and far corner points (+0xd0..+0x14c, in world space by the camera matrix or the given one) and, unless asked not to, a bounding sphere (+0x150 centre, +0x17c radius). |
confirmed (code) |
0x001210f8 |
Cam_StartShake |
Base shake start (vtable +0x15c entry, call slot +0x15c): level 1-3 sets amplitude 0.5/0.75/1.0, time 0.10/0.15/0.18 s, rumble base 0/0x30/0x60; 0 stops. shake. |
confirmed (code) |
0x00121298 |
Cam_UpdateShake |
Shake and rumble update (every camera kind): eases the amplitude 65% toward the level's, counts the time down, drives the pad rumble byte (+0x41) and adds a random view offset while switch 6 is on. shake. |
confirmed (code) |
0x00121720 |
Camera_ProbeGroundFlags |
Casts a 25 m ray straight down from 1.5 m along the camera's direction; the triangle hit sets +0x1d4 (type bit 0x20) and +0x1d5 (bit 0x10); for a player's current camera the 0x20 flag goes to his colour controller (0x0018bdd0, 0x005fdeb8[player]). |
confirmed (code) |
0x00121888 |
Cameras_DecideViews |
Each update: decides which view each player shows (switches 3/13, Camera_HumanCounts) and sets the last target 0x005d91b8. switches. |
confirmed (code) |
0x00121e30 |
ICamera_IsViewEnabled |
Vtable +0x28c: a camera's view counts when its byte +0x1d6 is set and its player's flag 0x0050b1e8[player] is set (a locked camera shared by both players uses player 1's). |
confirmed (code) |
0x00121f50 |
ICamera_TestObjectVisible |
Vtable +0x164: frustum test (+0x16c) of an object's bounding sphere (object vtables +0x64 centre, +0x5c radius). |
confirmed (code) |
0x00121fd8 |
Camera_FrustumTestSphere |
Vtable +0x16c: sphere vs the 6 frustum planes; 0 inside, 1 crossing a plane, 2 outside. |
confirmed (code) |
0x00122060 |
Camera_FrustumTestBox |
Vtable +0x184: a box's 8 corners (0x00338178) vs the frustum planes; 0 outside, else 1 when partly inside. |
confirmed (code) |
0x00122128 |
Camera_SetDrawDistance |
Vtable +0x1b4: draw distance +0x58; also the device camera's far clip while 0x0050c698 is set. |
confirmed (code) |
0x001221a0 |
Camera_StoreScriptRef |
Vtable +0x1e4: stores a value from the script system (0x00512b04 slot +0xcc) at +0x1cc (inferred: the Lua callback to run at the end). |
inferred |
0x001221e0 |
Camera_CallScriptRef |
Vtable +0x14c: when +0x1cc is set and the script system is ready (slot +0x4c), calls it (slot +0x8c) (inferred). |
inferred |
0x00122248 |
CameraTargets_Contains |
Is an object's handle in the shared target list 0x005d91a8. |
confirmed (code) |
0x001222b0 |
CameraTargets_Add |
Adds an object's handle to the shared target list 0x005d91a8 (once). |
confirmed (code) |
0x00122438 |
CameraTargets_Remove |
Removes an object from the target list 0x005d91a8; optionally recounts 0x0050b194. |
confirmed (code) |
0x001224c0 |
CameraTargets_Swap |
Swaps two entries of the target list 0x005d91a8. |
confirmed (code) |
0x00122548 |
Camera_CanSeePoint |
Is a point within range (capped at the camera's far clip) of a camera, inside its frustum and with no collision hit on the ray from the camera. | confirmed (code) |
0x00122728 |
Camera_PushOutOfHumans |
Moves a camera point out of nearby humans (handles 0-0x3b, two may be skipped): a human whose radius (0x00219860, scale × 0.065 more) plus the near plane reaches the point and whose height span holds it pushes the point out along the view's side axis (inferred). |
inferred |
0x00122b80 |
Cameras_ResetLevel |
Level camera reset: clears the target and object lists, sets the switches to their defaults (switches), split mode 0, re-lays out the views. | confirmed (code) |
0x00122ca0 |
Cameras_SetAspectAll |
Stores the game aspect 0x0050b204 and applies it (vtable +0x19c) to every camera in handle slots 0x3c-0x6b except blends (type 5). |
confirmed (code) |
0x00122d38 |
Camera_StoreHudAspect |
Stores the overlay camera aspect 0x0050b208. |
confirmed (code) |
0x00122d48 |
Camera_AdjustScreenPoint |
Moves a normalised screen point (0..1) for the view: × away from the centre by half of (player 1's camera aspect × 0.75 - 1), y by half of (the device's vertical scale - 1). HUD, captions, prompts and arrows use it to stay on screen. | confirmed (code) |
0x00122ed0 |
Cameras_LayoutViews |
Counts the views: a player's base camera shows when it counts (vtable +0x28c) and switch 3 is on; two players on the same locked camera at the same place share one view. Sets 0x0050b198 views, 0x0050b19c players, fov scaled by 0x0050b200 (at least 40) when going to two views, and the view grid 0x0050b1a8/0x0050b1aa from split mode (0: 2 × 1, 1: 1 × 2, 2: 2 × 2). |
confirmed (code) |
0x001233c0 |
CameraObjects_Add |
Appends a handle to the registered-object list 0x005d9198. |
confirmed (code) |
0x001233e8 |
CameraObjects_Remove |
Removes a handle from the registered-object list 0x005d9198 if present. |
confirmed (code) |
0x00123428 |
CameraObjects_SendMessage12 |
Sends task message 0x12 to the task of each of the 4 registered objects (0x005d9198) that is set. |
confirmed (code) |
0x001234e0 |
CameraObjects_Clear |
Clears the registered-object list 0x005d9198. |
confirmed (code) |
0x00123500 |
Camera_HumanCounts |
Does a human still count for the cameras: 0 while down or dead, or in state 0x00223b70 unless unlock 6/15 or a 0x0041e420 count is set. switches. |
confirmed (code) |
0x00123588 |
Cam_ICamera_StaticInit |
Static init/exit of Cam_ICamera.cpp: clears the object list 0x005d9198 and target list 0x005d91a8 to -1 and the last target. |
confirmed (code) |
0x00123610 |
Cam_ICamera_StaticInitStub |
Static-init stub of Cam_ICamera.cpp: 0x00123588(1, 0xffff). |
confirmed (code) |
Cam_Failed, the death camera (type 12)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00123630 |
CamFailed_Construct |
Cam_Failed (death camera, type 12) constructor, 0x200 bytes, vtable 0x005357d0: no human (+0x1e0), height 5 (+0x1e8), turn rate 1 (+0x1ec), +0x1f0/+0x1f4 1, rotating flag +0x1fe = 1. |
confirmed (code) |
0x001236b8 |
CamFailed_Destroy |
Cam_Failed destructor. | confirmed (code) |
0x001236e0 |
CamFailed_Activate |
Death camera activation (vtable +0x144): looks at the last target 0x005d91b8 if it is a player human, else at whichever player is nearer the previous camera (+0x1e4); then places itself (0x001238a8). |
confirmed (code) |
0x00123888 |
CamFailed_SetTarget |
Death camera: sets the human it looks at (+0x1e0) and places the camera (0x001238a8). |
confirmed (code) |
0x001238a8 |
CamFailed_Place |
Death camera placement: look-at = the human's position (+0x180); height (+0x1e8) = an upward ray of near + 5 m (mask 0x200) less the near plane; camera straight above, looking down (pitch about -88°) at a random yaw (Random_Int(36) × 10° - 180°); rotating flag set. |
confirmed (code) |
0x00123ba0 |
CamFailed_Deactivate |
Death camera vtable +0x154: releases itself when it is no longer its player's current camera. |
confirmed (code) |
0x00123bf0 |
CamFailed_Update |
Death camera update: while rotating, yaws by turn rate × 15°/s and closes in on the body at 0.18 m/s down to 2.5 m; when player 1's screen-effects manager's fade out has fully covered the screen (fade level +0x1d8 >= 1, rate +0x1dc > 0) ends the blur pulse at once (0x0018d058, 0 s, reverse) and stops rotating (Graphics). |
confirmed (code) |
Cam_Fixed (type 0)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00123ea0 |
CamFixed_Construct |
Cam_Fixed (type 0) constructor, 0x210 bytes, vtable 0x00535a90: no target (+0x200), snap flag (+0x204) 0, aim point (+0x1f0) zero; the offset +0x1e0 is set only by CameraCreateFixed (0x0011c6b8). |
confirmed (code) |
0x00123f08 |
CamFixed_Destroy |
Cam_Fixed destructor. | confirmed (code) |
0x00123f30 |
CamFixed_Activate |
Fixed camera activation (vtable +0x144): target = the player's entry of the target list; computes the look-at (CamFixed_ComputeLookAt), finishes an update, then resets (vtable +0x13c). |
confirmed (code) |
0x00123fb8 |
CamFixed_Reset |
Fixed camera reset (vtable +0x13c, CameraReset): runs one update of 1/60 s and sets the snap flag +0x204 so the next update does not smooth. |
confirmed (code) |
0x00124000 |
CamFixed_ComputeLookAt |
Fixed camera: look-at point = the average position of the listed targets that still count, plus the offset. | confirmed (code) |
0x00124248 |
CamFixed_SmoothLookAt |
Fixed camera: damps the new look-at point (+0x180) and aim point (+0x1f0) against last update's: a rise over 0.5 m halves then quarters, a jump over 1 m quarters, a rise of 0.1-0.5 m eases by 1 - 1.875(dz - 0.1), a move of 0.4-1 m by 1 - 1.25(h - 0.4) (the rail camera's damping, rail). |
confirmed (code) |
0x00124690 |
CamFixed_Update |
Fixed camera update: with targets, recomputes and (unless snapped) smooths the look-at, shakes, and faces it (0x00337028); base finish; clears the snap flag. |
confirmed (code) |
Cam_Follow (type 2)¶
Cam_Follow.cpp, 0x00124778-0x00134b90; the behaviour is under Behaviour.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00124778 |
Cam_Follow_Construct |
Cam_Follow constructor (0x480 bytes, vtable 0x00535d50): distances 4/9/6, pitch 15°, fov 65, offset (0,0,1.4), lag 0.22, pitch limits -20..50°, near 0.1, far 0x005d91c0 (115); clears the latches and the camera-obstacle list. the follow camera object. |
confirmed (code) |
0x00124be0 |
Cam_Follow_Destroy |
Cam_Follow destructor. | confirmed (code) |
0x00124c28 |
Cam_Follow_CopySettings |
Copies another follow camera's distances, pitch, fov, near plane and offset into this one, then applies them (0x00125888). Used when a second player joins. |
confirmed (code) |
0x00124d00 |
Cam_Follow_Reset |
Follow camera reset (vtable +0x13c, CameraReset): place behind the target (0x00124f38(180,-1)), target pitch and fov back to the configured ones, line-of-sight pull-in. setting up. |
confirmed (code) |
0x00124f38 |
Cam_Follow_PlaceBehind |
Places the camera at a heading from the target's facing (180 = behind) at a distance picked from the band presets (min/default/max-0.5) and sets the zoom step. setting up. | confirmed (code) |
0x001250a8 |
Cam_Follow_PlaceAtHeading |
Puts the camera at a heading relative to the target's facing and a distance about the look-at point, pitch to the target pitch; optionally sets the band to that distance (+0.5 deep) and the zoom step from it. | confirmed (code) |
0x001254f0 |
Cam_Follow_StepZoom |
Sets the zoom step +0x400 and the upper pitch limit +0x3ac from a distance (50/40/30°). pitch. |
confirmed (code) |
0x00125588 |
Cam_Follow_GetZoomLevel |
Zoom level from the zoom step +0x400: 2 far, 1 default, 0 close (one player); 1 or 2 with two. update step 12. |
confirmed (code) |
0x001255f8 |
Cam_Follow_SetHeading |
CamSetFollowHeading core: snaps the look-at point, turns the camera to the given orientation's horizontal forward, stands it at the band's near edge, sets the wanted and previous positions and runs one 1/60 s update. |
confirmed (code) |
0x00125870 |
Cam_Follow_Nop |
Empty method called by the follow destructor and deactivation. | confirmed (code) |
0x00125888 |
Cam_Follow_ApplyConfig |
Turns the view to the configured pitch at once and eases the fov to its new value over 1 s (+0x39c); height-hold ease scale +0x398 = 0.25. setting up. |
confirmed (code) |
0x001259e8 |
Cam_Follow_SetMinDistance |
Sets the minimum distance +0x300 (clamped against the others). |
confirmed (code) |
0x00125a40 |
Cam_Follow_SetMaxDistance |
Sets the maximum distance +0x304 and recomputes the lower pitch limit +0x3b0. |
confirmed (code) |
0x00125ac8 |
Cam_Follow_SetDefaultDistance |
Sets the default distance +0x308 and the band default .. default + min(0.5, max - min). |
confirmed (code) |
0x00125b60 |
Cam_Follow_SetTargetPitch |
CamSetFollowAngle core: target pitch (degrees) clamped to [+0x3b0, +0x3ac]; with snap, turns the view to it at once and sets the wanted/previous positions; clears the sprint latch +0x466. |
confirmed (code) |
0x00125c50 |
Cam_Follow_SetPosition |
CamSetFollowPos core: snaps the look-at point, places the camera at a world position and sets its wanted and lag copies to it. |
confirmed (code) |
0x00125cc0 |
Cam_Follow_Activate |
Follow activation (vtable +0x144): recount targets, snap look-at, distance clamped to the band, hard band = band, wanted near edge cleared, recovered distance = near edge. setting up. |
confirmed (code) |
0x00125e50 |
Cam_Follow_ActivateStep |
Follow activation step: places the camera at the band and snaps the look-at point; arms the split timers. | confirmed (code) |
0x001260e8 |
Cam_Follow_Deactivate |
Follow vtable +0x154 (another camera taking over): ends a target takeover whose player left, re-lays out split views, clears the close-up flash (0x00126370), applies a pending heading target at once. |
confirmed (code) |
0x00126370 |
Cam_Follow_ClearFlash |
For both players' follow cameras clears the flash alpha +0x478 and timer +0x424, and the colour controllers' alpha byte +0x1b4. |
confirmed (code) |
0x001263e8 |
Cam_Follow_StartShake |
Follow camera shake start (vtable call slot +0x15c): in the combat camera shakes are 0.66 as strong and the rumble thresholds lowered. shake. |
confirmed (code) |
0x00126450 |
CameraObstacles_Add |
Adds a world object to the camera-obstacle list 0x00715280 (31 entries, count 0x0050b238; a full list ignores the add); WorldObject_Init calls it only for kind 29 TYPE_MOVINGVEHICLE while player 1's follow camera exists. tilt. |
confirmed (code) |
0x00126488 |
CameraObstacles_Remove |
Removes a world object from the camera-obstacle list 0x00715280 (the last entry fills its slot); WorldObject_Delete calls it. |
confirmed (code) |
0x00126520 |
CameraObstacles_Clear |
Clears the camera-obstacle list 0x00715280 (the follow constructor calls it). |
confirmed (code) |
0x00126558 |
Cam_Follow_TakeOverTarget |
One player: an AI goal (0x002b8ad8) hands the follow camera another human as its target, 2.5-30 m from player 1: target swapped in the target list, placed behind, target pitch -5°, fov 70, minimum distance 2 m less, band at the minimum, +0x465 set; the old values are saved for 0x00126878. |
confirmed (code) |
0x00126878 |
Cam_Follow_EndTakeOver |
Ends Cam_Follow_TakeOverTarget: player 1 back in the target list, fov, target pitch, minimum distance and band restored, +0x465 cleared. |
confirmed (code) |
0x00126a30 |
Cam_Follow_EnableSprintZoom |
CamEnable(5): the sprint zoom switch +0x468; off also clears its state. sprint zoom. |
confirmed (code) |
0x00126a68 |
Cam_Follow_ArmSplitTimers |
Sets the two co-op split/merge timers +0x41c/+0x420 to 0.72 s so the next decision acts at once. |
confirmed (code) |
0x00126a88 |
Cam_Follow_CountdownFlash |
Counts +0x476 up to +0x477; then, on a player's current camera with a player target, a small shake and (unless his colour controller is fading) a flash: alpha 225 (+0x478) on the colour controller, timer +0x424 = 0.2 s. Used when co-op views split or merge. |
confirmed (code) |
0x00126c40 |
Cam_Follow_DecideSplit |
Co-op (2 players): decides whether the players share one view or split the screen. Merge needs both in view, within 11 m and no colour controller busy; split/merge waits for +0x41c/+0x420 to pass 0.72 s; toggles the follow cameras' views (vtable +0x294) and re-lays out (Cameras_LayoutViews). |
confirmed (code) |
0x001276a0 |
Cam_Follow_OnViewCountChange |
Co-op: when the view count changed, re-snaps the look-at points, re-applies band and zoom step for the new count, resets the target pitch to 25° for a player target and schedules the flash (0x00126a88). |
confirmed (code) |
0x00127ac8 |
Cam_Follow_UpdateSplitScreen |
Co-op step of the follow update: with one listed target merges to player 1's view, else DecideSplit; then OnViewCountChange. | confirmed (code) |
0x00127c10 |
Cam_Follow_Prime |
Run when a follow camera is made current (Camera_MakeActive, the view decision, GameState_SyncPlayers): one 1/60 s update (or the view decision the first time), arms the split timers, gathers targets, split-screen step, another update when the player has a base camera. | confirmed (code) |
0x00127cd8 |
Cam_Follow_GetFocusPoint |
Follow vtable +0x24c: the point it frames: the look-at point +0x180, or +0x2d0 while a watched player (+0x320) is kept in view of a non-player target. |
confirmed (code) |
0x00127d48 |
Cam_Follow_RefreshLookAt |
Gathers the targets (0x001282a0) and recomputes the look-at point (0x00127d88, with snap). |
confirmed (code) |
0x00127d88 |
Cam_Follow_LookAt |
Look-at point = target + offset, its move limited by length (20% above 0.8 m); hidden-in-shadow height 1.65 m. update step 5. | confirmed (code) |
0x00128298 |
Cam_Follow_Nop2 |
Empty function. | confirmed (code) |
0x001282a0 |
Cam_Follow_GatherTargets |
Takes the follow camera's targets from the first two entries of the target list (+0x314/+0x318, count +0x444), falling back to the last target +0x31c. script calls. |
confirmed (code) |
0x00128678 |
Cam_Follow_AimView |
One player target with +0x454 set: heading target 15° off the player's per-player aim vector (record +0x5750), upper pitch limit 50°, lower limit from (0.075 - offset z) / max distance (at least -20°), and a pitch override; raises the turn rate up to 4x while the view is far off (inferred: an aiming view). |
inferred |
0x00128b20 |
Cam_Follow_HeadLook |
Turns the player's head toward the camera heading while the right stick turns the camera (within 150° of the facing). Head look. | confirmed (code) |
0x00128cf0 |
Cam_Follow_SprintZoom |
Sprint zoom: pulls the band in to the minimum and the target pitch to 7° while sprinting, back 250 ms after. sprint zoom. | confirmed (code) |
0x00129050 |
Cam_Follow_RightStick |
Right stick yaw/pitch rates, zoom and centre buttons, look-behind (reverse) button into +0x458. 2,160 bytes (0x00129050-0x001298bf, 540 instructions, one jr ra at the end); the next function is Cam_Follow_LookBehind at 0x001298c0. right stick. |
confirmed (code) |
0x001298c0 |
Cam_Follow_LookBehind |
Look-behind while the reverse button is held (+0x458 = pad): compares the target's facing with the view; past 75° pulls the band out to 8.5 m (+0x455, band saved in +0x35c), back within 45° restores it, and swings the view to face the player. |
confirmed (code) |
0x00129c78 |
Cam_Follow_AutoFollow |
Auto-follow gate: picks the default (0x0012a400) or auto-centre (0x00129f88) rule. heading. |
confirmed (code) |
0x00129f88 |
Cam_Follow_AutoCentre |
Auto-centre rule: rate from the angle between the facing and the view. heading. | confirmed (code) |
0x0012a400 |
Cam_Follow_AutoFollowDefault |
Default auto-follow rule (22.5-157.5°). heading. | confirmed (code) |
0x0012a7d8 |
Cam_Follow_HeightProbe |
Height probe: short ray (flag 0x200) setting the height-hold state +0x453. height hold. |
confirmed (code) |
0x0012aae0 |
Cam_Follow_EaseBand |
Eases the band's near edge to the wanted +0x34c and steps the zoom. sprint zoom step 4. |
confirmed (code) |
0x0012ac58 |
Cam_Follow_EaseRoll |
Eases the roll +0x408 toward +0x404 over the timer +0x40c (else 60°/s) and rolls the view by it (inferred: roll about the view axis). |
inferred |
0x0012adf0 |
Cam_Follow_UpdateEntry |
Follow update (vtable +0x134): clears the yaw rate +0x360, runs the flash countdown when armed, then Cam_Follow_Update when dt > 0. |
confirmed (code) |
0x0012ae58 |
Cam_Follow_Update |
The follow camera's update: look-at, fov, stick, auto-follow, leash, pitch, height hold, heading target, lag, hard band, collision. update. | confirmed (code) |
0x0012d3b8 |
Cam_Follow_YawAtRate |
Yaws the camera about the look-at point by rate × dt and stores the rate +0x360. |
confirmed (code) |
0x0012d4e8 |
Cam_Follow_Pitch |
Pitch about the look-at point, clamped to [+0x3b0, +0x3ac]. pitch. |
confirmed (code) |
0x0012d688 |
Cam_Follow_Yaw |
Yaw about the look-at point. heading. | confirmed (code) |
0x0012d7a8 |
Cam_Follow_MoveBand |
Moves the distance band and recomputes the lower pitch limit. pitch. | confirmed (code) |
0x0012d908 |
Cam_Follow_GetViewPitch |
Current pitch of the view (from the camera to the look-at point). | confirmed (code) |
0x0012da20 |
Cam_Follow_ReactToLedge |
Called by human code (0x0023dc58) at a ledge: heading target the given heading +-92°, target pitch the upper limit - 5° (old saved in +0x3d4), sprint zoom cancelled, over the given time (0.35 s for 0). |
confirmed (code) |
0x0012de48 |
Cam_Follow_TurnToQuat |
Converts an orientation quaternion to a matrix and calls Cam_Follow_TurnToMatrix (Cam_Mug, 0x00137e08 / 0x00138b90). |
confirmed (code) |
0x0012ded8 |
Cam_Follow_TurnToMatrix |
Sets a timed heading target (+0x350, the matrix's heading turned 180°) and pitch override (+0x354) over the given times, +0x475 set; optionally merges co-op views; with a camera passed, queues it as the hand-over (+0x47c, blend +0x3c0) and sets the pitch override. Used when mug and mini-game cameras cut in and hand back. |
confirmed (code) |
0x0012e170 |
Cam_Follow_KeepInView |
Keeps a watched target in view, yaw at most 270°/s. heading. | confirmed (code) |
0x0012e9a8 |
Cam_Follow_FrameEnemy |
Combat camera: holds the enemy 27° off centre. combat camera. | confirmed (code) |
0x0012ec50 |
Cam_Follow_TiltOverObstacles |
Sweeps a sphere from the look-at point to the camera against the camera-obstacle boxes (0x00715280); tilts the view over a box it looks along (+0x3a8, eased 90%, decays 10%) or pulls the camera in front of it. Helpers. |
confirmed (code) |
0x0012f3e0 |
Cam_Follow_SlopePitch |
From the height ray's ground normal, eases a pitch offset +0x3a4 10% per update toward the slope's angle (sets +0x460); 0 on flat ground. Helpers. |
confirmed (code) |
0x0012f858 |
Cam_Follow_RaiseWhenBlocked |
When the collision step pulls the camera in, raises the wanted position toward a height over the look-at, chooses a clear heading (0x0012fd20) for one target, and sphere-pushes the aim point (1.0 m) again. Helpers. |
confirmed (code) |
0x0012fd20 |
Cam_Follow_ChooseClearHeading |
Blocked view, one target: casts 11 rays round the face's normal (0, ±30°, ±45°, ±60°, ±77°, ±82°), scores 9 windows of 3 and stores the best as the swing heading +0x43c; latches +0x462..+0x464. Helpers. |
confirmed (code) |
0x001303c8 |
Cam_Follow_SphereTest |
Switch 1: a physics bound sphere (radius = near plane) at the camera collided with the physics world, ignoring object kinds 0xc-0xe, 0x10 and 0x1c; keeps the camera off what it hits (+0x410 timer 0.15 s). Helpers. |
confirmed (code) |
0x00130990 |
Cam_Follow_Collide |
Follow world collision: rays, side probes, swing away, sphere pushes, next update's lag. collision. | confirmed (code) |
0x00134ab0 |
Cam_Follow_StaticInit |
Static init of Cam_Follow.cpp: far clip 115 (0x005d91c0) and cosines of 75, 45, 53.13 and 10° (0x005d91c4..0x005d91d0). |
confirmed (code) |
0x00134b70 |
Cam_Follow_StaticInitStub |
Static-init stub of Cam_Follow.cpp. | confirmed (code) |
Cam_Hood (type 11)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00134b90 |
CamHood_Construct |
Cam_Hood (type 11) constructor, 0x220 bytes, vtable 0x00536010: no vehicle (+0x1e0), look offset +0x1f0 (0, 0, 1.53), mount offset +0x200 (0, 0.3, 1.7), fov 60 (also the wanted fov +0x214), near 0.1, far 115. |
confirmed (code) |
0x00134c80 |
CamHood_Destroy |
Cam_Hood destructor. | confirmed (code) |
0x00134ca8 |
CamHood_CopySettings |
Copies another hood camera's vehicle, mount and look offsets, wanted fov and fov (Cameras_AddPlayer). | confirmed (code) |
0x00134d28 |
CamHood_Activate |
Hood camera activation (vtable +0x144): placed on its vehicle's mount at once, then a base update. |
confirmed (code) |
0x00134d78 |
HoodCam_PlaceOnMount |
Hood camera: position = the vehicle's position (an object with class flag 0x40: its transform-table entry) plus the mount offset in the vehicle's frame; orientation from the vehicle. | confirmed (code) |
0x00134f08 |
HoodCam_AimAtTargets |
Hood camera: look-at point = the mean position of the counted targets plus the look offset's height. | confirmed (code) |
0x00135028 |
CamHood_GatherTargets |
Hood camera: the counted players among the target list's first two entries (count +0x218), else the last target kept in +0x210. |
confirmed (code) |
0x00135138 |
HoodCam_Update |
Hood camera update: on the mount, the fov stepped toward +0x214 by at most dt°rees, aimed at the targets, base finish. |
confirmed (code) |
Cam_Locked (type 1)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x001352f0 |
CamLocked_Construct |
Cam_Locked (type 1) constructor, 0x220 bytes, vtable 0x005362d0: heading, pitch, roll 0 (+0x200..+0x208), kept-in-view list (+0x20c, two handles) cleared. |
confirmed (code) |
0x00135390 |
CamLocked_Destroy |
Cam_Locked destructor. | confirmed (code) |
0x001353b8 |
CamLocked_SetAngles |
Locked camera: heading, pitch and roll in°rees (+0x200/+0x204/+0x208), then the orientation (0x001353e0). |
confirmed (code) |
0x001353e0 |
CamLocked_BuildOrientation |
Locked camera orientation = rotation about z by the heading × about × by the pitch × about y by the roll. | confirmed (code) |
0x001355f8 |
CamLocked_Activate |
Locked camera activation (vtable +0x144): one base update of 1/60 s. |
confirmed (code) |
0x00135680 |
CamLocked_Update |
Locked camera update: placed, aimed 3 m ahead, kept out of walls, keeps its listed humans inside the view's sides, shake. locked cameras. | confirmed (code) |
0x001358d0 |
CamLocked_SetKeptHuman |
CamLockLocked core: adds or removes a human in the locked camera's kept-in-view list (two entries). |
confirmed (code) |
0x00135960 |
CamLocked_PushHumanIntoView |
Pushes a listed human back inside the view when within 0.3 m of the left or right frustum plane, with a world ray and a ground snap. locked cameras. | confirmed (code) |
0x00135ca8 |
LockedCam_KeepHumansInView |
Runs the push for each human of the locked camera's list. locked cameras. | confirmed (code) |
Cam_Mini, the mini-game camera (type 8)¶
Cam_Mini.cpp: the shots of the mount and stereo-theft mini-games (Mini-game and mugging cameras).
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00136068 |
CamMini_Construct |
Cam_Mini (type 8, Minigame) constructor, 0x260 bytes, vtable 0x00536590: two humans (+0x1e0/+0x1e4), the kind (+0x25c), the camera to return to (+0x240), shot distance +0x254 (20, set to 3 by the set-ups), done flag +0x258. |
confirmed (code) |
0x00136160 |
CamMini_Destroy |
Cam_Mini destructor. | confirmed (code) |
0x00136188 |
CamMini_Deactivate |
Mini-game camera vtable +0x154: releases itself. |
confirmed (code) |
0x001361a8 |
CamMini_Start |
Mini-game camera start (kind 0 from Player_UpdateMounting via 0x0022d030, kind 2 from StereoTheft_Start): saves the base camera to return to, copies its fov and near plane, places the shot (0x00137670); from a follow camera the follow is turned to the shot's orientation for the hand-back (Cam_Follow_TurnToMatrix, 0.15 s). |
confirmed (code) |
0x001363e8 |
CamMini_PlaceKind0 |
Mini-game shot for kind 0: 3 m from the mounting player's feet + 0.5 m on his clearer side, a yaw and a pitch picked at random from five-entry tables (shot tables) (up to 3 tries until 0x001374d8 finds it clear), else a fixed 36° pitch. |
confirmed (code) |
0x00136a88 |
CamMini_PlaceKind1 |
Mini-game shot for kind 1: as kind 0 with other yaw and pitch tables (fallback pitch 32°). No caller passes kind 1. | confirmed (code) |
0x00136f68 |
CamMini_PlaceKind2 |
Mini-game shot for kind 2 (the stereo): 3 m from the stereo + 0.3 m, one of eight yaws and three pitches, tried with 0x001374d8 (clear for 2.3 m) (shot tables). |
confirmed (code) |
0x001374d8 |
CamMini_TestShot |
Tests a mini-game shot: a sphere sweep from the look-at point toward the camera; true when the clear distance reaches the wanted one (+0x254). |
confirmed (code) |
0x00137670 |
CamMini_PlaceShot |
Mini-game shot set-up: a physics bound sphere (near-plane radius) for the tests, then the kind's placement (0x001363e8 / 0x00136a88 / 0x00136f68). |
confirmed (code) |
0x00137808 |
CamMini_Update |
Mini-game camera update: keeps the humans framed (turns at most 3x the angle error per second); when the done flag +0x258 is set (0x0022d1f8) goes back to the saved camera with a 0.3 s blend. |
confirmed (code) |
0x00137c68 |
CamMini_StaticInit |
Static init of Cam_Mini.cpp. | confirmed (code) |
0x00137cb0 |
CamMini_StaticInitStub |
Static-init stub of Cam_Mini.cpp. | confirmed (code) |
Cam_Mug, the mugging camera (type 7)¶
Cam_Mug.cpp, Mini-game and mugging cameras.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00137cd0 |
CamMug_Construct |
Cam_Mug (type 7) constructor, 0x250 bytes, vtable 0x00536850: mugger, victim (+0x1e0/+0x1e4), intro sound handle +0x1ec, look-at offset +0x200 (0, 0, 1.45), fov ease time +0x244 0.33 s. |
confirmed (code) |
0x00137dc0 |
CamMug_Destroy |
Cam_Mug destructor. | confirmed (code) |
0x00137de8 |
CamMug_Deactivate |
Mugging camera vtable +0x154: releases itself. |
confirmed (code) |
0x00137e08 |
CamMug_Start |
Mugging camera start (0x002729a8): only from a single-view follow camera; saves it (+0x230), copies its fov and near, places the shot (0x00138078) and plays vags/misc/mug_intro (2D). |
confirmed (code) |
0x00138078 |
CamMug_PlaceShot |
Mugging shot: about the pair, a random yaw and pitch from small tables, distance 2.5 m (1.8 m when blocked), tested with a physics sphere against vehicles and the collision mesh. | confirmed (code) |
0x00138b90 |
CamMug_Update |
Mugging camera update: eases the fov to 50 over +0x244, turns at most 135°/s to keep both humans framed, keeps 3 m from humans (Camera_PushOutOfHumans); when the mugging ends stops the intro, plays vags/misc/mug_outro, turns the follow camera (Cam_Follow_TurnToQuat) and blends back in 0.3 s. |
confirmed (code) |
0x00139d00 |
CamMug_StaticInit |
Static init of Cam_Mug.cpp: 0x005d91d8 = tan(5°). |
confirmed (code) |
0x00139d48 |
CamMug_StaticInitStub |
Static-init stub of Cam_Mug.cpp. | confirmed (code) |
Cam_Power, the power-move camera (type 6)¶
Cam_Power.cpp, Power-move camera.
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00139d68 |
CamPower_Construct |
Cam_Power (type 6) constructor, 0x340 bytes, vtable 0x00536b10: no human (+0x320), shot count +0x331 = −99 (none), mode +0x330 = 2, unplaced +0x32e = 1, saved fov +0x324 = −1. Fields. |
confirmed (code) |
0x00139df8 |
CamPower_Destroy |
Cam_Power destructor. | confirmed (code) |
0x00139e20 |
CamPower_Deactivate |
Power camera vtable +0x154, run when another camera becomes current: Camera_Release(self, 1) (0x0011e440), so the object is freed and 0x005d9188[0] cleared; the next begin makes a new one. |
confirmed (code), confirmed (runtime) |
0x00139e40 |
CamPower_GetOrientation |
Power camera vtable +0x22c: the orientation of the camera it came from (+0x334), else its own. |
confirmed (code) |
0x00139e90 |
CamPower_StartShake |
Power camera shake (vtable +0x15c): only for a human on the target list (CameraTargets_Contains). |
confirmed (code) |
0x00139ef8 |
CamPower_Update |
Each update: ends (0x0013a3b0) when the human is gone, down or dead, or out of the paired move; else puts the camera back at the cut's position, pushes it out of nearby humans and keeps the cut's orientation (re-aimed only while a shake moves the view). Holding a shot. |
confirmed (code) |
0x0013a170 |
CamPower_Begin |
Event 0x39: saves the current camera (+0x334; when the power camera is already current, keeps the one it saved), copies its near plane and far clip, stores its fov at +0x324, takes the human (the event's, or the saved camera's first target when that is a player) and his transform now, count 0, unplaced; mode = the event's +0x06 (1, else 2). |
confirmed (code) |
0x0013a3b0 |
CamPower_End |
Event 0x3d and the update's end: when it is player 0's or player 1's current camera, makes the saved camera current with no blend (Camera_MakeActive(0 s, saved)); clears +0x32f; count −1. |
confirmed (code), confirmed (runtime) |
0x0013a448 |
CamPower_AddShot |
Event 0x3b: while begun and unplaced, stores shot slot (0-7) in world space from the human's transform at the begin: position +0x1e0 + 16·slot, orientation +0x280 + 16·slot; count + 1. |
confirmed (code), confirmed (runtime) |
0x0013a548 |
CamPower_CutToShot |
Event 0x3a (fov = the event's float, 50° when not above 0): the first cut chooses the shots (0x0013a818) and makes the camera current with no blend; every cut jumps to the current shot and swaps current and alternate. The cut. |
confirmed (code), confirmed (runtime) |
0x0013a818 |
CamPower_PlaceShot |
Tests every stored shot with two swept spheres (from beyond the human's head back to the shot, and from his feet to the shot); keeps the clear ones (mask +0x32c) and picks the current and alternate shots from a counter that turns every power move (0x0050b2a0). Choosing the shots. |
confirmed (code), confirmed (runtime) |
Cam_Rail (type 9)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x0013b118 |
CamRail_Construct |
Cam_Rail (type 9) constructor, 0x3f0 bytes, vtable 0x00536dd0: no points, mode 0, fov target 60, settings off (-1 / -FLT_MAX), offset +0x320 zero, +0x3d0 1.5, +0x3d4 0.5, +0x3d8 -1. |
confirmed (code) |
0x0013b290 |
CamRail_Destroy |
Cam_Rail destructor. | confirmed (code) |
0x0013b2b8 |
CamRail_Reset |
CamSetupRail's reset: no points, mode 0, settings and ease times off, fov target the current fov, switches 7 and 8 on. rail. | confirmed (code) |
0x0013b358 |
CamRail_CopySettings |
Copies another rail camera's points, offset, settings, lead, mode and fov (Cameras_AddPlayer). | confirmed (code) |
0x0013b508 |
CamRail_Activate |
Rail camera activation (vtable +0x144): gathers targets, sets the target point, finishes an update, sets the fov and every current setting to its target at once, then resets (vtable +0x13c). |
confirmed (code) |
0x0013b5d8 |
CamRail_Deactivate |
Rail camera vtable +0x154: re-lays out co-op views when needed (0x0013ccd8) and clears the rail flash (0x0013b6f8). |
confirmed (code) |
0x0013b698 |
CamRail_Reset |
Rail camera reset (vtable +0x13c): arms the split timer, clears the blend +0x3cc and, with two or more points, runs one 1/60 s update with +0x3e8 set (no damping). |
confirmed (code) |
0x0013b6f8 |
CamRail_ClearFlash |
For both players' rail cameras clears the flash alpha +0x3eb and timer +0x3e0, and the colour controllers' alpha byte. |
confirmed (code) |
0x0013b770 |
CamRail_ClearPoints |
Rail point count +0x350 and current segment +0x352 to 0. |
confirmed (code) |
0x0013b780 |
CamRail_AppendPoint |
Appends a rail point (16 bytes at +0x1e0, at most 16). rail. |
confirmed (code) |
0x0013b7b8 |
CamRail_GatherTargets |
The rail camera's targets from the target list, filtered by switches 3, 8, 12 and Camera_HumanCounts. rail. | confirmed (code) |
0x0013ba68 |
CamRail_StartShake |
Rail camera shake (vtable +0x15c): only for a human on the target list. |
confirmed (code) |
0x0013bad0 |
CamRail_UpdateTargetPoint |
The rail camera's target point P and look-at point by mode and settings 3, 4, 9. rail. | confirmed (code) |
0x0013bfe0 |
CamRail_DampTargetPoint |
Damps P and the look-at point toward last update's (the dz / h thresholds). rail. | confirmed (code) |
0x0013c590 |
CamRail_ArmSplitTimer |
Co-op split/merge timer +0x3dc = 0.5 s. |
confirmed (code) |
0x0013c5a8 |
CamRail_Flash |
On a player's current rail camera: small shake and the alpha-225 flash on his colour controller (+0x3eb, timer +0x3e0 0.2 s), as Cam_Follow_CountdownFlash. |
confirmed (code) |
0x0013c710 |
CamRail_DecideSplit |
Co-op (2 players) with rail cameras: whether the players share one view or split, as Cam_Follow_DecideSplit (players in frame and close, timer +0x3dc). |
confirmed (code) |
0x0013ccd8 |
CamRail_OnViewCountChange |
Co-op: after the view count changed, re-frames and flashes each rail camera (0x0013c5a8). |
confirmed (code) |
0x0013ce78 |
CamRail_Prime |
Run when a rail camera is made current (Camera_MakeActive, the view decision): views decided, split timer armed, targets gathered, split step, one 1/60 s update when it is a player's current camera. | confirmed (code) |
0x0013cf20 |
CamRail_UpdateSplitScreen |
Split-screen extras (two player cameras only): DecideSplit and OnViewCountChange; on a merge to one view one more update with +0x3e9 set. rail. |
confirmed (code) |
0x0013d010 |
CamRail_Update |
Rail camera update: ease settings and lead, targets, target point, damping, placement by mode, split extras. rail. | confirmed (code) |
0x0013d6b0 |
CamRail_UpdatePosition |
Mode 0 placement: segment choice, projection with setting 0, blend, height ceiling, look-at with setting 8, ends hold, collision. rail. | confirmed (code) |
0x0013dcc8 |
CamRail_PlaceMode3 |
Mode 3 placement (setting 5 on, the Armies levels 60-64): pushes the targets into frame, then the look-at point chases a goal 2 m in plan from the hindmost target's rail point, forward only, by min(40% of the gap, (his forward speed + 1 m/s) × dt); the camera is its projection on the rail; the look-at point 2 m ahead at the pitch setting 5; holds at the ends (+0x3e4). mode 3. |
confirmed (code) |
0x0013e708 |
CamRail_PlaceLeading |
Modes 1 and 2 (CamLeadRail): the camera stands the lead ahead of or behind the target's place on the rail, or with switch 7 on off the rail beside it, with a ray back toward the rail and a 0.5 m sphere push of the look-at point. leading. | confirmed (code) |
0x0013f930 |
CamRail_EaseValue |
Moves one rail setting toward its target by (target - current) × dt / time left. rail. | confirmed (code) |
0x0013f9b0 |
CamRail_PitchToPoint |
Signed angle of a direction from the horizontal (negative when the point is below +0x2f8's height), used by the rail update's angle settings. |
confirmed (code) |
0x0013fac8 |
CamRail_SetMode3Angle |
Setting 5: below -360 turns mode 3 off (back to mode 0), else mode 3 with that angle in°rees (+0x388). |
confirmed (code) |
0x0013fb30 |
CamRail_SetFixedPitch |
Setting 8: below -360 off, else the look-at pitch in°rees (0 becomes -0.05) at +0x394. |
confirmed (code) |
0x0013fba0 |
CamRail_ChooseSegment |
Chooses the rail segment for P (nearest point, then the segment P projects inside). rail. | confirmed (code) |
0x00140308 |
CamRail_ProjectOnSegment |
Projects P on the segment, clamped, and pulls it toward P by setting 0. rail. | confirmed (code) |
0x00140708 |
CamRail_CollideTwoTargets |
Collision with two targets and switch 8, or while held: when the targets are not all in frame (0x00140ad0, by their radii) and not held, puts back the previous position, orientation and look-at; then pushes the targets into frame (0x00140d68). frame. |
confirmed (code) |
0x00140830 |
CamRail_Collide |
Mode 0's last step for the first target, with no world test: with switch 2 off moves the camera along its segment to keep it max(2, 0.4 × height above P) from P in plan; with switch 2 on keeps the player 2 m from the rail's vertical planes. collision. |
confirmed (code) |
0x00140ad0 |
CamRail_TargetsInFrame |
True when every target, by its radius (player: scale × 0.5; other: capsule radius; or the given margin when the last argument is 0), is inside the left, right and bottom frustum planes (vtable +0x174), and with switch 2 (+0x3e6) beyond the plane 2 m in front of the camera. frame. |
confirmed (code) |
0x00140d68 |
CamRail_KeepTargetsInFrame |
Moves each target (players and others) back inside the left or right and the bottom frustum planes, out of walls toward the camera and onto the ground below (Human_SetTransform); the camera does not move. frame. |
confirmed (code) |
Cam_Scene (type 4)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x001421b8 |
CamScene_Construct |
Cam_Scene (type 4) constructor, 0x1f0 bytes, vtable 0x00537090: +0x1e0 0, +0x1e4 -1. Made by the scene start, 0x00354558 and the movie player; the scene sets its view each frame. |
confirmed (code) |
0x00142210 |
CamScene_Destroy |
Cam_Scene destructor. | confirmed (code) |
0x00142238 |
CamScene_Update |
Scene camera update (vtable +0x134): stores dt and finishes a base update; the scene player moves it. |
confirmed (code) |
Cam_Spline, the path camera (type 3)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00142260 |
CamSpline_Construct |
Cam_Spline (type 3, the path camera) constructor, 0x350 bytes, vtable 0x00537350: no source camera or callback, fov 45 (+0x330), points cleared (0x00142308). |
confirmed (code) |
0x001422e0 |
CamSpline_Destroy |
Cam_Spline destructor. | confirmed (code) |
0x00142308 |
CamSpline_ClearPoints |
Clears the path's 8 points (positions +0x1e0, orientations +0x260, times +0x2ec, callbacks +0x30c), the count +0x334, the progress +0x338 and the running flag +0x33c. |
confirmed (code) |
0x00142368 |
CamSpline_Start |
CamSetupPoizo core: source camera (+0x2e0), fov (0 takes the source's), near, far (0 takes the source's), aspect; the end callback (+0x2e4) from the script system; progress cleared; the source's view becomes point 0 with the given time; placed there. path cameras. |
confirmed (code) |
0x00142578 |
PoizoCam_Reverse |
CamReversePoizo core: puts the points in reverse order with their times shifted, progress to the start, placed at the new first point, restarted with the new end callback. |
confirmed (code) |
0x00142690 |
CamSpline_Activate |
Path camera activation (vtable +0x144): running flag +0x33c set, one base update of 1/60 s. |
confirmed (code) |
0x001426c0 |
PoizoCam_Update |
Path camera update: while it is player 1's current camera, advances along the Catmull-Rom curve through the points (0x00142db8), slerping orientations, calling each point's callback and the end callback. path cameras. |
confirmed (code) |
0x00142a58 |
PoizoCam_AddPoint |
Appends a path point (position, orientation, time, callback); at most 8. | confirmed (code) |
0x00142ac8 |
CamSpline_AddPointAngles |
CamAddPoizoPoint core: orientation from heading (about z), pitch (about x) and roll (about y) in°rees, then PoizoCam_AddPoint. |
confirmed (code) |
0x00142d28 |
CamSpline_AddPointFromCamera |
CamAddPoizoPointCam core: a path point from another camera's position (vtable +0x21c) and orientation (vtable +0x224). |
confirmed (code) |
0x00142db8 |
CamSpline_EvalCurve |
Catmull-Rom point at t on segment n of the path (the end points repeated at the ends): -1.5t^3 + 2t^2 + 0.5t and the other three weights. | confirmed (code) |
Cam_Transition, the blend camera (type 5)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00142f98 |
CamBlend_Construct |
Cam_Transition (type 5, the blend camera) constructor, 0x240 bytes, vtable 0x00537610: no source (+0x210) or destination (+0x214), start orientation identity, fov 60 (+0x224, +0x22c, +0x230), near 0.3 (+0x228), +0x234 1. |
confirmed (code) |
0x00143050 |
CamBlend_Destroy |
Cam_Transition destructor. | confirmed (code) |
0x00143078 |
CamBlend_Start |
Starts a blend: the duration and the cameras. blends. | confirmed (code) |
0x00143090 |
CamBlend_GetAspect |
Blend camera vtable +0x1fc: the destination's aspect, mixed with the source's by t (+0x220) when there is a source. |
confirmed (code) |
0x00143130 |
CamBlend_GetLens234 |
Blend camera vtable +0x234: the destination's value mixed with the source's by t. |
confirmed (code) |
0x001431d0 |
CamBlend_GetLens23c |
Blend camera vtable +0x23c: the destination's value mixed with the source's by t. |
confirmed (code) |
0x00143270 |
CamBlend_GetLens244 |
Blend camera vtable +0x244: the larger of the source's and destination's values. |
confirmed (code) |
0x00143300 |
CamBlend_Activate |
Blend activation (vtable +0x144): snapshots the source's orientation, position, look-at, fov, near, far and draw distance (+0x1e0..+0x230) as the start, activates the destination (vtable +0x144); +0x234 stays 1 only when the source is a scene or path camera. |
confirmed (code) |
0x00143540 |
CamBlend_Deactivate |
Blend camera vtable +0x154: releases itself when it is no longer its player's current camera. |
confirmed (code) |
0x00143590 |
CamBlend_Update |
Blend update: destination (and source) updated, then a linear blend of look-at and position and a slerp of orientation by t, far clip min'd; at t = 1 the destination becomes current. blends. | confirmed (code) |
Cam_Win (type 13)¶
| Address | Name | What it does | Evidence |
|---|---|---|---|
0x00143918 |
CamWin_Construct |
Cam_Win (type 13) constructor, 0x200 bytes, vtable 0x005378d0: no target (+0x1e0), distance 1 (+0x1e4), angle -10° (+0x1e8), height 1.4 (+0x1ec), speed 15°/s (+0x1f0), direction 1 (+0x1f4), orbiting (+0x1f8). Rumble: win camera. |
confirmed (code) |
0x001439b0 |
CamWin_Destroy |
Cam_Win destructor. | confirmed (code) |
0x001439d8 |
CamWin_Activate |
Win camera start (vtable +0x144): look-at above the target, placed on its circle. Rumble: win camera. |
confirmed (code) |
0x00143c58 |
CamWin_SetDirection |
Win camera: circling direction +0x1f4 = 1 for a positive argument, else -1. |
confirmed (code) |
0x00143c78 |
CamWin_Update |
Win camera update: circles the target. Rumble: win camera. | confirmed (code) |
Data¶
Types¶
Every camera is one of fourteen classes; each class's vtable function word +0x1ec (the +0x1e8 slot) returns its
type, confirmed (code) for all fourteen (the list, with tags, vtables and sizes).
The factory 0x0011e1b0(type, name, player) allocates types 0, 1, 4 and 0x10 afresh; for 2, 3, 5, 7 and 8 it calls a
getter that makes the camera on first use and keeps it (per player in 0x005d9158-0x005d918c, or one in
0x0050b16c); for 0xc and 0xd it calls the getter with "do not make" and so only returns an existing one. Types 6,
9 and 0xb are never made by the factory, only by their getters. No class returns 10, 14 or 15. confirmed (code).
Switches¶
CamEnable(switch, on, player) (Camera_EnableFeature, 0x0011de58) writes one of fourteen flags; a level's camera
reset 0x00122b80 sets them back (switches 1 and 13 to 0, the rest to 1), confirmed (code). The flags, scopes,
defaults and readers are on the list. What the readers show, inferred from the
code at the cited addresses:
- Two-player views.
0x00121888decides each update whether each player's view is shown. With switch 3 on and two players, a view is kept only while its player still counts (0x00123500: 0 while0x00227e60holds for the human, and for one state of0x00223b70unless unlock 6/15 is set), so the other player gets the whole screen; switch 13 keeps both views. With switch 3 off both views always show. Switches 4 and 12 apply the same test to the follow and rail cameras' targets. - Shake (
0x00121298): the shake amount drives the pad's rumble byte (pad record+0x41) in every case; the random view offset is added only while switch 6 is on. - Right stick (
0x00129050): returns at once while0x0050b1b0[player]or switch 0 (0x0050b1b8[player]) is 0, so neither the stick nor the zoom buttons act. - Look-behind (switch 11,
0x0050b23c, confirmed (code) at0x0012b4b4): passed to0x00129050as its eighth argument (forced to 0 unless the player runs or sprints, and while he is tagging: The right stick). With it 0 the reverse-camera button (0x0050b230) never turns the view round, and a press on a button shared with the zoom counts as a zoom tap.Human_SetWheelchairControlclears it. - Power camera (switch 9,
0x0050b1d4, one flag for both players): read only byAnim_FireFrameEvents(0x00101dd8, at0x00102da8,0x00102f84and0x00103274), which ignores the power camera's begin (0x39), cut (0x3a) and add-shot (0x3b) events while it is 0; the end event (0x3d) does not read it. Confirmed (code). It is 1 from every level's camera reset (Cameras_ResetLevel0x00122b80) and nothing butCamEnablechanges it: no option, cutscene state or player count.level99_lesson2.luaturns it off while the flash dealer respawns, andlevel55_subway.luacalls it too (the list). What the events do: Power-move camera. - Switches 2, 7 and 8 are read only by the rail camera (type 9), which
level99never makes; switch 7 lets the leading modes stand the camera off the rail, the lead from the target in plan (leading, confirmed (code)). - Switches 3 and 4 change nothing with one player: both only gate the second player's view and target
(
0x00121888,0x001282a0; inferred).level99turns them off around its cut-aways and fights.
Globals¶
The camera manager's state, confirmed (code) at the functions cited (The camera manager):
| Address | Meaning |
|---|---|
0x005d9148[2] |
each player's base camera, the one returned to (0x0011f9c8) |
0x005d9150[2] |
each player's current camera (0x0011f9b0) |
0x005d9158[2] / 0x005d9160[2] / 0x005d9168[2] |
each player's follow / rail / hood camera |
0x005d9170[2] / 0x005d9178[2] / 0x005d9180[2] / 0x005d9188[2] |
each player's mug / mini-game / blend / power camera |
0x0050b16c / 0x0050b170 / 0x0050b174 |
the one path, death and win camera |
0x005d9190[2], 0x0050b180 |
the camera stack (top index, −1 empty): the camera a scene returns to |
0x005d9198[4] |
objects registered with CamRegisterObject; message 0x12 goes to each (0x00123428) |
0x005d91a8[4] |
the shared target list (CamTarget); 0x0050b194 its count, refreshed each update |
0x005d91b8 |
the last target (CamGetLastTarget), set by the view decision 0x00121888 |
0x0050b198 / 0x0050b19c |
views shown / player cameras, counted by 0x00122ed0 |
0x0050b1a0 / 0x0050b1a4 |
split mode (CamSetSplitMode) / the mode in use (2 with three or more views) |
0x0050b1a8 / 0x0050b1aa |
the view grid: columns and rows (mode 0: 2 × 1, 1: 1 × 2, 2: 2 × 2) |
0x0050b1e8[2] |
per player: his view may show (0x00121e30) |
0x0050b200 |
field-of-view scale when the screen splits (at least 40°) |
0x0050b204 / 0x0050b208 / 0x0050b20c |
game aspect, overlay aspect, overlay scale (Graphics) |
0x0050b230 / 0x0050b234 |
the reverse-camera and zoom buttons |
0x00715280[31], 0x0050b238 |
the moving vehicles (kind 29) the follow camera tilts over (0x0012ec50); WorldObject_Init adds them |
The switches sit beside them (0x0050b1b0-0x0050b2b4).
The base camera object¶
Every camera class starts with Cam_ICamera (constructor 0x00120868). Confirmed (code) at the constructor and the
cited functions; the lens fields are also on The streamed world.
| Offset | Meaning | Constructor |
|---|---|---|
+0x00 |
vtable | 0x00535510 |
+0x10 |
position (float[4]) |
0 |
+0x20 |
orientation quaternion (x, y, z, w); rotates the camera's local axes into the world: +y is the view direction, +z up, +x right (see below) | identity |
+0x30 |
name (15 characters) | |
+0x40 |
the camera's handle (a slot from 0x3c to 0x6b) |
|
+0x44 / +0x48 |
field of view, degrees / the one used (+ 0x0050b178, halved in split mode 0) |
60 |
+0x4c |
aspect | 0x0050b204 |
+0x50 / +0x54 / +0x58 |
near plane / own far clip / draw distance | 0.3 / 60 / 60 |
+0x5c-+0x68 |
view window (0x00120a98) |
|
+0x70-+0xc0 |
six frustum planes (normal, distance), inward | |
+0xd0-+0x14c |
near and far corner points | |
+0x150 / +0x17c |
bounding sphere centre / radius | |
+0x180 |
look-at point | 0 |
+0x1a0 |
the last update's dt |
|
+0x1cc |
a script reference (0x001221a0, inferred: the end callback) |
0 |
+0x1d4 / +0x1d5 |
the ground under the view has type bit 0x20 / 0x10 (0x00121720) |
0 |
+0x1d6 |
the view may show | 1 |
+0x1d8 |
the player it belongs to (−1 none) | −1 |
Axes. Confirmed (code). Every camera class places itself with Mat_LookAt (0x00337028, eye, target), which
calls Mat_LookAtUp (0x00337168). That writes a row-per-axis matrix:
| Row | Axis | Value |
|---|---|---|
+0x00 |
local +x, right | normalise(forward × up) |
+0x10 |
local +y, forward | normalise(target - eye) |
+0x20 |
local +z, up | right × forward |
The up hint is world +z (0x00511700), or (1, 1, 1) normalised when the forward's z is beyond ±0.98; eye and
target within 0.005 m return 0 and leave the camera as it was. The rows' w and the matrix's position are zeroed.
Mat_ToQuat (0x003365c8) turns that matrix into the quaternion at +0x20. It treats the rows as the rotated
axes (x = (m12 - m21) / 4w with m[row][col]), so the quaternion maps local +y to the view direction. This is the
same convention as the scene cameras.
Class fields after +0x1e0, confirmed (code) at the constructors and setters:
| Class | Fields |
|---|---|
Cam_3rdPerson (0x230) |
+0x1e0 target; +0x1f0 angle quaternion; +0x200 eased orientation; +0x210 distance (5.1); +0x214 height (1.8); +0x220 look-at offset (0, 0, 1.5); field of view 50, near 0.5, far 75 |
Cam_Failed (0x200) |
+0x1e0 the human; +0x1e4 the camera before it; +0x1e8 height (5); +0x1ec turn rate (1, × 15°/s); +0x1f0-+0x1fc values CamUseDeathCamera saves; +0x1fe rotating |
Cam_Fixed (0x210) |
+0x1e0 offset added to the look-at point (CameraCreateFixed's offset, w = 1); +0x1f0 the aim point: CamFixed_ComputeLookAt (0x00124000) copies the new look-at point there, CamFixed_SmoothLookAt (0x00124248) damps it against last update's on its own, the shake moves it, and the camera faces it (0x00124690); +0x200 target; +0x204 snapped this update (no damping) |
The follow camera object¶
0x480 bytes. Fields written by the constructor and CfgFollowCamera, confirmed (code); "runtime" values are from
level99 with the player standing, after the tutorial script's own calls.
| Offset | Meaning | Constructor | CfgFollowCamera in level99 (runtime) |
|---|---|---|---|
+0x180 |
look-at point (target position + offset) | player position + (0, 0, 1.4) | |
+0x200 |
the target's offset (set by CamSetupFollow) |
||
+0x1e0 / +0x1f0 |
the camera's position and orientation at the end of the last update (copied from +0x10 / +0x20) |
||
+0x240 |
the aim point the view faces | look-at + 0.36 m along the facing, at the body's height | |
+0x250 |
the wanted position (after the leash and the rules that turn it) | ||
+0x260 |
the held position: the wanted position after the lag and the hard band, before collision | ||
+0x270 |
last update's look-at point | ||
+0x2b0 |
the position the collision step returned on the last update ("allowed") | ||
+0x2e0 |
the eased copy of the target's rotation the aim point leads along (35% per update) | ||
+0x210-+0x21c |
look-at offset from the target's feet | (0, 0, 1.4) | (0, 0, 1.4) |
+0x300 |
minimum distance | 4 | 3 |
+0x304 |
maximum distance | 9 | 6.6 |
+0x308 |
default distance (also written to +0x344 and +0x32c) |
6 | 4.8 |
+0x30c (and +0x3b4) |
pitch, radians | 15° | 0.2269 (13°) |
+0x310 (and +0x394) |
field of view, degrees | 65 | 65 |
+0x32c / +0x330 |
the leash band: default and default + min(0.5, max − min) (also +0x344 / +0x348) |
6 / 6.5 | 3.0 / 3.5 at checkpoint 1; 4.8 / 5.3 in the street, and 3.0 / 3.5 while sprinting (In the street) |
+0x320 / +0x3fc |
handle of a human or object the camera keeps in view (CamSetSecondary) / its range |
||
+0x3cc |
the band's near edge saved by the combat camera (0 for none) | 0 | |
+0x46f |
the combat camera is on | 0; 1 with L1 held at a target | |
+0x33c / +0x340 |
the hard band: the leash band widened by max(5%, 0.2 m) and max(6%, 0.35 m) | ||
+0x34c |
the band's wanted near edge: +0x32c eases toward it (Sprint zoom); −1 for none |
3.0 while sprinting, else −1 | |
+0x36c |
seconds the target has run or sprinted (zeroed when it stops on the ground) | ||
+0x3c8 |
a band saved by another zoom (0x00126558 / 0x00126878, not the sprint); while it is set the sprint zoom leaves the band alone |
0 | 0 |
+0x3e0 / +0x3e8 / +0x3e4 |
the sprint zoom's saved band near edge, zoom distance and target pitch (+0x3e4 −FLT_MAX for none) |
4.8 / 6.6 / 13° | |
+0x448 |
sprint zoom state: 0 off, 1 zoomed in, otherwise the game time (ms) at which to zoom back out | 0 | |
+0x45b / +0x45c / +0x45d |
+0x45b: the collision step's main ray was blocked this update; +0x45c: a sticky copy of it; +0x45d: a sticky "the view was blocked" latch. The latches hold until the player stops, and auto-follow stays off while they are set (Heading) |
0 | 0 in the street; 1 for the whole run at checkpoint 1 |
+0x466 / +0x467 / +0x468 |
sprint zoom latched / armed for this sprint / enabled (CamEnable(5, on)) |
0 / 0 / 1 | |
+0x474 |
auto-centre latch: cleared while the stick points more than 157.5° from up, set again when the target moves | 1 | |
+0x350 |
heading target (a direction; −FLT_MAX for none) | ||
+0x354 |
pitch override (−FLT_MAX for none), set only by aiming (0x00128678) and TurnToMatrix (update step 16) |
||
+0x370 |
hold timer of a reverse button shared with the zoom button (one per pad) (buttons) | ||
+0x47c / +0x3c0 |
queued hand-over: the camera to make current when the timed move +0x40c ends, and the blend time (s); set by TurnToMatrix (mini-game and mugging cameras) |
0 / 0 | |
+0x3d4 |
target pitch saved by ReactToLedge (−FLT_MAX for none) | ||
+0x35c |
band near edge saved while looking behind | ||
+0x455 / +0x458 / +0x459 |
view pulled out behind / pad holding the reverse button (−1 for none) / drop the reverse hold (looking behind) | ||
+0x453, +0x220 / +0x230, +0x324 |
height hold: on, the stored face's normal and point, the held height | ||
+0x464 / +0x471 / +0x479 |
collision: wall push allowed (set with +0x462 / +0x463 by 0x0012fd20) / a given-up turn found one side open / full-block turn latch (0, 1, 2, −1) (collision) |
||
+0x358 |
right-stick yaw rate, rad/s | ||
+0x368 |
stick hold timer: 0.334 s after any camera input | ||
+0x380 |
recovered distance (eases 10% per update) | ||
+0x388 |
position lag: share of the wanted move covered per update | 0.22 | |
+0x398 |
scale of the height hold's 30% ease (1 normally; 0.25 after 0x00125888) |
1 | |
+0x3ac |
an upper pitch limit | 50° | 30° at checkpoint 1; 40° in the street (band 4.8-5.3) |
+0x3b0 |
a lower pitch limit: atan((1 − offset.z) / max), at least −20° |
−20° | |
+0x3b8 |
right-stick pitch rate, rad/s | ||
+0x400 |
zoom step: the preset the zoom button goes to next (the default while the band is at the minimum, the maximum while it is at the default, the minimum while it is at the maximum) | 6.5 | 4.8 at checkpoint 1; 6.6 from checkpoint 2 |
+0x40c |
timed-move timer, seconds (heading target, pitch override, and the sprint zoom's 0.5 s) | ||
+0x434 / +0x438 |
side factors capping the stick yaw: 1, or −1 on the side of a wall push (right stick) | ||
+0x444 |
number of targets | ||
+0x418 |
the aim point's lag, worked out each update from 0.06 | 0.06 | 0.061 standing, 0.14-0.16 running |
+0x440 |
the current distance | 1.85 standing (runtime) | |
slot +0x1a4 |
near plane | 0.1 | 0.1 |
0x005148a0 (global) |
slow-motion factor | 0.2 |
The far clip is 115 for this camera (The streamed world).
level99's values¶
CameraCreateFollow passes the global.lua constants (Scripts): minimum 3, maximum
6.6, default 4.8, pitch 13°, field of view 65, near plane 0.1, offset (0, 0, 1.4), slow motion 0.2. Confirmed
(runtime) in the object above.
Behaviour¶
Setting up¶
CamSetupFollow(name, target)(0x0011bfa8): get the player'sCam_Follow, store the target (slot+0x1bc), place the camera at the target's position plus the offset at+0x200, call0x00124f38(180, -1)(a reset of its heading and state, inferred) and return the camera's handle. Confirmed (code).CfgFollowCamera(min, max, default, pitchDegrees, fov, near, {offset}, slowmo)(0x0011c0b8, player 0's follow camera only): the three distance setters (each clamps against the others; the default also sets the band to default .. default + min(0.5, max − min); the maximum recomputes the lower pitch limit+0x3b0), the pitch as both the configured and the target pitch, the field of view as both the configured and the wanted one, then0x00125888: the view is turned to the pitch at once and the field of view eases to the new value over 1 s (+0x39c= the difference per second). Then the offset (+0x210), the near plane (slot+0x1a4) and the slow-motion factor0x005148a0. Last, with one player camera, the band is moved to the minimum (3.0-3.5 m) and the zoom step to the default, exactly asCamSetFollowZoom(0)(Script calls); with two, asCamSetFollowZoom(1). Confirmed (code); confirmed (runtime): every checkpoint 1 state read band 3.0 / 3.5, zoom step 4.8, upper pitch limit 30°.CameraMakeActive(camera, seconds, ...)(0x0011b770→0x0011ee08): with 0 seconds the camera becomes current at once; otherwise the blend camera (type 5,0x0011fac8) runs between the old and the new one (Blends).CameraReset(0x00365a10) then calls the camera's slot+0x13c. Confirmed (code).- Activation (slot
+0x144,0x00125cc0, whenever the follow camera becomes current directly, including at the end of a blend): it recounts the targets, snaps the look-at point (no ease), keeps the camera's direction from the look-at point but puts it at its distance clamped to the band, sets the hard band to the band, clears the wanted near edge+0x34c(−1) and sets the recovered distance+0x380to the band's near edge. Confirmed (code).
CameraReset on the follow camera (0x00124d00(camera, 1)), confirmed (code): 0x00124f38(180, −1) places the
camera behind the target (heading 180° from its facing, inferred from 0x001250a8) at a distance picked from the
current one clamped to the band: the minimum when it is at most halfway from the minimum to the default (zoom step
then the default), the default when at most halfway from the default to the maximum (zoom step the maximum), else the
maximum − 0.5 (zoom step the minimum); then the previous look-at points are set to the current one, the wanted near
edge cleared, the target pitch set back to the configured pitch (13°) and reached at once, the wanted field of view
set back to the configured one (over 1 s), and a line-of-sight test from the target pulls the camera in if the world is
in the way. It does not move the band. For a follow camera the other player's follow camera is reset too.
The follow update¶
0x0012ae58 runs once per character step (30 Hz; it stores its dt argument at +0x1a0, where the heading rules read
it) for each follow camera. It is a leash camera: the camera does not sit at a fixed spot behind the player but
stays where it is until the player drags it, and a few separate rules turn it. The steps below are in the order the
code runs them (0x0012ae58-0x0012d3a0); confirmed (code) at the cited addresses unless marked. Angles are given
in degrees, the code holds radians. "One target" means one human in the camera's target list (+0x444 = 1), the
single-player case.
- Targets and states. The target list is gathered (
0x001282a0); with one target the update reads, for the steps below, whether the player is idle, walking, running or sprinting (the run timer+0x36cgrows bydtwhile he runs or sprints), whether he is tagging, in a fight stance, or hidden in shadow, and sets the aiming flag+0x454while his control function is the throw-aim one (Human_MoveThrowAim) with his+0x256set and none of state flags0x1c00000000. Two factors passed to the collision step are 2.0 and 0.25 (1.9 and 0.3 × scale with a target: a probe height and a sphere-radius factor, inferred). - Right stick and buttons (
0x00129050, below): yaw rate+0x358, pitch rate+0x3b8, the zoom, centre and reverse buttons, and the head-look angles of step 17. - Bookkeeping.
+0x46e= the player is in a fight stance that counts for the camera; the sprint-zoom latches (Sprint zoom); and the saved pitch+0x3d4(kept by ReactToLedge) is put back into the target pitch+0x3b4once the player no longer has state flag0x1800000000(falling or a long fall, Characters): after he lands. - Previous look-at (
+0x180copied to+0x270), then the band eases toward a wanted near edge (0x0012aae0, the sprint zoom) and the stick hold timer+0x368counts down bydt. - Look-at point (
+0x180,0x00127d88, called with 0 from the update at0x0012b740): the target's position plus the offset (runtime: feet + 1.4 m). The offset itself eases toward its wanted value by 15% per update (+0x200toward+0x210). Unless the target is jumping or falling (0x00227f90), the look-at point's move this update is limited by its lengthd(in 3D, from the previous look-at point+0x270): above 0.8 m it moves 20% of the way; from 0.4 to 0.8 m it moves1 − 2 × (d − 0.4)of the way (100% at 0.4 m, 20% at 0.8 m); below 0.4 m all of it. So a climb's rise of 1.3-2.6 m is followed at 20% an update until 0.8 m is left, then in two more updates, and a jump is followed directly. While the player is hidden in shadow (record flag0x200000, tested by0x00228168, set byBrain_SetHiddenInShadow0x0028ee88→0x0022ff88) and not running or sprinting, with one target, the look-at height is instead set straight to feet + 1.75 − 0.1 = 1.65 m (no ease;0x0012b3fc-0x0012b7e0). While aiming the look-at point is the player's aim origin (player record+0x5750).level99has no hiding spot on its path (inferred). - Field of view eases toward
+0x394at+0x39cdegrees per second, at most 7.5, or over the timed move's remaining time+0x40cwhen one runs (then|fov − wanted| / +0x40cper second). - Reset when lost (only with one player camera,
0x0050b19c= 1): when the held position+0x260is more than 1.3 × the maximum distance+0x304from the look-at point, or the camera is more than 30 m above or below it,0x00124d00puts the camera behind the player asCameraResetdoes. - Look-behind (
0x001298c0, below) while the reverse button is held, and on the update it is let go. - Framing rules, only while the stick has been still for 0.334 s (
+0x368= 0) and the view is not pulled out behind (+0x455= 0), or while a hand-over is queued (+0x47c): the combat camera (player 1 with the lock-on button,0x0012e9a8), else keep in view of the fight target (0.4) or of a secondary target (+0x320, 0.25); a queued heading from+0x43c(+0x462: the heading target+0x350=+0x43cwith a 0.1 s timer, and the band moved to the default distance when it was at the minimum); then auto-follow (0x00129c78, Heading) when nothing above framed the player. - Leash to the distance band
+0x32c-+0x330: when the distance from the wanted position to the look-at point leaves the band, the wanted position is moved along that line back to the nearer edge.level99's band is 3.0-3.5 m at checkpoint 1 and 4.8-5.3 m in the street. - Pitch toward its target
+0x3b4(0x0012d4e8, a rotation about the look-at point), skipped in the height hold; see Pitch. - Height hold
+0x453(below): the probe0x0012a7d8enters or leaves it; while it is set the wanted position's height moves 30% (close zoom), 40.5% (default) or 48% (far) of the way per update toward the look-at height plus the height it held (+0x378++0x324), times+0x398(1, or 0.25 after0x00125888).0x00125588reads the zoom level from the zoom step+0x400(with one player camera: 2 far when+0x400≤ the minimum, 1 default when+0x400> the default, else 0 close). Confirmed (code) at0x0012c0c0-0x0012c14c. - Aiming (below): while
+0x454is set,0x00128678sets the heading target, the pitch limits and a pitch override every update. On the first update after aiming ends the wanted near edge+0x34cgoes back to the distance saved in+0x3cc, the lower pitch limit to max(−20°, an angle of (1 − offset z) / maximum distance,0x004b8d30, inferred an arcsine), the target pitch into the limits, and the timer+0x40cto at least 0.35 s. - Placed heading
+0x3f0(inferred: a scene's hand-back): with one target, no stick turn this update and+0x3f0set (with+0x465, only while the player runs),0x001250a8puts the camera at that heading round the look-at point at the far edge of the band, every update. - Heading target
+0x350(the centre button, ReactToLedge, aiming,TurnToMatrix,+0x43c): when set, the camera yaws about the look-at point by the angleabetween its horizontal forward and the target heading, or bya × dt / +0x40cwhen the timer runs and that is smaller, toward it; the target (and+0x43c) is cleared onceais within 0.1° (0x3ae4c389is 0.1° in radians, passed to the cosine). Without a heading target the right stick's yaw rate turns the camera by+0x358 × dt(0x0012d3b8) and the rate is cleared. So a heading target overrides the right stick. - Pitch (Pitch): the sprint zoom (
0x00128cf0) when its latch is due; then, with a stick pitch rate, the target pitch moves by+0x3b8 × dtwithin the limits (with the slope flag+0x460= 1, also kept on the side of+0x3a4that its sign gives) and the camera pitches byrate × dt; without one, a pitch override+0x354(when above −FLT_MAX) is reached: with the timer+0x40c= 0 at once (when not aiming) or at 10°/s (when aiming, cleared within 0.001°), with the timer by(override − pitch) × dt / +0x40c; the target pitch follows the camera's pitch meanwhile. With neither, outside the height hold, the pitch returns to its target at most 85°/s (1.4835rad/s). - Head look (
0x00128b20, below) while the stick gave a head angle, with one target, the player idle and not in a fight stance. - Position lag, only when there is no heading target and
+0x46b(set byPlaceBehind) is clear, and not while the reverse button is held unless the view is pulled out:wanted = held + (wanted − held) × +0x388, withheldthe position+0x260of the last update.+0x388is 0.22 normally (0.27 after a blocked collision, chosen by the collision step of the previous update), 0.8 while aiming, and 0.05 while the player is in a task of type 11 (inferred: a grapple) with no other input. When the move is under 10⁻⁵ m but the stick pitches the camera, only the height moves, by max(+0x388, 0.35). - Hard band: the wanted position is then kept between
+0x33cand+0x340from the look-at point: the leash band widened by max(5% of the near edge, 0.2 m) and max(6% of the far edge, 0.35 m); when they shrink they ease back by 1% per update. In a task of type 11 the band is the leash band × 0.85 and × 1.2; on the first update the player has state flag0x1000000000(a long fall) the far edge becomes min(2 × far, 1.1 × the maximum distance) and the near edge max(itself, 0.85 × near). The result is the held position+0x260. - Tilt over camera obstacles (
0x0012ec50), then world collision (0x00130990, below), which also moves the aim point+0x240; the camera is placed (vtable slot+0x1bc) at the position it returns, that position and the orientation are copied to+0x1e0/+0x1f0(the camera of the last update, which the next update's collision step reads), the shake is applied (0x00121298), and the camera is turned to face the aim point+0x240withMat_LookAt(0x00337028), not the look-at point.Mat_LookAtfails only when the eye and the aim point are within 0.005 m; the position is then put back and the orientation kept. There is no line-of-sight test here. Then the roll eases (0x0012ac58) and the view frustum is rebuilt. - Timers:
+0x40ccounts down bydt; at 0 the timed move ends (+0x475and+0x43ccleared) and a queued hand-over runs: when+0x47cholds a camera,Camera_MakeActive(0x0011ee08) makes it current for this camera's player, blending over+0x3c0seconds, and both are cleared (0x0012d07c-0x0012d0c4, confirmed (code); see Mini-game and mugging cameras).+0x410and+0x414count down. A fade timer+0x424sets the alpha byte+0x1b4of the two objects at0x005fdeb8and0x005fdebctomin(10 × t, 1) × 225(inferred: the player models fading when the camera is close).+0x46band+0x469are cleared.
The aim point¶
The view does not face the look-at point: it faces an aim point +0x240 that leads the player a little along his
facing and rises and falls with his body, eased twice. The collision step moves it each update (0x00130990, at
0x00133320-0x00134290); confirmed (code) there and confirmed (runtime) through the fence_climb scenario (below).
With one target that is a player (with any other target, only a 1.0 m sphere push of the old aim point, when switch
0x0050b1c0 is 0):
- Lead.
+0x2e0, a copy of the target's rotation, is slerped 35% of the way to it each update (snapped while the target does not count for the cameras, while the camera aims,+0x454, or while the target plays clip 1). Its forwardf, the view's directionv(from the camera of the last update+0x1e0to the look-at point) andL = max(0.75, 0.15 × band near edge):Lis scaled by allowed ÷ wanted distance when the world holds the camera in, and by 0.4 while the target has state flag0x40or0x80(mask0xc0: he is grabbing someone from the front or the rear, Combat). A world ray (WorldManager_RayCast0x0040db88, at0x00133604) from the look-at point alongf,L − 0.5 × L × |f · v|long, gives the lead pointP= look-at +f× (the free length − 0.01). So running away from the camera the aim point is about 0.36 m ahead of the look-at point (0.375 − 0.01 at the default band; runtime 0.363 standing, 0.365 running). - Height. With
d= the human's body point height+0x4e8− 1.0 (the body point+0x4e0is the hips' position in the body's frame, runtime 1.035 m standing, 0.93-1.0 running, 2.37 at the top of a fence vault, 0.62 landing). While the target runs or sprints (below) this step is replaced. Otherwise: nothing (Pstays at the look-at height) while|d|≤ 0.27, unless the human has any of the state flags0xe2c00:0x400tackling (on top of the victim),0x800being tackled,0x2000(not named; paired attacks read it),0x20000arrested,0x40000knocked out,0x80000knocked down (Combat, Characters,Human_EndDownState0x0025e250), so on the ground or in a tackle the height follows the body with no dead band. Otherwisedis scaled bymax(0, (0.27 − |d|) × 0.9 / 0.27 + 0.1)when|d|≤ 0.27, bymin(1, (|d| − 0.27) × 0.9 / 0.27 + 0.1)up to 0.54, and kept beyond; a negativedis lowered by(offset z − 1) × 0.6more when the look-at offset is above 1.25 m (0.24 inlevel99). The height goalzt= look-at height +d; the new height is the old aim point's height moved 35% towardzt, but where that does not lie between the look-at height andztit isztitself; the result is stored as the look-at point's height+0x188(0x001339c0). (With+0x465, the target takeover of0x00126558, no dead band and 75%.)
Running and sprinting (0x00133674-0x001337d0), confirmed (code): +0x450 is a switch the constructor sets
to 1 (0x00124940, its only writer), so it is always on. The update (0x0012b310-0x0012b3f4) passes the
collision step two flags of this update: sprinting (gait +0x1a8 5 with no blocking record flags,
0x00223a98) and running or sprinting (that, or gait 4, 0x00223a60); both are 0 under the takeover +0x465.
While either is set the height step above is skipped and the old aim point is moved instead:
- running (gait 4): by 0.22 × (+0x4e0 − (0, 0, 1)), the body point's offset as it is, not turned by the
human's heading; the sway +0x3f8 is set to 0. So the aim point bobs with the hips: 0.22 × (0.93-1.0 − 1), a
little below its old height each update;
- sprinting: a sideways sway. The sway +0x3f8 moves by +0x3f4 × 0.028 × a random number in 0.125-0.25
(0x00335420), 0.0035-0.007 m an update; past ±0.028 it is held at +0x3f4 × 0.028 and the direction +0x3f4
(1.0 at construction) flips. The aim point moves sideways by the sway along the view's horizontal side axis (the
unit cross product made at 0x00130b68) and up by (2 × |sway| − 0.028) × 0.6, so between −0.017 and +0.017 m.
3. Push. Unless the camera aims, P is pushed out of the world by a sphere of radius 1.0 (0.95 with two targets ×
the target's scale; CollisionMesh_SpherePush).
4. Lag +0x418, worked out afresh each update from 0.06: × (1 + (1 − c) × 0.5) where c = |unit(velocity) ·
camera forward|, both in 3D (the velocity's z too), once the target is faster than his walk speed (else c =
1); + s × it × ((1 − c) × 0.5625 + 0.1875) with s = (horizontal speed − walk speed) ÷ sprint speed (≥ 0; s
is scaled by (0.75 − +0x38c) × 4/3 while the timer +0x38c runs down, set to 0.375 s after a blocked view clears
and 0.75 s after a sphere push ends); × 2.5 when the target's 3D speed is above walk + 18% of (run − walk) and
either the new aim point would end farther from P than the look-at point is, or the stick is not his (+0x1b of
the per-player record 0) (below); × 1.25 when slower and the latches +0x45b/+0x45d are set
on a slope (+0x460). Then +0x418 = last update's value + 3% of the difference; 0.6 while the human has state flag
0x1000000000 (a long fall); at least 0.8 when the main ray is blocked while aiming; at least 0.3 during a takeover;
halved while the target does not count or the game mode is not 1. Runtime: 0.061 standing, 0.14-0.16 running.
5. Move: +0x240 = old + (P − old) × +0x418 (when the main ray is blocked by a wall whose normal is nearly
level, by min(1, (3.5 − the feet's distance in front of it) × 0.4) × 0.3 instead); then, if that left it farther
from the look-at point than P is, it is pulled back in plan to P's distance.
Steps 4 and 5 in detail (confirmed (code), 0x00133b40-0x00134180; old is the aim point +0x240 before
this update, P the pushed lead point):
- The tests in player terms. "The target does not count for the cameras" is
Camera_HumanCounts(0x00123500) returning 0: he is down or dead (Human_IsDownOrDead0x00227e60: dying, dead, knocked out or tackled, state0x180050000; not knocked down), or he is cuffed (Human_IsCuffed0x00223b70, state0x20000) and cannot free himself (he still counts with upgrade (6, 15) unlocked and a key, item 6, carried, Crimes). "The game mode is not 1" isGameModeStack_TopId(0x0015e748) ≠ 1: another mode is on top of gameplay (Boot). The long fall (state flag0x1000000000, read only for a player, human+0x1b0≠ −1) is set byHuman_BuildFallClip(0x0025d020) when a fall (anim state 26) starts while he is knocked down, knocked out or cuffed (state0xe0000), dying or dead, or already falling (held flag0x400000): he goes over an edge limp, as when knocked or thrown off a ledge, and plays the long-fall cycles 422 / 425. - The timer
+0x38c. Set to 0.375 s when the main ray hit a face this update (its hit plane is not zero) but the hit does not hold the camera (+0x45bclear) while the sticky copy+0x45cis still set; set to 0.75 s when the main ray hit a face this update and the step-3 push ofP(the 1.0 mCollisionMesh_SpherePush, which returns whether it touched a wall) touched nothing this update but touched (or did not run, while aiming) on the last (+0x45a, this update's result kept at the end of the step). With any other target the push in question is the 1.0 m push of the old aim point. It counts down bydtonly whiles> 0 (the target faster than his walk), so it waits while he stands. - The overshoot test (the ×2.5), confirmed (code) at
0x00133dcc-0x00133ec8and confirmed (runtime) by thefall_hightrace (Long falls): the speed it compares is the 3D speed (sp+0x35c,vmul.xyzat0x00130f90; thesabove uses the horizontal one,sp+0x358,vmul.xyat0x00130f68). Withnew=old+ (P−old) × the lag being built, it compares squared 3D distances: (|P− look-at|² > 10⁻⁵ or the slope latch+0x460) and |P− look-at|² < |P−new|², so the lag is × 2.5 whenever this update's aim point would still be farther fromPthan the look-at point is (about 0.36 m, the lead); only when that fails is the per-player record's+0x1bread (0: × 2.5). (This page used to read the first distance as |P−old|, which made the test never hold; the subtrahend at0x00133df4is the pointer atsp+0x3b4, the look-at point+0x180.) The ×1.25 needs the target at or below walk + 18% of (run − walk), the latch bytes+0x45bor+0x45dset (+0x458&0xff00ff000000) and the slope latch+0x460. - The blocked-wall share replaces the lag in step 5 when the main ray's hit holds the camera (
+0x45b), the camera does not aim, and the hit face is steep: |normalz| < 0.6 (0x005d91cc, steeper than 53.13°). Its "feet's distance in front" is the target's transform position (his feet) against the hit face's plane:n · feet − d, withnanddthe plane of the main ray's hit (the ray from the look-at point toward the camera). The share ismin(1, (3.5 − that distance) × 0.4) × 0.3: 0.3 within 1 m of the wall, falling to 0 at 3.5 m. (Aiming, the lag is at least 0.8 instead.)
The camera's yaw and pitch therefore lag the look-at point: the trace's cam_pitch (camera to look-at) is not the
pitch of the view; the view's pitch is camera to +0x240.
The body point¶
A human's +0x4e0 (+0x4e8 its height) is pose bone 2 of his skeleton (HAnim node 0, the hips: the parent of the
spine and of both legs, Animation), its position in the model's frame: z up, the model facing
+y, relative to the feet and not turned by the human's heading, times the human's scale (+0x65c, 0.97 for
Rembrandt). Confirmed (code): each update Human_StateUpdate (0x0023fea8, vtable +0x13c, at 0x002400d4) calls the
human's bone transform (vtable +0xbc, Human_GetBoneTransform 0x0023bde8) with bone 2 and the destination
+0x4e0 (the 32-byte entry: position at +0x4e0, rotation at +0x4f0); in the same call it fetches bone 0 (the root,
for the root motion), 3 into +0x540, 5 into +0x520 and 6 into +0x500 (the head look's origin). The bone comes from
the per-update bone cache (Combat), built from the playing pose and already composed through
the parent table, so it is the animated hips, not the clip's root track. With no model instance (+0xd8 0) it is not
written and keeps its old value. Its position is the pelvis channel's: the idle's pelvis height 1.067 × 0.97 = 1.035,
the runtime value standing (inferred from that match).
When: in the human's state update, after the state function and the head look and before the root motion and the move. The cameras update at the end of the same 30 Hz step (Coney), so the camera reads this update's value; whether the clip time has already been advanced in this step when the cache is built is not traced.
Long falls¶
User checklist item 48 asked what the follow camera does when the player falls from a great height. There is no
long-fall camera for a plain fall: the long-fall state flag 0x1000000000 (aim lag 0.6, update step 19's wider hard
band) is set only when a fall starts limp or while the drop's held flag 0x400000 is still set, and a 40 m fall
does not set it. What keeps the view on the player is the aim point's lag, which climbs from 0.06 toward
about 0.21 because a fall is fast in 3D.
The trace (confirmed (runtime), PCSX2 2.9.94, 2026-10-10, scenarios repo:research/traces/scenarios/fall_high.toml
and fall_high_stick.toml, field set fall of repo:research/traces/fields.toml): slot 1, Rembrandt in level99's
street; at frame 5 his feet are put 40 m up and he falls for 71 updates (2.4 s), to 34 m/s. Every update:
- State: the drop starts at once (clip 428, anim state 26 by
Human_BuildFallClip0x0025d020, held flag0x400000for 4 updates, state0x800000000falling). The state word's0x1000000000(state_hi0x10) is never set, with the stick at rest or at 45 %: the fall clip is built once. The landing gives state0x80001(knocked down) and clip 294 with the stick at rest, the roll 436 with it held. - Look-at point
+0x180: follows the feet + 1.4 m exactly, every update (step 5). - Aim lag
+0x418: from the update the fall passes about 3 m/s, the value built each update is 0.06 × (1 + (1 −c) × 0.5) × 2.5 = 0.211-0.224 (c0.03-0.2: the fall runs across the view; the ratio to 0.06 × (1 + (1 −c) × 0.5) is 2.50 ± 0.03 at every sampled update); eased 3% an update, the stored lag reads 0.061 (step 12), 0.077 (18), 0.121 (30), 0.161 (48), 0.188 (72). The ×2.5 is the overshoot test's: the per-player record's+0x1bis 1 throughout. - Aim point
+0x240: trails above the look-at point by 0.23 m (step 12), 1.6 (24), 2.7 (36), 3.4 (48), 4.1 (60) and 4.7 m at 34 m/s (72): about v / 30 × (1 − lag) / lag. - Camera, the stick at rest: 4.4-5.4 m from the look-at point, pitched 13° above it at the start, 25° (step 30), 38° (48) and 51° (74), as its 22 % position lag leaves it above the falling look-at point; the view's pitch (camera to aim point) goes from −13° to +10° (+14° on the last update in the air). No reset behind the player (step 7's 30 m limit is not reached once falling), and the hard band only eases in (far edge 5.65 to 5.01 m).
- Camera, the stick at 45 %: the target pitch drops to 7° and the camera keeps 3.5 m from the look-at point, near its height, so the aim point's trail tilts the view up: +16° (step 30), +38° (42), +46° (54), +55° (72). The original looks up a good way too in a long fall with the stick held, but its view stays under about 55°.
Coney's difference¶
Inferred, from repo:src/camera/follow_camera.cpp (stepAim()) and repo:src/human/player.cpp
(followTargetOf()): Coney builds the lag from the horizontal speed only. c is 1 while the horizontal speed is
under the walk speed, and the ×2.5 needs !stickOwned and a horizontal speed above walk + 18% of (run − walk), with no
overshoot test. In a straight fall the horizontal speed is about 0, so the lag stays 0.06, and at 35 m/s the aim
point settles about 1.167 × 0.94 / 0.06 ≈ 18 m above the look-at point (11 m at 20 m/s) instead of under 5 m. With
the camera 3.5-5 m from the look-at point the view then faces nearly straight up: the user's "camera flies up".
Coney's longFall (airborne while not counting or grounded) is not the cause: the original does not set it on a plain
fall either. (Coney's headless --trace could not show this: with the release build of 2026-10-10 the trace's camera
columns stayed constant even in the walk60 scenario.)
The fix for the implementer: in stepAim(), take c from the 3D velocity against the camera's 3D forward, with its
gate on the 3D speed; keep s on the horizontal speed; gate the ×2.5 on the 3D speed and apply it when |P −
look-at|² > 10⁻⁵ (or the slope latch) and |P − look-at|² < |P − new|² (new = the old aim point + (P − the
old aim point) × the lag being built), else when the stick is not the player's. Test: a synthetic follow-camera test
with the target falling straight down 1.167 m an update (35 m/s) for 70 updates from rest: the lag passes 0.18 and the
aim point stays under 5 m above the look-at point (the trace: 0.188 and 4.7 m at step 72), and the view's pitch with
the camera held 3.5 m away at the look-at height stays under 60°. The overshoot test also applies at a run, so rerun
the walk60 and run_circle diffs afterwards (the runtime lag there is 0.14-0.16).
Climbs¶
The follow camera has no climbing mode: the climb start (Climb_TryStart 0x002826f0,
Characters) calls no camera function (confirmed (code) for its direct callees), and the only
traversal hooks are the drop (Ledges), the landing shake and the long-fall far edge (update step 19). There
are no ladders or drainpipes: the executable holds no such names, and climbing is the fence and wall climb (inferred
from the string search and the climb code). A climb is followed by the ordinary rules: the look-at point's distance
limit (step 5) and the aim point.
Does the original's camera jump when the player climbs? No, the climb itself moves it smoothly; one level spot
pulls it in once. Confirmed (runtime), PCSX2 2.9.94, slot 7 (level99 checkpoint 3, the street fence), scenario
fence_climb (left stick 100 % straight ahead, triangle every 2 updates from 4.6 m before the fence), every update:
| Updates | Body | Camera |
|---|---|---|
| run (410) | 7.80 m/s, body point 0.93-1.0 m | moves 0.260 m per update with the player, 5.65 m from the look-at point (the hard band's far edge), pitch 10.8° |
| 440, the 2 snap updates | feet 1.11 m, then 1.17 m forward in one update each | look-at point moves 0.22 m, then 0.41 m (20% of the gap, step 5); the camera 0.22 m, then 0.41 m: no jump |
| 440, to the fence | the clip carries the body 0.18 m per update | camera 0.37, 0.33, 0.30, … 0.22 m per update as the look-at point catches up; its distance stays 5.65 m |
| 440's last update | the body point rises to 1.49 m | the camera moves 0.91 m in one update (5.65 → 4.93 m from the look-at point) |
| 441, over the fence | body point 1.59 → 2.37 → 1.79 m; feet stay at ground height | aim point height 1.67 → 2.23 m (eased: 0.03-0.05 m per update); camera moves 0.07-0.11 m per update and creeps back out (4.93 → 5.15 m) |
| 442, landing | body point 2.37 → 0.62 → 0.98 m | aim point height eases back down to 1.60 m over 10 updates |
The one 0.91 m move is the street's disabled panel (In the street): the camera, held 5.65 m behind, is
then 0.27 m behind the 2.65 m panel at y = 31.17 and comes to rest 0.25 m in front of it; from the next update
the main ray hits the panel (+0x290 read its plane, y = 31.17), because the camera of the last update is now in
front of it (World collision), and the camera stays held 4.9-5.4 m away, creeping out at about 3% of the
gap per update with the 0.27 lag. It comes from the level's data at that spot: in the same run without the climb
(stopping against the fence) the camera crossed the panel at 0.26 m per update with no pull, then moved 0.34 m
once, pulled in to 5.47 m as the player stopped. Which test pulled it in on 440's last update (the main ray was
clear on that update, +0x45b 0, while the blocked-view latch +0x45d was set) is not traced. What does move with
a climb is the aim point, which rises 0.6 m with the body during the vault and comes
back down over a third of a second, and the look-at point's 20% follow of the snap. A Coney camera that jumps on
every climb is missing these two eases, or pulls in on faces the original ignores.
Heading¶
The yaw rotation is done by 0x0012d688(angle), about the look-at point. Four rules call it, confirmed (code):
- Auto-follow (
0x0012a400), the default: let a be the angle between the camera's horizontal forward and the player's facing. Nothing happens below 22.5° or above 157.5°, so running towards the camera does not spin it. The rate is(a − 22.5°) × 2.667per second below 45° (0 to 60°/s), 60°/s from 45° to 135°, and above 135° it falls from 120°/s back to 60°/s at 157.5°. Each update turns bymin(a, rate × dt)toward the facing. - Auto-centre option (
0x00129f88, used instead when the per-pad option bytes0x0050b240and0x0050b248are both set; they are 1 in thelevel99save used at runtime and 1 by default, Feel): nothing below 22.5°; from 22.5° to 90° the rate is(a − 45°) × 2.444 + 45°per second (negative below 26.6°, so the step is then a small turn the other way; 45°/s at 45°, 155°/s at 90°); 200°/s (3.4907rad/s) from 90° to 100°; from 100° to 157.5° it falls linearly ((157.5° − a) × 2.435 + 60°) from 200°/s to 60°/s, unless the call's fourth argument is set, which keeps 200°/s up to 157.5°. That argument is the update's local atsp + 0x1d4, set at0x0012b2ac: 1 when there is one target, he is in a fight stance (Human_IsInFightStance0x00228340, state flags0x3) and he still counts for the cameras (0x00123500), else 0; confirmed (code). So a player squaring up gets the fast swing over the whole range. Beyond 157.5° the rule turns only when the target's record has state flag 4 (0x002265f0(target, 4), "has a target" in Combat, inferred there; the same flag can stand in for "moving").ais the angle between the target's facing (its rotation in the transform table) and the camera's own horizontal forward: vtable slot+0x224returns the orientation+0x20, whose y axis (Quat_AxisY0x003363b0), flattened and normalised, is the view as placed at the end of the previous update, toward the aim point+0x240, not the look-at point. The facing's y axis is used flattened but not normalised. The angle isπ/2 − atan(c / √(1 − c²))of the cosinec, with an atan (0x003356a0, a range-reduced rational form) good to about 10⁻⁵° (inferred: one of its constants sits in memory Ghidra cannot read). Confirmed (code); confirmed (runtime) in In the street. Each update turns bymin(a, rate × dt)(dt=+0x1a0) toward the facing. The cosine of each threshold is computed with0x004b8a70(cosine, inferred from the thresholds' use). - When either rule runs (
0x0012ae58, the call at0x0012bd80): one target (+0x444= 1), the target passes0x00123500(not in states0x180050000of record word+0x00), no camera flags in+0x460 & 0xffff0000, nothing watched (+0x320= −1), no yaw from earlier steps this update, and no camera input in the last 0.334 s (+0x368= 0). Inside0x00129c78(arguments read at the call,0x0012bd30-0x0012bd84), confirmed (code): the default rule needs "running": the target's gait+0x1a8is 4 or 5 with no blocking record flags (0x00223a60,0x00223a98). The auto-centre rule needs "moving": gait 2, 4 or 5 (walk, run, sprint;0x00223a40adds the walk), or state flag 4 of the record; not 0, 1 or 3, so not while the body moves slower than a walk or at a jog's speed. It also needs+0x474= 1 (cleared on an update whose stick vector in the per-player record,+0x00/+0x04, points more than 157.5° from +y, and set again at the start of any update whose gait is not 0, so it blocks only the updates with the stick pulled back that far; whether that vector is the camera-turned stick is not traced),+0x455= 0, no right-stick input, the top animation task's clip without descriptor flag0x8000(0x00175be8), none of the human flags0x18003ff0, and the collision bytes+0x45band+0x45dclear: the collision step sets+0x45bwhen its main ray from the look-at point is blocked (World collision), so auto-follow stops on the update after one in which the view was blocked.+0x45dis a latch: it is set at0x00132fa4when the step's local "view blocked" (sp + 0x364, set at0x001327a0from the blocked localsp + 0x368, which0x0013207c,0x001321e4and the swing-away give-up0x001325a4set) is true, and+0x45ccopies+0x45bthe same way (0x00132fb4). Both are cleared by0x00124778(0x00124980) and at the end of the collision step (0x00132fd0-0x0013314c) on an update whose view is not blocked (sp + 0x364= 0) when either the camera is no longer held in (its wanted distance from the look-at point,sp + 0xc0tosp + 0x20, is within 10⁻⁵ of the distance the step allows,sp + 0x348), or the allowed distance plus 3% of that gap reaches the wanted distance the step started from (sp + 0x34c), or the zoom button was pressed this update (the stick step's flag, the step's stack argument0x4c8). Otherwise the wanted position is moved out to the allowed distance plus 3% of the gap and the latches stay. Here "allowed distance" is the distance from the last look-at point+0x270to+0x2b0, the point the step returned on the last update, and the 3% step is capped at the distance the step started from. While the view is blocked (+0x45bor the local set) the wanted position is instead drawn to the allowed distance plus 4% of the gap, and the hold timer+0x414starts at 1.5 s if it is not running; on the clearing path it starts at 1.5 s (3 s while+0x460is set, 0.1 s with+0x463and a timed move). This is the final form of the rule (0x00132f5c-0x00133250, read in full). The recovered distance+0x380moves 10% of the way to the allowed distance at the end of every step (0x00134a18). Nothing else reads it: the constructor zeroes it and the activation sets it to the band's near edge, so it can be left out. Confirmed (code); the meaning of the locals is inferred. While the player runs, the leash keeps the wanted position at the band's far edge beyond what the walls allow, so the latches hold until he stops (runtime). So one blocked update keeps auto-follow off until the player stops (Runtime checks). Confirmed (runtime), slot 1, stick 100 % sideways: the rule turned 110-129°/s every update at gait 4; with+0x45bwritten to 1 before each of 21 updates it turned 0 on each of them, and 129°/s again on the next. The gaits explain what In the street saw: no turn in the walk and run start clips (gait 0-1 while the walk start moves at 0.76 m/s, 3 in the run start's middle) or the landing clip (4.23 m/s, gait 3), and a turn during the run start's first five updates and the run stop's slower updates (gait 2). - Keep the target in view (
0x0012e170(factor, range), called with 0.25 and+0x3fcfor the humanCamSetSecondarygives,+0x320, and with 0.4 in one fight case): when the target's direction from the camera is more thanfov × factorfrom the view's, the camera yaws 35% of the excess per update, at most 270°/s (4.712rad/s), toward it. With a range above 0 it acts only while the target is within the range and a ray (mask0x200) from the look-at point reaches it;level99passes 0, so no range and no ray. It runs only when nothing else turned the camera this update,+0x463and+0x454are clear, and it replaces auto-follow, which needs+0x320to be NilHandle. Confirmed (code). - The right stick (rate
+0x358) and the heading target (+0x350, step 15 above).
Pitch¶
- The target pitch
+0x3b4is 13° inlevel99(CfgFollowCamera). - The lower limit
+0x3b0 = atan((1 − offset.z) / far), at least −20° (0x0012d7a8, run when the distance band moves); the target pitch is raised to it. - The upper limit
+0x3acfollows the zoom step (0x001254f0): 50° at the minimum distance, 40° above the default, and 30° at the default when0x0050b19cis 1 (50° otherwise).0x0050b19cis not an option: it is the number of player cameras, counted by0x00122ed0(written at0x00123248) over the camera slots from0x005d9148up to the player count (*(0x0051489c) + 0x224), so 1 means one player and the 30° limit is the single-player case. Beside it,0x0050b198is the number of split-screen views in use and0x0050b1a8/0x0050b1aathe views' grid (from0x0050b1a0). Confirmed (code). The same function sets the zoom distance+0x400to the minimum, maximum or default. - Without input the pitch is driven back to the target at most 85°/s (step 16).
Sprint zoom¶
While the player sprints, the camera pulls in to the minimum distance and lowers its target pitch to 7°, and both go back 250 ms after the sprint ends. Confirmed (code) at the cited addresses and confirmed (runtime) in the street save (slot 1, stick 100 % and L2, every field below read every update; PCSX2 2.9.94):
- Detecting the sprint (in the update,
0x0012b310-0x0012b3f4): the target's stored gait+0x1a8is 5 with no blocking record flags (0x00223a98) → "sprinting" this update (a local,sp + 0x1e8). On the first such update of a sprint (+0x36c, the run time, still 0) the switch+0x468is copied to the arm byte+0x467. - Latching (in the update, between the right-stick step and the band's ease,
0x0012b504-0x0012b5d8): while sprinting and armed, when the player's brain (0x0021d408) has no enemies (byte+0x152, the oneBrHasEnemiesreads) or the nearest of its up to 16 enemies (handles at+0x164,0x004db5b0with the squared distance of0x00336ce0, FLT_MAX0x00548ad8for none) is closer than 12 m (squared distance < 144),+0x467is cleared and+0x466set; with+0x466set and+0x4480,+0x448= 1. On the first update not sprinting with+0x466set:+0x466= 0 and+0x448= the game time + 250 ms. (A time left in+0x448by an earlier sprint, as in the save, also lets the function run; it then has nothing to do until the next sprint.) - The zoom function
0x00128cf0(camera, sprinting, 0)runs every update once the game time has passed+0x448(so at once for 1, after 250 ms for a time; call at0x0012c5c8).- Sprinting, with no other zoom's band saved (
+0x3c8= 0): the first time, it saves the band's near edge in+0x3e0, the zoom distance in+0x3e8and sets the timer+0x40cto 0.5 s; with one player camera (0x0050b19c= 1, Pitch) it sets the wanted near edge+0x34cto the minimum distance (+0x300, 3.0) and steps the zoom distance to the default (0x001254f0, which also sets the upper pitch limit to 30°); otherwise (two players) it leaves the band and sets+0x3e0to the maximum − 0.5 and+0x3e8to the default. It then saves the target pitch in+0x3e4and moves the target pitch toward 7° (0.122173 rad): on the update the timer reads 0.5 nothing; then by|7° − pitch| / +0x40c × dtper update, which is a straight line arriving as the timer runs out (without a timer, 30°/s). - Not sprinting:
+0x34c= the saved+0x3e0,+0x3e0= 0, the zoom distance back to+0x3e8,+0x40c= 0.5 s; the target pitch moves back to+0x3e4the same way while the timer runs. Once both the band and the pitch are within 10⁻⁵ of their goals,+0x448= 0 and+0x3e4= −FLT_MAX: the zoom is over.
- Sprinting, with no other zoom's band saved (
- The band's ease (
0x0012aae0, every update while+0x34c> 0): withd=+0x34c−+0x32candT=+0x40c, the near edge moves byd × |d| / T × dt(ord × 3.5 × dt,d × 4.5 × dtwith+0x4480, when no timer runs), clamped at+0x34c; the far edge is the near edge + 0.5; when within 10⁻⁵ the edge snaps and+0x34c= −1. The zoom step follows the band's near edge asCamSetFollowZoomsets it (Script calls): at or belowmin + 0.4 × (default − min)(3.72 m) the step is the default (30° with one player camera), at or belowdefault + 0.6 × (max − default)(5.88 m) the maximum (40°), else the minimum (50°) (0x001254f0). Confirmed (runtime): 40° until the near edge passed 3.72 m, then 30° (sprint and combat camera).
So the band moves 4.8 → 4.569 (1.8² / 0.4667 / 30 = 0.231), 4.379, 4.221, … 3.216, then 3.0 when the timer's last
float (about 1.5 × 10⁻⁸) makes the step reach the goal: 14 updates, the measured curve to 0.001 m; the target pitch
falls 6° / 14 = 0.4286° per update. At runtime +0x34c read 3 and +0x40c 0.4667 on the first update at gait 5,
+0x466 was 1 from then until the run stop's first update, when +0x448 became that time + 250 ms; 8 updates later
(267 ms) +0x34c read 4.8 and the band and pitch went back over the next 14 updates, and on the 15th +0x448 read 0
and +0x3e4 −FLT_MAX. The saved zoom distance was 6.6 (the maximum): the upper pitch limit read 30° in the sprint and
40° again from the 6th update of the way back.
CamEnable(5, on) (Camera_EnableFeature, 0x0011de58 → 0x00126a30) sets the switch +0x468; turning it off
also clears +0x34c, +0x3e4, +0x448, +0x466 and +0x467. The constructor sets it to 1 (it read 1 in the
save). Confirmed (code).
The right stick¶
0x00129050 reads the right stick's raw bytes from the pad record (0x005dd810 + pad × 0x50, +0x1a x and +0x1b
y, 0-255 with 128 at rest), confirmed (code). It acts only on a player's current camera, and only when the per-pad
camera options 0x0050b1b0 and 0x0050b1b8 allow it.
| Input | Raw range | Effect |
|---|---|---|
| Stick left | x ≤ 64 | yaw rate +0x358 from 150°/s at x = 0 to 60°/s at x = 64 (2.618 − (x / 64) × π/2 rad/s) |
| Stick right | x ≥ 176 | yaw rate from −60°/s at x = 176 to −150°/s at x = 255 |
| Stick up | y ≤ 8 | pitch rate +0x3b8 from 85°/s at y = 0 to 55°/s at y = 8 |
| Stick down | y ≥ 232 | pitch rate from −55°/s at y = 232 to −85°/s at y = 255 |
| Between | no turn: a dead zone of ±48 horizontally and nearly the whole travel vertically |
- Inversion flags per pad swap the signs:
0x0050b1f0for yaw (it also swaps the two side factors before they are read),0x0050b1f8for pitch. - Any turn sets
+0x368to 0.334 s, which holds off the framing and auto-follow rules (update step 9). - Side factors. A left turn's rate is capped at
+0x438 × 150°/sand happens only while+0x438> 0 (a right turn:+0x434); a turn sets the other side's factor back to 1. The collision step sets both to 1 every update except after its wall push, which sets the side the camera is pressed against to −1, so the stick cannot turn the camera into that wall on the next update. Confirmed (code) in0x00129050and at the end of0x00130990. - The stick also gives two head-look angles for Head look: a yaw (only tested for being non-zero)
of 60° at full deflection to
1.8° at the dead-zone edge (
60° − (x / 64) × 58.2°, mirrored to the right) and, only while the target pitch is inside its limits, a pitch of 22.5° to 1.8° up (22.5° − (y / 8) × 20.7°) or −1.8° to −22.5° down. - The stick, the centre button and the reverse button act only when the pad's options allow it (
0x0050b1b0, switch 0 at0x0050b1b8), on a player's own camera; the stick and the centre button also need+0x452(set every update with one target) and the call's seventh argument clear. Confirmed (code) in0x0012ae58(0x0012af5c-0x0012b0e0, passed at0x0012b4c8):- Seventh argument (right-stick strafing): set only when the top game mode is 1 (
GameModeStack_TopId0x0015e748), both strafe options are on (CfgStrafeRStick0x005104d0andCfgStrafe0x005104a8) and the first target's control function (+0x1bc) is the lock-on movementHuman_FightStanceMove(0x00241b90, the member pointer at0x00548a00). Both options are 0 in the image and only those two bindings write them, which no script calls, so in the shipped game the argument is always 0 (inferred): the stick always turns the camera. When set, it also skips the pitch. - Eighth argument (look-behind): switch 11, but 0 unless the target runs or sprints and 0 while it is
tagging (
0x0012b4a0-0x0012b4b8). "Runs or sprints" is the update's local flag (stack+0x1e4, cleared at0x0012aff8) that the sprint detection sets: the first target's gait+0x1a8is 5 (0x00223a98) or 4 (0x00223a60) with record+0x08clear (no attack or held state), and with two targets the second as well (0x0012b310-0x0012b3ac); a target at gait 0 skips the tests; the takeover+0x465clears it (0x0012b428). Tagging is the first target's state0x2000000(0x002238c0) with one target, or both targets' (s5,0x0012b2b0-0x0012b2d8). - Ninth argument (pitch allowed): the stick's pitch acts only when the views shown equal the player cameras
(
0x0050b198=0x0050b19c) or the camera has one target. - The whole call is skipped when the camera has no target (
+0x444= 0).
- Seventh argument (right-stick strafing): set only when the top game mode is 1 (
Buttons¶
The three camera buttons are pad masks (bits as in the pad record); their defaults, read
from the data at 0x0050b22c-0x0050b234, confirmed (code):
| Global | Button | Default |
|---|---|---|
0x0050b22c |
centre | 0x200, L3 (the left stick pressed) |
0x0050b230 |
reverse (look behind) | 0x8, R1 |
0x0050b234 |
zoom | 0x400, R3 (the right stick pressed) |
CamAssignRevCamButton changes the reverse button; the only script call is wchair.lua (StartIt), which sets it
to 0 (none) for the wheelchair mini-game.
- Zoom (
0x00129050, then0x0012d7a8and0x001254f0): a press moves the band by+0x400 − nearand steps+0x400to the next preset (see the field table). When the reverse and zoom buttons are the same button, a hold timer (+0x370for the first pad) grows bydtwhile it is held: a press zooms at once (the timer has only just started) and resets the timer; holding it past 0.17 s also counts as the reverse button. A press that follows a hold longer than 0.17 s only resets the timer, unless the look-behind switch (11) is off, when every press zooms. With separate buttons a zoom is any new press. - Centre (
0x00129050at0x00129338-0x001293a0): with one target, no clear heading latched (+0x463= 0, helpers), not in a mini-game (0x00233c50) and not player 1 in the combat camera, a new press sets the heading target+0x350to the player's facing (the heading of his rotation,0x00335f48) and, when no timer runs,+0x40c= 0.2 s; the stick does nothing that update. So the camera swings behind the player over 0.2 s (update step 15). - Reverse: see Looking behind.
Looking behind¶
Only while the game mode on top is 1 (play, inferred), switch 11 is on and the player runs or sprints (gait 4 or
5 with record +0x08 clear, the eighth argument), holding the reverse button stores the pad in
+0x458; letting go clears it, and so does the gate closing while it is held (0x00129050 at
0x001292a4-0x001292e8: the look-behind ends on the update the player slows to a walk, stops or starts an attack,
as if R1 were let go). Confirmed (code). So R1 never turns the camera round from a standstill, a walk or a fight
stance: a player blocking (standing, gait 0) or locked on (the combat walk is gait 3,
Combat) gets no look-behind, and R1 is free for the block. Not checked at runtime.
0x001298c0, confirmed (code), with the 75° and 45° cosines set by Cam_Follow_StaticInit (0x00134ab0):
- First held update: the band's near edge is saved in
+0x35c. When the angle between the player's facing and the camera's reversed forward is more than 75° (the usual case: the camera behind the player), the band is moved out to 8.5 m (0x0012d7a8with8.5 − near),+0x455set, and the camera yaws about the look-at point by that whole angle at once (0x0012d688): it stands in front of the player looking back at him. The wanted and held positions (+0x250,+0x260) are set to the result, so there is no lag. Within 75° nothing happens. - Held: nothing more while the angle stays within 45°; if the player turns further, the look ends: the band
goes back to the saved near edge,
+0x455is cleared and+0x459set (which drops the held button at the next stick read). - Let go: the band goes back to the saved near edge,
+0x455is cleared and the camera is turned back behind the player at once, again with no lag.
While the button is held the framing rules and auto-follow are off (update step 9) and the lag is skipped until the view is pulled out (step 18).
R1 at a run looks behind and does not block. The reverse button is R1, which also blocks
(Combat), but the block's fight test Human_CanFight (0x00224f28) fails while the human
runs or sprints free: gait +0x1a8 4 (0x00223a60) or 5 (0x00223a98) with record +0x08 clear. Confirmed
(code). So at a run R1 leaves the gait, the speed and the run clip alone and only turns the view; the block starts
on the first update the gait drops below 4 with R1 still held. Confirmed (runtime), PCSX2 2.9.94, scenario
run_block (level99's street, slot 1; stick 98 % straight up, R1 held at the run twice, then held while the stick
went back to rest):
| Update | Gait, speed | Clip, state | Camera |
|---|---|---|---|
| R1 pressed at the run (51) | 4, 7.80 m/s, unchanged for the 40 held updates | 410 (the run), state 0, record +0x08 0 |
+0x458 = 0 (pad 0) and +0x455 = 1 on the first held update; the yaw jumped from 90.9° to −89.0° that update; the band's near edge (+0x32c) 4.8 → 6.1 m |
| R1 let go (91) | 4, 7.80 m/s | 410 | +0x458 back to 255, +0x455 0, the yaw back to 91.0° at once, the near edge 4.8 m |
| The stick to rest with R1 held (141) | 0, 0 m/s that update | 606 BLOCK_SUSTAIN, state 0x8001, record +0x08 0x80000 for 5 updates |
the look ends that update (+0x458 255, the yaw back behind) |
Ledges¶
When the player walks off an edge, the camera turns to show the drop. Confirmed (code):
- Who calls it.
Human_StartFall(0x0023dc58) callsReactToLedge(0x0012da20) with the heading of the player's velocity (his facing when he is not moving), −1 and time 0, on his follow camera, only when: he is a player (+0x1b0≠ −1), not jumping (state0x400000000), the number of players equals the number of views (0x0050b198), he is on the camera's target list, the current camera is the follow camera (type 2), and a world ray straight down from his body point (vtable+0x9c) raised by his height (vtable+0x5c) and moved 0.75 m along that heading, as long as the height + 1.5 m, hits nothing: there is a drop ahead. ReactToLedge(heading, −1, time, camera)does nothing unless the camera has a target, the target has state flag0x800000000(falling), and the stick has been still for 0.334 s (+0x368= 0). Then:- The heading target
+0x350= the heading. When the target does not hold flag 6 and moves at leastwalk + 0.18 × (run − walk)speed, that is all: the camera simply turns to face the way he is going. - Otherwise (walking, or slower) it looks for the open side: two rays along ± the heading's sideways direction,
9.5 m long, from the target's x, y at the height
+0x208, and again from a point 1 m along the heading dropped by a 5 m ray to its hit − 0.3 m. If the room to the left is smaller than to the right the heading is turned by −92°, else by +92° (0x3fcd87ad= 92° in radians), and wrapped to 0-2π: the camera swings round to look along the edge from the side with more room. - The target pitch is saved in
+0x3d4(if not already saved) and set to the upper pitch limit − 5°, so the camera looks down over the edge;+0x448= 0,+0x3e4= −1.7 × 10³⁸. - The timer
+0x40c= time, 0.35 s when 0 (always fromHuman_StartFall).
- The heading target
- Afterwards: the turn runs as any heading target (update step 15) and the saved pitch is put back when the
player no longer has state flag
0x1800000000(step 3), so after he lands. - The second argument −1 is read only as the heading target's timer fallback: unused here (inferred).
Landing (Human_Land 0x0023e090, confirmed (code)): when a player lands with a vertical speed below −1.5
m/s, his player camera's shake slot +0x15c is called with (human, level, 1): level 1 above −14.9 m/s (no
damage), 2 from −14.9 to −20.5 m/s (fall damage), 3 below −20.5 m/s (a fatal fall). For the follow camera
that is 0x001263e8, the follow camera's shake (only for a human on its target list), so the levels are
the shake's levels 1-3; the last argument 1 (+0x1d7) lets the shake move the view, not only the rumble
(Shake).
Height hold¶
The height hold drops a raised camera under an overhang (a beam, an awning's edge, a bridge's underside) and keeps
it there instead of following the leash's height. Confirmed (code) in 0x0012ae58 (0x0012bf14-0x0012c078) and
0x0012a7d8; the player-terms reading is inferred. Below, feet = +0x378 − the offset's z (+0x208), where
+0x378 is the look-at height, refreshed every update while not holding and frozen while holding; H is the probe
height of update step 1 (2.0 m; with a target 1.9 m × his scale, changed in some animations, not traced); the near
plane is the camera's vtable +0x5c.
- Probe (update step 12): runs while the main ray did not block last update (
+0x45b= 0) and the held position+0x260is higher thanfeet+H− 0.5 m (1.4 m for a normal-sized player): a ray with mask0x200from the wanted position's x, y at the heightfeet+H+ 0.75 m + the near plane, level toward the look-at point (0x00336da8), as long as their distance in plan (0x00336d68). - Entering (
0x0012a7d8, called withfeet,feet+Hand a table of two heights at0x00548ad0): needs a hit more than 10⁻⁵ along the ray, on a steep face (its normal's |z| under cos 53.13° = 0.6,0x005d91cc), while not holding. The face's plane goes to+0x220and the hit point to+0x230. The same level ray is then cast lower, atfeet+ 2.395 m and thenfeet+ 2.148 m: forward, and when that is clear, backward from its far end. The first height clear both ways is taken. If both hit, a last try atfeet+H; if that hits either way there is no hold. Otherwise the hold starts:+0x453= 1 and+0x324= the height − the near plane −+0x378, so step 12 eases the wanted position's height to that height less the near plane. - In player terms: the camera stands higher than the player's head, something steep sits between it and the look-at point about 0.75 m above head height, and there is clear space 2.0-2.4 m above his feet: the camera comes down into that space and stays there.
- Leaving (
0x0012c048): when the slope flag+0x460is set, the probe did not run or hit nothing, or (holding) the wanted position is no longer in front of the stored plane (its distance in plan under 10⁻⁵):+0x453= 0 and the plane is cleared. So the hold also needs the probe ray to keep hitting something every update. - While holding: update step 12 eases the height and step 11 (the pitch toward its target) and the 85°/s pitch return of step 16 are skipped.
Aiming¶
While the player aims a throw (+0x454, update step 1), Cam_Follow_AimView (0x00128678) runs every update,
confirmed (code):
- On the first update the wanted near edge
+0x34cis saved in+0x3ccand set to 0.8 m, so the camera comes in close; the upper pitch limit becomes 50° and the lower one max(−20°, the angle of (0.075 − offset z) / maximum distance), and the old look-at point is copied to+0x240and+0x270. - The look-at point is the aim origin (player record
+0x5750) moved 0.05 m toward the aim target (+0x5760). - Heading target: the heading from the player's body point raised by the look-at offset to the aim target, −15°, with no timer, so the camera turns there at once.
- Pitch override: when the view's pitch is more than
fov × 0.1875°from the pitch toward the aim target, the override+0x354= that pitch clamped to the limits (reached at 10°/s, update step 16), and the yaw rate is raised by up to 3 × its value (× (1 + 3 × min(1, (off − fov × 0.1875°) / (fov × 0.3375° − fov × 0.1875°)))). - The lag is 0.8 (update step 18). When aiming ends see update step 13.
Head look¶
With the stick turning the camera and the player idle (update step 17), 0x00128b20 turns his head toward where
the camera looks, confirmed (code):
- Yaw: the signed angle between the camera's horizontal forward and the player's facing; 0 beyond 150° (so he does not twist to look behind). Pitch: the stick's head pitch (above).
HeadLook_AtAngles(0x00299f98) stores them in the human's head-look state (+0x284) with weight 0.75, flags0x20 | 4, for(dt × 1000 & 0xff) + 100ms (133 ms at 30 Hz), so the look lapses about 4 updates after the stick is let go.- The turn (
Human_UpdateHeadLook0x00248428, each human update): the yaw is clamped to ±55° (±90° with flag0x40, not set here) and the pitch to ±45°; each update the head's angles step toward them by weight × 12° (9° at 0.75;Angle_StepToward, last argument 30°, not traced); flag0x20keeps the look from relaxing when the yaw passes 60°.CharacterInstance_SetHeadLook(0x00175c58) then clamps the pitch to −30°..22.5° for the bones (graphics).
World collision¶
0x00130990 (about 3,000 lines decompiled) runs after the camera's wanted position is known. Confirmed (code) for the
calls and constants; the overall reading is inferred:
- Materials it ignores: at
0x00130a5cit copies a list from0x00548ab0tosp + 0x40: 30LOW_FENCE, 122RAILING, 107CHAINLINK_NOCLIMB, ended by 1. EveryCollisionMesh_RayCastof the step passes it as its fourth argument, the materials to skip (0x001311a8,0x001312e0,0x00131730, ...). So the camera sees through low fences, railings and unclimbable chain-link. Confirmed (code). - A height ray (
0x00130c28) straight down from the look-at point (0x00511770, (0, 0, −1)), as long as the look-at offset plus 0.5 m (1.9 m), mask0x200: when it hits a face whose normal'szis at most cos 15° (a slope, or the top of something under the look-at),0x0012f3e0eases a pitch offset (+0x3a4) 10% per update toward the slope's angle, 0 on flat ground. - The main ray (the cast at
0x001311b4, the recast at0x001312e4): from the look-at point toward the wanted position, its full length, with mask0x200 | 0x800 | 1(| 1only with one target). Through the ray cast's rules that skips triangles with type bit 9 (0x200), and tests disabled triangles too (mask bit 0); one-sided faces are hit only from their front. When the hit is a disabled triangle (flag bit 0 clear, and not0x800) and either the camera of the last update (+0x1e0, the position copied at the end of each update) is not in front of its plane (by less than 0.001 × (1 + the plane normal · the direction from the last look-at point+0x270to it)) or the look-at point is less than 0.5 m in front of it, the ray is cast again without mask bit 0, so the disabled triangle is ignored; a hit sets+0x45b(which latches auto-follow off until the player stops,+0x45d, Heading) and its distance becomes the limit the rest of the step works from. When that ray hits something that is not a ceiling (normalzabove cos 150°), a second ray (0x001314d0) from the camera of the last update to the look-at point checks whether the obstacle is between them; if so (and the latch+0x479is not −1), the main ray is cast again from a point moved along the view by0.8 / tan(3 × probe angle)and that distance is added to its hit. Confirmed (code) for the masks, the 0.5 m and the recast; the meaning of the second ray inferred. - Side probes (casts from
0x00131744): the main ray turned about the vertical through the look-at point by 3, 2 and 1 × the probe angle to each side (the loop counts down from 2), each as long as the main ray, with the same mask and the same disabled-triangle recast. The probe angle is 7° at the near edge of the distance band down to 4° at the far one (7° − 3° × t,tthe position in the band). The free angle found on each side is limited to 3 × the probe angle. A table of fractions 1.0, 0.7, 0.5, 0.3 and 0.15 (0x00548b70) is copied to the stack beside them (0x0013106c) but never read: no other instruction in0x00130990touches that copy (confirmed (code); a leftover). After the swing the step keeps the two free angles in+0x42cand+0x430(0 for a side with nothing in the way,0x00132be4-0x00132c04), for the next update's centring test below. - Swinging away: when one side is clearly freer (the two differ by more than 7.5°), the camera yaws toward it by
20% of the needed angle per update. When the view is fully blocked it turns toward the target direction by
the angle between them, at most dt × 480°/s (
8.377581rad/s); the latch+0x479records the first direction (1 for a positive turn, 2 for a negative one) and, when the needed turn reverses, becomes −1: the turn is given up (0), the player's position is kept in+0x2c0, and the camera pulls in to the hit instead.+0x479is cleared when the view is not fully blocked (inferred). Confirmed (code) before0x001325cc. - Centring in a gap (
0x001320fc-0x00132198, confirmed (code)): when both sides are blocked and the probe hits are uneven (the farthest hit lies beyond the nearest and short of the full ray) the 7.5° rule is not used. Instead, while the innermost probe on one side or the other hits short of the full ray and both free angles have changed by more than 0.1° since the last update (+0x42c,+0x430), the swing for this update is+0x428 × 0.75 + (a − b) / 2 × 0.25(athe+0x430side's free angle,bthe+0x42cside's), stored back in+0x428: an eased turn toward the middle of the opening (inferred: a player walking down an alley). When only one side changed, or neither, it does not turn.+0x428is written only here and zeroed by the constructor. - While a hand-over is queued (
+0x47cset, update step 21) the main ray, the side probes and the swing away are skipped (0x001313b0jumps to0x00132060), so the short turn before a mini-game or mugging shot is not held back by walls. Confirmed (code). - What moves the camera, in order (confirmed (code) at the addresses cited; the step works on a copy of the
wanted position
C, from the look-at pointLalong the unit directionu= (C−L) / |C−L|):- The swing away (above) turns
CaboutL. - Pull-in (
0x001327f0-0x00132a20), when the main ray or the side probes are blocked:C=L+u× the nearest hit distance among the main ray and the six side probes (the probes are as long as the main ray, so a face met at a slant by a probe can be nearer than the main ray's hit). With only side probes blocked and+0x471clear, the distance is instead the farthest probe hit, or, when that is nearer than last update's allowed distance, last update's distance moved toward it by 2 ×+0x1a0(the time step, inferred) of the gap.Cis then moved off the hit's plane, and off a second probe plane if there is one, until it ismin front of it, withm= min(near plane × (1 −n· (−u)), 0.01 m): almost nothing for a face met head-on. No margin like Coney's 0.2 m. - Creep out (
0x00132f5c-0x00133250, Heading): whenCis now farther fromLthan the allowed distance of the last update, it is put at that distance plus 4% of the gap (blocked) or 3% (clearing). A pull-in nearer than last update's distance stands at once. Camera_PushOutOfHumans(0x00122728) whenCis more than min(+0x300,+0x32c) or 1 m fromL.- The aim point
Pwork, including both radius-1.0 sphere pushes (The aim point step 3, and again in0x0012f858after it raisesP): those pushes moveP(or+0x240with two targets), neverC. - Raise when blocked (helpers, below) lifts
C's height only; its plan distance is unchanged. - The wall push (below), only while
+0x464is set: the one sphere push of the camera itself, radius the near plane + 10⁻⁵.
- The swing away (above) turns
So the distance a blocked view leaves is the nearest of the seven hits, less up to 1 cm; the near plane is not kept
clear of the face unless the wall push runs. In the street run above the camera came to rest 0.25 m in front of the
disabled panel; a slanted side probe meeting the panel nearer than the main ray fits that (inferred). (The sway
+0x3f8 is the sprint's, at most 0.028 m: The aim point step 2.)
The aim point gets its own 1.0 m push. Wall push (only while +0x464 is set, set by choose a clear
heading; one view per player; the final position's distance in front of a near-vertical face,
|n.z| < cos 10°, under 1.05 × the near-plane radius, or the main ray blocked): a sphere push of the near-plane
radius, then the camera is moved off the plane to that radius, the wanted and held positions are reset to the band's
near edge on the line from the look-at point, +0x45b is cleared, the side factor on the side the camera is turning
toward (inferred) is set to −1 and the other to 1 (right stick), and the lag becomes max(lag, 1): the
next update takes the position at once. Confirmed (code) from 0x0013464c to 0x00134a18.
- The lag it chooses for the next update (+0x388), in this order, confirmed (code) from 0x00132a24:
1. 0.27 when the main ray was blocked (+0x45b), or when the full-block turn was given up (+0x479 = −1)
and +0x471 is clear; 0.27 − 0.05 × s when +0x471 is set (set when a given-up turn finds one side
open), s being the nearer side probe's free distance as a share of the probe length (0 to 1); else
0.22.
2. Back to 0.22 when the main hit is a ceiling (normal z below cos 150°) and a slope pitch +0x3a4 is set.
3. When neither +0x45b nor +0x471 is set, the new value is reached only by 0.5% per update from the old
one (old + (new − old) × 0.005), so after a block the lag drifts back from 0.27 to 0.22 over a few seconds.
Otherwise, when the camera moved and no timed heading target runs, it is scaled by
min(1, d × 0.2222) × 0.7 + 0.3, d the free distance in front (so 30% of it at 0 m, all of it from 4.5 m).
- Side factors +0x434 / +0x438 are set to 1 at the end of every collision step except after the wall push
above. They are not eased.
- Recovered distance +0x380 eases toward the distance the probes allow by 10% per update; nothing reads it
(inferred from the field's readers), so it can be left out.
- No final line-of-sight test: the position the step returns is used as it is (update step 20).
The step's helpers¶
Confirmed (code) at each function; where the reading of a direction or a gate is not certain it says so.
- Slope pitch (
0x0012f3e0, called at0x00130c8c). The height ray (down from the look-at point, the offset + 0.5 m long) must hit a slope between 15° and 53.13° (normalzfrom cos 15° down to 0.6,0x005d91cc). Withhthe horizontal direction from the look-at point to the camera,dthe 3D one, andnthe slope's normal:- Camera uphill (
h · n< −cos 87°): a ray from the look-at point's x, y at feet + max(offset / 4, 0.25 m), along −n, as long as max(the camera's distance in plan, 6 m), mask0x200. If it hits the same plane (normals within 0.5°),+0x460= 1 and, whend · n< 0 (the camera below the slope's plane), the target isasin(−d · n);+0x3a4moves 10% of the way toward it but never past it (min); otherwise+0x3a4= 0. - Camera downhill (
h · n> cos 87°): the same ray from feet + 1.45 m; on the same plane,+0x460= 1 and, whend · n> 0, the target is−asin(d · n) × (d · n) / 2, eased 10% (max); otherwise 0. - Use (
0x00130c94-0x00130ef4): while|+0x3a4|> 10⁻⁵ the wanted, held and last-wanted positions are turned about the look-at point by+0x3a4round the camera's horizontal side axis (a pitch), and the turned height is kept in+0x3a0; when+0x460was not set this update the offset first decays 10%. So on a slope the camera is raised off the uphill ground and lowered a little toward downhill ground (the sign inferred from the two cases).+0x460also ends the height hold, bounds the stick's target pitch (update step 16) and picks the 0.22 lag under a ceiling.
- Camera uphill (
- Tilt over camera obstacles (
0x0012ec50, before the collision step). The objects listed at0x00715280are the moving vehicles:WorldObject_Init(0x00391adc) adds an object only when its type's kind byte+0x86is 29 (TYPE_MOVINGVEHICLE) and player 1's follow camera already exists (Cam_GetFollow(0, 0)); no other flag is tested. On the disc that is six object types (dyn_coney_car_a,dyn_hearse,dyn_ridecart,dyn_rogues_car,dyn_s_train_kill_p,dyn_turnbul_bus, Objects).WorldObject_Deleteremoves it, the follow camera's constructor clears the list, and a 32nd vehicle is not listed. They are boxes: their type's size (+0x78,+0x7c,+0x80) halved, at their position and rotation. A sphere of radius max(the near plane, the player's capsule radius) is swept from the look-at point to the wanted position against each (Sweep_SphereOrientedBox); the first hitt< 1 is used:- when
t= 0 or the view runs within 10° of the object's −y axis (cos 170°), the camera tilts over it: withDthe camera's distance,hits height over the look-at point andtopthe box top + 1.0 m over the look-at point, ifh<topthe target isasin(h / D) − asin(top / D)and+0x3a8eases 90% toward it (max),+0x461set; - otherwise the camera is pulled in to
t × D − nearalong the line, and if that moves it away from the last camera position+0x1e0, only 30% of the way from there. - While
|+0x3a8|> 10⁻⁵ the wanted position is turned by it about the look-at point round the view's side axis; without a tilt this update it decays 10% per update.
- when
- Raise when blocked (
0x0012f858, when the step pulls the camera in).s= clamp((m − dist) / (m − 1), 0, 1),m= max(minimum distance, 0.6 × near edge),distthe pulled-in distance: 0 atm, 1 at 1 m. For a wall hit (normal|z|< 0.6), not aiming, the target a player: the camera's height movessof the way up toward feet + 2.0 + 0.085 m (feet + 0.5 m or the look-at height in states0xe0c00/0x2000, feet + 0.145 × scale with the last argument set; inferred: crouching), never down. Then with one target, the main ray blocked, no clear heading latched (+0x463) and no timed move (+0x475), choose a clear heading (below); then the height movessof the way to that target − 0.05 m once more, applied to the aim pointP(one target;+0x240with two), andPis sphere-pushed again with radius half of 2.0 (1.0 m; 0.5 m on one sway path), the same push as The aim point step 3. Its result goes to the step's push flag (+0x45a). The camera is not pushed here. - Choose a clear heading (
0x0012fd20). Only when the player is less than 1.8 m in front of the blocking face, not aiming, not holding someone in front, and the allowed distance is under 9 m (or he is in a fight stance that counts, or has one of state flags0x3800f0c00). It sets+0x462,+0x463and+0x464(the wall push), and casts 11 rays (mask0x201) from the look-at point: along the face's normal, and turned about the vertical by ±30°, ±45°, ±60°, ±77° and ±82°, eachL / cos(angle)long (Lthe allowed distance); each scores its free length, at mostL. The best average of three neighbouring rays wins (ties go to the straight one), and+0x43c= that ray's heading − 180°: update step 9 then turns the camera there over 0.1 s. The three latches clear when neither the camera nor the look-at point is within 1.65 m in front of the blocking face (end of0x00130990). - Sphere test (
0x001303c8, switch 1, off by default). A physics sphere of the near-plane radius at the camera is collided with the physics world; hits on object kinds 0xc-0xe, 0x10 and 0x1c are ignored (what they are is not traced). Any other hit puts the camera at the contact point plus the near plane along the contact normal, except on a box body, where the camera is moved along the view line over a timer+0x410(0.15 s when unset) to stay two near planes off the box's nearer face (inferred).
At runtime (stick magnitude 1.0, level99 checkpoint 1), after running for 1.2 s (a run needs a stick magnitude of
at least 0.95, method): the camera was 3.16 m from the look-at point
(3.08 m horizontally) and 0.70 m above it, a pitch of about −12.8°, which is the 3.0-3.5 m leash band and the 13° target
pitch. Confirmed (runtime).
Runtime checks¶
PCSX2 2.9.94, level99 checkpoint 1, Rembrandt, read over PINE once per update; stick magnitudes by the
stick-table method, right stick by the keyboard (full deflection).
"Wanted position" is +0x250, the camera's own position +0x10. Confirmed (runtime) unless marked:
- Position lag. With the player moved 2 m away in one write, the gap between the camera and its wanted position
shrank by a factor of 0.78 per update (0.120, 0.094, 0.074, 0.057, 0.045 m), with
+0x388reading 0.226: 22% per update.+0x388was 0.157 standing at the start spot (pulled in by a wall) and climbed to 0.227 within 0.5 s of running into the open, the 0.5%-per-update drift of World collision. - Leash band. While running, the wanted position stays 3.50 m from the look-at point (the band's far edge); standing, it stays wherever it was inside the band (3.00 m at the start). At the start spot the camera itself is pulled in to 1.85 m by the walls and recovers toward 3.5 m over about 1.5 s of running.
- Right stick yaw. Full deflection right (raw x = 255): the wanted position turns by exactly 5.00° per update
(150°/s) from the first update; the camera's own yaw follows with the position lag, reaching about 5° per update
after 0.4 s, and coasts for a few updates after release. The 0.334 s hold timer
+0x368read 0.301 (one update counted down) while the stick was held. - Right stick pitch. Full deflection up:
+0x3b8= 85°/s; the pitch target+0x3b4stopped at the upper limit+0x3ac= 30°, which is the one-player (0x0050b19c= 1) case at the close zoom (zoom step 4.8). The view's pitch eased from 15° to 30° behind it. - Auto-follow was not seen. Running at 7.8 m/s (gait 4) with the facing held 63-78° away from the view for
1.5 s, in the open, the wanted position turned only as the moving look-at point dragged it: no rotation of its own,
with the auto-centre option on (as saved) and with
0x0050b240written to 0 (the default rule). By the code either rule should then turn about 3.4° (auto-centre) or 2° (default) per update. Every gate listed under Heading that can be read over PINE passed (+0x444= 1, record words+0x00and+0x08zero,+0x460= 0,+0x320= −1,+0x368= 0,+0x455= 0,+0x474= 1, the run clip's descriptor flags 0); the condition that blocks it was not found then (the update's locals cannot be read without breakpoints). - Why: the blocked-view latch. A later run from the same spot (left stick 100 % up for 35 updates, then 70 %
up and 70 % left, then released) read
+0x458-+0x45fevery update.+0x45bwas 1 for the first 19 updates, while the walls held the camera at 1.85 m, and 0 after.+0x45cand+0x45dwere 1 from the start and stayed 1 through the whole run, clearing on the second update after the player stopped (gait 0). The camera's wanted position turned 0° of its own on every update (its angle about the new look-at point, before and after the update). The same run with+0x45cand+0x45dwritten to 0 before every update: they came back while+0x45bwas 1, stayed 0 after, and once the stick went diagonal the wanted position turned 0.6-1.6° per update of its own (18-47°/s), the auto-follow rule at work. So a blocked view at the start of a run keeps auto-follow off until the player stops. In the street the run starts in the open, nothing is latched, and the rule turns. Confirmed (runtime).
In the street¶
PCSX2 2.9.94, the street saves (in level99's world, Feel comparison), Rembrandt, the camera read every
update while a scripted pad played; the per-pad option bytes 0x0050b240 / 0x0050b248 were 1 (the auto-centre
rule) and 0x0050b19c was 1. Confirmed (runtime) unless marked:
- Band and distance. The leash band was 4.8-5.3 m (
CfgFollowCamera's default 4.8 and + 0.5), the hard band as described (far edge + 0.35 m): the camera stood 5.30 m from the look-at point, 5.49 m walking and 5.65 m running, at pitches of 13°, 12.6° and 11.1°. - Auto-follow runs. The wanted position turned about the look-at point by the auto-centre rule's rate
(
(a − 45°) × 2.444 + 45°per second above 22.5°, 200°/s from 90° to 100°, falling to 60°/s at 157.5°) whenais the angle between the player's new facing and the camera's view at the start of the update (its position then to its look-at point): within 5°/s on average from 30° to 90°, over 656 updates of walks, runs, sprints and turns; just above 22.5° it turned slightly the other way, as the rule's negative rate says. It turned while walking, running, sprinting and in the air, and not while standing, during the walk and run start clips or during the landing clip 436 (the gaits, Heading). Because the stick is turned by the camera, a stick held 90° to the side makes the player run in a circle. Which camera turns the stick (confirmed (code)):Pads_Update(0x001454a8, step 2 ofHumans_Update, before the locomotion; the cameras update last) builds the matrix from the player camera's orientation (vtable slot+0x224, the+0x20that faces the aim point) as left by the previous update, stores it at0x005ddc90 + player × 0x10, andPad_Update(0x00144fb0) turns the stick(x, y)by its x and y axes and restores the stick's length (reversed while+0x455is set). The stored quaternion is reused instead only while the follow camera's+0x473is 0; the constructor,Cam_Follow_Deactivateand a stick at rest set it to 1, and only the co-op view change (0x001276a0, its third argument) can clear it. So in one-player play the stick's frame is exactly the orientation the auto-centre rule measuresafrom. Inferred check, with the locomotion's turn (Characters, 18° limit, ease to 2.0 rad, carry 0.8; steady step = 5 × 18° × ease(e)): at the run's circle the original turns 6.37° an update, soe= 90° − 76.6° + 6.37° = 19.8° before the step, and the rule gives 6.47° (194°/s). The circling rate (re-measured 2026-10-05, slot 1, stick held 90° to the side for 120 updates after 30 updates straight up; rates averaged over updates 100-158):
| Stick | Gait | Player's turn | Rule's turn about the look-at point | Leash drag | a |
|---|---|---|---|---|---|
| 35 %, 60 % or 80 % sideways | walk, 1.63 m/s | 143°/s | 127°/s | 16°/s | 77° |
| 100 % sideways | run, 7.80 m/s | 191°/s | 122°/s | 72°/s | 72° |
| 70 % / 70 % (a full diagonal) | run | 61°/s | 24°/s | 42°/s | 34° |
"Rule's turn" is the wanted position's rotation about the new look-at point (what the auto-centre rule adds;
the leash moves it only along that line), "leash drag" the rotation of the old wanted position from the old to
the new look-at point (the target's sideways move); they add up to the camera's turn, and the player's facing
turns with the camera, a steady. The 122°/s and 127°/s an earlier reading gave as the circling rate are the
rule's share alone: the original circles at about 190°/s at a run. The a in that table is measured to the
look-at point, which is why the rule's turn came out 3-10°/s above the formula. The rule measures a against the
camera's orientation, which faces the aim point. Re-recorded 2026-10-07 (the run_circle scenario, with the
orientation +0x20, the aim point and the turn rate +0x360 that 0x0012d688 writes): with a taken from the
previous update's orientation and this update's facing, the formula gives the written rate to 0.1°/s on every
update except the three on the 90° boundary itself. At the run's steady circle that a is 76.6° (122.3°/s), while
the view to the look-at point gives 74.8°: the aim point sits 0.17-0.40 m off the look-at point. Confirmed
(runtime).
- Sprint zoom. From the first update at the sprint gait the band's near edge went 4.8 → 4.569, 4.379, 4.221,
4.085, 3.968, 3.863, 3.770, 3.686, 3.607, 3.533, 3.462, 3.391, 3.315, 3.216, 3.0 (the far edge 0.5 more), and the
target pitch +0x3b4 fell by 0.4286° per update from 13° to 7°: both over 14 updates. In the sprint the camera
settled 4.70 m from the look-at point at a pitch of 5.2°. Both went back the same way over 14 updates, starting
8 updates after the run stop began (the body at about 2.5 m/s). A run (gait 4) did not change them.
- Right stick. Yaw at 30, 60 and 100 % to the right (raw x 189, 217, 255): 74.8, 106.7 and 150.0°/s applied to
the wanted position from the first update, as the table above gives. Pitch up at 100 %: the target rose 85°/s to
the upper limit 40° and stayed there after release.
- Look-at height after a climb's rise (slot 8, the feet rising 1.28 m onto a trash can and 2.64 m onto a roof):
the look-at point moved 20 % of the way to feet + 1.4 m per update for 4-7 updates, then covered the last
0.5-0.6 m in 2 updates, so the view's pitch stayed at 3.6° or more. During a jump it follows the feet directly.
- Fences. Through a running fence climb (slot 7, material 30) the camera stayed 4.9-5.3 m away while the fence stood
between it and the player, and passed through the fence afterwards without pulling in: its collision does not see that
fence, because every ray of the collision step excludes its material (World collision). Nor did the face
that pulled Coney's camera in 0.19 m behind the player: it is not a low face but a disabled two-sided panel of
level99's mesh (triangle 27, material 91, flags 0xf442: bit 0 clear, 4.1 m wide and 2.65 m tall across the run's
path at y = 31.17), read from the save's RAM; slot 7 has 8 disabled triangles (that panel, two of material 187
STOREDOOR_GLASS, four of material 2 GLASS) and slot 1 none, while every triangle on the disc is enabled, so the
game switched them off (Collision, 0x0034fba0). The main ray tests disabled triangles but
recasts without them when the look-at point is within 0.5 m of the plane (World collision), which is the
case here (inferred; the panel's data and the disabled counts are confirmed (runtime), read from the saves' RAM).
Scenes take the camera and give it back¶
The rest of scene playback (records, roles, letterbox, skipping) is on Scenes.
level99 starts with SuperRunScene(IntroScene) at checkpoint 1 (Scripts); IntroScene is a table
(SceneId l99_c1, the humans and objects that take part, ReturnFunc = P1.StartTraining). The global.lua
helpers fill in defaults and call the engine. Confirmed (code) for the script (global.lua, read as bytecode) and the
engine at the cited addresses:
SuperRunScene(t)hides the HUD, clears the gang's wanted level, blacks the screen at once (ScreenQueueEffect(1, 0)), revives the scene's humans, and preloads the scene (ScenePreload(id, "gPlayCutScene")), keepingtintblScene[id].gPlayCutScene(id)sets defaults:Barstrue,Delay0, and whenBlendCamis not given,BlendCam= −1 andFadeIntrue (a givenBlendCamsetsFadeInfalse). It joins each human to the scene (GoalJoinCinematic, or the animation / fixed-scene variants), adds the objects, and callsScenePlayCinematic(id, Delay, "PreCashTheWorld", Bars, not NoSkip, Looping, Freeze, BlendCam, Final, Chain).- Scene start (
0x00353818→0x0039d870). When the scene has its own camera (scene data+0x22), a scene camera (type 4,0x0011e1b0(4, …)) is made or reused, given the current camera's view (slot+0xac→+0xb4), and the current camera is pushed on the camera stack (0x00120450; for a blend, locked or other wrapper camera, types 5-8, the camera inside it). The scene camera then becomes current at once (0x0011ee08with 0 seconds). A scene without a camera keeps the current one, saves its position (scene+0x80, the camera at+0x90) and moves it to the scene's anchor. - While a scene camera is current,
CameraMakeActivefor player 1 does not switch: it replaces the camera on the stack (CameraStack_PopthenCameraStack_Push), so the script changes what the scene returns to. The test inCamera_MakeActive(0x0011ee08) is the byte0x0051489c + 0x410non-zero, player index 0, and player 0's current camera of type 4. That byte is the cinematic scene state of Scenes (start step 4: 0 → 1 → 2 → 3, back to 0 at the end), so only a cinematic scene, not an in-game one, redirects the call. Confirmed (code). - Scene end (
0x0039f450): the camera is popped (0x00120488), reset (slot+0x13c, asCameraReset) and made current withBlendCamseconds: above 0 the blend camera (type 5) runs between the scene camera and it; 0 or −1 is a cut. In one game-mode case (the mode object at0x0015e718reporting 8) it first takes the scene camera's view, field of view and near plane. The scene camera is then released (0x0011e440) and the cameras' update runs once with dt = 0.17 s (0x0011e878(0.17)), so the follow camera settles before the next frame. A scene without a camera puts the kept camera back at its saved position and resets it (0x0039ec60). - The script's end callback (
global.lua, run when the scene ends) callsReturnFunc(for the intro,P1.StartTraining), turns gang spotting back on unlessBlendCamwas 0, and withFadeInfades the screen in over 0.5 s (ScreenQueueEffect(0, 0.5)).
So the intro hands back to the follow camera with a cut hidden by a 0.5 s fade-in, the follow camera having been
reset and run for 0.17 s. Confirmed (runtime): after the intro the stack index 0x0050b180 is −1, its slot 0 still
holds the popped follow camera, and the follow camera is the current and previous camera of player 1.
Script calls in level99¶
Every camera call of level99.lua, level99_combat.lua, level99_lesson1.lua, level99_lesson2.lua and the
global.lua helpers they reach (read from the scripts' bytecode; the bindings: mission 1
coverage). Confirmed (code) at the cited functions.
| Call (where) | Effect |
|---|---|
CameraCreateFollow("follow", player) (AddCameras) |
CamSetupFollow + CfgFollowCamera(3, 6.6, 4.8, 13, 65, 0.1, {0, 0, 1.4}, 0.2) (Setting up): band 3.0-3.5 m, zoom step 4.8, upper pitch limit 30° |
CameraMakeActive(MainCam, 0) then CameraReset(MainCam) (AddCameras) |
current at once; then placed behind the player at 3.0 m, pitch 13° |
CamEnable(3/4, false) (combat setup), true again (lesson 1) |
no effect with one player (Switches) |
CamTarget(1, MainCam, player) / CamTarget(0, …) (lesson 1) |
removes the player from the shared target list, later adds it back. The follow camera takes its targets from the list's first two entries and falls back to its last target (+0x31c) when the list is empty (0x001282a0), so with one player nothing changes |
CamSetFollowZoom(1) (lesson 1 setup; lesson 2 after DealerPoizo and the dealer's respawn) |
band 4.8-5.3 m, zoom step 6.6, upper pitch limit 40° |
CameraCreateLocked(name, pos, fov, heading, pitch, 0, 0.1, far) + CameraMakeActive(name, 0) |
a cut to a locked camera (nine in the tutorial: fov 50 or 65, far 72.6-150) |
CameraReset(MainCam) + CameraMakeActive(MainCam, 1) |
back to the follow camera, placed behind the player, with a 1 s blend (Blends); after VerminWait with 0 s, a cut |
CamSetFollowAngle(-10) (lesson 2 P3.BreakFence, between the reset and the blend) |
target pitch −10° clamped to the limits, so the lower limit atan(−0.4 / 5.3) = −4.3° (the camera looks up at the fence); reached at once; it stays until the next CameraReset (after DealerPoizo) |
CamSetSecondary(Teacher.Vermin, 0, p) / (NilHandle, 0, p) (lesson 2, both players) |
keep Vermin in view (Heading, factor 0.25, no range) instead of auto-follow; NilHandle ends it |
CamEnable(0, false/true) (lesson 2 P3.Player2Jumps … P3.VerminJumped) |
right stick and zoom buttons off while the camera watches Vermin's jump |
CamEnable(9, false/true) (lesson 2 P3.DealerHit … P3.CheckForFlash) |
power-move cameras off while the dealer respawns |
ScreenQueueEffect(2, 1) / (3, 1) around VerminCar, PedCam, ClimbPoizo, JumpCam, VerminFencePoizo |
letterbox in / out over 1 s (Screen effects); VerminWait, FenceCam and DealerPoizo have none |
CamSetFollowZoom(preset, player = −1) (0x0011c470), for each player's follow camera (or one): the look-at point is
snapped (0x00127d48, its previous values set to it, so no ease), then the band is moved so that its near edge is
the preset distance (kept 0.5 m deep, clamped to [min, max], and saved in +0x344/+0x348), the lower pitch limit
recomputed from the new far edge, and the zoom step set to the next preset: 0 → near edge the minimum, step the
default (30°); 1 → the default, step the maximum (40°); 2 → the maximum (band max − 0.5 .. max), step the
minimum (50°). With two player cameras 0 acts as 1. The camera itself is not moved: the leash drags it into the new
band over the next updates. Confirmed (runtime): checkpoint 1 states read 3.0 / 3.5, step 4.8, 30°; a run left to
itself from l99-warriors-fight-start until lesson 1, and the street saves, read 4.8 / 5.3, step 6.6, 40°.
CamSetFollowAngle(degrees) (0x0011c3b8): snaps player 0's look-at point, then for every follow camera sets the
target pitch to degrees clamped to [+0x3b0, +0x3ac], turns the view to it at once (the wanted, previous and own
positions all set to the result) and clears the sprint latch +0x466. Confirmed (code).
CamSetFollowHeading(degrees) (0x0011c2f0, 14 calls in level5, level80 and level87, all after a scene:
170-200, once 0) is relative to the target's facing, not a world heading. It snaps player 0's look-at point and
calls Cam_Follow_PlaceBehind(degrees, −1) (0x00124f38) on every follow camera, the same placement as
CameraReset with 180 (above). Cam_Follow_PlaceAtHeading (0x001250a8) builds the direction D = the first
listed target's forward (Quat_AxisY of its transform-table quaternion, 0x00714b10 + index × 0x20; world +y
when there is no target) turned by degrees about −z (Quat_FromAxisAngle 0x00335ea0 uses the axis
(0, 0, −1) at 0x00511710), so a positive angle turns clockwise seen from above, as the characters' headings do.
The camera's orientation is turned so that its backward axis lies along D, then the camera is put at the look-at
point + distance × D and pitched to the target pitch. So 180 stands it behind the player looking where he faces,
190 behind and 10° round to his left (clockwise from his facing, seen from above), 0 in front looking at his face.
Confirmed (code).
In detail, confirmed (code):
- Player 0's follow camera: the look-at point is snapped (
0x00127d48) and copied to the previous look-at points+0x240and+0x270. - For each player's follow camera,
Cam_Follow_PlaceBehind(degrees, −1)(0x00124f38), the same call asCameraReset's:- Distance: the current one (
+0x3e0, the sprint zoom's saved edge, when it is set; else the camera's distance from the look-at point,+0x260to+0x180) clamped to the band[+0x32c, +0x330], then rounded to a preset: at mostmin + (default − min) / 2→ the minimum (zoom step the default); at mostdefault + (max − default) / 2→ the default (zoom step the maximum); else the maximum − 0.5 (zoom step the minimum). The band is moved to that distance .. distance + min(0.5, max − min), and the lower pitch limit follows (0x001250a8with its last argument 1). - Heading:
degreesis relative to the target's facing, not a world heading: the target's forward (the y axis of its transform-table rotation,0x00714b10) is turned bydegreesabout −z (Quat_FromAxisAngle0x00335ea0uses the axis at0x00511710, (0, 0, −1)), so a positive angle turns clockwise seen from above; the camera is put on that side of the look-at point: 180 is behind, 90 on the target's right, −90 on his left. With no target in the list the world's +y is turned instead. - Pitch: the view is turned to the current target pitch
+0x3b4(the target pitch is not reset). - The camera is placed at the look-at point − forward × distance (the held position
+0x260), and the wanted+0x250, last-update+0x1e0and allowed+0x2b0positions are all set to it (no lag, no collision memory);+0x46bis set for one update (it keeps the next update's lag from running).
- Distance: the current one (
Cam_Follow_SetHeading (0x001255f8, which stands the camera at the band's near edge and runs a 1/60 s update) is
not this binding's: only the activation step and the co-op view change call it.
Script calls in level95¶
Every camera call of level95.lua, level95_clubhouse.lua, level95_coney.lua and level95_workout.lua (read from
the scripts' bytecode, as for level99). The hub plays the same follow camera at every checkpoint. Confirmed (code):
| Call (where) | Effect |
|---|---|
CameraCreateFollow("follow", player), CameraMakeActive(follow, 0), CameraReset(follow) (AddCameras, from RunLevel) |
as level99: CfgFollowCamera(3, 6.6, 4.8, 13, 65, 0.1, {0, 0, 1.4}, 0.2), band 3.0-3.5 m, zoom step 4.8, upper pitch limit 30°, behind the player |
CamSetFollowZoom(0) (SetupClubhouseEnvironment from StartLevel, and SetupConeyEnvironment) |
band stays at the minimum, 3.0-3.5 m: unlike level99 from checkpoint 2 (4.8-5.3 m) |
CamSetFollowAngle(10) (StartTheWalk, after StartDoorWalk; FinalStartLevel, the opening walk's callback, run by WalkFinish after ExitDoorCam) |
target pitch 10°, reached at once; any later CameraReset puts back the configured 13° (ExitDoorCam is also the door trigger's message-4 handler) |
CameraCreateLocked("Door1EnterCam" … "Door5EnterCam", …) (SetupClubhouseEnvironment) |
stored as env.Doors[1..5] = {door, cam}; the previous env.Doors cams are CamDeleted first; field of view 50, far 50 |
CameraCreateLocked("Door1LeaveCam" … "Door5LeaveCam", …) (SetupConeyEnvironment) |
the same table for the outside; far 90. Only env.FinishLeaveClubhouse calls this set-up, so no *LeaveCam exists until the player first leaves the clubhouse |
StartDoorWalk: CameraMakeActive(WalkTable.cam, 0) when WalkTable.cam is set, else CameraReset(MainCam) |
a cut to the door's locked camera, or the follow camera put behind the player at the start flag |
StartDoorWalk, leaving only, with a second player: CamEnable(3, false), CameraReset(MainCam) |
switch 3 is the two-player view rule (switches): nothing with one player |
ExitDoorCam(flag, human): CamEnable(3, true), CameraMakeActive(MainCam, 0.75), CameraReset(MainCam) |
back to the follow camera, behind the player, with a 0.75 s blend; ignored when human is given and is not a player; with a flag it also clears the flag's message-4 handler and turns its sphere off |
CameraSetClipping(MainCam, 0.1, 50 / 90 / 350) |
far clip 50 in the clubhouse, 90 on Coney Island, 350 in one scene set-up |
CamEnable(0, true / false) (ActivateWarchief / DeactivateWarchief) |
right stick and zoom buttons on / off around the warchief menu |
the clubhouse menus' locked cameras (MenuCam, name, options, replay, rumble, exit1, Radio, Cleon, Ajax, Vermin, the workout's three) |
cuts at fields of view 50-70, far 50 (299.6 for one workout shot) |
So in the hub the follow camera stands 3.0-3.5 m from the look-at point (the hard band 2.80-3.85 m) at a 10° target pitch after the opening walk (13° after a later reset), with the upper pitch limit 30°.
The door walks¶
A door walk is one scripted walk of the player through a clubhouse door, with an optional locked camera. Confirmed
(code), from the bytecode of level95.lua (functions PrepareDoorWalk, StartDoorWalk, WalkFinish, SkipWalk,
SkipWalkFinish, ExitDoorCam, and the anonymous env.FinishEnterClubhouse / env.FinishLeaveClubhouse):
PrepareDoorWalk(enter, loc, callback, cam)only fills the globalWalkTable:loc,enter,startandterm(fWchiefStart[loc]andfWchiefEnd[loc]whenenteris true, elsefRoamStart[loc]andfRoamEnd[loc]),door = env.Doors[loc].door,callback, andcam(the fourth argument, unchanged).-
Who calls it, three places:
Caller enterloccallbackcamStartLevel(the level's opening, when unlock 6/3 is not set andLoadLightis false; otherwiseDoQuickMapand no walk)2 WCLoc(5 on a first visit, while unlock 6/4 is not set)FinalStartLevelnil env.FinishEnterClubhouse(30 ms, or 5 s whileenv.BuildCac, afterenv.EnterClubhouse)true env.flagIndexnil env.Doors[env.flagIndex].cam, aDoorNEnterCamenv.FinishLeaveClubhouse(30 ms afterenv.LeaveClubhouse, the message-0 handler of eachfClubexit_Nflag, the door's action prompt)false env.flagIndexconey.CheckTutoriala DoorNLeaveCamThe two
envcallers first runSetupClubhouseEnvironment/SetupConeyEnvironment, which make the cams, so the cam handed over is the fresh one.StartLevelpasses no camera: the opening walk has no door shot. 3.StartDoorWalk(fromStartTheWalk100 ms afterStartLevel, or straight afterPrepareDoorWalkin theenvcallers unlessLoadLightis set): teleports the player tostart, cuts tocam(or, with no cam,CameraReset(MainCam)), registersSkipWalkas the skip callback, opensdoorto −90°, and gives the playerGoalMoveToFlag(player, term, 2, 0, 0, 0.5, 0, true)(a walk, gait 2, to within 0.5 m, then facing the flag). It setsSetMsgHandler(term, 8, "WalkFinish"). On the opening walkStartTheWalkthen setsCamSetFollowAngle(10). 4. What ends it: the flag's message 8, which the move-to-flag goal sends once when the player is inside its radius (0.5 m) and turned within 15° of the flag's heading (ai.md End,0x002da4a0); the goal plans again each time a move ends short of the radius, which is not a new walk. Or the skip button:SkipWalkteleports toterm, resets the follow camera and 100 ms later (SkipWalkFinish) callsWalkFinish. 5.WalkFinish(flag, human)first clears the message-8 handler and the skip callback, so it runs once; at the end it setsWalkTable = nil. Its camera step, only whenWalkTable.camis set: - entering:ExitDoorCam(flag, human)at once: the 0.75 s blend back to the follow camera, reset behind the player. - leaving: theDoorNLeaveCamstays on.TriggerSphereCfg(term, true, r, 0, 200)with r = 1 m (with a second player: 3, 2, 2.5, 1.5, 2 m for doors 1-5) andSetMsgHandler(term, 4, "ExitDoorCam"): the blend back starts when the player walks out of that sphere (message 4 is "left or died", TriggerSphereCfg).Then it closes the door, gives control back (
ActivateWarchief, a 0.5 s fade effect 3) and callscallback(FinalStartLevelon the opening walk:CamSetFollowAngle(10)again).
So the opening walk of level95 is played on the follow camera: no DoorNEnterCam, no blend, no switch change;
the camera is reset behind the player at the start flag, pitched to 10° and follows the walk. The walk happens once.
In Coney a walk that repeats three times means StartDoorWalk runs three times or the walk does not end on the first
message 8; the script has one StartTheWalk (scheduled once, and it sets itself to nil after running), so
speculative causes are Coney sending message 8 before the turn to the flag's heading, the env callers firing at the
level's opening as well, or the scheduled function running more than once.
At runtime in the clubhouse¶
Reference numbers to compare Coney with. Confirmed (runtime), PCSX2 2.9.94, 2026-10-07: from a level99 state
(slot 7, only read) with the scripted-pad and right-stick patches, level95 was loaded by writing its level
index (29 in the level table at W_GameState + 0x14e8) to mode 8's +0x20 (0x005e5db0) and 3 to
W_GameState + 0x14c, the two writes MenuLoadLevel makes (Front end). The level opened in the
clubhouse with the opening walk done. The camera object (*0x005d9158) was read over PINE about every 0.1 s, so the
times are wall-clock and approximate:
| Moment | Camera |
|---|---|
| standing after the opening walk, feet at (−184.92, 95.46, −194.38) | 3.50 m from the look-at point (feet + 1.4 m), pitch 10.0°, band 3.0-3.5 m, hard band 2.80-3.85 m, zoom step 4.8, upper pitch limit 30°, lower −6.58°, lag 0.22 |
| walking, left stick 50 % straight up (raw y 47), about 1.6 m/s | the distance grows to 3.69 m and the pitch drops to 9.5° while he walks; back to 3.50 m and 10.0° about 0.5 s after he stops |
| right stick 50 % right (raw x 208) for 1.0 s, player still | the view turns clockwise at about 93-96°/s (the formula gives 96.5°/s), about 100° in all; during the turn the distance dips to 3.42 m and the pitch rises to 10.24°, both back within 0.5 s of letting go |
| a run, left stick 100 % up, into a wall after 0.5 s | while he pressed into the wall the camera stood 3.85 m away (the hard band's far edge) and stayed there after he stopped, although the wanted and held positions (+0x250, +0x260) were 3.50 m away; the cause is not traced |
The first visit's opening, at runtime¶
The same load (the profile of slot 7 has not done the hub tutorial, unlockable (6, 4) locked, so WCLoc is 5),
recorded from the moment the level starts. Headings are the game's (Quat_Heading, clockwise from +y,
Maths); a recorder heading (2 atan2(q.z, q.w), anticlockwise) is 360° less this. Times are game time from
the level's start (*(0x0050b734) + 0x48, which the load sets back to 0) and updates at 30 a second; the player was
read every one or two updates, so each moment is good to about 2 updates. Confirmed (runtime), PCSX2 2.9.94, 2026-10-07;
what each step is comes from the scripts (level95.lua: StartLevel, FinalStartLevel, and the tutorial table's
RunTutorial, ShowANewItem, StopWalk and Setup), confirmed (code) for the bytecode:
| Game time (update) | What happens |
|---|---|
| 0 | the player stands at fWchiefStart_1 (−188.6, 95, −194.38), where AddWarchief makes him |
| 0.10 s (3) | teleported to fWchiefStart_5 (−185.2, 112.7, −193.79), facing the flag's 182°; brain "dead" (+0x09 = 1) |
| 0.43-0.70 s (13-21) | turns in place towards the end flag |
| 0.73 s (22) | walk 1 starts (gait 1, then 2 from 1.23 s), straight down −y at about 1.6 m/s |
| 4.07 s (122) | stops at (−185.48, 107.63): 5.07 m walked, 0.43 m short of fWchiefEnd_5 (−185.5, 107.2, −193.8), heading 182°, inside the goal's 0.5 m radius |
| 4.17 s (125) | turned to 177.4° (recorded 182.6°; within the goal's 15° of the flag's 182°): message 8, so WalkFinish runs between 4.10 and 4.27 s (updates 123-128), then FinalStartLevel |
| 4.27 s (128) | walk 2 starts at once: FinalStartLevel sees the tutorial locked, puts the player back under script control (DeactivateWarchief) and gives him GoalMoveToFlag(player, fWchiefEnd_1, walk, 0.5 m) |
| about 7.6 s (229) | he enters the volume box vbTutorialStart (centre (−187.8, 98, −194.1), size (6, 4, 4); entered at y ≈ 102, so the size is a half-extent, inferred): tutorial.RunTutorial gives the same GoalMoveToFlag again (its Resume clears the running move; the new move action waits a random 0-250 ms first, the brain asking for speed 0 meanwhile, AI); the positions, read every 1-2 updates, show no stop here, so the walk goes on, and starts the tutorial text and shots |
| 12.21 s (366) | walk 2 ends at (−184.92, 95.47), 0.47 m from fWchiefEnd_1 (−184.9, 95, −194.3), inside the goal's 0.5 m radius: 12.2 m in 7.9 s, facing 178° (the game's clockwise heading; the recorder's 182° is anticlockwise) |
| 12.24-12.44 s (367-373) | the goal's face-the-flag turn (faceFlag true; facing 178°, he is 89° off the flag's 89°): a turn action at the standing 12° per update, which ends within 15°, at 97.6° (recorded as 262.4°); then the goal arrives, its End sends the flag message 8, and tutorial.StopWalk (the flag's message-8 handler, installed by RunTutorial) flushes the player's goals and removes the handler; he stands |
| 25.05 s (751) | brain "dead" back to 0: control returns (ActivateWarchief, at the end of the tutorial's last item) |
The tutorial (ShowANewItem, rescheduled after each item's time) is seven items, one skipped: item 1, 4.5 s, the
text LEVEL95.ACT[11] on the follow camera; items 2-5, 2.5 s each, a cut (blend 0) to the locked cameras name,
options, replay and rumble with the flags fVermin and fMenu flashing; item 6 is not defined and is
skipped; item 7, 2.5 s, the exit1 camera on fClubexit_4. That is 17.0 s; then a cut back to MainCam (no blend
and no reset) and ActivateWarchief (BrDead(player, false), right stick on). From the box at about 7.6 s that
gives about 24.6 s, as seen (25.05 s).
So in the original the opening has two walks, not three: the door walk (5.1 m, 4.1 s) and one walk to fWchiefEnd_1
(12.2 m), which the tutorial's second GoalMoveToFlag continues rather than restarts, and which ends as walk 1 does:
arrival inside the radius, the turn to the flag's heading (to within 15°), message 8. Control comes back about 25 s
after the level starts (21 s after WalkFinish). A walk 1 that ends at y 108.1 after 3.7 m is 0.9 m from
fWchiefEnd_5, outside the 0.5 m radius, so the original would not have sent message 8 there.
Blends between cameras¶
CameraMakeActive(camera, seconds > 0) makes the blend camera (type 5) current when there is a previous camera and
the new one is not itself a blend (0x0011ee08 → 0x00143078). Its update (0x00143590), each step, confirmed
(code):
- Runs the destination camera's own update (and the source's, when it is another camera), so the follow camera keeps leashing to the player during the blend.
t = min(elapsed / seconds, 1), elapsed counting bydt.- Look-at point =
lerp(start look-at, destination's look-at, t); orientation = a slerp of the start matrix toward the destination's byt(0x00336a00); position =lerp(start position, destination's position, t), then pushed out of the collision mesh (CollisionMesh_SpherePush). The start values are the source's view when the blend began: linear in time, no easing. - The field of view is the destination's; a second lens value (slot
+0x20c, inferred the far clip) ismin(current, lerp(start, destination, t)). - When
elapsed ≥ seconds, the destination becomes current directly (0x0011ee08with 0 s), which runs its activation (Setting up, step 4).
Locked cameras¶
A locked camera (type 1, CameraCreateLocked) stays where it was put, looking along its heading and pitch. Its update
(0x00135680) places it, aims it at a point 3 m ahead along its forward, keeps it out of walls with a line-of-sight
test from its position, runs 0x00135ca8 and applies the shake. Its orientation comes from three angles
in degrees, by the conventions of Scripted camera angles. 0x00135ca8 does not
track: each human CamLockLocked lists (none in level99) that comes within 0.3 m of the view's left or right edge
is pushed back inside, an invisible wall at the frame's sides (confirmed (code)). The test point is 1.4 m above the
physics body; the camera's planes 0 and 1 (camera +0x70, inward normals; left and right inferred) are tried in
turn, and the first one the point is within 0.3 m of pushes it to exactly 0.3 m inside; the sideways part of the
step is clipped by a world ray, and the feet are snapped to the ground (ray from 2.4 m up, 3.4 m long). Details in
CamLockLocked. Confirmed (code).
Scripted camera angles¶
CameraCreateLocked(name, {x, y, z}, fov, heading, pitch, roll, near, far) (binding 0x00365d38: Lua arguments 4,
5 and 6 go to +0x200, +0x204, +0x208) and CamAddPoizoPoint({x, y, z}, heading, pitch, roll, time,
callback) (binding 0x00366e18) take the same three angles in degrees and turn them into the camera's
orientation quaternion the same way: CamLocked_BuildOrientation (0x001353e0) and CamSpline_AddPointAngles
(0x00142ac8) are the same code. Confirmed (code) at both:
- Three axis quaternions, each
(axis × sin(a/2), cos(a/2))witha= angle × π/180 (sinf0x004b8c40,cosf0x004b8a70):qhabout world +z (0x00511700) by the heading,qpabout +x (0x005116e0) by the pitch,qrabout +y (0x005116f0) by the roll. q = qh ⊗ qp ⊗ qr, Hamilton products (the vector-unit sequence at0x00135568computes(w1·v2 + w2·v1 + v1 × v2, w1·w2 − v1·v2)withqhon the left, then the result timesqr). With column vectors that isR = Rz(h) · Rx(p) · Ry(r): the pitch turns about the camera's own right axis after the heading, and the roll about its own forward axis; the pitch is not about world x.q(x, y, z, w) is stored at+0x20(and+0x1f0for a locked camera, the orientation it keeps). It maps the camera's local axes to the world (Axes: +x right, +y forward, +z up), asQuat_AxisY(0x003363b0:(2(xy − wz), 1 − 2(x² + z²), 2(yz + wx))) reads it.
So, with h, p, r the script's angles:
| Axis | World vector |
|---|---|
| forward (+y) | (−sin h · cos p, cos h · cos p, sin p) |
| up (+z) | (cos h · sin r + sin h · sin p · cos r, sin h · sin r − cos h · sin p · cos r, cos p · cos r) |
| right (+x) | forward × up; with r = 0 it is (cos h, sin h, 0) |
- Heading 0 looks along world +y and grows anticlockwise seen from above (90 looks along −x). This is the
opposite way round from the characters' headings and
Vec_FromHeading((sin h, cos h), clockwise, Maths): a camera headinghlooks where a character heading−hfaces. - Pitch positive looks up, negative looks down: every
level99locked camera that looks down at the street has a negative pitch (−2.7° to −27.7°). - Roll positive tips the camera's top toward its right (+x). Of the 362
CameraCreateLockedandCamAddPoizoPointcalls in the disc's scripts (constant arguments), 7 pass a roll, all locked cameras and all small:level87's tag camerasone−0.1°,ten−4.6°,eleven0.9°,thirteen0.4°;level95's two new-item shots 0.4°;level84_final'sMercyCam0.4°. At 0.4° the horizon tips about 2 pixels across half the 640 width (320 × tan 0.4°), so onlytenshows clearly. The other rolls in the game: the mugging shot's Dutch tilt of 15° or 11° (Mini-game and mugging cameras), and the follow camera's eased roll (Cam_Follow_EaseRoll0x0012ac58:q= orientation ⊗ (0, sin(r/2), 0, cos(r/2)), a turn about the camera's own +y, its view axis), whose target+0x404only the constructor writes (0), so it stays 0. - What the view needs for a roll: nothing beyond the camera's own axes. The original draws through the current
camera's orientation quaternion (
+0x20): the view matrix below takes up =Quat_AxisZ(q)with right and forward from the sameq, so a roll tips the picture with no separate step. A port that rebuilds the view from the eye, the look-at point and the world's +z drops it; one that carriesQuat_AxisZ(q)(orqitself) to the renderer keeps it. To see it, uselevel87's cameraten. - The locked update (
0x00135680) then rebuilds the same orientation each frame: the eye is the position (+0x1e0, also+0x10), the target the eye + 3 m ×Quat_AxisY(q), the up hintQuat_AxisZ(q)(0x00336458), throughMat_LookAtUp(0x00337168) andMat_ToQuat, so the roll survives (the default up hint ofMat_LookAtwould drop it). Its look-at point is that target. - The view matrix (world to camera) is the inverse of
[right, forward, up]placed at the eye: camera-space(x, y, z) = (right · (P − eye), forward · (P − eye), up · (P − eye)), with +y the depth; the lens is on The streamed world.
Runtime check (confirmed (runtime), PCSX2 2026-10-07, a copy of the user's slot 10 in level99 checkpoint 3,
every locked camera object read from the handle table 0x006ebd38 slots 0x3c-0x6b). The angles at
+0x200-+0x208 are the script's; +0x20 equals the formula above to 5 decimals for all eight, and each
camera made current (player 1's current-camera word 0x005d9150 pointed at it) showed its shot:
| Camera | Position | h, p, r | fov / far | Quaternion +0x20 (x, y, z, w) |
Forward |
|---|---|---|---|---|---|
VerminCar |
60.925, 43.425, 2.146 | 151.47183, −11.52631, 0 | 65 / 107.3 | −0.02474, −0.09732, 0.96427, 0.24515 | −0.4680, −0.8609, −0.1998 |
PedCam |
44.246, 39.242, 1.752 | 49.16566, −2.67624, 0 | 50 / 150 | −0.02124, −0.00971, 0.41589, 0.90911 | −0.7558, 0.6532, −0.0467 |
VerminWait |
50.613, 43.172, 1.936 | 21.78949, −6.02318, 0 | 50 / 72.6 | −0.05159, −0.00993, 0.18874, 0.98062 | −0.3691, 0.9234, −0.1049 |
FenceCam |
47.497, 37.941, 3.507 | 177.78334, −11.85450, 0 | 50 / 150 | −0.00200, −0.10325, 0.99447, 0.01924 | −0.0379, −0.9779, −0.2054 |
ClimbPoizo |
64.084, 15.313, 9.375 | 143.57188, −27.69311, 0 | 50 / 150 | −0.07480, −0.22733, 0.92229, 0.30348 | −0.5258, −0.7124, −0.4647 |
JumpCam |
57.213, −1.465, 6.173 | 88.01634, 0.31346, 0 | 50 / 150 | 0.00197, 0.00190, 0.69476, 0.71924 | −0.9994, 0.0346, 0.0055 |
VerminFencePoizo |
25.282, 6.984, 2.012 | −90.95051, −3.46206, 0 | 50 / 150 | −0.02118, 0.02154, −0.71262, 0.70090 | 0.9980, −0.0166, −0.0604 |
BreakFencePoizo |
23.674, 0.423, 1.087 | 178.03990, 4.13726, 0 | 50 / 150 | 0.00062, 0.03609, 0.99920, 0.01709 | −0.0341, −0.9968, 0.0721 |
All eight have near 0.1. In level80 (checkpoint 1, the "Let's Go" tutorial) the current camera is StartCam1
((−194.746, 130.353, 2.699), h −163.123, p −3.893, fov 65, far 150): +0x20 reads (0.00498, −0.03360, 0.98860,
−0.14667), the formula's quaternion negated (the same rotation; Mat_ToQuat picks the sign), and level2's first
camera StartLock ((453.389, 203.602, 15.861), h −111.904, p −19.474) reads (−0.09469, 0.14013, −0.81663, 0.55182),
the formula's to 4 decimals; confirmed (runtime).
Worked example, FenceCam (level99_lesson2, checkpoint 3, the shot of Vermin going
over the chain-link fence): CameraCreateLocked("FenceCam", {47.497, 37.941, 3.507}, 50, 177.78334, −11.85450, 0,
0.1, 150). qh = (0, 0, sin 88.892°, cos 88.892°) = (0, 0, 0.99981, 0.01934); qp = (sin −5.927°, 0, 0, cos
−5.927°) = (−0.10326, 0, 0, 0.99465); q = qh ⊗ qp = (−0.00200, −0.10325, 0.99447, 0.01924), as read. Forward =
(−sin 177.78° · cos 11.85°, cos 177.78° · cos 11.85°, sin −11.85°) = (−0.0379, −0.9779, −0.2054): south, 11.9°
down. Up = (−0.0079, −0.2053, 0.9787), right = (−0.9993, 0.0387, 0); look-at point (47.383, 35.007, 2.891), as
read at +0x180. View translation (right, forward, up rows times −eye) = (45.994, 39.622, 4.733). The frame
looks down the alley at the fence from above head height; read with the pitch's sign flipped it would look 11.9°
up, over the fence into the buildings.
Path cameras¶
A path camera (CamSetupPoizo, points added with CamAddPoizoPoint / CamAddPoizoPointCam) holds at most 8 points
and moves along a Catmull-Rom curve through them, slerping between the points' orientations; a point's time is the
time to the next point. Its update (PoizoCam_Update, 0x001426c0) runs only while it is player 0's current
camera, so a script makes it active itself. Confirmed (code); details per call on
Cameras.
Rail cameras¶
A rail camera (type 9, one per player, Camera_GetPlayerRail 0x0011fbb0) slides along a polyline of up to 16
points while it frames its targets: the chases of level2, level3 and 17 later levels. Confirmed (code) at the
addresses cited; the bindings' arguments are on Cameras.
Set-up. CamSetupRail (Camera_SetupRail 0x0011cce8) names the
camera, adds the human to the camera target list (CameraTargets_Add(human, 1)), sets the field of view, near and far
planes (far at most 150) and the look-at offset (+0x320, its z also the start of setting 9), then CamRail_Reset
(0x0013b2b8): no points (+0x350 count, +0x352 current segment), mode 0 (+0x3ed), the distance, setting 3 and 4
targets 0, the height target −1 (off), the angle targets off (−FLT_MAX), all ease times 0, the field-of-view target the
current one, and switches 7 and 8 back on. CamAddRailPoint appends
points (+0x1e0, 16 bytes each). The camera does nothing until a script makes it current (CameraMakeActive(rail,
seconds, nil, player)).
Settings. CamModifyRail(setting, value, seconds, player)
(Camera_ModifyRail 0x0011d228) stores a target and an ease time; every update CamRail_EaseValue (0x0013f930)
moves each current value toward its target by (target − current) × dt / time left and takes the frame's time off,
so the value arrives linearly in seconds (0: at once).
| Setting | Target / time / current | Unit | Used by mode 0 as |
|---|---|---|---|
| 0 | +0x378 / +0x3a8 / +0x3c8 |
m, 0 = off (negative stored as 0) | the most the camera stays from its target point in plan |
| 1 | +0x37c / +0x3a4 / +0x358 |
m, negative = off | the most the camera stands above the first target's feet |
| 2 | +0x374 / +0x3a0 / the lens |
degrees, negative ignored | field of view |
| 3 | +0x380 / +0x3ac / +0x360 |
m | the target point's shift along the rail |
| 4 | +0x384 / +0x3b0 / +0x364 |
m | the look-at point's further shift along the rail |
| 5 | +0x388 / +0x3b4 / +0x368 |
degrees, below −360 = off | switches the rail to mode 3 (0x0013fac8) and is that mode's view pitch (mode 3) |
| 6 | +0x38c / +0x3b8 / +0x3d0 |
eased (0x0013d35c) but never read: nothing else touches +0x3d0 after the constructor |
|
| 7 | +0x390 / +0x3bc / +0x3d4 |
m | not read by modes 0-2; in mode 3 the margin the keep-in-frame step (0x00140d68, at 0x00141814 and 0x001418b8) keeps each target inside the view's two side planes, in place of the target's radius (a player's scale × 0.5) |
| 8 | +0x394 / +0x3c0 / +0x36c |
degrees, below −360 = off | a fixed pitch for the look-at (below) |
| 9 | +0x398 / +0x3c4 / +0x328 |
m | the look-at offset's height above the targets |
Settings 0 and 1 start from the camera's present distance and height when they are switched on from off, and a height set negative eases back to the present height before it switches off. Setting 2 eases the lens itself.
One update (CamRail_Update, 0x0013d010, skipped when the frame time is 0), in order:
- Ease the settings (above) and the lead (
+0x35ctoward+0x370,CamLeadRail). - Targets (
CamRail_GatherTargets,0x0013b7b8): only entries 0 and 1 of the shared target list (0x005d91a8, up to 4) are read, players or not, in order (+0x340,+0x344, count+0x348). A resolved entry is kept at once when switch 3 is on, or when switch 12 is on, the rail is in mode 0 and switch 8 is on (both default on); otherwise it must passCamera_HumanCounts(0x00123500: not down or dead, and not cuffed unless unlock 6/15 and an item count say so), which has no player test, so a standing non-player counts. With switch 3 on, unless player 0's current camera is type 5 or (one player camera, or as many views as cameras) and both view flags are set, each rail keeps only the entry of its own player index (none when the list is shorter). When the count grows,+0x3e5and (not in mode 3)+0x3e7are set and the split timer is armed. None: the update stops here. So a one-player list [player 1, Diego] with switch 3 off gives two targets, the player first. - The target point (
CamRail_UpdateTargetPoint,0x0013bad0)P= the plain mean of the targets' transform-table positions (their feet), each weighted equally. In mode 0, withdthe current segment's direction flattened to the ground plane and normalised: - the look-at point starts as
P;P.z+= the offset's z (setting 9); - setting 3 non-zero:
P+=d× setting 3, and the look-at point =P+d× setting 4; - setting 3 zero, one target: the look-at point += the whole offset
(x, y, z)when setting 4 is 0, else its z and thend× setting 4; - setting 3 zero, two targets: the look-at point =
P.
In modes 1-3 P += the offset and the look-at point = P (with two targets, modes 1 and 2 first pull P back
toward the camera when the targets are spread wider than the camera's distance to them). With a split screen
(0x0050b1a4 = 0 and the view's +0x44 ≠ +0x48) settings 3 and 4 and the offset's x and y are halved.
In modes 0-2, unless +0x3e8 is set or the first target has a state flag of 0x1c00000000, 0x0013bfe0 then
damps P and the look-at point toward last frame's values. With Δ = new − old, h its length in plan and
dz its rise (the rise tests are skipped when the segment has no length in plan):
dz > 0.5 → old + Δ × 0.5, then its z keeps only a quarter of its rise; else h > 1 → old + Δ × 0.25;
else dz > 0.1 → with f = 1 − 1.875 (dz − 0.1), old + Δ × min(2f, 1), then its z keeps f of its
rise; else h > 0.4 → old + Δ × (1 − 1.25 (h − 0.4)); else no damping. While +0x3ec is 1 the look-at
point moves only 40% of the way each frame, until it arrives (then +0x3ec = 0).
4. Placement by mode (+0x3ed): 0 CamRail_UpdatePosition (0x0013d6b0, below), 1 and 2 CamRail_PlaceLeading
(0x0013e708, set by CamLeadRail: the camera stands the lead ahead of or behind the target's place on the
rail, below), 3 0x0013dcc8 (below).
5. Split-screen extras (0x0013cf20, only with two player cameras); a timer at +0x3e0 fades the co-op flash,
alpha 225 on both colour controllers' byte +0x1b4 (Two players); then the camera's distance to
P is kept at +0x3d8 and the flags
+0x3e5, +0x3e7, +0x3e8, +0x3e9 cleared.
Mode 0 placement (CamRail_UpdatePosition):
- Unless the camera is held (
+0x3e4), choose the segment (CamRail_ChooseSegment,0x0013fba0): the rail point nearestP(3D) starts the segment, the last point giving the last segment (a tie between two points takes the later one only whenPprojects inside its segment); then, whenPprojects inside that segment (0 ≤ t ≤ length) it stays, else it moves to the previous or the next segment, whicheverPprojects inside. A change of segment sets a blend+0x3cc= 0.1. - Project
Pon the segment (CamRail_ProjectOnSegment,0x00140308):Q= the foot of the perpendicular, clamped to the segment's ends. With setting 0 on, whenQis farther than it fromPin plan,Qmoves towardP(in plan) until it is exactly that far: the camera leaves the rail to stay within reach. - While the blend is on,
Q= camera + (Q− camera) × blend and the blend grows by 5% a frame until it reaches 1 or the step is under 1 cm: a soft hand-over between segments.Q+= the vector at+0x330, which is always zero: only the constructor (zero,0x0013b25c) andCamRail_CopySettings(0x0013b3d0) write it (mode 3 also subtracts it,0x0013e4a8). - Height: with setting 1 on,
Q.zis lowered to the first target's feet + setting 1 when it is higher (a ceiling, it never raises the camera). The first target is entry 0 of the gathered list (+0x340, read at0x0013d848): the first kept entry of the shared list, so player 1 on a [player, Diego] rail. The camera stands atQ. - Look-at: with setting 8 on, the look-at point is replaced by the point 6 m from
Qtoward the targets in plan, raised by 6 × tan(setting 8). The camera then faces the look-at point. - Ends: when
Qis the rail's first point and the look-at point lies behind the rail's start, orQis the last point and the look-at point lies beyond the end, the hold+0x3e4is set (0x0013dafc) and the camera is put back as it was at the start of the update (position, orientation,P, look-at point), then faces the look-at point again. This hold lasts one update: step 7 clears it. So the camera stands still while the look-at point is past an end and follows again on the first update it is not. - Collision (
0x0013dbbc-0x0013dc1c): when the camera is held (by step 6 or byCamLockRail), or with switch 3 off, switch 8 on and two or more targets,CamRail_CollideTwoTargets(0x00140708) runs and the hold is then set back to its value at the start of placement. OtherwiseCamRail_Collide(0x00140830) runs for the first target (below); it does not touch the hold.
The hold (+0x3e4, confirmed (code) at the addresses cited):
- Mode 0 held at the start of placement (the value reaches
CamRail_UpdatePositionas its fifth argument,t0=+0x3e4≠ 0 at0x0013d498): steps 1-6 are skipped. The camera's position, orientation,Pand look-at point are put back to their values at the start of the update (last update's), the camera faces the look-at point and the frustum is rebuilt. Step 7 then runsCamRail_CollideTwoTargets: when a target is out of frame (0x00140ad0) it pushes the targets back into the frame (0x00140d68, below); a held camera is not put back, it stays. The hold is written back as 1, so aCamLockRail(true)lock lasts untilCamLockRail(false). - Every writer: the constructor (0,
0x0013b1fc);Camera_LockRail(0x0011d180, the script's value);CamRail_CopySettings(0x0013b498, copies player 1's, so a joining player inherits his lock); the mode 0 ends test (1) and step 7 (the start value); mode 3 (1, below).CamRail_Reset(0x0013b2b8),CamRail_Activate(0x0013b508),CamSetupRailand switching modes never write it: a lock survivesCamSetupRailandCameraMakeActive. - Modes 1 and 2 (
CamRail_PlaceLeading,0x0013e708) never read it: a lock does not stop them. - Mode 3 (
0x0013dcc8) sets it when the camera reaches the rail's first or last point (squared distance under 10⁻¹⁰,0x0013e260,0x0013e2ec) and, while it is set, no longer moves along the rail toward the target: the camera keeps its position and its look-at point, still reframing (0x00140d68) and shaking. Mode 3 never clears it, so a mode-3 camera that reaches an end stays there untilCamLockRail(false)(inferred from the list of writers).
Collision (CamRail_Collide, 0x00140830, confirmed (code))
does no world or physics test.
Its one caller is mode 0 (0x0013dc18). Let A be the first target's position in the transform table
(0x00714b00 + index × 0x20) plus the look-at offset +0x320, and B his position as copied at the start of
his own update (human +0x10, vtable +0x9c) plus the same offset. The helper 0x004dbb68 then acts by switch 2
(+0x3e6):
- Switch 2 off (the default): the camera keeps its distance from
P. Withm= max(2, 0.4 × (camera z −P.z)), when the camera is nearer thanmtoPin plan, it moves along the current segment's direction bymminus that distance: toward the segment's end, or toward its start when it is withinmof the end (segment length −m≤ its distance from the segment's first point). One step, with no easing or timer; it can leave the camera still nearer thanm. The player is not moved. The camera then faces the look-at point and the frustum is rebuilt. - Switch 2 on (
level83_c5_apartment.lua): the player is kept at least 2 m in plan from the vertical planes through the previous, current and next segments. Each plane is unbounded, built from the segment and world up, with its positive side the oneBis on. In turn, whenAis nearer than 2 m to a plane on that side,Ais pushed along the plane's normal to 2 m (plus 10⁻⁵). If any plane pushed, the player is placed atAminus the offset with his current rotation (Human_SetTransform0x0023d440, his vtable+0x6c). When he is jumping or in a long fall (state flags0x400000000or0x1000000000) he is landed first (Human_Land0x0023e090). The camera is neither moved nor turned. CamRail_Collidealso builds a vertical plane 2 m in front of the camera along its level forward and passes it with a 0.3 m margin. The helper uses that plane only for a rail in modes 1-3 (it would pushAto 0.3 m beyond it), which never callCamRail_Collide: the 0.3 m and 2 m have no effect.
Keeping the targets in frame (confirmed (code) at the addresses cited). Two helpers,
reached only through CamRail_CollideTwoTargets (mode 0, step 7) and mode 3's placement (0x0013e088). Planes are the
camera's frustum planes from Camera_BuildFrustum (0x00120c50), read through vtable +0x174(i), normals inward:
0 left, 1 right, 2 near, 3 bottom, 4 top, 5 far. For each target, A is his transform-table position (feet) plus the
look-at offset +0x320, B his start-of-update position (human +0x10) plus the same offset, and r his radius:
a player's scale × 0.5, or for a non-player (+0x1b0 = −1) his capsule radius (Human_GetCapsuleRadius).
CamRail_TargetsInFrame(0x00140ad0) is a test, one pass over the targets: false as soon as one hasn · A − d < rfor plane 0, 1 or 3 (left, right, bottom; never the top), or, with switch 2 (+0x3e6), for the vertical plane 2 m in front of the camera along its forward axis. The 0.3 its caller passes is the radius only when its last argument is 0;CamRail_CollideTwoTargetspasses 1, so the targets' own radii are used and the 0.3 is dead.CamRail_CollideTwoTargets(0x00140708): when that test fails and the camera is not held, the camera's position, orientation, target pointP(+0x180) and look-at point (+0x2f0) go back to their values at the start of the update and it faces the look-at point again (frustum rebuilt); held or not, it then calls the keeper. The camera is never pulled back or up.CamRail_KeepTargetsInFrame(0x00140d68) moves the targets, not the camera: one pass, one correction of each kind per target, no loop. Before it,zmax/zminare the highest and lowest of all the targets' feet and start-of-update heights, andzmax+= max(offset z+0x328, 1 m). Per target, in order:- With switch 2 on: in mode 0, the same 2 m push off the previous, current and next segments' vertical planes as
CamRail_Collide(above); in modes 1-3,Ais pushed beyond the plane 2 m in front of the camera byr. - Sides: if
Ais within the margin of the left plane (else the right; only one per update), its move fromBis split into the part along the plane's normal and the rest; the normal part becomes the margin (+ 10⁻⁵) inside the plane, and the rest is cut short by a ray fromB(mask0x200, as long as the rest + the margin) so it stops the margin before a wall. The margin isr, or setting 7 (+0x3d4) in mode 3. - Bottom (both callers pass this on): when
Ais withinrof the bottom plane it is moved across it by the shortfall, keeping its distance from the camera (inferred from the vector steps), again cut short by a ray. - Line of sight, when any step moved him (or in mode 3 on player 0's current camera while
+0x3e5is set): a ray (mask0x200, plus bit 0 in mode 3) from the point 2 m in front of the camera, atA's height (the camera's in mode 3), towardA, as long as min(that distance, 0.7 × the far plane) + 3r; on a hitAis putr(along the face's normal) before the face. - When a step moved him or the ray hit:
A− offset is his new place; his height is found by a ray straight down fromzmax,zmax − zmin + 1m long, and set to the hit + 0.01 m (kept when nothing is hit). If he is jumping or in a long fall (state flags0x1400000000) he is landed (Human_Land(1, 1)) and his height moves only 30% of the way. He is placed there with his current rotation (Human_SetTransform, vtable+0x6c).
- With switch 2 on: in mode 0, the same 2 m push off the previous, current and next segments' vertical planes as
So on a two-target rail every target, non-players included (level5's Diego), is pushed back inside the left,
right and bottom edges of the view and kept out of walls between him and the camera; the camera only stops.
Mode 3 placement (CamRail_PlaceMode3, 0x0013dcc8; confirmed (code) at the
addresses cited unless marked). CamModifyRail(5, angle) at or above −360 sets the mode (+0x3ed = 3) and the angle
(0x0013fac8); below −360 it goes back to mode 0. Only the five Armies of the Night levels (level60-level64) use
it, each with CamSetupRail("rail", player, 22, {0, −0.25, 1.25}, 0.5, 90), then CamModifyRail(5, −7, 0) and
CamModifyRail(7, 0.5); their gang scripts (level60_mothers, level61_hihats, level63_orphans,
level64_riffs) later set setting 7 to −10 or −15 (a survey of the disc's scripts, values only). The mode's pieces:
- The angle is a pitch. The look-at point
+0x2f0is always put 2 m from the camera in plan and 2 × tan(angle) above it, so the camera looks at a fixed pitch: −7° in those levels, looking down. The camera itself stays on the rail (setting 1 aside, below); nothing raises it. - Start, once, on the first update after
CamRail_Activate(0x0013b544sets+0x3e7; the update clears it at its end,0x0013d678, andCamRail_GatherTargetssets it again only outside mode 3,0x0013b9a0): the base speed+0x1a4= 1 m/s;P= the targets' mean (CamRail_UpdateTargetPoint), segment chosen andPprojected (CamRail_ChooseSegment,CamRail_ProjectOnSegment); the camera is placed at that pointQ; the look-at point at 2 m fromQtowardPin plan, at the pitch; the goal+0x310= the look-at point.+0x3ea= 1 when the first target stands to the left of the segment's direction (z of the cross product > 0). Switching setting 5 on later does not restart: the next start is the next activation. - Projection in mode 3 (
0x00140308): along the segment in plan (the plan distance divided by the plan length of the segment's unit vector, so the height follows the rail), and clamped only before the rail's first point and past its last; on an inner segment the point may run past the segment's end. Setting 0's reach is applied in mode 0 only. - Each later update, with at least one target (none: the camera keeps its place, still shakes and faces the
look-at point):
- Which target. With two or more, the one farthest back along the current segment's direction (smallest signed distance of his feet from the segment's start along it); with one, entry 0.
CamRail_KeepTargetsInFrame(0x00140d68, above) moves the targets back into frame, with the margin setting 7 and the line of sight from the 2 m point while+0x3e5is set (the update a target joined). Mode 3 passes+0x3eaand its negation ina2/a3, but the keeper overwrites both registers before reading them (0x00140dd0-0x00140dd4):+0x3eais dead.P(+0x180) = that target's feet + (0, 0, offset z+0x328): only the offset's height; in mode 3 the mean and the damping of update step 3 are replaced.- Goal. Segment chosen for
P,Pprojected toQ'; the goal =Q'+ 2 m towardPin plan, at the pitch. It is taken (+0x310= goal) only when it lies ahead of the look-at point along the segment's direction (plan dot product > 0): the camera never goes back along the rail. - Ends. On segment 0, a camera within 10⁻⁵ m of the first point, or on the last segment within 10⁻⁵ m of
the last point, sets the hold
+0x3e4(the hold). Inferred: a camera that starts clamped at the first point holds at once, so these levels must start the player ahead of it. - Move, when the goal was taken, the hold was off at step 4 and is still off: with
v= the target's stored velocity (Task_GetVelocity0x003a1f00: the task's+0x30, zero while its flags+0x54have0x600; for a human what the locomotion writes, so 0 while a clip alone moves the body, and not the move made by a push) along the segment's direction in plan, the look-at point moves toward the goal by min(0.4 × the gap, (max(v, 0) + 1 m/s) ×dt). So it moves 1 m/s faster than the target goes forward, never more than 40% of the gap in one update. The segment is then chosen again for the moved look-at point (so it crosses joints on its own) and the camera = the look-at point projected on the rail. - Segment blend. While
+0x3cc> 0 (set to 0.1 by a change of segment): the camera = old position −+0x330++0x3cc× (new − old), and+0x3cc× 1.15 each update, until it reaches 1 or the step is under 0.3 m in plan (then the new position is taken at once).+0x330is always zero (only the constructor andCamRail_CopySettingswrite it), so the subtraction does nothing. - The look-at point is put again 2 m from the camera in plan, toward where it was, at the pitch.
- Without a move (no goal ahead, or held) the goal is set to the look-at point and the camera keeps its place.
- Finish:
+0x2e0= the position; with setting 1 above 0 the height is capped at the first target's feet + setting 1; the camera is placed; the shake (0x00121298, given the look-at point+0x2f0) moves the look-at point, so it turns the view; the camera faces the look-at point (Mat_LookAt) and the frustum is rebuilt.
So the Armies levels' camera trails the hindmost fighter along a one-way rail at a fixed downward pitch, catching up at his forward speed + 1 m/s, and pushes everyone back inside the view (0.5 m inside the sides; with −10 or −15 during the gang fights, inferred: up to that far outside, so the side push stops).
Leading placement, modes 1 and 2 (CamRail_PlaceLeading, 0x0013e708, set by
CamLeadRail(lead, seconds, ahead); confirmed (code) at the cited
addresses unless marked). s = +1 in mode 1 (ahead), −1 in mode 2 (behind); the lead is +0x35c. The target point
P comes from update step 3 as in mode 0 (mean, offset, damping); the hold is never read.
- Segment.
CamRail_ChooseSegment(P)as in mode 0, then, whenP's plan distance from the segment's start is more than the segment's plan length, step forward whilePlies beyond the next point along the segment (plan dot product > 0, up to the last segment); then step back whilePlies before the segment's start. - Foot and lead point.
u= the segment's direction in plan;F= start +u× (P's plan distance along it), at the start point's height;B=F+s× lead ×u. With two or more targets,Bmoves on bys× the farthest any target's feet are ahead ofPalongu(positive values only; in mode 2 the first pass never adds one, as decompiled, so trailing adds nothing there). - Lead point's segment. The same forward and back steps for
B. If they change the segment,FandBare worked out again on the new one (the farthest-ahead target added again, in either mode) andBis the mean of the old and newB: the camera does not jump at a joint. - Rail point.
t=B's plan distance from the segment's start ÷ the segment's plan length, a length (the plan length ofB− start, never negative) and not clamped on an inner segment. So a lead point before the start gives the sametas one that far after it: in mode 2 withPless than the lead past the first point, the camera stands (lead −P's distance) past the start, mirrored, not before it; and withPup to the lead behind the start, lead + that distance past it. Ends: on segment 0 withPmore than the lead behind the start,t= 0 (the first point); on the last segment, unlessPis still before the last point andFis at least the lead from it, the camera takes the last point (t= 1).C= the 3D lerp of the segment's points byt, so the height follows the rail. - Switch 7 (
0x0050b2ac, default 1, set to 1 again by everyCamSetupRail,0x0013b2b8): when on, the camera leaves the rail:X=P+ (lead + the farthest-ahead extra) × the plan direction fromFtoC, atC's height. So it stands exactly the lead ahead of (or behind) the target in plan, beside the rail point. Off,X=C, on the rail.level54_parkturns it off (CamEnable(7, nil, 0)). - Collision, only when
X≠C: a world ray (CollisionMesh_RayCast, mask0x200) fromCtowardX; on a hitXis pulled back towardCto stop short of the face (by max(0.5, 2 × the value of vtable slot+0x5c) plus that value; inferred: the near plane), and a second ray back fromXcan shorten it again. ThenPis pushed out of the world by a 0.5 m sphere (CollisionMesh_SpherePush) and the look-at point+0x2f0=P. With the switch off the look-at point is update step 3's. - Finish:
+0x2e0=X; with setting 1 above 0,X's height is set to the first target's feet + setting 1 (a fixed height here, not mode 0's ceiling);Camera_PushOutOfHumans(0x00122728, 3 m); placed; faces the look-at point; with+0x3e8set, the position and orientation are copied to the caller; frustum; with two or more targetsCamRail_CollideTwoTargets(0x00140708, frame). While a shake runs the view is faced again after it and+0x3ec= 2; on the update it stops+0x3ecgoes 2 → 1, so the look-at point eases back at 40% an update (update step 3).
Levels calling CamLeadRail (survey of the disc's scripts, values only): level2 (3 m), level9 (4 m),
level51 chapters 8 and 9 (12 m, ahead), level54 and its alley / park scripts (7 and 8 m, ahead false,
and the chase's own chase.CUR_LEAD_DIST), level81_chase (14 and 15 m), level84_chase (12 m, ahead).
In level3's chase (level3) the rail runs straight along y = 349 at z = 29.97, beside the
rooftops the player runs along; the main framing (setting 3 = −8, 4 = 6, 0 = 5.5, 1 = 4, 9 = 2, field of view 78°)
puts the camera 8 m behind the player along the rail, at most 5.5 m from him in plan and 4 m above his feet, looking
at a point 2 m behind him and 2 m up. Inferred from the code above with the script's values.
Fixed and third-person cameras¶
- The fixed camera (type 0,
CameraCreateFixed) stands at one point and looks at the average of the humans on the shared target list plus an offset; its target is put on that list. Confirmed (code) at0x00124690. - The third-person camera (type 16,
CameraCreateThird) sits a distance behind its target's facing, eased 10% an update toward it, a height above the look-at point and swung by an angle. Confirmed (code) at0x001205e8.
Combat camera¶
While the player fights with a lock-on, the follow camera pulls in and frames the enemy. Confirmed (code) in the
update at 0x0012bb00-0x0012bbe8; confirmed (runtime) as noted.
- When: one target, the target is player 1, the player is pad-controlled (per-player
+0x1b), and0x00233c50holds: the mode0x00510228is not 0 (1 by default;CfgAutoCloseModesets it, no script calls it), one player, the player rides nothing (+0xc4), and the player has a fight target (0x00226e60) with record flags0x8and0x4both set (L1 held at a target; with mode 2, also any update in the lock-on movement state0x00241b90). Confirmed (runtime), slot 6 copy, L1 held facing the pedestrian: flags0xd,+0x46f1 on the third update after the press, 0 on the second after the release. Inlevel99_combat.lua's sparring L1 is disabled (EnableCommand(player, 6/8, 0)) and the camera stayed off. - On entry (
+0x46fset): the band's wanted near edge is saved in+0x3cc(the sprint zoom's saved edge if one is held, else a wanted edge in progress, else the near edge) and set to 2.4 m; unless+0x3d0holds a pitch, the target pitch becomes 15°. The band eases byd × 4.5 × dtper update (no timer, Sprint zoom step 4), so 4.8 → 4.44, 4.134, 3.874, …, 2.4 in about 50 updates (runtime, to 0.001 m); the zoom step follows it (30° below 3.72 m). - Each update while on:
0x0012e9a8takes the enemy's point (its position plus half its body point+0x4e0, the hips in the body's frame, not turned by his heading, so about 0.5 m up; an object's position + 0.5 m) and the angle at the look-at point between the camera's horizontal view and the direction to the enemy; outside 25°-29° it yaws by(angle − 27°) × 0.455toward 27°; the turn is capped at 640°/s (11.17rad/s) when the enemy is more than 29° off, not when it is under 25°. So the enemy is held 27° off the view's centre, beside the player. This counts as this update's turn: auto-follow and keep-in-view do not run. - On exit: the wanted near edge is set back to the saved
+0x3cc, which is cleared; the band eases back the same way (runtime: 2.4 → 2.76, 3.066, …, 4.8). The target pitch stays 15° until the nextCameraResetorCfgFollowCamera(runtime: 15° 150 updates later). - Shakes started while it is on are 0.66 as strong, and the pad's rumble thresholds are lowered by a quarter (Shake).
Death camera¶
CamUseDeathCamera(human, ms, ms2) (0x0011daa8) makes player 1's Cam_Failed (type 12) current at once, keeping the
camera it replaced (+0x1e4), tints the screen over ms, starts a blur after ms2, hides the HUD and removes the
overhead icons. It is the game-over shot's start (below) with the two times as arguments: the game-over shot is
(6500, 1500), Rumble Survival passes (7000, 100000). Confirmed (code) at the cited addresses, in order:
- Nothing happens unless
humanresolves and the failed camera can be made (Cam_GetFailed(1)). - View flags:
0x0050b1e8= 1 and0x0050b1ec= 0, player 1's and player 2's "view may show" flags (0x00121e30): player 1's view on, player 2's off.Cameras_DecideViews(0x00121888) sets both again on its next run (both 1 with one player; worked out from who still counts in co-op), so the write matters only until then (inferred: in co-op it drops player 2's half of the screen at once). - Player 0's current camera goes to
+0x1e4, andCamera_MakeActive(0, failed, 0)cuts to the shot. Its activation (0x001236e0) picks a target (the last target0x005d91b8when it is a player, else whichever player stands nearer the camera it replaced) and places itself. - It saves the screen tint (
+0x1f8, the manager's target tint+0x1c4), the HUD state (+0x1fc), the ambience level (+0x1f0) and the music volume (+0x1f4). - Target:
CamFailed_SetTarget(failed, human)(0x00123888) then sets+0x1e0=humanand places the camera again (a new random heading). Sohumanis the one the shot looks down on; the activation's pick is overwritten. - Tint:
ScreenFx_BlendTintTo(ms / 1000, 0x005fdeb8, 0xd0000014)blends player 1's screen-effects manager (0x005fdeb8, the colour controller the death camera watches) from its current tint to0xd0000014, the same dark red at 81% as the game-over shot (Rendering), overmsmilliseconds. - Blur pulse:
ScreenFx_StartBlurPulse(ms / 1000, 0x005fdeb8, 0, ms2)arms the same manager's blur (Graphics): its level stays 0 untilms2milliseconds have passed (+0x204, tested by0x0018d1d0against the start time+0x208), then rises to full overms/ 1000 s and holds. With Rumble's 100000 the blur waits 100 s, so in practice the arena's death has the tint and no blur (inferred). HUD_Hide(0x600840), thenHuman_RemoveOverheadIcon(0x00227388, message0x15to the icon at+0x360, Crimes) for every player and forhuman. Nothing is stopped: no brain, pad or velocity is touched here.
What undoes it. Nothing in the camera code returns to +0x1e4: activation is its only reader. The shot stays
current until a script makes another camera active (CameraMakeActive); its deactivation (0x00123ba0) then releases
it. But Camera_MakeActive (0x0011ee08) refuses any switch while the player's current camera is type 12 and the
game state's +0x14c is 1 (a failure): during a mission failure the death camera cannot be replaced, only after the
state changes (the retry sets it to 0). Confirmed (code). The HUD and tint come back only in MissionFailed_Toggle
(0x00155408) on a retry from a checkpoint, and only when player 0's current camera is still type 12: the tint is set
back at once to the saved +0x1f8, the blur pulse is ended (0x0018d058 with 0 s, reverse), and the HUD is restored
(HUD_Restore) when +0x1fc says it was shown. The saved ambience and music volume are not read back there (not traced
elsewhere).
The parts of the shot itself:
- Target (
0x001236e0): the last target (0x005d91b8) when it is a player, else whichever player stands nearer the camera it replaced;CamUseDeathCamerathen overrides it with itshuman(step 5). - Placement (
0x001238a8): the look-at point is the human's position; a ray straight up from it, near plane + 5 m long (mask0x200), gives the height: the camera sits that far above the body, less the near plane, looking straight down (a pitch of about −88°) at a random yaw (one of 36 steps of 10° from −180°). - Each update (
0x00123bf0): it turns about the vertical at 15°/s and sinks toward the body at 0.18 m/s, no nearer than 2.5 m, until player 1's screen-effects manager has faded out fully (Fade); it then ends the blur pulse at once and stops moving.
The game-over shot (Cam_GetFailed(1) in the gameplay mode's update, 0x00158728), confirmed (code) at the
cited addresses:
- When.
W_GameState + 0x14cis 1 (a failure: the players are out or busted, Combat), the level is not an Armies level, the game-state flags+0x152do not have bit 2 (with it the countdown goes to 0 at once and the menu comes with no shot), and player 0's camera is not already type 12. In player terms: - Armies levels (
GameState_IsArmiesLevel, levels 60-64) never get the shot: their failure counts down at most 90 updates (3 s, the same cap as a level completed) with the current camera still running, then pushes the Armies game-over (ANGameOver_Toggle) instead ofMissionFailed_Toggle. +0x152bit 2 means a player fell out of the world. Its only writer besides the level reset (0,GameState_ResetForLevel) isHuman_StateUpdateat0x002403ec. When player 1's or player 2's human (handles+0x228,+0x22c) is more than 20 m below the collision mesh's lowest vertex (Characters), it sets the failure reason to global string0x13(MissionFailed_SetReason,0x001d1fb8),+0x118= 0,+0x14c= 1 and+0x152= 2. It does this only while game-state+0x150has bit 2, whichGameState_ResetForLevelsets (7) and nothing else writes: always, in practice (inferred). That same update the countdown goes to 0 and the mission-failed menu is pushed: no shot, no tint, no wait.- The shot starts only while the failed camera does not yet exist (
Cam_GetFailed(0)= 0); later updates of the same failure find it and skip the start. The slot0x0050b170is cleared again byCameras_ClearSlots(0x0011ea68) andCamera_Release(0x0011e7e0), so each new level's failure gets a fresh shot (inferred). - Start, once:
Cam_GetFailed(1)makes the camera on first use (0x0050b170); player 0's current camera is kept in+0x1e4, andCamera_MakeActive(0, failed, 0)makes the shot current with no blend (a cut), which runs the activation above (target, then placement). It saves the screen tint (+0x1f8), the HUD state (+0x1fc), the ambience level (+0x1f0) and the music volume (+0x1f4), starts a 6.5 s tint blend to0xd0000014(bytes red 0x14, green 0, blue 0, alpha 0xd0: a dark red at 104/128, 81%, Rendering) and a blur pulse (1500 ms), hides the HUD and removes the overhead icons. - Placement (
0x001238a8): the look-at point is the human's transform position (the feet); a ray straight up from it, near plane + 5 m long, gives the height (hit distance − near plane: 5 m with nothing above); the camera is put that far above the feet, pitched −88.28° about its x axis (a half-angle of −0.7704 rad), then turned about its own forward axis, which points almost straight down, by a random multiple of 10° from −180° (Random_Int(36)): to the eye, a top-down shot at a random heading. - Motion, each update while rotating (
+0x1fe= 1): it turns about its own forward (view) axis at turn rate (+0x1ec, 1) × 15°/s, and its distance to the look-at point shrinks by 0.18 m/s × the step, never below 2.5 m; the camera stays on the view axis, so it sinks straight down toward the body while the picture turns. It stops (and starts the next screen pulse) once the screen effect's blend has finished (+0x1d8≥ 1 and+0x1dc> 0 on0x005fdeb8). - Length. The level-end countdown is 180 updates (6 s); cross with more than 10 left cuts it to 10 and
speeds the tint blend up to 1/3 s. Each update the ambience level is lowered toward 0.7 (by 0.7 / (0.9 × the
updates left)). At 0 the tint blend is finished, the ambience and music volumes are put back and the
mission-failed menu (mode
0xc) is pushed; leaving it for a retry puts the saved tint back and restores the HUD. Over a full 6 s the shot turns 90° and sinks from 5 m to about 3.9 m; the 6.5 s tint blend has not finished by then, so the stop in step 4 is not reached before the menu (inferred from the two lengths; the menu stops the gameplay update, so the shot then holds).
In Coney the death camera is repo:src/camera/failed_camera.h, and Cameras::startFailed() cuts to it once per
level. It keeps the base camera's lens (60°, near 0.3, far 60) because its constructor sets none of its own
(inferred). Other choices:
- Coney's reading: it stays straight above the feet as it sinks.
- Coney choice: the screen-effect stop is not read.
- The tint, blur, HUD, ambience and music parts of the start are not built yet.
- With one player, the target is always player 1.
Mini-game and mugging cameras¶
Both are per-player cameras made on first use; both start only from a single-view follow camera, return to it with a
0.3 s blend, and turn the follow camera to the shot's direction first (Cam_Follow_TurnToMatrix, 0x0012ded8) so the
hand-back does not swing. The cut in is not immediate: the start queues the new camera as the follow camera's
hand-over (+0x47c, blend +0x3c0), the follow camera turns for 0.15 s (its collision probes off), and only
then does the new camera become current, blending over 0.08 s for a mini-game shot (0 for kind 1) or 0.15 s
for a mugging (CamMini_Start 0x001363a4, CamMug_Start 0x0013804c). On the way back the mugging camera turns
the follow camera again (0.35 s, nothing queued; CamMug_Update 0x00138ec8). Confirmed (code) at the cited addresses;
the shot tables' purpose (variety) is inferred.
- The mini-game camera (
Cam_Mini, type 8,0x001361a8) frames two humans (kind 0, the mount fromPlayer_UpdateMounting) or a human and the stereo (kind 2,StereoTheft_Start); no caller passes kind 1. The shot is 3 m from a point on the first human (the mounting player's feet + 0.5 m, or the stereo + 0.3 m), at a yaw and pitch drawn at random from small tables; up to three draws are tested with a sphere sweep (0x001374d8, clear for at least 2.3 m), then a fixed shot (pitch 36°; 30° and yaw 25° for the stereo; 32° for kind 1) (shot tables). Each update turns it at up to 3 × its angle error per second to keep both in frame. The mini-game's end (0x0022d1f8) sets its done flag (+0x258), and the next update blends back. - The mugging camera (
Cam_Mug, type 7,0x00137e08, from0x002729a8) playsvags/misc/mug_introas it cuts in. The shot (0x00138078) is 2.5 m from the pair (1.8 m when the world is in the way) at a random yaw and pitch from small tables. Each update (0x00138b90) eases the field of view to 50° over 0.33 s, turns at up to 135°/s to keep both humans framed and stays 3 m from other humans (Camera_PushOutOfHumans,0x00122728). When the mugger or the victim leaves the hold it stops the intro, playsvags/misc/mug_outroand blends back.
Shot tables¶
The random draws use Random_IntRange(low, high) (0x003353f0, both ends included). Angles in degrees; "yaw" is a
turn about the vertical, "pitch" about the shot's side axis. Confirmed (code) at each function.
Mount (CamMini_PlaceKind0, 0x001363e8; +0x1e0 the mounting player):
- The point is his feet + min(max(offset z, 0), 0.5) m (+0.5 m with the usual offset), the distance 3 m
(
+0x254). - The base direction is his side axis: both sides are tested at 3 m (
CamMini_TestShot0x001374d8, a sphere sweep from the point that must stay clear for at least 2.3 m); the clear side wins, a frame-counter bit breaks a tie, and nothing is placed when neither is clear. - Up to three draws: pitch one of 10, 16.7, 27, 0, 22.5, yaw one of −22, 21, 0, 12, −14. A draw of 0 and 0 is placed without a test; any other is turned (pitch, then yaw) and tested. The fourth try is pitch 36°, yaw 0; if that fails too the shot is not placed.
Stereo theft (CamMini_PlaceKind2, 0x00136f68; +0x1e0 the stereo, whose car gives the axes):
- The point is the stereo's position + 0.3 m, the distance 3 m (tested for 2.3 m).
- One roll of the dice per shot (
Random_Int(100)): under 80 the yaw comes from all eight entries 10, −15, 90, 30, 115, 60, 75, 48, else only from the first two. - Each try also draws
+0x244= 3-7° (a float range; its sign flipped on an odd frame counter; its use is not traced). The pitch is 0 for yaws under 21.5° or over 91.5°, else one of 17.1, 21.8, 28. - Three tries, then a fixed pitch 30°, yaw 25°.
Mugging (CamMug_PlaceShot, 0x00138078; +0x1e0 the mugger, +0x1e4 the victim):
- The point is the mugger's position + the offset
+0x200, whose height starts at 1.45 m; the base orientation is the victim's turned 90° about the vertical (so the camera looks across the pair). - Pitch
+0x23c: one of 0, 19, −5, 12.8, 7, −3 (draw 0-5); 19, 12.8 and 7 also raise the point by 0.14, 0.11 and 0.075 m. Yaw+0x238: one of −4.5, 4.5, 0, 8, −8, −13.5, 13.5 (draw 0-6), only the first five when the pitch is 15° or more. - Test: a physics sphere of the near-plane radius pushed 2.5 m + near along the shot (object kinds 0xc-0xe, 0x10 and 0x1c ignored) and a world ray (materials 30 and 122 skipped) must both stay clear for at least 1.8 m; the camera goes to the ray's hit − the near plane, with the point raised by 2.5 × tan 5°.
- Two tries: the drawn shot, then the plain one (victim turned 90°, no pitch or yaw); with neither the camera is
not placed (
+0x24c= 1). - A placed shot is rolled (a Dutch tilt), confirmed (code) at
0x00138078and0x00138b90: - The angle (
+0x234, radians): 15° (0.2618), or 11° (0.1920) when bit 0 of the game time in ms is set; then negated when bit 1 is set. The game time is theGameTimer's+0x48(0x0050b734, read through its vtable+0x34,0x004dc9a8, which returns+0x48), so the four cases (+15°, +11°, −15°, −11°) come from the shot's start time, about equally often. - How it is applied: the orientation is multiplied on the right by a rotation about the camera's own +y
(its view axis; the constant
0x00511730= (0, 1, 0, 1)),q' = q ⊗ (0, sin(r/2), 0, cos(r/2)), as the scripted roll is (Scripted camera angles): a positive angle tips the top of the picture toward its right. - Held for the whole shot: each update (
CamMug_Update) rebuilds the orientation by looking from the eye at the aim point+0x210with the world's up (Mat_LookAt0x00337028, thenMat_ToQuatinto+0x20), keeps that un-rolled orientation in+0x220, and applies the same roll again; the angle never eases. The shot keeps the tilt until the blend back to the follow camera (0.3 s). When the follow camera is in its state 2 (its vtable+0x1ec), it is first turned (Cam_Follow_TurnToQuat) to the shot's base orientation with no roll (the victim's facing turned 90°, then the shot's yaw+0x238and pitch+0x23c); the tilt goes away through the blend. - To draw it, a port needs only the camera's up from the rolled quaternion (
Quat_AxisZ(q)), as for the scripted rolls.
Power-move camera¶
Cam_Power (type 6) films a finisher from fixed angles. The finisher's animation clip carries the whole sequence as
frame events: at its start it begins a sequence and adds up to eight camera shots placed around the attacker,
then each cut jumps the view to a shot, and an end event (or the move ending) hands the view back. The
player sees a hard cut to a still camera at a 50° field of view, a hard cut to the other angle on each extension
hit that has a cut, and a hard cut back to the follow camera where it was. Confirmed (code) at the functions in
the table; confirmed (runtime) where marked (At runtime).
The events¶
Anim_FireFrameEvents (0x00101dd8) handles the four events as the clip's cursor passes their frames
(Animation format for the 24-byte event and its scaling). Confirmed
(code) at the handlers:
| Type | Handler | Calls | Switch 9 | Payload |
|---|---|---|---|---|
0x39 begin |
0x00102ac8 |
Cam_GetPower(0, create), then CamPower_Begin(cam, mode, human) |
needed | +0x06 s16 mode: 1 = one shot, anything else = 2 (two shots that alternate) |
0x3b add shot |
0x00102fbc |
Cam_GetPower(0) (never creates), then CamPower_AddShot(cam, &pos, &quat, slot) |
needed | +0x04 s16, must be 0 (else the event is ignored); +0x06 s16 slot 0-7; +0x08-+0x0c s16 position x / 1023, y / 1023, z / 2047 m; +0x0e-+0x12 s16 quaternion x, y, z × 2⁻¹⁵, w = √(1 − x² − y² − z²) |
0x3a cut |
0x00102ea0 |
Cam_GetPower(0), then CamPower_CutToShot(fov, cam) |
needed | +0x08 read as one 32-bit float (lwc1 f12,0x8(s1) at 0x00102fa8): the field of view, used when above 0 |
0x3d end |
0x001032a8 |
CamPower_End on player 0's current camera when it is a power camera |
not read | none |
The shot's frame. The position is in the frame of the human the camera follows, at the moment of the begin:
x to his right, y forward, z up from his feet (the transform's position in 0x00714b00, not the head). The
quaternion turns the camera's own axes (+y the view, +z up, +x right, Axes) into that
frame, so in the world the shot's orientation is q_h ⊗ q_e and its position p_h + q_h · p_e, with q_h, p_h
the human's rotation and position at the begin (CamPower_AddShot's quaternion products, 0x0013a470-0x0013a530).
Confirmed (code), confirmed (runtime). On the disc (the survey below) shots stand 0.3 to 6.4 m from the human in plan
(median 2.8 m), from 0.2 m below his feet to 4.4 m above them (median 0.9 m), pitched from straight down to 36° up
(median 1° down), and look within 54° of a point 1 m above his feet (median 12°): low, close and aimed at the body.
Every cut on the disc holds −1.0 (bits 0xbf800000), so every power shot is at 50°; no event carries a near or
far plane.
Who counts (the handlers' common gate, confirmed (code)). Let p be the player number (+0x1b0) of the clip's
human. A human with no player number takes his partner's (handle +0xc4) when his behaviour class (+0xcc) is one
of 0x4d-0x4f, 0x6a, 0x77, 0x78, 0x80, 0xa0, 0xa1, 0xb2, 0xb3, 0xed (the shake event's list; the
classes are not identified, and the clips that need it are bosses' counters, Which clips); any other
human with no player number is dropped, so an AI ally's half of a tandem does nothing. The begin is also dropped when the
human plays anim 654, 656, 658 or 660 (the special attacks' reactions) against a partner of class 0xb2 or 0xb3.
In hide-and-seek (0x0050b1dc, set by HideAndSeekGoal_Start) anim 654 or 656 instead counts as player 0,
following the clip's own human.
The begin's own conditions (0x00102ca4-0x00102e78). Player 0's current camera must be the follow (type 2),
fixed (0), power (6) or rail (9) camera, a rail camera not in its mode 3 (+0x3ed), and switch 9 on. With one
player camera (0x0050b19c = 1) it passes no human, so CamPower_Begin follows the saved camera's first target
(the follow camera's +0x314, the rail camera's +0x340) when that is a player: the player, even when the clip that
fired is his partner's. With two player cameras it goes ahead only on one shared view (0x0050b198 = 1) and when
the clip's player and his partner's are both players, following player p's human; on a split screen there is no
power camera. The add-shot event needs the same camera types and an existing power camera.
Fields¶
The class's own fields, confirmed (code) at the constructor and the event functions:
| Offset | Meaning | Constructor |
|---|---|---|
+0x180 |
look-at point (base field): set at the cut, 0.305 m beyond the human along the view | |
+0x1e0 + 16·i |
shot i's position, world space, i < 8 | |
+0x260 |
the human's position at the begin | |
+0x270 |
the position of the shot cut to (the update returns to it) | |
+0x280 + 16·i |
shot i's orientation quaternion, world space | |
+0x300 |
the human's orientation at the begin | |
+0x310 |
the orientation of the shot cut to | identity |
+0x320 |
the human followed | 0 |
+0x324 |
the saved camera's field of view (written by the begin, read by none of the class's functions) | −1 |
+0x328 |
distance from the shot to the human + 0.305 m (the look-at distance) | 0 |
+0x32c |
u16 mask of the shots that passed the tests | 0 |
+0x32e |
unplaced: set by the begin, cleared by the first cut; shots are added only while it is set | 1 |
+0x32f |
in a move: set by the begin, cleared by the end | 0 |
+0x330 |
mode, 1 or 2 | 2 |
+0x331 |
s8 shot count; −1 after the end, −99 before any begin | −99 |
+0x332 / +0x333 |
s8 current and alternate shot, −1 for none | |
+0x334 |
the saved camera, the one handed back to | 0 |
Begin¶
CamPower_Begin (0x0013a170), confirmed (code): clears the two humans the shot test ignores (0x0050b27c,
0x0050b280); saves player 0's current camera in +0x334 (when that is already the power camera, the camera it
saved stays, so a second begin in the same move does not nest); copies that camera's near plane and far clip
(vtable +0x204 → +0x1a4, +0x20c → +0x1ac) and stores its fov in +0x324; takes the human (above; with none
the count stays below 0 and every later event is ignored); copies his position and rotation from the transform table
into +0x260 / +0x300; sets unplaced and in-a-move, count 0, and the mode. The camera is not made current: the
view changes only at the first cut.
Shots¶
CamPower_AddShot (0x0013a448) stores the shot in world space at once (above), so the angles are fixed to where
the human stood at the begin, not to where he is later, and counts it. It ignores a slot of 8 or more and any shot
after the first cut. The count is a count, not the highest slot: every clip on the disc adds slots 0 to n − 1.
Confirmed (code).
Choosing the shots¶
On the first cut, CamPower_PlaceShot (0x0013a818) tests each stored shot and picks two. Confirmed (code):
- The head point: the human's live position (now, not at the begin) raised by 1.45 m, or by 0.75 m while he plays one of the mount power strikes (anims 225, 227, 229, 231, 233, 235).
- The test sphere: radius max(the human's capsule radius, 0.1) (the capsule radius is 0.35 × his scale,
Physics; 0.1 is the power camera's own radius, vtable
+0x5c); in hide-and-seek the smaller of the two. It is swept withIPhysics_CollideShapeagainst the level mesh and the bodies (query options0x12813), the human himself being the owner, and the filter0x00136040skips his partner (handle+0xc4) and, when the human and his partner are both in a paired move, the partner's partner (0x0050b27c/0x0050b280). - Two sweeps per shot. First from a point beyond the human, on the shot's line of view at the shot's distance
from the head point plus the capsule radius (raised by the radius − 0.1 when the shot is not above the head),
back to the shot; then from the human's feet to the shot. A shot is good when neither sweep hits (no contact, or
one at a fraction of 1 or more), and bit i of
+0x32cis set. A blocked shot is dropped, not moved closer. - None good: no cut. The power camera never becomes current and the follow camera stays for the whole move.
- The pick starts from the counter
0x0050b2a0(0-7, 0 at boot):c = counter mod n. In mode 2 it looks for a good pair(c, c + 1 mod n), movingcon by one up to n times; a pair found gives the current and the alternate shot. With no good pair, or in mode 1, the current shot is the first good one fromcon, and there is no alternate. - Order: in mode 2, of the pair, the one farther in plan (x, y) from the saved camera's position becomes the current shot, so the first cut is the bigger change of angle.
- The counter moves on by one (mod 8) whenever at least one shot was good, so successive power moves start from different shots: the same move done twice in a row is filmed from different angles.
The cut¶
CamPower_CutToShot (0x0013a548), confirmed (code):
- nothing when the count is 0 or below (no begin, or after the end); with no human it ends the camera;
- on the first cut it chooses the shots (above);
- with a current shot: the camera's position and orientation become that shot's and are kept in
+0x270/+0x310; the look-at distance+0x328= the distance from the shot to the human's live position + 0.305 m, and the look-at point+0x180is that far along the view; - on the first cut, unless it already is, the camera becomes player 0's current camera with no blend
(
Camera_MakeActive(0 s, power, player 0)after one 1/60 sICamera_FinishUpdate): a hard cut; - then current and alternate swap (when there is an alternate), so the next cut goes to the other shot of the pair: in mode 2 the cuts alternate A, B, A ...; in mode 1 every cut returns to the one shot;
- the field of view becomes the event's when it is above 0 (also gated by
0x0050b29c, data, always 1), else 50°, at once (ICamera_SetFov), with no ease; - unplaced is cleared: later cuts do not test the shots again, even if the human has moved.
Every cut is a hard cut: the camera jumps in one update, and no blend camera (type 5) is made. Confirmed (runtime).
Holding a shot¶
CamPower_Update (0x00139ef8), each camera update while it is current, confirmed (code):
- Ends (
CamPower_End) when the human is gone, down or dead (Human_IsDownOrDead), or, outside hide-and-seek, when he is in neither side of a paired move (state0x1000, the attacker, or0x2000, the victim, Combat), or he is a victim whose partner has gone or left0x1000. - Otherwise the camera is put back at the cut's position and orientation (
+0x270,+0x310), the position is pushed out of nearby humans (Camera_PushOutOfHumanswith 3.0, none skipped), and the look point is set 3 m along the view. - The shake (
Cam_UpdateShake, Shake) moves that look point; only when it did is the view re-aimed at it (Mat_LookAtUpwith the shot's own up axis). The camera does not track the human: the view stays fixed for the whole shot. Confirmed (runtime): position and orientation unchanged to the fourth decimal across a shot.
The end and the hand-back¶
CamPower_End (0x0013a3b0) runs on the clip's 0x3d event (whatever switch 9 says) or from the update's checks.
When the power camera is a player's current camera it makes the saved camera current with no blend. That camera
was not updated while the power camera showed, so it comes back exactly where it was before the move and goes on
from there (the follow camera's activation, Setting up, step 4). The power camera is released as
soon as it stops being current (CamPower_Deactivate → Camera_Release), and the next begin makes a new one with
no shots. Confirmed (code); confirmed (runtime).
So an extended power move keeps the power camera only if the next part starts before the current clip reaches
its end event; the part's own cut (0x3a) then swaps to the alternate shot. If the end comes first, the view is
back on the follow camera and the next part's cut is dropped (no power camera exists); a part with a begin of its
own (the ~2 variants below) starts a new sequence. Some parts have no cut and keep the shot. Inferred from the
code; the kept shot and the swap confirmed (runtime).
Slow motion and shake¶
- Slow motion (
0x2e/0x2f, Slow motion) slows only the characters' step. The cameras still update at 1/30 s and the power camera is still, so slow motion changes nothing in it, except that the clip's events come later in updates (the clip advances 1/150 s an update). Inferred from the code at the cited addresses. - Shake: event
0x20in a power move's clip (131 of the 164 clips below have one) shakes player 0's current camera, the power camera, through its slot+0x15c(CamPower_StartShake), which starts it only for a human on the camera target list. The shake turns the view through the look point; the camera does not move. Its time counts down bydt × min(1, 54 × step), so a shake in slow motion lasts longer (Shake).
Which clips¶
A survey of the disc's animation clips (the coney-tools extract output; events counted, no clip data copied):
164 clip files have power-camera events. 94 begin a sequence (mode 2: 80, mode 1: 14), 53 only cut and
end (later parts of a move), 17 only end. Shots per begin: 8 (73 clips), 4 (19), 1 (2). Event +0x04 is 0 and
the cut's word −1.0 in every one. Families:
| Family | Begin | Shots | Notes |
|---|---|---|---|
grab power strikes gen_grab_front_power{1-24}_p1 (anims 57, 63) |
mode 2 | 8 | as every begin on the disc: at frame 0-6, all its shots on one later frame (the next in 61 of 94), the first cut on the shots' frame or after (the next in 56, up to 22 later) |
their extensions _p2, _p3 (anims 59, 61, 65, 67) |
none; the ~2 variants of 3 and 4 have one |
one cut each or none (power 1, 10, 11, 15 _p2), then the end |
|
mount power strikes gen_mount_power{1-20}_p1, _p2, _p3 (anims 225-236) |
mode 2 on _p1 |
8 | as the grab's |
weapon grabs gen_grab_front_bottle, _knife, gen_grab_back_bottle, _knife |
mode 1 | 8 | |
weapon mounts gen_beerbottle_mounting, gen_knife_mounting |
mode 2 | 8 | |
wall grabs gen_grab_front_wall_back, _front, _left, _right |
mode 1 | 4 | |
stealth kills player_stealthkill_barehands, _baton, _knife, _weak |
mode 1 | 8 | |
bottle_smash_attacker |
mode 1 | 8 | |
tandem starts gen_tandem1, gen_tandem2 (_attacker_begin, _grabber_begin), gen_tandem_knife_*_beg* |
mode 2 | 8 | no end: their _end clips end it |
tandem ends gen_tandem3, gen_tandem_bat (_attacker_end, _grabber_end), gen_tandem5_*_end |
mode 2 | 8; 4 for tandem 5 | |
bosses: bopp_bo_grab_counter_victim, bopp_bo_tackle_counter_react, hiha_bo_*, samo_bo_tandem*, ghost_grab_front_power_react, dest_bo_special_nut_kck_react, virgil_uppercut_react |
mode 2 (Virgil 1) | 4; 8 for Hi-Hat's grab counter; 1 for Dest and Virgil | some with two cuts or no end |
No rage clip has its own events: a raging power strike plays another grab power clip (63, 65 ...). The full list:
The 164 clips: frames, begin (frame, mode), shots, cut frames, end frame
| Clip | Frames | Begin (frame, mode) | Shots | Cuts at frames | End at frame |
|---|---|---|---|---|---|
bopp_bo_grab_counter_victim |
53 | 3, 2 | 4 | 9 | 32 |
bopp_bo_grab_counter_victim~2 |
53 | 3, 2 | 4 | 9 | 32 |
bopp_bo_tackle_counter_react |
60 | 3, 2 | 4 | 17, 34 | 50 |
bopp_bo_tackle_counter_react~2 |
60 | 3, 2 | 4 | 17, 34 | 50 |
bottle_smash_attacker |
34 | 3, 1 | 8 | 15 | 29 |
dest_bo_special_nut_kck_react |
99 | 0, 2 | 1 | 16 | - |
gen_beerbottle_mounting |
48 | 0, 2 | 8 | 8 | 28 |
gen_grab_back_bottle |
41 | 6, 1 | 8 | 10 | 29 |
gen_grab_back_knife |
57 | 6, 1 | 8 | 12 | 47 |
gen_grab_front_bottle |
32 | 0, 1 | 8 | 11 | 23 |
gen_grab_front_knife |
62 | 0, 1 | 8 | 11 | 52 |
gen_grab_front_power10_p1 |
33 | 3, 2 | 8 | 4 | 28 |
gen_grab_front_power10_p2 |
70 | - | - | - | 70 |
gen_grab_front_power11_p1 |
39 | 3, 2 | 8 | 8 | 37 |
gen_grab_front_power11_p2 |
40 | - | - | - | 38 |
gen_grab_front_power11_p3 |
32 | - | - | 3 | 31 |
gen_grab_front_power12_p1 |
37 | 0, 2 | 8 | 2 | 30 |
gen_grab_front_power12_p2 |
50 | - | - | 1 | 43 |
gen_grab_front_power12_p3 |
50 | - | - | 1 | 46 |
gen_grab_front_power14_p1~2 |
41 | 0, 2 | 8 | 2 | 28 |
gen_grab_front_power14_p2~2 |
45 | - | - | 1 | 24 |
gen_grab_front_power15_p1 |
35 | 0, 2 | 8 | 2 | 25 |
gen_grab_front_power15_p2 |
40 | - | - | - | 30 |
gen_grab_front_power15_p3 |
35 | - | - | 2 | 25 |
gen_grab_front_power16_p1 |
40 | 0, 2 | 8 | 2 | 36 |
gen_grab_front_power16_p2 |
49 | - | - | 3 | 43 |
gen_grab_front_power16_p3 |
33 | - | - | 2 | 30 |
gen_grab_front_power17_p1 |
31 | 0, 2 | 8 | 2 | 24 |
gen_grab_front_power17_p2 |
31 | - | - | 1 | 25 |
gen_grab_front_power17_p3 |
31 | - | - | 1 | 23 |
gen_grab_front_power18_p1 |
55 | 2, 2 | 8 | 4 | 49 |
gen_grab_front_power18_p2 |
28 | - | - | 2 | 26 |
gen_grab_front_power19_p1 |
45 | 1, 2 | 8 | 3 | 39 |
gen_grab_front_power19_p2 |
51 | - | - | 1 | 45 |
gen_grab_front_power19_p3 |
20 | - | - | 1 | 19 |
gen_grab_front_power1_p1 |
22 | 0, 2 | 8 | 5 | 20 |
gen_grab_front_power1_p2 |
30 | - | - | - | 25 |
gen_grab_front_power1_p3 |
25 | - | - | 5 | 22 |
gen_grab_front_power20_p1 |
45 | 0, 2 | 8 | 2 | 40 |
gen_grab_front_power20_p2 |
36 | - | - | 0 | 35 |
gen_grab_front_power21_p1 |
45 | 0, 2 | 8 | 2 | 44 |
gen_grab_front_power21_p2 |
37 | - | - | 0 | 36 |
gen_grab_front_power22_p1 |
45 | 0, 2 | 8 | 2 | 41 |
gen_grab_front_power22_p2 |
30 | - | - | 1 | 27 |
gen_grab_front_power23_p1 |
30 | 1, 2 | 8 | 3 | 24 |
gen_grab_front_power23_p2 |
34 | - | - | 2 | 26 |
gen_grab_front_power2_p1 |
40 | 0, 2 | 8 | 2 | 36 |
gen_grab_front_power2_p2 |
42 | - | - | 16 | 38 |
gen_grab_front_power2_p3 |
24 | - | - | 7 | 21 |
gen_grab_front_power3_p1 |
37 | 0, 2 | 8 | 14 | 33 |
gen_grab_front_power3_p1~2 |
37 | 3, 2 | 8 | 14 | 33 |
gen_grab_front_power3_p2 |
24 | 0, 2 | 8 | 7 | 21 |
gen_grab_front_power3_p2~2 |
24 | - | - | 7 | 21 |
gen_grab_front_power4_p1 |
33 | 3, 2 | 8 | 6 | 28 |
gen_grab_front_power4_p1~2 |
33 | 0, 2 | 8 | 6 | 28 |
gen_grab_front_power4_p2 |
23 | - | - | 4 | 19 |
gen_grab_front_power4_p2~2 |
23 | 0, 2 | 8 | 4 | 19 |
gen_grab_front_power5_p1 |
55 | 3, 2 | 8 | 5 | 45 |
gen_grab_front_power5_p2 |
28 | - | - | 1 | 27 |
gen_grab_front_power6_p1 |
29 | 1, 2 | 8 | 3 | 25 |
gen_grab_front_power6_p2 |
42 | - | - | 15 | 35 |
gen_grab_front_power6_p3 |
22 | - | - | 7 | 16 |
gen_grab_front_power8_p1 |
35 | 6, 2 | 8 | 10 | 27 |
gen_grab_front_power8_p2 |
26 | - | - | 11 | 21 |
gen_grab_front_power9_p1 |
39 | 3, 2 | 8 | 6 | 32 |
gen_grab_front_power9_p2 |
45 | - | - | 10 | 37 |
gen_grab_front_power9_p3 |
26 | - | - | 6 | 19 |
gen_grab_front_power9_p3~2 |
26 | - | - | 6 | 19 |
gen_grab_front_wall_back |
34 | 3, 1 | 4 | 8 | 26 |
gen_grab_front_wall_front |
54 | 3, 1 | 4 | 5 | 50 |
gen_grab_front_wall_left |
29 | 0, 1 | 4 | 11 | 24 |
gen_grab_front_wall_right |
28 | 0, 1 | 4 | 9 | 24 |
gen_knife_mounting |
60 | 0, 2 | 8 | 8 | 49 |
gen_mount_power10_p1 |
60 | 0, 2 | 8 | 3 | 40 |
gen_mount_power10_p2 |
45 | - | - | - | 29 |
gen_mount_power11_p1 |
45 | 0, 2 | 8 | 4 | 37 |
gen_mount_power11_p2 |
46 | - | - | 1 | 35 |
gen_mount_power11_p3 |
36 | - | - | 0 | 25 |
gen_mount_power12_p1 |
42 | 0, 2 | 8 | 2 | 36 |
gen_mount_power12_p3 |
36 | - | - | 5 | 31 |
gen_mount_power13_p1 |
38 | 0, 2 | 8 | 2 | 25 |
gen_mount_power13_p2 |
49 | - | - | 4 | - |
gen_mount_power14_p1 |
40 | 0, 2 | 8 | 4 | 30 |
gen_mount_power14_p2 |
55 | - | - | 6 | 40 |
gen_mount_power14_p3 |
45 | - | - | 3 | 34 |
gen_mount_power15_p1 |
38 | 0, 2 | 8 | 4 | 31 |
gen_mount_power15_p2 |
38 | - | - | 2 | 28 |
gen_mount_power15_p3 |
39 | - | - | 2 | 26 |
gen_mount_power16_p1 |
50 | 0, 2 | 8 | 2 | 45 |
gen_mount_power16_p2 |
28 | - | - | 2 | 23 |
gen_mount_power17_p1 |
51 | 0, 2 | 8 | 3 | 40 |
gen_mount_power17_p2 |
50 | - | - | 4 | 40 |
gen_mount_power18_p1 |
52 | 0, 2 | 8 | 2 | 47 |
gen_mount_power18_p2 |
50 | - | - | - | 45 |
gen_mount_power18_p3 |
46 | - | - | 2 | 42 |
gen_mount_power19_p1 |
40 | 0, 2 | 8 | 2 | - |
gen_mount_power19_p2 |
29 | - | - | 0 | 24 |
gen_mount_power1_p1 |
45 | 4, 2 | 8 | 11 | 37 |
gen_mount_power1_p2 |
32 | - | - | 6 | 24 |
gen_mount_power1_p2~2 |
32 | 0, 2 | 8 | 6 | 24 |
gen_mount_power20_p1 |
37 | 2, 2 | 8 | 5 | 29 |
gen_mount_power20_p2 |
39 | - | - | - | 26 |
gen_mount_power2_p1 |
43 | 0, 2 | 8 | 5 | 31 |
gen_mount_power2_p1~2 |
43 | 0, 2 | 8 | 9 | 31 |
gen_mount_power2_p2 |
25 | - | - | 4 | 22 |
gen_mount_power2_p2~2 |
25 | - | - | 8 | 22 |
gen_mount_power3_p1 |
51 | 3, 2 | 8 | 23 | 45 |
gen_mount_power3_p1~2 |
51 | 0, 2 | 8 | 23 | 45 |
gen_mount_power3_p2 |
30 | - | - | 10 | 26 |
gen_mount_power3_p2~2 |
30 | 0, 2 | 8 | 10 | 26 |
gen_mount_power4_p1 |
39 | 3, 2 | 8 | 7 | 33 |
gen_mount_power4_p2 |
30 | - | - | - | 26 |
gen_mount_power4_p3 |
26 | - | - | 4 | 22 |
gen_mount_power5_p1 |
56 | 4, 2 | 8 | 9 | 40 |
gen_mount_power5_p2 |
30 | - | - | 8 | 23 |
gen_mount_power6_p1 |
37 | 6, 2 | 8 | 11 | 31 |
gen_mount_power6_p2 |
34 | - | - | 6 | 28 |
gen_mount_power6_p3 |
30 | - | - | 9 | 24 |
gen_mount_power7_p1 |
53 | 3, 2 | 8 | 17 | 48 |
gen_mount_power7_p1~2 |
53 | 0, 2 | 8 | 17 | 48 |
gen_mount_power7_p2 |
42 | - | - | 7 | 36 |
gen_mount_power7_p2~2 |
42 | 0, 2 | 8 | 7 | 36 |
gen_mount_power7_p3 |
35 | - | - | 7 | 22 |
gen_mount_power8_p1 |
45 | 3, 2 | 8 | 5 | 28 |
gen_mount_power8_p2 |
47 | - | - | 9 | 32 |
gen_mount_power8_p3 |
41 | - | - | 8 | 23 |
gen_mount_power9_p1 |
47 | 0, 2 | 8 | 3 | 33 |
gen_mount_power9_p2 |
53 | - | - | - | 42 |
gen_mount_power9_p3 |
41 | - | - | 2 | 27 |
gen_tandem1_attacker_begin |
27 | 3, 2 | 8 | 14 | - |
gen_tandem1_attacker_end |
27 | - | - | - | 20 |
gen_tandem1_grabber_begin |
27 | 3, 2 | 8 | 14 | - |
gen_tandem1_grabber_end |
20 | - | - | - | 19 |
gen_tandem2_attacker_begin |
35 | 3, 2 | 8 | 26 | - |
gen_tandem2_attacker_end |
35 | - | - | - | 18 |
gen_tandem2_grabber_begin |
35 | 3, 2 | 8 | 26 | - |
gen_tandem2_grabber_end |
31 | - | - | - | 17 |
gen_tandem3_attacker_end |
30 | 0, 2 | 8 | 2 | 28 |
gen_tandem3_grabber_end |
32 | 0, 2 | 8 | 2 | 30 |
gen_tandem5_attacker_end |
35 | 2, 2 | 4 | 4 | 34 |
gen_tandem5_attacker_end~2 |
35 | 2, 2 | 4 | 4 | 34 |
gen_tandem5_victim_end |
43 | 2, 2 | 4 | 9 | 25 |
gen_tandem_bat_attacker_end |
50 | 0, 2 | 8 | 10 | 35 |
gen_tandem_bat_grabber_end |
50 | 0, 2 | 8 | 10 | 35 |
gen_tandem_knife_attacker_beg |
51 | 0, 2 | 8 | 2 | - |
gen_tandem_knife_attacker_end |
16 | - | - | - | 15 |
gen_tandem_knife_grabber_begi |
51 | 0, 2 | 8 | 2 | - |
gen_tandem_knife_grabber_end |
13 | - | - | - | 12 |
ghost_grab_front_power_react |
47 | 4, 2 | 4 | 6, 26 | 40 |
hiha_bo_grab_counter_victim |
39 | 5, 2 | 8 | 7 | - |
hiha_bo_tackle_counter_react |
55 | 0, 2 | 4 | 10 | 54 |
player_stealthkill_barehands |
41 | 3, 1 | 8 | 6 | 34 |
player_stealthkill_baton |
67 | 3, 1 | 8 | 8 | 55 |
player_stealthkill_knife |
40 | 3, 1 | 8 | 6 | 34 |
player_stealthkill_weak |
39 | 4, 1 | 8 | 6 | 38 |
samo_bo_tandem3_attacker_end |
35 | 2, 2 | 4 | 4 | 34 |
samo_bo_tandem3_victim_end |
45 | 2, 2 | 4 | 9 | 25 |
samo_bo_tandem_1_attacker_beg |
34 | 3, 2 | 4 | 6, 26 | - |
samo_bo_tandem_1_attacker_end |
19 | - | - | - | 15 |
samo_bo_tandem_1_victim_begin |
34 | 3, 2 | 4 | 5 | - |
samo_bo_tandem_1_victim_end |
19 | - | - | - | 16 |
samo_bo_tandem_2_attacker_end |
46 | 3, 2 | 4 | 5 | 41 |
samo_bo_tandem_2_victim_end |
43 | 3, 2 | 4 | 8 | 39 |
virgil_uppercut_react |
44 | 0, 1 | 1 | 21 | - |
Two players and other humans¶
Confirmed (code) at the gate and CamPower_Begin, except where marked:
- One power camera, player 0's: every event asks
Cam_GetPower(0), and the end looks at player 0's current camera. With one player camera the shots are placed around the player (the follow camera's target), even when the clip that fired is a boss's counter; the boss clip's shots then land in the player's frame, not the boss's (inferred from the code; not seen at runtime). - Two players on one view: the shots follow the human of the player whose clip fired; the power camera is player 0's and replaces the shared view.
- Split screen: no begin, so no power camera (outside hide-and-seek).
- The player as victim: an AI's clip counts only for the boss classes above, through its partner, the player. An ordinary enemy's finisher on the player does not move the camera.
At runtime¶
Confirmed (runtime), PCSX2 2.9.94, a copy of quick-save slot 6 (level99, PoizoCiv placed 1.0 m in front and made a
puppet), scenarios power_camera, power_camera_ext and power_camera_rage in
repo:research/traces/scenarios/ (circle to grab, cross held and square for the power strike, cross taps for the
extension; L1 + R1 with the rage meter written full for the raging one), the player's current camera (0x005d9150)
and the power camera's fields read every update, 2026-10-11:
- The power strike 57 (
gen_grab_front_power10_p1) started on update 67, the begin came on update 67 (mode 2, count 0,0x005d9188set), the shots and the cut on update 68: count 8, mask0xff(every shot clear in the open street), the pair (4, 5), and the view cut in one update from the follow camera at (36.24, 43.00, 2.76), 65°, to shot 5 at (33.73, 34.75, 1.45), 50°, near 0.1, far 115 (copied from the follow camera). The eight stored shots equal the clip's events turned by the player's heading at the begin (160.7°) and added to his position, to the millimetre. The camera stayed still until update 100, the end event at frame 28 of 33, when the follow camera came back at (36.24, 43.00, 2.76), where it had been;0x005d9188read 0 from then on. The counter0x0050b2a0went from 4 to 5. - With the extension 59 (
gen_grab_front_power10_p2, a part with no cut) the shot held from update 68 through 59, and the follow camera came back on update 181 (the end at the part's last frame). - Raging, 63 then 65 (
gen_grab_front_power14_p1~2,_p2~2): the begin on update 134, the cut on 135 to shot 4 at (31.33, 39.27, 3.48), looking down 53°, with 5 the alternate; the extension started on update 157 and its cut on 158 jumped to shot 5 at (35.06, 40.31, 1.93) in one update; the follow camera came back on 189 at its position from before the move. Both shots' orientations equalq_h ⊗ q_efrom the clip. - Slow motion did not come in (
0x005102ccstayed 1/30 s): neithergen_grab_front_power10norpower14~2has the pair.
Not seen at runtime: a blocked shot, mode 1, two players, a boss's counter.
For the implementer¶
Coney has no power camera yet. To build it from this section:
- clip events
0x39,0x3b,0x3a,0x3dwith the payloads above (Coney's clip loader must keep+0x04,+0x06and the word at+0x08); act only for player 0's camera, with the gate above; - begin: remember the current camera, the followed human's transform and the mode; shots: store them in world space at once;
- the first cut: test each shot with the two sphere sweeps, pick the pair from a counter kept across moves (mod 8, from 0), the farther in plan first, hard cut; later cuts: swap to the other shot, hard cut; fov 50°;
- each update: hold the shot, push it out of humans, let the shake turn the view; end on the event, on leaving the paired move, or when the human is down or dead; hand back with no blend to the saved camera, unchanged since the begin; forget the shots;
- tests with synthetic clips: begin, 8 shots, a cut and an end give a hard cut to the expected world pose and a hard cut back; two cuts alternate; blocked shots are skipped; an end before the next part's cut leaves that cut ignored; switch 9 off ignores all but the end.
Hood and third-person cameras¶
- Hood (
Cam_Hood, type 11,CamSetupHood): each update (0x00135138) it sits on its vehicle at the mount offset+0x200in the vehicle's frame (0x00134d78: x along the vehicle's x axis, y along its y axis, z added to the height; default (0, 0.3, 1.7)), looks at the mean position of the players that still count raised by the look offset+0x1f0's z (0x00134f08, which reads+0x1f8only; default (0, 0, 1.53)), and steps its field of view toward the wanted one+0x214by at mostdtdegrees per update, so 1° per second. Confirmed (code).+0x214is 60, set by the constructor and copied byCamHood_CopySettings;CamSetupHoodsets the fov itself, not+0x214, so a script's 65° drifts back to 60° over 5 s (confirmed (code); the arithmetic, not seen at runtime). - Third person (
Cam_3rdPerson, type 16,CameraCreateThird): each update (0x001205e8) the look-at point is the target's position plus the offset (default (0, 0, 1.5)); the orientation eases 10% toward the target's; the camera standsdistance(default 5.1) behind along it andheight(default 1.8) up, swung byangleabout the look-at point. Confirmed (code).
Two players¶
With two player cameras (0x0050b19c = 2) the follow and rail cameras decide each update whether the players share one
view or split the screen. Confirmed (code) at 0x00126c40 and 0x0013c710; the purpose of each test is inferred:
- Merging needs both players inside the shared view, within 11 m of each other and neither colour controller
fading; splitting follows when that stops holding. Either waits until its timer has run 0.72 s (follow,
+0x41c/+0x420) or 0.5 s (rail,+0x3dc). - On a change (
0x001276a0) the views are re-laid out (0x00122ed0: one view, or the grid of the split mode), the band and zoom step re-applied for the new count, the target pitch set to 25°, and one or two updates later a short shake and a white flash (alpha 225, fading over 0.2 s) on the colour controller (0x00126a88). - When the screen splits, each view's field of view is scaled by
0x0050b200(at least 40°), and split mode 0 halves the view window's width (The camera manager).
Shake and rumble¶
A shake is started on a camera through its vtable slot +0x15c (0x001263e8 for the follow camera, 0x001210f8 in
the base) as (human, level, view), confirmed (code). The last argument goes to +0x1d7: 1 lets the shake move the
view, 0 makes it a rumble only. The callers:
- a hit reaction (
Human_PlayReaction,0x0026a6d0), with the attack's strength bits(flags & 0x30) >> 4as the level: on the attacker's player camera with view 1 (0x0026a8fc/0x0026a924), the level raised by one (to at most 3) while the attacker has bit0x80000in his word+0xe0(raging, inferred); and, from strength 2, on the victim's camera with view 0 (0x0026a988): the one hit only feels it in the pad; - rage start (
Human_StartRageMode,0x00236e90): level 1, view 1; - a landing (
Human_Land,0x0023e090, under the follow camera above): view 1; - the animation event of type
0x20(Anim_FireFrameEvents0x00101dd8, at0x0010354c): level = the event's value, view 1, on a player's camera. The human is the clip's own unless he has a state of0x82830, when it is his foe or his grab partner (or the human of handle+0xc4); a target whose+0xcckind is one of0x77,0x78,0x6a,0x80,0xa0,0xa1,0xb2,0xb3,0x4d-0x4f,0xedgives the shake back to the clip's human (the kinds' meaning not traced).
Levels: 1: amplitude 0.5, 0.10 s, rumble base 0; 2: 0.75, 0.15 s, 0x30;
3: 1.0, 0.18 s, 0x60; 0 stops it. The update (0x00121298, every camera kind): the amplitude eases 65% per
update toward the level's while its time lasts; the time counts down by dt × min(1, 54 × step) (so slower in slow
motion); the pad's rumble byte (+0x41 of the pad record) gets 255 × current / level amplitude when
that exceeds (base >> 2) + 0x28, capped at base + 0x60.
Slots and the view offset (0x00121298, confirmed (code)). A camera keeps three shakes, slot player + 1 (0 for
a shake with no human): level amplitude +0x1a8, time +0x1b4, current amplitude +0x1c0, rumble base +0x1d0.
While a slot's time is positive its current eases 65% toward the level; once the time is negative it decays × 0.6 per
update and is zeroed under 0.001; it is never above the level. When a slot's current reaches 0 the slot, the offset
and the pad's rumble bytes (+0x40, +0x41) are cleared. Each update, only in game mode 1, with A = min(the sum of
the currents, the largest level), the offset o (+0x190) is, per axis,
o = A × (0.5 × o_last + 0.5 × (u − 0.5)), with u a fresh uniform draw in [0, 1) (Random_Unit, 0x003354e0, three
draws for x, y, z),
so each axis stays within ±A / 2 m and changes smoothly (half the last offset carried over). It is added only when
A > 0, +0x1d7 is set and switch 6 (0x0050b1d0) is on. Each caller passes the point it moves: the follow camera
passes its aim point +0x240 (0x0012cf94, just before Mat_LookAt), so its shake turns the view and does not
move the camera; with no point given the base adds it to the look-at point +0x180.
Shakes from the world: the passing train¶
Set dressing shakes the cameras through slot 0, the slot of a shake with no human, with any amplitude and time, not
through the three levels. Two one-instruction methods of the base camera (every camera class keeps them, 0x00535510
and 0x00535d50 alike): vtable +0x1d4 (0x004db180, swc1 f12, 0x1a8(a0)) sets slot 0's level amplitude +0x1a8,
and +0x1dc (0x004db188, swc1 f12, 0x1b4(a0)) its time +0x1b4; the script-object helpers
ScriptObj_SetCameraWorldShakeLevel (0x003a4608) and ScriptObj_SetCameraWorldShakeTime (0x003a4668) call them on
a player's current camera. Neither touches the rumble base +0x1d0 or the view switch +0x1d7. Confirmed (code).
The train (part_train_sound, PartTrainSound_Update 0x003db690, Script types):
the sound object that travels with a subway or elevated train. While it is within 60 m of a camera, each of its
updates (every 5 updates there; every 60 when farther than 120 m) does, for every view:
- level amplitude = (60 − d) × 0.004237288 (= 0.25 / 59),
dthe distance in metres from that view's camera (vtable+0x21c) to the train sound, so 0 at 60 m and 0.25 at 1 m; 0.25 flat when within 1 m of any camera; - time = 0.95 s.
So nothing triggers it but distance: no script call, no sound event, no animation event. The shake then runs through
the usual update (above): the current amplitude eases 65 % a camera update toward the level, the time
counts down, and because the train re-arms the time every 5 updates (about 0.17 s) it never runs out while the train
is in range; the level follows the distance, rising as the train comes and falling as it goes. Once out of range the
last level holds for the rest of its 0.95 s, then the current decays × 0.6 an update to 0. The view offset is the
shake's own (each axis A × (0.5 × o_last + 0.5 × (u − 0.5)), so within ±A / 2 m, smoothed by half the last
offset), added to the follow camera's aim point, so the view turns slightly; the camera does not move. It needs
+0x1d7 set (the camera constructor 0x00120868 sets it to 1; a shake started with view 0 clears it until the next
view-1 shake) and switch 6 on.
No pad rumble: Cam_UpdateShake (0x00121298) finds the pad of slot k from the player list only for k ≥ 1
(slot 0 keeps pad −1), so slot 0 never writes the rumble bytes. Confirmed (code); confirmed (runtime) below.
Runtime (confirmed (runtime), PCSX2 2.9.94, a copy of quick-save slot 6 in level99, player standing, the
player camera *(0x005d9150) read every 33 ms, 2026-10-10). level99's subway (six dyn_s_train_med_full cars and
the part_train_sound, moved by the scene Coney_Sub1 or Coney_Sub2; each run's return function schedules the
next 30 s later) passed three times, at game times 557.4 s, 604.2 s and 650.8 s (46.6 s apart: the scene plus the
30 s wait). Each pass:
- slot 0's level rose from 0.004 to a peak of 0.106 (the closest approach, 60 − 0.106 / 0.004237 = 35 m) and fell back, with the time at 0.883-0.950 s throughout (re-armed every 5 updates); the train was within 60 m for about 3.2 s, then the last level held for 0.95 s and decayed; about 4.2 s of shake in all;
- the current amplitude followed the level within one or two updates (0.0027 on the first update at a level of 0.0041, then 65 % of the gap each update);
- the view offset (
+0x190) stayed within ±0.026 m per axis (the bound at the peak is ±0.053 m), changing every update (30 Hz); +0x1d7was 1, switch 6 (0x0050b1d0) 1, and the pad's rumble bytes (0x005dd810 + 0x40/+0x41) 0 on every sample, with the pad's actuators enabled (+0x4c= 1).
The other world shakes on slot 0 (confirmed (code), the arguments at the cited calls): PartPelhamSubway_Update
(0x003da744) and PartLevel2Subway_Update (0x003d9bcc) set a level of 0.02 for 4 s on each view's
camera while within 30 m and 50 m of one; also PartExplosionScreenShake_Init, PartMotorbikeGang's message and
update, PartPlasterDrop_OnMessage, SubPlasterDrop_Init and SubShk_Update (arguments not traced).
Coney status and the fix for the implementer (Coney as of 781d7768): camera::CameraShake
(repo:src/camera/camera_shake.h) has one shake started at levels 1-3 only; there is no slot 0, no way to set an
amplitude and time from the world, and no part_train_sound behaviour (the particle systems do not know the type).
To do:
- give the shake slot 0 (or a second shake on the camera manager) with
setWorldShake(amplitude, seconds), the original's+0x1d4/+0x1dc; its current is summed with the human slots and the total capped at the largest level (Slots and the view offset); it never rumbles the pad; - a
part_train_soundbehaviour: within 60 m of the player camera, every 5 updates, level = (60 − d) × 0.25 / 59 (0.25 within 1 m) and time 0.95 s; plus its sound (Script types); part_pelham_subwayandpart_level2_subwaywhen those levels come: 0.02 for 4 s within 30 m / 50 m;- replace Coney's stand-in offset (
kOffsetScale) with the original's form above, now known: it decides how much the view moves (at the train's peak about ±2.6 cm on the aim point).
Slow motion¶
0x005148a0 (CfgFollowCamera's last argument, 0.2 in global.lua) scales the characters' step: an animation
event of type 0x2e on a player's clip (0x00101dd8, one player only) calls 0x0041ab30, which sets the step
0x005102cc to 0x005148a0 / 30 (1/150 s) and marks the player; type 0x2f (0x0041ab60) unmarks it and, when no
player is marked, sets the step back to 1/30 s. Every character update then advances dt = 1/150 s, so humans,
animation and the cameras' character-step logic run at 20% speed while frames keep their rate. Confirmed (code).
Which clips (a survey of the disc's animation clips, counts only): 52 clip files (44 distinct names) carry the
pair, always one 0x2e and one 0x2f, 1 to 8 clip frames apart (4.4 on average), so a slow-motion moment is short.
They are all finishing moves: the grab power moves (gen_grab_front_power*, 18 files, plus 2 reactions) and the
mount power moves (gen_mount_power*, 16), the tandem attacks' ends (gen_tandem* and samo_bo_tandem*, 13 files,
one begin), the bottle smash (bottle_smash_attacker) and one boss counter (bopp_bo_grab_counter_victim, 2).
No rage, running or ordinary attack clip has one. Since the event fires only on a player's clip, a non-player's
finisher does not slow time. Inferred: counted from the clips' frame events, not read from the code.
The other writers of the step, confirmed (code): HuSetSlowMo(fraction) (Human_SetSlowMo, 0x0023b778), step =
fraction / 30 for 0 < fraction < 1, else 1/30; and Luther's boss tactic: BossLutherTactic_StartSlowMotion
(0x0030c8c8) sets 1/60 s for 1 s of game time, then BossLutherTactic_UpdateSlowMotion (0x0030c8f8) 1/120 s for
4 s, then 1/30 s again.
Game time does not slow down. GameTimer_Update (0x00145a10) advances game time (+0x40, milliseconds +0x48)
by its own fixed step (+0x4c, 1/30 s) × its scale (+0x50, 1.0 from the reset; no other writer among the timer's
functions) every update and never reads 0x005102cc. So everything timed in game milliseconds (the combat timers such
as stun and ground; the missions' and Luther's own timers above) keeps counting frames at 30 a second while the
characters move at 20%: a stun of 1 s ends after 30 frames, in which a slowed character has lived only 0.2 s (inferred
from the code; not seen at runtime). The cameras are not slowed either: Humans_Update runs Cameras_Update
(0x0011e878) with max(step, 1/30) (0x00249b50), so 1/30 s.
Coney's implementation¶
src/camera/follow_camera.* is the follow camera of Cam_Follow_Update (0x0012ae58), stepped after the human
by src/human/player.* and drawn by --play-level (Running Coney);
src/camera/follow_collision.* holds its rays against the collision mesh:
- the look-at point is the feet + 1.4 m, its move each update limited by its length
das in step 5: all of it within 0.4 m,1 − 2 × (d − 0.4)of it to 0.8 m, 20 % beyond; in the air (a jump or a fall) it follows the feet directly. A climb's rise of 2.64 m is followed at 20 % for 6 updates, then in 3 (the last one a few millimetres); - the view faces the aim point (
FollowCamera::aimPoint(), the trace'saim_x-aim_lag), stepped after the world collision: the lead along the target's facing eased 35% an update (0.365 m ahead standing or walking away, as at runtime), the height from the body point (Human::bodyPoint(), the hips' bone 2 times the scale) with its dead band, the running bob and the sprint sway (Coney's own seeded random numbers), the 1.0 m sphere push, the lag from 0.06 (from the horizontal speed only, unlike the original: Long falls) and the pull back in plan. Coney's reading of the state flags: grabbing (0xc0) is holding a victim in a grab's pair; down (0xe2c00) is holding one with no pair (a tackle), mounted, grounded, arrested or knocked out; the target counts for the cameras unless dying, dead, knocked out, mounted or arrested (the upgrade and key exception is not built), and a long fall is airborne while not counting or grounded. The long fall's 0.6 and the halving apply to the update's move, not the eased lag (Coney's reading); the blocked-wall share uses the main ray's hit face. Inferred: the lead's world ray is a mesh ray with no type mask, skipping the see-through materials, and the lead's view direction is taken across the ground (runtime 0.363 m, not the 0.375 m a pitched view gives). Not built: the slope case, the 0.75 s timer after the aim push, the takeover, aiming, other game modes and two targets; - the wanted position stays put unless its distance leaves the leash band, 4.8-5.3 m (the default distance and 0.5 m more, as in the street), then moves along that line to the band; the camera moves 22% of the way to it each step, held inside the hard band (4.56-5.65 m), which widens at once and shrinks by 1% of the difference an update;
- auto-follow (
0x00129c78): the auto-centre rule (Heading) turns the wanted position toward the player's facing at the rule's rate of the angleabetween the facing and the camera's view at the start of the update, at the stored gaits 2, 4 and 5 (walk, run, sprint; so not standing, not in the walk start at 0.76 m/s nor at a jog's speed, whatever clip plays). It is held off from a blocked main ray until the player stops (the+0x45dlatch, set by+0x45band cleared on the second update standing, as at runtime), for 0.334 s after right-stick input, and while the left stick points more than 157.5° from up (+0x474). With the option off (the debug menu's Auto-centre) the default rule (0x0012a400) runs instead, at the run and sprint gaits only. With the stick held sideways the player runs in a circle: Rembrandt in the sandbox turns about 197°/s at a run and 144°/s at a 35 % walk, against the original's 191°/s and 143°/s (disc test[disc][player][sandbox]); - the sprint zoom (Sprint zoom) with the page's fields: the first update at the sprint gait arms
and latches it (
+0x467,+0x466) when the player has no enemies or the nearest is within 12 m (the brain's query is a hook,Player::setNearestEnemy(), with no enemies until Coney has brains; out of range the arm waits); the zoom function (0x00128cf0) saves the band, the zoom distance and the target pitch, starts the 0.5 s timer, sets the wanted near edge to the minimum distance and steps the zoom to the default (upper pitch limit 30°); the band's ease (0x0012aae0, early in the next updates) moves the near edge byd × |d| / T × dt, which gives the street's 4.569, 4.379, … 3.216, 3.0 to 0.001 m over 15 updates; the target pitch goes to 7° in a straight line over 14. The first update off the sprint gait sets the way back for 250 ms later; 8 updates on the zoom function puts back the band, the zoom (40° again) and the timer, the same curves run back, and on the 15th update the zoom is over.enableSprintZoom()isCamEnable(5, on)(0x00126a30); - the pitch eases toward its target at 85°/s, between the lower limit (the larger of -20° and the slope of 0.4 m over
6.6 m) and the upper one of the zoom distance (
0x001254f0): 50° at the minimum, 40° above the default, 30° at the default with one player camera (0x0050b19c= 1); - the height hold (step 12):
holdHeight()eases the wanted position's height 30 % ×+0x398of the way an update toward the height above the look-at point it held; - the right stick turns the wanted position at the raw rates (yaw up to 150°/s outside the ±48 dead zone, pitch near the ends of the travel) and holds off for 0.334 s after any input;
- collision (World collision): the main ray from the look-at point to the camera with mask
0x200 | 0x800 | 1, so it tests disabled triangles, and every ray passing through materials 30LOW_FENCE, 122RAILINGand 107CHAINLINK_NOCLIMB(0x00548ab0); a hit on a disabled triangle is cast again without them when the look-at point is less than 0.5 m in front of its plane or the player's feet are not in front of it. A hit that stands sets+0x45band pulls the camera to 0.2 m short of it, never nearer than 0.5 m. The side probes turn the main ray about the look-at point's vertical by 1, 2 and 3 × the probe angle (7° at the band's near edge to 4° at its far edge) each way; - the camera's forward vector turns the player's stick before the human sees it;
- the script calls (Script calls):
src/camera/cameras.*keeps player 1's cameras (the follow camera, the locked ones, which is current, the blend, the scene stack, the switches, the target list, the watched human, the shake and slow motion), andsrc/scripting/camera_bindings.*makesCamSetupFollow,CfgFollowCamera(FollowCamera::configure(): band 3.0-3.5 m, zoom step 4.8, 30°),CamSetFollowZoom,CamSetFollowAngle(clamped, reached at once),CameraReset(behind the player at the nearest preset, pitch back to 13°),CameraCreateLocked,CameraMakeActive,CamEnable(switches 0, 5 and 6 act),CamTargetandCamSetSecondary(keep in view, 0.25 of the field of view, 35 % of the excess, at most 270°/s) real. The zoom step follows the band's near edge as it eases (3.72 m and 5.88 m); - blends (Blends):
CameraBlendlerps the position and look-at point and slerps the orientation from the view shown when it began to the destination's live view, linear in time, the far clip never growing; at the end the destination becomes current directly, which runs the follow camera's activation (FollowCamera::activate()). A cut (Cameras::cuts()counts them) is drawn as a jump: a frame drawn between two steps takes the new camera whole instead of sweeping from the old one, as the original draws only whole steps. A locked camera looks along its angles at a point 3 m ahead, its far clip at most 150. While it is current it keeps the humansCamLockLockedlists inside its sides (camera::keepInView(),repo:src/camera/locked_camera.h): the head point 1.4 m up is pushed to 0.3 m inside the left side, or else the right, the move's part along the side clipped by the world 0.3 m short of a wall, the feet snapped to the ground, and after one push every later listed human is placed again, as the original's flag carries over. Coney's readings: the sides are the planes at half the horizontal field of view either side (left first); the move is from the human's feet at the last update; an airborne human is not landed (no fall damage); - the path camera (Path cameras,
repo:src/camera/path_camera.h):CamSetupPoizostarts it from a camera's view,CamAddPoizoPoint/CamAddPoizoPointCamappend up to 8 points, and while it is current it flies the Catmull-Rom curve (each end point's neighbour standing in for the missing one), slerping the orientations, and gameplay calls each point's function as it is reached and the end function at the last, where it stays.CamReversePoizoflies it back: the points reversed, each segment's time moved with it, the new end function, the flight started again (Coney's reading: each point keeps its own function). A point's angles turn it as a locked camera's do (camera::scriptedOrientation(), the same code in the original); Coney's choice: the look-at point is 3 m ahead; - the combat camera (Combat camera): with L1 held while the player has a fight target
(
Fighter::target()) the band's wanted near edge goes to 2.4 m (4.8, 4.44, 4.134, ... at 4.5/s) and the target pitch to 15°, the enemy's point (position + half its velocity) is turned toward 27° off the view's centre (0.455 of the excess, at most 640°/s beyond 29°), and on release the saved band comes back while the pitch stays; - the shake (Shake) at the three strengths, 0.66 in combat, its time counted with the characters' step,
and the rumble byte it drives. Player 1's camera shakes when a reaction plays to his hit (at the hit code's strength
bits), when he reacts himself from strength 2, and at level 1 when his rage starts (
Fighter::reactionShake(),Fighter::rageStarted(), read byhuman::Playerafter the step); - slow motion (Slow motion): the player's clip events
0x2e/0x2fset the characters' stepSlowMotion::stepSeconds()toCfgFollowCamera's factor of 1/30 s and back, and every human of the step (Humans::update()) advances by it: animation, motion, stamina, gravity, an attack's steer and a grab's alignment; - in play (
--play-level, the story): gameplay (GameplayMode) makes player 1's cameras before each level's script, gives them to the bindings (BindingContext::cameras, read at each call) and to the level (ScriptedCast), whose player steps them (Player::setCameras());CamSetSecondaryfinds its human at its live position (the scripts' brains). A scene's camera is theirs too: the play mode's scene stage pushes and pops it (Cameras::beginScene(),setSceneView(),endScene()), so play blends back to the follow camera over the scene'sBlendCam. The level streams round the current camera and draws through its lens, the far clip capping the draw distance; --trace FILEwrites the camera's position, look-at point, wanted position, distance, angles, band and auto-follow turn after every step, with the player's state (Running Coney).
The world viewer keeps its own free camera with the player camera's lens (The streamed world).
Coney choices where the research is silent:
- One player camera:
0x0050b19c, the number of player cameras (0x00122ed0), is 1 (the debug menu's One player camera), as Coney has no split screen. Off is the two-player case: the sprint keeps the band and only lowers the pitch, and the way back goes to the maximum distance less 0.5, as the page says. - The zoom distance starts at the maximum (6.6 m, upper pitch limit 40°), as read in the street, for a camera no
script configures (the sandbox,
--play-levelwithout scripts). - Locked cameras: their angles and path points' are turned into a view as the original does
(
camera::scriptedOrientation():q = qz(heading) × qx(pitch) × qy(roll), heading anticlockwise from +y, a positive pitch looking up, Scripted camera angles), and the play mode draws through the camera's own up, so a roll shows. The line of sight test, the blend's sphere push and keep-in-view's ray (for a range above 0) are left out. - A follow camera that is not current is not updated; it only notes where the player is, so a reset or the activation places it on him.
- The shake: one shake on the manager, applied to whichever camera is current; the view offset is a random share in
[-1, 1] of the amplitude × 0.05 m on each axis (the original's form: Shake); after its time the amplitude
eases back to 0 at the same 65 %. Coney has one player camera, so a player's hit shakes player 1's. The animation
event that starts a shake (type
0x20, Shake) is not wired yet. - Slow motion: the combat timers (stun, ground and game time,
nowMs) still count whole updates of 1/30 s, and the cameras update by the frame's 1/30 s, as in the original (Slow motion). - The sprint time
+0x36ccounts only at the sprint gait and is zeroed off it, so every sprint arms the zoom (a run before the sprint, as in the street's runs, would otherwise keep it from arming). A sprint that starts while the band is still going back saves the band it was going back to, so a quick second sprint does not keep a band left half-way. - The side probes give each side's room as the largest clear multiple of the probe angle with every smaller one
clear; when the two differ by more than 7.5° the wanted position turns toward the roomier side by 20 % of half the
difference (the turn that would even them up) each update. The fully blocked view's 480°/s turn, the side factors
+0x434/+0x438, the second ray from the target, the sphere pushes and the next update's lag are not implemented. - Disabled triangles: the main ray's rule is in place, but which triangles the game switches off in a level
(
0x0034fba0from a game object's box, Collision) is not known from the data yet, so every triangle of a level stays enabled in Coney and the street's disabled panel still pulls the camera in there. - The height hold is never entered by the player yet: the probe and its lower heights are now on the page (Height hold) but not modelled, nor the two special modes' 40.5 % and 48 %.
- Beyond 157.5° the auto-centre rule's falling line is carried on for a running player (5°/s at 180°). The other
gates of Heading (human flags, the clip's descriptor flag
0x8000, state flag 4, the watched target) are not modelled. - The blocked-view latch clears on the second update the player stands (gait 0, on the ground), as at runtime;
the clearing condition is now on the page (Heading), and
+0x45cis not modelled apart from it. - A fresh camera (at the start, or after the player is put back) sits behind the player at 4.8 m and 13°.
--startwith a distance and a yaw places it there instead, its wanted position with it and the hard band stepped once (FollowCamera::place()), so a trace scenario starts with the camera of the original's save state. - The Rumble cameras (
src/camera/win_camera.*, Rumble: win camera):CameraCreateWinsets up the one win camera on the winner, which starts again when made active (the scripts teleport the winner in the same frame) and orbits until replaced; its stop condition is not read.CamDeleteforgets a locked camera, cutting to the follow camera when it was current (Coney choice). CamSetFollowHeading(degrees)(FollowCamera::placeBehind(), the placementCameraResetuses): the camera at once along the player's forward turned clockwise bydegrees(180 behind him), looking back at him at the target pitch, at the preset distance nearest its own (the sprint zoom's saved edge while one is held) clamped to the band; the band moves to that preset and the zoom step to the one after it.- The rail, fixed and third-person cameras (
src/camera/rail_camera.*,fixed_camera.*,third_camera.*): the rail camera follows Rail cameras for player 1 (one target, the set-up's): the settings and their easing, the target and look-at points, the damping, mode 0's segment choice, foot, reach, hand-over, ceiling, fixed pitch and end hold, and the leading modes. Coney choices and stand-ins: the end hold lasts only while its condition does (what clears it is not traced); the tie between two nearest rail points, the 40% look-at catch-up (+0x3ec), the+0x330vector, the split-screen halving and extras, mode 3 (settings 5, 6, 7) and the push out of world collision are not built; a rail with no points stands 3 m behind and 1 m above the look-at point. The fixed camera always looks at the average (the original's single-entry case is not traced).CamDeleteforgets a fixed camera as it does a locked one and keeps third-person cameras.
Notes for implementers¶
level99's calls (Script calls):CfgFollowCameraleaves the band at the minimum (checkpoint 1 plays at 3.0-3.5 m);CamSetFollowZoom(1)from checkpoint 2 moves it to 4.8-5.3 m without moving the camera;CameraResetputs the camera behind the player and the pitch back to 13°;CameraMakeActive(…, 1)is a linear 1 s blend from the locked camera's view to the live follow camera;CamSetSecondaryswaps auto-follow for keep-in-view;CamSetFollowAngle(-10)clamps to the lower limit (−4.3°) and stays until the next reset.- Combat camera (Combat camera): with L1 held at a target, band to 2.4 m (4.5/s ease), target pitch 15° (kept afterwards), enemy held 27° off centre; restore the saved band on release.
- A first follow camera that matches the numbers: look at the player's feet + 1.4 m; keep the camera where it is unless its distance to the look-at point leaves the 3.0-3.5 m band, then move it along that line to the band; move 22% of the way to the wanted position each 30 Hz step; hold a 13° pitch; 65° horizontal field of view, near 0.1, far 115.
- Heading: in the street the auto-centre rule (Heading) turns the camera toward the facing whenever the
player moves, with the angle measured along the camera's view at the start of the update, which faces the aim point,
not the look-at point (about 2° more at a run's circle; In the street); with the stick held sideways the
player runs in a circle. At
level99's checkpoint 1 it was not seen. Implement it, with the leash and the right stick. The player's stick is turned by the camera's heading before it reaches the character (Characters), so the camera must ease, never snap. Gate it on the gait (walk, run or sprint; not idle, sneak speed or jog), not on which clip plays, and hold it off from a blocked main ray until the player stops (the+0x45dlatch; this is why checkpoint 1 has none). With the stick held sideways the original circles at about 190°/s at a run and 143°/s at a walk, the rule's turn plus the leash's drag. - Sprint zoom (Sprint zoom): on the first update at the sprint gait save the band, zoom and
target pitch and start a 0.5 s timer; the band's near edge moves by
d × |d| / T × dttoward the minimum distance (dwhat is left,Tthe timer, after it has counted down once), the target pitch in a straight line to 7°; 250 ms after the sprint gait ends, the same back to the saved values. - Look-at point: limit its move per update by the distance rule of step 5 (20% above 0.8 m, a linear share from 0.8 to 0.4 m, all of it below), except while jumping or falling; any trigger on "a rise" is not what the original does.
- Right stick: yaw 60-150°/s outside a ±48 raw dead zone, applied to the wanted position at once (the lag smooths it); pitch only near the ends of the travel; 0.334 s of no auto-follow after any input.
- Collision: start with a ray from the look-at point and pull in to the hit, with the main ray's mask, its disabled-triangle rule and the excluded materials 30, 122 and 107 (World collision); the triangles' enabled bits must follow the game (doors, glass and barriers the game switches off), or a disabled panel pulls the camera in. The side probes and swing-away rules can come later.
- Update order: the cameras update at the end of the characters' 30 Hz step, after movement (Characters), and the device takes the lens once per frame.
- Keep the camera deterministic (no real time) so the test mode can compare frames.
In Coney, CamCanSee asks whether player 1's current view sees the point 1 m above a human or 0.3 m above an
object (camera::canSeePoint(): within the range and the far clip, inside the 4:3 view window beyond the near clip,
no collision triangle on the line, Coney's reading: the default ray mask), and CamGhostDoor sets type bit
0x200 on a door's two triangles, which the follow camera's rays skip. CameraSetClipping sets a locked camera's
near and far clips and the follow camera's own far clip (at most 150; the follow camera keeps its 0.1 near clip),
CameraGetActive answers the current camera's handle (NilHandle for a scene's or for player 2), CamGetPos its
position, and CamSetFollowPos puts the follow camera at a point at once.
Open questions¶
- Look-behind in a fight (answered, Looking behind): R1 turns the view round only while the player
runs or sprints (gait 4 or 5, record
+0x08clear), so never while he blocks or walks locked on. Confirmed (code) at0x0012b4a0; a runtime check (R1 held at a standstill, at a walk and at a run) is still to be made. - The size of
Cam_Follow_RightStick(answered): 2,160 bytes,0x00129050-0x001298bf, ending with its onlyjr ra; nothing calls or jumps to0x00129100(176 bytes in, theaddiuthat completes the address of switch 00x0050b1b8after alui), so that address is not a function start. Confirmed (code). - R1 at a run: block or look-behind? (answered, Looking behind): the block's fight test fails at a
run or sprint, so R1 keeps gait 4, the 7.80 m/s and the run clip and turns the view; the block starts the update
the gait drops below 4. Confirmed (code) at
0x00224f28; confirmed (runtime). - The look-behind's band at a run: the near edge
+0x32cread 6.1 m while R1 was held (from 4.8), not the 8.5 m0x0012d7a8is given; which field holds the 8.5 m is open. - The circling rate (answered): the original circles at about 190°/s at a run and 143°/s at a walk; the 122°/s
and 127°/s first given were the rule's share (In the street). The rule's
ais measured from the camera's orientation, which faces the aim point. Measured that way, the formula matches the turn exactly. - The sprint zoom (answered, Sprint zoom, with its gates: no enemies or the nearest within 12 m,
and
0x0050b19cthe number of player cameras). - The collision step (answered, World collision): the table of fractions is never read, and the side
angle history (
+0x428-+0x430) drives the centring in a gap. - Slow motion's clips (answered, Slow motion): grab and mount power moves, tandem ends, the bottle smash and one boss counter.
- The combat camera's 0.4 keep-in-view (answered):
0x0012e170(0.4)keeps the player's brain target (brain+0x124) in view while he is in a fight stance that counts for the camera and no secondary target is watched (update step 9). - Teleports (answered):
Teleport/TeleportToFlagdo not touch the cameras. Confirmed (code): neitherObject_Teleport(0x00385bb8) norObject_TeleportToFlag(0x00385db0) calls the camera code. Neither do the human's set-transform slot (Human_SetTransform0x0023d440: pair break, ground ray, brain heading, "moved" marks+0x1b3and+0x54bit0x40000000) or slot+0x14c(Human_ResetMotion0x0023f158: velocities and turn zeroed, transform re-applied), and the only reader of+0x1b3isHuman_GetNavPolygon. So the follow camera sees only the target's new position. Its look-at point follows a move of more than 0.8 m at 20% of the gap per update (update step 5), and once the held position is more than 1.3 × the maximum distance from the look-at point (or 30 m above or below it), step 7 puts it behind the player asCameraResetdoes, with one player camera only. A script that wants a cut callsCameraResetitself (inferred: the camera's path for a long move follows from those two steps; not seen at runtime). - The power camera's events and shots (answered, Power-move camera): begin
0x39, add shot0x3b, cut0x3a, end0x3d; eight shots at most, in the followed human's frame at the begin; the first cut tests them with two sphere sweeps and picks a pair from a counter; every cut and the hand-back are hard cuts. - The power camera, still open: a boss's counter in one-player play places the shots around the player, not the
boss (code); whether that looks right on screen, and a blocked shot, mode 1 and two players, are not seen at
runtime. The begin stores the saved camera's fov (
+0x324) and nothing in the class reads it. The behaviour classes that hand their events to the partner (0x4d-0x4f,0x6a,0x77,0x78,0x80,0xa0,0xa1,0xb2,0xb3,0xed) are not identified.