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...
- indirection simple : connaissant l'indice de la matière
d'un triangle, comment obtenir la description de la matière ? en
c++, il suffirait de déclarer un tableau de matières et de
l'indexer avec (l'indice de) la matière d'un triangle,
- indirection double : connaissant l'indice d'un
triangle, comment retrouver l'indice de la matiere et obtenir la
description de la matiere ? il suffit d'utiliser 2 tableaux, un
qui contient l'indice de la matière de chaque triangle et
l'autre qui contient les matieres.
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