Dans le TP1 vous avez construit quelques maillages (repère,
grille, terrain, etc.) et appris à les afficher, d'abord avec les
utilitaires de gKit, puis avec un shader qui vous avez paramétré.
L'objectif de ce tp est de comprendre comment décrire complètement
un maillage à openGL et de le dessiner directement, sans utiliser
les utilitaires.
attention : vérifiez que les différents tests (profondeur,
orientation des faces, etc.) sont correctement configurés avant de
passer à la suite, plus de détails dans le tuto.
Pour décrire un maillage à openGL, il faut fournir les données
qui décrivent les sommets et les primitives (triangles, lignes,
etc) et les associer aux entrées déclarées par le vertex shader :
les attributs.
C'est relativement simple à faire, en 2 étapes : la première alloue de la mémoire sur la carte graphique et transfère toutes les données, la deuxième associe les données aux attributs déclarés dans le shader.
(pourquoi ? le shader est une fonction et les attributs
de sommets sont des paramètres de cette fonction... le pipeline
exécute les vertex shaders en parallèle, et il doit avoir accès à
la totalité des données)
étape 1 : buffers, allocation et transfert des données
la carte graphique possède sa propre mémoire qui n'est pas
accessible directement par le processeur principal. openGL permet
d'allouer de la memoire graphique, et renvoie un identifiant (ce
n'est pas un pointeur, les espaces d'adressage ne sont pas les
memes...) sur la zone mémoire allouée, ce qui permet par exemple
d'initialiser son contenu (copier des données). cet identifiant
s'appelle un buffer. il suffit de le créer avec glGenBuffers(), de
le sélectionner avec glBindBuffer() puis de transférer les données
(les coordonnées des sommets du maillage dans notre cas) avec
glBufferData() ou glBufferSubData().
GLuint buffer;
init( )
// construit n sommets
std::vector<Point> sommets;
{ ... }
// cree un buffer
glGenBuffers(1, &buffer);
// selectionne le buffer
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// dimensionne le buffer et l'initialise / transfere les donnees du maillage
, le vecteur sommets
glBufferData(GL_ARRAY_BUFFER, /* size */ sommets.size() * sizeof(Point), /* data */ (const float*) sommets.data(), /* usage */ GL_STATIC_DRAW);
// la taille du buffer est exprimee en octets !!
tous les détails sont dans le tuto,
section buffers.
étape 2 : configuration d'un vertex array
maintenant que les données sont dans la mémoire de la carte
graphique, il faut les associer aux attributs déclarés dans le
vertex shader. Le pipeline affecte automatiquement la valeur de
l'attribut lors de l'exécution du vertex shader. Dans le tp
précédent, le vertex shader déclare :
in vec3 position;
ce qui indique que la variable 'position' est une entrée du
shader, et qu'elle est de type vec3, c'est à dire 3 floats ou un
Point.
c'est glVertexAttribPointer( ) qui indique dans quel buffer se
trouvent les coordonnées de chaque sommet du maillage. il faut
également préciser ou se trouve la première valeur par rapport au
début du buffer et comment trouver les valeurs suivantes, cf
paramètres offset et stride de glVertexAttribPointer.
mais avant de pouvoir utiliser glVertexAttribPointer, il faut
créer un objet openGL qui conserve cet ensemble de paramètres (ou
se trouve chaque attribut), c'est un vertex array. La démarche est
la même que pour créer un buffer :
GLuint vertex_array;
init( )
// cree le vertex array
glGenVertexArrays(1, &vertex_array);
// selectionne le vertex array pour l'initialiser
glBindVertexArray(vertex_array);
pour associer le contenu d'un buffer à une variable d'un shader,
il faut récupérer l'identifiant de la variable (comme pour
affecter une valeur à un uniform), cf glGetAttribLocation( ), et
sélectionner le buffer contenant les données sur GL_ARRAY_BUFFER :
init( )
// sélectionner le vertex array, si necessaire, parametre implicite de glVertexAttribPointer
// glBindVertexArray(vertex_array);
// sélectionner le buffer contenant les donnees, si necessaire, parametre implicite de glVertexAttribPointer
// glBindBuffer(GL_ARRAY_BUFFER, buffer);
// récuperer l'identifiant de l'attribut declare dans le vertex shader
GLint attribute= glGetAttribLocation(program, "position");
// décrit un attribut composé de 3 floats, placé au début du buffer actuellement sélectionné sur GL_ARRAY_BUFFER et "espacé" de sizeof(Point) octets.
glVertexAttribPointer(attribute, /* size */ 3, /* type */ GL_FLOAT,
/* normalize */ GL_FALSE,
/* stride */ sizeof(Point),
/* offset */ 0);
// !! attention : le buffer contenant les donnees est un parametre implicite, c'est le buffer selectionne sur GL_ARRAY_BUFFER,
// cf glBindBuffer(GL_ARRAY_BUFFER, buffer); dans l'étape 1 !!
glEnableVertexAttribArray(attribute);
// attention : ne pas oublier d'activer l'attribut !!
tous les détails sont dans le tuto,
section vertex array.
remarque : si votre vertex shader a "besoin" de plusieurs attributs pour fonctionner, il faut les configurer un par un, cf tuto, section vertex array.
exercice 1 :
modifiez votre tp (cf fonction init()) pour créer un buffer pour
stocker les coordonnées des sommets du terrain.
la classe Mesh fournit un std::vector de vec3 contenant les
coordonnées des sommets, cf Mesh::positions(), ainsi que les
normales, cf Mesh::normals() et d'autres fonctions utilitaires
pour simplifier cette partie, cf doc
de la classe Mesh, section manipulation de buffers
d'attributs.
créez et configurez un vertex array pour l'attribut déclaré dans
votre vertex shader.
avant de pouvoir dessiner (cf fonction draw( )), il faudra bien
sur sélectionner le vertex array, le shader program et affecter
une valeur à ses uniforms. ensuite, il faudra utiliser la bonne
version de glDraw( ) en fonction de la description du maillage. si
le maillage est indexé il faut utiliser glDrawElements( ), sinon,
c'est glDrawArrays( ).
draw( )
glUseProgram(program);
// affecter une valeur aux uniforms
{ ...}
// selectionne les parametres decrivant l'organisation memoire des attributs dans les buffers...
// vertex array est plus court comme formulation...
glBindVertexArray(vertex_array);
glDrawArrays(GL_TRIANGLES, /* first */ 0, /* count */ terrain.vertex_count());
les différentes versions de glDraw() sont décrites dans le tuto.
et voila, votre application dessine directement avec openGL !!
#include "mesh.h"
#include "wavefront.h"
Mesh objet;
init( )
objet= read_mesh("fichier.obj")
// verifie que le maillage contient au moins un sommet..
if(objet.positions().size() == 0)
// erreur, rien a afficher...
return -1;
// creer un buffer, un vertex array, etc.
// compiler un shader program
remarque : pour mettre au point vos shaders plus facilement, utilisez la fonction reload_program() qui permet de re-compiler les shaders sans quitter l'application.
GLuint program;
init( )
program= read_program("tutos/...glsl");
program_print_errors(program);
draw( )
if(key_state('r'))
{
clear_key_state('r');
reload_program(program, "tutos/...glsl");
program_print_errors(program);
}
exercice 1 :
comment éclairer "correctement" un objet / un point d'un triangle
?
une solution correcte calcule le cosinus de l'angle entre la
normale du triangle et la direction du soleil (par exemple, ou
d'une autre source de lumière).
il est très simple de calculer le cosinus de l'angle entre 2
vecteurs, c'est le produit scalaire des 2 vecteurs normalisés (de
longueur 1) :
shader( )
vec3 N= ... ; // normale du triangle
vec3 L= ... ; // direction du soleil
float cos_theta= dot(normalize(N), normalize(L));
vec3 color= vec3(1, 0, 0) * cos_theta;
gl_FragColor= vec4(color, 1);
que faut-il modifier dans l'application et éventuellement dans le
vertex shader pour que le fragment shader puisse faire ce calcul ?
quelles sont les informations supplémentaires à transmettre au
fragment shader ?
comment obtenir les coordonnées du point du triangle associé à un
fragment ?
attention : les coordonnées des différents vecteurs
doivent être exprimées dans le même repère...
remarque : pour calculer les normales des sommets /
triangles d'un Mesh, cf Annexe, à la fin du sujet.
éclairez vos objets de cette manière.
vous pouvez aussi donner une couleur de base différente à chaque
objet. comment ? que faut-il modifier ?
plus de détails sur les calculs d'éclairage / de reflets dans ce
cours : cf CPE5
/ cm3
exercice 2 :
calculer réellement les ombres projettées par des objets est peu
plus compliqué. mais on peut très bien utiliser une
approximation...
l'idée est de remplacer la forme de l'objet qui projette une
ombre par une forme simple, une sphère, par exemple, et de
construire la forme de l'ombre crée par cette sphère. dans le cas
d'une source directionnelle comme le soleil, l'ombre crée par une
sphère sera un cylindre. il ne reste plus qu'a déterminer si un
point d'un objet est inclus dans le cylindre, ou pas, pour
déterminer qu'il est à l'ombre ou éclairé.
connaissant le centre et le rayon d'une sphère, la direction
principale du soleil, comment décrire le cylindre ?
connaissant les coordonnées d'un point, comment déterminer qu'il
appartient au cylindre et qu'il est à l'ombre ?
modifiez votre application et vos shaders.
comment obtenir les coordonnées du point du triangle associé à un
fragment ?
exercice 3 :
il est également possible de décrire d'autres types de source de
lumière. un spot ou une source ponctuelle / petite ampoule.
comment éclairer un point par un spot ? c'est le même principe
que précédement, si le point est dans le cone representant le
volume éclairé par le spot, le point est éclairé.
un cone est décrit par un sommet, une direction et un angle.
comment déterminer qu'un point est à l'intérieur d'un cone ?
quelle est la forme de l'ombre projettée par une sphère
lorsqu'elle est éclairée par un cone ? ou par un point ?
exercice 4 :
et avec plusieurs ombres projettées et plusieurs spots ? qu'est ce
qui change ?
comment fournir toutes ces informations aux shaders ?
dans la premiere partie vous avez construit un maillage
représentant la surface d'un terrain, ou chargé un fichier .obj.
Pour calculer la couleur des fragments en fonction de leur
orientation par rapport à une source de lumière, il faut connaitre
les normales des sommets, mais elles ne sont pas toujours
définies.
comment les calculer, connaissant les coordonnées des sommets des
triangles ?
il suffit de parcourir les triangles du mesh, de calculer 2
aretes du triangle et un produit vectoriel permet de calculer un
3ieme vecteur perpendiculaire aux 2 aretes.
vous pouvez écrire une fonction :
cette fonction recalcule les normales géométriques des triangles et les objets éclairés auront un aspect très facetisé. il est aussi possible de construire la normale des sommets en calculant, par exemple, la moyenne des normales des triangles adjacents à chaque sommet. c'est un peu plus long, mais pas très compliqué, les détails sont dans le tuto.
void build_normals( Mesh& mesh )
{
printf("building normals...\n");
std::vector<vec3> positions= mesh.positions();
// reconstruit completement l'objet
mesh= Mesh(GL_TRIANGLES);
// calcule les normales geometriques des triangles
for(int i= 0; i +2 < (int) positions.size(); i+= 3)
{
// recupere les sommets du triangle
Point a= Point(positions[i]);
Point b= Point(positions[i +1]);
Point c= Point(positions[i +2]);
// calcule 2 aretes du triangle
Vector ab(a, b);
Vector ac(a, c);
// produit vectoriel des aretes == normale
Vector n= normalize(cross(ab, ac));
// attention a l'orientation...
// re-insere les sommets et leurs normales
mesh.normal(n).vertex(a);
mesh.normal(n).vertex(b);
mesh.normal(n).vertex(c);
}
printf(" positions %d, normals %d\n", (int) mesh.positions().size(), (int) mesh.normals().size());
}
exemple des différences :
à gauche : normales par triangle, à droite : normales par sommet
les objets décrits par un fichier wavefront .obj ne sont pas
toujours d'une couleur uniforme. Un objet peut etre découpé en
plusieurs parties, chacune associée à une couleur. Une solution
simple pour afficher un objet de ce type consiste à affecter la
couleur de chaque partie directement aux sommets.
// affecte la couleur des matieres aux sommets des triangles
void build_colors( Mesh &mesh )
{
printf("building colors...\n");
std::vector<vec3> positions= mesh.positions();
std::vector<vec3> normals= mesh.normals();
std::vector<Material> materials= mesh.mesh_materials();
std::vector<unsigned int> triangle_materials= mesh.materials();
// reconstruit completement l'objet
mesh= Mesh(GL_TRIANGLES);
for(int i= 0; i +2 < (int) positions.size(); i+= 3)
{
// recupere les sommets du triangle
Point a= Point(positions[i]);
Point b= Point(positions[i +1]);
Point c= Point(positions[i +2]);
Vector na;
Vector nb;
Vector nc;
// recalcule aussi les normales, si necessaire
if(normals.size() != positions.size())
{
// calcule 2 aretes du triangle
Vector ab(a, b);
Vector ac(a, c);
// produit vectoriel des aretes == normale
Vector n= normalize(cross(ab, ac));
na= n;
nb= n;
nc= n;
}
else
{
// ou recupere les normales, si possible
na= Vector(normals[i]);
nb= Vector(normals[i +1]);
nc= Vector(normals[i +2]);
}
// recupere la couleur du triangle
Color diffuse= materials[triangle_materials[i / 3]].diffuse;
// re-insere les sommets, leurs normales et leurs couleurs
mesh.color(diffuse).normal(na).vertex(a);
mesh.color(diffuse).normal(nb).vertex(b);
mesh.color(diffuse).normal(nc).vertex(c);
}
printf(" positions %d, normals %d, colors %d\n", (int) mesh.positions().size(), (int) mesh.normals().size(), (int) mesh.colors().size());
}