3d camera fps
There is no browser build of this example yet.
Is it the same as C?
- Desktop:verified The Zig version renders a final frame that is byte-identical to the C example.
- Browser:not checked yet The browser comparison does not cover this example yet.
//! raylib [core] example - 3d camera fps
//!
//! Zig-API translation of examples/core/core_3d_camera_fps.c (raylib 6.2).
//! Example contributed by Agnis Aldiņš (@nezvers) and reviewed by Ramon Santamaria (@raysan5)
//! Example licensed under an unmodified zlib/libpng license.
//! Copyright (c) 2025 Agnis Aldiņš (@nezvers)
const std = @import("std");
const rl = @import("raylib");
const rm = rl.math;
const pi: f32 = std.math.pi;
// Movement constants
const gravity: f32 = 32.0;
const max_speed: f32 = 20.0;
const crouch_speed: f32 = 5.0;
const jump_force: f32 = 12.0;
const max_accel: f32 = 150.0;
// Grounded drag
const friction: f32 = 0.86;
// Increasing air drag, increases strafing speed
const air_drag: f32 = 0.98;
// Responsiveness for turning movement direction to looked direction
const control: f32 = 15.0;
const crouch_height: f32 = 0.0;
const stand_height: f32 = 1.0;
const bottom_height: f32 = 0.5;
// Slow down diagonal movement
const normalize_input = true;
/// Body structure
const Body = struct {
position: rl.Vector3 = .{ .x = 0, .y = 0, .z = 0 },
velocity: rl.Vector3 = .{ .x = 0, .y = 0, .z = 0 },
dir: rl.Vector3 = .{ .x = 0, .y = 0, .z = 0 },
is_grounded: bool = false,
};
const sensitivity: rl.Vector2 = .{ .x = 0.001, .y = 0.001 };
var player: Body = .{};
var look_rotation: rl.Vector2 = .{ .x = 0, .y = 0 };
var head_timer: f32 = 0.0;
var walk_lerp: f32 = 0.0;
var head_lerp: f32 = stand_height;
var lean: rl.Vector2 = .{ .x = 0, .y = 0 };
pub fn main() void {
// Initialization
const screen_width = 800;
const screen_height = 450;
rl.initWindow(screen_width, screen_height, "raylib [core] example - 3d camera fps");
defer rl.closeWindow(); // Close window and OpenGL context
// Initialize camera variables
// NOTE: updateCameraFps() takes care of the rest
var camera: rl.Camera = .{
.position = .{
.x = player.position.x,
.y = player.position.y + (bottom_height + head_lerp),
.z = player.position.z,
},
.target = .{ .x = 0, .y = 0, .z = 0 },
.up = .{ .x = 0, .y = 0, .z = 0 },
.fovy = 60.0,
.projection = .perspective,
};
updateCameraFps(&camera); // Update camera parameters
rl.disableCursor(); // Limit cursor to relative movement inside the window
rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second
// Main game loop
while (!rl.windowShouldClose()) { // Detect window close button or ESC key
// Update
const mouse_delta = rl.getMouseDelta();
look_rotation.x -= mouse_delta.x * sensitivity.x;
look_rotation.y += mouse_delta.y * sensitivity.y;
const sideway = axis(.d, .a);
const forward = axis(.w, .s);
const crouching = rl.isKeyDown(.left_control);
updateBody(&player, look_rotation.x, sideway, forward, rl.isKeyPressed(.space), crouching);
const delta = rl.getFrameTime();
head_lerp = rm.lerp(head_lerp, if (crouching) crouch_height else stand_height, 20.0 * delta);
camera.position = .{
.x = player.position.x,
.y = player.position.y + (bottom_height + head_lerp),
.z = player.position.z,
};
if (player.is_grounded and (forward != 0 or sideway != 0)) {
head_timer += delta * 3.0;
walk_lerp = rm.lerp(walk_lerp, 1.0, 10.0 * delta);
camera.fovy = rm.lerp(camera.fovy, 55.0, 5.0 * delta);
} else {
walk_lerp = rm.lerp(walk_lerp, 0.0, 10.0 * delta);
camera.fovy = rm.lerp(camera.fovy, 60.0, 5.0 * delta);
}
lean.x = rm.lerp(lean.x, @as(f32, @floatFromInt(sideway)) * 0.02, 10.0 * delta);
lean.y = rm.lerp(lean.y, @as(f32, @floatFromInt(forward)) * 0.015, 10.0 * delta);
updateCameraFps(&camera);
// Draw
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(.ray_white);
{
rl.beginMode3D(camera);
defer rl.endMode3D();
drawLevel();
}
// Draw info box
rl.drawRectangle(5, 5, 330, 75, rl.fade(.sky_blue, 0.5));
rl.drawRectangleLines(5, 5, 330, 75, .blue);
rl.drawText("Camera controls:", 15, 15, 10, .black);
rl.drawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 15, 30, 10, .black);
rl.drawText("- Look around: arrow keys or mouse", 15, 45, 10, .black);
const velocity_length = rm.vector2Length(.{ .x = player.velocity.x, .y = player.velocity.z });
rl.drawText(rl.textFormat("- Velocity Len: ({d:0>6.3})", .{velocity_length}), 15, 60, 10, .black);
}
}
/// -1, 0 or 1 from a pair of opposing keys
fn axis(positive: rl.KeyboardKey, negative: rl.KeyboardKey) i8 {
return @as(i8, @intFromBool(rl.isKeyDown(positive))) - @intFromBool(rl.isKeyDown(negative));
}
/// Update body considering current world state
fn updateBody(body: *Body, rot: f32, side: i8, forward: i8, jump_pressed: bool, crouch_hold: bool) void {
var input: rl.Vector2 = .{ .x = @floatFromInt(side), .y = @floatFromInt(-forward) };
if (normalize_input and side != 0 and forward != 0) input = rm.vector2Normalize(input);
const delta = rl.getFrameTime();
if (!body.is_grounded) body.velocity.y -= gravity * delta;
if (body.is_grounded and jump_pressed) {
body.velocity.y = jump_force;
body.is_grounded = false;
// Sound can be played at this moment
//rl.setSoundPitch(fx_jump, 1.0 + (rl.getRandomValue(-100, 100)*0.001));
//rl.playSound(fx_jump);
}
const front: rl.Vector3 = .{ .x = @sin(rot), .y = 0.0, .z = @cos(rot) };
const right: rl.Vector3 = .{ .x = @cos(-rot), .y = 0.0, .z = @sin(-rot) };
const desired_dir: rl.Vector3 = .{
.x = input.x * right.x + input.y * front.x,
.y = 0.0,
.z = input.x * right.z + input.y * front.z,
};
body.dir = rm.vector3Lerp(body.dir, desired_dir, control * delta);
const decel = if (body.is_grounded) friction else air_drag;
var hvel: rl.Vector3 = .{ .x = body.velocity.x * decel, .y = 0.0, .z = body.velocity.z * decel };
const hvel_length = rm.vector3Length(hvel); // Magnitude
if (hvel_length < max_speed * 0.01) hvel = .{ .x = 0, .y = 0, .z = 0 };
// This is what creates strafing
const speed = rm.vector3DotProduct(hvel, body.dir);
// Whenever the amount of acceleration to add is clamped by the maximum acceleration constant,
// a Player can make the speed faster by bringing the direction closer to horizontal velocity angle
// More info here: https://youtu.be/v3zT3Z5apaM?t=165
const top_speed = if (crouch_hold) crouch_speed else max_speed;
const accel = rm.clamp(top_speed - speed, 0.0, max_accel * delta);
hvel.x += body.dir.x * accel;
hvel.z += body.dir.z * accel;
body.velocity.x = hvel.x;
body.velocity.z = hvel.z;
body.position.x += body.velocity.x * delta;
body.position.y += body.velocity.y * delta;
body.position.z += body.velocity.z * delta;
// Fancy collision system against the floor
if (body.position.y <= 0.0) {
body.position.y = 0.0;
body.velocity.y = 0.0;
body.is_grounded = true; // Enable jumping
}
}
/// Update camera for FPS behaviour
fn updateCameraFps(camera: *rl.Camera) void {
const up: rl.Vector3 = .{ .x = 0.0, .y = 1.0, .z = 0.0 };
const target_offset: rl.Vector3 = .{ .x = 0.0, .y = 0.0, .z = -1.0 };
// Left and right
const yaw = rm.vector3RotateByAxisAngle(target_offset, up, look_rotation.x);
// Clamp view up
var max_angle_up = rm.vector3Angle(up, yaw);
max_angle_up -= 0.001; // Avoid numerical errors
if (-look_rotation.y > max_angle_up) look_rotation.y = -max_angle_up;
// Clamp view down
var max_angle_down = rm.vector3Angle(rm.vector3Negate(up), yaw);
max_angle_down *= -1.0; // Downwards angle is negative
max_angle_down += 0.001; // Avoid numerical errors
if (-look_rotation.y < max_angle_down) look_rotation.y = -max_angle_down;
// Up and down
const right = rm.vector3Normalize(rm.vector3CrossProduct(yaw, up));
// Rotate view vector around right axis
var pitch_angle = -look_rotation.y - lean.y;
pitch_angle = rm.clamp(pitch_angle, -pi / 2 + 0.0001, pi / 2 - 0.0001); // Clamp angle so it doesn't go past straight up or straight down
const pitch = rm.vector3RotateByAxisAngle(yaw, right, pitch_angle);
// Head animation
// Rotate up direction around forward axis
const head_sin = @sin(head_timer * pi);
const head_cos = @cos(head_timer * pi);
const step_rotation = 0.01;
camera.up = rm.vector3RotateByAxisAngle(up, pitch, head_sin * step_rotation + lean.x);
// Camera BOB
const bob_side = 0.1;
const bob_up = 0.15;
var bobbing = rm.vector3Scale(right, head_sin * bob_side);
bobbing.y = @abs(head_cos * bob_up);
camera.position = rm.vector3Add(camera.position, rm.vector3Scale(bobbing, walk_lerp));
camera.target = rm.vector3Add(camera.position, pitch);
}
/// Draw game level
fn drawLevel() void {
const floor_extent = 25;
const tile_size: f32 = 5.0;
const tile_color1: rl.Color = .{ .r = 150, .g = 200, .b = 200, .a = 255 };
// Floor tiles
var y: i32 = -floor_extent;
while (y < floor_extent) : (y += 1) {
var x: i32 = -floor_extent;
while (x < floor_extent) : (x += 1) {
const position: rl.Vector3 = .{ .x = @as(f32, @floatFromInt(x)) * tile_size, .y = 0.0, .z = @as(f32, @floatFromInt(y)) * tile_size };
if (y & 1 != 0 and x & 1 != 0) {
rl.drawPlane(position, .{ .x = tile_size, .y = tile_size }, tile_color1);
} else if (y & 1 == 0 and x & 1 == 0) {
rl.drawPlane(position, .{ .x = tile_size, .y = tile_size }, .light_gray);
}
}
}
const tower_size: rl.Vector3 = .{ .x = 16.0, .y = 32.0, .z = 16.0 };
const tower_color: rl.Color = .{ .r = 150, .g = 200, .b = 200, .a = 255 };
// One tower in each quadrant
var tower_pos: rl.Vector3 = .{ .x = 16.0, .y = 16.0, .z = 16.0 };
for ([_]enum { none, x, z }{ .none, .x, .z, .x }) |flip| {
switch (flip) {
.none => {},
.x => tower_pos.x *= -1,
.z => tower_pos.z *= -1,
}
rl.drawCubeV(tower_pos, tower_size, tower_color);
rl.drawCubeWiresV(tower_pos, tower_size, .dark_blue);
}
// Red sun
rl.drawSphere(.{ .x = 300.0, .y = 300.0, .z = 0.0 }, 100.0, .{ .r = 255, .g = 0, .b = 0, .a = 255 });
}/*******************************************************************************************
*
* raylib [core] example - 3d camera fps
*
* Example complexity rating: [★★★☆] 3/4
*
* Example originally created with raylib 5.5, last time updated with raylib 5.5
*
* Example contributed by Agnis Aldiņš (@nezvers) and reviewed by Ramon Santamaria (@raysan5)
*
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
* BSD-like license that allows static linking with closed source software
*
* Copyright (c) 2025 Agnis Aldiņš (@nezvers)
*
********************************************************************************************/
#include "raylib.h"
#include "raymath.h"
//----------------------------------------------------------------------------------
// Defines and Macros
//----------------------------------------------------------------------------------
// Movement constants
#define GRAVITY 32.0f
#define MAX_SPEED 20.0f
#define CROUCH_SPEED 5.0f
#define JUMP_FORCE 12.0f
#define MAX_ACCEL 150.0f
// Grounded drag
#define FRICTION 0.86f
// Increasing air drag, increases strafing speed
#define AIR_DRAG 0.98f
// Responsiveness for turning movement direction to looked direction
#define CONTROL 15.0f
#define CROUCH_HEIGHT 0.0f
#define STAND_HEIGHT 1.0f
#define BOTTOM_HEIGHT 0.5f
#define NORMALIZE_INPUT 0
//----------------------------------------------------------------------------------
// Types and Structures Definition
//----------------------------------------------------------------------------------
// Body structure
typedef struct {
Vector3 position;
Vector3 velocity;
Vector3 dir;
bool isGrounded;
} Body;
//----------------------------------------------------------------------------------
// Global Variables Definition
//----------------------------------------------------------------------------------
static Vector2 sensitivity = { 0.001f, 0.001f };
static Body player = { 0 };
static Vector2 lookRotation = { 0 };
static float headTimer = 0.0f;
static float walkLerp = 0.0f;
static float headLerp = STAND_HEIGHT;
static Vector2 lean = { 0 };
//----------------------------------------------------------------------------------
// Module Functions Declaration
//----------------------------------------------------------------------------------
static void DrawLevel(void);
static void UpdateCameraFPS(Camera *camera);
static void UpdateBody(Body *body, float rot, char side, char forward, bool jumpPressed, bool crouchHold);
//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
// Initialization
//--------------------------------------------------------------------------------------
const int screenWidth = 800;
const int screenHeight = 450;
InitWindow(screenWidth, screenHeight, "raylib [core] example - 3d camera fps");
// Initialize camera variables
// NOTE: UpdateCameraFPS() takes care of the rest
Camera camera = { 0 };
camera.fovy = 60.0f;
camera.projection = CAMERA_PERSPECTIVE;
camera.position = (Vector3){
player.position.x,
player.position.y + (BOTTOM_HEIGHT + headLerp),
player.position.z,
};
UpdateCameraFPS(&camera); // Update camera parameters
DisableCursor(); // Limit cursor to relative movement inside the window
SetTargetFPS(60); // Set our game to run at 60 frames-per-second
//--------------------------------------------------------------------------------------
// Main game loop
while (!WindowShouldClose()) // Detect window close button or ESC key
{
// Update
//----------------------------------------------------------------------------------
Vector2 mouseDelta = GetMouseDelta();
lookRotation.x -= mouseDelta.x*sensitivity.x;
lookRotation.y += mouseDelta.y*sensitivity.y;
char sideway = (IsKeyDown(KEY_D) - IsKeyDown(KEY_A));
char forward = (IsKeyDown(KEY_W) - IsKeyDown(KEY_S));
bool crouching = IsKeyDown(KEY_LEFT_CONTROL);
UpdateBody(&player, lookRotation.x, sideway, forward, IsKeyPressed(KEY_SPACE), crouching);
float delta = GetFrameTime();
headLerp = Lerp(headLerp, (crouching ? CROUCH_HEIGHT : STAND_HEIGHT), 20.0f*delta);
camera.position = (Vector3){
player.position.x,
player.position.y + (BOTTOM_HEIGHT + headLerp),
player.position.z,
};
if (player.isGrounded && ((forward != 0) || (sideway != 0)))
{
headTimer += delta*3.0f;
walkLerp = Lerp(walkLerp, 1.0f, 10.0f*delta);
camera.fovy = Lerp(camera.fovy, 55.0f, 5.0f*delta);
}
else
{
walkLerp = Lerp(walkLerp, 0.0f, 10.0f*delta);
camera.fovy = Lerp(camera.fovy, 60.0f, 5.0f*delta);
}
lean.x = Lerp(lean.x, sideway*0.02f, 10.0f*delta);
lean.y = Lerp(lean.y, forward*0.015f, 10.0f*delta);
UpdateCameraFPS(&camera);
//----------------------------------------------------------------------------------
// Draw
//----------------------------------------------------------------------------------
BeginDrawing();
ClearBackground(RAYWHITE);
BeginMode3D(camera);
DrawLevel();
EndMode3D();
// Draw info box
DrawRectangle(5, 5, 330, 75, Fade(SKYBLUE, 0.5f));
DrawRectangleLines(5, 5, 330, 75, BLUE);
DrawText("Camera controls:", 15, 15, 10, BLACK);
DrawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 15, 30, 10, BLACK);
DrawText("- Look around: arrow keys or mouse", 15, 45, 10, BLACK);
DrawText(TextFormat("- Velocity Len: (%06.3f)", Vector2Length((Vector2){ player.velocity.x, player.velocity.z })), 15, 60, 10, BLACK);
EndDrawing();
//----------------------------------------------------------------------------------
}
// De-Initialization
//--------------------------------------------------------------------------------------
CloseWindow(); // Close window and OpenGL context
//--------------------------------------------------------------------------------------
return 0;
}
//----------------------------------------------------------------------------------
// Module Functions Definition
//----------------------------------------------------------------------------------
// Update body considering current world state
void UpdateBody(Body *body, float rot, char side, char forward, bool jumpPressed, bool crouchHold)
{
Vector2 input = (Vector2){ (float)side, (float)-forward };
#if defined(NORMALIZE_INPUT)
// Slow down diagonal movement
if ((side != 0) && (forward != 0)) input = Vector2Normalize(input);
#endif
float delta = GetFrameTime();
if (!body->isGrounded) body->velocity.y -= GRAVITY*delta;
if (body->isGrounded && jumpPressed)
{
body->velocity.y = JUMP_FORCE;
body->isGrounded = false;
// Sound can be played at this moment
//SetSoundPitch(fxJump, 1.0f + (GetRandomValue(-100, 100)*0.001));
//PlaySound(fxJump);
}
Vector3 front = (Vector3){ sinf(rot), 0.f, cosf(rot) };
Vector3 right = (Vector3){ cosf(-rot), 0.f, sinf(-rot) };
Vector3 desiredDir = (Vector3){ input.x*right.x + input.y*front.x, 0.0f, input.x*right.z + input.y*front.z, };
body->dir = Vector3Lerp(body->dir, desiredDir, CONTROL*delta);
float decel = (body->isGrounded ? FRICTION : AIR_DRAG);
Vector3 hvel = (Vector3){ body->velocity.x*decel, 0.0f, body->velocity.z*decel };
float hvelLength = Vector3Length(hvel); // Magnitude
if (hvelLength < (MAX_SPEED*0.01f)) hvel = (Vector3){ 0 };
// This is what creates strafing
float speed = Vector3DotProduct(hvel, body->dir);
// Whenever the amount of acceleration to add is clamped by the maximum acceleration constant,
// a Player can make the speed faster by bringing the direction closer to horizontal velocity angle
// More info here: https://youtu.be/v3zT3Z5apaM?t=165
float maxSpeed = (crouchHold? CROUCH_SPEED : MAX_SPEED);
float accel = Clamp(maxSpeed - speed, 0.f, MAX_ACCEL*delta);
hvel.x += body->dir.x*accel;
hvel.z += body->dir.z*accel;
body->velocity.x = hvel.x;
body->velocity.z = hvel.z;
body->position.x += body->velocity.x*delta;
body->position.y += body->velocity.y*delta;
body->position.z += body->velocity.z*delta;
// Fancy collision system against the floor
if (body->position.y <= 0.0f)
{
body->position.y = 0.0f;
body->velocity.y = 0.0f;
body->isGrounded = true; // Enable jumping
}
}
// Update camera for FPS behaviour
static void UpdateCameraFPS(Camera *camera)
{
const Vector3 up = (Vector3){ 0.0f, 1.0f, 0.0f };
const Vector3 targetOffset = (Vector3){ 0.0f, 0.0f, -1.0f };
// Left and right
Vector3 yaw = Vector3RotateByAxisAngle(targetOffset, up, lookRotation.x);
// Clamp view up
float maxAngleUp = Vector3Angle(up, yaw);
maxAngleUp -= 0.001f; // Avoid numerical errors
if ( -(lookRotation.y) > maxAngleUp) { lookRotation.y = -maxAngleUp; }
// Clamp view down
float maxAngleDown = Vector3Angle(Vector3Negate(up), yaw);
maxAngleDown *= -1.0f; // Downwards angle is negative
maxAngleDown += 0.001f; // Avoid numerical errors
if ( -(lookRotation.y) < maxAngleDown) { lookRotation.y = -maxAngleDown; }
// Up and down
Vector3 right = Vector3Normalize(Vector3CrossProduct(yaw, up));
// Rotate view vector around right axis
float pitchAngle = -lookRotation.y - lean.y;
pitchAngle = Clamp(pitchAngle, -PI/2 + 0.0001f, PI/2 - 0.0001f); // Clamp angle so it doesn't go past straight up or straight down
Vector3 pitch = Vector3RotateByAxisAngle(yaw, right, pitchAngle);
// Head animation
// Rotate up direction around forward axis
float headSin = sinf(headTimer*PI);
float headCos = cosf(headTimer*PI);
const float stepRotation = 0.01f;
camera->up = Vector3RotateByAxisAngle(up, pitch, headSin*stepRotation + lean.x);
// Camera BOB
const float bobSide = 0.1f;
const float bobUp = 0.15f;
Vector3 bobbing = Vector3Scale(right, headSin*bobSide);
bobbing.y = fabsf(headCos*bobUp);
camera->position = Vector3Add(camera->position, Vector3Scale(bobbing, walkLerp));
camera->target = Vector3Add(camera->position, pitch);
}
// Draw game level
static void DrawLevel(void)
{
const int floorExtent = 25;
const float tileSize = 5.0f;
const Color tileColor1 = (Color){ 150, 200, 200, 255 };
// Floor tiles
for (int y = -floorExtent; y < floorExtent; y++)
{
for (int x = -floorExtent; x < floorExtent; x++)
{
if ((y & 1) && (x & 1))
{
DrawPlane((Vector3){ x*tileSize, 0.0f, y*tileSize}, (Vector2){ tileSize, tileSize }, tileColor1);
}
else if (!(y & 1) && !(x & 1))
{
DrawPlane((Vector3){ x*tileSize, 0.0f, y*tileSize}, (Vector2){ tileSize, tileSize }, LIGHTGRAY);
}
}
}
const Vector3 towerSize = (Vector3){ 16.0f, 32.0f, 16.0f };
const Color towerColor = (Color){ 150, 200, 200, 255 };
Vector3 towerPos = (Vector3){ 16.0f, 16.0f, 16.0f };
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
towerPos.x *= -1;
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
towerPos.z *= -1;
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
towerPos.x *= -1;
DrawCubeV(towerPos, towerSize, towerColor);
DrawCubeWiresV(towerPos, towerSize, DARKBLUE);
// Red sun
DrawSphere((Vector3){ 300.0f, 300.0f, 0.0f }, 100.0f, (Color){ 255, 0, 0, 255 });
}From raylib 6.2. The original on GitHub.