Camera Controls in Raftel Engine

This tutorial walks you through creating and controlling cameras in Raftel Engine, allowing you to navigate and view your 3D scenes from any perspective.

What is a Camera?

In a 3D graphics application, a camera acts as the viewer's eyes into the virtual world. It defines:

Matrix and Transforms

A camera in Raftel Engine uses two primary matrices:

  • View Matrix: Transforms world coordinates into camera space (what the camera sees)
  • Projection Matrix: Transforms camera space into clip space (how perspective is applied)

Creating a Camera

To use a camera in your Raftel Engine application, start by creating a Camera instance:

Creating a Camera
#include "raftel/window.hpp"
#include "raftel/camera.hpp"

int main() {
  auto windowSystemOpt = Raftel::WindowSystem::make();
  auto windowOpt = Raftel::Window::make("Camera Example", *windowSystemOpt);
  
  if (!windowOpt) {
    std::cerr << "Error creating window.\n";
    return -1;
  }
  
  windowOpt->MakeContextCurrent();
  
  // Create a camera instance
  Raftel::Camera camera(windowOpt.get());
  
  // Main application loop
  while (!windowOpt->ShouldClose()) {
    windowOpt->input->updateKeys();
    windowOpt->clear();
    
    // Update the camera (processes movement and updates matrices)
    camera.Update(windowOpt);
    
    // Render your scene...
    
    windowOpt->swapBuffers();
  }
  
  return 0;
}

The Camera constructor requires a pointer to a Raftel::Window object, which it uses to access input controls and screen dimensions. The camera is updated each frame to process any movement or rotation input.

Camera States

Raftel Engine cameras can be in one of two states:

Normal State

In this state, the camera remains static unless explicitly moved through code. This is useful for fixed camera positions or pre-defined camera paths.

Possessed State

In this state, the camera responds to user input for interactive movement. This enables first-person or free-flying camera controls.

You can switch between states using the SetState method:

Switching Camera States
// Set to possessed state (user-controlled)
camera.SetState(Raftel::CameraState::Possessed, windowOpt->window_);

// Set to normal state (static)
camera.SetState(Raftel::CameraState::Normal, windowOpt->window_);

Cursor Visibility

When in Possessed state, the camera typically hides and captures the cursor to enable mouse-look controls. This is automatically managed by the SetState method.

Interactive Camera Controls

Raftel's Camera class includes built-in controls for interactive navigation when in Possessed state. To enable these controls, call the PresetCamera method in your main loop:

Enabling Camera Controls
while (!windowOpt->ShouldClose()) {
  windowOpt->input->updateKeys();
  windowOpt->clear();
  
  // Enable preset camera controls
  camera.PresetCamera(windowOpt.get());
  
  // Update camera matrices
  camera.Update(windowOpt);
  
  // Render scene...
  
  windowOpt->swapBuffers();
}

The default controls when in Possessed state are:

Movement

  • W - Move forward
  • S - Move backward
  • A - Strafe left
  • D - Strafe right
  • Space - Move up
  • Left Ctrl - Move down

Rotation

  • Mouse movement - Look around (yaw and pitch)
  • Q/E - Roll left/right (if enabled)

Speed Control

  • Left Shift - Move faster when held
  • Mouse Scroll - Adjust movement speed

State Toggle

  • Tab - Toggle between Normal and Possessed states
  • Right Mouse Button - Also toggles camera state

Toggling Camera State

You can allow users to toggle between camera states using keyboard keys or mouse buttons:

Toggle Camera State with Key/Button
// Toggle camera state with Tab key
camera.ToggleState(windowOpt.get(), Raftel::Input::Keys::Key_Tab);

// Toggle camera state with right mouse button
camera.ToggleState(windowOpt.get(), Raftel::Input::Buttons::Button_Right);

These methods check if the specified key/button was pressed this frame and toggle the camera state if so.

Adjusting Camera Movement Speed

You can customize how fast the camera moves through the 3D scene:

Camera Speed Control
// Increase speed while Left Shift is held
camera.ChangeSpeedWithKey(windowOpt.get(), Raftel::Input::Keys::Key_LeftShift, 10.0f, 3.0f);

