Raytracer docs 🌐 FR

Home / Graphical interface

Graphical interface

The Raytracer's interactive interface is a runtime plugin (user_interface.so). It shows the scene, the editing panels and the render viewport, and lets you manipulate objects with the mouse — down to editing the individual vertices of a mesh.

SFML .so plugin ICoreAccess Interactive editing

Overview

The graphical interface is implemented as a runtime plugin (user_interface.so) loaded by the core's plugin loader. The entry point is the UserInterface class (which implements IUserInterface), and its plugin type is PluginType::USER_INTERFACE. The source lives in srcs/plugins/user_interface/.

The runtime integration cycle is as follows:

  1. the core loads plugins from ./plugins;
  2. the UI plugin is retrieved by its type USER_INTERFACE;
  3. create(ICoreAccess&) is called;
  4. the UI uses ICoreAccess to trigger scene and render actions;
  5. on shutdown, destroy() is called and the plugin instance is unloaded.

UserInterface creates and owns the SFML window event loop, builds the menus, side panels and render-viewport widgets, forwards user actions to ICoreAccess, synchronizes viewport selection with panel selection, and displays toasts and auxiliary windows. It provides:

  • scene lifecycle actions (new / open / save / save as / import);
  • object creation from the menu (primitives / lights);
  • selection and property editing through the panels;
  • render status display and render-view controls;
  • cluster join / start actions through dedicated windows;
  • toast notifications for success and errors.

The UI depends on SFML (graphics, window, system, network). If the viewport renderer plugin is missing, the UI reports it through a toast and keeps running.

The Component contract

Every widget derives from a common abstraction, Component, and shares the same lifecycle:

StageRole
setFont(...)Assigns the font used for text.
layout(...)Computes geometry (for widgets that own their position/size).
update(mouse)Computes the hovered state from the mouse position.
handleEvent(event, mouse)Mutates state in reaction to events and fires callbacks.
draw(window)Draws the widget into the window.

The base struct exposes the state fields enabled, hovered and visible. Viewport projection and picking helpers (including projectToPixel(...), pickViewportLight(...) and the material-slider filter isMaterialFloatSlider(name, model)) are grouped in ViewportHelper.

Panels

The panels (in srcs/plugins/user_interface/panels/) form the side inspector column. Each one rebuilds itself dynamically based on the selected object.

PanelRole
HierarchyPanelTree-like camera / lights / primitives list, and selection handling.
ObjectPanelInspector / editor for the selected object's properties.
MaterialPanelMaterial-focused inspector for the selected primitive.
RendererPanelRender-texture display and render-tab controls.
CameraPanelEditing of the camera resolution, position and rotation.

HierarchyPanel

Displays the scene as a tree-like list (camera, lights, primitives) and handles selection. It supports multi-selection, camera selection, and selection sync with viewport picking. A hide request callback (setOnItemHideRequest) lets an object be hidden, and the panel handles its own internal scroll and scrollbar.

ObjectPanel

Inspector and editor for the selected object's properties. Its key method rebuild(const ISceneObject *currentObject) reconfigures the panel dynamically based on the object's runtime type. It exposes:

  • the common transforms — position, rotation and scale;
  • for primitives — a material dropdown and float properties converted to sliders;
  • for lights — a color picker and an intensity field.

It is also the anchor for vertex editing: the Convert to Mesh button, the vertex navigator < Point N / total >, the Vertex field and the - / + buttons of the Size row all appear here (see Vertex editing).

MaterialPanel

Material-focused inspector for the selected primitives. Its method rebuild(const ISceneObject *currentObject) builds the controls from the current material state. It provides:

  • a model selector (Phong vs PBR) through a SegmentedControl;
  • base-color editing through a ColorPicker;
  • dynamic float sliders, filtered by material model through ViewportHelper::isMaterialFloatSlider(...).

RendererPanel

Displays the render output texture and the top render-tab controls: render comment / status text, a close-render button, and pixel-space mapping utilities (getViewportPixel(...)). It keeps the viewport scale and bounds synchronized with the current render dimensions.

CameraPanel

Lets you edit the active camera: the resolution (width and height), the position and the rotation, through text fields and vector fields. Its method rebuild(ICamera *camera) populates the fields from the current camera.

Reusable components

Reusable widgets are defined in srcs/plugins/user_interface/components/. They are assembled by the panels and windows.

ComponentRole
ButtonClickable button, fires an onClick callback.
CheckboxBoolean checkbox.
ColorPickerColor picker (material color, light color).
DropdownSelection dropdown (e.g. material).
SliderSlider for a float value (material properties).
SegmentedControlSegmented selector (e.g. Phong / PBR model).
TextFieldText / value input field.
VectorFieldThree-component vector input (position, rotation, scale).
SeparatorVisual separator between sections.
Menu systemMenu bar and menu items (components/menu/).

