M2 Image

TP1 - prise en main



Partie 1 :  installez gKit

cf la rubrique installation de la doc.

modifiez un des tutos, par exemple le tuto6 (version plutot "c") ou le tuto7 (version plutot "c++"), pour afficher plusieurs objets, par exemple des cubes disposés sur une grille :

rappel : transformations standards et matrices model, view, projection, et viewport. c'est la matrice model qui permet de placer et d'orienter un objet dans le monde.

indications : si tout ça est un peu flou, relisez :





affichez le temps cpu et gpu (dérivez de la classe AppTime, au lieu de la classe de base App).

quelles remarques peut on faire sur le temps cpu et le temps gpu en fonction du nombre d'objets affichés (faites plusieurs essais en modifiant le nombre d'objets affiché, ainsi que le nombre de lignes et de colonnes de la grillle) ?

est ce que le temps d'affichage varie selon la position de la camera ? et le temps cpu ? gpu ?

au lieu d'afficher plusieurs fois le même objet, chargez des objets différents, par exemple ceux de quaternius :



mêmes questions : est ce que le temps cpu et/ou gpu changent ?

Partie 2 : et avec openGL ?

modifiez votre programme pour dessiner les objets directement avec openGL.
Le mieux est de faire les modifications progressivement, en suivant les tuto9_shader et tuto9_buffer :
    0. relisez interface C et openGL, ainsi que shaders et GLSL,
    1. utilisez le shader minimaliste du tuto9 pour dessiner vos objets, au lieu de laisser draw() le faire à votre place, (ou le tp 4 de l'année passée pour plus de détails)
    2. changez la couleur des objets...
    3. créez les buffers et configurez le vertex array object et dessiner directement

indication : commencez par la version simple, avec un seul objet...

pour les curieux : comment décrire plusieurs objets avec les mêmes attributs de sommets (par exemple : position + normale) ? un vertex array object et un vertex buffer par objet ? ou est ce qu'une autre organisation permettrait de gagner du temps ? pourquoi ? comment faire ?

indications : il existe 2 interfaces pour configurer un vao, la version openGL 3.3, cf glVertexArrayPointer() et la version openGL 4.3, cf glVertexArrayFormat() / glBindVertexBuffer(), relisez les détails, si nécessaire (la section se trouve à la fin de la page).

Partie 3 : animation et déformations


chargez 2 keyframes d'une animation : robot (quaternius)

on suppose que la première keyframe correspond au temps t= 0 et la 2ième au temps t= 1.
comment interpoler (linérairement) la position de chaque sommet en fonction du temps t, connaissant sa position à t= 0 et à t= 1 ?

écrivez le vertex shader qui réalise cette interpolation.
quelles informations sont nécessaires ?
comment les fournir au shader ?

modifiez votre shader et votre programme.

indication : float global_time() de window.h renvoie le temps écoulé depuis le lancement de l'application, en millisecondes.


bonus :
chargez toutes les keyframes de l'animation et affichez les, en boucle. comment décrire / organiser toutes ces informations ? un vao par keyframe ? ou autre chose ?
rappel : il existe 2 versions de l'interface de configuration des vao...

super bonus : et si on veut afficher un groupe de robots dont les animations ne sont pas identiques ? (chaque robot ne pose pas le pied en même temps que les autres)


Partie 4 : plusieurs matières

assets de quaternius

chaque triangle d'un Mesh est associé à une matière, cf la classe Material.
affichez vos objets en fonction de leurs matières. cf Mesh::materials() et la classe Materials pour les récuperer. les indices des matieres des triangles sont dans Mesh::material_indices(), ou Mesh::triangle_material_index()

il existe bien sur plusieurs solutions. une solution courante consiste à trier les triangles par matiere, puis à afficher chaque groupe de triangles séparement, ce qui permet de modifier tous les paramètres (couleurs, textures, etc.) sans problèmes. c'est aussi la solution la plus couteuse pour le cpu. cf Mesh::groups().

est-il réellement nécessaire de faire plusieurs affichages ?
peut-on re-organiser les "informations" différemment pour afficher tous les triangles et leurs matieres, en une seule fois / avec le même fragment shader ?
testez votre solution et comparez.

et si les matieres utilisaient des textures en plus de couleurs / coefficients ?

la solution à ce problème est classique et très simple : il suffit d'utiliser une indirection simple, ou une indirection double...

avec openGL, on peut faire la même chose, mais il y a une contrainte à respecter : les tableaux d'uniforms sont statiques. il faut déclarer leur taille et au total, les uniforms déclarés par un shader ne peuvent pas occupper plus de 32Ko... du coup, il est plus raisonnable d'utiliser la version avec une seule indirection qui n'utilise qu'un (petit) tableau de matieres.

si l'on ne souhaite afficher qu'un seul objet, il suffit de déclarer la bonne taille dans le shader. mais si l'on veut utiliser le meme shader pour dessiner des objets différents, il faut une solution plus souple. la encore, la solution est classique : il suffit de déclarer une taille suffisante pour le plus "gros" objet (tout en restant dans la limite de 32Ko), et de n'utiliser qu'une partie du tableau pour afficher les autres.

remarque : oui, on peut utiliser des uniform buffers (opengl 3.3) ou des shader storage buffers (opengl 4.3) pour stocker plus de valeurs (taille limitee par la memoire de la carte graphique).

reste un probleme : comment obtenir l'indice de la matière du triangle (sans utiliser d'uniforms) ? une solution directe est d'utiliser un attribut de sommet. il faudra utiliser glVertexAtttribIPointer(), pour le configurer au lieu du glVertexAttribPointer() habituel.
dernière remarque : vu la limite sur le nombre de matieres, il est raisonnable d'utiliser un GL_UNSIGNED_BYTE, un entier non signe sur 8bits (et oui, ca limite le nombre de matieres à 256. mais stocker 256 descriptions de matières occuppe une bonne partie des 32Ko disponibles...)

