1/5


1. 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".

  • Séance modélisation (I): Création d’un terrain et ajout d’arbres

  • Séance texture (II): ajout de texture et de billboard sur le terrain. Ajout de déformation procédurale sur le terrain.

  • Séance animation par modèle générateur (III): Ajout d’un modèle d’oiseau suivant une trajectoire dans la scène

  • Séance animation par modèle physique (IV): Ajout d’une corde


Pour vous familiariser avec les notions de textures et d’animations en dehors de votre scène "principales", d’autres petites 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

3d scene

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

  • Des vecteurs et matrices (2D, 3D, 4D) - en partie similaires à celles disponible en GLSL

  • Des structures de maillages prêtent à être affichées avec OpenGL, ainsi que les shaders associées

  • Des controleurs de scène 3D tel qu’une caméra controlable à la souris.

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

  • Code écrit de manière minimaliste, afin d’être simple à comprendre (pas nécessairement le plus générique ou efficace).

  • Objets écrits pour être légers et sans états cachés. Vous pouvez généralement modifier directement les attributs des objets.

  • La bibliothèque n’impose pas de "framework" de rendu, ainsi le code et les fonctionalitées restent proche d’un développement C++/OpenGL à partir de rien, et il est possible de réaliser des appels directs à OpenGL sans l’aide de la bibliothèque.

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

    • Remarque: Ce code pourra être mise à jour au cours des séances si cela se révèle nécessaire

  • Compilez et exécutez le code. Vous devriez voir apparaitre une scène 3D avec un quadrangle.

Remarques sur la compilation:

  • Un fichier README détaille les instructions de compilations

  • Sous Linux, il est possible d’utiliser le Makefile fourni.

  • Sous les autres systèmes, il est préférable d’utiliser le fichier CMakeLists.txt (vous devez avoir CMake installé sur votre système).

  • Si vous utilisez votre ordinateur personnel, il est également nécessaire d’avoir installé la bibliothèque GLFW sur votre système


  • 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

    • Rotation: clic gauche + déplacement de la souris (modèle de trackball par défaut)

    • Zoom: clic droit + déplacement haut/bas de la souris

    • Panning (/translation dans le plan de la caméra): CTRL + clic gauche + déplacement de souris

    • Avance/Recule (/translation dans le plan orthogonal à la caméra): CTRL + clic droit + déplacement haut/bas de la souris

Structure générale du code

Role des principaux répertoires

  • scenes/ Contient les codes associées à la mise en place des scènes 3D.

    • Les exercices correspondent au code décrit dans le répertoire scenes/3D_graphics/. Vous éditerez le code principalement dans ces répertoires.

    • La scène par défault peut être trouvée dans le répertoire scenes/3D_graphics/00_default/

    • Le fichier scenes/current_scene.hpp définit quelle scène (ou exercice) doit être compilé. Chaque scène est associée à un mot clé. Pour compiler une autre scène, il suffit de modifier ce mot clé.

  • vcl/ Contient le code source de la bibliothèque VCL: ensemble de structures et fonctions pour facilier la mise en place de scène 3D.

  • third_party/ Bibliothèques externes utilisées par VCL tel que glad (OpenGL loader), imgui (GUI), lodepng (chargeur d’images PNG).

  • main/ Décrit les appels principaux du programme, la structure de base d’une scène, initialise les bibliothèques externes, charge les données et lance la boucle d’animation.

Bibliothèque VCL

Le code source de la bibliothèque VCL est organisé de la manière suivante

  • vcl/base/ fonctionalités de bases de la bibliothèque tel que les assertions et gestions d’erreurs.

  • vcl/containers/ définit des conteneurs basiques avec des fonctionalités permettant de faciliter l’utilisation de buffer de données: ex. buffer = version étendue d’un std::vector, et buffer_stack = version étendue d’un std::array.

  • vcl/math/ définit des fonctions d’aide mathématiques et des structures de vecteurs et matrices 2/3/4D.

  • vcl/opengl/ contient les objets et fonctions facilitant l’utilisation les appels OpenGL.

  • vcl/shape/ correspond aux objets 3D tels que les maillages et courbes. Les formes 3D peuvent être stockés en mémoire centrale (RAM) (ex. stockage de buffer de positions), ou en mémoire du GPU dans leur version affichage (_drawable) (stocke uniquement les indices des VBOs/VAO ainsi que les paramètres uniformes).

  • vcl/interaction/ contient les objets et fonctions non géométriques associées à l’interaction avec la scène 3D tel que la caméra ou les "timers".

  • vcl/wrapper/ structures et fonctions "wrapper" pour l’utilisation de bibliothèque externes (glfw, imgui, lodepng).

Remarque sur l’éditeur de code

  • La bibliothèque contient de multiples fichiers, assurez vous que vous soyez capable d’utiliser votre éditeur de code ou IDE de manière à naviguer efficacement entre ces fichiers et à pouvoir passer rapidement de l’utilisation d’une fonction ou objet à sa signature et à son code.

  • Il vous est conseillé d’utiliser QtCreator (disponible sur les PC de l’école). QtCreator est un IDE C++ capable de charger un projet complet à partir de la lecture d’un fichier CMakeLists.txt (non invasif pour le reste du code), et propose de manière native la complétion de code.

qtcreator

  • Notez que le code doit être exécuté depuis la racine de du dossier. Vous devez paramétrer le run setting (ou run directory) de l’IDE afin de pouvoir exécuter le programme depuis celui-ci. Appelez un enseignant si vous n’avez jamais fait la procédure.