// Adjust speed with mouse scroll wheel
camera.ChangeSpeedWithScroll(windowOpt.get(), 0.1f);

The ChangeSpeedWithKey method takes these parameters:

The ChangeSpeedWithScroll method adjusts speed based on mouse scroll input, with the second parameter controlling sensitivity.

Positioning and Orienting the Camera

You can explicitly set the camera's position and rotation:

Camera Positioning
// Set camera position (x, y, z)
camera.SetPosition(glm::vec3(0.0f, 5.0f, -10.0f));

// Set camera rotation (pitch, yaw, roll) in degrees
camera.SetRotation(glm::vec3(15.0f, 180.0f, 0.0f));

// Move camera relative to its current position
camera.Translate(glm::vec3(0.0f, 0.0f, -1.0f));  // Move forward 1 unit

// Rotate camera around an axis
camera.Rotate(45.0f, glm::vec3(0.0f, 1.0f, 0.0f));  // Rotate 45 degrees around Y axis

Using Camera Matrices in Shaders

The main purpose of a camera is to provide view and projection matrices for rendering. You can apply these matrices to your shaders:

Using Camera Matrices
// Method 1: Get matrices and set them manually
Raftel::ShaderProgram shaderProgram;
shaderProgram.use();

// Set view and projection matrices
glm::mat4 viewMatrix = camera.getViewMatrix();
glm::mat4 projectionMatrix = camera.getProjectionMatrix();

shaderProgram.setMatrix4("view", viewMatrix);
shaderProgram.setMatrix4("projection", projectionMatrix);

// Method 2: Let the camera set uniforms automatically
shaderProgram.use();
camera.setUniforms(shaderProgram);

Shader Uniform Names

When using camera.setUniforms(), your shader must have uniform variables named view and projection to receive the camera matrices.

Complete Example

Here's a complete example that demonstrates creating and using a camera in a 3D scene:

Complete Camera Example
#include "raftel/window.hpp"
#include "raftel/shader.hpp"
#include "raftel/camera.hpp"
#include "raftel/mesh.hpp"
#include "raftel/input.hpp"

int main(void)
{
    // Create window system and window
    auto windowSystemOpt = Raftel::WindowSystem::make();
    auto windowOpt = Raftel::Window::make("Camera Tutorial", *windowSystemOpt);
    if (!windowOpt) {
        std::cerr << "Can't create window\n";
        return -1;
    }

    // Make window context
    windowOpt->MakeContextCurrent();

    // Create camera
    Raftel::Camera camera(windowOpt.get());
    
    // Set initial camera position
    camera.SetPosition(glm::vec3(0.0f, 5.0f, 10.0f));
    camera.SetRotation(glm::vec3(-15.0f, 180.0f, 0.0f));

    // Create a mesh to visualize
    auto cube = Raftel::MeshFactory::createCube(2.0f);
    auto cube_texture = Raftel::Texture::loadTexture("../assets/textures/cubetex.png");
    cube->GetMaterialByIndex(0)->setAlbedo(cube_texture);
    cube->setupMesh();
    
    // Load shaders
    Raftel::ShaderProgram shaderProgram;
    if (!shaderProgram.load("../assets/shaders/basic.vs", "../assets/shaders/basic.fs")) {
        std::cerr << "Error loading shaders\n";
        return -1;
    }

    // Main loop
    while (!windowOpt->ShouldClose())
    {
        // Update input
        windowOpt->input->updateKeys();
        windowOpt->clear();

        // Handle camera state toggle with right mouse button
        camera.ToggleState(windowOpt.get(), Raftel::Input::Buttons::Button_Right);
        
        // Enable sprint with Left Shift
        camera.ChangeSpeedWithKey(windowOpt.get(), Raftel::Input::Keys::Key_LeftShift, 10.0f, 3.0f);
        
        // Allow speed adjustment with mouse wheel
        camera.ChangeSpeedWithScroll(windowOpt.get(), 0.1f);
        
        // Process camera input and update matrices
        camera.PresetCamera(windowOpt.get());
        camera.Update(windowOpt);

        // Render the cube
        shaderProgram.use();
        
        // Set camera uniforms
        camera.setUniforms(shaderProgram);
        
        // Set model matrix (position in world space)
        glm::mat4 model = glm::mat4(1.0f);
        shaderProgram.setMatrix4("model", model);
        
        // Draw the cube
        cube->draw(shaderProgram);

        // Swap buffers
        windowOpt->swapBuffers();
    }

    return 0;
}

