CPE 4 ITI 2017-2018


TP1 - pipeline et shaders


L'objectif du tp est de comprendre comment décrire un objet 3d, de le placer et de l'orienter dans le monde. Dans un premier temps, vous ne manipulerez pas directement openGL pour vous concentrer sur l'essentiel : le pipeline, et ses entrées : attributs de sommets, et transformations.


Partie 1 : application interactive

Dézippez gKit, et générez les projets pour votre environnement de développement. si nécessaire relisez le tuto1.

Vérifiez que tout fonctionne, par exemple :

premake4 gmake
make tuto1GL -j8
./bin/tuto1GL


N'oubliez pas qu'il y a de nombreux tutos et explications dans la doc !

Partie 2 : c'est vide !

exercice 1 : un carré

compilez tutos/tuto2.cpp (cf make tuto2) et utilisez la classe Mesh pour décrire un carré affichable sans transformation.

modifiez la fonction init(). Pour afficher l'objet, il faudra modifier la fonction draw( ).


quels points sont directement affichables (quelle propriété vérifie leurs coordonnées) ?
quelle orientation utiliser pour décrire les sommets du carre ?
comment indiquer à openGL celle que vous avez choisi ?

si nécessaire, relisez le tuto3.

il y a 2 manières d'utiliser les fonctions de Mesh :

vérifiez que l'orientation de vos triangles est correcte.
par défaut la fonction draw(mesh) affiche les triangles mal orientés avec des hachures oranges.
activez ou désactivez le test d'élimination des faces arrières (cf back face culling, tuto3) et changez l'orientation, si nécessaire.

remarque : on peut définir une couleur par sommet avec mesh.color( Color(r, g, b) ).


exercice 2 : un repère

modifiez votre programme pour afficher les 3 axes XYZ du repère du monde. Vous pouvez décrire des droites avec Mesh(GL_LINES).
chaque droite est composée de 2 sommets.

remarque : vous pouvez dessiner des lignes plus épaisses avec glLineWidth(1.5); (à placer dans init()).


exercice 3 : une camera

utilisez une camera (cf la classe Orbiter) pour observer votre scene 3d.
dessinez les 3 axes pour le point de vue de la camera.

vous pouvez déplacer la position de la camera avec Orbiter::move().
vous pouvez utiliser key_state(SDLK_UP, SDLK_DOWN, SDLK_LEFT, SDLK_RIGHT) pour contrôler les déplacements avec les fleches du clavier, ou n'importe quelle lettre, par exemple : key_state('j')).

