references/full.md
# Roblox Camera: Full Reference
> **Code in this reference is illustrative. Adapt to your game and verify in Studio before production use.**
Camera work is **client-only**. `workspace.CurrentCamera`, `CFrame` setters, and `PreRender` callbacks must run in a `LocalScript` or `Script` with `RunContext = Client`. Server scripts silently drop camera writes.
## The Camera Object
Each client has exactly one camera, accessible at `workspace.CurrentCamera`. Other `Camera` instances exist for `ViewportFrame`s (mini-map, security camera, etc.) but `CurrentCamera` is the player's main view.
### CameraType Enum
| Value | Behavior | Requires CameraSubject? |
|-------|----------|------------------------|
| `Fixed` (0) | Stationary. | No |
| `Attach` (1) | Moves with subject at a fixed offset; rotates as subject rotates. | Yes |
| `Watch` (2) | Stationary, rotates to keep subject centered. | Yes |
| `Track` (3) | Moves with subject's position, doesn't auto-rotate. | Yes |
| `Follow` (4) | Moves with subject + rotates to keep centered. | Yes |
| `Custom` (5) | Default mode used by Roblox core scripts. | No |
| `Scriptable` (6) | No default behavior. You set `CFrame` manually. | No |
| `Orbital` (7) | Fixed Y position, rotates around player. | Yes |
`Attach`, `Watch`, `Track`, `Follow`, and `Orbital` all require a valid `CameraSubject`. Without one, behavior is undefined.
### CameraSubject
Set to the local `Humanoid` by default for character following (with `Humanoid.CameraOffset` applied). Can also be set to a `BasePart` (e.g. a `VehicleSeat`).
```luau
-- Restore default
camera.CameraSubject = player.Character:FindFirstChildWhichIsA("Humanoid")
```
`CameraSubject = nil` reverts to the previous value (you cannot set it to nil).
## Camera Properties (full)
| Property | Type | Notes |
|----------|------|-------|
| `CFrame` | CFrame | Position + orientation. Defaults overwrite this unless `Scriptable`. |
| `CameraSubject` | Instance | Humanoid, BasePart, etc. |
| `CameraType` | CameraType | See enum above. |
| `FieldOfView` | float (degrees) | 1–120. Vertical FOV when `FieldOfViewMode = Vertical`. |
| `FieldOfViewMode` | Enum.FieldOfViewMode | `Vertical` or `Diagonal`. |
| `DiagonalFieldOfView` | float (degrees) | Read when `FieldOfViewMode = Diagonal`. |
| `MaxAxisFieldOfView` | float (degrees) | Read when `FieldOfViewMode = Diagonal`. |
| `NearPlaneZ` | float (studs) | Near clipping plane. |
| `ViewportSize` | Vector2 | Pixel size of the viewport. |
| `HeadLocked` | bool | VR: locks view to head. |
| `HeadScale` | float | VR: scales head CFrame. |
| `Focus` | CFrame | CFrame the camera points at (managed by CameraType logic, not by you). |
| `VRTiltAndRollEnabled` | bool | VR-only. |
| `CoordinateFrame` | CFrame | Hidden alias for `CFrame`. Use `CFrame`. |
## Camera Methods (full)
| Method | Returns | Use |
|--------|---------|-----|
| `GetRenderCFrame()` | CFrame | The "true" rendered CFrame, including VR head rotation not reflected in `.CFrame`. |
| `GetRoll()` | float (radians) | Roll set via `SetRoll` (not roll manually applied via CFrame). |
| `SetRoll(rollAngle)` | () | **Outdated**: apply roll via `CFrame.Angles(0, 0, roll)` on CFrame instead. |
| `ScreenPointToRay(x, y, depth?)` | Ray | Unit ray from screen pixel coords. Accounts for GUI inset. |
| `ViewportPointToRay(x, y, depth?)` | Ray | Unit ray from device-safe viewport coords. Does NOT account for GUI inset. |
| `WorldToScreenPoint(worldPos)` | Vector3 | `(x, y, onScreen)`. Pixels accounting for GUI inset. |
| `WorldToViewportPoint(worldPos)` | Vector3 | `(x, y, onScreen)` in device-safe viewport coords. |
| `GetPartsObscuringTarget(castPoints, ignoreList)` | {BasePart} | Parts obscuring the camera's view of given world points. |
| `ZoomToExtents()`, `Interpolate(...)` | () | Editor camera methods, not for gameplay cameras. |
| `PanUnits`, `TiltUnits`, `GetPanSpeed`, `GetTiltSpeed` | various | Editor viewport camera controls. |
### Event
`InterpolationFinished`: fires when `Interpolate` completes.
## ScreenPointToRay vs ViewportPointToRay
The two are NOT interchangeable:
| Method | Coordinates | GUI inset? | Use when |
|--------|-------------|-----------|----------|
| `ScreenPointToRay(x, y)` | Pixel coords, origin = top-left **below** top bar | Yes | Mouse picking, player-facing UI clicks |
| `ViewportPointToRay(x, y)` | Device-safe pixel coords, origin = top-left of Roblox top bar | No | Raw device input, drawing in viewport space |
Both return a **unit Ray** (1 stud long). To get an actual raycast, multiply `Direction`:
```luau
local unitRay = camera:ScreenPointToRay(mouseX, mouseY)
local result = workspace:Raycast(unitRay.Origin, unitRay.Direction * 500)
```
`ScreenPointToRay` only works for `workspace.CurrentCamera`. ViewportFrame cameras have an initial `(1,1)` viewport size that doesn't update until you set them as CurrentCamera.
## CFrame Math
### Constructors
| Constructor | Notes |
|-------------|-------|
| `CFrame.new()` | Identity at origin. |
| `CFrame.new(pos: Vector3)` | Position only, identity rotation. |
| `CFrame.new(x, y, z)` | Position only. |
| `CFrame.new(x, y, z, qX, qY, qZ, qW)` | Position + quaternion rotation. |
| `CFrame.new(x, y, z, R00, R01, ..., R22)` | Position + raw 3x3 rotation matrix. |
| `CFrame.new(pos, lookAt)` | **Legacy** (back-compat); use `CFrame.lookAt`. |
| `CFrame.lookAt(at, lookAt, up?)` | Construct at `at` oriented toward `lookAt`. Optional `up` defaults to `(0,1,0)`. Fails if `lookAt` directly above `at` (use `lookAlong`). |
| `CFrame.lookAlong(at, direction, up?)` | Construct at `at` oriented along `direction`. |
| `CFrame.fromRotationBetweenVectors(from, to)` | Rotation that maps `from` to `to`. |
| `CFrame.fromEulerAngles(rx, ry, rz, order?)` | Euler angles (radians), default XYZ. |
| `CFrame.fromEulerAnglesXYZ(rx, ry, rz)` | XYZ order (== `CFrame.Angles`). |
| `CFrame.fromEulerAnglesYXZ(rx, ry, rz)` | YXZ order. |
| `CFrame.Angles(rx, ry, rz)` | XYZ Euler, shorthand. |
| `CFrame.fromOrientation(rx, ry, rz)` | YXZ Euler, shorthand. |
| `CFrame.fromAxisAngle(axis: Vector3, angle: number)` | Rotation around an arbitrary axis. |
| `CFrame.fromMatrix(pos, xVector, yVector, zVector)` | From explicit basis vectors. |
### Properties
| Property | Type | Notes |
|----------|------|-------|
| `Position` | Vector3 | Position component. |
| `Rotation` | Vector3 | Euler angles (radians). |
| `X`, `Y`, `Z` | number | Components of `Position`. |
| `LookVector` | Vector3 | -Z axis in world space. Forward. |
| `RightVector` | Vector3 | X axis in world space. |
| `UpVector` | Vector3 | Y axis in world space. |
| `XVector`, `YVector`, `ZVector` | Vector3 | Same as Right/Up/-LookVector. |
### Methods
| Method | Returns | Use |
|--------|---------|-----|
| `cf:Inverse()` | CFrame | Inverse. |
| `cf:Lerp(goal, alpha)` | CFrame | Linear interp 0..1. |
| `cf:Orthonormalize()` | CFrame | Renormalize (fix drift from many small ops). |
| `cf:ToWorldSpace(...cfs)` | CFrame(s) | `= cf * other`. |
| `cf:ToObjectSpace(worldCf)` | CFrame | `= cf:Inverse() * worldCf`. |
| `cf:PointToWorldSpace(p)` | Vector3 | Treats `p` as position. |
| `cf:PointToObjectSpace(p)` | Vector3 | Inverse for positions. |
| `cf:VectorToWorldSpace(v)` | Vector3 | Treats `v` as direction (no translation). |
| `cf:VectorToObjectSpace(v)` | Vector3 | Inverse for directions. |
| `cf:GetComponents()` | (number×12) | Flatten to 12 numbers. |
| `cf:ToEulerAngles()` | (rx, ry, rz) | XYZ Euler. |
| `cf:ToEulerAnglesXYZ()` | (rx, ry, rz) | XYZ Euler. |
| `cf:ToEulerAnglesYXZ()` | (rx, ry, rz) | YXZ Euler. |
| `cf:ToOrientation()` | (rx, ry, rz) | YXZ Euler. |
| `cf:ToAxisAngle()` | (axis, angle) | Decompose into axis-angle. |
| `cf:FuzzyEq(other, epsilon?)` | bool | Approximate equality. |
| `cf:AngleBetween(other)` | float | Angle between two CFrame orientations. |
`BasePart.CFrame` is auto-orthonormalized, but `CFrame` math operations can drift; call `:Orthonormalize()` before passing to APIs that don't auto-normalize.
### Math Operations
| Op | Type | Returns |
|----|------|---------|
| `cf + cf` | CFrame | Sum of components (rarely the right choice). |
| `cf - cf` | CFrame | Difference. |
| `cf * cf` | CFrame | Composition: rotate/translate by right operand in left's space. |
| `cf * Vector3` | Vector3 | Point: position transformed. |
| `cf / cf` | CFrame | Right-multiplied by inverse. |
## Custom Camera Controllers
### Smooth follow (third person)
```luau
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local camera = workspace.CurrentCamera
local OFFSET = Vector3.new(0, 4, 12)
local SMOOTH = 8
camera.CameraType = Enum.CameraType.Scriptable
RunService.PreRender:Connect(function(dt)
local character = player.Character
if not character then return end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then return end
local target = root.CFrame * CFrame.new(0, 2, 0)
local desired = CFrame.lookAt(target.Position + OFFSET, target.Position)
local alpha = math.min(dt * SMOOTH, 1)
camera.CFrame = camera.CFrame:Lerp(desired, alpha)
end)
```
### First-person
```luau
camera.CameraType = Enum.CameraType.Scriptable
RunService.PreRender:Connect(function()
local head = character:FindFirstChild("Head")
if not head then return end
camera.CFrame = head.CFrame + Vector3.new(0, 0.5, 0)
end)
```
For real first-person you also want to hide the local head: set the head's `LocalTransparencyModifier = 1` (client-only) on the local character, or render with a `FirstPersonAccessory` rig.
### Third-person with adjustable zoom (mouse wheel)
```luau
local zoomDist = 12
local MIN_ZOOM, MAX_ZOOM = 2, 30
UIS.InputChanged:Connect(function(input, gpe)
if gpe then return end
if input.UserInputType == Enum.UserInputType.MouseWheel then
zoomDist = math.clamp(zoomDist - input.Position.Z * 2, MIN_ZOOM, MAX_ZOOM)
end
end)
```
### Orbital camera (fixed Y, mouse-look)
```luau
local yaw, pitch = 0, 0
local DIST = 12
local PITCH_LIMIT = math.rad(80)
UIS.InputChanged:Connect(function(input, gpe)
if gpe then return end
if input.UserInputType == Enum.UserInputType.MouseMovement then
yaw = yaw - input.Delta.X * 0.003
pitch = math.clamp(pitch - input.Delta.Y * 0.003, -PITCH_LIMIT, PITCH_LIMIT)
end
end)
RunService.PreRender:Connect(function(_dt)
local root = character.HumanoidRootPart
local orbit = CFrame.Angles(0, yaw, 0) * CFrame.Angles(pitch, 0, 0)
local offset = orbit.LookVector * DIST
camera.CFrame = CFrame.lookAt(root.Position + offset, root.Position)
end)
```
### Mouse-look (FPS)
```luau
local ROT_SPEED = 0.003
local PITCH_LIMIT = math.rad(89)
local yaw, pitch = 0, 0
UIS.InputBegan:Connect(function(input, _gpe)
if input.UserInputType == Enum.UserInputType.MouseButton2 then
UIS.MouseBehavior = Enum.MouseBehavior.LockCenter
end
end)
UIS.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton2 then
UIS.MouseBehavior = Enum.MouseBehavior.Default
end
end)
UIS.InputChanged:Connect(function(input, gpe)
if gpe then return end
if input.UserInputType == Enum.UserInputType.MouseMovement then
yaw = yaw - input.Delta.X * ROT_SPEED
pitch = math.clamp(pitch - input.Delta.Y * ROT_SPEED, -PITCH_LIMIT, PITCH_LIMIT)
end
end)
RunService.PreRender:Connect(function()
if not character:FindFirstChild("Head") then return end
local head = character.Head
camera.CFrame = CFrame.new(head.Position) * CFrame.Angles(0, yaw, 0) * CFrame.Angles(pitch, 0, 0)
end)
```
## Cutscenes
### Basic tween
```luau
local TweenService = game:GetService("TweenService")
local camera = workspace.CurrentCamera
-- Snapshot the current mode; restoration must put back what the player had.
local prevCameraType = camera.CameraType
camera.CameraType = Enum.CameraType.Scriptable
local tweenInfo = TweenInfo.new(2, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut)
local targetCFrame = CFrame.lookAt(Vector3.new(0, 20, 30), Vector3.new(0, 5, 0))
local tween = TweenService:Create(camera, tweenInfo, {CFrame = targetCFrame})
tween:Play()
tween.Completed:Wait()
-- Restore the mode the player actually had, not a hardcoded default.
camera.CameraType = prevCameraType
```
### Multi-shot cutscene with input lock
```luau
local cutsceneId = 0 -- ownership token: a newer cutscene supersedes older ones
-- The snapshot of the state this cutscene locks. At most one cutscene owns
-- it: a newer run ADOPTS the pending snapshot instead of re-snapshotting
-- values it already finds locked to zero.
local cutsceneSnapshot: {
cameraType: Enum.CameraType,
walkSpeed: number,
jumpPower: number,
}? = nil
local function playCutscene(cameraPath: {CFrame}, duration: number)
cutsceneId += 1
local myId = cutsceneId
-- Adopt the prior run's snapshot if one is pending; only the FIRST
-- cutscene reads the player's live values. A replacement re-snapshot
-- would capture our own locked zeros and restore them as if they were
-- the player's real state.
local saved = cutsceneSnapshot
-- Function scope: both the snapshot branch and the lock/restore below
-- need the humanoid, so the lookup must not live inside the conditional.
local humanoid = character:FindFirstChildWhichIsA("Humanoid")
if not saved then
saved = {
cameraType = camera.CameraType,
walkSpeed = humanoid.WalkSpeed,
jumpPower = humanoid.JumpPower,
}
cutsceneSnapshot = saved
end
camera.CameraType = Enum.CameraType.Scriptable
humanoid.WalkSpeed = 0
humanoid.JumpPower = 0
for _, target in ipairs(cameraPath) do
local info = TweenInfo.new(duration / #cameraPath, Enum.EasingStyle.Sine)
local tween = TweenService:Create(camera, info, {CFrame = target})
tween:Play()
tween.Completed:Wait() -- also resumes if a newer tween cancels this one
if cutsceneId ~= myId then
return -- superseded: the newer cutscene owns camera, movement,
-- AND the pending snapshot; leave it pending for that run
end
end
-- Restore the snapshot, never hardcoded defaults. Only the newest run
-- reaches here (older runs returned early), and the snapshot still
-- pending is the original player state.
humanoid.WalkSpeed = saved.walkSpeed
humanoid.JumpPower = saved.jumpPower
camera.CameraType = saved.cameraType
cutsceneSnapshot = nil -- state is restored: nothing pending
end
```
Overlapping runs are safe by construction: the first run snapshots the live
values; every superseding run reuses that same snapshot; only the run that
finishes last (the current owner) restores it and clears the pending slot.
### Screen shake
```luau
local shakeToken = 0 -- ownership token: only the newest shake drives state
local shakeIntensity = 0
local prevOffset = CFrame.identity
local lastShakeCFrame: CFrame? = nil -- camera CFrame as our last shake write left it
local function startShake(intensity: number, duration: number)
shakeToken += 1
local myToken = shakeToken
shakeIntensity = intensity
task.spawn(function()
local t = 0
while t < duration do
t += task.wait()
if myToken ~= shakeToken then return end -- a newer shake owns state
shakeIntensity = intensity * (1 - t / duration)
end
if myToken ~= shakeToken then return end
shakeIntensity = 0
end)
end
RunService.PreRender:Connect(function()
if shakeIntensity <= 0 then
-- Strip the final offset exactly once so it does not stay baked in.
-- Only strip when the camera STILL holds our last write: another
-- system may have overwritten camera.CFrame since, and stripping
-- from that fresh base would subtract an offset it never contained.
if prevOffset ~= CFrame.identity then
if lastShakeCFrame and camera.CFrame == lastShakeCFrame then
camera.CFrame = camera.CFrame * prevOffset:Inverse()
end
prevOffset = CFrame.identity
lastShakeCFrame = nil
end
return
end
local offset = CFrame.new(
(math.random() - 0.5) * shakeIntensity,
(math.random() - 0.5) * shakeIntensity,
(math.random() - 0.5) * shakeIntensity
)
-- Re-base on the CURRENT camera CFrame each frame, and remember it: strip
-- the previous frame's offset only while our last write is still current.
-- If another system moved the camera between frames, that fresh base never
-- contained our offset, so subtracting it would corrupt their CFrame;
-- instead the stale offset is simply dropped (resets shake baseline).
local base = camera.CFrame
if lastShakeCFrame and base == lastShakeCFrame then
base = base * prevOffset:Inverse() -- our offset is still baked in
end
camera.CFrame = base * offset
lastShakeCFrame = camera.CFrame
prevOffset = offset
end)
```
## World ↔ Screen Conversion
```luau
-- World to screen (with GUI inset)
local screenPos, onScreen = camera:WorldToScreenPoint(worldPos)
-- screenPos.X, screenPos.Y are pixel coordinates (accounting for GUI inset)
-- onScreen = true if point is within the visible viewport
-- screenPos.Z > 0 means in front of camera
-- World to viewport (raw device coords)
local viewportPos, onScreen = camera:WorldToViewportPoint(worldPos)
```
## ViewportFrame Portals (seamless portals)
Community technique (DevForum "Making seamless portals - Tutorial", thiagop123, 2026, https://devforum.roblox.com/t/making-seamless-portals-tutorial/4731945) for rendering a live portal to another location: clone the destination world Model into a `ViewportFrame` whose camera is a second `Camera` instance, and display it on the portal's face. Crossing is handled by raycasting the `HumanoidRootPart`'s movement against the portal part each frame; when the character crosses the front face (`dot ≥ 0.999`), teleport it and remap position, direction, and velocity relative to the destination surface (180° mirrored); directionality replaces a cooldown. A per-frame "physics hole" (collision disabled within ~5 studs) lets the character pass through the opening without falling off the edges; a `CollisionGroups` server script handles portal-air collision. Verify performance: each pair costs 2 world clones + 2 clones per character + per-frame render (see the thread's common-problems table for StreamingEnabled, back-face entry, and camera-flicker/`BindToRenderStep` pitfalls).
## Mouse-to-World Raycast Pattern
```luau
local function mouseHit(mouseX, mouseY, maxDist)
local unitRay = camera:ScreenPointToRay(mouseX, mouseY)
return workspace:Raycast(unitRay.Origin, unitRay.Direction * maxDist)
end
local result = mouseHit(mouse.X, mouse.Y, 1000)
if result then
local hitPart = result.Instance
local hitPos = result.Position
local hitNormal = result.Normal
-- ...
end
```
## Camera in Studio MCP
When scripting a camera via `execute_luau` in Studio (Play mode), set the camera on the active client. Useful for testing cutscenes:
```luau
-- In execute_luau, the local script context is the editing client
local camera = workspace.CurrentCamera
camera.CameraType = Enum.CameraType.Scriptable
camera.CFrame = CFrame.lookAt(Vector3.new(0, 50, 50), Vector3.zero)
```
For non-Play testing (Edit mode), `CurrentCamera` exists but the perspective doesn't render the way gameplay does; Scriptable camera changes won't be visible until Play.
## Common Mistakes
- **Camera work on the server.** Silently dropped. Must be in `LocalScript` or client `RunContext`.
- **Forgetting `CameraType = Scriptable`.** Defaults overwrite your CFrame every frame.
- **Using `Heartbeat` instead of `PreRender` for camera.** Adds 1 frame of lag. (`RenderStepped` still works but is superseded by `PreRender`.)
- **Setting `CameraSubject = nil` "expecting it to detach".** It reverts to previous value.
- **Reading `Camera.CFrame` in VR for gameplay math.** Use `GetRenderCFrame()` for true view (includes head rotation).
- **Using `SetRoll` because it's convenient.** It's outdated; apply roll via `CFrame.Angles(0, 0, roll)` on CFrame.
- **`CFrame.new(pos, lookAt)` because old tutorials say so.** Legacy/back-compat. Use `CFrame.lookAt(at, lookAt)`.
- **Confusing `ScreenPointToRay` with `ViewportPointToRay`.** GUI inset handling differs. Use `ScreenPointToRay` for mouse input.
- **Forgetting to restore `CameraType` after a cutscene.** Player is stuck.
- **Trying to `Raycast` with the unit Ray directly.** It's only 1 stud long. Multiply `Direction` by the actual distance.
- **Modifying the local head in first-person without `LocalTransparencyModifier`.** Players see their own head blocking the view. Use `LocalTransparencyModifier = 1`, not `Transparency` (Transparency replicates).
- **Setting `Humanoid.JumpPower = 0` to disable jump in a cutscene.** Remember to restore it. Or use `Humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)`.
- **Trying to set Camera.CFrame every frame without clearing the render connection on player respawn.** Stale references after respawn. Reconnect in `CharacterAdded`.
## Temporary camera ownership and collision checks
- Coordinate temporary views through one camera owner or explicit ownership tokens. Release on timeout, cancellation, subject removal, character replacement, round transition, and feature teardown. Restore the current character/controller when required; do not replay a snapshot captured before a newer camera mode or respawn.
- Test eliminated players who already respawned in the lobby separately from round survivors. Exercise a menu or spectate view opened during a death view, then close it after respawn. Confirm camera subject, controls, and zoom belong to the current character.
- Let collision clearance override a preferred minimum camera distance. Do not clamp a collision-limited offset outward through the obstacle. Test corners, shelves, low ceilings, obstructed candidate angles, and a safe fallback when no comfortable offset exists.
- When using `Spherecast`, handle geometry intersecting the initial sphere separately; the sweep does not detect those initial overlaps. Verify the final view visually rather than accepting an unobstructed open-space test as collision proof.
SKILL.md
---
name: roblox-camera
description: "Use when scripting or fixing Roblox camera behavior: CFrame placement, screen raycasts, first/third-person, shoulder views, cutscenes."
when_to_use: "Also when the camera clips through walls, jitters, aims wrong on touch or gamepad, or needs a death cam or spectate view."
last_reviewed: 2026-10-14
sources:
- https://create.roblox.com/docs/reference/engine/classes/Camera
- https://create.roblox.com/docs/reference/engine/enums/CameraType
- https://create.roblox.com/docs/reference/engine/datatypes/CFrame
- original
- https://raw.githubusercontent.com/Roblox/creator-docs/main/content/en-us/reference/engine/classes/WorldRoot.yaml
---
# Roblox Camera
## When to Load
Load when scripting custom camera behavior (cutscenes, third-person follow, custom rotation), manipulating `CFrame` for placement/rotation, raycasting from the screen, or building a non-default player view. Client-only.
## Quick Reference
**The Camera**: `workspace.CurrentCamera`: one per client. Set `CameraType = Scriptable` to disable defaults and take full control. Without it, defaults overwrite your CFrame every frame.
**CameraType**: `Fixed`, `Attach`/`Watch`/`Track`/`Follow` (subject-following), `Custom` (default), `Scriptable` (no default), `Orbital` (fixed Y, rotates around player). `CameraSubject` cannot be `nil`: setting it reverts.
**Key properties**: `CFrame`, `CameraSubject`, `FieldOfView` (deg), `FieldOfViewMode` (`Vertical`/`Diagonal`), `NearPlaneZ`, `ViewportSize`, `HeadLocked`, `HeadScale`, `Focus`.
**CFrame essentials**:
| Operation | Luau |
| --- | --- |
| Position only | `CFrame.new(position)` |
| Look at target | `CFrame.lookAt(position, target, upVector)` |
| XYZ rotation | `CFrame.Angles(rx, ry, rz)` |
| Local to world | `cf * Vector3.new(0, 0, 10)` |
| Interpolate | `cf:Lerp(goal, alpha)` |
| Unit axes | `cf.LookVector`, `cf.RightVector`, `cf.UpVector` |
**Custom camera loop**: always `PreRender`, never `Heartbeat`:
```luau
camera.CameraType = Enum.CameraType.Scriptable
RunService.PreRender:Connect(function(dt)
local desired = CFrame.lookAt(head.Position - offset, head.Position)
camera.CFrame = camera.CFrame:Lerp(desired, math.min(dt * 10, 1))
end)
```
**Raycasting from camera**: `camera:ScreenPointToRay(mx, my)` (accounts for GUI inset) vs `camera:ViewportPointToRay(mx, my)` (raw, NO inset). `ScreenPointToRay` returns a unit Ray (1 stud); multiply `Direction` by length for the actual raycast.
**Pitfalls**:
- Client-only. Server sets silently dropped.
- Without `Scriptable`, defaults overwrite your CFrame every frame.
- `PreRender` for camera (visual sync). `Heartbeat` adds 1-frame lag. `RenderStepped` still works but is superseded by `PreRender`.
- `Camera.CFrame` lacks VR head rotation; use `GetRenderCFrame()` for the true view.
- `SetRoll` is outdated; apply roll via `CFrame.Angles(0, 0, roll)` on CFrame.
- `CFrame.new(pos, lookAt)` is legacy (back-compat only); use `CFrame.lookAt(at, lookAt)` for new code.
See `references/full.md` for first/third-person recipes, cutscenes, screen shake, mouse-look, full API.