M1 synthèse d'images
2025
TP2 - shaders, matières
et textures
Partie 1 : matière et shader
maintenant que vous savez afficher des objets à peu près comme vous
voulez (cf tp précédent), il est temps de regarder comment
fonctionnent les shaders.
projets/tp4.cpp et tp4.glsl fournissent un premier exemple
minimaliste, qu'il va falloir améliorer...
exercice 1 : compilez tp4 et vérifiez que tout fonctionne
correctement.
exercice 2 : ou sont les normales de l'objet ? comment les
obtenir ?
modifiez la fonction init() de tp4.cpp.
indications : dans meshio.h, il y a une fonction
read_meshio_data(), que fait-elle ? comment récupérer les positions,
leurs indices, et les normales d'un objet ?
exercice 3 : comment créer les buffers openGL avec les
positions, leurs indices et les normales ?
vérifiez que l'objet s'affiche toujours.
indication : relisez le tp précédent, si nécessaire.
exercice 4 : et le shader ?
comment rendre les normales des sommets de l'objet accessibles au
vertex shader ?
puis, comment rendre les normales accessibles au fragment shader ?
vous pouvez vérifier en utilisant les normales comme couleur du
fragment (ou leurs valeurs absolues, plutôt)...
exercice 5 : matière diffuse
on veut calculer la lumière réfléchie par chaque fragment vers la
camera / le pixel. on suppose que l'on connait la couleur de base de
la matière.
on peut représenter la situation avec le schéma suivant :
le fragment se trouve sur le point p, on connait sa normale et sa
couleur, que manque-t-il pour faire le calcul ? comment transmettre
ces valeurs au shader ?
Ensuite, il ne reste plus qu'à calculer le cosinus de l'angle entre
la normale en p et la direction vers la lumière, et de se rappeler
que les calculs sur les points et les vecteurs doivent se faire avec
des coordonnées dans le même repère.
rappel : on connait habituellement les matrices qui
permettent de faire les changements de repère, il suffit de les
donner au shader. comment ?
rappel : il y a une relation entre le cosinus et le produit
scalaire...
indication : regardez les fonctions définies dans uniforms.h,
à quoi servent-elles ? cf doc
en ligne.
Partie 2 : texture et shader
lorsque la surface d'un objet n'est pas paramétrée, on ne peut
pas la texturer... il faut construire une paramétrisation, cf le
projet...
sauf que non, il est possible d'utiliser une paramétrisation
implicite comme le "tri-planar mapping" vu au semestre précédent.
rappel : on peut utiliser trois textures \( T_x, T_y, T_z
\) correspondant aux 3 plans yz, xz, xy (ou une seule) et pondérer
ces 3 couleurs en fonction des coordonnées de la normale :
\[
uv= (p - pmin) / (pmax - pmin)
\]
\[
color= \frac{ T_x(uv_{yz}) \cdot n_x +
T_y(uv_{yz}) \cdot n_y + T_z(uv_{yz}) \cdot n_x }{ n_x + n_y + n_z
}
\]
il suffit de construire uv, les coordonnées de texture en
normalisant les coordonnées du point associé au fragment entre 0
et 1. le plus simple est d'utiliser les coordonnées dans le repère
objet.
comment transmettre pmin et pmax au shader ? modifiez la fonction
init() pour calculer pmin et pmax, ie les valeurs extrêmes des
coordonnées des sommets de l'objet.
indication : regardez dans vec.h, il exsite les fonctions
Point min(Point, Point), et pareil pour max().
pour les curieux : on peut aussi jouer avec les pondérations
des 3 couleurs / textures pour modifier le mélange des textures,
cf le cours du semestre précédent qui propose d'utiliser une
puissance != 1..

comment déclarer la texture dans le shader ?
indication : il faudra charger une image et la transformer
en texture openGL, cf texture.h et read_texture()...
indication : program_use_texture() définit dans uniform.h
devrait pas mal vous aider pour configurer openGL et le shader...
Partie 3 : reflets et shader
si on souhaite utiliser un autre modèle de matières ? que faut-il
modifier ? par exemple Blinn-Phong ?
mêmes questions que dans la partie 2. qu'est ce qu'on calcule ?
quelles sont les paramètres du shader ? comment lui transmettre
ces informations ?
on peut représenter la situation avec le schéma suivant :

Partie 4 : et si on peignait les reflets ?
note : les illustrations sont tirées de https://kumokairo.github.io/posts/understanding-and-implementing-regular-matcaps-in-unity/
on utilise l'image d'une sphère éclairée par un environnement
quelconque et on reproduit les reflets, que l'on observe sur la
sphère, sur notre objet en utilisant uniquement ses normales !

à gauche une sphère éclairée. à droite l'image
de cette sphère...
on a une sphère éclairée dans l'image de gauche, on fabrique
l'image de droite avec (en floutant un peu les bords pour éviter
des petits défauts de reprojection...)
il faut faire un peu de gymnastique avec les coordonnées de la
normale de l'objet, mais l'idée est que chaque normale (visible
et orientée vers la camera...) correspond à un point sur la
sphère et donc à un pixel dans l'image. cette manière de
"peindre" les reflets d'un objet s'appelle un matcap, pour
material capture. il existe des catalogues complets de matcaps
qui permettent de créer des styles graphiques vraiment très
variés. la plupart des jeux en cell shading et les logiciels de
sculpture utilisent cette technique. faites une recherche
d'images matcap pour vous rendre compte...
on peut, bien sur, utiliser l'image obtenue (à droite) pour
éclairer un autre objet (c'est l'objectif de la méthode).
pour chaque fragment, on connait la normale dans le repère
camera, on en déduit le point sur la sphère correspondant et on
lit la couleur du pixel dans l'image (avec des coordonnées
normalisées entre 0 et 1) :
ou encore :
donc en résumé :
on connait la normale du fragment dans le repère camera (quelle
transformation utiliser pour passer du repère de l'objet au
repère camera ?)
ces normales font quasi tout le temps face à la camera, ie on
peut oublier leur coordonnée sur l'axe Z... ce qui permet de les
projeter directement dans l'image de la sphère ! ie on utilise
que les coordonnées X et Y. il ne reste plus qu'à trouver
comment calculer les coordonnées normalisées entre 0 et 1 pour
lire la couleur correspondant à cette normale...
pour une normale parfaitement alignée avec la camera, l'axe Z,
on va lire la couleur du pixel au milieu de l'image, donc aux
coordonnées (0.5, 0.5).
pour une normale complètement à gauche (-1 0 0), il faut lire le
pixel à gauche et sur la ligne du milieu, donc (0, 0.5)
idem pour une normale à droite (1, 0, 0), on va lire le pixel
(1, 0.5)
idem pour les normales en haut et en bas... tout ça pour dire
que le quart haut-droit de l'image correspond aux normales avec
des coordonnées > 0, le quart bas-gauche correspond aux
normales avec des coordonnées < 0 et les 2 autres quarts
correspondent aux 2 autres combinaisons.
il suffit de renormaliser les coordonnées des normales, ie
l'intervalle [-1 1] vers l'intervalle [0 1] !
oui, le fragment shader fait 2 lignes au final... (calculer les
coordonnées de texture, et lire la texture...)