gKit2 light
principes du lancer de rayons

Un pipeline graphique permet de calculer la couleur des pixels d'une image représentant une scène 3d éclairée par une ou plusieurs sources de lumières. Le pipeline openGL basé sur la rasterization / fragmentation est une solution particulière. On peut résumer son fonctionnement :

zbuffer[]= max
image[]= noir
pour chaque triangle
transformer les sommets du triangle dans le repère projectif de la camera
si une partie du triangle est visible dans l''image (ou dans le frustum de la camera)
transformer les sommets dans le repere de l''image
pour chaque pixel de l''image
si le centre du pixel est inclus dans le triangle
interpoler les attributs des sommets
interpoler la profondeur
calculer la couleur
si profondeur < zbuffer[pixel]
image[pixel]= couleur
zbuffer[pixel]= profondeur
Point max(const Point &a, const Point &b)
renvoie la plus grande composante de chaque point. x, y, z= max(a.x, b.x), max(a.y,...
Definition: vec.cpp:35

ou en gros

zbuffer[]= max;
pour chaque triangle
pour chaque pixel
si profondeur < zbuffer[pixel]
zbuffer[pixel]= profondeur

à la fin du parcours, tous les pixels contiennent la profondeur du triangle le plus proche de la camera ainsi que sa couleur.

le lancer de rayon fonctionne dans l'autre sens :

rayons[]
pour chaque pixel
rayons[pixel]= rayon passant par le centre du pixel
zbuffer[]= max
image[]= noir
pour chaque rayon
pour chaque triangle
si le rayon touche le triangle
si profondeur < zbuffer[rayon]
interpoler les attributs des sommets
calculer la couleur
image[rayon]= couleur
zbuffer[rayon]= profondeur

à première vue, les 2 solutions ne sont pas très différentes. Pourtant, il y a une différence fondamentale, le lancer de rayons fonctionne pour un ensemble de rayons quelconque. Dans l'exemple ci-dessus, les 2 images seront identiques. Mais on peut facilement créer de nouveaux rayons pour tester la visibilité des sources de lumière et ajouter très simplement les ombres dans l'image, ce qui est plus difficile à faire avec openGL, par exemple.

Une autre différence importante est ce que l'on peut calculer l'intersection d'un rayon avec d'autres objets que des triangles : des spheres, des cubes, des plans, des cylindres, des cones, des tores, des fractales, des champs de distances, des fonctions implicites, etc. sans avoir à trianguler la surface de ces objets. Par contre, il faudra écrire les différentes fonctions d'intersections.

rayon ?

Un rayon est une droite (ou un segment) dans l'espace (de la scène, par exemple) qui passe par le centre d'un pixel dans l'image. Mais il faut connaitre au moins 2 points pour représenter une droite. lesquels ? L'idée est que le rayon est l'ensemble des points de la scène qui se projettent sur le centre du pixel.

Quelles sont les coordonnées du centre d'un pixel dans le repère de la scène ? Il suffit de se rappeller que l'on peut transformer des coordonnées du repère image vers le repère de la scène en utilisant les transformations inverses des transformations standards. si on connait des coordonnées \( p_{scene} \) dans le repère de la scène, on peut écrire :

\[ q_{image} = Image \times Projection \times View \times p_{scene} \]

et

\[ p_{scene} = ( Image \times Projection \times View )^{-1} \times q_{image} \]

Quelles sont les coordonnées d'un pixel dans le repère \( Image \) ? On connait directement x et y, il ne reste plus que z ? Par définition, z = 0 sur le plan proche du frustum de la camera et z = 1 sur le plan far du frustum. repassez dans le cours d'intro sur les transformations si ce n'est pas clair.

Résultat, on peut calculer les coordonnées de 2 points sur le rayon : x, y sur le plan near (z = 0), et x, y sur le plan far (z = 1) :

\[ origine_{scene} = ( Image \times Projection \times View )^{-1} \times \begin{bmatrix} x \\ y \\ 0 \end{bmatrix}\\ extremité_{scene} = ( Image \times Projection \times View )^{-1} \times \begin{bmatrix} x \\ y \\ 1 \end{bmatrix}\\ \]

On peut donc représenter le rayon par les 2 points : origine et extremité. Mais en général, pour calculer les intersections avec les objets, on utilise plutot une autre convention : origine et direction. direction est le vecteur entre l'origine et l'extremité \( direction = extremité - origine \).

remarque : on connait un autre point sur le rayon, c'est le centre de projection de la camera, ou la position de la camera. Les coordonnées sont (0, 0, 0) dans le repère camera :

\[ origine_{scene} = ( View )^{-1} \times \begin{bmatrix} 0 \\ 0 \\ 0 \end{bmatrix}\\ \]

intersection ?

Il ne reste plus qu'à trouver comment calculer l'intersection d'un rayon / d'une droite avec les objets qui composent la scène.

Il faut exprimer le fait qu'une intersection représente un point du rayon et un point de l'objet. permière question, comment représenter un point sur le rayon ? la solution classique utilise la forme paramétrique des droites : \( p(t) = o + t \vec{d} \), \( t \) identifie un point sur la droite / le rayon. il represente la position du point sur la droite.

Lorsque le rayon est un segment, il "contient" tous les points entre son origine et son extremite : \( p(t)= o + t \vec{d} \) avec \( t \in [0 .. 1]\). on peut bien sur retrouver l'origine et l'extrémité du segment : \( p(0)= origine \) et \( p(1) = extremite \).

Il est également possible de représenter une demi-droite infinie au lieu d'un segment, dans ce cas \( t \in [0 .. \infty)\), et l'extremité n'existe pas, on ne connait que l'origine et la direction du rayon.

De manière générale pour décrire un rayon on utilise, \( o \), \( \vec{d} \) et la borne de l'intervalle \( t_{max} = 1 \) ou \( \infty \) selon le cas :

struct Ray
{
Point o; // origine
Vector d; // direction
float tmax; // intervalle [0 tmax]
// le rayon est un segment, on connait origine et extremite, et tmax= 1
Ray( const Point& origine, const Point& extremite ) : o(origine), d(Vector(origine, extremite)), tmax(1) {}
// le rayon est une demi droite, on connait origine et direction, et tmax= \inf
Ray( const Point& origine, const Vector& direction ) : o(origine), d(direction), tmax(FLT_MAX) {}
// renvoie le point sur le rayon pour t
Point point( const float t ) const { return o + t * d; }
};
representation d'un point 3d.
Definition: vec.h:21
rayon.
Definition: tuto_bvh2.cpp:20
representation d'un vecteur 3d.
Definition: vec.h:59

attention : Les calculs d'intersection qui sont décrits juste après se font avec la droite infinie qui porte le rayon. Lorsque le calcul trouve une ou plusieurs intersections, il faut vérifier qu'elles se trouvent dans l'intervalle \( [0 .. t_{max}] \) du rayon. La droite infinie peut toucher un objet, mais pas nécessairement le rayon.

plan

comment décrire l'ensemble de points sur un plan ? il existe de nombreuses manières, mais l'idée est de choisir une représentation qui permet d'écrire le calcul de l'intersection comme la recherche du zero d'une fonction.

On peut représenter un plan par un point \( a \) et une normale \( \vec{n} \), noté \( plan(a, \vec{n}) \) et déterminer l'ensemble des points \( p \) appartenant au plan comme les zeros de la forme implicite :

\[ \vec{n} \cdot \vec{ap} = 0 \]

remarque : les points \( p \) sont sur le plan si les vecteurs \( \vec{ap} \) et \( \vec{n} \) sont perpendiculaires, leur produit scalaire est nul dans ce cas.

calculer l'intersection du rayon et du plan se resume à trouver le point du rayon \( p(t) \) qui se trouve aussi sur le plan. le point d'intersection vérifie les 2 propriétés en même temps :

\[ \vec{n} \cdot \vec{a p(t)} = 0 \]

rappel : \( p(t) = o + t\vec{d} \) désigne un point du rayon.

il ne reste plus qu'à trouver quelle valeur de t vérifie ces conditions :

\[ \begin{eqnarray*} \vec{n} \cdot \vec{ap(t)} & = & 0\\ \vec{n} \cdot ((o + t \vec{d}) - a) & = & 0\\ \vec{n} \cdot ((o - a) + (t \vec{d})) & = & 0\\ \vec{n} \cdot (o - a) + \vec{n} \cdot (t \vec{d}) & = & 0\\ \vec{n} \cdot (o - a) + t (\vec{n} \cdot \vec{d}) & = & 0\\ t (\vec{n} \cdot \vec{d}) & = & - \vec{n} \cdot (o - a)\\ t & = & \frac{- \vec{n} \cdot (o - a)}{\vec{n} \cdot \vec{d}}\\ t & = & \frac{\vec{n} \cdot (a - o)}{\vec{n} \cdot \vec{d}}\\ \end{eqnarray*} \]

rappel : calcul avec des points, des vecteurs et des produits scalaires, cf wikipedia

on peut remarquer que, sans trop de surprise, un rayon intersecte toujours un plan, sauf lorsque le rayon est parallele au plan et que la direction du rayon et la normale du plan sont perpendiculaires.

Par contre, il ne faut pas oublier que l'on ne s'interresse qu'aux intersections se trouvant devant la camera, c'est à dire \( t > 0 \), il faut bien faire la différence entre les intersections de la droite et des objets testés et les intersections du rayon (la demi droite positive) avec les objets...

cube

On peut représenter un cube par les plans qui portent chaque face. pour chaque axe, il y a une paire de plans parallèles : le rayon entre dans le cube par un plan et ressort de l'autre coté, par l'autre plan. Le rayon traverse l'espace compris entre les 2 plans pour un intervalle \( t \in [t_{min} .. t_{max}] \).

remarque : si le rayon est orienté dans l'autre sens, s'il traverse l'espace de la droite vers la gauche, il faut inverser les bornes de l'intervalle \( [t_{min} .. t_{max}] \). Les bornes de l'intervalle doivent vérifier \( t_{min} \leq t_{max} \)

On peut décrire un cube par un centre \( c \) , et 3 axes \( \vec{i}, \vec{j}, \vec{k} \). Les calculs d'intersections rayon / plans sont réalisés comme ci-dessus :

Pour calculer l'intersection avec le cube, il faut calculer ces 3 intervalles (un par paire de plans / par axe) et vérifier que les intervalles se chevauchent : que leur intersection n'est pas vide.

Il y a aura une intersection si \( [t_{min_i} .. t_{max_i}] \cap [t_{min_j} .. t_{max_j}] \cap [t_{min_k} .. t_{max_k}] \cap [0 .. t_{max}] \neq \phi \)

cube aligné sur les axes

c'est la meme chose, mais les axes sont implicitement \( \vec{x}, \vec{y}, \vec{z} \). Les produits scalaires d'un vecteur \( \vec{v} \) avec les axes d'un repère se simplifient :

\[ \vec{x} \cdot \vec{v} = \begin{bmatrix} 1\\ 0\\ 0 \end{bmatrix} \cdot \begin{bmatrix} v_x\\ v_y\\ v_z \end{bmatrix} = v_x, \, \vec{y} \cdot \vec{v} = \begin{bmatrix} 0\\ 1\\ 0 \end{bmatrix} \cdot \begin{bmatrix} v_x\\ v_y\\ v_z \end{bmatrix} = v_y, \mbox{ et } \vec{z} \cdot \vec{v} = \begin{bmatrix} 0\\ 0\\ 1 \end{bmatrix} \cdot \begin{bmatrix} v_x\\ v_y\\ v_z \end{bmatrix} = v_z \]

au final, ce n'est pas si compliqué :

bool intersect( const Point& pmin, const Point& pmax, const Ray& ray )
{
Point rmin= pmin;
Point rmax= pmax;
// verifier la direction du rayon, sur chaque axe
// echanger tmin, tmax, si la direction est < 0
if(ray.d.x < 0) std::swap(rmin.x, rmax.x); // rmin.x <= rmax.x
if(ray.d.y < 0) std::swap(rmin.y, rmax.y); // rmin.y <= rmax.y
if(ray.d.z < 0) std::swap(rmin.z, rmax.z); // rmin.z <= rmax.z
// intersection avec les plans
Vector invd= Vector(1 / ray.d.x, 1 / ray.d.y, 1 / ray.d.z);
Vector dmin= (rmin - ray.o) * invd;
Vector dmax= (rmax - ray.o) * invd;
// intersection des 4 intervalles... 3 pour les paires de plans + intervalle du rayon
float tmin= std::max(dmin.z, std::max(dmin.y, std::max(dmin.x, float(0))));
float tmax= std::min(dmax.z, std::min(dmax.y, std::min(dmax.x, ray.tmax)));
// si l'intervalle est valide, il y a intersection
return (tmin <= tmax);
}
Point min(const Point &a, const Point &b)
renvoie la plus petite composante de chaque point. x, y, z= min(a.x, b.x), min(a.y,...
Definition: vec.cpp:30

il y a encore quelques astuces de calculs pour éviter les problèmes lorsque la direction du rayon est parallèle à un axe : par exemple pourquoi le code précédent multiplie par l'inverse d'une valeur au lieu de calculer directement la division...

sphere

Pour une sphère de centre \( c \), et de rayon \( r \), même stratégie : les points de l'espace sont sur la sphère s'ils se trouvent à la bonne distance du centre :

\[ \begin{eqnarray*} | p - c | & = & r \\ | p - c | - r & = & 0 \\ | p - c |^2 - r^2 & = & 0\\ (p - c) \cdot (p - c) - r^2 & = & 0 \end{eqnarray*} \]

rappel : on a utilisé une relation entre le produit scalaire et (le carré de) la longueur du vecteur : \( \vec{u} \cdot \vec{u} = |u|^2 \)

il ne reste plus qu'à trouver la valeur de \( t \) pour que \( p(t) \), le point sur le rayon soit aussi sur la sphère :

\[ \begin{eqnarray*} (p(t) - c) \cdot (p(t) - c) - r^2 & = & 0\\ (o + t\vec{d} - c) \cdot (o + t\vec{d} - c) - r^2 & = & 0\\ ((o - c) + t\vec{d}) \cdot ((o - c) + t\vec{d}) - r^2 & = & 0\\ (\vec{d} \cdot \vec{d}) t^2 + 2\vec{d} \cdot (o - c) t + (o - c) \cdot (o - c) - r^2 & = & 0\\ \end{eqnarray*} \]

il faut relire attentivement ce résultat, mais il est sous une forme assez simple au final :

\[ \begin{eqnarray*} a t^2 + b t + k & = & 0\\ a & = & \vec{d} \cdot \vec{d}\\ b & = & 2\vec{d} \cdot (o - c)\\ k & = & (o - c) \cdot (o - c) - r^2 \end{eqnarray*} \]

il ne reste plus qu'à calculer les zeros du polynome : les détails sont sur wikipedia et ici et ce résultat est assez intuitif : une droite peut passer à coté de la sphère, la toucher en un seul point, ou la traverser en 2 points.

si \( b^2 - 4ak > 0 \), les solutions s'écrivent :

\[ \begin{eqnarray*} t_1 & = & \frac{-b + \sqrt{b^2 - 4ak}}{2a}\\ t_2 & = & \frac{-b - \sqrt{b^2 - 4ak}}{2a}\\ \end{eqnarray*} \]

Par contre, il ne faut pas oublier que l'on ne veut que les intersections avec le rayon, pas les intersections avec la droite. il faut donc aussi vérifier le signe des solutions \( t_1 > 0 \) et \( t_2 > 0 \)

pour les curieux : on peut gagner pas mal de temps en ne calculant qu'une seule solution, et en normalisant \( \vec{d} \) à l'avance, \( | \vec{d} |= 1 \) et aussi \(| \vec{d} |^2 = \vec{d} \cdot \vec{d} = a = 1\). Selon le cas, on sait à l'avance que l'origine du rayon se trouve à l'exterieur de la sphère, et il suffit de calculer la plus petite racine \( > 0 \).

triangle

Il y a plusieurs manières de faire ce calcul. par exemple, calculer l'intersection du rayon et du plan qui porte le triangle (représenté par un sommet du triangle et sa normale géométrique, \( a \) et \( \vec{n}= \vec{ab} \times \vec{ac} \), puis vérifier que le point du plan est aussi à l'intérieur du triangle.

Il faut aussi se rappler que l'on veut les coordonnées barycentriques du point d'intersection pour obtenir les memes informations que la rasterization / fragmentation, mais surtout pour pouvoir interpoler les attibuts des sommets du triangle.

du coup, on peut représenter un point dans le plan du triangle \( abc \) à l'aide des coordonnées barycentriques :

\[ p(\alpha, \beta, \gamma) = \alpha a + \beta b + \gamma c \]

mais les points à l'intérieur du triangle vérifient quelques propriétés supplémentaires :

\[ \begin{eqnarray*} \alpha & \geq & 0 \mbox{ et }\alpha \leq 1\\ \beta & \geq & 0 \mbox{ et }\beta \leq 1\\ \gamma & \geq & 0 \mbox{ et }\gamma \leq 1\\ \alpha + \beta + \gamma & =& 1 \end{eqnarray*} \]

\( \alpha \) peut être recalculée en fonction des 2 autres : \( \alpha= 1 - \beta - \gamma \) et on utilise la forme simplifiée :

\[ p(\beta, \gamma) = (1 - \beta - \gamma) a + \beta b + \gamma c \]

remarque : il existe plusieurs conventions pour cette simplification, n'importe laquelle des 3 peut etre calculée implicitement.

il ne reste plus qu'à écrire qu'un point doit être en même temps sur le rayon et dans le triangle :

\[ \begin{eqnarray*} p(t) & = & p(\beta, \gamma)\\ o + t\vec{d} & = & (1 - \beta - \gamma) a + \beta b + \gamma c\\ o + t\vec{d} & = & a + \beta (b - a) + \gamma (c - a)\\ - t\vec{d} + \beta (b - a) + \gamma (c - a) & = & (o - a) \\ \end{eqnarray*} \]

on obtient un système linéaire, 3 équations et 3 inconnues ( \( t, \beta, \gamma \)), une solution élégante n'utilisant que des produits scalaires et vectoriels est proposée dans cet article :

en pratique

Comment calculer une image avec du lancer de rayons et des triangles ? (et une camera) (et une source de lumiere) (et des matieres...)

int main( )
{
const char *orbiter_filename= "data/cornell_orbiter.txt";
const char *mesh_filename= "data/cornell.obj";
// cree l'image resultat
Image image(1024, 768);
// charge une camera
Orbiter camera;
if(camera.read_orbiter(orbiter_filename) < 0)
return 1; // erreur, pas de camera, pas d'image
// charge des triangles
Mesh mesh= read_mesh(mesh_filename);
// recupere les triangles du mesh
std::vector<Triangle> triangles;
{
int n= mesh.triangle_count();
for(int i= 0; i < n; i++)
triangles.push_back( Triangle(mesh.triangle(i), i) );
assert(int(triangles.size() > 0);
// pas de triangles, pas d'image...
}
// recupere les transformations standards
camera.projection(image.width(), image.height(), 45);
Transform model= Identity();
Transform view= camera.view();
Transform projection= camera.projection();
Transform viewport= camera.viewport();
Transform inv= Inverse(viewport * projection * view * model);
// c'est parti, parcours tous les pixels de l'image
for(int y= 0; y < image.height(); y++)
for(int x= 0; x < image.width(); x++)
{
// generer le rayon sur le centre du pixel (x, y)
Point origine= inv(Point(x + .5f, y + .5f, 0));
Point extremite= inv(Point(x + .5f, y + .5f, 1));
Ray ray(origine, extremite);
// calculer les intersections avec tous les triangles
Hit hit;
hit.t= 1; // intersections dans l'intervalle [0 .. 1]
for(int i= 0; i < int(triangles.size()); i++)
{
Hit h= triangles[i].intersect(ray, hit.t);
if(h)
// ne conserve que l'intersection la plus proche de l'origine du rayon
// triangle::intersect(ray, tmax= hit.t) ne renvoie vrai que si t < hit.t
hit= h;
/* remarque : Hit() definit un operateur de conversion vers bool qui renvoie vrai si hit.triangle_id != -1
if(h) { ... }
est equivalent a :
if(h.triangle_id != -1) { ... }
et on peut ecrire tout ca de maniere encore plus compacte :
if(Hit h= triangles[i].intersect(ray, hit.t))
hit= h;
*/
}
if(hit)
// pixel rouge en cas d'intersection
image(x, y)= Red();
}
// enregistre l'image
write_image(image, "render.png");
return 0;
}
representation d'une image.
Definition: image.h:21
representation d'un objet / maillage.
Definition: mesh.h:112
Mesh & triangle(const unsigned int a, const unsigned int b, const unsigned int c)
Definition: mesh.cpp:190
int triangle_count() const
renvoie le nombre de triangles.
Definition: mesh.cpp:433
representation de la camera, type orbiter, placee sur une sphere autour du centre de l'objet.
Definition: orbiter.h:17
int read_orbiter(const char *filename)
relit la position de l'orbiter depuis un fichier texte.
Definition: orbiter.cpp:116
Transform viewport() const
renvoie la transformation viewport actuelle. doit etre initialise par projection(width,...
Definition: orbiter.cpp:83
Transform projection(const int width, const int height, const float fov)
fixe la projection reglee pour une image d'aspect width / height, et une demi ouverture de fov degres...
Definition: orbiter.cpp:47
Transform view() const
renvoie la transformation vue.
Definition: orbiter.cpp:40
Color Red()
utilitaire. renvoie une couleur rouge.
Definition: color.cpp:41
int write_image(const Image &image, const char *filename)
enregistre une image dans un fichier png.
Definition: image_io.cpp:85
Transform Inverse(const Transform &m)
renvoie l'inverse de la matrice.
Definition: mat.cpp:197
Transform Identity()
construit la transformation identite.
Definition: mat.cpp:187
Mesh read_mesh(const char *filename)
charge un fichier wavefront .obj et renvoie un mesh compose de triangles non indexes....
Definition: wavefront.cpp:14
intersection avec un triangle.
Definition: tuto_bvh2.cpp:33
representation d'une transformation, une matrice 4x4, organisee par ligne / row major.
Definition: mat.h:21
triangle pour le bvh, cf fonction bounds() et intersect().
Definition: tuto_bvh.cpp:84

et voila, pas bien compliqué !! malgré des explications un peu longues...

remarque : la boucle qui calcule les intersections fonctionne de la meme maniere quelque soit le type des objets :

hit.t= 1 // ou \inf si le rayon est une demi droite sans extremite
pour chaque objet
si la droite touche l''objet
trouver l''intersection la plus proche de l''origine du rayon qui appartient a l''intervalle [0 hit.t]
// sinon pas d''intersection avec le rayon...
hit.XX= informations sur l''intersection
hit.t= intersection la plus proche dans l''intervalle [0 hit.t]

dans le code d'exemple, la fonction Triangle::intersect() ne renvoie vrai que si une intersection valide (dans l'intervalle [0 .. hit.t]) existe, ce qui simplifie pas mal l'ecriture de la boucle...

hit.t= 1 // ou \inf si le rayon est une demi droite sans extremite
pour chaque objet
si intersection dans l''intervalle [0 hit.t]
hit= intersection

bon, afficher des pixels rouges, ne permet pas vraiment de vérifier que les intersections fonctionnent correctement. on peut construire une couleur avec le résultat de l'intersection :

if(hit)
// coordonnees barycentriques de l'intersection
image(x, y)= Color(1 - hit.u - hit.v, hit.u, hit.v);
representation d'une couleur (rgba) transparente ou opaque.
Definition: color.h:14

le sommet \( a \) des triangles apparait en rouge, le sommet \( b \) en vert et le sommet \( c \) en bleu...

matière diffuse, source de lumière, et lumière réfléchie...

pour calculer la couleur des pixels en fonction de la matière du triangle et de sa normale interpolée, il faut ajouter quelques éléments :

pour interpoler la normale au point d'intersection en fonction des normales des sommets du triangle, il suffit de ré-utiliser les coordonnées barycentriques du point d'intersection avec le triangle, exactement comme le pipeline openGL :

\[ \vec{n}(\beta, \gamma) = (1 - \beta - \gamma) \vec{n_a} + \beta \vec{n_b} + \gamma \vec{n_c} \]

Vector normal( const Mesh& mesh, const Hit& hit )
{
// recuperer le triangle complet dans le mesh
const TriangleData& data= mesh.triangle(hit.triangle_id);
// interpoler la normale avec les coordonnées barycentriques du point d'intersection
float w= 1 - hit.u - hit.v;
Vector n= w * data.na + hit.u * data.nb + hit.v * data.nc;
return normalize(n);
}
Vector normalize(const Vector &v)
renvoie un vecteur unitaire / longueur == 1.
Definition: vec.cpp:123
representation d'un triangle.
Definition: mesh.h:95
vec3 nc
normales
Definition: mesh.h:97

pour récupérer la couleur de la matière :

Color diffuse_color( const Mesh& mesh, const Hit& hit )
{
const Material& material= mesh.triangle_material(hit.triangle_id);
return material.diffuse;
}
const Material & triangle_material(const unsigned int id) const
renvoie la matiere d'un triangle.
Definition: mesh.cpp:296
Color diffuse
couleur diffuse / de base.
Definition: materials.h:17

pour trouver les sources de lumière, il faut parcourir les triangles du Mesh et vérifier que leur matière émet de la lumière. Comme c'est un peu long, ce sera fait une seule fois au début du programme :

struct Source
{
Point s;
Color emission;
};
// recuperer les sources de lumière dans le mesh
std::vector<Source> sources;
int n= mesh.triangle_count();
for(int i= 0; i < n; i++)
{
const Material& material= mesh.triangle_material(i);
if(material.emission.r + material.emission.g + material.emission.b > 0)
{
// utiliser le centre du triangle comme source de lumière
const TriangleData& data= mesh.triangle(i);
Point p= (data.a + data.b + data.c) / 3;
sources.push_back( { p, material.emission } );
}
}
printf("%d sources\n", int(sources.size()));
assert(sources.size() > 0);
void printf(Text &text, const int px, const int py, const char *format,...)
affiche un texte a la position x, y. meme utilisation que printf().
Definition: text.cpp:140
Color emission
pour une source de lumiere.
Definition: materials.h:19
vec3 c
positions
Definition: mesh.h:96

Il ne reste plus qu'à calculer la lumière réfléchie par une matière diffuse, comme dans le tp précédent (cf lumière et matière et shader et brdf)

if(hit)
{
// position et emission de la (premiere) source de lumiere
Point s= sources[0].s;
Color emission= sources[0].emission;
// position du point d'intersection
Point p= ray.o + hit.t * ray.d;
// direction de p vers la source s
Vector l= normalize(Vector(p, s));
// interpoler la normale au point d'intersection
Vector pn= normal(mesh, hit);
// calculer la lumiere reflechie vers la camera / l'origine du rayon
float cos_theta= std::abs(dot(pn, l));
Color fr= diffuse_color(mesh, hit) / M_PI;
Color color= emission * fr * cos_theta
image(x, y)= color;
}
float dot(const Vector &u, const Vector &v)
renvoie le produit scalaire de 2 vecteurs.
Definition: vec.cpp:137

et voila :

il ne reste plus qu'à ajouter les ombres...

indication : il suffit de construire un rayon entre p et s, le point sur la source, et de vérifier qu'il n'y a pas d'intersection entre eux, pour que la lumière arrive au point p, sinon p est à l'ombre...

et alors ?

Et avec d'autres objets, qu'est ce qui change ? Il faut écrire les fonctions d'intersection rayon / objet pour chaque cas.

openGL dessine les triangles "tout seul", par contre manipuler l'api d'openGL est probablement plus compliqué. mais on profite des processeurs parallèles de la carte graphique qui sont particulièrement rapides.

pour les curieux : oui, bien sur, on peut écrire un shader openGL qui calcule les intersections rayons / triangles de la même manière, cf tuto_raytrace_fragment.cpp et raytrace.glsl

la version en ligne de PBRT (un livre de référence sur le lancer rayons et les calculs réalistes) propose également les tests d'intersections pour plusieurs objets :

Les fonctions implicites permettent aussi de faire énormement de choses, regardez quelque exemples sur shadertoy.com, presque tous les shaders utilisent une variante du lancer de rayon, le lancer de sphère / sphere tracing. En attendant le cours de M2 d'Eric Galin sur le sujet, vous pouvez jeter un ou plusieurs yeux sur le blog d'Inigo Quilez, le créateur de shadertoy.

Mais le principal problème du lancer de rayon est sa lenteur, du coup il est nécessaire de réfléchir un peu pour calculer des intersections sur plus de 100 objets. cf le lancer de rayons, ça rame ? ou pas ?.

annexe

il manque les définitions des structures Hit et Triangle :

// intersection avec un triangle
struct Hit
{
float t; // p(t)= o + td, position du point d'intersection sur le rayon
float u, v; // p(u, v), position du point d'intersection sur le triangle
int triangle_id; // indice du triangle dans le mesh
// pas d'intersection
Hit( ) : t(FLT_MAX), u(), v(), triangle_id(-1) {}
// intersection
Hit( const float _t, const float _u, const float _v, const int _id ) : t(_t), u(_u), v(_v), triangle_id(_id) {}
// renvoie vrai si l'intersection est valide
operator bool ( ) { return (triangle_id != -1); }
};
struct Triangle
{
Point p; // sommet a du triangle
Vector e1, e2; // aretes ab, ac du triangle
int id; // indice du triangle dans le mesh, permet de recuperer sa matiere, et les attributs des sommets (normales, etc)
Triangle( const TriangleData& data, const int _id ) : p(data.a), e1(Vector(data.a, data.b)), e2(Vector(data.a, data.c)), id(_id) {}
/* calcule l'intersection ray/triangle
cf "fast, minimum storage ray-triangle intersection"
renvoie faux s'il n'y a pas d'intersection valide (une intersection peut exister mais peut ne pas se trouver dans l'intervalle [0 tmax] du rayon.)
renvoie vrai + les coordonnees barycentriques (u, v) du point d'intersection + sa position le long du rayon (t).
convention barycentrique : p(u, v)= (1 - u - v) * a + u * b + v * c
*/
Hit intersect( const Ray &ray, const float tmax ) const
{
Vector pvec= cross(ray.d, e2);
float det= dot(e1, pvec);
float inv_det= 1 / det;
Vector tvec(p, ray.o);
float u= dot(tvec, pvec) * inv_det;
if(u < 0 || u > 1) return Hit(); // pas d'intersection, u doit etre entre 0 et 1
Vector qvec= cross(tvec, e1);
float v= dot(ray.d, qvec) * inv_det;
if(v < 0 || u + v > 1) return Hit(); // pas d'intersection, v doit etre entre 0 et 1-u pour que w= 1-u-v soit aussi entre 0 et 1
float t= dot(e2, qvec) * inv_det;
if(t < 0 || t > tmax) return Hit(); // pas d'intersection, t doit etre entre 0 et tmax
return Hit(t, u, v, id); // intersection valide p(u, v)= (1-u-v)*a + u*b + v*c
}
};
Vector cross(const Vector &u, const Vector &v)
renvoie le produit vectoriel de 2 vecteurs.
Definition: vec.cpp:129

le code complet est dans tuto_rayons.cpp