Modélisation Procédurale

Objectif de scène 3D obtenue en fin de séance
Code
Considérons désormais le code présent dans le répertoire scenes_inf443/03b_modeling/- > Assurez vous que cette scène compile et s'exécute.

Fichiers:
- - main.cpp: ne contient que les appels génériques pour créer la fenêtre, gérer les interactions claviers/souris, et lancer la boucle d'affichage. Les objets propres à la scènes sont initialisés et affichés par le biais d'un objet scene.
- - scene.cpp/hpp: Il s'agit de la classe où sont définis les objets propres à la scène, et contient les variables nécessaires à leur affichage.
-
- - Scene contient la fonction initialize() qui est appelée une unique fois depuis la fonction main() avant le premier affichage, et la fonction display_frame() qui est appelée en permanence lors de la boucle d'affichage.
- - terrain.cpp/hpp et tree.cpp/hpp: sont des fichiers annexes qui permettent de regrouper les fonctionalités de
-
- - création/gestion d'un terrain sous la forme d'un champ de hauteur,
- - création d'arbres réalisé à partir de simple primitives.
Modélisation du terrain
Principe
La surface affichée correspond à un "terrain" définie sous la forme d'un champ de hauteur \(z=f(x,y)\). La fonction calculant cette hauteur pour un \((x,y)\) donné est la fonction evaluate_terrain_height(x,y) dans le fichier terrain.cpp. Pour afficher ce terrain, il est nécessaire de génerer un maillage. Cela est réalisé par la fonction create_terrain_mesh qui prend en paramètre la longueur du terrain suivant x/y.Les sommets de ce maillages sont tels que les coordonnées \((x,y)\) échantillonnent une grille régulière le long des direction \(x\) et \(y\). Et la hauteur \(z\) est obtenue par l'appel à la fonction evaluate_terrain_height.
// Fill terrain geometry for(int ku=0; ku<N; ++ku) { for(int kv=0; kv<N; ++kv) { // Compute local parametric coordinates (u,v) \in [0,1] float u = ku/(N-1.0f); float v = kv/(N-1.0f); // Compute the real coordinates (x,y) of the terrain float x = (u - 0.5f) * terrain_length; float y = (v - 0.5f) * terrain_length; // Compute the surface height function at the given sampled coordinate float z = evaluate_terrain_height(x,y); // Store vertex coordinates terrain.position[kv+N*ku] = {x,y,z}; } }
Fonction de hauteur
A l'heure actuelle, la hauteur du terrain est définie comme une gaussienne centrée sur l'origine,float evaluate_terrain_height(float x, float y) { vec2 p_0 = { 0, 0 }; float h_0 = 2.0f; float sigma_0 = 3.0f; float d = norm(vec2(x, y) - p_0) / sigma_0; float z = h_0 * std::exp(-d * d); return z; }
On pourra utiliser les paramètres suivants:
-
- \(p_i=\{(-10,-10),(5,5),(-3,4),(6,4)\}\)
- \(h_i=\{3,-1.5,1,2\}\)
- \(\sigma_i=\{10,3,4,4\}\)


- - Notez que le terrain est définie sur l'intervalle \((x,y)\in[-10,10]\) et le maillage contient \(100\times100\) sommets.
-
- - Ces paramètres peuvent être modifiés dans la fonction initialize().
- - Vous pouvez définir un vecteur d'éléments de taille fixe avec la syntaxe suivante:
vec2 p_i[4] = { {-10,-10}, {5,5}, {-3,4}, {6,4} }; float h_i[4] = {3.0f, -1.5f, 1.0f, 2.0f};
Une autre possibilitée serait:
std::array<vec2,4> p_i = { vec2{-10,-10}, vec2{5,5}, vec2{-3,4}, vec2{6,4} }; std::array<float,4> h_i = {3.0f, -1.5f, 1.0f, 2.0f};
Modélisation d'arbres sur le terrain
Nous souhaitons modéliser une série d'arbres/sapins présents sur le terrain. Pour cela, nous allons modéliser un arbre individuel à l'aide de primitives géométriques, puis réaliser son affichage en de multiples endroits du terrain.
Modélisation du tronc
Le tronc est modélisé sous la forme d'un cylindre, plus précisément sont approximation par un maillage triangulaire.
Une fonction create_cylinder_mesh dans le fichier tree.cpp est prévue pour générer ce cylindre, votre objectif est de la compléter.
Le rayon et la hauteur du cylindre sont passés en paramètre. On pourra supposer qu'il s'agit d'un cylindre dont l'axe central est orienté suivant la coordonnée \(z\), et centré de manière à passer par l'origine.

