Explore a scene with a first-person camera
Goal: walk on a large ground with the arrow keys (or WASD/ZQSD) and look around with the mouse, as in a game, instead of rotating around the origin.
Starting point: the default project template (
src/scene.hpp,src/scene.cpp, see Your first scene). It already declares the shapessphereandcylinder, atimer, and the memberscamera_control,environmentandgui: reuse them, do not declare them again. The first comment of each code block says in which file and where its lines go: add means insert them, replace means replace the whole function by the one shown.
Choose a camera controller
The camera of the default scene is driven by a controller: an object that reads the
mouse and keyboard and updates a camera model. Changing the behavior of the camera only
means changing the type of camera_control:
| Controller | Behavior |
|---|---|
camera_controller_orbit_euler |
default: rotates around a target point, keeps the up axis |
camera_controller_2d_displacement |
walks on a plane: arrows/WASD move, left drag looks around |
camera_controller_fly_mode |
plane-like flight: always moves forward, arrows pitch and roll |
camera_controller_first_person |
free rotation around the camera position, no fixed up axis |
All controllers share the same functions (initialize, action_mouse_move,
action_keyboard, idle_frame, camera()), so the rest of the scene does not change.
1. Change the type (scene.hpp)
In scene_structure, change the type of the existing camera_control member (do not add a
second one), and add a ground to walk on:
// scene.hpp - replace the line `camera_controller_orbit_euler camera_control;` camera_controller_2d_displacement camera_control;
// scene.hpp - add in scene_structure mesh_drawable ground;
2. Set the up axis and the initial view (once)
At the beginning of initialize(), the template already calls
camera_control.initialize(inputs, window) and set_rotation_axis_z() (z is the up
direction: the camera walks in the xy plane). Replace its look_at call by:
// scene.cpp - replace the camera_control.look_at call in initialize() camera_control.look_at({ 0.0f, -5.0f, 1.0f } /*eye*/, { 0,0,1.0f } /*target*/);
Then create the ground, under the shapes of the template:
// scene.cpp - add at the end of initialize() ground.initialize_data_on_gpu(mesh_primitive_quadrangle({-20,-20,-0.5f}, {20,-20,-0.5f}, {20,20,-0.5f}, {-20,20,-0.5f})); ground.material.color = {0.6f, 0.85f, 0.5f};
// scene.cpp - add in display_frame(), before timer.update() draw(ground, environment);
The eye is at z = 1, that is 1.5 above the ground: the displacements keep this height
constant. The function display_info of the template prints the controls of the current
controller (camera_control.doc_usage()) in the terminal.
3. Forward the events (every frame)
The template already calls the controller in each callback. Only remove the shift test
in mouse_move_event: with this controller, Shift is the "run" key.
// scene.cpp - replace mouse_move_event() void scene_structure::mouse_move_event() { camera_control.action_mouse_move(); // left drag: look around, right drag: move forward }
The other two callbacks stay as they are:
// scene.cpp - already in the template camera_control.action_keyboard(); // in keyboard_event(): Shift+C captures the mouse cursor, Esc releases it camera_control.idle_frame(); // in idle_frame(): arrows / WASD move, at every frame
display_frame() does not change: camera_control.camera().matrix_view() returns the view
matrix of any controller.
Common pitfalls
- The keys do nothing:
idle_frame()does not callcamera_control.idle_frame(). The displacement is applied there, at every frame, as long as a key is pressed (not inkeyboard_event, which is only called when a key changes state). - The camera walks on a wall or the horizon is tilted: the up axis does not match the
scene. Call
set_rotation_axis_z()for a z-up scene (the CGP default),set_rotation_axis_y()for a y-up scene (many OBJ files), and call it beforelook_at. - The mouse cursor disappears and the camera turns by itself: Shift+C captured the cursor (infinite mouse tracking for game-like control). Press Esc or Shift+C to release it.
- Compile error on
camera_control.look_atwith two arguments: the fly and free controllers (camera_controller_fly_mode,camera_controller_first_person) also need the up vector,look_at(eye, target, up); the walking one accepts and ignores it.
Variations
- Fly over a terrain: declare
camera_controller_fly_mode camera_control;and initialize it withcamera_control.look_at({0,-5,2}, {0,0,0}, {0,0,1});. Tune its speed withcamera_control.speed(keys R/F or +/- change it at run time, up tospeed_max). - Follow the ground of a terrain: after
camera_control.idle_frame(), set the height of the camera,camera_control.camera_model.position_camera.z = terrain_height(x, y) + 1.7f;where terrain_height is your own function. - Render the same frame from a second, fixed camera: declare a second controller, and before
drawing the shapes it should see, set
environment.camera_viewtocamera2_control.camera().matrix_view()(both cameras draw into the same image).
Source: EPITA Ani3D repository epita_ani3d_lab_code-2027, examples
07_camera/01_camera_fly_mode, 07_camera/02_camera_2d_displacement and
07_camera/03_multiple_cameras (scene.hpp and scene.cpp).