Lien rapide vers la bibliothèque de code sur GitHub

Introduction

Objectif général

L'objectif de la suite des séances est de partir d'une scène vide, et d'y ajouter au fur et à mesure des séances différents éléments afin de construire une scène 3D "complexe".







Pour vous familiariser avec les notions de textures et d'animations en dehors de votre scène "principales", d'autres scènes 3D annexes minimalistes vous seront proposées.

Rem. Les deux dernières scènes finales sont ambitieuses, il ne sera pas indispensable d'arriver à incorporer l'ensemble de ces éléments dans votre scène au bout des 5 séances.

Bibliothèque de code

assets/3d_scene.png

Dans la suite des TD, vous disposez d'une bibliothèque de code (appelée VCL: Visual Computing Library) vous permettant de développer des applications en OpenGL sans avoir à tout recoder à partir de zéro.

La bibliothèque propose un ensemble de fonctionalité basique pour faciliter la programmation 3D tels que

L'objectif de cette bibliothèque est d'être aussi légère que possible afin de vous permettre de développer plus aisément, tout en restant très proche du code OpenGL basique (sans états cachées).
Bien que cela ne soit pas nécessaire, vous devriez être en mesure de comprendre l'ensemble des parties du code, qui est développé suivant les principes suivants

Rem. La bibliothèque est développée pour vos TD, il ne s'agit pas d'une bibliothèque standard.

Téléchargement et lancement de la bibliothèque

La bibliothèque de code est hébergée sur github: https://github.com/drohmer/inf443-vcl



> Téléchargez, compilez et exécutez le code présent dans le répertoire scene/inf443/00_introduction/. Vous devriez voir apparaitre une scène 3D avec un quadrangle.

Lorsque le programme est lancé un modèle de caméra manipulable à la souris/clavier est déjà implémenté. Il permet les transformations suivantes

Structure générale du code

Role des principaux répertoires

Bibliothèque VCL

Le code source de la bibliothèque VCL est organisé de la manière suivante
Rem. Pour les étudiants sous Linux/MacOS: La librairie de code contient de nombreuses fonctions et structures réparties dans plusieurs fichiers. Pour pouvoir utiliser efficacement cette librairie, il est important que votre éditeur puisse: auto-compléter vos noms de variables/functions, vous indiquer les arguments attendues par les fonctions, et vous permettre de naviguer aisément vers les définition des fonctions et structures.
Pour cela, il est fortement conseillé que vous testiez l'utilisateur d'IDE (tels que QtCreator par ex.) dans le cas où vous éditiez votre code jusqu'à présent avec des éditeurs de texte légers (Visual Studio Code, etc.).

Utilisation de la bibliothèque



Affichage "wireframe"

Notez l'appel à la fonction draw_wireframe permettant d'afficher la triangulation d'une forme à l'aide d'une couleur unique.

N'hésitez pas à utiliser cette fonction afin d'aider au debug pour mieux visualiser l'organisation des triangles des formes que vous allez pouvoir créer par la suite.

> Affichez la triangulation de la sphère par la même approche, et notez que vous pouvez adapter la couleur (r,g,b)\(\in [0,1]^3\) utilisée pour l'affichage des segments.

Ajout d'une primitive

Ajoutez l'affichage d'un cube dans la scène. Pour cela, on procèdera de la manière suivante

mesh cube_cpu = mesh_primitive_cube(); // generate the triangulation of the cube
cube = mesh_drawable(cube_cpu);        // send the mesh structure to the GPU
cube.shading.color = {0,1,0};          // set the color of the cube to be green
cube.transform.translate = {-1,-1,1};  // translate the cube
draw(cube, scene);

Rem. La fonction mesh_primitive_cube peut prendre deux arguments optionnels: le centre du cube, et la longueur des arêtes.

Rem. Structures mesh/mesh_drawable

Notez la présence des deux types de variables: mesh et mesh_drawable.

La structure mesh correspond à la structure d'une surface triangulée stockée en mémoire centrale (RAM) par des buffers de données accessibles depuis le code C++.

struct mesh
{
    buffer<vec3> position;
    buffer<vec3> normal;
    buffer<vec4> color;
    buffer<vec2> uv;
    buffer<uint3> connectivity;
    ...
}

Cette structure est utile lors de la création d'une forme, ou lorsque les coordonnées doivent être modifiés interactivement. On peut alors accéder à n'importe quelle données dans une fonction (ex. cube_cpu.position[2] += {0,-1,0};)

Ces données ne sont par contre pas directement accessible à la carte graphique pour réaliser l'affichage de la forme correspondante. On peut donc manipuler les données de cette structure, mais pas l'afficher directement

