10/03/2013 2 avis

Render To Texture avec OpenGL 3+ et GLSL shaders

Render To Texture avec OpenGL 3+ et GLSL shaders

Le rendu en texture est une méthode spécifique de rendu d'un objet ou d'une scène 3D en deux passes. L'idée de base est simple, on fait un premier rendu de notre scène dans une texture, puis l'on fait un second rendu de cette texture directement à l'écran. Généralement, on plaque cette texture sur un quadrilatère faisant toute la taille du canvas OpenGL. Cette opération permet de mettre en place de nombreux effets en travaillant sur la texture générée.

Préparation de la pipeline

De nombreuses personnes buttent sur OpenGL à cause d'un versioning assez chaotique et d'un très grands nombres d'extensions à utiliser. Afin, d'éviter tout problème de compatibilité, je vous recommande de respecter ces critères. Dans le cas contraire, il faudra peut être adapté un peu mon code pour assurer sa portabilité.

  • Gestionnaire de fenêtrage GLUT (avec GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH)
  • L'utilitaire d'extension GLEW
  • OpenGL 3.3+ (core profile)
  • GLSL 3.3+

Création d'une texture vierge

Il faut d'abord créer une texture RGB vierge. Cette texture sera remplie directement par les couleurs de sortie du premier fragment shader lors du rendu de votre scène.

// Génération d'une texture
GLuint idTexture;
glGenTextures(1, &idTexture);
 
// Binding de la texture pour pouvoir la modifier.
glBindTexture(GL_TEXTURE_2D, idTexture);
 
// Création de la texture 2D vierge de la taille de votre fenêtre OpenGL
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 1024, 768, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
 
// Paramètrage de notre texture (étirement et filtrage)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

Mise en place d'un FrameBufferObject pour contenir la texture

Dans un rendu normal, il n'y avait pas besoins de créer un FrameBufferObject. En effet, OpenGL en créé un nativement, ce dernier contient l'image affichable à l'écran. Dans notre cas, nous devons créer un FBO supplémentaire pour contenir notre texture.

// Génération d'un second FBO
GLuint idFBO = 0;
glGenFramebuffers(1, &idFBO);

// On bind le FBO
glBindFramebuffer(GL_FRAMEBUFFER, idFBO);

// Affectation de notre texture au FBO
glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, idTexture, 0);
 
// Affectation d'un drawbuffer au FBO
GLenum DrawBuffers[2] = {GL_COLOR_ATTACHMENT0};
glDrawBuffers(1, DrawBuffers);

Cette partie est optionnelle (mais recommandée). Si, vous modèliser une scène utilisant de la profondeur (Z-buffer), il faut créer un DepthRenderBuffer et l'ajouter dans le FrameBufferObject.

(optionnel)
// Création d'un buffer de profondeur
GLuint idDepthrenderbuffer;
glGenRenderbuffers(1, &idDepthrenderbuffer);

// Binding du buffer de profondeur
glBindRenderbuffer(GL_RENDERBUFFER, idDepthrenderbuffer);

// Paramètrage du RenderBuffer (selon la taille du canvas OpenGL)
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 1024, 768);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, idDepthrenderbuffer);
(optionnel)
Afin de vérifier que votre FrameBufferObject a bien été parametré, vous pouvez utiliser la fonction suivante :
// Vérification
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
	return false;

// On débind le FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0);
Astuce: Pour éviter les déformations lors du redimentionnement de votre fenêtre OpenGL, il faut adapter les tailles de la texture et du depthbuffer constamment.

Première passe OpenGL : Génération de la texture

Les shaders

Il est très facile de dire au fragment shader d'écrire ses couleurs RGB dans la texture. En effet, nous avions choisit de créer notre texture dans l'emplacement zéro (GL_COLOR_ATTACHMENT0), il suffit de rajouter dans les définitions des variables de votre fragment shader : layout(location = 0) out vec3 color_out.

#version 330
#classic fragment shader

in vec4 color_v;

layout(location = 0) out vec4 color_out; // placage des couleurs sur la texture0

void main()
{
	color_out = color_v;
}

Changement dans la fonction d'affichage

Il y a très peu de changements par rapport à un rendu classique. Il faut uniquement activer le FBO et la texture que nous avons crée précédemment.

void display(void)
{
	// -------------------------------------------------------------------------------------------------------------
	// 1ere passe OpenGL (model -> texture)
	// -------------------------------------------------------------------------------------------------------------

	// Activation du test de profondeur
	glEnable(GL_DEPTH_TEST);
	
	// Activation et binding la texture
	glBindTexture(GL_TEXTURE_2D, idTexture);
	glActiveTexture(GL_TEXTURE0);
	
	// Activation du FBO
	glBindFramebuffer(GL_FRAMEBUFFER, idFBO);
	glViewport(0, 0, g_windowWidth, g_windowHeight);
	
	// Changement de la couleur de background
	glClearColor(0.0f,0.0f,0.0f,1.0f);
	
	// Rafraichissement des buffers (reset)
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	LoadShaders(); // Chargement des shaders
	LoadUniform(); // Transmission des variables uniformes au shaders
	model->draw(); // Rendu du modèle dans la texture
	
	[...]
}

