M1-Images

TP4 - textures, shaders et apparence



L'objectif du tp est d'écrire quelques shaders pour se familiariser avec quelques solutions / problèmes classiques.
Vous pouvez propotyper les shaders avec shader_kit ou directement dans votre application.

rappel : les tutos de gkit...

Partie 1 : vertex / fragment shaders, communication et changements de repères

exercice 1 : matière diffuse

Reprennez votre cours et écrivez un vertex shader qui récupère la normale de chaque sommet et calcule l'orientation de la surface par rapport à une source de lumière placée dans la scène. Cette valeur sera ensuite transmise au fragment shader. Comment ?

attention : les calculs sur les vecteurs doivent se faire dans le même repère, sinon, vous aurez des surprises.

question : dans quel repère peut-on faire les calculs d'orientation ?
réponse : n'importe lequel (local, monde ou caméra), par contre, il faudra transformer certains vecteurs pour connaitre leurs coordonnées dans le repère choisi pour faire les calculs.

exemple : faire les calculs dans le repère du monde.
quelles sont les informations nécessaires ? la position du point, la normale de la surface portant le point, et la position de la source de lumière.

dans quels repères connait-on ces valeurs ?
    position du point, normale à la surface : a priori, on connait les valeurs dans le repere local de l'objet
    position de la camera : c'est l'origine (0, 0, 0) dans le repère camera...
    position de la source de lumière : à choisir, mais a priori ce sera une position dans le repère du monde.

de quelles transformations a-t-on besoin ? (pour les passer dans le repère du monde)
    passage repère local de l'objet vers monde : matrice model
    passage repère camera vers monde : inverse de la matrice camera / view. (pourquoi l'inverse ?)
    passage repère local de l'objet vers camera : matrice (view * model). (pourquoi pas model * view ?)

d'ou viennent les valeurs ?
    position du point : c'est un attribut de sommet, déclaré comme entrée du vertex shader, cf Mesh::vertex()
    normale de la surface : c'est aussi un attribut de sommet, déclaré comme entrée du vertex shader, cf Mesh::normal()
    position de la source de lumière : c'est un paramètre uniform du shader, déclaré comme une variable globale. c'est l'application qui fixe sa valeur. cf program_uniform()
    matrice de transformation : c'est aussi un paramètre uniform du shader, déclaré comme une variable globale. c'est l'application qui fixe sa valeur. cf program_uniform()

vous pouvez écrire les 2 shaders (vertex et fragment) dans un fichier .glsl :
#version 330

#ifdef VERTEX_SHADER
layout(location= 0) in vec3 position;    // attribut de sommet
layout(location= 2) in vec3 normal;     // attribut de sommet

uniform mat4 mvpMatrix;    // matrice passage du repere local au repere projectif homogene de la camera
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 viewInvMatrix;

void main( )
{
    // obligation du vertex shader : calculer les coordonnées du sommet dans le repère projectif homogene de la camera
    gl_Position= mvpMatrix * vec4(position, 1);
     
    // normale de la surface, dans le repere monde
   
    // position de la camera dans le repere du monde
     
    // direction entre le sommet et la source de lumiere
    // calculer le cosinus de l'angle entre les 2 directions
    // transmettre l'orientation au fragment shader...
}
#endif

#ifdef FRAGMENT_SHADER
// recuperer l'orientation calculee dans le vertex shader...

out vec4 fragment_color;

void main( )
{
    // utiliser l'orientation pour modifier la couleur de base... a completer
    fragment_color= vec4(1, 0.5, 0, 1);    // orange opaque
}
#endif

question : qu'est ce qui change, si les calculs sont faits dans le repere camera ?


exercice 2 : et avec une texture ?

Il faut donner 2 informations supplémentaires aux shaders, la position des sommets dans la texture, et l'image elle même.

La position des sommets dans la texture est un attribut de sommet et l'image est un uniform de type sampler2D :
layout(location= 1) in vec2 texcoord;    // uniquement dans un vertex shader

uniform sampler2D texture0;    // dans n'importe quel shader
Les attributs de sommets viennent de Mesh::texcoord( ) et la texture doit d'abord etre chargée avec read_texture(0, fichier ) (cf texture.h) et utilisee lors de l'affichage avec program_use_texture( ) (cf uniforms.h).

question : si le fragment shader veut lire une couleur dans une texture, comment connaitre les coordonnées de texture d'un sommet ?
réponse : il faut transmettre explicitement les coordonnées de texture depuis le vertex shader (cf pipeline graphique, partie 2, ou tuto5GL)


Partie 2 : shading

exercice 1 : cell / toon shading

en modifiant la valeur du cos theta, il est assez simple de créer des aplats de couleurs, au lieu d'un degradé très régulier. Cette solution est utilisée dans de nombreux jeux (avec de nombreuses étapes supplémentaires pour rendre le résultat plus subtil...)

indication : il suffit de créer des "paliers". le plus simple est d'arrondir à l'entier le plus proche. pour créer 5 paliers : floor(cos_theta * 5) / 5


exercice 2 : détection des silhouettes

pour compléter le shader précédent, il est possible de détecter les silhouettes de l'objet et de les afficher d'une couleur différente pour structurer un peu le dessin.

indication : une manière simple de détecter une silhouette est de calculer l'angle (ou son cosinus...) entre la normale et la direction vers la camera. si l'angle est proche de +/-90°, la silhouette n'est pas loin...




remarque : cette solution est très simple, mais n'est pas très précise, il faudrait utiliser un maillage avec des triangles vraiment tout petits (de la taille d'un pixel, une fois dessiné) pour quelle fonctionne correctement.

exercice 3 : j'aime pas bigguy déguisé en zelda...

si vous préferrez une solution plus réaliste, les détails des calculs sont la (cf équations 20, 23, 33 et 34) :
"Microfacet Models for Refraction through Rough Surfaces" , B. Walter 2007

sinon, utilisez la version vue en cours, le modèle de Blinn-Phong normalisé :

        fr(o, l) = kd / pi + (1 - kd) * (n+8) / (8*pi) * pow(cos_theta_h, n)
        color= emission * fr(o, l) * cos_theta

avec :
        k : proportion du comportement diffus, et (1 - k) proportion du comportement reflechissant,
        n : concentration du reflet,

theta= angle entre la normale et la direction vers la source de lumière,
theta_h= angle entre la normale et la direction h= normalize(o + l)
o : direction vers la camera
l : direction vers la source

cos (u, v) = cosinus de l'angle entre les vecteurs u et v de longueur 1 : dot(normalize(u), normalize(v))

exercice 4 : j'aime pas battlefield...

si vous préferrez des solutions plus libres, le LitSphere shading est une évolution du cell / toon shading :
" The Lit Sphere: A Model for Capturing NPR Shading from Art", P.P. Sloan, 2001

L'idée est de "peindre" une texture qui sera utilisée à la place du calcul de la réflexion de la lumière, ce qui permet de faire d'assez jolies choses, faites une recherche d'images sur "lit sphere shading" ou "mat cap shading" pour vous faire une idée...