Camera System

#include <safi/ecs/components.h>

SafiCamera is an ECS component that describes a viewpoint. Attach it to an entity (alongside SafiTransform) and tag exactly one such entity with SafiActiveCamera — the engine's render system picks that viewpoint up automatically.

Struct

typedef struct SafiCamera {
    float fov_y_radians;
    float z_near;
    float z_far;

    vec3  target;   /* legacy look-at hint; kept for back-compat with
                       scenes / code that write it directly            */
    vec3  eye;      /* world-space camera position                      */
    vec3  forward;  /* unit vector, direction the camera looks          */
    vec3  up;       /* unit vector, view-space up (usually +Y)          */

    mat4  view;     /* cached — reserved for future use                 */
    mat4  proj;
} SafiCamera;

The render system builds view from eye / forward / up every frame. If eye is all-zero (the default for a freshly-created component), it falls back to the legacy eye = target + (0,0,3) convention so code that only sets target keeps working.

Active camera

typedef struct SafiActiveCamera { char _unused; } SafiActiveCamera;  /* tag */

Attach SafiActiveCamera to exactly one entity that also has SafiCamera. The render system queries (SafiCamera, SafiActiveCamera) and uses the first match. Swapping cameras = moving the tag. The editor camera uses this tag to take over the view in Edit mode and restore the gameplay camera on Play.

Creating a camera

ecs_entity_t cam = ecs_new(world);
ecs_set(world, cam, SafiTransform, safi_transform_identity());
ecs_set(world, cam, SafiCamera, {
    .fov_y_radians = glm_rad(60.0f),
    .z_near = 0.1f,
    .z_far  = 100.0f,
    .eye      = { 0, 1.5f,  6.0f },
    .forward  = { 0, 0,    -1.0f },
    .up       = { 0, 1.0f,  0.0f },
});
ecs_add(world, cam, SafiActiveCamera);

Or, for a "look at a point" style:

ecs_set(world, cam, SafiCamera, {
    .fov_y_radians = glm_rad(60.0f),
    .z_near = 0.1f,
    .z_far  = 100.0f,
    .target = { 0, 0, 0 },   /* triggers the legacy look-at-origin fallback */
});

Helpers

safi/render/camera.h exposes two helpers that any system (picking, gizmos, screen-space tools) can share:

void safi_camera_build_view_proj(const SafiCamera *cam,
                                 int screen_w, int screen_h,
                                 mat4 out_view, mat4 out_proj);

void safi_camera_screen_ray     (const SafiCamera *cam,
                                 int screen_w, int screen_h,
                                 float cursor_x, float cursor_y,
                                 vec3 out_origin, vec3 out_dir);

These reuse exactly the pose logic the render system applies, so "what the user sees" and "what a raycast hits" can never drift out of sync.

Serialization

SafiCamera serialises with the full pose (target, eye, forward, up). Scene files written before the pose fields existed still load — the deserializer synthesises eye / forward / up from the old target-only convention. See Scene Serialization.

See also