02 — La boucle de rendu
Ce que tu vas apprendre
- Le squelette de la boucle de rendu, autour de la fonction
trace - Comment garder la bonne intersection : la plus proche
- Surfaces implicites contre paramétriques
- Pourquoi une interface
Objectcommune simplifie tout
Prérequis
- Les rayons caméra de l'article précédent
- L'intersection rayon-sphère vue dans la série fondations
On sait fabriquer un rayon par pixel. Il faut maintenant le lancer et trouver ce qu'il rencontre. C'est la boucle de rendu : le cœur battant du moteur, qui orchestre tout le reste.
Le squelette
cppfor (int j = 0; j < imageHeight; ++j) {
for (int i = 0; i < imageWidth; ++i) {
Ray ray = generateCameraRay(i, j); // article 01
pixels[i][j] = trace(ray, objects); // ↓
}
}
Toute l'intelligence est dans trace : lancer un rayon, trouver l'objet le plus proche, et renvoyer une couleur. C'est la fonction récursive de la série fondations (reflets, réfraction), mais on se concentre ici sur sa première mission — la visibilité.
Trouver l'intersection la plus proche
Un rayon peut croiser plusieurs objets. Celui qu'on voit est le plus proche de la caméra. La boucle d'intersection garde donc la plus petite distance positive :
cppObject* findClosest(const Ray& ray, const std::vector<Object*>& objects,
float& tNear, Vec2f& uv, Object*& hit)
{
tNear = INFINITY;
hit = nullptr;
for (Object* obj : objects) {
float t; Vec2f barycentric;
if (obj->intersect(ray, t, barycentric) && t < tNear) {
tNear = t; // plus proche que tout ce qu'on a vu
uv = barycentric;
hit = obj;
}
}
return hit;
}
Le t est le paramètre du rayon O + t·D : c'est la distance le long du rayon. Le uv est l'information « où sur l'objet » on a tapé (utile pour les textures et l'ombrage lisse, articles suivants). Tout objet expose la même méthode intersect : c'est ce contrat commun qui permet à la boucle d'ignorer le type réel de l'objet.
Implicite contre paramétrique
Comment un objet teste-t-il l'intersection ? Tout dépend de sa façon d'être décrit, et il y a deux grandes familles, comme l'explique le chapitre Parametric and Implicit Surfaces de Scratchapixel.
Surface implicite : définie par une équation f(P) = 0 que les points de la surface vérifient. La sphère en est l'exemple type : ‖P − C‖² − r² = 0. Pour intersecter, on injecte le rayon O + t·D dans l'équation et on résout en t (une équation du second degré pour la sphère, comme vu dans la série fondations). Les plans et les quadriques se traitent pareil.
Surface paramétrique : décrite par une fonction qui engendre les points à partir de paramètres, par exemple S(u, v) pour un patch de Bézier. Pratique pour générer la surface, mais l'intersection avec un rayon y est nettement plus pénible.
C'est l'une des grandes raisons pour lesquelles on triangule tout : un triangle a une intersection simple et exacte, là où une surface paramétrique demanderait une résolution numérique coûteuse. On y vient dès l'article suivant.
Une interface commune
L'astuce d'architecture, c'est l'interface Object partagée :
cppstruct Object {
virtual bool intersect(const Ray&, float& t, Vec2f& uv) const = 0;
virtual void getSurfaceProps(/* ... */) const = 0; // normale, coords texture
Vec3f color;
};
Sphère, plan, triangle, maillage : chacun implémente intersect à sa façon, mais la boucle de rendu les manipule tous de la même manière. Ajouter un nouveau type de géométrie ne touche pas une ligne de la boucle. C'est exactement le genre de découplage qu'on prêche en architecture logicielle, ici au service du rendu.
La boucle est en place ; il lui manque la primitive reine. L'article suivant explique pourquoi c'est le triangle, et comment on l'intersecte.
Sources
- Scratchapixel. A Minimal Ray-Tracer: Rendering Simple Shapes — Parametric and Implicit Surfaces. scratchapixel.com
- Scratchapixel. A Minimal Ray-Tracer: Rendering Spheres. scratchapixel.com
- Shirley, P. Ray Tracing in One Weekend, sections « Adding a Sphere » et « An Abstraction for Hittable Objects ». raytracing.github.io