M2 Image

TP2 - plus de cubes...



Partie 1 : des cubes

maintenant que vous savez afficher des objets, comment peut on dessiner un ensemble de cubes pour représenter une scène façon Minecraft ? par exemple :




l'idée est d'utiliser une carte d'altitude / un terrain pour positionner quelques milliers de cubes. par exemple, en utilisant cette image (cf repertoire data/terrain) :



exercice 1 :
placer les cubes et dessinez-les directement avec les utilitaires de gKit

rappel : la camera regarde l'axe -Z


exercice 2 :
même question, utilisez un shader et dessinez directement avec glDrawArrays() / glDrawElements(), cf la doc... si nécessaire


exercice 3 : le bon draw !
même question, existe-t-il un draw openGL plus adapté à ce type d'affichage ? (ie dessiner plein de fois le même cube)
comment l'utiliser ? qui ira plus vite ? le cpu ? le gpu ? les 2 ?


Partie 2 : des cubes différents

on souhaite enrichir un peu plus la scène en utilisant des cubes différents, en fonction de l'altitude, par exemple :



vous pouvez utiliser le pack d'objets de quaternius : cube_world.zip (ou un autre, au choix)




exercice 1 :
c'est malin, comment peut-on dessiner rapidement une scène composée d'objets différents ?


bonus : et avec des personnages / animaux qui se promènent dans la scène ? et de la végétation ?


Partie 3 : trop de cubes

lorsque la scène devient trop grande, dessiner tous les cubes qui ne sont pas visibles dans l'image devient très / trop long. une idée est de découper la carte en régions, et de vérifier qu'une région est visible par la camera (ie dans son frustum) avant de la dessiner.


exercice 1 :
comment déterminer qu'un point est visible par la camera ? ie qu'il appartient à son frustum ?


exercice 2 :
même question pour un cube aligné sur les axes ? comment déterminer que le cube est, au moins en partie, visible ? serait-il plus simple de vérifier que le cube n'est pas visible ?
proposez un test qui vérifie qu'un cube ne peut pas etre visible. l'idée est de trouver un plan qui sépare le frustum et le cube...

indication : c'est la même idée que pour dessiner un triangle, si tous les sommets d'un objet sont de l'autre coté du plan, les 2 objets sont séparés...
indication : le frustum est un cube aligné sur les axes dans le repère projectif...


exercice 3 :
utilisez le test précédent et ne dessinez que les régions visibles.
euh, c'est malin, comment on fait pour continuer à utiliser l'instanciation ?

indication : et si on changeait le contenu du buffer d'instances ? ie celui qui contient les positions des cubes à dessiner...


quelle taille choisir pour une région ? quelle influence peut avoir ce parametre ?



Partie 4 : et les ombres ?

lorsque l'on dessine un triangle, on a pas d'information sur les autres triangles qui pourraient lui faire de l'ombre...

il va falloir dessiner 2 fois la scène,

mais on "garde" les triangles visibles par la source de lumière, on sait qu'ils sont éclairés, c'est le résultat de la 1ere étape. pour chaque pixel de l'image dessinée pour le point de vue de la source on connait la distance entre la source de lumière et le triangle visible.

ensuite, lorsque l'on dessine la scène pour le point de vue de la camera, on transforme le point visible pour la camera dans le repère / le point de vue de la source. il ne reste plus qu'à comparer la distance entre le point transformé et la source avec la distance de la 1ere étape, qui correspond au triangle le plus proche de la source, ie celui qui est éclairé. si le point est à la bonne distance / sur le triangle éclairé, il est lui aussi éclairé, sinon sa distance est plus grande et il est à l'ombre.

exercice 1 : dessinez la scène pour le point de vue de la source de lumière.

on commence par construire les transformations qui permettent d'observer la scène depuis la position de la source de lumière. il faut, comme d'habitude, définir les transformations model, view, projection et image.

le plus direct est d'utiliser une projection ortographique, cf Ortho() qui "projette" un cube [left .. right] x [bottom .. top] x [znear .. zfar] du repère camera vers le repère projectif.
des exemples complets sont dans la doc en ligne, cf "decals et projection de textures"


exercice 2 : création du framebuffer et des textures

étape suivante, il faut conserver les triangles visibles par la source de lumière, ils seront éclairés. ces triangles sont aussi les plus proches de la source de lumière (par construction), on veut conserver au minimum le zbuffer, ie la distance entre la source de lumière et le triangle visible / éclairé.

