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.
Both camera types are value types. Pass them by pointer when you need modifications to persist across frames.
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;
target maps to the centre of the screen.
target point, not the screen origin.
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.
// 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;
Clamp cam.target to world bounds before lerping to prevent the camera from showing out-of-bounds areas at the map edges.
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
target - position, normalised internally.
CAMERA_PERSPECTIVE (0) or CAMERA_ORTHOGRAPHIC (1).
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();
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. |
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.
Store yaw, pitch, and radius as floats. Derive position each frame from spherical coordinates. Do not store position as authoritative state — derive it.
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;
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);
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.
raylib exposes four conversion functions that are frequently needed for mouse-picking, UI anchoring, and raycasting:
| Function | Input → 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) |
Ray ray = GetMouseRay(GetMousePosition(), cam); RayCollision hit = GetRayCollisionBox(ray, boundingBox); if (hit.hit) { DrawSphere(hit.point, 0.1f, RED); }
Vector2 worldPos = GetScreenToWorld2D(GetMousePosition(), cam2d); // worldPos is now in the same coordinate space as your tiles, sprites, etc.
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.
#include "rlgl.h" rlSetClipPlanes(0.05f, 5000.0f); BeginMode3D(cam); // draw scene EndMode3D();
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.
// 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();
Offset cam.offset (2D) or cam.position (3D) by a decaying random vector. Never modify cam.target for shake — that shifts the follow point.
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; }
Naively scaling zoom causes the view to drift. The correct approach is to adjust target so the world point under the cursor stays fixed:
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); }
| Function pair | Scope |
|---|---|
| BeginMode2D / EndMode2D | 2D world-space transform |
| BeginMode3D / EndMode3D | 3D perspective/ortho projection |
| BeginTextureMode / EndTextureMode | Redirect output to RenderTexture2D |
| BeginScissorMode / EndScissorMode | Clip draw calls to a screen rectangle |
| BeginShaderMode / EndShaderMode | Apply a custom shader to enclosed draws |
These pairs can be nested. The common order is BeginTextureMode → BeginMode3D → BeginShaderMode → draw → EndShaderMode → EndMode3D → EndTextureMode.