Vulkan 1.4 RTX Hardware $49.99/seat
Graphics 1.0 Alpha — Complete Emissive GI · ACES · Depth Fog Flux Studios ∮

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
⚠ Always pass --no-mango. MangoHUD causes segfaults on Vulkan teardown when overlaid on the Ra Engine window. This flag is not optional.

Compiling Shaders

After any change to .rchit, .rgen, .rmiss, or .rint files, recompile the SPIR-V:

./compile_shaders.sh

The active raygen is raygen.rgenraygen_rgen.spv. The file raygen_aa.rgen is kept in sync but not compiled into the active pipeline.

Test Scenes

ScenePurpose
scenes/material_showcase_v2.gltfMaterial / lighting test bed. All material types represented.
scenes/launch_up_tutorial1.gltfLaunch Up Level 1. 17 rising platforms, goal pad at Y=26.5m.

02. Hardware Requirements

TierGPUNotes
Development / MinimumNVIDIA RTX 3060Primary dev target and minimum required GPU. Per-object BLAS architecture.
SupportedAMD RX 6600RDNA2 hardware RT. RADV driver — note primitiveOffset quirks.
SupportedIntel Arc A380Xe-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:

  1. Reflections — mirror and glossy bounces, depth-gated at bounce ≤ 1 for shadow rays
  2. Refractions — enter/exit pairs tracked via fromRefraction integer in RayPayload; suppresses reflection spawning inside refraction chains
  3. GI (Global Illumination) — single bounce, multi-sample. Modes: Off / 1 / 2 / 4 samples. fromGI flag 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

SettingBehavior
ACES OnFilmic S-curve tone map. Rolls off highlights.
ACES OffLinear passthrough. Vivid, saturated theatrical look.
Gamma OnsRGB gamma correction applied (2.2 curve).
Gamma Off + ACES OffPreferred 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];
BLAS rebuild functions: 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.

viewInversemat4
projInversemat4
numLightsuint
frameCounteruint
aaSamplesuint
jitterStrengthfloat
aaPatternuint
reflectionsEnableduint
maxBouncesuint
glossySamplesuint
emissiveStrengthfloat
fogDensityfloat
giSamplesuint
acesEnableduint
gammaEnableduint
highlightFirstVertexuint
highlightVertexCountuint
flyCrosshairEnableduint
highlightStyleuint
hudEnableduint
hudScorefloat
hudHeightfloat
reachIndicatorEnabledint
reachIndicatorNormfloat
ambientSkyTopRfloat
ambientSkyTopGfloat
ambientSkyTopBfloat
ambientSkyHorizonRfloat
ambientSkyHorizonGfloat
ambientSkyHorizonBfloat
Any mismatch in field order between shaders and the C++ CameraUBO struct will cause silent data corruption — wrong settings reaching the GPU with no validation error. Total: 30 fields. reachIndicatorEnabled and reachIndicatorNorm are declared in closesthit.rchit for layout sync only — they are only consumed in raygen.rgen. The six ambientSky* tail fields (purple) are read by miss.rmiss for background and GI ambient. Individual floats avoid std140 vec3 padding misalignment.

05. Shader System

Shader Files

FileStageNotes
raygen.rgenRay GenerationActive pipeline. Handles tonemapping and crosshair pass.
raygen_aa.rgenRay GenerationKept in sync. Not compiled into active pipeline.
closesthit.rchitClosest HitMaterial eval, light culling, GI/reflection/refraction dispatch.
miss.rmissMissSky / 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:

KeyAction
SAdd spot light at camera position
AAdd point light at camera position
ESCCancel 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

ExtensionPurpose
KHR_lights_punctualPoint and spot lights. Reference must live in node.extensions.KHR_lights_punctual.light in raw JSON.
KHR_materials_transmissionGlass / refractive materials.
KHR_materials_iorIndex of refraction for transmissive materials.
RA_materials_reflectivityRa-specific reflectivity scalar (custom extension).
_MATERIALINDEXPer-vertex material index accessor. Required for multi-material meshes.

Light Nodes — Critical

TinyGLTF uses 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 fieldTypeDescription
ra_labelstringDisplay name shown in the scene hierarchy.
ra_eulervec3Euler rotation in degrees (baked, survives save/reload after round-trip fix).
ra_scalevec3Non-uniform scale. Min component: 0.01.
ra_groupstringGroup 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

KeyMenu
F1Console View — scrollable stdout/stderr capture panel
F2Video settings (ACES, Gamma, GI samples, fog, reflections...)
F3Scene hierarchy view
F4Fly mode toggle (physics suspended, free 6DOF camera)
F5Hot-reload scene — reloads GLTF from disk without restart
F6Toggle light helper gizmos — sphere (point) and cone (spot/directional) overlays; RAY_MASK_HELPER so they cast no shadow and don't appear in reflections/GI

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

KeyAction
KP+ or = (hold)Add Object submenu: C=cube (1×1×1, spawns 2m ahead)
KP* or Shift+8 (hold)Add Light submenu: S=spot, A=point
GSnap selected object to ground (Möller–Trumbore raycast from bottom face)
~Toggle grab mode (move selected object or whole group)
Alt+GGrab entire group — moves all members together
Alt+DDelete selected object and rebuild GPU buffers
Alt+CToggle collision on selected object or group (collides field; [no-col] indicator shown in hierarchy; round-trips via ra_collision GLTF extra)
Ctrl+DDuplicate selected object — copies geometry + original vertices; auto-selected; buffers dynamically resized
Ctrl+ZUndo last delete — 25-slot deque; restores geometry + material + light data
Ctrl+SSave scene to GLTF
EnterSelect group header — subsequent edits apply to all group members
TABCycle property edit mode (Position → Rotation → Scale → Roughness → Emissive...)
Cycle property backwards/forwards; also used to expand/collapse group in hierarchy
10Direct property selection by digit (up to 10 properties per object type)
ESCCancel current submenu / exit — triggers save-confirmation dialog if unsaved changes

