M2 image

TP2 - ca va couper ?
affichage efficace d'un (grand) terrain




Partie 1 : préparation

Créez une structure Region qui servira à regrouper toutes les informations nécessaires pour afficher une partie du terrain. Créez également une structure Terrain ou Scene qui permettra de parcourir toutes les régions.

Une région sera un block de 64x64 cubes, il faudra conserver les informations permettant d'afficher ces cubes / instances. Par exemple, la translation permettant de placer chaque instance dans la scène ainsi que la matière associée. Les coordonnées de l'englobant seront aussi nécessaires pour les tests de visibilité.

La scène (ou le terrain) sera un simple ensemble de régions.

Comment organiser les données ?
Il y a bien sur plusieurs solutions :
  1. régions indépendantes : 1 ou plusieurs buffers + vertex array object décrivants les translations à appliquer à chaque instance,
  2. scène : la scène conserve un gros buffer avec les translations de toutes instances de toutes les régions et 1 seul vao. une région ne stocke que 2 nombres : l'indice de la première instance et le nombre d'instances à dessiner.

Pour choisir entre ces solutions (et il y en a d'autres), il faut imaginer les conséquences pour dessiner le terrain complet. L'algorithme complet est très simple :

Selon l'organisation des données, afficher les régions visibles va nécessiter de reconfigurer le pipeline, ou pas.
remarque : on peut s'attendre à ce que la meilleure solution soit celle qui utilise le moins openGL (qui nécessite le moins d'appels de fonctions de l'api 3d)...

  1. pour chaque region de la scène
        si la region est visible
            glBindVertexArray()
            glDrawInstanced()

  2. glBindVertexArray()
    pour chaque region de la scene
        si la region est visible
            glDrawInstancedBaseInstance()

complétez ces 2 solutions en imaginant que chaque région contient des instances de plusieurs types de matieres, qui nécessitent des shaders differents (et des textures différentes...) pour être dessinées. quelle est, a priori, la meilleure solution ?


Construction du terrain

comme présenté en TP, prennez le temps d'écrire les fonctions qui calculent les pentes et éventuellement les normales, qui permettent de choisir une unité de modélisation et d'affecter une matière à chaque instance.

vous pouvez utiliser la fonction Image::sample( const float x, const float y ) pour évaluer l'altitude d'un point quelconque, ou lieu de n'utiliser que des coordonnées entières avec Image::operator() ( const int x, const int y ).



Partie 2 : régions et visibilité

Pour déterminer qu'une région est visible, il faut connaitre ses coordonnées min et max sur les 3 axes, le cube englobant de la région.
Ecrivez une fonction permettant de déterminer si cet englobant est séparé du frustum de la camera.

dans quel repère peut-on faire le test ?
comment vérifier que votre test fonctionne correctement ?

indication : si vous avez besoin de connaitre le poids homogène d'un point transformé dans le repère projectif (homogène), utilisez :
Point p= { ... };
Transform mvp= { ... };

vec4 ph= mvp(vec4(p));
// ph.w est le poids homogene et ph.xyz sont les coordonnées du point p projetté


modifiez la boucle d'affichage de la scène pour n'afficher que les régions visibles. comptez le nombre de regions affichées et réglez les distances proche et loin de la projection de la camera pour rester le plus souvent possible dans le budget de 256x256 cubes affichés.

remarque : c'est la fonction Perspective() de mat.h qui construit la projection. vous aurez besoin de modifier la camera, cf la classe Orbiter

indication : n'hésitez pas à suréchantilloner le terrain pour générer une très grande surface, 4096x4096, par exemple.

bonus : écrivez une classe camera première personne pour déplacer librement l'observateur dans la scène, au lieu de tourner autour, comme l'orbiter. vous aurez besoin de la fonction LookAt() de mat.h.
rappel : la transformation View est l'inverse de la transformation qui permet de placer et d'orienter la camera dans la scène.


Partie 3 : plusieurs matières

chargez le pack de textures de dokucraft, cf tp précédent, les textures des blocs des matières de minecraft se trouvent le répertoire assets/minecraft/textures/blocks. repérez les textures permettant d'afficher les différents types de terrain que vous souhaitez : herbe, pierre, terre, eau, etc.

comment afficher des cubes texturés ? reliez le tuto sur les textures, ou la version simplifiée, si nécéssaire.

dans un premier temps, utilisez la même texture sur les 6 faces des cubes, par exemple grass_top.png. que faut-il modifier pour texturer différemment les faces des cubes ? par exemple : une texture sur la face supérieure et une texture différente sur les autres faces, cf grass_top.png, grass_side.png ?

comment afficher des touffes d'herbes, le feuillage des arbres, les fleurs, etc. ? les textures sont semi-transparentes, certains texels sont opaques (alpha=1), mais pas tous (alpha < 1)... par exemple : fern.png, double_plant_grass_top.png et double_plant_grass_bottom.png.

rappel : mot clé discard dans GLSL


Partie 4 : et les ombres ?

exercice 1 : modifiez les fragment shaders afin de calculer la couleur de chaque matière en fonction de la pente du terrain et de la direction du soleil.

rappel :
le résultat dépend du cosinus de l'angle entre la normale et la direction du soleil :

float cos_theta= dot(normalize(n),normalize(soleil));

dans quel repère peut-on faire le calcul ? est-il nécessaire de transformer certaines directions ?

exercice 2 : pour déterminer qu'un point du terrain est à l'ombre, il faut commencer par dessiner le terrain depuis le point de vue du soleil.
quelle transformation, point de vue, projection, etc. utiliser pour observer le terrain depuis la direction du soleil ?

indication :
vous aurez probablement besoin d'une projection orthographique
Transform Ortho( const float left, const float right, const float bottom, const float top, const float znear, const float zfar )
{
    float tx= - (right + left) / (right - left);
    float ty= - (top + bottom) / (top - bottom);
    float tz= - (zfar + znear) / (zfar - znear);
   
    return Transform(
        2.f / (right - left),                    0,                     0, tx,
                           0, 2.f / (top - bottom),                     0, ty,
        0,                                       0, -2.f / (zfar - znear), tz,
        0,                                       0,                     0, 1);
}

        cf glOrtho(), ou les détails de la construction sur scratchapixel.com, mais attention à l'orientation des matrices !!

exercice 3 : écrire le zbuffer dans une texture.
vous aurez besoin de configurer et de dessiner dans un framebuffer object, ou fbo, cf tuto rendu multi-passses.

exercice 4 : afficher les ombres.
il ne reste plus qu'à dessiner "normalement" depuis la camera et dans la fenêtre (associée au fbo 0).
modifiez les fragment shaders pour récupérer dans le zbuffer la distance de l'objet visible et éclairé par le soleil et déterminez si le soleil est visible depuis le point du terrain associé au fragment.

bonus : comment organiser la description des régions afin de pouvoir dessiner chaque région avec un seul draw ? pourquoi ?