Ra Engine
A Vulkan 1.4 hardware ray tracing engine with a terminal-first UI philosophy. Theatrical-quality lighting informed by McCandless theory. Built at Flux Studios under Color It Company LLC.
Hardware RTX
Full VK_KHR_ray_tracing_pipeline. Reflections, refractions, single-bounce GI — all hardware accelerated.
Terminal-First UI
ANSI escape codes, F-key menu system. No ImGui rendering over the viewport.
GLTF 2.0 Scenes
tinygltf with KHR_lights_punctual, KHR_materials_transmission, RA custom extras.
McCandless Lighting
Theatrical lighting theory baked into the pipeline. Inverse-square attenuation, calibrated intensities.
01. Quick Start
Running the Engine
# Standard launch — skip recompile, suppress MangoHUD
./go.sh --skip-compile --no-mango
# Launch with a specific scene
./go.sh --skip-compile --no-mango scenes/launch_up_tutorial1.gltf
# Play mode (locks cursor, enables game input)
./go.sh --skip-compile --no-mango --play scenes/launch_up_tutorial1.gltf
# Full recompile from scratch
./go.sh --clean --no-mango
Compiling Shaders
After any change to .rchit, .rgen, .rmiss, or .rint files, recompile the SPIR-V:
./compile_shaders.sh
The active raygen is raygen.rgen → raygen_rgen.spv. The file raygen_aa.rgen is kept in sync but not compiled into the active pipeline.
Test Scenes
| Scene | Purpose |
|---|---|
| scenes/material_showcase_v2.gltf | Material / lighting test bed. All material types represented. |
| scenes/launch_up_tutorial1.gltf | Launch Up Level 1. 17 rising platforms, goal pad at Y=26.5m. |
02. Hardware Requirements
| Tier | GPU | Notes |
|---|---|---|
| Development / Minimum | NVIDIA RTX 3060 | Primary dev target and minimum required GPU. Per-object BLAS architecture. |
| Supported | AMD RX 6600 | RDNA2 hardware RT. RADV driver — note primitiveOffset quirks. |
| Supported | Intel Arc A380 | Xe-HPG hardware RT. Validated with Mesa ANV. |
Platform: Fedora Linux. Vulkan 1.4 required. VK_LAYER_KHRONOS_validation available via VK_INSTANCE_LAYERS for debug builds.
03. Renderer
Ra Engine uses a full hardware ray tracing pipeline. Every pixel is ray traced — there is no rasterization fallback. The pipeline dispatches from raygen.rgen and traces recursively through closest-hit and miss shaders.
Ray Tracing Pipeline
Recursive ray order per primary ray:
- Reflections — mirror and glossy bounces, depth-gated at bounce ≤ 1 for shadow rays
- Refractions — enter/exit pairs tracked via
fromRefractioninteger in RayPayload; suppresses reflection spawning inside refraction chains - GI (Global Illumination) — single bounce, multi-sample. Modes: Off / 1 / 2 / 4 samples.
fromGIflag skips shadow rays on GI hits. Correct 2× Monte Carlo scale factor applied.
After all tracing, a post-imageStore pass in raygen.rgen draws the crosshair over the tonemapped image.
Tonemapping & Color
| Setting | Behavior |
|---|---|
| ACES On | Filmic S-curve tone map. Rolls off highlights. |
| ACES Off | Linear passthrough. Vivid, saturated theatrical look. |
| Gamma On | sRGB gamma correction applied (2.2 curve). |
| Gamma Off + ACES Off | Preferred default. Pure linear, maximum vibrancy. |
Acceleration Structure
Ra Engine uses a per-object BLAS architecture. Each SceneObject owns a PerObjectBLAS struct. The TLAS holds one instance per BLAS, all with identity transforms.
// DigifluxEngine — per-object BLAS array (parallel to geometry SceneObjects)
struct PerObjectBLAS {
VkAccelerationStructureKHR as = VK_NULL_HANDLE;
VkBuffer buf = VK_NULL_HANDLE;
VkDeviceMemory mem = VK_NULL_HANDLE;
};
std::vector<PerObjectBLAS> objectBLASes;
Vertex positions are world-space baked. applyAllObjectTransforms() bakes the full transform into vertex data before BLAS build. gl_ObjectToWorldEXT is therefore identity per instance and not referenced in any shader.
Index addressing in closesthit: gl_InstanceCustomIndexEXT stores firstIndex for each TLAS instance, mapping the local gl_PrimitiveID back to the correct slot in the global index buffer:
uint baseIndex = gl_InstanceCustomIndexEXT;
uint i0 = indices[baseIndex + 3 * gl_PrimitiveID + 0];
uint i1 = indices[baseIndex + 3 * gl_PrimitiveID + 1];
uint i2 = indices[baseIndex + 3 * gl_PrimitiveID + 2];
buildObjectBLAS(idx, obj) — single object; buildAllBLASes() — full rebuild; buildTLAS() — rebuild TLAS after any BLAS change; destroyAllBLASes() — cleanup before full rebuild or shutdown. Register each SceneObject before calling buildAllBLASes().
04. Camera UBO — Field Order
The CameraUBO struct must match exactly across all shader files and the C++ host. Order is fixed — do not reorder.
CameraUBO struct will cause silent data corruption — wrong settings reaching the GPU with no validation error. Total: 24 fields. reachIndicatorEnabled and reachIndicatorNorm are declared in closesthit.rchit for layout sync only — they are only consumed in raygen.rgen.
05. Shader System
Shader Files
| File | Stage | Notes |
|---|---|---|
| raygen.rgen | Ray Generation | Active pipeline. Handles tonemapping and crosshair pass. |
| raygen_aa.rgen | Ray Generation | Kept in sync. Not compiled into active pipeline. |
| closesthit.rchit | Closest Hit | Material eval, light culling, GI/reflection/refraction dispatch. |
| miss.rmiss | Miss | Sky / environment color on ray miss. |
Light Culling
Per-light culling in closesthit.rchit uses an influence radius computed as:
float influenceRadius = sqrt(light.intensity * 1000.0);
Lights beyond this radius from the hit point are skipped entirely, with no shadow ray cast.
Glass / Zero-Roughness Safety
Alpha is clamped to 1e-4 in GGX BRDF evaluation to prevent NaN from division by zero at perfect mirror roughness:
float alpha = max(roughness * roughness, 1e-4);
06. Lighting
Ra Engine uses physically-based inverse-square attenuation. Intensities must be calibrated carefully — oversaturated values wash all surfaces to white.
Intensity Calibration
Use inverse-square attenuation: illuminance = intensity / (distance²). Start low and increase. A point light at intensity = 10 will fully illuminate geometry within ~3 metres.
GI Performance Flags
fromGI— set on GI rays. Skips shadow ray casting on GI hits, halving secondary ray count.fromRefraction— integer in RayPayload. Suppresses reflection spawning inside refraction chains without breaking enter/exit pairs.- Shadow ray depth gate — shadow rays only cast at bounce depth ≤ 1.
Adding Lights in the Editor
Press KP* (hold) to open the Add Light submenu:
| Key | Action |
|---|---|
| S | Add spot light at camera position |
| A | Add point light at camera position |
| ESC | Cancel submenu |
07. Scene Format — GLTF 2.0
Ra Engine loads scenes via tinygltf. Scenes must conform to GLTF 2.0 with the extensions listed below.
Supported Extensions
| Extension | Purpose |
|---|---|
| KHR_lights_punctual | Point and spot lights. Reference must live in node.extensions.KHR_lights_punctual.light in raw JSON. |
| KHR_materials_transmission | Glass / refractive materials. |
| KHR_materials_ior | Index of refraction for transmissive materials. |
| RA_materials_reflectivity | Ra-specific reflectivity scalar (custom extension). |
| _MATERIALINDEX | Per-vertex material index accessor. Required for multi-material meshes. |
Light Nodes — Critical
node.light (integer field) for light references, NOT node.extensions. The raw JSON must have the light reference in node.extensions.KHR_lights_punctual.light, but never read it from the generic extensions map in C++ code — use node.light only.
Material Extras
Ra Engine reads the following custom node extras:
| Extra field | Type | Description |
|---|---|---|
| ra_label | string | Display name shown in the scene hierarchy. |
| ra_euler | vec3 | Euler rotation in degrees (baked, survives save/reload after round-trip fix). |
| ra_scale | vec3 | Non-uniform scale. Min component: 0.01. |
| ra_group | string | Group membership name for hierarchy grouping. |
08. Scene Editor
The editor is accessed via F-key menus overlaid as ANSI terminal text. No ImGui is used. All interaction is keyboard-driven.
F-Key Menu System
| Key | Menu |
|---|---|
| F1 | Console View — scrollable stdout/stderr capture panel |
| F2 | Video settings (ACES, Gamma, GI samples, fog, reflections...) |
| F3 | Scene hierarchy view |
| F4 | Fly mode toggle |
Hierarchy View (F3)
The HierarchyView constructor signature (in order):
HierarchyView(
SceneManager* sceneMgr,
lights* lightsPtr,
bool* lightsNeedUpdate,
LightDeleteCB onLightDelete,
LightAddCB onLightAdd,
ObjectAddCB onObjectAdd,
SaveSceneCB onSaveScene,
TlasRebuildCB onTlasRebuild,
SnapToGroundCB onSnapToGround,
MaterialUpdateCB onMaterialUpdate,
materials* materialsPtr,
float keyRepeatDelay,
float keyRepeatRate
);
Controls Reference
| Key | Action |
|---|---|
| KP+ (hold) | Add Object submenu: C=cube (1×1×1, spawns 2m ahead) |
| KP* (hold) | Add Light submenu: S=spot, A=point |
| G | Snap selected object to ground (Möller–Trumbore raycast from bottom face) |
| ~ | Toggle grab mode (move selected object or whole group) |
| Alt+G | Grab entire group — moves all members together |
| Alt+D | Delete selected object and rebuild GPU buffers |
| Enter | Select group header — subsequent edits apply to all group members |
| ← → | Expand / collapse group in hierarchy |
| ESC | Cancel current submenu / deselect |
Fly Mode (F4)
| Key | Action |
|---|---|
| W / S | Forward / back along camera front |
| A / D | Strafe left / right |
| Space | Move up (world Y) |
| C | Move down (world Y) |
| Q / E | Roll left / right |
| Shift | 2× speed multiplier |
| F4 | Exit fly mode (roll resets to 0) |
float roll member. updateCamera() applies glm::rotate around cameraFront. The view matrix in rtx_pipeline.cpp must use cameraUp — not a hardcoded world up — or roll will not reach the GPU.
Console View (F1)
F1 opens a full-screen scrollable terminal panel that captures all stdout and stderr into a 500-line ring buffer. Output is ANSI-stripped for storage and re-coloured by line type (normal / success / warning / error). The real terminal still receives the original coloured output simultaneously via a tee streambuf.
| Key | Action |
|---|---|
| ↑ / ↓ | Scroll one line |
| PgUp / PgDn | Scroll half a page |
| Home / End | Jump to top / bottom of buffer |
| F1 | Close console |
ConsoleView::render() writes via origCout_ (the pre-redirect streambuf), never through std::cout, eliminating any risk of recursive capture during rendering.
Object Groups
Objects are assigned to named groups via the ra_group node extra or via the editor. The current scene name is displayed at the bottom-left border of the hierarchy panel — it turns red when there are unsaved changes. Press Enter on a group header to select the whole group; subsequent property edits apply to all members. Alt+G grabs the whole group for movement.
glfwSetCharCallback does not fire when GLFW_CURSOR_DISABLED is active. All text input uses keyCallback-based keyToChar() translation instead.
09. Build System
go.sh Flags
| Flag | Effect |
|---|---|
| --skip-compile | Skip C++ recompile, use existing binary |
| --no-mango | Disable MangoHUD overlay (always required) |
| --play | Interactive scene picker. Pre-selects last used scene — press Enter to reuse. scenes/_archive/ excluded from list. |
| --fullscreen | Session-only fullscreen. Temporarily sets start_fullscreen = true in ra_engine.conf, restores original value on exit. |
| --clean | Full recompile from scratch (calls make clean) |
| --validate | Enable Vulkan validation layers |
Scene file path is the final positional argument — writing it to disk updates .last_scene for the next session. Without a positional argument, go.sh loads the scene from .last_scene, falling back to material_showcase_v2.gltf on first run.
Incremental Builds
The Makefile uses -MMD -MP for dependency tracking. Without --clean, only changed translation units recompile. Shader compilation is always a separate step via compile_shaders.sh — the build system does not auto-detect shader changes.
# Typical dev workflow
./compile_shaders.sh # after shader edit
./go.sh --skip-compile --no-mango scenes/myscene.gltf
# After C++ changes
./go.sh --no-mango scenes/myscene.gltf
# Debug run with Vulkan validation
./go.sh --no-mango --validate scenes/myscene.gltf
10. Platform Notes
AMD (RADV driver)
primitiveOffset=0 instead of relying on the offset parameter.
Linux / X11 + GLFW
glfwSetCharCallback does not fire when GLFW_CURSOR_DISABLED is active (X11 limitation). Use keyCallback-based keyToChar() translation for all text input that may occur while the cursor is locked.
MangoHUD
MangoHUD causes segfaults during Vulkan teardown when running against Ra Engine. Always pass --no-mango. Do not attempt to use MangoHUD for performance monitoring with this engine — use Vulkan timestamp queries or VK_EXT_debug_utils markers instead.
11. Critical Pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| gl_ObjectToWorldEXT in shaders | Not needed — would be identity | Vertex positions are world-space baked. Do not use; not referenced in any active shader. |
| SceneObject registered after buildAllBLASes() | Object missing from TLAS | Register every SceneObject before calling buildAllBLASes(). |
| CameraUBO field mismatch | Silent wrong values in shaders | Keep struct field order identical in all .rgen/.rchit and C++ header. |
| Light node in node.extensions map | Lights not loading | Use node.light integer field in TinyGLTF. Raw JSON uses KHR_lights_punctual.light. |
| GLFW char callback with locked cursor | Text input silently drops | Use keyToChar() in keyCallback instead. |
| GGX roughness = 0 | NaN in GI / reflection output | Clamp alpha to max(roughness², 1e-4). |
| Moving objects via delta (no snapshot) | Float drift over many moves | Always move from originalVertices snapshot + new position. |
| RADV primitiveOffset | Wrong geometry indexed | Use primitiveOffset=0 with per-object index buffers. |
12. Lua Scripting
Ra Engine drives game logic through Lua scripts. Drop a .lua file in scripts/ and the full engine API is available as the global table engine. No recompile needed — scripts hot-reload live on save.
Script Contract
Every script must define three global functions the engine calls automatically:
| Function | When called |
|---|---|
| function init() | Once on load and again after every hot-reload. Re-discover object IDs here. |
| function update(dt) | Every frame before rendering. dt = delta seconds. |
| function shutdown() | On engine exit. Write persistent state via engine.set_var(). |
| function get_status_line() | Optional. Return a short string for the terminal status footer. |
Hot-Reload
The engine stat()s the script file every frame. On mtime change it tears down the Lua state, reopens it, and calls init() again. Lua variables do not survive hot-reload — persist anything important with engine.set_var(), which lives in C++ and is unaffected by Lua restarts.
Quick Example
local goal_id = 0
local score = 0.0
function init()
goal_id = engine.find_object("goal_pad")
score = engine.get_var("game.score", 0.0)
end
function update(dt)
local _, y, _ = engine.get_camera_position()
score = y * 100.0
engine.set_hud_data(score, y)
if goal_id ~= 0 and engine.player_overlaps_object(goal_id) then
engine.print_status("★ LEVEL COMPLETE!")
end
end
function shutdown()
engine.set_var("game.score", score)
end
13. Built with Ra Engine
Launch Up
Vertical FPS platformer. Height-based scoring, 100 pts/metre. Steam page live. Launch title. Jump gravity −18.0, coyote time 0.15s.
Ra Engine is sold at $49.99/seat on Steam through digiflux.one. Games built with Ra Engine are cross-promoted through the digiflux.one pipeline. Launch Up on Steam is built with Ra Engine.