Sommaire

Chapitre 1 - Préambule : fonctionnement d'un rendu 3D

Cet article est une introduction au rendu 3D temps réel dans sa globalité, il présente les méthodes usuelles pour procéder au traitement puis à l'affichage d'une image à partir de données brut ; modèles 3D et textures principalement. Il ne contient aucune information relative à OpenGL mais la connaissance de son contenu est fondamentale pour appréhender l'API. Cette indépendance lui confère en revanche l'avantage de vous fournir des notions qui vous seront utiles dans l'apprentissage et l'utilisation d'autres API.

Force est de constater que les notions de rendu temps réel sont souvent acquises au travers de l'apprentissage d'une API, cependant je ne favorise pas cette pratique car elle peut conduire à des incompréhensions, des erreurs de programmation ou de mauvaises habitudes. Un néophyte risque d'associer un concept avec son implémentation propre à l'API étudiée. Il est important de comprendre les opérations qui sont effectuées lorsque l'ont appelle des routines de notre API, ne serait-ce qu'un minimum. Par la suite des connaissances plus approfondies permettront des optimisations plus subtiles mais importantes.

Géométrie et textures

Le rendu temps réel via GPU se résume à deux choses très simple : la spécification ainsi que l'envoi de données au GPU, et la façon dont ces données seront interpretées et traitées par celui-ci. Nous allons dans un premier lieu nous intéresser à ces données sans nous soucier de la façon dont nous les traiteront pour synthétiser notre image. La seconde étape est plus subtile et nécessite de savoir de quelles données nous disposons.

Quoi que l'on cherche à dessiner on passera toujours par l'intermédiaire d'une forme géométrique simple, une primitive. Les formes complexes, comme par exemple les têtes de singes, peuvent être représentées par plusieurs primitives collées les unes aux autres. Ainsi assemblées, ces surfaces planes peuvent former des surfaces d'apparence courbe. Bien entendu plus il y aura de primitives dans un modèle 3D plus celui-ci sera précis et joli, mais il y aura également plus de données à traiter. Trouver un compromis entre ces deux limitations est le travail du modeleur, celui du programmeur étant de concevoir et d'implémenter des algorithmes rapides sans trop se préoccuper de la taille des données fournies.

Les primitives de base sont le point, le segment de droite et le triangle. Parmi ces trois primitives le triangle est le seul à offrir une portion d'un plan, ceci nous permettra de l'utiliser afin de remplir des zones de l'écran délimitées par un polygone et non seulement un simple point ou un segment, ces derniers étant en général représentés par de simples pixels. Le triangle est le polygone le plus élémentaire ce qui lui assure la plus grande généricité et la définition la plus simple ; un triangle n'est représenté que par ses trois sommets. Notez qu'un sommet se dit vertex en anglais et vertices au pluriel.

Notion de sommet

Intuitivement on pourrait penser qu'un sommet permet de repérer un point dans l'espace pour la construction future d'une forme géométrique, et c'est en partie vrai. En réalité la notion de sommet dans le rendu 3D s'étend au delà du simple point dans l'espace. Un sommet contient un ensemble abstrait de données appelé attributs utilisé pour la construction et le rendu du ou des primitives utilisant ce sommet.

Par défaut ces attributs n'ont aucun usage particulier, c'est à l'utilisateur de leur en trouver un.

Représentation

Les attributs des sommets sont des vecteurs à 4 composantes dont le type est laissé au choix de l'utilisateur. Selon l'usage que l'on en fera il pourra y avoir ou non des restrictions sur ces types. Voici à quoi pourrait ressembler la structure d'un sommet :
set Vertex {
  Vector4D attribute1
  Type     attribute1_type
  Vector4D attribute2
  Type     attribute2_type
  Vector4D attribute3
  Type     attribute3_type
  ...
}

Grâce à l'information de typage la carte graphique sera capable de lire correctement tous nos attributs.

Textures

Les textures sont à l'instar des sommets une ressource exploitable par la carte graphique, à la différence qu'elles se présentent sous la forme d'un tableau de données à 1, 2 ou 3 dimensions. À deux dimensions les textures servent généralement à représenter des images. La notion d'image englobe non seulement les photos de vacances mais aussi tout type d'information pouvant d'être stockée dans un tableau ; ainsi une image peut représenter de la géométrie. Nous verrons plus tard que ce cas est intéressant et particulièrement utilisé. Les textures à une dimension permettent de stocker une image qui ne fait qu'un pixel de hauteur (ou de largeur, nous verrons que cela n'a pas d'importance) et les textures 3D permettent en gros de stocker plusieurs images 2D.

Chaque type de texture possède des utilisations particulières. En réalité il existe même d'autres types de textures, mais au final cela se rapportera toujours à une texture à 1, 2 ou 3 dimensions, les différences se trouvant plus dans la méthode de traitement et donc dans les usages qui en découlent.

Représentation

De même que pour les sommets une brève présentation de la représentation interne des textures s'avère nécessaire même si elle paraît évidente. En effet, ceci par exemple pourrait représenter une texture 2D :
set Texture2D {
  Int width
  Int height
  Pixel pixels[width * height]
}

Il reste cependant un type à définir, c'est Pixel. Dans la mesure où une texture est à usages multiples, le type d'un pixel doit rester libre et non se cantonner à de simples composantes RGBA. Ainsi un pixel pourra être un simple flottant, un nombre d'entiers choisi entre 1 et 4, etc. Les types disponibles sont de plus en plus nombreux avec le temps et l'évolution des cartes, cependant il y a certaines restrictions, dépendantes de l'API, des drivers de la carte et de la carte elle-même.

