openFrameworks port of Kevin Suffern’s Ray Tracing from the Ground Up core (originally wxRayTracer / RTTGU), with a progressive multi-threaded CPU renderer that composites into a transparent
README
ofxRayTracer
openFrameworks port of Kevin Suffern’s Ray Tracing from the Ground Up core (originally wxRayTracer / RTTGU), with a progressive multi-threaded CPU renderer that composites into a transparent
ofFbo.Dependencies
- ofxImGui (declared in
addon_config.mkasADDON_DEPENDENCIES)
# from OF root
ln -sfn /path/to/ofxRayTracer addons/ofxRayTracer
ofxImGui must also be present in addons/
cd addons/ofxRayTracer/example-imgui
make -j && make RunReleaseStatus
| Area | State |
|---|---|
| Suffern core (World, Sphere, Plane, Matte, RayCast, Pinhole, samplers) | Working |
| Progressive multi-thread tile render | Working |
| Transparent FBO overlay (misses = alpha 0) | Working |
| OF 3D preview underlay | Live World geometry |
| EasyCam ↔ Pinhole sync (shared viewport) | Working |
| ofxImGui panel (palette themes, button UI) | Working |
| Window / 2× / 4× / standard resolutions | Working |
| PNG / TIFF export | Working |
| GPU / compute path | Planned |
| Specular / Phong / reflective / shadows | Planned (Whitted stub present) |
| BVH / meshes | Planned |
Layout
ofxRayTracer/
addon_config.mk
src/ # OF facade
libs/raytracer/
Builds/ # Textbook World::build registry (Suffern RTTGU)
RTBuild.h/.cpp # register / list / run named builds
reference/ # complete chapter demos
student/ # YOUR builds — see STUDENT_BUILDS.md
Cameras/ GeometricObjects/ …
example/ # Minimal progressive render
example-imgui/ # ImGui + ofxImAnim, export, resolution
example-learn/ # Learning lab: builds + skeleton tutorials
scripts/update_xcode_projects.shTextbook builds (RTTGU)
Suffern configures each scene inside
World::build. Classic samples (and hadryansalles/ray-tracing-from-the-ground-up) #include one BuildXXX body into World.cpp.Here each chapter demo is a named builder so OF can switch at runtime:
| id | Lesson |
|---|---|
ch03.single_sphere | Bare-bones SingleSphere + World::sphere |
ch03.multiple_objects | Flat set_color + MultipleObjects tracer |
ch14.matte_spheres | Matte materials, lights, pinhole (default) |
rt.rebuild(w, h, samples, "ch03.single_sphere");
rt.start();
for (const RTBuildInfo& b : ofxRayTracer::listBuilds()) {
// b.id, b.title, b.chapter, b.description, b.skeleton (raw TODOs)
}Add your own under
libs/raytracer/Builds/student/ with RT_REGISTER_BUILD — see student/STUDENT_BUILDS.md. Each reference build also stores a skeleton string for the learning UI (keep it raw; students fill TODOs).Examples
example-learn (textbook lab)
- Chapter build list → rebuild World + progressive render
- ofxImAnim FadeText / stepped lesson lines + skeleton reveal
- Next step walks description → raw TODO skeleton
- Implement new scenes in
Builds/student/without forking the core
cd example-learn && make -j && make RunRelease
or open example-learn/example-learn.xcodeproj
example-imgui (production-style UI)
| Control | Action |
|---|---|
| Render / Restart | Start progressive render |
| Sync Camera + Render | Bake EasyCam → Pinhole, then render |
| Window / 2× / 4× | RT resolution relative to window (auto-updates on resize) |
| 720p / 1080p / 1440p / 4K … | Fixed standard sizes |
| Save PNG / Save TIFF | Export when render is complete → bin/data/exports/ |
| Sample buttons 1 / 4 / 9 / 16 | AA samples per pixel |
| Preview / Raytrace toggle buttons | Layer visibility |
| Key | Action |
|---|---|
SPACE | Sync camera + restart |
C | Cancel |
E | Export PNG |
1–4 | Sample presets |
raytracer.setup(ofGetWidth(), ofGetHeight(), 1);
raytracer.syncFromEasyCam(cam);
raytracer.start();
// …
if (raytracer.isComplete()) {
raytracer.savePng("exports/frame.png"); // keeps alpha
raytracer.saveTiff("exports/frame.tif", true, ofColor::black); // optional solid bg
}example (minimal)
Keyboard-only HUD; same core renderer.
---
Architecture
ofApp
├── ofEasyCam + drawPreview3D() // interactive GL underlay
└── ofxRayTracer
├── World (Suffern scene graph)
├── TileRenderPool (N worker threads)
└── ofPixels → ofTexture / ofFbo (RGBA, transparent misses)CPU render path
1. Split the view plane into tiles (default 32×32). 2. Worker threads pull tiles and call
World::render_pixel → Pinhole::render_pixel → Matte / RayCast. 3. Hits write opaque RGB; misses write alpha 0 for overlay compositing. 4. Main thread merges completed tiles into ofPixels, uploads once per frame to the GPU texture/FBO. 5. Display letterboxes the buffer into the window so high-res renders (2× / 4K) stay aspect-correct and aligned with the GL preview.---
Optimisations already in place
These are the main performance / correctness improvements over the original single-pixel OF glue:
| Optimisation | What it does |
|---|---|
| Tile-based work queue | Image is split into tiles; workers grab whole tiles instead of one pixel at a time (far less channel / sync overhead). |
| Multi-threaded CPU pool | TileRenderPool uses std::thread count defaulting to hardware concurrency. |
| Single GPU upload / frame | Workers fill CPU pixel buffers; the main thread batches tile merges and does one loadData per update(). |
| Transparent miss compositing | Background rays store alpha 0 so GL preview (or a clear colour) shows through without a second mask pass. |
| Pinhole path restored | Per-pixel rays use Suffern pinhole UVW + view distance, not the old orthographic hack. |
| EasyCam ↔ Pinhole FOV match | Vertical FOV maps to view-plane distance d = (s·vres/2) / tan(fov/2) so GL and RT share the same frustum. |
| Shared letterboxed viewport | Preview cam.begin(rect) and FBO draw use the same rectangle (aspect of RT buffer), preventing the “FBO offset when orbiting” bug. |
| Resize debounce | Window resize rebuilds resolution after ~250 ms so continuous drag doesn’t thrash cancel/rebuild/start. |
| Header renames for OF 0.12 | RTSampler.h / RTConstants.h avoid silent includes of MetalANGLE’s Sampler.h / Constants.h. |
C++17 std::shuffle | Replaces removed std::random_shuffle in Suffern samplers. |
Complexity note
Intersection is still O(objects) linear scan per ray (Suffern textbook style). That is fine for the demo sphere field; it will not scale to dense meshes without acceleration.
---
What can still be improved
High impact (CPU)
1. BVH / SAH (or grid) — Biggest win once triangle meshes or hundreds of primitives land. Shadow rays and reflections multiply the benefit. 2. Per-worker RNG / sampler state — Multi-sample AA and Monte Carlo need thread-local
RandomNumber / count-jump indices so workers don’t race. 3. SIMD ray–sphere packets — Structure-of-arrays spheres + 4/8-wide rays (SSE/NEON) for primary visibility. 4. Russian roulette + max depth — Path/Whitted efficiency; ViewPlane::max_depth exists but materials for reflection/refraction are incomplete. 5. Sub-image GPU upload — glTexSubImage2D only dirty tiles instead of re-uploading the full frame every time a tile finishes. 6. Progressive refinement — First pass 1 spp interactive, then accumulate higher spp without discarding the buffer. 7. Scene rebuild without full World::build() — Resize currently rebuilds the default scene; keep geometry and only change ViewPlane resolution + camera.Materials & lighting (Suffern completeness)
8. Phong / glossy specular BRDFs 9. Shadows (
shadow_hit on lights) 10. Area lights + emissive materials (AreaLighting tracer is stubbed) 11. Reflective / transparent Whitted materials 12. Ambient occlusion / environment light using hemisphere sampling GPU / hybrid (later examples)
13. Compute-shader primary rays for spheres + Matte (same feature set as now, much faster). 14. Hybrid: GPU primary visibility + CPU secondary rays / materials. 15. Metal / Vulkan RT for production; keep this core as the educational reference.
Tooling / UX
16. Scene editor — add/move spheres from ImGui; serialise builds. 17. Mesh import (ofMesh / assimp → triangle accel). 18. EXR / linear HDR export for grading pipelines. 19. Live camera scrub — low-res continuous RT while orbiting, full-res on commit.
---
Notes for OF 0.12+ / Xcode / projectGenerator
MetalANGLE ships headers named
Sampler.h and Constants.h. This addon renames the Suffern headers to:RTSampler.h(class stillSampler)RTConstants.h
RTConstants.h file not found in Xcode
projectGenerator is not “out of date” for this — it is a nested-addon path issue:
1. Examples live inside the addon (
apps/ofxRayTracer/example-imgui/), so PG often sets the ofxRayTracer group path to ofxRayTracer/ instead of ... 2. Flat Suffern includes (#include "RTConstants.h") need every libs/raytracer/ folder on the header search path. 3. Project.xcconfig from the template sets HEADER_SEARCH_PATHS = $(OF_CORE_HEADERS) only; without $(inherited) + our paths, Xcode never sees Utilities/RTConstants.h.Fix already in the repo:
example/App.xcconfig— permanentOFX_RAYTRACER_HEADERS(includes…/Utilities)Project.xcconfigends with#include? "App.xcconfig"- After PG, ofxRayTracer group path should be
..(re-applied when regenerating)
# regenerate Xcode project
projectGenerator -o"$OF_ROOT" -p"osx" -a"ofxRayTracer,ofxImGui" \
"$OF_ROOT/apps/ofxRayTracer/example-imgui"
open the new project (not the old emptyExample.xcodeproj)
open example-imgui/example-imgui.xcodeprojPrefer
example-imgui.xcodeproj / example.xcodeproj. Do not use legacy emptyExample.xcodeproj.After source / addon changes
Whenever files are added/removed under
src/ or libs/, or addons change, regenerate Xcode projects:./scripts/update_xcode_projects.sh # both examples
./scripts/update_xcode_projects.sh example-imguiThis runs projectGenerator, then re-applies nested-addon fixes (
ofxRayTracer group path .., App.xcconfig header paths, $(inherited)).make builds are unaffected (makefile auto-discovers addon include dirs).License
Suffern RTTGU sample code: non-commercial / GPL as per original notices. Addon glue / OF integration: same spirit; see file headers.
ofxAddons