Skip to content

Guides Flying Camera

Devin edited this page Apr 27, 2026 · 1 revision

Guides · Flying Camera

What you'll build

A free-look camera driven by mouse + WASD. Yaw / pitch in radians, a Vec3 position, a per-frame LookAt into your view matrix.

Prerequisites

The why

LookAt(eye, target, up) is enough math for any camera. The interactivity is just choosing how eye and target move. For a flycam:

  • yaw rotates around world up (Y) → moves the look-at horizontally.
  • pitch rotates around the camera's right axis → moves the look-at vertically.
  • eye + forward * dt * speed translates along the look direction.

Right-handed: VCK's LookAt writes -f into the third row, so the camera looks down -Z (matches Perspective which negates z into w). A target at world (0, 0, -1) lands at view-space z = -1 — already asserted in the test harness.

Code

#include "VCK.h"
using namespace VCK;

struct FlyCam {
    Vec3  position = {0, 1, 3};
    float yaw      = 0.0f;       // radians, around world +Y; 0 looks down -Z
    float pitch    = 0.0f;       // radians; clamp to (-pi/2, +pi/2)
    float speed    = 4.0f;       // metres / sec
    float sens     = 0.0025f;    // radians / pixel
};

FlyCam cam;

Vec3 Forward(const FlyCam& c)
{
    const float cp = std::cos(c.pitch);
    return { std::sin(c.yaw) * cp,
             std::sin(c.pitch),
            -std::cos(c.yaw) * cp };
}

Vec3 Right(const FlyCam& c)
{
    return { std::cos(c.yaw), 0.0f, std::sin(c.yaw) };
}

Forward is what LookAt will subtract from eye to find a target one unit ahead. Right is the strafe direction; the world-up cross with Forward gives the same answer modulo sign — pick the one whose sign matches your input bindings.

Mouse input via GLFW

VCK::Window is a thin GLFW wrapper. The raw GLFWwindow* is reachable through whatever escape hatch your build needs (GetFramebufferSizeCallback / SetWindowRefreshCallback are exposed; for cursor + key state you can call GLFW directly using a stored handle). Here's the pattern with a cached GLFWwindow*:

double lastX = 0, lastY = 0;
bool   firstMouse = true;

void OnMouse(GLFWwindow* w, double x, double y)
{
    if (firstMouse) { lastX = x; lastY = y; firstMouse = false; return; }
    const double dx = x - lastX;
    const double dy = y - lastY;
    lastX = x; lastY = y;

    cam.yaw   += static_cast<float>(dx) * cam.sens;
    cam.pitch -= static_cast<float>(dy) * cam.sens;

    const float kPitchLim = 1.55f; // ~89 deg, just below pi/2
    if (cam.pitch >  kPitchLim) cam.pitch =  kPitchLim;
    if (cam.pitch < -kPitchLim) cam.pitch = -kPitchLim;
}

OnMouse runs in GLFW's callback, which fires from window.PollEvents() on the main thread. No threading concerns; just clamp pitch so you can't roll over the pole.

WASD + dt

void Tick(GLFWwindow* w, float dt)
{
    Vec3 fwd   = Forward(cam);
    Vec3 right = Right(cam);

    auto down = [w](int key) { return glfwGetKey(w, key) == GLFW_PRESS; };

    if (down(GLFW_KEY_W)) cam.position = cam.position + fwd   * (cam.speed * dt);
    if (down(GLFW_KEY_S)) cam.position = cam.position - fwd   * (cam.speed * dt);
    if (down(GLFW_KEY_A)) cam.position = cam.position - right * (cam.speed * dt);
    if (down(GLFW_KEY_D)) cam.position = cam.position + right * (cam.speed * dt);
    if (down(GLFW_KEY_SPACE))         cam.position.y += cam.speed * dt;
    if (down(GLFW_KEY_LEFT_CONTROL))  cam.position.y -= cam.speed * dt;
}

dt comes from std::chrono::steady_clock between frames. See EasyCubeExample for the timing skeleton.

View matrix → UBO

void DrawFrame()
{
    if (window.IsMinimized()) return;

    VCK::HandleLiveResize(window, device, swapchain, framebuffers, pipeline);

    Frame& f = scheduler.BeginFrame();

    // ... acquire ...

    Vec3 fwd  = Forward(cam);
    Vec3 eye  = cam.position;
    Vec3 look = eye + fwd;

    const float aspect = static_cast<float>(swapchain.GetExtent().width)
                       / static_cast<float>(swapchain.GetExtent().height);

    CameraUBO ubo{};
    ubo.view = LookAt(eye, look, {0, 1, 0});
    ubo.proj = Perspective(Radians(60.0f), aspect, 0.1f, 100.0f);
    camera.Write(sync.GetCurrentFrameIndex(), ubo);

    // ... bind pipeline, descriptors, push constants, draws, end pass ...

    scheduler.EndFrame();
}

The camera updates every frame on the CPU; the UBO upload is one memcpy per frame slot via VulkanUniformSet<CameraUBO>::Write. No staging needed — UBO buffers are CPU-visible by design.

Lock and hide the cursor

glfwSetInputMode(rawWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glfwSetCursorPosCallback(rawWindow, OnMouse);

GLFW_CURSOR_DISABLED hides the cursor and provides infinite raw motion through glfwGetCursorPos deltas — exactly what flycams want.

What's next

VCK · Vulkan Core Kit

Getting Started

Guides

Reference

More


Single source of truth for the full API surface is the doc block at the top of VCK.h.

Clone this wiki locally