Utilisation de la bibliothèque

  • Observez le fichier main.cpp, vous devriez reconaitre l’organisation générale du programme, avec, en particulier le lancement de la boucle d’animation. Notez les appels à exercise.setup_data et exercise.frame_draw que vous compléterez dans les exercices par la suite.

  • Observez les fichiers du répertoire scenes/3D_graphics/00_default/. Chaque exercice/scène possède l’organisation suivante: Un objet scene_model est défini, et implémente (au minimum) les méthodes setup_data et frame_draw. scene_model est un objet dérivé de scene_base qui décrit la signature (et un corps vide) de différentes fonctions potentiellement utilisées depuis le programme principal. Chaque objet scene_model peut gérer une structure interne qui lui est propre et s’adapter en fonction du type de scène.

Affichage "wireframe"

Notez que l’affichage d’une forme est réalisé par la fonction draw. Cette fonction nécessite une structure de type mesh_drawable et une caméra. Si un shader a été associé à l’objet mesh_drawable, celui-ci est utilisé par défaut (dans le cas présent il s’agit du shader pré-chargé et stocké dans shaders["mesh"]).

Il est possible de forcer l’utilisation d’autres shaders lors de l’affichage.

  • Ajoutez le code suivant dans la fonction frame_draw à la suite du premier draw.

draw(surface, scene.camera, shaders["wireframe"]);

Notez qu’après exécution du nouveau programme, vous pouvez visualiser les arêtes des triangles associés au maillage. Cet affichage étant réalisé par le second appel à draw et utilisant le shader "wireframe" (le code correspondant du shader est situé dans le répertoire scenes/shared_assets/shaders/wireframe/)

Note. Il sera conseillé d’observer régulièrement l’organisation de vos triangles lorsque vous créerez des formes. L’une des approches possibles consiste à utiliser le shader affichant ces triangles sous formes de lignes.

Ajout d’une primitive

Ajoutez l’affichage d’une sphère dans la scène. Pour cela, on procèdera de la manière suivante

  • Ajoutez la déclaraction d’une structure vcl::mesh_drawable sphere; dans la déclaration de scene_model (fichier default.hpp)

  • Ajoutez le code suivant: création d’une sphère, ainsi que sa conversion en mesh_drawable dans la fonction setup_data

// Create a mesh approximating a sphere (unit radius by default)
mesh sphere_cpu = mesh_primitive_sphere(); // mesh_primitive_sphere is a helper function (several primitive are available)
// Send sphere_cpu data onto GPU
sphere = mesh_drawable(sphere_cpu);
// Set uniform parameter of the sphere
sphere.uniform.color = {1,0,0};                  //red sphere
sphere.uniform.transform.translation = {-0.1f,0.5f,0.25f}; // translate sphere display
sphere.uniform.transform.scaling = 0.1f;                   // scale the sphere to new radius for its display
sphere.shader = shaders["mesh"]; // associate default shader to sphere
  • Ajoutez l’appel à l’affichage de la sphère dans frame_draw

draw(sphere, scene.camera);

Note. Il n’est pas nécessaire de stocker de structure intermédiaire (mesh) lors de la création de la sphère, ni d’appeler explicitement le constructeur de mesh_drawable. Ainsi, la création de la sphère sous la forme mesh_drawable peut se faire par l’appel direct

sphere = mesh_primitive_sphere();

Dans ce cas, le constructeur de mesh_drawable est implicitement appelé à partir d’un objet de type mesh.


Structures mesh/mesh_drawable

La structure mesh stocke en mémoire centrale (RAM) des buffers de données

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

Ces données sont directement accessibles depuis le programme C++ . Cette structure est utile lors de la création d’une forme, ou lorsque les coordonnées doivent être modifiés interactivement. Ces données ne sont par contre pas directement accessible à la carte graphique pour réaliser l’affichage de la forme correspondante.


La structure mesh_drawable correspond à une structure de type mesh lorsque les données sont sur la mémoire de la carte graphique. Les données (position, normal, etc) ne sont plus accessible directement dans le code C++ et seuls les indices des VBO et du VAO sont stockés dans une sous structure mesh_drawable_gpu_data.
Cette structure est prette à être affiché lors de l’utilisation de la fonction draw.
Additionnellement, une sous structure mesh_drawable_uniform stocke également les paramètres uniforms qui sont envoyés à la carte graphique au moment de l’affichage.

struct mesh_drawable
{
    mesh_drawable_gpu_data data;
    mesh_drawable_uniform uniform;
    ...
}
struct mesh_drawable_gpu_data
{
    GLuint vao;
    unsigned int number_triangles;

    GLuint vbo_index;

    GLuint vbo_position;
    GLuint vbo_normal;
    GLuint vbo_color;
    GLuint vbo_texture_uv;
    ...
}
struct mesh_drawable_uniform
{
    affine_transform transform; // translation, rotation, scaling, scaling_axis
    shading_mesh shading; // ambiant, diffuse, specular, specular_exponent
    vec3 color;
    float color_alpha;
}

Affichage de multiples instances d’un même objet

Supposons que l’on souhaite afficher plusieurs sphères ayant des propriété différentes (couleur, position, etc).

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

Remplacez la ligne draw(sphere, scene.camera) par le code suivant

sphere.uniform.color = {1,1,0};
sphere.uniform.transform.translation = {0,0,0.5};
draw(sphere, scene.camera);

sphere.uniform.color = {0,0,1};
sphere.uniform.transform.translation = {1,0,0.5};
draw(sphere, scene.camera);

Observez le résultat qui affiche désormais deux sphères. La première, jaune, est située à la position \((0,0,0.5)\), alors que la seconde, bleue, est située en position \((1,0,0.5)\).

En utilisant cette approche, il est possible d’afficher un grand nombre d’objets en utilisant de manière très légère une unique instance des coordonnées de l’objet en mémoire.