Most widgets follow the same shared pattern:

  1. they store geometry in SFML shapes / text;
  2. they compute the hovered state in update(...);
  3. they mutate state in handleEvent(...);
  4. they expose callbacks (onClick, onChange, etc.);
  5. they return a cursor hint through getCursor().

This uniform pattern (setFont / layout / update / handleEvent / draw + callbacks) makes adding a new widget very mechanical: just implement the Component contract.

Auxiliary windows

Pop-up windows are implemented in srcs/plugins/user_interface/windows/. They derive from the abstract Window class, the base for threaded SFML sub-windows. Each derived window implements handleEvent(...), drawUi() and updateUi(); loop() creates the window and runs the poll / update / draw cycle, while destroy() stops the running flag and joins (or detaches) the thread safely.

WindowRole
ExploratorWindowFile/folder explorer for opening or saving a scene (OPEN / SAVE modes).
LoadWindowMerge / import decision window between the current scene and the loaded scene.
JoinClusterWindowInput of the cluster connection parameters (IP and port).
MarketWindowMaterial market (Local and Online tabs) to browse and add materials.
  • ExploratorWindow: directory listing with parent navigation (..), hidden-file filtering, a filename field in save mode, and a confirmation button (Open / Save).
  • LoadWindow: presents paired checkbox groups for the camera fields and the environment-light coefficients; the user chooses, property by property, whether the value comes from the current scene or the loaded scene, then the selected values are written back to the active scene.
  • JoinClusterWindow: IP and port fields (numeric validation); the callback windowCallback(std::string ip, size_t port) is triggered on Enter validation or a JOIN click.
  • MarketWindow: browsing of a material library with search, under two tabs — local materials and online materials — with add-to-scene buttons.

Toast notifications

Toasts are transient, non-blocking notifications stacked in the bottom-right of the UI (toast/Toast.hpp, toast/ToastManager.hpp).

TypeUsage
INFONeutral information.
SUCCESSConfirmation of a successful action.
WARNINGNon-blocking warning.
ERRORError (e.g. missing renderer plugin).

The manager exposes push(title, content, type) to create an entry, update() to apply fade-in / fade-out and remove expired items, and draw(window) to render the stack of animated toasts.

Transform gizmos

When a single object is selected in the viewport, a gizmo lets you transform it directly with the mouse. Three tools are available:

ToolShortcutAction
MoveGTranslation through the gizmo arrows and planes.
RotateRRotation through the gizmo rings.
ScaleSScaling through the gizmo handles.

The T key toggles the gizmo frame between World and Local. These shortcuts only act when the viewport has focus, an object is selected, you are not in vertex-edit mode, and movement (fly) mode is off — so that G / R / S don't fight the WASD/ZQSD fly keys.

The tool choice is also available in the ObjectPanel (callbacks onGizmoModeChanged / onGizmoSpaceChanged), and a status badge (gizmo readout, World/Local frame) is drawn in the viewport.

Vertex editing

Vertex editing lets you move the individual vertices of an editable primitive directly in the viewport, by dragging on-screen handles or typing coordinates. It is the interactive counterpart to the object transform (position / rotation / scale) exposed in the ObjectPanel.

What is editable

A primitive is editable when it implements IEditablePrimitive in addition to IPrimitive. The UI discovers the capability at runtime with dynamic_cast<IEditablePrimitive *>(primitive) — the same runtime-capability pattern the renderer uses for selection awareness — so the IPrimitive plugin contract is left untouched.

PrimitiveVertices
Triangleits 3 corners.
Meshthe unique (welded) vertices of the loaded .obj; a shared vertex moves every incident face.

The analytic primitives (cube, sphere, plane, cylinder, cone, torus, tanglecube, fractal) are defined by parameters rather than vertices and are not vertex-editable: move / scale them with the ObjectPanel transform fields.

Convert to Mesh

To edit an analytic primitive vertex by vertex, select it and press Convert to Mesh in the ObjectPanel. Its surface is tessellated into a triangle mesh (baked to generated_meshes/<name>_<n>.obj), the primitive is replaced by that mesh, and the mesh is selected so you can immediately enter edit mode. Because it is a normal file-backed mesh, it saves / reloads and accepts vertex_overrides like any other mesh.

  • Infinite primitives (a plane) cannot be converted.
  • A cube is baked exactly to its 8 corners / 12 triangles (flat faces), so it stays a clean editable box.
  • Everything else uses a generic surface sampling: smooth / convex shapes (sphere, cone, cylinder) come out clean, while non-convex ones (torus, tanglecube) are approximated.