A l'inverse, la structure mesh_drawable est l'équivalent d'une structure de type mesh mais lorsque les données sont sur la mémoire de la carte graphique. Les données (position, normal, etc) ne sont alors plus accessible directement dans le code C++ et seuls les indices des VBO et du VAO sont accessibles.
De plus cette structure stocke des informations supplémentaires tels que le shader et l'indice de texture potentiellement utilisés, ainsi que les variables uniformes associées à la forme qu'il faut envoyer à chaque affichage (transformation affine, shading/couleur).
Cette structure est pensée pour être prette à être affichée lors de l'utilisation de la fonction draw.

struct mesh_drawable
{
    std::map<std::string, GLuint> vbo; // Generic storage for a set of VBOs
    GLuint vao;

    GLuint number_triangles;
    GLuint shader;
    GLuint texture;

    // Uniform
    affine_rts transform;
    shading_parameters_phong shading;
}

Une structure de type mesh peut être convertie en mesh_drawable par l'appel au constructeur mesh_drawbable(variableMesh). Lors de cette construction, les appels à glBufferData sont réalisés sur les différents champs de la variable mesh.

Notez que la variable de type mesh, et mesh_drawable sont indépendantes. Une fois mesh_drawable générée les modifications apportées à la variable mesh n'y sont pas reportée. Il ne faut donc générer la variable de type mesh_drawable qu'une fois que votre structure mesh est finalisée.

Nous verrons par la suite qu'il est possible de faire des mises à jours sur les VBO d'un mesh_drawable.

Lorsque vous afficher un maillage qui ne varie pas, la structure mesh n'est qu'une étape temporaire avant sa conversion vers une structure de type mesh_drawable servant à son affichage. Il est donc possible de créer la variable de type mesh comme une simple variable locale qui a une durée de vie limitée.
Il est également possible de se passer totalement du stockage explicite de la variable de type mesh, et d'enchainer sa création avec la conversion vers un mesh_drawable: Par exemple, il serait possible possible d'écrire cube = mesh_drawable(mesh_primitive_cube());

Notez cependant qu'il n'est pas prévue de récupérer en RAM des informations passée sur le GPU. C'est à dire qu'une structure de type mesh_drawable ne peut pas être convertie en mesh. S'il est nécessaire de modifier des éléments de la structure mesh, il faudra stocker cette variable.

Affichage de multiples instances d'un même objet

Supposons que l'on souhaite afficher plusieurs cubes ayant des propriété différentes (couleur, position, etc).

Il est possible de réutiliser une même structure mesh_drawable, mais d'afficher celle-ci à plusieurs reprise en modifiant ces paramètres uniforms avant chaque affichage.

Remplacez la ligne draw(cube, scene); par le code suivant

cube.shading.color = {1,1,0};
cube.transform.translate = {2,2,0.5};
draw(cube, scene);

cube.shading.color = {1,0,1};
cube.transform.translate = {-1,-2,0.5};
draw(cube, scene);

Observez le résultat qui affiche désormais deux cubes. Le premier, jaune, est situé à la position \((2,2,0.5)\), alors que le second, magenta, est situé en position \((-1,-2,0.5)\). Notez que les paramètres uniforms doivent être ajustés avant (et non après) la demande d'affichage de l'objet.

Cette approche est efficace pour afficher un grand nombre d'objets en étant économe en place mémoire: ici un seul maillage générique a besoin d'être stocké, mais il est affiché à plusieurs reprises avec des paramètres uniformes différents.

Affichage d'une surface

Dans cette partie, nous allons modéliser une surface sous la forme d'une fonction de hauteur \(z=f(x,y)\).

Considérez la fonction suivante

mesh create_terrain()
{
    // Number of samples of the terrain is N x N
    const unsigned int N = 100;

    mesh terrain_mesh; // temporary terrain storage (CPU only)
    terrain_mesh.position.resize(N*N);

    // Fill terrain geometry
    for(unsigned int ku=0; ku<N; ++ku)
    {
        for(unsigned int kv=0; kv<N; ++kv)
        {
            // Compute local parametric coordinates (u,v) \in [0,1]
            const float u = ku/(N-1.0f);
            const float v = kv/(N-1.0f);

            // Compute the local surface function
            const float x = 5*(u-0.5f);
            const float y = 5*(v-0.5f);
            const float z = std::exp(-(x*x+y*y));

            // Store vertex coordinates
            terrain_mesh.position[kv+N*ku] = {x,y,z};
        }
    }


    // Generate triangle organization
    //  Parametric surface with uniform grid sampling: generate 2 triangles for each grid cell
    for(size_t ku=0; ku<N-1; ++ku)
    {
        for(size_t kv=0; kv<N-1; ++kv)
        {
            const unsigned int idx = kv + N*ku; // current vertex offset

            const uint3 triangle_1 = {idx, idx+1+N, idx+1};
            const uint3 triangle_2 = {idx, idx+N, idx+1+N};

            terrain_mesh.connectivity.push_back(triangle_1);
            terrain_mesh.connectivity.push_back(triangle_2);
        }
    }

  // update the other buffers (normal, color, uv) with default parameters
  terrain_mesh.fill_empty_field();

    return terrain_mesh;
}