OpenGL Moderne

Tutoriel 14 : rendu dans une texture

Opengl-tutorial.org

Traducteur

Alexandre Laurent

Le rendu dans une texture (render to texture) est une méthode pratique pour créer une multitude d'effets. L'idée de base est de dessiner une scène, tout comme vous le faisiez habituellement, mais cette fois, dans une texture que vous pouvez réutiliser plus tard.

Commentez

- Titre : OpenGL Moderne

- Auteur : Opengl-tutorial.org

- Traducteur : Alexandre Laurent

- Parution : 24 juin 2014

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 opengl-tutorial.org. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Navigation

- I. Introduction

- II. Rendu dans une texture

- II-A. Créer une cible de rendu

- II-B. Dessiner dans la texture

- II-C. Utiliser la texture générée

- III. Résultats

- IV. Aller plus loin

- IV-A. Utiliser la profondeur

- IV-B. Multi-échantillonnage

- IV-C. Cible de rendu multiples

- V. Exercices

- VI. Remerciements

- Navigation

Navigation

Tutoriel précédent : application des normales

Sommaire

Tutoriel suivant : textures de lumière

I - Introduction

Le rendu dans une texture (render to texture) est une méthode pratique pour créer une multitude d'effets. L'idée de base est de dessiner une scène, tout comme vous le faisiez habituellement, mais cette fois, dans une texture que vous pouvez réutiliser plus tard.

Les applications de cette technique incluent les caméras dans le jeu, le post-processing et autant d'effets graphiques que vous le souhaitez.

II - Rendu dans une texture

On a trois tâches : créer une texture dans laquelle on va faire le rendu, faire le rendu dans celle-ci et utiliser la texture générée.

II-A - Créer une cible de rendu

La chose dans laquelle on va dessiner s'appelle un tampon d'image (Framebuffer). C'est un conteneur pour les textures et optionnellement un tampon de profondeur. Il est créé exactement comme un autre objet OpenGL :

// Le tampon d'image, regroupant 0, 1 ou plus de textures et 0 et 1 tampon de profondeur.
GLuint FramebufferName = 0;
glGenFramebuffers(1, &FramebufferName);
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);

Maintenant on doit créer la texture qui va contenir la sortie RGB du shader. Ce code est très classique :

// La texture dans laquelle on va dessiner
GLuint renderedTexture;
glGenTextures(1, &renderedTexture);

// "Lie" la nouvelle texture créée : toutes les fonctions suivantes vont modifier cette texture
glBindTexture(GL_TEXTURE_2D, renderedTexture);

// Donne une image vide à OpenGL (le dernier « 0 »)
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, 1024, 768, 0,GL_RGB, GL_UNSIGNED_BYTE, 0);

// Filtrage léger. Obligatoire !
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

On doit aussi créer un tampon de profondeur. C'est optionnel et dépend de ce que vous voulez afficher dans votre texture ; mais comme on va afficher Suzanne, on a besoin du test de profondeur.

// Le tampon de profondeur
GLuint depthrenderbuffer;
glGenRenderbuffers(1, &depthrenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 1024, 768);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer);

Finalement, on configure le tampon d'image :

// Définit "renderedTexture" comme notre couleur d'attache #0
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, renderedTexture, 0);

// Définit la liste de tampons à dessiner.
GLenum DrawBuffers[1] = {GL_COLOR_ATTACHMENT0};
glDrawBuffers(1, DrawBuffers); // "1" est la taille de DrawBuffers

Quelque chose a pu mal se passer durant ce processus, selon les capacités du GPU. Voici comment vérifier les erreurs :

// Toujours vérifier que notre tampon d'image est ok
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
return false;

II-B - Dessiner dans la texture

Le rendu dans la texture est direct. Liez simplement le tampon d'image et dessinez votre scène comme d'habitude. Facile !

// Rendu dans le tampon d'image
glBindFramebuffer(GL_FRAMEBUFFER, FramebufferName);
glViewport(0,0,1024,768); // Rendu dans l'intégralité du tampon d'image, du coin inférieur gauche au coin supérieur droit

Le fragment shader nécessite une adaptation mineure :

layout(location = 0) out vec3 color;

Cela signifie que lors de l'écriture dans la variable « color », on va écrire dans la cible de rendu 0, qui correspond à la texture, car DrawBuffers[0] est GL_COLOR_ATTACHMENTi, qui est dans notre cas, renderedTexture.

Pour récapituler :

- color sera écrit dans le premier tampon à cause du layout(location=0) ;

- le premier tampon est GL_COLOR_ATTACHMENT0 car DrawBuffers[1] = {GL_COLOR_ATTACHMENT0} ;

- renderedTexture est attaché à GL_COLOR_ATTACHMENT0, donc c'est où la couleur sera écrite ;

- en d'autres mots, vous pouvez remplacer GL_COLOR_ATTACHMENT0 par GL_COLOR_ATTACHMENT2 et cela fonctionnera toujours.

Il n'y a pas de layout(location=i) dans OpenGL < 3.3, mais vous pouvez utiliser glFragData[i] = myvalue à la place.

II-C - Utiliser la texture générée

On va dessiner un simple rectangle pour remplir l'écran. On doit utiliser les tampons habituels, shaders, identifiants…