dessinez chaque axe d'une couleur différente, par exemple l'axe X en rouge (cf mesh.color(Red())), l'axe Y en vert (cf mesh.color(Green()) et l'axe Z en bleu (cf mesh.color(Blue()).


exercice 4 : une grillle

modifiez votre programme pour dessiner une grille de 10x10 cases en plus des axes du repère. les limites de la grille seront -5, 5 sur X et Z.



vous pouvez aussi utiliser les déplacements de la souris pour controler la camera, cf Orbiter::rotation() et SDL_GetRelativeMouseState().
par exemple :

        Orbiter camera;

        // deplace la camera
        int mx, my;
        unsigned int mb= SDL_GetRelativeMouseState(&mx, &my);
        if(mb & SDL_BUTTON(1))              // le bouton gauche est enfonce
            camera.rotation(mx, my);
        else if(mb & SDL_BUTTON(3))         // le bouton droit est enfonce
            camera.move(mx);
       

Partie 3 : c'est plat !

L'objectif est de construire une géométrie plus "interressante", par exemple un terrain, à partir d'une carte d'altitude.

Il est très simple de charger une image et de parcourir ses pixels :

#include "color.h"
#include "image.h"

#include "image_io.h"

Image data= read_image("data/terrain/Clipboard01.png");

for(int y= 0; y < data.height(); y++)
for(int x= 0; x < data.width(); x++)
{
    Color pixel= data(x, y);
    ...

}

Chaque groupe de 4 pixels voisins va permettre de décrire 2 triangles, les coordonnées des sommets seront dans le plan xz et l'altitude sera sur l'axe y.
utilisez les coordonnées x, y des pixels dans l'image comme coordonnées des sommets dans le plan.

Pour afficher le terrain complet, il faut régler la camera avec les points extremes :
Mesh terrain= Mesh(GL_TRIANGLES);
...

Point pmin, pmax;
terrain.bounds(pmin, pmax);

Orbiter camera;
camera.lookat(pmin, pmax);

remarque :
attention à l'orientation des triangles...
remarque : attention la camera regarde l'axe -Z...




Partie 4 : shaders

Vous utilisez pour l'instant les utilitaires de gKit (cf draw.h) pour construire les buffers d'attributs et configurer le vertex array (cf Mesh::create_buffers()), construire, compiler (cf Mesh::create_program() et parametrer un shader (cf draw(Mesh, Orbiter))...

L'objectif de cette partie est d'écrire un premier shader (et de le parametrer...) afin d'afficher le terrain, à la place du shader construit automatiquement par gKit.

rappel :
le vertex shader transforme les coordonnées des sommets des triangles depuis un repère arbitraire vers le repère projectif homogène de la camera.
il a donc besoin de 2 informations : les coordonnees du sommet à transformer et la transformation (une matrice 4x4) à appliquer.
les coordonnées du sommet sont traitées directement par le pipeline (cf vertex array et buffers), par contre la matrice de transformation est une entrée (un paramètre uniform) que l'application doit affecter au shader.

un vertex shader minimaliste ressemblera à :
#version 330

uniform mat4 matrix;   
layout(location= 0) in vec3 position;

void main( )
{
    gl_Position= matrix * vec4(position, 1);
}
la déclaration de l'attribut position respecte une convention particulière : la décoration layout(location= 0) indique que l'attribut position est l'attribut numero 0, et la classe Mesh crée un buffer et configure un vertex array en utilisant cette convention.

le fragment shader doit simplement renvoyer une couleur :
#version 330

void main(
)
{
    gl_FragColor= vec4(1, 0, 0, 1);    // tous les pixels seront rouge.
}

l'application est responsable de compiler les 2 shaders, de les linker dans un program et d'affecter une valeur à tous les uniforms / paramètres avant de pouvoir dessiner :
étape 1 : compiler les shaders
    le plus simple est d'utiliser un utilitaire (cf tuto) pour compiler les shaders, et d'afficher les erreurs eventuelles :
#include "program.h"

GLuint program;

init( )
    program= read_program("...");

    program_print_errors(program);
    il faut écrire les 2 shaders dans le même fichier texte (extension au choix, .glsl est classique), en les séparant par des #ifdef / #endif  :
#version 330

#ifdef VERTEX_SHADER
uniform mat4 matrix;   

layout(location= 0) in vec3 position;

void main( )
{
    gl_Position= matrix * vec4(position, 1);
}
#endif


#ifdef FRAGMENT_SHADER
void main(
)
{
    gl_FragColor= vec4(1, 0, 0, 1);    // tous les pixels seront rouge.
}
#endif


étape 2 : affecter une valeur à un uniform
    utiliser la bonne version de glUniform() en fonction du type de la valeur, les détails sont dans le tuto.
   
    quelle transformation utiliser pour afficher le terrain ?
    rappel : les transformations standards sont model, view, projection et viewport. et gKit définit la classe Transform (cf mat.h) pour les représenter / manipuler.

    a priori, model est l'identité dans l'exemple du terrain, et les transformations view et projection sont calculées par la camera, cf Orbiter::view() et Orbiter::projection(). il suffit de les composer ensemble pour connaitre la transformation complète.
    par exemple :
#include "mat.h"

Orbiter camera;

draw( )
    Transform model= Identity();
    Transform view= camera.view();
    Transform projection= camera.projection(window_width(), window_height(), 45);

    Transform t= ... ; // composer les 3 matrices, dans quel ordre ? model * view * projection, ou autre chose
   
    // selectionner le shader program

    glUseProgram(program);


    // affecte une valeur au parametre declare dans le vertex shader
    //
uniform mat4 matrix; cf l'exemple ci-dessus

    //   etape 1 : recupere l'identifiant de la variable declaree dans le shader
    GLint location= glGetUniformLocation(program, "matrix");

    //   etape 2 : affecte une valeur à la variable déclarée dans le shader, utilise la "bonne" version de glUinform

    glUniformMatrix4fv(location, 1, GL_TRUE, t.buffer());
   
    // ou :
    // utilise un utilitaire de uniforms.h
    // program_uniform(program, "matrix", t);

étape 3 : dessiner !
draw( )
    // dessine le terrain
    draw(terrain, program, /* position */ true, /* texcoord */ false, /* normal */ false, /* color */ false);

    les derniers paramètres de la fonction draw( ) indiquent quels attributs doivent etre transferés dans un buffer et configurés dans un vertex array (cf Mesh).
    le vertex shader  minimaliste n'utilise qu'un seul attribut, position.

vous pouvez consulter un exemple complet pour vous aider à comprendre comment tout ça se connecte, cf application tuto3GL.cpp et shader


exercice 1 :
écrivez les shaders minimalistes pour dessiner le terrain. chargez et compilez le program avec read_program() dans la fonction init( ).

pour afficher le terrain dans la fonction draw( ) : composez les transformations nécessaires, affectez la valeur à la matrice déclarée dans le vertex shader :
Mesh terrain;
GLuint program;


init( )
    program= read_program("...");
    program_print_errors(program);

    terrain= ...


draw( )
    Transform t= ... ;


    // configure le pipeline, selectionne le shader program
    glUseProgram(program);


    // affecte une valeur au parametre declare dans le vertex shader
    //
uniform mat4 matrix; cf exemple ci-dessus

    //   etape 1 : recupere l'identifiant de la variable declaree dans le shader
    GLint location= glGetUniformLocation(program, "matrix");

    //   etape 2 : affecte une valeur à la variable déclarée dans le shader, utilise la "bonne" version de glUinform

    glUniformMatrix4fv(location, 1, GL_TRUE, t.buffer());

    // dessine le terrain
    draw(terrain, program, /* position */ true, /* texcoord */ false, /* normal */ false, /* color */ false);


les derniers paramètres de la fonction draw( ) indiquent quels attributs doivent etre transferés dans un buffer et configurés dans un vertex array (cf Mesh). le vertex shader minimaliste n'utilise qu'un seul attribut, position.


exercice 2 :
calculez la couleur du fragment en fonction de l'altitude.



c'est le vertex shader qui connait les coordonnées du sommet et donc son altitude. il est plus simple de calculer la couleur dans le vertex shader et de la transmettre au fragment shader en déclarant un varying : une sortie supplémentaire dans le vertex shader et l'entree associée dans le fragment shader.

rappel :
#version 330

#ifdef VERTEX_SHADER

layout(location= 0) in vec3 position;  // attribut 0
uniform mat4 matrix;                   // parametre uniform

out vec3 color;                        // varying, sorite du vertex shader

void main( )
{
    gl_Position= ... ;
    color= ... ;                       // affecter une valeur au varying
}
#endif


#ifdef FRAGMENT_SHADER
in vec3 color;                         // meme type, meme nom que la sortie declaree dans le vertex shader

void main( )
{
    gl_FragColor= vec4(color, 1);
}
#endif



exercice 3 :
si l'altitude d'un sommet est faible, utilisez une couleur bleue, par exemple, pour representer la surface d'un ocean.



exercice 4 :
modifiez le vertex shader pour animer la surface de l'ocean.

vous pouvez utiliser une fonction périodique, comme un sinus, par exemple, pour modifier l'altitude des sommets de la surface de l'ocean / de faible altitude.
vous pourrez également utiliser le temps pour animer la surface, cf global_time() de window.h qui renvoie le temps écoulé depuis le lancement de l'application (exprimé en millisecondes, 1 image = 16ms)

comment transmettre la valeur de ce nouveau paramètre aux shaders ?


exercice 5 / bonus
utilisez l'orientation des triangles, leur normale, pour modifier leur couleur de base.

comment calculer la normale de chaque triangle ?
comment calculer la normale de chaque sommet (de chaque trianfgle) ?
comment affecter ces valeurs à un attribut déclaré dans le vertex shader ?
comment modifier la couleur de base en fonction de la normale ?