Les transformations de visualisation sont les transformations géométriques qui permettent d'obtenir une représentation bidimensionnelle (sous forme d'image) d'objets définis dans l'espace à trois dimensions.
Les transformations géométriques utilisées sont les transformations affines et la projection perspective.
Ces transformations sont mises en œuvre dans l'algorithme de rendu par objet, qui est fréquemment programmé sur les processeurs graphiques (GPU) via l'interface de programmation OpenGL.
Le système considéré est constitué de différents objets. L'ensemble des objets constitue la scène. Le système de formation de l'image est appelé la caméra (au sens de chambre photographique, camera en anglais). Dans ce document, on ne s'intéresse qu'à l'aspect géométrique du problème, pas aux problèmes d'éclairage ni au remplissage final des pixels (rastérisation).
Le positionnement des objets dans l'espace se fait au moyen de transformations affines. Certains objets peuvent être positionnés de manière absolue, d'autres sont positionnés relativement à d'autres objets. La méthode matricielle permet de faire cela facilement.
Les points caractéristiques des objets, par exemple les sommets des polyèdres, sont définis par leurs coordonnées homogènes :
On note M la matrice 4x4 de la transformation affine qui permet (par multiplication à gauche) de placer les points dans l'espace. Bien sûr, différents objets peuvent avoir des matrices différentes.
La deuxième étape consiste à positionner et orienter la caméra dans l'espace. Nous adoptons la convention consistant à fixer la caméra à l'origine du repère, sa ligne de visée étant l'axe Oz, dans le sens des Z décroissants. Au lieu de déplacer la caméra, nous appliquons une transformation affine à la scène. En effet, déplacer la caméra par une transformation affine revient à déplacer la scène par la transformation inverse.
Notons V la matrice de visualisation, qui définit la transformation affine appliquée à la scène et permet donc de rendre compte de la position et de l'orientation de la caméra.
À ce stade des transformations, nous obtenons donc :
Le produit VM est appelée matrice de visualisation-modélisation (modelview matrix). Si l'on veut déplacer la caméra sans modifier la scène, on doit changer la matrice V seulement, c'est pourquoi il est préférable de stocker ces deux matrices individuellement.
L'étape suivante est la multiplication par la matrice de perspective. Pour une projection orthoscopique, on saute cette étape.
d est la distance entre le centre de projection et le plan image. Nous allons aussi introduire un plan lointain en z=e, comme indiqué sur la figure suivante :
Figure pleine pagePar convention, seuls les points vérifiant la condition e<z<d seront représentés graphiquement sur l'image finale. Nous verrons plus loin l'intérêt de fixer un plan lointain. On remarque qu'avec la convention de visée adoptée d et e sont négatifs, tout comme z des points représentés.
L'application de la matrice de perspective à un point de coordonnées (x,y,z,1) donne :
Après la division de perspective (division par la 4ième coordonnée), on obtient :
(x',y') sont les coordonnées de la projection dans le plan image. z' contient l'information de distance, qu'il faut garder pour l'algorithme du tampon de profondeur (élimination des points cachés). Cet algorithme consiste à ne retenir, sur chaque pixel de l'image finale, que le point projeté le plus proche, c'est-à-dire celui dont le z est le plus grand (car celui-ci est négatif).
On choisit à présent a et b pour que le plan proche (le plan image) et le plan lointain soient invariants par la transformation de perspective. On obtient :
z' est alors une fonction croissante de z : les points les plus proches ont un z' plus grand (négatif).
On obtient ansi un point (x',y',z') compris entre le plan proche et le plan lointain, dont la projection sur le plan image est une simple projection orthoscopique.
Voici la matrice de perspective obtenue :
Le volume visualisé est déjà limité en z par le plan proche et le plan lointain. On le limite aussi dans les directions x et y. La figure suivante montre comment il est limité dans la direction x :
Figure pleine pageLa largeur du plan image est ainsi Lx. On limite de manière analogue dans la direction y par une hauteur Ly. Après l'application de la projection de perspective, le volume ainsi délimité devient un parallélépipède dont les côtés sont (Lx,Ly,d-e).
Le plan image, c'est-à-dire le plan proche, est centré sur l'axe (Oz). On appliquera donc à la scène représentée la translation qui convient pour que son point central soit à peu près au milieu de l'image (intersection de l'axe Oz avec le plan image).
La réduction des coordonnées (ou normalisation) consiste à appliquer un changement d'échelle de manière à ramener les coordonnées des points dans l'intervalle [-1,1]. L'opération de troncature (clipping), réalisée par le GPU, consiste à éliminer les points dont une des coordonnées est en dehors de l'intervalle [-1,1].
Une matrice de transformation affine effectuant cette réduction est :
La transformation de z effectuée est (lorsque w=1) :
On remarque que d-e>0 car ces deux distances algébriques sont négatives. Les points situés sur le plan lointain (z=e) ont donc une abscisse finale z'=1. Les points du plan proche (z=d) ont une abscisse finale z'=-1). Les points les plus proches de la caméra ont donc un z' plus petit.
On doit finalement multiplier cette matrice par la matrice de perspective calculée plus haut :
La matrice RP est appelée matrice de projection. La projection proprement dite se fait lorsqu'on divise par la 4ième coordonnée w (division de perspective). Cette matrice conduit à des valeurs négatives de la quatrième coordonnée w (car les points représentés ont un z négatif). Dans l'implémentation OpenGL, cela conduit à des points finaux invisibles car le test de troncature suppose des valeurs de w positives. Par ailleurs, le fait que d<0 implique un changement de sens des axes (Ox) et (Oy) : l'axe (0x) apparaît vers la gauche dans l'image finale. Pour éliminer ces inconvénients, il suffit de considérer la matrice opposée :
Le volume visualisé est un tronc de pyramique (pyramid frustum), d'ou le nom frustum donné couramment à cette transformation.
Il faut bien sûr placer l'objet à représenter dans le volume visualisé au moyen d'une translation, ce qui revient à déplacer la caméra avec la translation opposée. Par exemple, si l'objet est initialement défini au voisinage de l'origine, on lui appliquera une translation négative selon l'axe z pour le placer à peu près au milieu du volume visualisé (le tronc de pyramide). Cela revient à éloigner la caméra de l'objet, pour que celui-ci soit dans son champ de vision. Cette transformation de positionnement de l'objet fait partie de la transformation de visualisation définie plus haut.
La délimitation du volume dans la direction z (par les plans proche et lointain) est une nécessité pour plusieurs raisons :
Le tampon de profondeur est en effet constitué d'une matrice d'entiers 32 bits (un élément de la matrice correspond à un pixel sur l'image finale). Le fait de limiter le volume visualisé par un plan lointain permet de garantir une bonne précision absolue sur la profondeur, de manière à pouvoir distinguer la profondeur de deux points qui auraient des profondeurs très proches. Pour cette raison, le plan proche et le plan lointain doivent être choisi de manière à contenir toute la scène intéressante à représenter et le plan lointain ne doit pas être choisi trop loin. Bien sûr, si les objets se déplacent au cours du temps et s'éloignet de la caméra, il faut redéfinir de temps en temps la position du plan lontain.
En pratique, il est plus simple de définir la matrice de projection à partir d'un angle de vue et des proportions de l'image. Soit α le champ angulaire dans la direction y. Plus cet angle est grand, plus l'effet de perspective est marqué. On introduit aussi le raport d'aspect (aspect ratio) défini par :
Les éléments 0,0 et 1,1 de la matrice de projection se calculent de la manière suivante :
Pour finir, voici le résultat de l'application de la matrice de projection à un point (x,y,z,1) :
La division de perspective (division par w') conduit à :
Les coordonnées (x'',y'') sont dans l'intervalle [-1,1]. Dans l'implémentation OpenGL, elles sont converties en coordonnées de pixels après le traitement par le fragment shader.
La coordonnée z'' (la profondeur) est utilisée pour l'algorithme du tampon de profondeur (Z-buffer). L'intervalle [e,d] est transformé en [1,-1] (-1 pour le plan proche, 1 pour le plan lointain). En conséquence, l'élimination des parties cachées se fait en gardant les points dont la valeur de z'' est plus petite (les points les plus proches de la caméra).
Dans l'implémentation OpenGL, les points dont la valeur de x'', y'' ou z'' est en dehors de l'intervalle [-1,1] ne sont pas tracés (opération de troncature). En fin de traitement, les coordonnées (x'',y'') sont converties en coordonnées de pixel (nombres entiers) pour l'image finale. La profondeur z'' est ramenée dans l'intervalle [0,1] par une transformation affine (0 correspond au plan proche). Pour chaque pixel, la profondeur du point le plus plus proche de la caméra est stockée dans le tampon de profondeur. Si on souhaite consulter ce tampon, il faut savoir que ses valeurs sont dans l'intervalle [0,1] (intervalle par défaut, qu'il est possible de modifier). L'élimination des parties cachés au moyen du tampon de profondeur se fait en réalité sur les valeurs de profondeur dans l'intervalle [0,1]. Cependant, le passage de l'intervalle [-1,1] à [0,1] ne change pas l'ordre des valeurs.
Remarque : il est important que les valeurs de z'' à l'issu de la projection et de la division de perspective soient dans l'intervalle [-1,1], car cet intervalle correspond aux points qui sont conservés dans l'opération de troncature (clipping). Dans certains textes sur OpenGL, il y a une confusion entre cet intervalle et l'intervalle final [0,1]. Certains auteurs définissent la matrice de projection pour qu'elle donne z'' dans l'intervalle [0,1]. Ce choix est une erreur (à moins de modifier le réglage de troncature par défaut d'OpenGL) car les points dont la valeur de z'' est dans l'intervalle [-1,0] ne seraient pas éliminés.
La bibliothèque de fonctions de calculs matriciels GLM comporte des fonctions permettant de créer des matrices de projection, en particulier la fonction suivante :
glm::perspective(fov,ratio,near,far)
fov est l'angle de champ en radians, ratio est le rapport d'aspect de l'image (largeur sur hauteur), near est la distance du plan proche et far est la distance du plan lointain (c.a.d. les valeurs absolues de d et de e). Tous ces arguments sont des flottants 32 bits.
Un exemple permet de tester cette fonction. Considérons la matrice de projection obtenue par :
glm::mat4 P = glm::perspective(1.0f,1.0f,1.0f,10.0f)
Il s'agit de la matrice de projection pour un angle de vue de 1 radian, un rapport d'aspect de 1, un plan proche à une distance de 1 et un plan lointain à une distance de 10. La multiplication de cette matrice par le vecteur (0,0,1,1) donne (0,0,-1,1), ce qui conduit à z''=-1 après la division de perspective. La multiplication de cette matrice par le vecteur (0,0,10,1) donne (0,0,10,10), ce qui conduit à z''=1 après la division de perspective. On obtient bien une profondeur finale dans l'intervalle [-1,1], les points les plus proches de la caméra ayant une profondeur plus petite.
La projection orhoscopique est une projection orthogonale sur le plan image. Un point dans l'espace de coordonnées (x,y,z) se projette simplement en un point de cooordonnées (x,y) sur le plan image. Dans ce cas, la position du plan image sur l'axe (Oz) n'a pas d'importance (mais il doit être perpendiculaire à cet axe), mais on garde tout de même un plan proche et un plan lointain pour les raisons données ci-dessus. La figure suivante montre le volume représenté dans le cas d'une projection orthoscopique.
Figure pleine pagePour une projection orthoscopique, la matrice P est l'identité. La projection perspective n'a pas de raison d'être mais, dans l'implémentation OpenGL, la division par la 4ième coordonnée se fait dans tous les cas (le GPU ne sait pas quel type de projection on fait). Lorsque la matrice R est appliquée au point de coordonnées (x,y,z,1), on obtient :
La division par w' donne bien les coordonnées (x',y') dans l'intervalle [-1,1] et la profondeur z' dans l'intervalle [-1,1] (la valeur -1 correspondant au plan proche). Dans l'implémentation OpenGL, les points dont une des coordonnées n'est pas dans l'intervalle [-1,1] ne sont pas tracés (opération de troncature).
Pour les deux types de projection, l'axe (Ox) doit être orienté vers la droite de l'image et l'axe (Oy) doit être vers le haut de l'image. C'est bien le cas avec les matrices de projection définies ci-dessus.
La bibliothèque GLM comporte des fonctions pour créer des matrices de projection orthoscopiques, en particulier :
glm::ortho(xmin,xmax,ymin,ymax,near,far)
Les distances near et far sont positives et correspondent respectivement à -d et -e. Voici un exemple :
glm::mat4 P = glm::ortho(-1,1,-1,1,1,10)
La multiplication de cette matrice par le vecteur (0,0,-1,1) donne (0,0,-1,1). La multiplication par le vecteur (0,0,-10,1) donne (0,0,1,1). La profondeur finale est bien dans l'intervalle [-1,1] et les points les plus proches de la caméra ont la profondeur la plus faible.
Pour l'algorithme de rendu par objet, l'ensemble des transformations se résume à la multiplication par une seule matrice :
La matrice de projection RP et la matrice de visualisation-modélisation VM sont généralement calculées séparément et multipliées au dernier moment, éventuellement par le GPU.
En partant des coordonnées d'un point (x,y,z), on obtient donc :
et enfin la division de perspective :
Les coordonnées finales des points sur le plan image sont dans l'intervalle [-1,1]x[-1,1]. La dernière étape (effectuée par le GPU) est la transformation de viewport : elle consiste à transformer ces coordonnées réduites en coordonnées de pixels, en fonction de la taille de l'image en pixel. L'image finale est en effet une matrice comportant nx par ny pixels. Il s'agit d'une transformation affine de changement d'échelle qui transforme -1 en 0 et +1 en nx-1 (pour l'axe horizontal). Pour éviter une déformation, il faut bien sûr que les proportions de l'image finale soient les mêmes que celle du plan image, c'est-à-dire :
Comme déjà expliqué plus haut, la profondeur z'' des points qui sont conservés par l'opération de troncature (clipping) sont dans l'intervalle [-1,1] mais ces valeurs sont finalement ramenées dans l'intervalle [0,1] par une transformation affine. La sélection du point le plus proche pour chaque pixel se fait avec cette profondeur. Avec les matrices définies dans ce document, il faut garder les points dont la profondeur est plus faible.
Voyons les transformations utilisées dans l'algorithme du lancer de rayon.
Les objets et la caméra sont positionnés dans l'espace avec la matrice de modélisation-visualisation VM.
Chaque point du plan image se calcule en lancant un rayon issu du centre optique vers ce point. Soient (x'',y'') les coordonnées réduites d'un point du plan image, dans l'intervalle [-1,1]. Les coordonnées du point sont :
Le rayon initial est défini par le point O et par le vecteur directeur unitaire , dont les composantes sont :
L'algorithme du lancer de rayon consiste à rechercher le point d'intersection de ce rayon avec l'objet le plus proche. Pour chaque objet, il faut obtenir les coordonnées du point O et du vecteur dans le repère propre de cet objet. Cela se fait en utilisant la transformation (VM)-1. Pour transformer les composantes du vecteur directeur, il suffit de prendre une quatrième composante nulle.
Notons B le point d'intersection le plus proche et le vecteur directeur d'un rayon émis à partir de ce point (rayon réfracté ou rayon réfléchi). Les coordonnées de ce point et de ce vecteur sont tout d'abord obtenues dans le repère propre de l'objet. Il faut donc les transformer avec la matrice VM pour obtenir les coordonnées dans le repère de la caméra. Pour définir le vecteur directeur d'un rayon lancé vers une source de lumière, il faut d'abord transformer les coordonnées du point B et de la normale à la surface en ce point. La transformation des normales est expliquée dans Transformations affines. Le calcul de l'éclairement d'un point d'une surface, qui fait intervenir les angles entre les rayons et la normale, se fait dans le repère de la caméra.
Figure pleine pageLa méthode du lancer de rayon permet aussi d'effectuer la sélection d'un objet dans le cas du rendu par objet.
L'utilisateur sélectionne un point sur la fenêtre graphique (avec la souris). Il s'agit de déterminer, s'il existe, l'objet le plus proche auquel ce point appartient.
Le gestionnaire d'évènements fournit les coordonnées du point sélectionné en unité de pixels. À partir de ces coordonnées, il est aisé de calculer les coordonnées réduites (x'',y'') du point dans le plan image, en faisant attention à l'inversion du sens de l'axe y. Les coordonnées du point P (du plan image) sélectionné dans le repère (Oxyz) (la caméra est placée en O) sont alors :
On s'intéresse à la droite OP, dont le vecteur directeur unitaire est le vecteur défini ci-dessus. On cherche à déterminer une éventuelle intersection de cette droite avec un objet de la scène. Si la scène comporte plusieurs objets, la première étape consiste à déterminer cet objet.
Pour que cette étape d'identification se fasse simplement et efficacement, une solution est de générer (lors du tracé de l'image) un tampon de référence des objets similaire au tampon de profondeur. Lorsqu'on colorie un pixel sur l'image lors du rendu d'un objet, on stocke dans ce tampon une référence de l'objet (par exemple un indice sur un tableau des objets). Lorsqu'un nouvel objet est représenté, sa référence doit prendre la place de la précédente si le point de l'objet correspondant au pixel est plus proche que le point de l'objet déjà référencé. Cet algorithme est cependant difficile à mettre en œuvre car le test de profondeur est effectué par le GPU sans que l'on puisse savoir si le pixel demandé est rejeté ou pas (à notre connaissance).
Une autre méthode, plus simple mais moins efficace, est de chercher l'intersection de la droite OP avec tous les objets de la scène et de retenir l'objet dont l'intersection est la plus proche du point O. Cet algorithme peut être amélioré en associant à chaque objet une boîte contenante de forme parallélépipédique. On cherche tout d'abord l'intersection de la droite avec la boîte contenante, ce qui permet d'exclure rapidement les objets trop éloignés de la droite.
Pour déterminer l'intersection de la droite (OP) avec un objet (ou avec sa boîte contenante), il faut utiliser la matrice de modélisation-visualisation associée à cet objet et considérer sa matrice inverse (VM)-1. Cette matrice permet de déterminer la droite OP dans le repère propre de l'objet, ce qui permet de calculer l'intersection de cette droite avec l'objet (ou avec un élément de l'objet).
Il faut remarquer que la nécessité de disposer de l'inverse de la matrice VM (nécessaire aussi pour le calcul des normales aux surfaces) ne signifie pas que cette matrice doive être inversée. En effet, il est aisé de construire l'inverse de la matrice VM en même temps qu'on construit la matrice VM elle-même par composition de transformations affines (par multiplications à droite). Il suffit, pour chaque transformation affine , de considérer sa transformation affine inverse et de faire la multiplication à gauche au lieu de la faire à droite. Supposons par exemple que la transformation de visualisation soit la composition d'une rotation R1 appliquée à l'objet et d'une translation T1 (qui positionne l'objet dans le champ de la caméra). La matrice modélisation-visualisation s'écrit :
La rotation s'applique la première aux coordonnées des points de l'objet, suivie de la translation. La matrice inverse est :
comme on peut le vérifier facilement en multipliant ces deux matrices. En conséquence, l'application de R1 par multiplication à droite lors du calcul de VM est associée à l'application de R1-1 par multiplication à gauche lors du calcul de (VM)-1.