---
name: roblox-camera
description: Use when changing how the Roblox camera behaves - custom, top-down, side-scroller or cutscene cameras, camera shake, FOV effects, first-person lock, zoom limits, screen-to-world rays - or when rendering 3D models in UI with ViewportFrames and WorldModels. Covers every documented Camera property, CameraType, the default PlayerModule camera, BindToRenderStep priorities and ViewportFrame lighting.
---

# Cameras and ViewportFrames

Each client has its own `Camera` at `workspace.CurrentCamera`. It does not replicate, so **all camera code runs on
the client**. `CurrentCamera` can be replaced at runtime, so read `workspace.CurrentCamera` when you need it instead
of caching it forever.

## Camera properties

| Property | Notes |
|---|---|
| `CFrame` | Position and rotation. The default scripts overwrite it every frame unless `CameraType` is `Scriptable`. |
| `CameraType` | `Enum.CameraType`, read by the default camera scripts (table below). |
| `CameraSubject` | What the default scripts follow: a `Humanoid` (uses its state and `Humanoid.CameraOffset`) or a `BasePart` (VehicleSeats get a vertical offset). Can't be set to `nil`. |
| `FieldOfView` | Vertical FOV in degrees, clamped 1 to 120, default 70. |
| `FieldOfViewMode` | Which FOV stays constant when the window resizes: `Vertical` (default), `Diagonal`, `MaxAxis`. |
| `DiagonalFieldOfView`, `MaxAxisFieldOfView` | The same FOV expressed along the diagonal or the longest axis. |
| `Focus` | The point the engine prioritises for lighting and detail. Default scripts set it; a `Scriptable` camera must set it every frame. It does not move the camera. |
| `HeadLocked` | VR: apply head tracking (default `true`; leave it on). `HeadScale` scales the VR view, `VRTiltAndRollEnabled` applies CFrame tilt and roll in VR. |
| `NearPlaneZ` | Read-only near clip distance, between -0.1 and -0.5 depending on platform. |
| `ViewportSize` | Read-only size of the device safe area, top bar included. Only `CurrentCamera` updates it. |

Methods: `ScreenPointToRay` / `WorldToScreenPoint` (GUI-inset coordinates, same as `AbsolutePosition` and
`InputObject.Position`), `ViewportPointToRay` / `WorldToViewportPoint` (viewport coordinates, same as
`UserInputService:GetMouseLocation()`), `GetPartsObscuringTarget(points, ignore)`, `GetRenderCFrame()` (the CFrame
actually rendered, including VR head movement) and `ZoomToExtents(boxCFrame, boxSize)` (moves the camera so a
bounding box fits on screen). `Interpolate`, `SetRoll`, `PanUnits`, `TiltUnits` and `CoordinateFrame` are deprecated.

## CameraType

| Type | Behaviour with the default scripts |
|---|---|
| `Custom` | Normal Roblox camera; movement mode comes from StarterPlayer and the player's settings |
| `Scriptable` | Default scripts stop touching the camera. You own `CFrame` and `Focus` |
| `Follow` | Moves with the subject and rotates to keep it centred |
| `Track` | Moves with the subject without rotating on its own |
| `Orbital` | Fixed height, rotates around the subject |
| `Attach` | Fixed offset from the subject, rotates with it |
| `Watch` | Stays in place and turns to look at the subject |
| `Fixed` | Stays in place |

Every type except `Scriptable` needs a valid `CameraSubject`. To return to normal after a custom camera:
`camera.CameraType = Enum.CameraType.Custom` and `camera.CameraSubject = humanoid`.

## How the default camera works

