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...)