06 — Écrire un ray tracer minimal
Ce que tu vas apprendre
- Comment les briques des articles précédents s'assemblent en un programme
- La fonction
Tracerécursive, cœur du moteur - Comment écrire l'image résultat sans aucune bibliothèque, en PPM
- Pourquoi un ray tracer tient sur une carte de visite
Prérequis
- Toute la série, en particulier l'algorithme et la récursivité de Whitted
- Des bases de C++ pour lire le code ; rien à installer d'autre qu'un compilateur
Voici la promesse tenue : avec ce qu'on a vu, on peut écrire un ray tracer complet en environ 300 lignes de C++, sans aucune dépendance externe. Pas de moteur, pas de bibliothèque graphique, pas de fichier de config. Un compilateur, et c'est tout. C'est là que le ray tracing devient concret.
Compiler : zéro dépendance
Le programme se compile en une commande, avec gcc ou clang :
bashc++ -O3 -o raytracer raytracer.cpp # gcc
clang++ -O3 -o raytracer raytracer.cpp # ou clang
Le -O3 active les optimisations : sur un ray tracer, ça change tout, car le code passe son temps dans des boucles de calcul vectoriel serrées.
La fonction Trace, cœur du moteur
Toute la logique tient dans une fonction récursive, Trace, qui prend un rayon et la profondeur de récursion courante, et renvoie une couleur. C'est l'assemblage direct des articles 04 et 05 :
cpp#define MAX_RAY_DEPTH 3 // la limite de l'article 05
color Trace(const Ray &ray, int depth)
{
// 1. Trouver l'objet le plus proche touché (article 04)
Object *hit = findClosestIntersection(ray, &pHit, &nHit);
if (!hit) return backgroundColor;
// 2. Objet transparent/réfléchissant : on récurse (article 05)
if (hit->isGlass && depth < MAX_RAY_DEPTH) {
color reflection = Trace(reflectedRay(ray, pHit, nHit), depth + 1);
color refraction = Trace(refractedRay(ray, pHit, nHit), depth + 1);
float Kr, Kt;
fresnel(hit->ior, nHit, ray.direction, &Kr, &Kt);
return Kr * reflection + Kt * refraction;
}
// 3. Objet opaque : rayon d'ombre + couleur (article 04)
if (!inShadow(pHit, light))
return hit->color * light.brightness;
return 0;
}
Note les deux conditions d'arrêt de la récursion : soit le rayon ne touche rien (backgroundColor), soit on a atteint MAX_RAY_DEPTH. Sans elles, le programme ne rendrait jamais la main.
La boucle de rendu
Au-dessus de Trace, la double boucle sur les pixels — exactement celle de l'article 04, mais qui appelle maintenant Trace au lieu de calculer la couleur en ligne :
cppfor (int j = 0; j < imageHeight; ++j) {
for (int i = 0; i < imageWidth; ++i) {
Ray primRay;
computePrimRay(i, j, &primRay); // rayon depuis l'œil vers le pixel
pixels[i][j] = Trace(primRay, 0); // profondeur 0 au départ
}
}
Le calcul de computePrimRay tient compte du champ de vision (le field of view, qui règle l'ouverture de la pyramide de l'article 01) et du ratio d'aspect de l'image, pour que les pixels ne soient pas déformés sur une image rectangulaire.
La classe Sphere
Les objets de la scène sont des sphères, parce que leur intersection est exacte et tient en quelques lignes (article 04). Une Sphere porte son centre, son rayon, sa couleur, son caractère réfléchissant/transparent et son indice de réfraction, et expose une méthode intersect(ray) qui résout l'équation du second degré et renvoie le t du point d'entrée. C'est cette méthode que findClosestIntersection appelle pour chaque objet.
Quelques choix de mise en scène valent d'être notés, parce qu'ils trahissent les limites du modèle de base :
- la source de lumière est rendue comme une sphère visible, pour qu'on voie ses reflets ;
- les sphères transparentes sont teintées légèrement en rouge, parce qu'un verre parfaitement clair est… invisible, donc difficile à montrer ;
- l'effet Fresnel est simplifié (approché par un facing ratio, l'angle de vue) ;
- les ombres des objets transparents apparaissent pleines et noires, ce qui est faux physiquement — un défaut assumé de cette version minimale.
La sortie : le format PPM
Comment afficher le résultat sans bibliothèque graphique ? On écrit un fichier PPM, le format d'image le plus bête qui existe : un en-tête texte, puis les valeurs RGB des pixels, brutes. Pas de compression, pas d'astuce. Le programme produit un untitled.ppm qu'on ouvre dans Photoshop, l'Aperçu de macOS ou GIMP. C'est moche à stocker, mais ça s'écrit en cinq lignes et ça ne dépend de rien. (La série « Digital imaging » du parcours détaillera ce format et comment l'afficher.)
Le ray tracer sur une carte de visite
Si tu doutes encore que l'algorithme soit compact, va voir le business card ray tracer, un défi lancé par Paul Heckbert sur comp.graphics en 1984 : faire tenir un moteur entier au dos d'une carte de visite. La version la plus célèbre, signée Andrew Kensler (1337 octets, avec même un flou de profondeur de champ), a été décortiquée ligne à ligne par Fabien Sanglard — une poignée de lignes de C illisibles mais parfaitement fonctionnelles :
bashclang++ -O3 -o minray minray.cpp
./minray > minray.ppm # la sortie standard redirigée dans un fichier PPM
Ces exercices de compression ne sont pas que de la frime : ils prouvent que la sophistication d'un rendu ne tient pas à la verbosité du code. L'idée — tracer la lumière à l'envers — est petite. C'est sa fidélité au réel qui fait sa puissance.
Tu as maintenant la chaîne complète, de l'intuition au programme. L'article suivant rassemble tout le vocabulaire de la série en un glossaire de référence.
Sources
- Scratchapixel. Writing a Basic Raytracer. scratchapixel.com — code source sur GitHub.
- Sanglard, F. (2013). Decyphering the Business Card Raytracer (analyse complète du code de Kensler). fabiensanglard.net
- Back of the Business Card Ray Tracers, Real-Time Rendering (blog) — panorama historique, du défi de Heckbert (1984) aux versions modernes. realtimerendering.com
- Spécification du format PPM, projet Netpbm. netpbm.sourceforge.net/doc/ppm.html
- Shirley, P. Ray Tracing in One Weekend. raytracing.github.io