`PlayerModule` (in the player's PlayerScripts) contains `CameraModule` and `ControlModule`. `CameraModule` binds
`"cameraRenderUpdate"` with `RunService:BindToRenderStep` at `Enum.RenderPriority.Camera.Value` (200). Each frame
the active controller computes CFrame and Focus from the subject, zoom and input, an occlusion module adjusts it
(`Zoom` pulls the camera in, `Invisicam` fades parts), and the result is written to `CurrentCamera`. It reads the
current `camera.CFrame` look direction as its starting point.

Settings live on StarterPlayer (defaults for everyone) and Player (per player at runtime):

- `CameraMode`: `Classic` or `LockFirstPerson`.
- `CameraMinZoomDistance` / `CameraMaxZoomDistance` (StarterPlayer defaults 0.5 and 400). Equal values lock zoom.
- `DevCameraOcclusionMode`: `Zoom` or `Invisicam`.
- Movement mode: `StarterPlayer.DevComputerCameraMovementMode` / `DevTouchCameraMovementMode`, named
  `Player.DevComputerCameraMode` / `DevTouchCameraMode` on Player. Values: `UserChoice`, `Classic`, `Follow`,
  `Orbital`, `CameraToggle` (computer only).
- Shift lock: `StarterPlayer.EnableMouseLockOption` (`Player.DevEnableMouseLock`).
- `Humanoid.CameraOffset` shifts the camera relative to the character: use it for over-the-shoulder views before
  writing a custom camera.

To freeze player movement during a cutscene:
`require(Players.LocalPlayer.PlayerScripts:WaitForChild("PlayerModule")):GetControls():Disable()` (and `:Enable()`).
PlayerModule is Roblox's default script, not a documented API, so keep such calls in one place.

## Scriptable camera

Update the camera in `BindToRenderStep` so it runs in a known order. Lower priority runs first; always pass
`.Value` of the enum (`Enum.RenderPriority`: First 0, Input 100, Camera 200, Character 300, Last 2000).

```luau
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer
local OFFSET = Vector3.new(0, 40, 25) -- top-down, slightly behind

local function start()
	workspace.CurrentCamera.CameraType = Enum.CameraType.Scriptable
	RunService:BindToRenderStep("TopDownCamera", Enum.RenderPriority.Camera.Value, function(dt)
		local camera = workspace.CurrentCamera
		local character = player.Character
		local root = character and character:FindFirstChild("HumanoidRootPart") :: BasePart?
		if not root then
			return
		end
		local goal = CFrame.lookAt(root.Position + OFFSET, root.Position)
		camera.CFrame = camera.CFrame:Lerp(goal, 1 - math.exp(-10 * dt)) -- frame-rate independent smoothing
		camera.Focus = CFrame.new(root.Position)
	end)
end

local function stop()
	RunService:UnbindFromRenderStep("TopDownCamera")
	local camera = workspace.CurrentCamera
	camera.CameraType = Enum.CameraType.Custom
	local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid")
	if humanoid then
		camera.CameraSubject = humanoid
	end
end
```

- To adjust the default camera instead of replacing it, keep `Custom` and bind at `Camera.Value + 1`, after it.
  Two bindings at the same priority run in random order.
- `RunService.PreRender` replaces `RenderStepped` for per-frame client work, but it has no ordering against the
  default camera. Use `BindToRenderStep` for camera code.
- Cutscenes: set `Scriptable`, then tween the camera:
  `TweenService:Create(camera, TweenInfo.new(2, Enum.EasingStyle.Sine), { CFrame = shot, Focus = shot }):Play()`.
- Sprint or zoom effects: tween `FieldOfView` (for example 70 to 80) and back.

## Camera shake

Add the shake after the camera updates, and remove it before the next update. Otherwise the default camera reads the
shaken CFrame as its new direction and the view drifts.

```luau
local RunService = game:GetService("RunService")

local trauma = 0 -- 0 to 1; add on hits and explosions, decays over time
local shake = CFrame.identity
local MAX_ANGLE = math.rad(3)

local function addShake(amount: number)
	trauma = math.min(trauma + amount, 1)
end

RunService:BindToRenderStep("ShakeUndo", Enum.RenderPriority.Camera.Value - 1, function()
	workspace.CurrentCamera.CFrame *= shake:Inverse()
	shake = CFrame.identity
end)

RunService:BindToRenderStep("ShakeApply", Enum.RenderPriority.Camera.Value + 1, function(dt)
	trauma = math.max(trauma - dt * 1.5, 0)
	if trauma == 0 then
		return
	end
	local strength = trauma * trauma * MAX_ANGLE
	local t = os.clock() * 20
	shake = CFrame.Angles(
		math.noise(t, 0.5) * strength,
		math.noise(0.5, t) * strength,
		math.noise(t, t + 0.5) * strength
	)
	workspace.CurrentCamera.CFrame *= shake
end)
```

Give players a setting to reduce or turn off shake.

## ViewportFrames

A `ViewportFrame` renders 3D objects inside UI: inventory previews, character portraits, minimaps.

```luau
local viewport = Instance.new("ViewportFrame")
viewport.Size = UDim2.fromOffset(160, 160)
viewport.BackgroundTransparency = 1
viewport.Ambient = Color3.fromRGB(200, 200, 200)    -- default
viewport.LightColor = Color3.fromRGB(140, 140, 140) -- default
viewport.LightDirection = Vector3.new(-1, -1, -1)   -- default
viewport.Parent = slot

local viewCamera = Instance.new("Camera")
viewCamera.FieldOfView = 40
viewCamera.Parent = viewport
viewport.CurrentCamera = viewCamera

local model = itemModel:Clone()
model:PivotTo(CFrame.identity)
model.Parent = viewport

local boxCFrame, boxSize = model:GetBoundingBox()
viewCamera.CFrame = CFrame.lookAt(boxCFrame.Position + Vector3.new(1, 0.5, 1) * boxSize.Magnitude, boxCFrame.Position)
viewCamera:ZoomToExtents(boxCFrame, boxSize) -- fit the model in view
```

- Move the **camera** to change the view (orbit it for a spinning preview); don't move the objects.
- **WorldModel:** parts directly in a ViewportFrame are only drawn. Put them in a `WorldModel` inside the
  ViewportFrame to animate them and raycast against them (they still are not physically simulated). Humanoid rigs
  inside a WorldModel get working joints, so you can load an animation on the rig's `Animator` and play it.
- Lighting is only `Ambient`, `LightColor` and `LightDirection`. Tint and fade the whole render with `ImageColor3`
  and `ImageTransparency`. A `Sky` child gives reflections (only its six `Skybox` faces are used).
- No shadows or post-processing, Neon and Glass render at lowest quality, and GuiObjects inside are not supported.
- `CurrentCamera` does not replicate, but the camera's CFrame and FieldOfView are saved with the ViewportFrame.
- Each ViewportFrame costs rendering time. Create them when shown and destroy WorldModels you no longer show.

## Mistakes to avoid

- Changing the camera from a server Script. It has no effect on players.
- Setting `CFrame` while `CameraType` is `Custom`: the default camera overwrites it next frame.
- Passing `Enum.RenderPriority.Camera` instead of `.Value` to `BindToRenderStep`, or using `RenderStepped` for camera
  code with no defined order.
- Forgetting `Focus` on a Scriptable camera, which leaves lighting detail stuck where the camera used to be.
- Shake that never undoes itself, so the view drifts.
- Mixing coordinate systems: `GetMouseLocation()` goes with `ViewportPointToRay`; `InputObject.Position` and
  `AbsolutePosition` go with `ScreenPointToRay` and `WorldToScreenPoint`.
- Cloning a player's character for a portrait without setting `Archivable = true` first (characters are not
  archivable, so `Clone()` returns nil).
- Never unbinding render steps when the custom camera ends.