Advanced Camera Techniques

Camera Paths

For cinematic sequences, you can animate a camera along a predefined path by updating its position and rotation each frame according to a spline or animation curve.

Follow Camera

To follow a character or object, update the camera position each frame to maintain a relative offset from the target, and orient the camera to look at the target.

Orbit Camera

Create an orbit camera by rotating around a focal point at a fixed distance, useful for object inspection or third-person views.

Multiple Cameras

Create multiple camera instances and switch between them to provide different viewpoints of your scene.

Implementing an Orbit Camera
// Orbit camera implementation example
void updateOrbitCamera(Raftel::Camera& camera, const glm::vec3& targetPosition, float distance, float& yawAngle, float& pitchAngle) {
    // Update angles based on mouse input
    yawAngle += mouseX * sensitivity;
    pitchAngle -= mouseY * sensitivity;
    
    // Clamp pitch to avoid gimbal lock
    pitchAngle = glm::clamp(pitchAngle, -89.0f, 89.0f);
    
    // Calculate camera position based on spherical coordinates
    float x = targetPosition.x + distance * cos(glm::radians(pitchAngle)) * cos(glm::radians(yawAngle));
    float y = targetPosition.y + distance * sin(glm::radians(pitchAngle));
    float z = targetPosition.z + distance * cos(glm::radians(pitchAngle)) * sin(glm::radians(yawAngle));
    
    // Set camera position and look at target
    camera.SetPosition(glm::vec3(x, y, z));
    
    // Calculate the direction vector
    glm::vec3 direction = glm::normalize(targetPosition - camera.getPosition());
    
    // Convert direction to euler angles
    float pitch = glm::degrees(asin(direction.y));
    float yaw = glm::degrees(atan2(direction.z, direction.x));
    
    // Set camera rotation
    camera.SetRotation(glm::vec3(pitch, yaw, 0.0f));
}

Best Practices

Camera Usage Tips

  • Call Update Once Per Frame: Always call camera.Update() once per frame to ensure matrices are correctly calculated.
  • Use Pos/Rot or Transform: Either use SetPosition/SetRotation or use Translate/Rotate for movement, but avoid mixing the two approaches.
  • Input Updates: Ensure windowOpt->input->updateKeys() is called before processing camera input.
  • State Management: Be aware of which state your camera is in when processing input.
  • Shader Compatibility: Ensure your shaders have the appropriate uniform variables for view and projection matrices.

API Reference

Here's a summary of the most important methods in the Camera class:

Camera API Summary
// Constructor
Camera(Raftel::Window* w);

// Position and rotation
glm::vec3 getPosition() const;
glm::vec3 getRotation() const;
void SetPosition(const glm::vec3& position);
void SetRotation(const glm::vec3& rotation);
void Translate(const glm::vec3& translation);
void Rotate(float angle, const glm::vec3& axis);

// Matrices
glm::mat4 getViewMatrix() const;
glm::mat4 getProjectionMatrix() const;
void Update(const std::unique_ptr& window);
void setUniforms(ShaderProgram& prg);

// Camera state
CameraState GetState() const;
void SetState(CameraState newState, GLFWwindow* window);
void ToggleState(Raftel::Window* window, Raftel::Input::Keys key_);
void ToggleState(Raftel::Window* window, Raftel::Input::Buttons button_);

// Movement speed
float getMovementSpeed() const;
void ChangeSpeedWithKey(Raftel::Window* window, Raftel::Input::Keys key_, float fastSpeed, float normalSpeed);
void ChangeSpeedWithScroll(Raftel::Window* window, float speedOffset);

// Input handling
void PossessedInput(Raftel::Window* window);
void PresetCamera(Raftel::Window* window);

For complete details, see the Camera Class Reference in the API documentation.