Raytracer docs 🌐 FR

Home / Architecture

Architecture

The codebase is organized around a central Core that orchestrates scene loading, plugin loading, rendering, the optional UI and the optional cluster mode. The design favors interfaces (testability, extensibility) and cleanly separates parsing, data ownership and rendering logic.

Layered view

Each responsibility lives in a dedicated module. The table below maps every module to its location in the sources and to its role.

ModuleLocationRole
Entry pointmain.cpp, srcs/core/Core.*Orchestrates the full lifecycle.
Scene load / savesrcs/core/scene/SceneParser.*, builder/*, factory/*, SceneRegister.*JSON reading, typed construction, reverse serialization.
Scene datasrcs/core/scene/Scene.*, bvh/*Camera, primitives, lights, materials, BVH acceleration structure.
Pluginssrcs/core/PluginLoader.*, srcs/common/IPlugin.hpp, IPluginLoader.hppDynamic loading of .so libraries.
Renderers / UIsrcs/plugins/renderer/*, srcs/plugins/user_interface/*Rendering and graphical interface plugins.
Geometry / lightssrcs/plugins/primitive/*, srcs/plugins/light/*Concrete primitive and light implementations.
Cluster (optional)srcs/core/cluster/*, srcs/common/cluster/*Distributed client/server rendering.
Tools & utilitiessrcs/core/obj/*, srcs/core/utils/*, srcs/core/exceptions/*, srcs/common/*OBJ import, PNG export, file watching, exceptions, math.

Dependency diagram

Overview of the main runtime dependencies:

main.cpp
   │
   ▼
 Core ───────────────► PluginLoader ───► plugins/*.so ──┬─► Renderer plugins (default, viewport)
   │                                                    └─► UI plugin (user_interface)
   │
   ├─► SceneParser ──┬─► parseCamera
   │        │        ├─► parseMaterials
   │        │        └─► parseObjects ──► SceneObjectBuilder ──┬─► PrimitiveBuilder
   │        │                                                  └─► LightBuilder
   │        ├─► ObjParser (.obj import)
   │        └─► SceneBuilder ──► Scene ──► BVHNode / buildBvh()
   │
   ├─► ISceneRenderer (active) ──► Scene
   ├─► RenderExporter ──► render.png
   ├─► SceneRegister ──► export .json
   ├─► FileObserver ──► hot reload of the scene file
   └─► ClusterModule (optional) ──► server / client ──► PacketFactory

The Core

The Core (srcs/core/Core.*) implements ICoreAccess and coordinates the entire lifecycle. In six main steps:

  1. Load pluginsloadPlugins() scans ./plugins, then loadRenderers() and loadUserInterface() retrieve instances by type.
  2. Parse & build — the .json file is parsed and the scene is built through the builders (loadScene() / loadNewScene()).
  3. Select the active renderer — among _defaultRenderer, _viewportRenderer and the internal cluster renderer.
  4. Build the BVH & renderscene.buildBvh() then renderFrame() / startRendering().
  5. Export the image — via RenderExporter to _renderOutputPath (default render.png).
  6. UI loop (optional) — the UI event loop runs and the FileObserver triggers hot reloading of the scene file.

Undo / redo history via JSON snapshots. The Core keeps a _history vector of snapshots (capped at HISTORY_LIMIT = 100) with a _historyIndex. Every mutation calls historyCapture(), which serializes the current state through snapshotScene(); historyUndo() / historyRedo() rebuild the scene from the target snapshot with restoreSnapshot(). A signature (historySignature()) avoids storing two identical consecutive snapshots.

During an undo/redo, the _suppressWatcherReload flag temporarily neutralizes the FileObserver so that an internal write is not mistaken for an external change to the file.

Plugin system

The rendering and interface modules are shared libraries (.so) loaded at runtime from ./plugins.

At startup, PluginLoader::load() calls dlopen(path, RTLD_NOW) then resolves, with dlsym, two mandatory symbols (a C ABI contract):

extern "C" IPlugin *create_plugin();
extern "C" void      destroy_plugin(IPlugin *plugin);

If either is missing, the dlopen is closed (dlclose) and the plugin rejected. Otherwise the created instance is stored in a PluginHandle { handle, instance, destroy } and can be queried by PluginType — currently RENDERER or USER_INTERFACE. On shutdown, unloadAll() destroys each instance then dlcloses its .so (important to avoid leaks reported by AddressSanitizer).

PluginTypeRole
renderer_default.soRENDERER“Default” renderer — full image rendering.
renderer_viewport.soRENDERER“Viewport” renderer — progressive rendering for the interface.
user_interface.soUSER_INTERFACESFML graphical interface (viewport, panels, gizmos).

Primitives and lights live in srcs/plugins/… but are currently compiled into the main executable and instantiated via the builders/factories — they are not (yet) loaded as dynamic .so plugins. The architecture nonetheless stays ready for that evolution (the PluginType enum even reserves commented-out PRIMITIVE/LIGHT entries).

Scene construction

Construction is deliberately separated from parsing, to keep parsing independent from concrete construction details:

scene.json
   │
   ▼
SceneParser ──┬─► parseCamera
   │          ├─► parseMaterials
   │          └─► parseObjects ──► SceneObjectBuilder ──┬─► PrimitiveBuilder
   │                                                    └─► LightBuilder
   ▼
SceneBuilder ──► Scene   (camera + materials + objects, dispatch by ObjectType)
  • SceneObjectBuilder — dispatcher: withType(string) enables primitive mode, withType(LightType) light mode; common setters (name, position…) are forwarded to the active internal builder, and build() returns an ISceneObject* (IPrimitive* or ILight*).
  • PrimitiveBuilder — builds concrete primitives (sphere, plane, cube, cylinder, cone, triangle, torus, tanglecube, fractal) from the parsed fields (position, rotation, scale, radius, height, size, vertex0..2, power, iterations, material…).
  • LightBuilder — builds PointLight and DirectionalLight (name, position/direction, intensity, color).
  • SceneBuilder — aggregates camera, materials and objects: creates Scene(camera, ambient, diffuse), registers materials by name, then dispatches by ObjectType (PRIMITIVE → addPrimitive, LIGHT → addLight).
  • PrimitiveFactory / LightFactory — default-value constructors (createPrimitive(type), createLight(type)), useful for creating objects from the editor without going through parsing.

SceneRegister performs the reverse direction (save path): it re-serializes the scene state to JSON (camera, environment coefficients, objects, materials), enabling round-trip editing in the interface. The special case type = "object" (mesh) is imported via ObjParser.

Rendering pipeline

An image is produced by applying the same sequence to every pixel:

  1. Camera ray — generate a primary ray P(t) = O + t·D for pixel (i, j), with bounds t ∈ [t_min, t_max].
  2. Accelerated intersection — test against the geometry, accelerated by the BVH built before rendering via scene.buildBvh() (global BVH + local per-mesh BVH).
  3. Closest hit — keep the smallest valid t (t, hit point, normal, material).
  4. Shading — at the hit point H = O + t·D: compute and orient the normal N, read the material parameters.
  5. Lights & shadow rays — for each light, an offset shadow ray (H + ε·N) tests visibility; if occluded the contribution is skipped, otherwise diffuse and specular are evaluated.
  6. Accumulation — ambient + visible direct contributions, clamped to the output range.
  7. PNG export — once all pixels are computed, the image buffer is written to disk (see Tools and utilities).

Numerical robustness: ε offset for secondary/shadow rays (avoids self-intersection shadow acne), rejection of near-zero denominators in linear solves (planes/triangles), consistent t ∈ [t_min, t_max] test, and normalization of vectors used in shading dot products.

Tools and utilities

Around the pipeline sit several focused utilities (directories srcs/core/obj/, srcs/core/utils/ and srcs/core/exceptions/):

ObjParser — .obj import

ObjParser (srcs/core/obj/ObjParser.hpp) reads a Wavefront .obj file and feeds the scene object list. It parses vertices (v), normals (vn) and faces (f) to produce ObjTriangle records (with smooth normal support), applies a transform (withPosition, withRotation, withSize), and exposes the raw triangles, vertices and face indices. The setEmitObjects(false) mode lets you retrieve geometry without emitting scene objects. Any parse error throws ObjParserException.

RenderExporter — PNG export

RenderExporter::saveToFile(render, path) (srcs/core/utils/RenderExporter.hpp) encodes a Render (width, height, pixels) into a hand-written PNG: it emits the PNG signature and the IHDR/IDAT/IEND chunks, uses zlib “stored” compression (uncompressed blocks), and computes the CRC32 and Adler32 checksums. The image is written as 8-bit truecolor RGB, with no dependency on an external image library. A failure to open the file throws ExportRenderException.

FileObserver — hot reload

FileObserver (srcs/core/utils/FileObserver.hpp) watches the scene file by remembering its last write time (std::filesystem::file_time_type). pollChanges() returns true when the file changed since the last call; the Core polls it in its loop to reload the scene automatically. setFilePath() and reset() let you (re)target the watched file.

Exceptions

Domain errors are modeled by dedicated exceptions (srcs/core/exceptions/), all derived from std::exception:

ExceptionThrown for
LoadingSceneExceptionScene loading — with an ExceptionType (UNKNOWN_FILE, CANNOT_OPEN_FILE, INVALID_FILE_EXTENSION, WRONG_FILE_CONTENT, OTHER).
BuildingObjectExceptionFailure to build a scene object in the builders.
ExportRenderExceptionFailure to export the image (opening/writing the PNG).
ObjParserExceptionError parsing an .obj file (defined alongside ObjParser).
ColorExceptionInvalid color value.
VectorExceptionInvalid vector operation (math).

Cluster module

The ClusterModule (srcs/core/cluster/*, interface srcs/common/cluster/*) provides a distributed rendering infrastructure: a server splits the image into tiles and hands them out to connected clients that return their pixels. It exposes startServer(scene, port), joinCluster(address, port) and leaveCluster(); rendering is coordinated by ClusterRenderCoordinator and ClusterRenderer.

Communication goes through typed packets built by PacketFactory (identified by PacketID), for example:

PacketRole
ClientJoinRequestA client requests to join the cluster.
ServerSceneDataThe server sends the scene to clients.
ServerRenderRequestThe server requests the rendering of a tile.
ClientTileDataA client returns a tile's pixels.
ServerRenderStateThe server broadcasts the rendering progress state.
  • --server [PORT] launches the interface and starts the server (startServer).
  • --connect IP PORT launches the interface and joins a server (joinCluster).