Camera Controller and Debug UI
The application layer provides the bridge between user input and the rendering engine through two primary components: the Camera Controller for navigation and the Debug UI for runtime inspection and tuning. These systems transform raw input into meaningful camera movement and expose the engine's internal state through an interactive ImGui panel, enabling both exploration of 3D scenes and real-time debugging of rendering parameters.
Camera Controller: WASD Navigation with Mouse Look
The CameraController class implements a familiar first-person navigation system found in games and 3D authoring tools. It translates keyboard and mouse input into camera movement, allowing users to freely explore loaded scenes.
Input Handling Architecture
The controller follows a simple state machine pattern that distinguishes between UI interaction mode and camera control mode. When you hold the right mouse button, the cursor disappears and mouse movements rotate the camera. Releasing the button returns control to the UI. This design prevents accidental camera movement when interacting with ImGui widgets while ensuring smooth navigation when intentionally exploring the scene.
The implementation carefully handles edge cases around ImGui input capture. When rotation is active, the controller ignores WantCaptureMouse to prevent flickering that would occur if the virtual cursor drifted over UI elements. For keyboard movement, it uses WantTextInput rather than WantCaptureKeyboard—this distinction matters because with keyboard navigation enabled in ImGui, WantCaptureKeyboard would be true whenever any widget is focused, permanently blocking WASD movement. The text input check only blocks movement during actual text entry.
Sources: camera_controller.cpp
Movement Controls
| Key | Action |
|---|---|
| W / S | Move forward / backward |
| A / D | Move left / right |
| Space | Ascend (world up) |
| Left Ctrl | Descend (world down) |
| Left Shift | Sprint (3x speed multiplier) |
| Right Mouse (hold) | Enable mouse look |
| F | Focus camera on scene bounds |
The movement system operates in camera-local space: forward and backward move along the camera's viewing direction, while left and right move perpendicular to it. The vertical axis (Space/Ctrl) always moves in world space (Y-up), preventing disorientation when looking up or down. The sprint multiplier of 3.0x provides quick traversal for large scenes without sacrificing precision for detailed examination.
Sources: camera_controller.cpp
Focus Target System
The F-key focus feature automatically positions the camera to frame the entire scene. When set_focus_target() receives an AABB (axis-aligned bounding box), pressing F computes a camera position that keeps the current orientation while ensuring the scene fits within the view frustum. The calculation uses the bounding sphere of the AABB and the tighter of the vertical or horizontal field of view to ensure the scene fits regardless of viewport aspect ratio.
Sources: camera_controller.cpp, camera.cpp
Camera Data Structure and Matrix Math
The Camera struct in the framework layer stores both input state (position, orientation, projection parameters) and derived state (view and projection matrices). This separation allows the controller to modify input values directly while the renderer consumes the pre-computed matrices.
Reverse-Z Projection
The camera uses reverse-Z projection, a technique that maps the near plane to depth 1 and the far plane to depth 0. This provides superior depth precision for distant objects compared to traditional projection. The implementation requires GLM_FORCE_DEPTH_ZERO_TO_ONE and Vulkan's VK_COMPARE_OP_GREATER depth comparison. The projection matrix is constructed manually rather than using glm::perspective to achieve this mapping:
projection[2][2] = near / (far - near)
projection[2][3] = -1
projection[3][2] = near * far / (far - near)Sources: camera.h, camera.cpp
Direction Vectors
The camera computes direction vectors from yaw and pitch angles:
- Forward: Derived from spherical coordinates, with yaw=0 looking along -Z
- Right: Always horizontal (no roll), perpendicular to forward in the XZ plane
- Up: Implicitly (0, 1, 0) through the use of
glm::lookAt
The view matrix is constructed using glm::lookAt with the world up vector, ensuring consistent orientation regardless of pitch.
Sources: camera.cpp
Debug UI: Runtime Inspection and Control
The DebugUI class provides a comprehensive ImGui panel that displays frame statistics, GPU information, and exposes nearly all runtime-tunable rendering parameters. It follows a stateless design where all data flows in through DebugUIContext and user actions flow out through DebugUIActions, making it easy to integrate without creating complex dependencies.
Frame Statistics
The FrameStats inner class accumulates per-frame delta times and computes statistics every second:
- Average FPS: Mean frame rate over the update interval
- Average frame time: Mean milliseconds per frame
- 1% Low FPS: Frame rate computed from the worst 1% of frame times (indicates stutter)
The 1% low metric is particularly valuable for identifying intermittent performance issues that average FPS might hide. By sorting frame times and averaging the slowest 1%, it quantifies the severity of frame time spikes.
Sources: debug_ui.cpp
Panel Organization
The debug panel organizes controls into collapsible sections:
| Section | Purpose |
|---|---|
| Header | FPS, GPU name, resolution, VRAM usage, VSync toggle |
| Path Tracing | PT-specific controls (bounces, firefly clamp, sampling options) |
| Denoiser | OIDN controls (auto/manual denoise, display toggle) |
| Camera | Position readout, FOV/near/far sliders |
| Scene | File loading, asset counts, culling statistics |
| Environment | HDR environment loading and display |
| Lighting | Light source mode selection, intensity/color controls |
| Features | Master toggles for skybox, shadows, AO, contact shadows |
| Shadow | Cascade settings, PCF/PCSS mode selection, bias controls |
| Ambient Occlusion | GTAO radius, directions, steps, temporal blend |
| Contact Shadows | Ray step count, distance, thickness parameters |
| Rendering | MSAA selection, shader reload, IBL intensity, exposure, debug views |
| Cache | Manual cache clearing for textures, IBL, and shaders |
Sources: debug_ui.cpp
Deferred Slider Pattern
Several controls use a custom "deferred" slider pattern that prevents intermediate values from affecting the renderer during text input. When you Ctrl+Click a slider to type a value, the underlying parameter remains at its pre-edit value until you press Enter or click away. This prevents the renderer from seeing partial keystrokes (like "1" while typing "100") that would cause visual flicker or expensive intermediate recomputation.
The pattern works by saving the original value before calling ImGui::SliderFloat, then restoring it if the item is active and WantTextInput is true. The change is only committed when text input completes.
Sources: debug_ui.cpp
Action-Based Side Effects
Rather than directly modifying engine state, the Debug UI returns actions that the application processes. This decoupling allows the UI to be stateless while enabling the application to handle complex side effects like resource recreation. For example, changing MSAA sample count or shadow map resolution requires rebuilding render targets—the UI signals this through DebugUIActions, and the application performs the recreation at a safe point in the frame.
Key actions include:
vsync_toggled: Requires swapchain recreationmsaa_changed: Requires render target recreationshadow_resolution_changed: Requires shadow map recreationscene_load_requested/env_load_requested: Async file loadingreload_shaders: Hot-reload shader compilationpt_reset_requested: Clear path tracing accumulationpt_denoise_requested: Manual denoise trigger
Sources: debug_ui.h
Debug Render Modes
The Rendering section provides a combo box for switching between visualization modes that help debug material and lighting issues:
| Mode | Description |
|---|---|
| Full PBR | Normal rendered output |
| Diffuse Only | Albedo × (diffuse lighting) |
| Specular Only | Specular reflections only |
| IBL Only | Image-based lighting contribution |
| Normal | World-space normal vectors |
| Metallic | Metallic channel visualization |
| Roughness | Roughness channel visualization |
| AO | Ambient occlusion only |
| Shadow Cascades | Color-coded cascade visualization |
| SSAO | Screen-space AO debug |
| Contact Shadows | Contact shadow debug view |
These modes are invaluable for validating that individual lighting components behave correctly and for identifying which part of the rendering pipeline might be causing visual artifacts.
Sources: debug_ui.cpp
Integration with Application Loop
The typical integration pattern in the application loop follows this sequence:
- Poll input:
glfwPollEvents()processes window messages - Update camera:
camera_controller.update(delta_time)processes input and updates camera matrices - Begin ImGui frame:
ImGui::NewFrame()starts UI construction - Draw debug UI:
debug_ui.draw(context)builds the panel and returns actions - Process actions: Application inspects
DebugUIActionsand applies side effects - Render: Camera matrices and UI state feed into the render graph execution
This separation of concerns keeps input handling, UI construction, and rendering distinct while allowing them to communicate through well-defined data structures.
Next Steps
With an understanding of camera navigation and runtime debugging, you can explore how the application orchestrates the entire rendering pipeline. The Renderer Orchestration page describes how the application coordinates render passes, manages resource lifetimes, and integrates the camera and debug UI into the frame flow. For understanding how scenes are prepared for rendering, see Scene Loading (glTF).