Skip to content

Tutorial 16 — Camera Rigs and Cinematic Post-Processing

Copyright (c) 2024-2026 Jack Waechter. MIT licensed.

This tutorial covers the rest of AP2_Camera.h beyond the basic orbit sketch in 3D, plus the full effect catalog in AP2_Post.h / AP2_Post_extra.h.

1. First-person camera

Yaw/pitch plus local-space movement is the standard FPS/walkthrough rig:

static AP_Camera cam;
static bool cam_ready = false;
float dt = (float)AP_GetDeltaTime();

if (!cam_ready) {
  cam = AP_CameraPerspective(AP_V3(0, 1.7f, 5.0f), AP_V3(0, 1.7f, 0), 65.0f);
  cam_ready = true;
}

if (AP_IsMouseDown(AP_MOUSE_RIGHT)) {
  float dx = (float)AP_GetMouseDeltaX();
  float dy = (float)AP_GetMouseDeltaY();
  AP_CameraRotateYawPitch(&cam, dx * 0.15f, -dy * 0.15f);
}

float forward = 0.0f, right = 0.0f, up = 0.0f;
if (AP_IsKeyDown(AP_KEY_W)) forward += 1.0f;
if (AP_IsKeyDown(AP_KEY_S)) forward -= 1.0f;
if (AP_IsKeyDown(AP_KEY_D)) right += 1.0f;
if (AP_IsKeyDown(AP_KEY_A)) right -= 1.0f;
if (AP_IsKeyDown(AP_KEY_E)) up += 1.0f;
if (AP_IsKeyDown(AP_KEY_Q)) up -= 1.0f;

AP_CameraMoveLocal(&cam, forward * 5.0f * dt, right * 5.0f * dt, up * 5.0f * dt);

AP_Begin3D(&cam);

AP_CameraRotateYawPitch updates target from the camera's current forward vector, so position and orientation stay independent — AP_CameraMoveLocal then walks position along that same forward/right/up basis.

To read the current facing back out (e.g. to show a compass or serialize a saved viewpoint):

AP_Vec2 yp = AP_CameraYawPitch(&cam); /* x = yaw, y = pitch, degrees */

2. Orbit camera, revisited

The sketch in 3D manually computes eye with trig. AP_CameraSetYawPitch does the same job when you'd rather store yaw/pitch as your source of truth and derive target:

static float orbit_yaw = 0.0f, orbit_pitch = 20.0f, orbit_dist = 6.0f;

orbit_yaw += (float)AP_GetMouseDeltaX() * 0.3f;
orbit_pitch = AP_Clampf(orbit_pitch - (float)AP_GetMouseDeltaY() * 0.3f, -80.0f, 80.0f);

AP_Camera orbit = AP_CameraPerspective(AP_V3(0, 0, orbit_dist), AP_V3(0, 0, 0), 50.0f);
AP_CameraSetYawPitch(&orbit, orbit_yaw, orbit_pitch);
AP_CameraTranslate(&orbit, AP_V3(0, 0, 0)); /* no-op placeholder for clarity */

3. Screen-space picking

Turn a mouse click into a world-space ray to select objects, place markers, or implement drag-and-drop onto a 3D scene:

double mx, my;
AP_GetMousePosition(&mx, &my);
float aspect = (float)AP_GetWindowWidth() / (float)AP_GetWindowHeight();
AP_Vec2 screen01 = AP_V2((float)mx / (float)AP_GetWindowWidth(),
                        (float)my / (float)AP_GetWindowHeight());

AP_Ray ray = AP_CameraScreenRay(&cam, aspect, screen01);
/* intersect ray against your scene's bounding volumes / meshes */

4. Post-processing basics

Post-processing captures the frame offscreen at AP_Clear() and runs effects at AP_Present(). Turn it on once, then tune parameters per-frame or per-scene:

AP_SetPostEnabled(true);
AP_SetGuiLayer(AP_GUI_LAYER_OVERLAY); /* UI stays crisp, unprocessed */

The base set in AP2_Post.h:

AP_SetPostVignette(0.35f);
AP_SetPostBloom(1.1f, 0.4f);                 /* threshold, intensity */
AP_SetPostColorGrade(1.05f, 1.02f, 0.0f);    /* saturation, contrast, brightness */
AP_SetPostChromatic(0.02f);
AP_SetPostGrain(0.05f);
AP_SetPostSharpen(0.15f);

Or set everything at once via the config struct, useful for save/load of a "look" preset:

AP_PostConfig look = AP_PostDefaultConfig();
look.enabled = true;
look.flags = AP_POST_VIGNETTE | AP_POST_BLOOM | AP_POST_COLOR_GRADE;
look.vignette = 0.35f;
look.bloom_threshold = 1.1f;
look.bloom_intensity = 0.4f;
look.saturation = 1.05f;
AP_SetPostConfig(&look);

5. Cinematic extras (AP2_Post_extra.h)

Exposure, tonemapping, and depth of field push a scene from "game" toward "rendered":

AP_SetPostExposure(1.15f);
AP_SetPostFilmic(0.6f);          /* filmic tonemap blend */
AP_SetPostGamma(2.2f);

AP_SetPostDepthOfField(0.5f);
AP_SetPostDOFFocus(6.0f);        /* focal distance in world units */
AP_SetPostDOFAperture(0.15f);

AP_SetPostBloomRadius(1.4f);
AP_SetPostBloomSoftness(0.5f);
AP_SetPostLensDirt(0.2f);

AP_SetPostFog(0.3f);
AP_SetPostFogDensity(0.05f);
AP_SetPostFogHeight(2.0f);

AP_SetPostMotionBlur(0.25f);

Color science knobs live alongside the basic color grade:

AP_SetPostTemperature(0.1f);  /* warm (+) / cool (-) white balance */
AP_SetPostTint(-0.05f);       /* green (-) / magenta (+) */
AP_SetPostClarity(0.2f);      /* local contrast */
AP_SetPostDetail(0.15f);      /* fine detail enhancement */

6. Stylized looks

The same pipeline can go the other direction — retro, glitchy, or cel-shaded — which is handy for menu screens, death/pause states, or a distinct art style:

/* CRT / retro TV */
AP_SetPostScanlines(0.4f);
AP_SetPostCRT(0.5f);
AP_SetPostVHS(0.3f);
AP_SetPostRGBSplit(0.15f);

/* Toon */
AP_SetPostCelShade(0.7f);
AP_SetPostOutline(0.5f);
AP_SetPostPosterize(6.0f);

/* Damage / glitch state */
AP_SetPostGlitch(0.6f);
AP_SetPostDisplacement(0.3f);
AP_SetPostNoise(0.2f);

/* Distortion */
AP_SetPostWave(0.2f);
AP_SetPostFisheye(0.15f);

Combine sparingly — most of these read best at low amounts layered on top of the cinematic set, not as a replacement for it.

7. Master intensity and runtime toggling

AP_SetPostIntensity scales the entire stack, which is the easiest way to expose a single "Effects strength" slider in a settings screen, or to fade effects in/out over a cutscene:

float t = AP_Clampf(elapsed / fade_duration, 0.0f, 1.0f);
AP_SetPostIntensity(t);

Toggle the whole system off for a screenshot mode or performance profiling pass:

AP_SetPostEnabled(false);

Next

Advanced 3D: a PBR product scene · Capstone: a complete application