indications :
    le fragment shader connait l'indice de la primitive qu'il dessine., cf gl_PrimitiveID
    on peut utiliser des tableaux d'uniforms,
    on peut ajouter des attributs aux sommets des triangles : pour configurer un attribut entier (l'indice de la matière par exemple), il faut utiliser glVertexAtttribIPointer(), au lieu de glVertexAttribPointer()
    on peut grouper plusieurs textures dans une seule plus grande : cf atlas de textures,
    il existe des tableaux de textures : cf GL_TEXTURE_2D_ARRAY


bonus :
peut on utiliser la même stratégie avec les matières de plusieurs objets ? quelles seraient les contraintes à respecter ?
    on peut dessiner plusieurs objets avec un seul glDraw(), cf glDrawArraysInstanced() et glMultiDrawArraysIndirect()...
    qui gère les ressources openGL ? chaque objet ? la technique d'affichage ? faut-il créer une gestion centralisee ou peut-on laisser chaque objet décider ?


Partie 5 : rendu direct et rendu différé...

openGL et les cartes graphiques, de manière générale, dessinent scrupuleusement les triangles dans l'ordre décrit par l'application / les buffers, etc. Tous les fragments nécessaires au dessin de chaque triangle sont exécutés. C'est parfaitement raisonnable, mais lorsque la géométrie dessinée devient un peu complexe, de nombreux fragments se projettent sur le même pixel de l'image et seul le fragment le plus proche de la camera sera visible dans l'image finale. Tous les calculs réalisés sur les fragments non visibles sont inutiles... Lorsque ces calculs deviennent importants, le temps de calcul de l'image complete augmente fortement et peut dépasser les 16ms permettant de conserver des animations parfaitement fluides. Le rendu differé est une technique permettant de n'exécuter les fragment shaders qu'une seule fois par pixel et de réduire le temps total de calcul de l'image.

explications et détails

Comment programmer cette technique ?
première différence importante avec les tps précédents : il faut dessiner 2 fois. Habituellement, les shaders font les calculs directement, au fur et à mesure que les fragments sont générés par l'unité de fragmentation / rasterization (et il suffit de dessiner une fois les triangles). L'idée du rendu différé est de stocker les informations nécessaires et de repousser la réalisation des calculs dans une deuxième étape, qui ne s'exécute que sur les fragments visibles dans l'image finale.

il faut donc pouvoir faire 2 choses avec openGL : stocker des informations lors de l'exécution des fragment shaders et ensuite, exécuter les fragment shaders sur tous les pixels de l'image.

pour openGL, ce sont les textures qui représentent des images (comme les buffers représentent des tableaux), il faut donc savoir les créer / manipuler. Pour que les fragment shaders puissent stocker des informations dans des textures, il faut configurer un nouvel objet openGL : un framebuffer.

tous les détails sont dans rendu multi passes.

dernière étape : comment exécuter des fragment shaders sur tous les pixels d'une image ?
réponse : les fragment shaders ne sont exécutés que pour dessiner des primitives / des triangles. il faut donc dessiner de la geometrie qui couvre tout l'espace image. Le plus intuitif est de dessiner 2 triangles qui forment un rectangle, mais le plus efficace consiste à dessiner un grand triangle rectangle qui couvre tout l'espace image.

question : dans quel repère est-il le plus simple de calculer les coordonnées des sommets de ces triangles (ou du triangle) ?

exercice 1 : pour commencer simple, afin de se concentrer sur les nouveautes, ne dessinez qu'un seul objet avec une seule matière.
quelles sont les informations à conserver pour évaluer l'apparence d'un fragment dans la 2ieme étape ?
combien de textures faut-il utiliser ? utilisez des textures GL_RGB32F ou GL_RGBA32F, qui permettent de stocker 3 ou 4 floats 32bits par pixel.

indication : utilisez vec4 texelFetch(sampler2D, ivec2 coordonnees pixel, 0) pour relire les informations stockées dans les textures.
pour relire la profondeur / zbuffer, la valeur est stockee dans la premiere composante .x des pixels de la texture (et pas dans .z) :
    float z= texelFetch(sampler2D, ivec2, 0).x;

et n'oubliez pas d'initialiser les parametres de filtrage des textures pour interdire le filtrage (lors de la création des textures) :
    glTexParamerteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParamerteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParamerteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);

