Raytracer docs 🌐 EN

Accueil / Architecture

Architecture

Le code s’organise autour d’un Core central qui orchestre le chargement de scène, des plugins, le rendu, l’interface optionnelle et le mode cluster optionnel. L’architecture privilégie les interfaces (testabilité, extensibilité) et sépare nettement l’analyse, la possession des données et la logique de rendu.

Vue en couches

Chaque responsabilité vit dans un module dédié. Le tableau ci-dessous relie chaque module à son emplacement dans les sources et à son rôle.

ModuleEmplacementRôle
Point d’entréemain.cpp, srcs/core/Core.*Orchestration du cycle de vie complet.
Chargement / sauvegarde de scènesrcs/core/scene/SceneParser.*, builder/*, factory/*, SceneRegister.*Lecture du JSON, construction typée, sérialisation inverse.
Données de scènesrcs/core/scene/Scene.*, bvh/*Caméra, primitives, lumières, matériaux, structure d’accélération BVH.
Pluginssrcs/core/PluginLoader.*, srcs/common/IPlugin.hpp, IPluginLoader.hppChargement dynamique des .so.
Renderers / UIsrcs/plugins/renderer/*, srcs/plugins/user_interface/*Plugins de rendu et interface graphique.
Géométrie / lumièressrcs/plugins/primitive/*, srcs/plugins/light/*Implémentations concrètes des primitives et des lumières.
Cluster (optionnel)srcs/core/cluster/*, srcs/common/cluster/*Rendu distribué client/serveur.
Outils & utilitairessrcs/core/obj/*, srcs/core/utils/*, srcs/core/exceptions/*, srcs/common/*Import OBJ, export PNG, surveillance de fichier, exceptions, maths.

Diagramme des dépendances

Vue d’ensemble des dépendances principales à l’exécution :

main.cpp
   │
   ▼
 Core ───────────────► PluginLoader ───► plugins/*.so ──┬─► Renderer plugins (default, viewport)
   │                                                    └─► UI plugin (user_interface)
   │
   ├─► SceneParser ──┬─► parseCamera
   │        │        ├─► parseMaterials
   │        │        └─► parseObjects ──► SceneObjectBuilder ──┬─► PrimitiveBuilder
   │        │                                                  └─► LightBuilder
   │        ├─► ObjParser (import .obj)
   │        └─► SceneBuilder ──► Scene ──► BVHNode / buildBvh()
   │
   ├─► ISceneRenderer (actif) ──► Scene
   ├─► RenderExporter ──► render.png
   ├─► SceneRegister ──► export .json
   ├─► FileObserver ──► rechargement à chaud du fichier de scène
   └─► ClusterModule (optionnel) ──► serveur / client ──► PacketFactory

Le Core

Le Core (srcs/core/Core.*) implémente ICoreAccess et coordonne tout le cycle de vie. En six grandes étapes :

  1. Chargement des pluginsloadPlugins() parcourt ./plugins, puis loadRenderers() et loadUserInterface() récupèrent les instances par type.
  2. Analyse & construction — le fichier .json est analysé et la scène est construite via les builders (loadScene() / loadNewScene()).
  3. Sélection du renderer actif — parmi _defaultRenderer, _viewportRenderer et le renderer de cluster interne.
  4. Construction du BVH & renduscene.buildBvh() puis renderFrame() / startRendering().
  5. Export de l’image — via RenderExporter vers _renderOutputPath (par défaut render.png).
  6. Boucle d’interface (optionnelle) — la boucle d’événements de l’UI tourne et le FileObserver déclenche le rechargement à chaud du fichier de scène.

Historique annuler / rétablir par instantané JSON. Le Core conserve un vecteur _history d’instantanés (limité à HISTORY_LIMIT = 100) avec un _historyIndex. Chaque mutation appelle historyCapture(), qui sérialise l’état courant via snapshotScene() ; historyUndo() / historyRedo() reconstruisent la scène à partir de l’instantané voulu avec restoreSnapshot(). Une signature (historySignature()) évite d’enregistrer deux instantanés identiques consécutifs.

Pendant un undo/redo, le drapeau _suppressWatcherReload neutralise temporairement le FileObserver pour ne pas confondre une écriture interne avec une modification externe du fichier.

Système de plugins

Les modules de rendu et d’interface sont des bibliothèques partagées .so chargées à l’exécution depuis ./plugins.

Au démarrage, PluginLoader::load() appelle dlopen(path, RTLD_NOW) puis résout, avec dlsym, deux symboles obligatoires (contrat C ABI) :

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

Si l’un des deux manque, le dlopen est refermé (dlclose) et le plugin rejeté. Sinon l’instance créée est stockée dans un PluginHandle { handle, instance, destroy } et interrogeable par PluginType — actuellement RENDERER ou USER_INTERFACE. À l’arrêt, unloadAll() détruit chaque instance puis dlclose son .so (important pour éviter les fuites signalées par AddressSanitizer).

PluginTypeRôle
renderer_default.soRENDERERRenderer « Default » — rendu complet de l’image.
renderer_viewport.soRENDERERRenderer « Viewport » — rendu progressif pour l’interface.
user_interface.soUSER_INTERFACEInterface graphique SFML (viewport, panneaux, gizmos).

Les primitives et lumières vivent dans srcs/plugins/… mais sont actuellement compilées dans l’exécutable principal et instanciées via les builders/factories — elles ne sont pas (encore) chargées comme .so dynamiques. L’architecture reste néanmoins prête pour cette évolution (l’énumération PluginType réserve d’ailleurs des entrées PRIMITIVE/LIGHT en commentaire).

Construction de la scène

La construction est volontairement séparée de l’analyse, afin de garder le parsing indépendant des détails de construction concrète :

scene.json
   │
   ▼
SceneParser ──┬─► parseCamera
   │          ├─► parseMaterials
   │          └─► parseObjects ──► SceneObjectBuilder ──┬─► PrimitiveBuilder
   │                                                    └─► LightBuilder
   ▼
SceneBuilder ──► Scene   (caméra + matériaux + objets, dispatch par ObjectType)
  • SceneObjectBuilder — répartiteur : withType(string) active le mode primitive, withType(LightType) le mode lumière ; les setters communs (nom, position…) sont transmis au builder interne actif, et build() renvoie un ISceneObject* (IPrimitive* ou ILight*).
  • PrimitiveBuilder — construit les primitives concrètes (sphere, plane, cube, cylinder, cone, triangle, torus, tanglecube, fractal) à partir des champs analysés (position, rotation, scale, radius, height, size, vertex0..2, power, iterations, material…).
  • LightBuilder — construit PointLight et DirectionalLight (nom, position/direction, intensité, couleur).
  • SceneBuilder — agrège caméra, matériaux et objets : crée Scene(camera, ambient, diffuse), enregistre les matériaux par nom, puis répartit selon ObjectType (PRIMITIVE → addPrimitive, LIGHT → addLight).
  • PrimitiveFactory / LightFactory — constructeurs par valeurs par défaut (createPrimitive(type), createLight(type)), utiles pour la création d’objets depuis l’éditeur, sans passer par le parsing.

SceneRegister effectue le chemin inverse (save path) : il re-sérialise l’état de la scène en JSON (caméra, coefficients d’environnement, objets, matériaux), permettant l’édition en aller-retour dans l’interface. Le cas particulier type = "object" (maillage) est importé via ObjParser.

Pipeline de rendu

Une image est produite en appliquant la même séquence à chaque pixel :

  1. Rayon caméra — génération d’un rayon primaire P(t) = O + t·D pour le pixel (i, j), avec bornes t ∈ [t_min, t_max].
  2. Intersection accélérée — test contre la géométrie, accéléré par le BVH construit avant le rendu via scene.buildBvh() (BVH global + BVH local par maillage).
  3. Hit le plus proche — conservation du plus petit t valide (t, point d’impact, normale, matériau).
  4. Ombrage — au point d’impact H = O + t·D : calcul et orientation de la normale N, lecture des paramètres du matériau.
  5. Lumières & rayons d’ombre — pour chaque lumière, un rayon d’ombre décalé (H + ε·N) teste la visibilité ; si occlus, la contribution est ignorée, sinon on évalue diffus et spéculaire.
  6. Accumulation — ambiant + contributions directes visibles, écrêtage dans la plage de sortie.
  7. Export PNG — une fois tous les pixels calculés, le tampon image est écrit sur disque (voir Outils et utilitaires).

Robustesse numérique : décalage ε pour les rayons secondaires/d’ombre (évite l’acné d’ombre par auto-intersection), rejet des dénominateurs proches de zéro dans les résolutions linéaires (plans/triangles), test cohérent t ∈ [t_min, t_max], et normalisation des vecteurs utilisés dans les produits scalaires de l’ombrage.

Outils et utilitaires

Autour du pipeline, plusieurs utilitaires ciblés (dossiers srcs/core/obj/, srcs/core/utils/ et srcs/core/exceptions/) :

ObjParser — import .obj

ObjParser (srcs/core/obj/ObjParser.hpp) lit un fichier Wavefront .obj et alimente la liste d’objets de la scène. Il analyse les sommets (v), les normales (vn) et les faces (f) pour produire des ObjTriangle (avec support des normales lissées smooth), applique une transformation (withPosition, withRotation, withSize), et expose les triangles bruts, les sommets et les indices de faces. Le mode setEmitObjects(false) permet de récupérer la géométrie sans émettre d’objets de scène. Toute erreur d’analyse lève ObjParserException.

RenderExporter — export PNG

RenderExporter::saveToFile(render, path) (srcs/core/utils/RenderExporter.hpp) encode un Render (largeur, hauteur, pixels) en PNG écrit à la main : génération de la signature PNG et des chunks IHDR/IDAT/IEND, compression zlib « stored » (blocs non compressés), et calcul des sommes de contrôle CRC32 et Adler32. L’image est écrite en truecolor RGB 8 bits, sans dépendance à une bibliothèque d’image externe. Un échec d’ouverture du fichier lève ExportRenderException.

FileObserver — rechargement à chaud

FileObserver (srcs/core/utils/FileObserver.hpp) surveille le fichier de scène en mémorisant sa date de dernière modification (std::filesystem::file_time_type). pollChanges() renvoie true lorsque le fichier a changé depuis le dernier appel ; le Core l’interroge dans sa boucle pour recharger la scène automatiquement. setFilePath() et reset() permettent de (re)cibler le fichier suivi.

Exceptions

Les erreurs métier sont modélisées par des exceptions dédiées (srcs/core/exceptions/), toutes dérivées de std::exception :

ExceptionLevée pour
LoadingSceneExceptionChargement de scène — avec un ExceptionType (UNKNOWN_FILE, CANNOT_OPEN_FILE, INVALID_FILE_EXTENSION, WRONG_FILE_CONTENT, OTHER).
BuildingObjectExceptionÉchec de construction d’un objet de scène par les builders.
ExportRenderExceptionÉchec de l’export de l’image (ouverture/écriture du PNG).
ObjParserExceptionErreur d’analyse d’un fichier .obj (définie avec ObjParser).
ColorExceptionValeur de couleur invalide.
VectorExceptionOpération vectorielle invalide (maths).

Module cluster

Le ClusterModule (srcs/core/cluster/*, interface srcs/common/cluster/*) fournit une infrastructure de rendu distribué : un serveur découpe l’image en tuiles et les distribue à des clients connectés qui renvoient leurs pixels. Il expose startServer(scene, port), joinCluster(address, port) et leaveCluster() ; le rendu est coordonné par ClusterRenderCoordinator et ClusterRenderer.

La communication passe par des paquets typés construits par PacketFactory (identifiés par PacketID), par exemple :

PaquetRôle
ClientJoinRequestUn client demande à rejoindre le cluster.
ServerSceneDataLe serveur envoie la scène aux clients.
ServerRenderRequestLe serveur demande le rendu d’une tuile.
ClientTileDataUn client renvoie les pixels d’une tuile.
ServerRenderStateLe serveur diffuse l’état d’avancement du rendu.
  • --server [PORT] lance l’interface et démarre le serveur (startServer).
  • --connect IP PORT lance l’interface et rejoint un serveur (joinCluster).