// Le rectangle plein écran du FBO
GLuint quad_VertexArrayID;
glGenVertexArrays(1, &quad_VertexArrayID);
glBindVertexArray(quad_VertexArrayID);

static const GLfloat g_quad_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f,  1.0f, 0.0f,
-1.0f,  1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
1.0f,  1.0f, 0.0f,
};

GLuint quad_vertexbuffer;
glGenBuffers(1, &quad_vertexbuffer);
glBindBuffer(GL_ARRAY_BUFFER, quad_vertexbuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_quad_vertex_buffer_data), g_quad_vertex_buffer_data, GL_STATIC_DRAW);

// Créer et compile le programme GLSL à partir des shaders
GLuint quad_programID = LoadShaders( "Passthrough.vertexshader", "SimpleTexture.fragmentshader" );
GLuint texID = glGetUniformLocation(quad_programID, "renderedTexture");
GLuint timeID = glGetUniformLocation(quad_programID, "time");

Maintenant vous souhaitez dessiner sur l'écran. Cela se fait en utilisant 0 comme second paramètre de glBindFramebuffer.

// Dessiner sur l'écran
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0,0,1024,768); // Dessine sur l'intégralité du tampon d'image, du coin inférieur gauche au coin supérieur droit

On peut dessiner le rectangle plein écran avec un tel shader :

#version 330 core

in vec2 UV;

out vec3 color;

uniform sampler2D renderedTexture;
uniform float time;

void main(){
color = texture( renderedTexture, UV + 0.005*vec2( sin(time+1024.0*UV.x),cos(time+768.0*UV.y)) ).xyz;
}

Ce code échantillonne simplement la texture, mais ajoute un léger décalage dépendant du temps.

III - Résultats

IV - Aller plus loin

IV-A - Utiliser la profondeur

Dans quelques cas vous pouvez avoir besoin de la profondeur lors de l'utilisation de la texture générée. Dans ce cas, dessinez simplement la texture comme suit :

glTexImage2D(GL_TEXTURE_2D, 0,GL_DEPTH_COMPONENT24, 1024, 768, 0,GL_DEPTH_COMPONENT, GL_FLOAT, 0);

(« 24 » est la précision, en bits. Vous pouvez choisir entre 16, 24 et 32, suivant vos besoins. Habituellement 24 suffit.)

Cela devrait être assez pour pouvoir démarrer, mais le code source fourni implémente aussi cela.

Notez que cela devrait être quelque peu plus lent, car le pilote ne sera pas capable d'utiliser quelques optimisations comme Hi-Z.

Dans cette capture d'écran, les niveaux de profondeur sont artificiellement « plus beaux ». Habituellement, c'est beaucoup plus difficile de voir quelque chose sur une texture de profondeur. Proche = Z tendant vers 0 = noir, Lointain = Z tendant vers 1 = blanc.

IV-B - Multi-échantillonnage

Vous pouvez écrire dans des textures utilisant le multi-échantillonnage au lieu d'utiliser les textures de « base » : vous devez simplement remplacer glTexImage2D par glTexImage2DMultisample dans le code C++ et sampler2D/texture par sampler2DMS/texelFetch dans le fragment shader.

Par contre, il y a un énorme inconvénient : texelFetch a besoin d'un argument supplémentaire, correspondant au numéro de l'échantillon à récupérer. En d'autres mots, il n'y a pas de méthode automatique de « filtrage » (le terme correct, lorsque l'on parle de multi-échantillonnage, est « résolution »).

Donc, vous pouvez résoudre la texture utilisant le multi-échantillonnage vous-même, dans une autre texture non utilisant le multi-échantillonnage, grâce à un autre shader.

Rien de difficile, mais c'est simplement pénible.

IV-C - Cible de rendu multiples

Vous pouvez écrire dans plusieurs textures en même temps.

Créez simplement plusieurs textures (toutes avec la même et correcte taille !), appelez glFramebufferTexture avec une couleur d'attache différente pour chaque, appelez glDrawBuffers avec des paramètres mis à jour (quelque chose comme (2, {GL_COLOR_ATTACHMENT0,GL_COLOR_ATTACHMENT1})) et ajoutez une autre variable de sortie dans votre fragment shader :

layout(location = 1) out vec3 normal_tangentspace; // ou n'importe quoi d'autre

Si vous avez besoin d'écrire un vecteur dans une texture, les textures à virgule flottante existe, avec une précision de 16 ou 32 bits au lieu de 8… voir la documentation de glTexImage2D (cherchez GL_FLOAT).

Dans les versions précédentes d'OpenGL, utilisez glFragData[1] = myvalue à la place.

V - Exercices

- Essayez d'utiliser glViewport(0,0,512,768); à la place de glViewport(0,0,1024,768); (essayez avec le tampon d'image et l'écran).

- Expérimentez avec d'autres coordonnées UV dans le dernier fragment shader.

- Transformez le rectangle avec une vraie matrice de transformation. Codez-la en dur puis essayez d'utiliser les fonctions de controls.hpp ; que remarquez-vous ?

VI - Remerciements

Cet article est une traduction autorisée dont le texte original peut être trouvé sur opengl-tutorial.org.

Navigation

Tutoriel précédent : application des normales

Sommaire

Tutoriel suivant : textures de lumière