Entering / leaving edit mode

  • Enter: select a single editable primitive, then press Tab — or double-click it in the viewport.
  • Leave: press Tab again or Escape. Selecting a different object (in the hierarchy, or by deleting the edited one) also leaves edit mode.
  • While in edit mode a banner "Edit Mode — <name>" is shown over the viewport and a toast confirms the transition.

Classic object picking in the viewport is disabled in edit mode so a click never selects a different object by accident. Right-drag still orbits the camera; the WASD/ZQSD fly keys stay available except during an active vertex drag (so the axis-lock keys don't also move the camera).

Handles, hovering and picking

Every vertex is projected to screen and drawn as a small circle:

ColorMeaning
Blueidle vertex.
Whitethe vertex under the cursor.
Yellowthe selected vertex.

Clicking picks the vertex whose handle is nearest the cursor within a small pixel radius; ties (overlapping handles) go to the vertex closest to the camera. Vertices behind the camera or off screen are skipped. For very dense meshes the number of drawn handles is capped (picking still considers them all).

Alternatively, use the < Point N / total > navigator in the ObjectPanel (just above the Size row): the arrows step through the shape's vertices one by one without aiming at the on-screen handles. Stepping enters edit mode if needed and highlights the current vertex in bright red on the shape.

Dragging a vertex

Drag a handle to move its vertex. By default the vertex slides in the plane that passes through its starting position and is parallel to the screen (normal = camera forward) — the intuitive "move it where I point" behaviour.

  • Axis lock: hold X, Y or Z during the drag to constrain the move to that world axis.
  • Keyboard: the selected vertex's world coordinates appear in a Vertex field in the ObjectPanel; typing a value moves the vertex and stays in sync with dragging.
  • Grow / shrink (- / + buttons): the - / + buttons in the Size row move the selected vertex away from / toward the shape's centroid, so a single point can be pushed out or pulled in. With no vertex selected the same buttons scale the whole object instead.

After every edit the scene BVH is flagged dirty and the viewport re-traces, so the change is visible immediately.

Mesh specifics

  • Moving a shared vertex updates all incident faces.
  • The affected face normals are recomputed, and smooth (interpolated) vertex normals are refreshed as the average of the incident face normals.
  • The mesh's local BVH is rebuilt when the vertex is released (onGeometryChanged). For small meshes it is also rebuilt live during the drag; for large meshes (> ~4000 triangles) the surface renders slightly stale during the drag and snaps to the final shape on release, which keeps dragging responsive.

Saving edits (serialization)

  • Triangle — its three corners are already part of the scene JSON, so edited positions are saved and reloaded verbatim.
  • Mesh — the .obj file is never modified. Edits are stored as object-space overrides in a vertex_overrides list on the mesh object and re-applied after the .obj is loaded.
{
    "name": "Cube",
    "type": "mesh",
    "file": "tests/obj/cube.obj",
    "position": [26.0, 10.0, 15.0],
    "rotation": [0.0, 0.0, 20.0],
    "scale": [14.0, 14.0, 14.0],
    "material": "Blue",
    "vertex_overrides": [
        { "index": 0, "position": [1.0, -1.0, 0.857] }
    ]
}

Because overrides are object-space, they survive later changes to the mesh transform and round-trip cleanly: load → edit → save → reload restores the edited shape. See tests/configs/vertex_edit.json for a scene (a Triangle and a small cube mesh) set up to try this out.

Limitations

  • Editing changes geometry only; it does not add or remove vertices/faces.
  • Smooth normals recomputed at edited vertices are geometric averages, so hard edges authored in the .obj (vn) are softened locally where you edit.
  • The move plane is view-parallel; there is no separate depth-along-view drag (use the Vertex field or an axis lock for precise placement).

Keyboard shortcuts

KeyContextAction
GViewport, object selectedMove gizmo tool.
RViewport, object selectedRotate gizmo tool.
SViewport, object selectedScale gizmo tool.
TViewport, object selectedToggle the World / Local frame.
TabEditable primitive selectedEnter / leave vertex-edit mode.
Double-clickEditable primitive (viewport)Enter vertex-edit mode.
EscapeEdit modeLeave vertex-edit mode.
X / Y / ZDuring a vertex dragLock the move to the world X / Y / Z axis.
- / +ObjectPanel Size rowMove the vertex away from / toward the centroid (or scale the object if no vertex is selected).

The camera fly keys (WASD/ZQSD style, with the wheel for speed) stay active in the viewport outside edit mode; that is why the gizmo shortcuts are disabled while movement mode is on.