Rem. Pensez au préalable à la manière dont vous allez ordonner les sommets et les triangles. Le cylindre peut rester une surface ouverte à ces extrémitées. Pensez à vérifier votre résultat en affichant votre cylindre avant de passer à la suite (l'affichage en mode wireframe peut également aider au debug si nécessaire).
Rem. 2 L'ordre dans lesquels les sommets sont énumérés pour représenter un triangle est utilisé pour donner une orientation à la normale des triangles. Il faut donc énumérez ceux-ci de manière cohérente pour obtenir une illumination homogène de votre surface. Si des oscillations sombres et clairs appraraissent, l'orientation de vos triangle peut être en cause.

Droite: Illumination attendue.
- - m.position est un buffer/tableau contenant des vec3.
- - Il est possible d'ajouter des valeurs au fur et à mesure dans un buffer. La taille de celui-ci augmente alors à chaque ajout
m.push_back(vec3{x,y,z});
- - Il est également possible de pré-alouer la taille d'un buffer pour y écrire des valeurs à des indices spécifiques par la suite.
m.position.resize(N);
-
- Notez que cette taille peut être modifiée au cours de l'exécution du programme par d'autres appels à .resize().
- - L'écriture à l'indice k (0 < k < N) du buffer se réalise alors avec la syntaxe
m.position[k]=vec3{x,y,z};
-
- Rem. N'utilisez pas la syntaxe buffer[-1] spécifique à Python. En C++, cela tente d'accéder à la case mémoire -1, c-a-d l'élément mémoire précédent le début du buffer. Ce qui peut aboutir à une erreur mémoire (Segmentation Fault), ou à une lecture de valeurs arbitraires. Le dernier élément peut être obtenu avec l'appel à buffer[buffer.size()-1].
- - Il est également possible de parcourir l'ensemble des valeurs de ce tableau avec les syntaxes suivantes:
// Parcours classique avec des indices: for(int k=0; k<m.position.size(); ++k) { vec3 p = m.position[k]; // ou vec3& p = m.position[k] pour récupérer une référence plutôt qu'une copie // ou encore m.position[k] = ... } // Parcours avec "range-based for loop" (copie) for(vec3 p : m.position) { // p recoit la copie des éléments de m.position // ex. std::cout<< p <<std::endl; } // Parcours avec "range-based for loop" (référence) for(vec3& p : m.position) { // p = ... // p étant une référence, il peut modifier les éléments de m.position }
Modélisation du feuillage
Le feuillage est constitué de 3 cones successifs (toujours sous forme de maillage).
Complétez la fonction create_cone_mesh de manière à créer un maillage de forme conique. Les paramètres d'entrées de la fonction sont respectivement: le rayon à la base du cone, la hauteur de celui-ci, un offset à appliquer entre \(z=0\) et la base du cone.
Rem. Le bas du cone représentant la base du feuillage, pensez à construire cette fois une surface fermée. Pensez également à vérifier votre résultat en affichant votre cylindre avant de passer à la suite.

(Après l'avoir réalisé vous même, ou si vous éprouvez des difficultés, un code solution possible de création du cone est proposé ici)
Arbre complet
Une fois les fonctions de tronc et de feuillage réalisées, il est possible d'assembler des éléments ensembles pour former l'apparence d'un arbre.
Créez la fonction suivante dans le fichier tree.cpp
mesh create_tree() { float h = 0.7f; // trunk height float r = 0.1f; // trunk radius // Create a brown trunk mesh trunk = create_cylinder_mesh(r, h); trunk.color.fill({0.4f, 0.3f, 0.3f}); // Create a green foliage from 3 cones mesh foliage = create_cone_mesh(4*r, 6*r, 0.0f); // base-cone foliage.push_back(create_cone_mesh(4*r, 6*r, 2*r)); // middle-cone foliage.push_back(create_cone_mesh(4*r, 6*r, 4*r)); // top-cone foliage.translate({0,0,h}); // place foliage at the top of the trunk foliage.color.fill({0.4f, 0.6f, 0.3f}); // The tree is composed of the trunk and the foliage mesh tree = trunk; tree.push_back(foliage); return tree; }
et ajoutez la signature de cette fonction dans le fichier tree.hpp (cgp::mesh create_tree();)
Si la signature de la fonction n'est pas présente dans le fichier tree.hpp, il ne sera pas possible d'appeler cette fonction depuis scene.cpp (scene.cpp inclue le fichier tree.hpp).
Notez que le tronc correspond au cylindre, et le feuillage est formé par trois cones placés l'un au dessus de l'autre puis concaténé dans un seul et même maillage par l'utilisation de la fonction push_back sur des structures mesh.

Les sommets du tronc et des cones sont affectés à des couleurs différentes. et finalement les maillages sont concaténés dans un seul et même objet de type "mesh".
Rem. L'utilisation de push_back sur des maillages permet de concaténer les buffers de données associées (coordonnées des sommets, couleurs des sommets, normales des sommets, indices formants les triangles). Cela permet de ne manipuler qu'une seule entitée "mesh" à la place de plusieurs. Par contre, cela signifie également qu'il n'est plus possible de manipuler ces entités individuellement par la suite (ex. si le tronc et feuillages possèdent une texture différente, il sera nécessaire de les gérer séparéments).
Peuplement du terrain
Positionnement d'un arbre unique
Essayez dans un premier temps de placer cet arbre à différents endroits du terrain. Pour cela, il est possible d'utiliser le paramètre uniforme de translation de la structure mesh_drawable ([mesh_drawble].model.translation = ...) afin d'afficher l'arbre à une position donnée (principe similaire au cas de la translation du cube dans la scène d'introduction). Notez que pour connaitre la hauteur d'un point sur le terrain de coordonnées \((x,y)\), vous pouvez explicitement appeler la fonction evaluate_terrain_height(x,y).Positionnement de plusieurs arbres.
Pour positionner plusieurs arbres, l'idée est de pré-stocker un ensemble de \(N_a\) positions sur le terrain. On note \(p_i\) la ième position pré-stockée (\(i\in[0,N_a-1]\)). Au moment de l'affichage, on applique alors l'algorithme suivant
For all positions pi tree.model.translation = pi draw(tree, environment);
std::vector<cgp::vec3> generate_positions_on_terrain(int N, float terrain_length);
- - Pour cela, on pourra calculer un ensemble de positions \(p_i\) tels que \(p_i\)=evaluate_terrain(x,y), avec (x,y) échantillonnés suivant une distribution aléatoire uniforme sur la taille du terrain.
- - La fonction rand_uniform() permet d'obtenir un nombre aléatoire entre 0 et 1 suivant une distribution uniforme. Vous pouvez également appeler rand_uniform(a,b) qui génère ce nombre alétoire sur l'intervalle \([a,b]\).
- Rappel:
- Pour ajouter une valeur dans un std::vector, on peut utiliser la syntaxe: nomVariable.push_back(valeur)
- Il est également possible de pré-allouer le vecteur, puis d'écrire à l'indice souhaité
-
- nomVariable.resize(maximalSize)
- nomVariable[k] = valeur;
- Attention: si vous accédez à un indice d'un vector au dela de la dimension allouée, il s'agit d'une erreur d'accès mémoire qui peut terminer brutalement l'execution (Segmentation Fault).
Une fois la fonction réalisée, appelez celle-ci dans votre scene et stockez les valeurs dans une variable globale de type std::vector<cgp::vec3> tree_position;.
- Cette variable doit être déclarée comme une variable de la classe scene (donc déclarée dans scene.hpp), car elle sera utilisée dans la fonction d'initialisation et d'affichage.
- N'initialisez les valeurs de tree_position qu'une seule fois à l'initialisation
-
- Que se passerait-il si vous initalisez vos valeurs de tree_position dans la fonction display() ?
- Utilisez ensuite ces positions de tree_position pour afficher votre arbres à ces multiples positions.
- tree_position est une variable déclarée dans l'en-tête comme une variable de la classe. Il ne faut donc pas re-déclarer son type lors de son initialisation:
-
- [OK]: tree_position = generate_positions_on_terrain(150, terrain_length);
- [KO]: std::vector<cgp::vec3> tree_position = generate_positions_on_terrain(150, terrain_length);
- Le second cas vient re-générer une variable locale à la fonction d'initialization. C'est cette variable qui sera affecté et non pas la variable de la classe qui restera alors vide.

-
- Notez qu'une seule instance de l'arbre est nécessaire. Il s'agit d'un seul arbre affiché plusieurs fois à des positions différentes. Faites bien attention en particulier de ne pas recréer de données de type "mesh_drawable" dans la boucle d'affichage.
- - Il est possible d'ajuster légèrement la hauteur des arbres (en abaissant la coordonnée z de la translation) pour éviter qu'ils apparaissent "hors du sol" à cause de la courbure du terrain.
Extensions
Extension 1 Faites en sorte d'éviter que deux arbres puissent être trop proches l'un de l'autre - et notamment qu'ils ne puissent s'intersecter. Pour cela, vous pourrez adapter l'algorithme créant les positions stockées des arbres. Rem. Compte tenu du faible nombre d'arbres et du calcul réalisé une unique fois à l'initialisation, un algorithme en complexité quadratique ne pose pas de problème.
Extension 2 (optionnel) En suivant une démarche similaire, il est possible d'ajouter d'autres objets formés de primitives simples. Exemple avec l'ajout de champignons.
- - Lors de l'affichage d'un très grand nombre d'objets, le programme va devenir lent du fait de l'appel explicite à draw pour chaque objet individuel.
- - Une méthode efficace pour l'affichage d'un grand nombre d'objets similaire consiste à réaliser de l'instancing.
- - L'utilisation de l'instancing via la bibliothèque CGP est documenté ici: [Use instancing], [Exemples de codes].