TD1 : Deferred shading

Rappels : ces TD font suites à ceux d'OpenGL de l'an dernier. Vous utiliserez une base de code similaire, basée sur GLFW. Télechargez les sources du projet : sia_td1.zip

Durant ce TD, vous allez mettre en œuvre le deferred shading, technique de rendu permettant de découpler le calcul de la visibilité du calcul de l'éclairage. Cette méthode procède en deux étapes :

  1. un g-buffer contenant les informations géométriques et d'apparence est généré à partir des objets de la scène,
  2. l'éclairage est évalué, pour chaque source lumineuse, uniquement pour les fragments visibles.

1. Création et remplissage du g-buffer

La première étape va consister à réaliser un rendu de la scène sans éclairage, et à stocker par pixel un certain nombre d'attributs (couleur, normale, profondeur) dans un g-buffer. En pratique, en OpenGL, vous utiliserez un FrameBuffer Object pour écrire directement dans des textures plusieurs valeurs en sortie du fragment shader.

1. La classe FBO vous permet de manipuler un FrameBuffer Object. Lisez et complétez la méthode FBO::init de telle sorte à ce qu'elle initialise un FBO possédant deux textures de couleur et une texture de profondeur (suivez les commentaires dans le code).

2. Créez une nouvelle paire de shaders gbuffer.vert/.frag et chargez le programme dans Viewer::init. Le vertex shader se contentera de transmettre la couleur du sommet et sa normale en espace caméra. Le fragment shader devra écrire ces vecteurs interpolés dans deux variables out vec4 out_color et out vec4 out_normal. Stockez également la variable uniforme specular_coef (coefficient spéculaire par objet) dans le canal alpha de out_color.

Attention, pensez à bien normaliser et transformer la normale dans l'intervalle $[0,1]$. En GLSL, vous pouvez préciser la cible d'écriture d'une sortie du fragment shader en faisant précéder le qualifier out par layout(location = i), avec i l'index du buffer à accéder. Assurez-vous d'écrire la couleur et la normale dans la bonne texture du FBO. La profondeur sera automatiquement écrite dans le depth buffer attaché au FBO.

3. Créez une variable de type FBO dans Viewer et initialisez-la dans Viewer::init à la même taille que la fenêtre d'affichage. Au début de la fonction Viewer::display, activez ce FBO (méthode bind) et mettez à zéro ses buffers avec glClear. Tout ce que vous dessinerez ensuite sera stocké dans le FBO. Activez le shader gbuffer et faite le rendu de la scène, puis appeler la méthode unbind du FBO.

Pour vérifiez qu'elles contiennent bien les informations attendues, sauvegardez les textures du FBO sur disque sous la forme d'images à l'aide de la méthode savePNG. Cette méthode prend en paramètre le nom du fichier (avec son extension) et l'index du buffer de couleur à sauvegarder (0 ou 1, en l'occurrence).

g-buffer g-buffer
couleurs normales

2. Calcul de l'éclairage

Dans cette seconde étape, vous utiliserez les informations stockées dans le g-buffer pour calculer l'éclairage. Pour cela, vous dessinerez un quad recouvrant complètement la fenêtre d'affichage de telle sorte à ce que le fragment shader d'éclairage soit évalué une fois par pixel.

1. Créez et initialisez un quad (class Quad) dans Viewer::init. Par défaut, ce quad va du point $(-1,-1)$ au point $(1,1)$, ce qui correspond précisément à la fenêtre d'affichage en Normalized Device Coordinates.

2. Créez une deuxième paire de shaders deferred.vert/.fragdans Viewer::init. Le vertex shader n'aura qu'à passer la position du sommet à gl_Position sans transformation (le quad étant déjà projeté).

Le fragment shader devra accéder aux trois textures du g-buffer (couleurs, normales et profondeurs) pour le pixel courant. Il vous faut donc connaître les coordonnées du fragment courant. Pour cela, vous utiliserez la variable GLSL gl_FragCoord. En la divisant par la taille de la fenêtre (passée comme uniform au shader), vous la transformerez dans l'intervalle $[0,1]$ et pourrez alors l'utiliser comme coordonnée de texture (texcoord).

Pour calculer l'éclairage selon le modèle de Blinn-Phong, il vous manque encore la position de l'objet dans le repère de la caméra. Celle-ci peut être reconstruite à partir de la profondeur z et de l'inverse de la matrice de projection, à l'aide de cette fonction :

uniform mat4 inv_projection_matrix;

vec3 VSPositionFromDepth(vec2 texcoord, float z)
{
    // Get x/w and y/w from the viewport position
    vec4 positionNDC = vec4(2 * vec3(texcoord, z) - 1, 1.f);
    // Transform by the inverse projection matrix
    vec4 positionVS = inv_projection_matrix * positionNDC;
    // Divide by w to get the view-space position
    return positionVS.xyz / positionVS.w;  
}

Avec cette position, la normale, la couleur et la position de la source lumineuse, calculez l'éclairage comme dans le fragment shader blinn.frag.

3. Dans la fonction Viewer::display, après avoir désactivé le FBO, activez ce nouveau shader. Passez-lui les trois textures du FBO (renderedTexture[0], renderedTexture[1] et depthTexture) comme s'il s'agissait de textures 2D traditionnelles, c'est-à-dire en utilisant glActiveTexture, glBindTexture et glUniform1i. Dessinez le quad : vous devriez obtenir exactement le même résultat qu'en rendu direct.

4. Le deferred shading prend tout son intérêt dans le cas de sources lumineuses multiples. Ajoutez de nouvelles lampes ponctuelles à la scène (dont vous pouvez faire varier la couleur). Pour chacune d'elles, répétez l'étape précédente en accumulant le résultat en mode additif. Pour cela, activez le blending (GL_BLEND) avant le calcul d'éclairage et spécifiez la fonction de mélange avec glBlendFunc. N'oubliez pas de désactiver le test de profondeur (GL_DEPTH_TEST).

deferred
Rendu final avec 3 sources lumineuses ponctuelles colorées