vous pouvez aussi utiliser un Sampler Object, cf glGenSamplers(), glSamplerParameteri(), et glBindSampler(); pour modifier les paramètres de filtrage lors de l'utilisation de la texture.


exercice 2 : si vous avez stocké la position du fragment dans une texture, comment éliminer cette texture ? comment re-calculer la position du fragment ?

pourquoi s'embetter à faire ca ? ca marche très bien en stockant la position dans une texture ? oui, bien sur. Mais ...
on peut gagner pas mal de temps, surtout sur des gpu avec une petite bande passante, comme le gpu intégré des portables. la différence sera moins visible sur les machines équipées d'une carte graphique dédiée. Plus les shaders de la premiere passe stockent d'informations, plus il faut de temps pour les enregistrer dans la memoire de la carte graphique. quelquesoit la configuration de la machine, si les shaders stockent 2 fois moins d'informations, les transferts mémoire seront 2 fois plus rapide.

indications : gl_FragCoord.xyz dans le fragment shader permet de connaitre les coordonnées dans l'espace image du fragment. quelle transformation permet de les connaitre dans le repère du monde ? de la scène ?

rappel : vous avez crée explicitement une texture zbuffer pour dessiner correctement la geométrie lors de la première étape.

question : quelles informations peut on obtenir dans la 2ième étape ?

exercice 3 : plusieurs matières (mais sans textures...)
que faut-il changer ?

exercice 4 : plusieurs de sources de lumière...

comment décrire plusieurs sources de lumière ?

comparer les performances lorsque vous augmentez le nombre de sources de lumière dans la scène. entre 1 et 1000 ?
combien de sources de lumières peut-on stocker simplement dans un tableau statique uniform ?
que faudrait-il faire pour utiliser un tableau plus grand ?

bonus : et en utilisant des formats plus compacts mais moins précis pour stocker les informations du fragment visible, par exemple des floats 16bits GL_RGB16F ou des GL_RGBA8 ? constatez-vous une différence de performance ?

Partie 6 : plein de sources de lumière !!

que pensez vous de cette solution :
Clustered Deferred and Forward Shading. O. Olson, M. Billeter, U. Assarson, 2012, + slides commentees

lorsqu'une scène comporte beaucoup de sources de lumière, il est intéressant de passer un peu de temps à identifier le sous ensemble de sources qui peuvent éclairer chaque fragment.

comment ?
l'idée est relativement simple : il suffit de se rendre compte que l'on peut borner l'influence d'une source de lumière ponctuelle en fonction de la distance.
tous les fragments suffisamment éclairés se trouvent à une distance inférieure à une limite...
(indication : le flux recu par un point p à une distance d de la source est doit etre supérieur à un epsilon. quelle est la valeur de d en fonction de l'emission de la source et de epsilon ?)

tous les fragments dessinés en dehors de la sphere bornant l'influence de la source sont considérés noirs : ils ne sont pas éclairés par la source. il est donc inutile d'inclure la source dans le calcul d'éclairage direct du point.

comment pre-calculer, pour chaque cellule d'une grille placée dans la scène, le sous ensemble de sources qui peuvent éclairer un fragment à l'interieur d'une cellule ?

pour simplifier la premiere version, utilisez une grille dans le repère de la scene, et une boite alignée sur les axes pour représenter l'influence de chaque source.
construisez, sur cpu, pour chaque cellule, le sous ensemble de sources contribuant à la cellule.

comment transmettre ces informations à un fragment shader ?
indications : texture 3d avec des pixels GL_R16UI / GL_RED_INTEGER, des entiers 16bits ? un ou plusieurs shader storage buffers ?

comparez avec l'exercice 4. combien de sources de lumière en conservant un rendu en 16ms ?

lorsque la scène est un peu étendue, les dimensions de la grille augmentent très très vite, et il est préférable d'utiliser une grille dépendant de la position / orientation de la camera, que faut-il changer ?
certains moteurs de rendu utilisent une grillle dans l'espace projectif de la camera, quel est l'interet de ce type de grille ?


il est tout à fait possible de construire les clusters (la voxelisation des sources) sur cpu, avec plusieurs threads, comme doom 2016 par exemple. ou sur gpu, avec des compute shaders... comme doom eternal...

pour les curieux : cours / slides commentes sur les différentes techniques utilisables

en images, étape par étape, comment doom eternal construit son rendu, cf ce blog
pour comparer avec doom 2016, c'est la