Dessin de primitives à partir de sommets

Cette partie se penche sur le rendu d'une image basée sur les données que nous avons créées plus haut. Le processus de rendu d'une primitive se déroule entièrement sur le GPU, ainsi nous allons parler ici de traitements qui seront programmés pour le GPU. Les initiés auront reconnu ici les shaders, pour les non initiés je détaillerai ce concept dans un autre article.

Traitement des sommets

Puisque la carte graphique ne sait rien interpréter par défaut, il va nous falloir programmer nous-même les opérations à effectuer sur chacun des sommets reçus en entrée pour le rendu. Chaque sommet est traité indépendamment des autres. Cette étape du rendu, plutôt simple, demande que l'on retourne les coordonnées de ce sommet en _espace écran_. Ce qui est généralement appelé l'espace écran (screen space en anglais) est en fait une base orthonormée représentant la fenêtre de rendu et dont le point (0, 0) se trouve au centre de la fenêtre et les points (1, 1), (-1, 1), (-1, -1), (1, -1) aux coins supérieur droit, supérieur gauche, inférieur gauche et inférieur droit de la fenêtre, respectivement.

Pour renvoyer ces coordonnées il est nécessaire de connaître l'attribut qu'on a choisi pour le stockage des informations de position. Il existe bien sûr des moyens pour les identifier afin de les reconnaître et les différencier entre l'étape de spécification des données vue précédemment et celle de traitement. Supposons que nous avons stocké les positions de nos sommets directement en espace écran dans l'attribut 1, le traitement d'un sommet pourrait se résumer à ceci :
function VertexSetup (Vertex vertex)
{
  ScreenSpaceCoordinates = vertex.attribute1
}

Projection

Dans l'exemple précédent nous avons considéré que nous possédions directement les coordonnées, écran de notre sommet, mais en pratique c'est rarement voire jamais le cas. De façon usuelle nous possédons les coordonnées de nos sommets dans l'espace monde (world space en anglais) en trois dimensions ; en effet si nous voulons faire du rendu 3D il est préférable de prendre un repère d'origine qui sera la base de tout objet dans notre "monde" virtuel.

La projection permet de transformer des coordonnées qui sont dans l'espace monde vers l'espace écran. Il s'agit en général d'un produit matriciel entre une matrice, dite de projection, et le vecteur des coordonnées en espace monde du sommet à projeter. La matrice de projection MProj est en général carrée d'ordre 4 et les coordonnées de position un vecteur 4D VPos (x, y, z, w) dont la composante w vaut 1. Une fois le produit MProj × VPos effectué il en résulte un vecteur 4D dont il faudra diviser les composantes (x, y, z) par la composante w pour obtenir les coordonnées en espace écran dans (x, y) et la profondeur dans la composante z. Nous reparlerons plus en détail de la profondeur d'un sommet plus bas dans cet article.

Caméra

Nous pourrions vouloir nous déplacer dans notre monde virtuel, nous verrions alors les objets défiler devant nous sans pour autant que la position en espace monde de ceux-ci ne soit modifiée. Il est cependant un fait que leur position en espace écran l'est effectivement dû au déplacement du point de vue duquel nous nous plaçons, appelé communément la caméra.

Afin de simuler l'effet de caméra nous n'avons d'autre choix que de déplacer tous les objets du monde. Pour cela nous allons simplement utiliser une matrice qui représentera les transformations à appliquer aux objets (et donc aux sommets) pour les déplacer de façon à créer l'illusion d'une caméra. Je vous laisse méditer sur la façon dont créer une telle matrice, grossièrement il s'agit d'inverser la matrice qui représenterait le déplacement de la caméra en espace monde comme s'il s'agissait d'un objet. Notre traitement d'un sommet pourrait donc être complété ainsi :

  function VertexSetup (Vertex vertex)
  {
    ScreenSpaceCoordinates = MProj * MView * vertex.attribute1
  }

Assemblage des primitives

Cette étape se charge de créer des primitives à partir d'un ensemble de sommets. Elle prend en entrée les sommets représentant la primitive à dessiner et elle renvoie une ou plusieurs primitives. Le nombre de sommets en entrée est directement relatif au type de la primitive que l'on souhaite dessiner ; par exemple pour un triangle on en aura trois. La création d'une primitive est en fait l'initiation d'un processus qui se chargera de dessiner effectivement cette primitive, indépendamment des autres. Remarquez effectivement que jusqu'ici nous n'avons traité les sommets qu'un par un, à partir d'ici un lien va être créé entre des groupes de sommets afin de les traiter de façon uniforme pour générer une primitive.

Rastérisation

À partir d'une primitive, la carte graphique peut interpoler les attributs des sommets qui la composent afin de la "remplir", c'est-à-dire de remplir l'espace délimité par la primitive en espace écran. En effet nous opérons maintenant en espace écran dans la mesure où les sommets ont déjà été transformés. Un peu à l'instar de l'assemblage des primitives, cette étape va lancer le processus de rendu d'un pixel pour chaque pixel représentant la surface de la primitive à l'écran. Je vous laisse consulter l'article Wikipédia concernant la rastérisation.

Image

Maintenant les données géométrique traitées, nous sommes arrivés à un point où nous allons manipuler le plus petit élément d'un rendu 3D : le pixel. De même que pour les sommets et comme dit précédemment, le traitement d'un pixel constitue une étape à part entière du processus de rendu.

Traitement des pixels

Bla.

Interpolation

Texturage

Tampon chromatique

tavu tu dessines dans un tamon. trop ouf.

Test de profondeur

Mélange des couleurs : blending