-
Notifications
You must be signed in to change notification settings - Fork 0
Guides Flying Camera
A free-look camera driven by mouse + WASD. Yaw / pitch in radians, a Vec3 position, a per-frame LookAt into your view matrix.
-
Guides · Shaders — you have a UBO with
view+proj. - Guides · Scene — you're rendering at least one mesh.
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 * speedtranslates 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.
#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.
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.
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.
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.
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.
- Guides · Flat Plane — give yourself ground to fly over
- Cookbook — recipe 17 (frustum culling), recipe 18 (picking)
- Home
- Quick Start
- Your First App
- Understanding cfg
- Build — Windows / Linux / macOS
Single source of truth for the full API surface is the doc block at the top of VCK.h.