la création / configuration d'un framebuffer est assez directe, mais il y a bien sur quelques détails techniques à régler, cf "rendu multi-passes". vous pouvez utiliser les utilitaires de texture.h pour créer simplement les textures nécessaires, cf make_depth_texture() ou make_vec3_texture(), etc. elles sont faites exactement pour ça !

au final, votre code ressemblera à :

    GLuint shadow_map= make_depth_texture( /* unit */ 0, /* width */ 256, /* height */ 256 );

    GLuint framebuffer= 0;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, framebuffer);

    glFramebufferTexture(GL_DRAW_FRAMEBUFFER, /* attachment */ GL_DEPTH_ATTACHMENT, shadow_map, /* mipmap */ 0);

    // verification de la configuration du framebuffer
    if(glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        return "error";

    // nettoyage...
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

pour dessiner dans le framebuffer, il ne reste plus qu'à le sélectionner (avant de dessiner la scene), mais attention aux paramètres implicites du draw() !
(par exemple les dimensions de l'image... cf glViewport())


exercice 3 : et maintenant les ombres !!

et voila, tout est pret !! il ne reste plus qu'à dessiner la scène depuis le point de vue de la camera et à écrire le test qui vérifie qu'un point (vu par la camera) est plus près ou plus loin que le triangle éclairé par la source de lumière. les détails de la projection et des transformations sont dans "decals et projection de textures" / section "finir le calcul : projetter le décal"

attention : dans le shader, la fonction texture(shadow_map, uv) renvoie un vec4, la distance stockée dans la texture se trouve dans la composante .x ou .r (et pas .z qui sera toujours 0...)


exercice 4 : et maintenant ? des ombres propres !!

et voila ça marche !


ou pas, c'est quand même moche, non ?

argh, mais pourquoi ??

le zbuffer stocke une distance par pixel lorsque l'on dessine un triangle pour le point de vue de la source de lumière :


ie la distance entre le point rouge et la source. mais lorsque l'on dessine le même triangle depuis la camera / un autre point de vue :




les centres des fragment de l'image se projettent sur des point différents à la surface du triangle. et comparer la distance des points rouges et des points noirs à la source de lumière est assez approximatif, ce qui crée tous les defauts des ombres dans l'image précédente.

bien sur, il y a plusieurs solutions : on peut ajouter une tolérance lors de la comparaison (comme pour le lancer de rayon, cf doc en ligne), on peut arrondir la position du point pour correspondre au point stocké, et recalculer sa distance avant de faire la comparaison, on peut aussi stocker (ou recalculer) la normale du triangle et interpoler la distance, etc. mais le test reste quand même fragile. on peut aussi stocker l'indice du triangle, dessiner les faces arrieres au lieu des faces avant (en modifiant les parametres du test d'orientation des triangles), etc. mais ces tests ne sont pas plus simples ni plus robustes.

on peut aussi remarquer que le test est faux puisque l'on compare les distances de 2 points differents alors qu'il assez simple de faire un test plus robuste (en faisant un peu plus de calculs...)

l'idée est relativement simple : on connait le point p (visible depuis la camera, en noir), et il faut calculer le point sur le même triangle que p qui se projette sur le centre du texel, en rouge. c'est un calcul d'intersection entre le plan du triangle et le rayon entre la source de lumiere et le centre du pixel du zbuffer sur lequel se projette p. cf principes du lancer de rayon / intersection rayon / plan pour les détails du calcul. au final, le plus "compliqué" est d'obtenir les coordonnées des points et vecteurs dans le même repère pour faire les calculs d'intersection.





pourquoi ça marcherait mieux que les autres tests ? par construction chaque pixel du zbuffer / shadow map conserve la distance entre le point visible pour le centre du fragment et la source de lumière / centre de la projection utilisée pour dessiner les triangles. lorsque l'on dessine les mêmes triangles pour le point de vue de la camera, avec une autre projection, le centre d'un fragment d'un triangle se projette sur un autre point du triangle. et comme ce sont des points différents, leurs distances à la source sont différentes. cette solution recalcule le point du triangle visible par le centre du fragment, ce qui permet de comparer cette profondeur à celle stockée dans le zbuffer.

et ça marche ?