Ray tracing — 06 — Écrire un ray tracer minimal

Assembler tout en un programme : la fonction Trace récursive, la classe Sphere, la boucle de rendu, la sortie au format PPM, et les fameux ray tracers tenant sur une carte de visite.

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 Trace ré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


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

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.