Fly Mode (F4)

KeyAction
W / SForward / back along camera front
A / DStrafe left / right
SpaceMove up (world Y)
CMove down (world Y)
Q / ERoll left / right
Shift2× speed multiplier
F4Exit fly mode (roll resets to 0)
Roll is stored in a 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.

KeyAction
/ Scroll one line
PgUp / PgDnScroll half a page
Home / EndJump to top / bottom of buffer
F1Close 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.

GLFW Character Input Note: glfwSetCharCallback does not fire when GLFW_CURSOR_DISABLED is active. All text input uses keyCallback-based keyToChar() translation instead.

New Engine Systems (Aug 2026)

SystemDetails
Moving Platform Support (#696)Player anchors to the object it stands on. groundObjectId + groundObjectAnchor track the supporting object each frame via setGroundSupport(). Delta applied before physics so the platform never slides out from under the player a frame late.
Undo Stack (Ctrl+Z)25-slot std::deque<DeletedObjectSnapshot>. Supports both geometry objects and lights. Pushed on deleteObject() and deleteLight(); popped by undeleteObject().
Light Helper Gizmos (#679/#680)Sphere (point) and cone (spot/directional) meshes generated at scene build from lights[]. Tagged RAY_MASK_HELPER in the TLAS — cast no shadows, invisible in reflections/GI. Toggled via F6. syncLightHelper(lightIndex) repositions a helper after its light moves.
Third-Person CameraV toggles thirdPersonMode. Scroll wheel adjusts thirdPersonDistance (1.5–15 m). A procedural capsule mesh (capsuleObjectId) is positioned behind the camera each frame via updatePlayerCapsule().
Per-Object Collision (#678)SceneObject::collides (default true, sparse on disk). Toggle via Alt+C. [no-col] indicator in hierarchy. Round-trips through ra_collision GLTF extra.
Exit Confirmation (#675)ESC with unsaved changes raises a S/D/ESC modal via requestExit(). exitConfirmArmed ignores the ESC that raised it. quitNow() is the unconditional close callback.
Horizontal Collision (#671)resolveHorizontalMovement() + collidesHorizontally(). Casts lateral rays against actual mesh triangles (not object AABBs) — correct for merged mesh scenes where per-object bounds span empty gaps.
Ray Visibility MasksRAY_MASK_GEOMETRY 0x01, RAY_MASK_HELPER 0x02. Written into VkAccelerationStructureInstanceKHR::mask at TLAS build; passed as cullMask to traceRayEXT so secondary rays skip helper gizmos in hardware.
Game Object SystemMesh-less entities (GameObject) loaded from GLTF nodes with extras["ra_type"]="game_object". Separate ID space from SceneObjects. Exposed to Lua via find_game_object, get/set_game_object_position, etc.
Tagged Mesh ObjectsSceneObject::tag + props map. Set from extras["ra_tag"] / extras["ra_props"]. Queried via find_objects_by_tag, get/set_object_prop.
Ambient Sky Color6 new UBO tail fields (ambientSkyTop/HorizonRGB). Read by miss.rmiss for background + GI ambient. Set via Lua engine.set_ambient_color().

09. Build System

go.sh Flags

FlagEffect
--skip-compileSkip C++ recompile, use existing binary
--no-mangoDisable MangoHUD overlay (always required)
--playInteractive scene picker. Pre-selects last used scene — press Enter to reuse. scenes/_archive/ excluded from list.
--fullscreenSession-only fullscreen. Temporarily sets start_fullscreen = true in ra_engine.conf, restores original value on exit.
--cleanFull recompile from scratch (calls make clean)
--validateEnable 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 is silently ignored on RADV. Use dedicated per-object HOST_VISIBLE index buffers with 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

PitfallSymptomFix
gl_ObjectToWorldEXT in shadersNot needed — would be identityVertex positions are world-space baked. Do not use; not referenced in any active shader.
SceneObject registered after buildAllBLASes()Object missing from TLASRegister every SceneObject before calling buildAllBLASes().
CameraUBO field mismatchSilent wrong values in shadersKeep struct field order identical in all .rgen/.rchit and C++ header.
Light node in node.extensions mapLights not loadingUse node.light integer field in TinyGLTF. Raw JSON uses KHR_lights_punctual.light.
GLFW char callback with locked cursorText input silently dropsUse keyToChar() in keyCallback instead.
GGX roughness = 0NaN in GI / reflection outputClamp alpha to max(roughness², 1e-4).
Moving objects via delta (no snapshot)Float drift over many movesAlways move from originalVertices snapshot + new position.
RADV primitiveOffsetWrong geometry indexedUse 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:

FunctionWhen 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
Full API reference → digiflux.one/docs/lua-api — all 30 engine functions documented with signatures, parameters, and examples.

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.

Laser Tag

Fast-paced laser tag action built with Ra Engine. Available on Steam.

Laser Tag Demo

Try the Laser Tag experience on Steam or itch.io.


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.

Revenue share: 2% annually over $100K, 1% annually over $1M. Resets each year. Terms are permanent.