M2 - Images
TP 1 - Lancer de rayons
Travail à rendre : pour la semaine du 14/12/2009
La
première partie à rendre concerne la réalisation
du lancer de rayon minimal, ainsi la prise en compte des sources de
lumières et des matières. Vous rendrez vos sources et un
makefile ainsi qu'un rapport de quelques pages.
Le document décrivant votre travail sur cette
première
partie s'attachera à décrire précisement les
repères (et les transformations associées) dans lesquels
vous avez choisi de travailler, ainsi que la représentation de
matière et les calculs d'éclairement que vous avez
choisis.
Vous apporterez un soin particulier à expliquer votre
méthode de génération des rayons (dans quel
espace, homogène ou réel, transformations
associées, etc.), ainsi qu'aux calculs liés aux modèles d'éclairements.
Vous rendrez votre rapport en pdf et une archive .tar.gz de vos sources (ainsi qu'un makefile).
Le
rapport et l'archive porterons le nom des personnes associées au
tp. Vous transmettrez le rapport et l'archive en utilisant le mail de l'université en précisant vos noms et numéros d'étudiants.
Ce TP propose de construire un lancer de rayons, élément
par élément.
Partie 1 : intersections primitive - rayon
Une des premières choses à faire est
de décrire un objet. Pour un lancer de rayons, un objet est
principalement défini par sa position dans l'espace et sa
fonction d'intersection (qui permettra de le visualiser).
Pour avancer rapidement vers un "système"
fonctionnel, commencons par un type d'objet simple comme une
sphère, un cube, etc.
Q1: représentez une sphère de centre
(0, 0, 0, 1)T et de rayon R.
Q2. écrivez une fonction d'intersection avec
un rayon passé en paramètre capable de renvoyer
l'existence de l'intersection ainsi que son abscisse, notée t.
Un rayon se définit par une origine et une
direction. Un point p( t )
le long du rayon se définit par : p( t ) = origine + t . direction
exemple : int
sphere_intersecter( const float *origine, const float *direction, float
*t )
Partie 2 : placer des objets dans la scène
Maintenant que nous avons des objets, il faut
construire une scène à visualiser. Il suffit de
positionner le repère local
de chaque objet dans le repère global de la scène.
Q1: attachez un changement de repère à
votre objet afin de le positionner dans la scène.
Q2: représentez un ensemble de primitives :
la "scène", écrivez la fonction d'intersection entre un
rayon et la scène :
exemple : int
scene_intersecter( const SCENE *scene, const float *origine, const
float *direction, float *t )
rappel : lorsque plusieurs objets de la scène
intersectent le rayon, seule l'intersection la plus proche de l'origine
du rayon est renvoyée.
Partie 3 : définir une caméra
Le dernier élément d'un lancer de
rayon est la caméra.
Q1. Une camera est placée et orientée
dans le repère global de la scène, comment
représenter cette transformation ?
Q2. Une camera doit également définir
sa projection : le passage du reprère global de la scène,
en 3D, à un plan image, en 2D. Comment représenter cette
transformation ?
Q3. Un point 3D placé dans son
repère local doit subir plusieurs transformations avant
d'être projetté par la caméra, lesquelles ?
Q4. Quelles les caractéristiques de l'espace
projectif de la caméra ? Comment retrouver des
coordonnées exploitables ?
Q5. Il manque, a priori, une dernière
étape : passer du repère projectif de la caméra au
plan image permettant de visualiser le résultat final.
Définissez cette dernière transformation.
Q6. Combien de matrices sont utilisées pour
représenter les transformations nécessaires ?
Partie 4 : générer un rayon
Nous pouvons presque commencer à calculer
l'image. Il
ne reste plus qu'à construire un rayon pour chaque
pixel de l'image. Le plus simple est de le créer dans le
repère projectif de la camera, puis de le passer dans le
repère global de la scène.
Q1: Ecrivez une fonction qui
génère un rayon pour le pixel (x, y) dans le
repère de la scène.
exemple : void
camera_generer_rayon(const CAMERA *camera, float x, float y, float
*origine, float *direction)
Q2. Modifiez la fonction d'intersection d'un
rayon et la scène (cf. Partie 2) afin de transformer le rayon
pour l'exprimer dans le repère local de chaque l'objet, ce
qui vous
permettra d'utiliser les fonctions d'intersection de la Partie 1.
Partie 5 : ... un peu d'ombre
Les différents éléments du
lancer de rayon sont maintenant disponibles, il ne reste plus
qu'à calculer une image. L'information la plus importante pour
"comprendre" la structure d'une scène 3D est la présence
d'ombres qui permettent de situer les objets les uns par rapport aux
autres.
Il faut donc, en premier lieu, définir une
source de lumière, un point placé dans le repère
global, pour déterminer si un objet est éclairé ou
à l'ombre.
Q1. ajoutez une source de lumière
à la description de votre scène. Pour un lancer de rayon,
une source se définit principalement par une position et sa
"capacité" à éclairer un point :
float
source_eclairer( const SOURCE *source, const float *point );
Q2. écrivez la fonction
source_eclairer() pour une source ponctuelle définie par une
position dans le repère de la scène, cette fonction
renvoie 1 si le point est éclairé et 0 sinon. Il suffit
de tester la visibilité d'un point de la source et du point
à éclairer en lançant un rayon, s'il y a une
intersection entre les deux points, ils ne sont pas visibles et le
point est à l'ombre.
Q3. vous allez constater que les
résultats sont "bruités", certains pixels sont à
l'ombre alors qu'ils devraient être éclairés, et
vice versa. Une solution simple consiste à décaler
légèrement l'origine et l'extrémité du
rayon. L'origine est "poussée" le long de la normale de l'objet
et l'extrémité est éloignée du point sur la
source (en direction du point). Pour obtenir la normale de la surface
sous le point "éclairé", vous devrez modifier les
fonctions d'intersection pour renvoyer également cette
information :
int
sphere_intersecter( const float *origine, const float *direction, float *normale, float *t )
int
scene_intersecter( const SCENE *scene, const float *origine, const
float *direction, float *normale, float *t )
remarque : utilisez vec3_add_const_mul(point, point, k,
direction) de sdlkit qui réalise le cacul
nécessaire p= p +
k*direction
Partie 6 : un peu de lumière ...
Les images obtenues jusqu'à présent
sont relativement pauvres, les objets ont tous le même aspect et
l'influence de la lumière se limite à du noir ou du blanc.
La physique qui régit le comportement de la
lumière permet d'ajouter facilement quelques "subtilités"
: selon l'orientation de la surface sur laquelle il est posé un
point ne reçoit pas la même quantité
d'énergie : elle est maximale lorsque la normale du point
éclairé est orientée vers la source de
lumière et décroit proportionnellement au cosinus de
l'angle (entre la normale et la direction de la source) dans les autres
cas.
La quantité de lumière
réçue par un point p est simplement : N . L, pour N, la normale de la surface au
point éclairé, et L,
la direction de la source de lumière observée par le
point. N et L doivent être des
vecteurs unitaires afin d'évaluer le cosinus de l'angle entre
les deux vecteurs.
Q1. modifiez la fonction source_eclairer pour
calculer l'énergie reçue :
float source_eclairer( const SOURCE
*source, const float *point, const float *normale );
remarque : utilisez les fonctions vec3_sub( ), vec3_norm( ) et vec3_dot( ) de sdlkit pour
faire les calculs.
Encore plus de lumière
Comment "gérer" plusieurs sources de
lumière ? La encore, la physique fournit une réponse
simple : si un point est éclairé par plusieurs sources de
lumière, il reçoit la somme des énergies
émises par chaque source.
Q2. ajoutez un tableau de source de lumières
à la description de votre scène et écrivez la
fonction :
float
scene_eclairer( const SCENE *scene, const float *point, const float
*normale );
qui renvoie la somme des énergies
reçues par 'point' orienté selon 'normale'.
Un problème va apparaître lors de la
visualisation des images utilisant plusieurs sources de
lumières, que se passe-t-il lorsque l'énergie recue par
un point dépasse 1 (ou 255, pour une image RGB classique) ? Les
appareils photos et les caméras ont le même
problème : certaines zones peuvent être sous
exposées ou sur-exposées !
Le plus simple est de limiter la somme
d'énergie reçue à la valeur affichable. Pour faire
mieux, il faut utiliser une reproduction de tons qui converti une image
en dynamique complète (ou image HDR, avec une énergie
associée à chaque pixel, au lieu d'une couleur) en une
image faible dynamique affichable sur un écran.
Des lumières différentes
Pour l'instant toutes les sources de lumière
émettent la même quantité d'énergie, il suffit d'ajouter la quantité d'energie émise e dans la description de la source de lumière :
Q3. stockez la quantité d'énergie e émise par la source et modifiez les calculs de point_eclairer :
remplacer (N . L) par e * (N . L)
Partie 7 : de la couleur
Votre lancer de rayon doit être maintenant
capable de produire des images présentant plusieurs objets
éclairés par plusieurs sources de lumières
différentes. Il manque quand même, un
élément essentiel : la couleur. Une approximation
grossière, mais courante, consiste a manipuler 3 bandes
d'énergies, une pour le rouge, une autre pour le vert et une
dernière pour le bleu.
Q1. modifiez la description des sources de
lumières, au lieu de ne stocker qu'une quantité
d'énergie émise, il faut en stocker trois : le rouge, le
vert et le bleu.
Q2. modifiez les fonctions scene_eclairer() et
source_eclairer(). Au lieu de renvoyer une énergie, elles
doivent maintenant renvoyer 3 énergies.
Partie 8 : des matières variées, diffuses,
spéculaires, réfléchissantes, ...
Pour obtenir des images plus variées, la
prochaine étape consiste à simuler des aspects de surface
différents selon les objets afin d'obtenir des surfaces
diffuses, réfléchissantes ou miroirs. L'aspect d'une
surface est représentée par une fonction, notée
BRDF(oeil, p, source), qui représente la fraction
d'énergie réfléchie entre les directions oeil et
source pour le point p (posé sur une surface de normale N). Les
directions oeil et source sont des vecteurs unitaires dont l'origine
est p.
Q1. comment représenter les différents
comportements ? diffus, réfléchissant, miroir ?
Comment décrire une
matière qui est 30% diffuse et 60% réflechissante ?
Proposez une structure MATIERE
que vous ajouterez à la structure primitive (qui permet de
placer un objet dans le repère de la scène).
Q2. écrivez la fonction :
float matiere_eclairer( const MATIERE *matiere, const float *oeil, const float *p, const float *N, const float *source );
Faut-il modifier quelquechose pour tenir compte d'une couleur de matière ?
Q2. modifiez source_eclairer() et scene_eclairer() afin d'utiliser matiere_eclairer().
Annexes
Calculs avec vec.h et mat44.h
Les fonctions de manipulation de vecteurs se trouvent dans vec.h et celles travaillant sur les matrices dans mat44.h.
Opérations sur les vecteurs :
Il y a plusieurs types de vecteurs : 2D, 3D et 4D.
Chaque groupe de fonctions est prefixé par la taille des
vecteurs manipulés, vec2 pour les vecteurs 2D, vec3 pour les
vecteurs 3D, etc. Les vecteurs sont représentés par des
tableaux de float, vous pouvez utiliser les types
prédéfinis VEC2, VEC3 et VEC (pour 4D).
Les fonctions prefixées par vecX_* existent pour les différentes tailles de vecteurs.
Création :
- void vecX_init(float *v, float x, float y [, float z [, float w]]); // création d'un vecteur dont les composantes sont connues.
- void vecX_zero(float *v); // création d'un vecteur null
- vecX_copy(float *dst, const float *v); // création d'un vecteur par copie d'un autre vecteur.
Addition :
- void vecX_add(float *dst, const float *v1, const float *v2);
- void vecX_sub(float *dst, const float *v1, const float *v2);
- void vecX_neg(float *dst, const float *v); // vecteur opposé
Produit :
- void vecX_mul(float *dst, const float *v1, const float *v2); // produit composante par composante
- void vecX_const_mul(float *dst, const float k, const float *v); // produit par un scalaire k
- vecX_dot(const float *u, const float *v); // produit scalaire
- vecX_cross(const float *u, const float *v); // produit vectoriel
Autres :
- float vecX_length(const float *v); // longueur du vecteur
- void vecX_norm(float *dst, const float *v); // normalisation d'un vecteur
Opérations sur les matrices homogènes :
Seules les matrices homogènes 4x4 sont
définies. Elles sont représentées par un tableau
de 16 float.
Création :
- void mat44_identity(float *a); // crée une matrice identité
- void mat44_zero(float *a); // crée une matrice nulle
Addition :
- void mat44_add(float *a, const float *b, const float *c); // A= B + C
- void mat44_sub(float *a, const float *b, const float *c); // A= B - C
Produit :
- void mat44_mul(float *a, const float *b, const float *c); // A= C * B !!
- void mat44_const_mul(float *a, float k, const float *b); // A= k * B
- void mat44_compose(float *a, const float *b); // A= A * B
Produit par un vecteur :
- void mat44_mul_vec(float *v, const float *a, const float *u); // v= A * u
- void mat44_compose_vec(const float *a, float *u); // u= A * u
Autres :
- int mat44_inverse(float *A, const float *B); // A= B-1
- void mat44_print(const float *a);
Transformation géométriques (construit une matrice A représentant une transformation) :
- void mat44_scale(float *a, float x, float y, float z); // homothétie
- void mat44_translate(float *a, float x, float y, float z); // translation par le vecteur (x, y, z)T
- void mat44_rotate(float *a, float angle, float x, float y, float z); // rotation de angle degrés autour de l'axe (x, y, z)T
- void mat44_lookat(float *a, const float *eye, const float *center, const float *up); // positionne la caméra
- void mat44_frustum(float *a, float left, float right, float bottom, float top, float near, float far); // definit le volume projetté / observé par la caméra
- void mat44_perspective(float *a, float fovy, float aspect, float near, float far); // définit une projection perspective
Les explications sur les différentes transformations sont dans le "openGL Programming Guide" / Chapitre 4. Les matrices construites par ces fonctions sont au format openGL, il est possible de les charger directement avec glLoadMatrixf().
Annexe : enregistrer une image HDR
installez imgtoy dans un répertoire
cd XXX
make
make install
vous pouvez utiliser ~/local/bin/viewhdr pour visualiser les images hdr et les convertir en tga.
exemple de création, remplissage et écriture d'une image HDR :
#include <assert.h>
#include "img.h"
#include "hdr.h"
void img_set_pix( IMG *img, const int x, const int y,
const float r, const float v, const float b )
{
assert(img != NULL);
assert(x >= 0 && x < img->largeur);
assert(y >= 0 && y < img->hauteur);
const unsigned int pix= (img->hauteur -1 - y) *
img->largeur * img->channels + x * img->channels;
img->dataf[pix]= r;
img->dataf[pix +1]= v;
img->dataf[pix +2]= b;
}
... quelque part dans le programme :
/* creer l'image resultat */
IMG *img= new_img_datafloat(largeur, hauteur);
/* ecrire des pixels dans l'image */
int y, x;
for(y= 0; y < hauteur; y++)
for(x= 0; x < largeur; x++)
img_set_pixel(img, x, y, 3.0f, 2.0f, 1.0f);
/* sauver l'image sous image.hdr */
hdr_write(img, "image.hdr");
free_img(img);