Camera Systems in raylib

01 Overview

raylib provides two distinct camera types: Camera2D for flat/orthographic views and Camera3D (aliased as Camera) for perspective or orthographic 3D scenes. They are plain structs — no hidden state, no inheritance. You own them, you update them.

raylib also ships UpdateCamera() with built-in modes for common behaviours (free-fly, orbital, first-person, third-person). These are suitable for quick prototypes. For any non-trivial game logic, drive the camera manually.

Note

Both camera types are value types. Pass them by pointer when you need modifications to persist across frames.

02 Camera2D

Struct Layout

raylib.hC
typedef struct Camera2D {
    Vector2 offset;    // screen-space offset applied after rotation/zoom
    Vector2 target;    // world-space point the camera looks at
    float   rotation;  // degrees, clockwise
    float   zoom;      // scale factor; 1.0 = pixel-perfect
} Camera2D;
offset Vector2 Screen-space translation applied after the world transform. Use this to place the camera's anchor point (e.g., screen centre) without shifting the target.
target Vector2 World-space position the camera is centred on. At zoom 1.0 with offset = (screenW/2, screenH/2), target maps to the centre of the screen.
rotation float Camera rotation in degrees. The pivot is the target point, not the screen origin.
zoom float Uniform scale. Values below 1.0 zoom out. Do not set to 0.

Minimal Usage

2d_camera.cC
Camera2D cam = { 0 };
cam.target   = (Vector2){ player.x, player.y };
cam.offset   = (Vector2){ screenWidth / 2.0f, screenHeight / 2.0f };
cam.rotation = 0.0f;
cam.zoom     = 1.0f;

// inside draw loop:
BeginMode2D(cam);
    DrawTexture(tilemap, 0, 0, WHITE);
    DrawRectangleRec(playerRect, RED);
EndMode2D();

Everything drawn between BeginMode2D / EndMode2D is in world space. Anything drawn outside (HUD, UI) is in raw screen space.

Smooth Follow

smooth_follow.cC
// Lerp target toward player each frame
float speed = 5.0f * GetFrameTime();
cam.target.x += (player.x - cam.target.x) * speed;
cam.target.y += (player.y - cam.target.y) * speed;
Tip

Clamp cam.target to world bounds before lerping to prevent the camera from showing out-of-bounds areas at the map edges.

03 Camera3D

Struct Layout

raylib.hC
typedef struct Camera3D {
    Vector3    position;   // camera position in world space
    Vector3    target;     // point the camera looks at
    Vector3    up;         // up direction (usually Y+)
    float      fovy;       // vertical field of view (degrees) or height for ortho
    int        projection; // CAMERA_PERSPECTIVE or CAMERA_ORTHOGRAPHIC
} Camera3D;

typedef Camera3D Camera; // convenience alias
position Vector3 Eye position. This is the origin of the view ray.
target Vector3 The point the camera looks toward. The view direction is target - position, normalised internally.
up Vector3 World up vector used to compute the right vector via cross-product. Must not be parallel to the view direction.
fovy float Vertical FOV in degrees for perspective projection. For orthographic, it is the view height in world units.
projection int CAMERA_PERSPECTIVE (0) or CAMERA_ORTHOGRAPHIC (1).

Minimal Usage

3d_camera.cC
Camera3D cam = { 0 };
cam.position   = (Vector3){ 10.0f, 10.0f, 10.0f };
cam.target     = (Vector3){  0.0f,  0.0f,  0.0f };
cam.up         = (Vector3){  0.0f,  1.0f,  0.0f };
cam.fovy       = 45.0f;
cam.projection = CAMERA_PERSPECTIVE;

BeginMode3D(cam);
    DrawGrid(10, 1.0f);
    DrawModel(model, origin, 1.0f, WHITE);
EndMode3D();

04 Camera Modes

UpdateCamera(Camera *camera, int mode) applies a built-in behaviour each frame. It reads mouse delta and keyboard state directly — it is not composable with custom input handling.

Mode constant Value Behaviour
CAMERA_CUSTOM 0 No built-in update. You control the camera entirely.
CAMERA_FREE 1 WASD + mouse look. Unconstrained 6DOF fly-through.
CAMERA_ORBITAL 2 Mouse drag orbits around target. Scroll to zoom.
CAMERA_FIRST_PERSON 3 WASD movement on the XZ plane, mouse look.
CAMERA_THIRD_PERSON 4 Like first-person but position offset behind target.
Warning

UpdateCamera calls GetMouseDelta() and checks multiple key states internally. If you call it alongside your own input logic, inputs will be consumed twice. Use CAMERA_CUSTOM and drive the camera manually in production code.

05 Manual Camera Control

Orbital Camera

Store yaw, pitch, and radius as floats. Derive position each frame from spherical coordinates. Do not store position as authoritative state — derive it.

orbital.cC
float yaw    = 0.0f;
float pitch  = 0.4f;   // radians
float radius = 12.0f;

// per-frame update
Vector2 delta = GetMouseDelta();
if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) {
    yaw   -= delta.x * 0.003f;
    pitch -= delta.y * 0.003f;
    pitch  = Clamp(pitch, -1.5f, 1.5f);
}
radius -= GetMouseWheelMove() * 1.2f;
radius  = Clamp(radius, 2.0f, 80.0f);

cam.position = (Vector3){
    target.x + radius * cosf(pitch) * sinf(yaw),
    target.y + radius * sinf(pitch),
    target.z + radius * cosf(pitch) * cosf(yaw)
};
cam.target = target;

First-Person Look

fps.cC
float yaw   = 0.0f;
float pitch = 0.0f;

