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
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: 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

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

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+ (hold)Add Object submenu: C=cube (1×1×1, spawns 2m ahead)
KP* (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
EnterSelect group header — subsequent edits apply to all group members
Expand / collapse group in hierarchy
ESCCancel current submenu / deselect

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.

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.


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.