Seconde passe OpenGL : Placage de la texture sur un quad

Création d'un quad avec des VertexBufferObject (optionnel)

Une des grandes forces de l'OpenGL moderne est certainement les Vertex Buffer Object. Nous allons utiliser cette structure pour modéliser notre quadrilatère devant acquellir la texture. Le quadrilatère prendra toute la taille de l'écran et sera modèliser par deux triangles.

(optionnel)
void generateFullQuad()
{
	numberOfVertices =  4;
	numberOfTriangles = 2;
	
	//Initialisation du tableau contenant les coordonnées XYZ des sommets dans le monde
	positions = new GLfloat[3 * numberOfVertices];
	
	//Initialisation du tableau contenant les coordonnées XYZ des sommets dans la texture
	texCoords = new GLfloat[2 * numberOfVertices];
	
	//Initialisation du tableau contenant indices des triangles
	indexes = new GLfloat[3 * numberOfTriangles];
	
	// Coin bas-gauche
	positions[0] = -1.0f;
	positions[1] = -1.0f;
	positions[2] = 0.0f;
	texCoords[0] = 0.0f;
	texCoords[1] = 0.0f;

	// Coin haut-gauche
	positions[3] = -1.0f;
	positions[4] = 1.0f;
	positions[5] = 0.0f;
	texCoords[2] = 0.0f;
	texCoords[3] = 1.0f;

	// Coin haut-droit
	positions[6] = 1.0f;
	positions[7] = 1.0f;
	positions[8] = 0.0f;
	texCoords[4] = 1.0f;
	texCoords[5] = 1.0f;

	// Coin bas-droit
	positions[9] = 1.0f;
	positions[10] = -1.0f;
	positions[11] = 0.0f;
	texCoords[6] = 1.0f;
	texCoords[7] = 0.0f;
	
	// Face triangulaire 1
	indexes[0] = 0;
	indexes[1] = 1;
	indexes[2] = 2;

	// Face triangulaire 2
	indexes[3] = 2;
	indexes[4] = 3;
	indexes[5] = 0;
	
	//Génération des VBO
	glGenBuffers(1, &idOfPositionArray);
	glGenBuffers(1, &idOfTexCoordArray);
	glGenBuffers(1, &idOfIndexArray);
	
	//Remplissage des VBO
	glBindBuffer(GL_ARRAY_BUFFER, idOfPositionArray);
	glBufferData(GL_ARRAY_BUFFER, 3 * numberOfVertices * sizeof(GLfloat), positions, GL_STATIC_DRAW);
	glBindBuffer(GL_ARRAY_BUFFER, idOfTexCoordArray);
	glBufferData(GL_ARRAY_BUFFER,  2 * numberOfVertices * sizeof(GLfloat), texCoords, GL_STATIC_DRAW);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, idOfIndexArray);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, 3 * numberOfTriangles * sizeof(GLuint), indexes, GL_STATIC_DRAW);
}
(optionnel)

Les shaders

Le quad final sera texturé à l'aide d'un nouveau couple de shaders.

Vertex shader

#version 330

in vec3 vertex_in;
in vec2 texCoord_in;
out vec2 texCoord_v;

void main()
{
	texCoord_v = texCoord_in;
	gl_Position =  vec4(vertex_in, 1.0);
}

Fragment shader

#version 330

in vec2 texCoord_v;
uniform sampler2D tex0;
out vec4 color_out;

void main()
{
	// Recupération de la couleur du pixel courant
	color_out = texture(tex0, texCoord_v.xy);;
}

Changement dans la fonction d'affichage

Pour finir, il faut rajouter quelques instructions dans notre fonction d'affichage.
void display()
{
	[...]

	// -------------------------------------------------------------------------------------------------------------
	// Deuxième passe OpenGL (texture -> quad)
	// -------------------------------------------------------------------------------------------------------------

	// Désactivation du FBO
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glViewport(0, 0, g_windowWidth, g_windowHeight);
	
	// Desactivation du test de profondeur
	glDisable(GL_DEPTH_TEST);

	LoadShaders(); // Chargement des shaders
	LoadUniform(); // Transmission des variables uniformes au shaders
	quad->draw(); // Rendu du quad

	// Désactivation de la texture
	glBindTexture(GL_TEXTURE_2D, 0);
}
Résultat render-to-texture
Magnifique, n'est ce pas ?

Vos réactions 2 avis

Comment #1

MnK a écrit : le 10/03/2013 à 8:09pm

Nice, nice.

Je m'en inspirera surement dépendant de la direction qu'on va prendre dans le TER.

Pour ton prochain tutoriel de programmation tu pourrais essayer de détailler l'ensemble des fonctions auxquelles tu fais appel? Cela semble un peu obscur par moments.

Merci en tout cas.

Comment #1

KottoGeek a écrit : le 13/09/2014 à 10:14pm

Ok pour le principe mais il en manque tellement de code (les fonctions par exemple) que c'est non expoitable

En l'occurrence comment tu lies les seconds shaders au programme?