// per-frame
Vector2 d = GetMouseDelta();
yaw   -= d.x * 0.002f;
pitch -= d.y * 0.002f;
pitch  = Clamp(pitch, -1.4f, 1.4f);

Vector3 forward = {
    cosf(pitch) * sinf(yaw),
    sinf(pitch),
    cosf(pitch) * cosf(yaw)
};

// WASD movement on the XZ plane
Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, (Vector3){0,1,0}));
if (IsKeyDown(KEY_W)) cam.position = Vector3Add(cam.position, Vector3Scale(forward, spd));
if (IsKeyDown(KEY_S)) cam.position = Vector3Subtract(cam.position, Vector3Scale(forward, spd));
if (IsKeyDown(KEY_A)) cam.position = Vector3Subtract(cam.position, Vector3Scale(right, spd));
if (IsKeyDown(KEY_D)) cam.position = Vector3Add(cam.position, Vector3Scale(right, spd));

cam.target = Vector3Add(cam.position, forward);
Tip

Call DisableCursor() at startup and EnableCursor() when you want to release the mouse. This prevents the cursor from hitting screen edges and clamping delta values.

06 Coordinate Transforms

raylib exposes four conversion functions that are frequently needed for mouse-picking, UI anchoring, and raycasting:

FunctionInput → Output
GetWorldToScreen Vector3 world → Vector2 screen
GetWorldToScreenEx Same, but accepts explicit width/height for render-texture targets
GetScreenToWorld2D Vector2 screen → Vector2 world (Camera2D)
GetMouseRay Vector2 mouse → Ray in world space (Camera3D)

Mouse Picking (3D)

picking.cC
Ray ray = GetMouseRay(GetMousePosition(), cam);
RayCollision hit = GetRayCollisionBox(ray, boundingBox);
if (hit.hit) {
    DrawSphere(hit.point, 0.1f, RED);
}

World Position from 2D Mouse

screen_to_world.cC
Vector2 worldPos = GetScreenToWorld2D(GetMousePosition(), cam2d);
// worldPos is now in the same coordinate space as your tiles, sprites, etc.

07 Frustum & Culling

raylib does not expose a frustum-culling API. The renderer submits every draw call you issue. For large scenes, perform your own sphere-frustum or AABB-frustum tests before calling draw functions.

The near and far clip planes are hardcoded at RL_CULL_DISTANCE_NEAR (0.01) and RL_CULL_DISTANCE_FAR (1000.0) in rlgl.h. To override them, call rlSetClipPlanes(near, far) before BeginMode3D.

clip_planes.cC
#include "rlgl.h"

rlSetClipPlanes(0.05f, 5000.0f);
BeginMode3D(cam);
    // draw scene
EndMode3D();
Warning

A large far/near ratio degrades depth buffer precision. For worlds larger than a few hundred units, consider separating the scene into depth layers and clearing the depth buffer between them.

08 Common Patterns

Split-Screen (multiple cameras)

splitscreen.cC
// Render each camera to its own RenderTexture2D, then blit side-by-side
RenderTexture2D targetA = LoadRenderTexture(screenW / 2, screenH);
RenderTexture2D targetB = LoadRenderTexture(screenW / 2, screenH);

BeginTextureMode(targetA);
    ClearBackground(RAYWHITE);
    BeginMode3D(camA); DrawScene(); EndMode3D();
EndTextureMode();

BeginTextureMode(targetB);
    ClearBackground(RAYWHITE);
    BeginMode3D(camB); DrawScene(); EndMode3D();
EndTextureMode();

BeginDrawing();
    // flip Y because OpenGL UVs are bottom-up
    Rectangle src = { 0, 0, screenW/2.0f, -(float)screenH };
    DrawTextureRec(targetA.texture, src, (Vector2){           0, 0 }, WHITE);
    DrawTextureRec(targetB.texture, src, (Vector2){ screenW/2, 0 }, WHITE);
EndDrawing();

Screenshake

Offset cam.offset (2D) or cam.position (3D) by a decaying random vector. Never modify cam.target for shake — that shifts the follow point.

shake.cC
if (shakeTimer > 0.0f) {
    float mag = shakeTimer * shakeMagnitude;
    cam.offset.x = baseOffset.x + (GetRandomValue(-100, 100) / 100.0f) * mag;
    cam.offset.y = baseOffset.y + (GetRandomValue(-100, 100) / 100.0f) * mag;
    shakeTimer -= GetFrameTime();
} else {
    cam.offset = baseOffset;
}

Zoom to Cursor (2D)

Naively scaling zoom causes the view to drift. The correct approach is to adjust target so the world point under the cursor stays fixed:

zoom_cursor.cC
float wheel = GetMouseWheelMove();
if (wheel != 0) {
    Vector2 mouseWorld = GetScreenToWorld2D(GetMousePosition(), cam);
    cam.offset = GetMousePosition();
    cam.target = mouseWorld;
    cam.zoom  += wheel * 0.1f * cam.zoom;   // proportional zoom
    cam.zoom   = Clamp(cam.zoom, 0.1f, 16.0f);
}

Quick Reference — Render Boundaries

Function pairScope
BeginMode2D / EndMode2D2D world-space transform
BeginMode3D / EndMode3D3D perspective/ortho projection
BeginTextureMode / EndTextureModeRedirect output to RenderTexture2D
BeginScissorMode / EndScissorModeClip draw calls to a screen rectangle
BeginShaderMode / EndShaderModeApply a custom shader to enclosed draws

These pairs can be nested. The common order is BeginTextureMode → BeginMode3D → BeginShaderMode → draw → EndShaderMode → EndMode3D → EndTextureMode.