CPE IMI 2017-2018


TP1 - projection / fragmentation






L'opération fondamentale réalisée par un pipeline de rendu est le dessin d'une primitive : déterminer quels pixels permettent de remplir une forme "simple" dans l'image résultat.

Cette opération est découpée en plusieurs étapes :


exercice 1 : transformations et projection

    Les objets et leurs sommets sont décrits dans un repère local, puis ces objets sont placés et orientés dans le repère de la scène. Un observateur / camera est également placé et orienté dans le repère de la scène. Une transformation de projection est aussi associée à l'observateur. Ces 3 transformations sont classiquement représentées par des matrices homogenes 4x4 : 
    Model (transformation du repère local au repère de la scène), 
    View (transformation du repère de la scène au repère camera)
    Projection (transformation du repère camera au repère projectif homogène de la camera),

Plus une autre qui représente les dimensions de l'image résultat :
    Viewport (transformation du repère projectif homogène vers le repère de l'image).

Il est aussi possible de représenter la projection et le passage dans le repère image directement, comme vu en cours.

Si l'on choisit une projection directe, ou peut on placer des points qui seront visibles / associés à un pixel de l'image ?

prise en main de gKit :

installez gKit, les informations sont sur la page précédente.

Vous pouvez générer la documentation avec doxygen. Elle sera consultable dans html/index.html (ou en ligne). Les classes de bases sont documentées dans la partie module de la documentation générée.



gKit utilise la classe Transform pour représenter et manipuler les transformations, (cf mat.h). Les classes Point et Vector (cf vec.h) permettent de représenter un point et un vecteur. Les fonctions de construction des transformations standards sont aussi disponibles : Identity(), Translation(), Rotate(), Perspective(). 

La transformation d'un point s'écrit directement :
    #include "mat.h"
    #include "vec.h"

    Transform T;                          // identité

    Transform T= Identity();              // identité aussi
    Transform T= RotateX(30);             // rotation de 30° autour de l'axe X
    Transform T= Translate( 0, 0, 50 );   // translation sur l'axe Z

    Point p;
    Point q= T(p);                        // renvoie le point reel transforme

La composition de transformations est aussi disponible : 
    Transform A, B;
    Transform C= A * B;

La transformation inverse est également calculée :
    Transform M= C.inverse();
    Transform M= Inverse(C);

Pour obtenir le point homogène après la transformation d'un point p :
    Point p= Point(1, 0, 1);
    vec4 h= p;

    vec4 ph= M(h);                        // renvoie le point homogene apres la transformation


gKit utilise la classe Image (cf image.h) pour représenter un ensemble de pixels et fournit également des fonctions permettant d'enregistrer l'image dans un fichier.
    #include "image.h"
    #include "image_io.h"

    Image image(largeur, hauteur);

    write_image(image, "resultat.bmp");


Les operateurs () de Image permettent de lire et de modifier la couleur du pixel de coordonnées x, y :
    Image image(1024, 512);

    image(x, y)= Color(1, 0, 0);
    Color pixel= image(x, y);

La classe de base Color (cf color.h)  représente une couleur par comme un vecteur à 4 composantes : rouge, vert, bleu, et transparence / alpha. alpha= 1 pour une surface opaque.

exemple :

    #include "color.h"       // couleur
    #include "image.h"       // image
    #include "image_io.h"    // entrees / sorties sur les images

    int main( )
    {
        Image image(512, 512);    // cree une image de 512x512 pixels
    
        // parcourir tous les pixels de l'image
        for(int y= 0; y < image.height(); y++)        // chaque ligne
            for(int x= 0; x < image.width(); x++)     // chaque colonne
                image(x, y)= Color(1, 0, 0, 1);       // colorie chaque pixel en rouge opaque
    
        // enregistre le resultat
        write_image(image, "out.png");
        return 0;
    }


exercice 2 : un triangle visible

choisissez les coordonnées de 3 sommets visibles par la camera / projetables.
écrivez le test d'inclusion d'un pixel dans le triangle défini par ces 3 sommets.
est ce que l'orientation du triangle à une influence sur le résultat ? est-ce vous pouvez dessiner le triange a, b, c et le triangle a, c, b ?
remplissez tous les pixels du triangle avec une couleur constante.


exercice 3 : interpolation barycentrique

avant de pouvoir dessiner plusieurs triangles, il faut déterminer la profondeur de chaque fragment / pixel.
modifiez le test d'inclusion pour calculer les coordonnées barycentriques du fragment.
utilisez les coordonnées pour interpoler la profondeur du fragment.
stockez cette profondeur dans une image de profondeur (vous pouvez utiliser une Image en n'utilisant qu'un seul canal, rouge, par exemple).


exercice 4 : Zbuffer et Ztest

modifiez votre programme afin de comparer la profondeur du fragment avec celle stockée dans le zbuffer.
si le fragment est plus proche, mettez à jour la couleur du pixel et le zbuffer.

quelle valeur par défaut faut-il choisir pour initialiser le zbuffer avant de commencer à dessiner ?

que faut-il modifier pour conserver le fragment le plus loin ? le dernier dessiné ?


exercice 5 : plusieurs triangles

chargez un objet simple et dessinez tous ses triangles.
comment déplacer les sommets afin qu'ils soient tous visibles ? (quelle transformation appliquer ?)

utilisez read_mesh( ) et la classe Mesh (cf mesh.h et wavefront.h) pour charger un fichier .obj.

pour pouvez accéder aux positions des sommets des triangles directement :
Mesh mesh= read_mesh("data/bigguy.obj");

for(int i= 0; i + 2 < (int) mesh.positions().size(); i+= 3)
{
    Point a= Point(mesh.positions().at(i));
    Point b= Point(mesh.positions().at(i+1));
    Point c= Point(mesh.positions().at(i+2));

    ...
}

pour placer l'objet devant la camera, vous aurez besoin de son englobant (sa boite englobante), vous pouvez la calculer avec bounds( ), cf mesh.h

vous pouvez utiliser les maillages suivants :


exercice 6 : ca va couper !

vous venez de traiter le cas simple des triangles entièrement visibles.
modifiez votre programme pour traiter les triangles qui ne se dessinent pas entièrement dans l'image.
existe-t-il un cas particulier ? que faire ?


exercice 7 : éclairage et orientation

comment calculer la couleur du fragment en fonction de son orientation par rapport à la camera ?
ou par rapport à une source de lumière placée dans le repère du monde ?


exercice 8 : c'est trop lent !

tester la totalité des pixels de l'image pour chaque triangle est très très long.
proposez une solution pour limiter le nombre de pixels testé par triangle,
ou proposez une solution pour limiter le nombre de triangles testé par pixel.

comment évaluer la complexité de votre algorithme (en fonction du nombre de triangles, du nombre de pixels ?)
quelle est la meilleure solution ?

indication : le rectangle englobant les sommets du triangle ? ou un rectangle de pixels ne contenant pas le triangle ?


peut-on limiter le nombre de triangles à dessiner ?
proposez une solution permettant de ne pas dessiner les triangles à l'arrière de l'objet (cf back face culling). en moyenne la moitiée des triangles est éliminée par ce test. vérifiez.

indication : quelle est l'orientation des triangles visibles par la camera ? des triangles de l'autre coté d'un objet opaque ?

proposez une solution permettant de ne pas dessiner les triangles qui ne se projettent pas sur l'image (entièrement à gauche, à droite, etc...) (cf frustum culling)

peut-on facilement déterminer qu'un (morceau d'un) triangle est entièrement caché / derrière la géométrie déjà dessinée dans le zbuffer ?
(cf hierarchical depth culling)


bonus : version parallèle

comment utiliser plusieurs threads pour accélérer l'affichage d'un ensemble de triangles ?

cet algorithme n'est pas si simple à paralléliser. un triangle peut générer 0 pixel, 1 seul, ou couvrir toute l'image.
associer un thread par triangle génère des conflits lors de l'écriture dans l'image et la mise à jour du zbuffer.
associer un thread par pixel génère beaucoup de tests (chaque pixel teste la totalité des triangles), mais cette solution est plus simple programmer (avec openMP, par exemple)

il faut limiter le nombre de tests par pixel. comment déterminer (efficacement) le sous ensemble de triangles pouvant influencer un pixel ?
(cf exercice 8)


pour les curieux :

est ce que tous les triangles ont une influence sur un pixel de l'image ?
comment éliminer les triangles qui ne peuvent pas avoir d'influence sur l'image ?

indication : la fragmentation n'est qu'une discrétisation du plan image...

présentation des solutions utilisées dans le moteur Frostbite développé par DICE / EA :
    "Optimizing the graphics pipeline with compute" G. Wihlidal, 2016,

une version DX11 opensource développée par AMD est dispo sur gpuopen.com : GeometryFX