OGLdev 06 – Translations

Introduction

Dans ce tutoriel, nous commençons à étudier les diverses transformations que peut subir un objet en 3D, lui permettant d’être affiché sur l’écran tout en maintenant l’impression de profondeur dans la scène.

Contexte

Dans ce tutoriel, nous commençons à étudier les diverses transformations que peut subir un objet en 3D, lui permettant d’être affiché sur l’écran tout en maintenant l’impression de profondeur dans la scène.

La méthode commune pour réaliser cela est de représenter chaque transformation en utilisant une matrice, de multiplier les matrices une à une, puis de multiplier la position du sommet par le produit final.

Chacun de ces tutoriels sera dédié à l’étude d’une transformation.

Ici nous étudions la transformation de translation qui est responsable du déplacement d’un objet le long d’un vecteur de taille et de direction quelconques.

Supposons que vous vouliez déplacer le triangle de l’image de gauche à l’emplacement de droite :

translation

Une façon d’y arriver serait de fournir le vecteur de déplacement (dans notre cas (1, 1)) en tant que variable uniforme au shader et tout simplement de l’ajouter à chaque sommet traité. Cependant cela casse la méthode de multiplication d’un groupe de matrice de manière à obtenir une seule transformation complète.

En outre, nous verrons par la suite que la translation n’est généralement pas la première transformation appliquée, donc il faudra alors multiplier la position par la matrice des transformations antérieures à la translation, puis ajouter la translation et enfin multiplier le résultat par la matrice des transformations postérieures à la translation…

C’est pour le moins alambiqué.

Une meilleure façon de faire est de construire une matrice qui représente la translation et de l’intégrer au processus de multiplication des matrices.

Mais peut-on trouver une matrice qui, multipliée par le point (0, 0) – sommet en bas à gauche du triangle de gauche – donne le point (1, 1) ?

En réalité ce n’est pas possible avec une matrice 2D (ni une matrice 3D, pour le point (0, 0, 0)).

De manière générale, on peut dire qu’il nous faut une matrice M qui selon un point P(x, y, z) et un vecteur V(v1, v2, v3) donnés fournit M * P = P1(x + v1, y + v2, z + v3). Autrement dit, cela signifie que cette matrice M déplace P vers l’emplacement P+V.

Nous voyons que chaque composante de P1 est la somme d’une composante de P et de la composante correspondante de V. La partie gauche de chaque équation de la somme est fournie par la matrice identité :

I \times P = P(x, y, z)

Il semble donc que nous devrions commencer par la matrice identité et déterminer les changements qui vont compléter la partie droite de l’équation de la somme de chaque composante (… + V1… + V2… + V3).

Voyons à quoi ressemblerait cette matrice identité améliorée :

\left(\begin{matrix}1 & a & b\\c & 1 & d\\e & f & 1\\\end{matrix}\right)\times\left(\begin{matrix}x\\y\\z\\\end{matrix}\right)=\left(\begin{matrix}x + a \times y + b \times z &\\c \times x + y + z \times z &\\e \times x + f \times y + z &\\\end{matrix}\right)

De ce calcul nous pouvons tirer deux conclusions :

  1. a, b, c, d, e et f doivent valoir zéro sinon chaque composante va être influencée par les deux autres (nous sommes donc de retour avec la matrice identité) ;
  2. Comme x, y et z ont un effet sur les trois composantes du résultat, quand ils valent zéro le résultat vaut le vecteur nul (et nous ne pourrons pas translater le vecteur nul)

Nous voulons une matrice qui fournisse la partie droite suivante du calcul :

... = \left(\begin{matrix}x + a \times y + b \times z + v_1 &\\c \times x + y + z \times z + v_2 &\\e \times x + f \times y + z + v_3 &\\\end{matrix}\right)

Nous devons donc trouver un moyen d’ajouter v1 – v3 comme nous le voyons ci-dessus et a […] f peuvent valoir zéro. Le résultat final sera notre vecteur déplacé.

C’est comme si nous voulions ajouter une quatrième colonne à la matrice mais alors notre calcul serait invalide. Nous ne pouvons pas multiplier une matrice 3 x 4 et un vecteur 3 x 1.

La règle est que l’on ne peut multiplier des matrices que si elles sont de la forme N x M et M x N.

Donc nous devons ajouter une quatrième composante au vecteur. Une bonne quatrième composante sera 1 car nous pouvons mettre v1 – v3 sur la quatrième colonne de la matrice et elles seront inchangées dans le résultat, car multipliées par 1.

Cependant notre matrice est toujours invalide, selon la règle ci-dessus. En effet, multiplier une matrice 3 x 4 par un vecteur 4 x 1 est toujours invalide, ajouter une quatrième ligne à la matrice et la mettre 4 x 4 la rend valide.

Finalement voici notre matrice de translation :

\left(\begin{matrix}1 & 0 & 0 & v_1\\0 & 1 & 0 & v_2\\0 & 0 & 1 & v_3\\\end{matrix}\right)\times\left(\begin{matrix}x\\y\\z\\1\\\end{matrix}\right)=\left(\begin{matrix}x + v_1\\y + v_2\\z + v_3\\1\\\end{matrix}\right)

Maintenant, même si x, y et z valent zéro, nous pouvons toujours les déplacer à n’importe quel emplacement.

La représentation d’un vecteur 3D en vecteur 4D de cette manière, est appelée « coordonnées homogènes » et est très populaire et utile au graphisme 3D. La quatrième composante est appelée « w ».

En fait le symbole interne gl_Position du vertex shader que nous avons vu dans le tutoriel précédent est un vecteur 4D et la composante « w » tient un rôle très important pour transformer la projection 3D en 2D.

La notation commune est d’utiliser w = 1 pour les points et w = 0 pour les vecteurs. La raison est que les points peuvent être déplacés mais pas les vecteurs.

On peut changer la taille d’un vecteur ou sa direction mais tous les vecteurs de même taille et direction sont considérés comme égaux, quelle que soit leur position d’origine. On peut simplement utiliser l’origine du repère pour tous les vecteurs.

Définir w = 0 et multiplier une matrice de translation par le vecteur retournera le même vecteur.

Explication du code

struct Matrix4f<br />
{
	float m[4][4];
};

Nous avons ajouté la définition d’une matrice 4 x 4 au fichier math_3d.h. Elle sera utilisée pour la plupart des matrices de transformation à partir de maintenant.

GLuint gWorldLocation;

Nous utilisons cet identificateur pour accéder à la variable uniforme contenant la matrice monde dans le shader. Nous l’appelons « monde », car ce que nous allons faire à l’objet est le déplacer là où nous le voulons, dans le système de coordonnées de notre monde virtuel.

Matrix4f World;
World.m[0][0] = 1.0f;
World.m[0][1] = 0.0f;
World.m[0][2] = 0.0f;
World.m[0][3] = sinf(Scale);
World.m[1][0] = 0.0f;
World.m[1][1] = 1.0f;
World.m[1][2] = 0.0f;
World.m[1][3] = 0.0f;
World.m[2][0] = 0.0f;
World.m[2][1] = 0.0f;
World.m[2][2] = 1.0f;
World.m[2][3] = 0.0f;
World.m[3][0] = 0.0f;
World.m[3][1] = 0.0f;
World.m[3][2] = 0.0f;
World.m[3][3] = 1.0f;

Dans la fonction de rendu nous préparons une matrice 4 x 4 et la remplissons en accord avec l’explication ci-dessus. Nous définissons v2 et v3 à zéro pour ne pas avoir de changement sur les composantes Y et Z de l’objet et définissons v1 au résultat du sinus.

Ainsi la coordonnée X sera déplacée d’une valeur qui oscillera entre -1 et 1. Tout ce qu’il nous reste à faire est de charger cette matrice dans le shader.

glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, &amp;World.m[0][0]);

Voici un autre exemple de fonction glUniform* pour charger des données dans les variables uniformes du shader. Cette fonction-ci charge une matrice 4 x 4 et il existe aussi des versions pour les matrices 2 x 2, 2 x 3, 2 x 4, 3 x 2, 3 x 3, 3 x 4, 4 x 2 et 4 x 3.

Le premier paramètre est l’emplacement de la variable uniforme (récupéré après la compilation du shader, via glGetUniformLocation).

Le second paramètre indique le nombre de matrices que nous mettons à jour. Nous y mettons 1 pour une matrice mais nous pouvons aussi utiliser cette fonction pour mettre à jour toutes les matrices de multiplication en un appel.

Le troisième paramètre déroute parfois les nouveaux venus. Il indique si la matrice fournie est exprimée ligne par ligne ou colonne par colonne.

Le fait est que les matrices C/C++ sont exprimées ligne par ligne par défaut. Cela signifie que lorsque l’on remplit un tableau bidimensionnel avec des valeurs, elles sont rangées en mémoire ligne après ligne avec le « haut » à l’adresse la plus basse.

Par exemple, en regardant le tableau suivant :

int a[2][3];
a[0][0] = 1;
a[0][1] = 2;
a[0][2] = 3;
a[1][0] = 4;
a[1][1] = 5;
a[1][2] = 6;

Visuellement, le tableau ressemble à la matrice suivante :

1 2 3

4 5 6

Et l’organisation en mémoire ressemble ça : 1 2 3 4 5 6 (avec 1 à l’adresse la plus basse).

Donc notre troisième paramètre de glUniformMatrix4fv est GL_TRUE, car nous fournissons une matrice ligne par ligne. Nous pourrions aussi le mettre à GL_FALSE, mais nous devrions alors transposer notre matrice (l’organisation en mémoire resterait identique, mais OpenGL interpréterait les quatre premières valeurs comme la première colonne et ainsi de suite, et agirait en conséquence).

Le quatrième paramètre est simplement l’adresse de début de la matrice en mémoire.

Le reste de la source provient du code du shader.

uniform mat4 gWorld;

Ceci est la variable uniforme d’une matrice 4 x 4. mat2 et mat3 sont aussi disponibles.

gl_Position = gWorld * vec4(Position, 1.0);

La position des sommets du triangle dans le tampon de sommets est un vecteur à trois composantes, mais nous avons convenu de la nécessité d’une quatrième composante ayant pour valeur 1.

Nous avons donc deux options :

  1. Placer des sommets avec 4 composantes dans le tampon de sommets
  2. Ajouter la quatrième composante dans le vertex shader

Il n’y aucun avantage à utiliser la première option : chaque position de sommet consommera quatre octets supplémentaires en mémoire pour une composante qui vaudra toujours 1.

Il est plus efficace de rester avec un vecteur à trois composantes et concaténer la composante « w » dans le shader. En GLSL cela est fait en utilisant vec4(Position, 1.0).

Nous multiplions la matrice par ce vecteur et stockons le résultat dans gl_Position.

En résumé, à chaque image nous générons une matrice de translation qui déplace la coordonnée X d’une valeur qui oscille entre -1 et 1. Le shader multiplie la position de chaque sommet par cette matrice ce qui se traduit par le déplacement d’un objet de gauche à droite et de droite à gauche.

Dans la plupart des cas, l’un des côtés du triangle sortira de la boîte normalisée après le vertex shader et le clipper le découpera. Nous ne verrons donc que la partie qui se trouve dans la boîte normalisée.

Résultat

OGLdev06

Remerciements

Merci à Etay Meiri de me permettre de traduire ses tutoriels.

OGLdev 05 – Variables Uniformes

Introduction

Dans ce tutoriel, vous allez apprendre à envoyer des informations à votre GPU à travers les variables uniformes.

Contexte

Dans ce tutoriel, nous allons rencontrer un nouveau type de variables pour les shaders : les variables uniformes. La différence entre les attributs de sommets et les variables uniformes est que les attributs de sommets contiennent des données qui sont spécifiques aux sommets et sont donc rechargées avec une nouvelle valeur depuis le tampon de sommets à chaque appel du shader.

Au contraire, la valeur des variables uniformes reste constante durant toute la fonction de dessin. Cela signifie que vous chargez la valeur avant d’appeler la fonction de dessin et que vous pouvez accéder à la même valeur à chaque appel du vertex shader.

Les variables uniformes sont utiles pour stocker des données telles que les paramètres de lumière (position, direction…), les matrices de transformation, les identificateurs des objets de texture et ainsi de suite.

Dans ce tutoriel, nous allons enfin avoir quelque chose qui bouge sur l’écran. Nous allons faire cela en utilisant une combinaison de variables uniformes dont nous changerons la valeur à chaque image via le callback d’inactivité fourni par GLUT.

En fait, GLUT n’appelle pas notre callback de rendu de manière répétée, à moins qu’il ne le doive. GLUT doit appeler le callback de rendu à la suite d’événements tels que la minimisation ou maximisation de la fenêtre ou son découvrement par une autre fenêtre.

Si nous ne changeons rien dans l’agencement des fenêtres après le lancement de l’application, le callback de rendu n’est appelé qu’une seule fois. Vous allez voir la sortie une seule fois et vous la verrez ensuite encore si vous minimisez puis maximisez la fenêtre.

Il était suffisant d’enregistrer uniquement le callback de rendu dans GLUT dans les tutoriels précédents mais là, nous souhaitons changer de manière répétée la valeur d’une variable. Nous le faisons en enregistrant un callback d’inactivité. La fonction d’inactivité est appelée par GLUT quand aucun événement venant du système de fenêtrage n’est reçu. Vous pouvez avoir une fonction dédiée pour ce callback, avec laquelle vous pouvez faire n’importe quelle opération de comptage telle que la mise à jour du temps, ou simplement enregistrer votre fonction de rendu en tant que callback d’inactivité.

Dans ce tutoriel nous appliquerons ce dernier choix et mettrons à jour la variable dans la fonction de rendu.

Explication du code

glutIdleFunc(RenderSceneCB);

Ici, nous enregistrons notre fonction de rendu en tant que callback d’inactivité. Notez que si vous choisissez d’utiliser une fonction d’inactivité spécifique, vous devrez appeler glutPostRedisplay à la fin de celle-ci sinon le callback d’inactivité sera appelé encore et encore mais pas la fonction de rendu. glutPostRedisplay marque la fenêtre actuelle comme devant être redessinée et pendant le prochain tour de la boucle principale de GLUT le callback de rendu sera appelé.

gScaleLocation = glGetUniformLocation(ShaderProgram, "gScale");
assert(gScaleLocation != 0xFFFFFFFF);

Après avoir lié le programme, nous lui demandons l’emplacement de la variable uniforme.

C’est un autre exemple du cas où l’environnement d’exécution de l’application C/C++ a besoin d’être associé à l’environnement d’exécution du shader. Vous ne pouvez pas accéder directement au contenu du shader et ne pouvez donc pas mettre directement ses variables à jour.

Quand vous compilez le shader, le compilateur GLSL affecte un indice à chacune des variables uniformes. Dans la représentation interne du shader dans le compilateur, l’accès à la variable est possible grâce à cet indice. Cet indice est récupérable par l’application via glGetUniformLocation.

Vous appelez cette fonction avec l’identificateur du programme et le nom de la variable. Cette fonction retourne l’indice ou -1 s’il y a eu une erreur.

Il est très important de vérifier les erreurs (comme nous le faisons ci-dessus avec l’assertion) ou les futures mises à jour de la variable ne seront pas transmises au shader. Il y a principalement deux raisons pour lesquelles cette fonction peut échouer :

  • vous pouvez avoir mal écrit le nom de la variable ;
  • elle a été supprimée suite à une optimisation du compilateur.

Si le compilateur GLSL voit qu’une variable n’est pas utilisée dans le shader, elle est tout simplement supprimée. Dans ce cas glGetUniformLocation échouera.

static float Scale = 0.0f;
Scale += 0.001f;
glUniform1f(gScaleLocation, sinf(Scale));

Nous créons une variable flottante statique que nous incrémentons un peu à chaque appel de la fonction de rendu (vous pouvez jouer sur 0.001 si ça va trop lentement ou trop vite sur votre machine).

La valeur qui est réellement passée au shader est le sinus de la variable « Scale », ceci afin de créer une jolie oscillation entre -1.0 et 1.0.

Notez que sinf() prend des radians et non des degrés en paramètre mais à ce stade ça n’a tout simplement pas d’importance. Nous voulons juste la sinusoïde générée par la fonction.

Le résultat de sinf() est passé au shader en utilisant glUniform1f.

OpenGL fournit de multiples instances de cette fonction avec la forme générale glUniform{1234}{if}. Vous pouvez les utiliser pour charger des valeurs dans des vecteurs 1D, 2D, 3D ou 4D (selon le nombre qui suit glUniform) de flottants ou d’entiers (suffixe « f » ou « i »).

Il y a aussi des versions qui prennent l’adresse d’un vecteur en paramètre, ainsi que des versions spéciales pour les matrices.

Le premier paramètre de cette fonction est l’indice de l’emplacement que nous avons récupéré en utilisant glGetUniformLocation.

Nous allons maintenant regarder les changements qui ont été faits dans le vertex shader (le pixel shader reste inchangé).

uniform float gScale;

Ici nous déclarons la variable uniforme dans le shader.

gl_Position = vec4(gScale * Position.x, gScale * Position.y, Position.z, 1.0);

Nous multiplions les valeurs X et Y de la position du vecteur avec la valeur qui est changée par l’application à chaque image. Pouvez-vous expliquer pourquoi le triangle est retourné au milieu de la boucle ?

Résultat

OGLdev05

Remerciements

Merci à Etay Meiri de me permettre de traduire ses tutoriels.

OGLdev 04 – Shaders

Introduction

Dans ce tutoriel, vous allez apprendre à charger un shader afin qu’il soit exécuter pendant le rendu de votre triangle.

Contexte

À partir de ce tutoriel, chaque effet et technique que nous implémenterons le sera en utilisant les shaders.
Les shaders sont la méthode moderne pour faire des graphiques 3D.
D’une certaine manière vous pourriez prétendre que c’est un retour en arrière, en effet la plupart des fonctionnalités 3D qui étaient fournies par le pipeline fixe et ne nécessitaient du développeur que la définition de paramètres de configuration (attributs de lumières, rotations…), doivent maintenant être implémentées par le développeur (via les shaders).
Cependant cette programmation permet une plus grande flexibilité et innovation.

Le pipeline programmable d’OpenGL peut être visualisé comme suit :

Pipeline-OpenGL

Le processeur de sommets exécute le vertex shader sur chaque sommet qui passe à travers le pipeline (et dont le nombre est déterminé par les paramètres donnés à la fonction de dessin).
Les vertex shaders n’ont aucune connaissance de la topologie des primitives rendues. En outre, vous ne pouvez pas vous débarrasser d’un quelconque sommet dans le processeur de sommets. Chaque sommet entre dans le processeur de sommets exactement une fois, subit les transformations et continue dans le pipeline.

L’étape suivante est le « processeur de géométries ».
Dans cette étape, la connaissance de la primitive complète (c’est-à-dire de tous ses sommets) ainsi que des sommets voisins est fournie au shader. Cela permet des techniques qui doivent prendre en compte les informations additionnelles en plus du sommet lui-même.
Le geometry shader a aussi la capacité de changer la topologie de sortie vers une autre que celle choisie lors de l’appel de la fonction de dessin.
Par exemple, vous pourriez lui fournir une liste de points et générer deux triangles (c’est-à-dire un rectangle) à partir de chaque point (technique connue sous le nom de « billboarding »).
De plus, vous avez la possibilité d’émettre de multiples sommets pour chaque appel du geometry shader et ainsi créer de multiples primitives selon la topologie de sortie choisie.

L’étape suivante dans le pipe est le « clipper ».
C’est une unité fixe avec une tâche précise : il découpe les primitives selon la boîte normalisée décrite dans le tutoriel précédent. Il les découpe aussi aux plans Z proche et lointain.
Il est aussi possible de fournir des plans de clipping et faire que le clipper coupe selon ces plans.
Les positions des sommets qui ont survécu au clipper sont maintenant mappées en coordonnées écran et le rasterizer les affiche sur l’écran selon leur topologie.
Par exemple, dans le cas de triangles, ça signifie trouver tous les points qui sont dans le triangle. Pour chaque point, le rasterizer appelle le processeur de fragment. Ici vous avez la possibilité de déterminer la couleur du pixel en la récupérant depuis une texture ou n’importe quelle technique voulue.

Les trois étapes programmables (processeur de sommets, géométrie et fragment) sont optionnelles. Si vous ne leur attachez pas de shader, la fonctionnalité par défaut sera exécutée.

La gestion de shaders est très similaire à la création d’un programme C/C++.
Tout d’abord vous écrivez le texte du shader et le rendez disponible dans votre application. Ça peut être fait simplement en écrivant le texte dans une chaîne de caractères dans le code source même, ou en le chargeant depuis un fichier texte externe (là aussi dans une chaîne de caractères).
Ensuite vous compilez les shaders un par un dans des objets de shader.
Puis vous liez les shaders dans un unique programme et le chargez sur le GPU. Lier les shaders donne au pilote l’opportunité de les optimiser en fonction des relations qu’ils ont entre eux.
Par exemple, vous pourriez coupler un vertex shader qui émet une normale avec un fragment shader qui l’ignore. Dans ce cas le compilateur GLSL du driver peut supprimer les fonctionnalités liées à la normale et permettre une exécution plus rapide du vertex shader. Si plus tard ce shader est couplé avec un fragment shader qui utilise la normale, alors lier l’autre programme générera un vertex shader différent.

Explication du code

GLuint ShaderProgram = glCreateProgram();

Nous démarrons le procédé de mise en place de nos shader par la création d’un programme. Nous lierons tous les shaders ensembles à ce programme.

GLuint ShaderObj = glCreateShader(ShaderType);

Nous créons deux shaders en utilisant l’appel ci-dessus. L’un d’eux aura pour type de shader GL_VERTEX_SHADER et l’autre aura pour type GL_FRAGMENT_SHADER.

Le procédé pour spécifier la source et compiler le shader est le même pour les deux.

const GLchar* p[1];
p[0] = pShaderText;
GLint Lengths[1];
Lengths[0]= strlen(pShaderText);
glShaderSource(ShaderObj, 1, p, Lengths);

Avant la compilation du shader, nous devons spécifier son code source.

La fonction glShaderSource prend le shader comme paramètre et vous laisse une certaine liberté quant à la manière de spécifier la source.

La source peut être définie dans plusieurs chaînes de caractères et vous devez alors fournir un tableau de pointeurs sur ces chaînes ainsi qu’un tableau d’entiers où chaque valeur est la longueur de la chaîne correspondante. Par simplicité, nous utilisons une simple chaîne de caractères pour la source complète et nous utilisons deux tableaux de un élément chacun, pour la source et pour sa longueur.

Le second paramètre de l’appel est le nombre d’emplacements des deux tableaux (juste un dans notre cas).

glCompileShader(ShaderObj);

La compilation du shader est très facile …

GLint success;
glGetShaderiv(ShaderObj, GL_COMPILE_STATUS, &amp;success);
if (!success)
{
	GLchar InfoLog[1024];
	glGetShaderInfoLog(ShaderObj, sizeof(InfoLog), NULL, InfoLog);
	fprintf(stderr, "Error compiling shader type %d: '%s'\n", ShaderType, InfoLog);
}

… cependant, vous obtiendrez généralement quelques erreurs de compilation.Le bout de code ci-dessus récupère le statut de compilation et affiche toutes les erreurs que le compilateur a rencontrées.

glAttachShader(ShaderProgram, ShaderObj);

Finalement nous attachons notre shader au programme. C’est très similaire à la spécification de la liste des objets pour lier dans un makefile. Comme nous n’avons pas de makefile ici, nous émulons ce comportement par programmation. Seuls les shaders attachés prennent part au procédé d’édition de lien.

glLinkProgram(ShaderProgram);

Après avoir compilé tous nos shaders et les avoir attachés au programme, nous pouvons finalement le linker.

Notez qu’après le link du programme, vous pouvez vous débarrasser des shaders intermédiaires en appelant glDeleteShader pour chacun d’entre eux.

glGetProgramiv(ShaderProgram, GL_LINK_STATUS, &amp;Success);
if (Success == 0)
{
	glGetProgramInfoLog(ShaderProgram, sizeof(ErrorLog), NULL, ErrorLog);
	fprintf(stderr, "Error linking shader program: '%s'\n", ErrorLog);
}

Notez que nous vérifions les erreurs en relation avec le programme (comme les erreurs de liaison) d’une manière un peu différente de celles liées aux shaders. Au lieu d’utiliser glGetShaderiv nous utilisons glGetProgramiv et au lieu de glGetShaderInfoLog nous utilisons glGetProgramInfoLog.

glValidateProgram(ShaderProgram);

Vous pouvez vous demander pourquoi nous avons besoin de valider le programme après qu’il ait été correctement lié.

La différence est que le l’édition des liens vérifie les erreurs basées sur la combinaison des shaders alors que l’appel ci-dessus vérifie si le programme peut s’exécuter dans l’état actuel du pipeline.

Dans une application complexe avec de multiples shaders et de nombreux changements d’état il est conseillé de valider avant chaque appel de la fonction de dessin. Pour notre application basique, nous ne le vérifions qu’une seule fois. Aussi, vous pouvez vouloir faire cette vérification uniquement pendant le développement et éviter la surcharge dans le produit final.

glUseProgram(ShaderProgram);

Finalement, pour utiliser le programme lié, vous l’affectez au pipeline en utilisant l’appel ci-dessus.

Ce programme restera actif pour tous les appels à la fonction de dessin jusqu’à ce que vous le remplaciez par un autre ou que vous le désactiviez explicitement (et activiez le pipeline fixe) en appelant glUseProgram avec NULL.

Si vous avez créé un programme qui contient un seul type de shader alors les autres étapes fonctionnent en utilisant leur fonctionnalité fixe par défaut.

Nous avons terminé l’explication des appels OpenGL relatifs à la gestion de shader.

Le reste de ce tutoriel est relatif au contenu des vertex et fragment shaders (contenus dans les variables pVS et pFS).

#version 330

Cette ligne dit au compilateur que nous ciblons la version 3.3 de GLSL. Si le compilateur ne la supporte pas il va émettre une erreur.

layout (location = 0) in vec3 Position;

Cette déclaration apparaît dans le vertex shader.

Elle déclare qu’un attribut spécifique au sommet, un vecteur de trois flottants, sera connu sous le nom « Position » dans le shader. « Spécifique au sommet » signifie que pour tous les appels du shader dans le GPU la valeur d’un nouveau sommet venant du tampon sera fournie.

La première section de la déclaration, « layout (location=0) », crée un lien entre le nom de l’attribut et l’attribut dans le tampon. C’est requis pour les cas où notre sommet contient plusieurs attributs (position, normale, coordonnées de texture…). Nous informons le compilateur quel attribut du sommet dans le tampon doit être affecté à l’attribut déclaré dans le shader.

Il y a deux méthodes pour faire cela.

  • Nous pouvons le déclarer explicitement comme nous le faisons ici (à zéro).
    Dans ce cas nous pouvons utiliser la valeur en dur dans notre application (ce que nous avons fait avec le premier paramètre de l’appel à glVertexAttributePointer).
  • Ou nous pouvons le laisser indéfini (et simplement déclarer « in vec3 Position » dans le shader) et demander sa position à l’exécution de l’application en utilisant glGetAttribLocation.
    Dans ce cas nous devrons fournir la valeur retournée à glVertexAttributePointer au lieu de la valeur en dur.

Nous avons choisi la méthode simple mais dans des applications plus complexes il vaut mieux laisser le compilateur déterminer les indices des attributs et les demander à l’exécution. Cela rend plus facile l’intégration de shaders à partir de sources multiples sans avoir à les adapter à la disposition de votre tampon.

void main()

Vous pouvez créer votre shader en liant ensemble de multiples shaders. Cependant il n’y a qu’une fonction « main » pour chaque étape de shader (VS, GS ou FS) qui est utilisée comme point d’entrée du shader.

Par exemple vous pouvez créer une bibliothèque de gestion de l’éclairage avec plusieurs fonctions et la lier avec votre shader, si tant est qu’aucune de ses fonctions n’est nommée « main ».

gl_Position = vec4(0.5 * Position.x, 0.5 * Position.y, Position.z, 1.0);

Ici nous effectuons des transformations codées en dur sur la position des sommets qui arrivent. Nous divisons par deux les valeurs X et Y et laissons Z inchangé.

gl_Position est une variable prédéfinie spéciale qui est définie pour contenir des coordonnées homogènes (composantes X, Y, Z et W) pour la position du sommet. Le rastériseur va regarder dans cette variable et l’utiliser comme position dans l’espace écran (après quelques transformations supplémentaires). Diviser X et Y par deux signifie que nous allons voir un triangle dont la taille est le quart de celle du triangle du précédent tutoriel.

Notez que nous mettons aussi W à 1.0. C’est extrêmement important afin que le triangle soit affiché correctement.

Effectuer la projection de la 3D vers la 2D est en réalité une action accomplie en deux étapes séparées.

  • Tout d’abord il faut multiplier tous les sommets par la matrice de projection (que nous développerons d’ici quelques tutoriels).
  • Ensuite le GPU effectue ce qui est connu comme le « perspective divide » à l’attribut position avant qu’elle n’atteigne le rastériseur.
    Cela signifie qu’il divise toutes les composante de gl_Position par sa composante W.

Dans ce tutoriel, nous ne faisons pas encore de projection dans le vertex shader mais l’étape de « perspective divide » est quelque chose que nous ne pouvons pas désactiver. Quelle que soit la valeur de gl_Position que nous sortons du vertex shader, ses composantes XYZ vont être divisées par le GPU en utilisant sa composante W.

Nous devons nous souvenir que sans celle-ci nous n’aurons pas les résultats que nous attendons. Afin de contourner l’effet de cette division, nous définissons W à 1.0. Une division par 1.0 n’affectera pas les autres composantes de la position du vecteur qui restera alors dans notre boîte normalisée.

Si tout s’est passé correctement, trois sommets avec les valeurs (-0.5, -0.5), (0.5, -0.5) et (0.0, 0.5) atteignent le rastériseur.

Le clipper n’a pas besoin de faire quoi que ce soit, car tous les sommets sont bien dans la boîte normalisée. Ces valeurs sont mappées en coordonnées dans l’espace écran et le rastériseur commence à parcourir tous les points dans le triangle. Pour chaque point le fragment shader est exécuté.

Le code de shader qui suit est pris du fragment shader.

out vec4 FragColor;

Habituellement le travail du fragment shader est de déterminer la couleur du fragment (pixel).

En plus, le fragment shader peut complètement se débarrasser du pixel ou changer sa valeur en Z (ce qui affectera le résultat du test de profondeur ultérieur).

La production de la couleur se fait en déclarant la variable ci-dessus. Les quatre composantes représentent R, G, B et A (pour alpha).

La valeur que vous placez dans cette variable sera reçue par le rastériseur et enfin écrite dans le tampon d’image.

FragColor = vec4(1.0, 0.0, 0.0, 1.0);

Dans le précédent couple de tutoriels, il n’y avait pas de fragment shader donc tout était dessiné avec la couleur par défaut qui est le blanc. Ici nous définissons FragColor à rouge.

Résultat

OGLdev04

Remerciements

Merci à Etay Meiri de me permettre de traduire ses tutoriels.

OGLdev 03 – Premier triangle

Introduction

Dans ce tutoriel vous allez apprendre à dessiner votre premier triangle avec OpenGL.

Contexte

Ce tutoriel est très court. Nous élargissons juste le tutoriel précédent afin d’afficher un triangle.

Dans ce tutoriel nous allons, une nouvelle fois, nous appuyer sur la boîte normalisée. Les sommets visibles doivent être dans la boîte pour que la transformation de la vue les fasse correspondre à des coordonnées visibles dans la fenêtre. En regardant le long de l’axe Z négatif, cette boîte ressemble à ça :

coord_system

Le point (-1.0, 1.0) correspond au coin bas gauche de la fenêtre, (-1.0, 1.0) correspond au coin haut gauche et ainsi de suite. Si vous étirez la position d’un des sommets hors de cette boîte, le triangle sera coupé et vous n’en verrez qu’une partie.

Explication du code

Vector3f Vertices[3];
Vertices[0] = Vector3f(-1.0f, -1.0f, 0.0f);
Vertices[1] = Vector3f(1.0f, -1.0f, 0.0f);
Vertices[2] = Vector3f(0.0f, 1.0f, 0.0f);

Nous avons agrandi le tableau afin qu’il contienne trois sommets.

glDrawArrays(GL_TRIANGLES, 0, 3);

Deux changements ont été apportés à la fonction de dessin : nous dessinons des triangles au lieu de points et nous dessinons trois sommets au lieu d’un seul.

Résultat

OGLdev03

Remerciements

Merci à Etay Meiri de me permettre de traduire ses tutoriels.

OGLdev 02 – Hello Dot

Introduction

Ce tutoriel vous présente GLEW pour la gestion des extensions OpenGL afin d’afficher votre premier point dans votre fenêtre.

Contexte

C’est notre première rencontre avec la bibliothèque GLEW, « OpenGL Extension Wrangler Library ». GLEW vous aide à contourner le mal de crâne qui vous attend si vous voulez gérer les extensions dans OpenGL. Une fois initialisé, il récupère les extensions disponibles sur votre plateforme, les charge et fournit un accès facile via un simple fichier d’en-tête.

Dans ce tutoriel, nous allons voir l’utilisation des « Vertex Buffer Objects » (VBO) pour la première fois. Comme son nom l’indique, ils sont utilisés pour stocker des sommets.

Les objets du monde 3D que vous essayez de virtualiser, que ce soit des monstres, des châteaux ou un simple cube tournant, sont toujours construits en connectant des groupes de sommets ensemble.

Les VBO sont la manière la plus efficace de charger des sommets sur le GPU. Ce sont des tampons qui sont stockés sur la mémoire vidéo et ils fournissent le plus faible temps d’accès au GPU, ils sont donc vivement recommandés.

Ce tutoriel et le suivant sont les seuls de cette série qui s’appuient sur le pipeline fixe plutôt que sur le pipeline programmable.

En réalité, aucune transformation ne va avoir lieu dans ces deux tutoriels. Nous nous appuyons simplement sur la manière dont le flux de données transite dans le pipeline.

Une étude plus approfondie du pipeline suivra dans les prochains tutoriels, mais pour l’instant, il suffit de comprendre qu’avant d’atteindre le rasterizer (qui dessine effectivement les points, lignes et triangles en utilisant les coordonnées écran), les sommets visibles ont leurs coordonnées X, Y et Z entre les bornes [-1.0, 1.0]. Le rasterizer fait correspondre les coordonnées à l’espace écran (exemple : si la largeur de l’écran est 1024 alors la coordonnée 0.0 correspond à 0 et 1.0 correspond à 1023).

Finalement, le rasterizer dessine les primitives selon la topologie spécifiée dans la fonction de dessin (voir l’explication de code ci-dessous). Comme nous n’attachons pas de shader au pipeline, nos sommets ne subissent aucune transformation.

Ça signifie que nous avons juste à leur donner une valeur entre les bornes précédentes afin de les rendre visibles. En fait, définir zéro pour X et Y place le sommet au milieu des deux axes et par là même au milieu de l’écran.

Installation de GLEW :

GLEW est disponible sur le site http://glew.sourceforge.net/.

La plupart des distributions Linux fournissent un paquet pour lui.

Sur Ubuntu vous pouvez l’installer en utilisant la ligne de commande suivante :

apt-get install libglew1.6 libglew1.6-dev

Explication du code

#include <GL/glew.h>;

Ici nous incluons l’unique fichier d’en-tête de GLEW.

Si vous incluez d’autres fichiers d’en-têtes OpenGL, vous devez faire attention à inclure ce fichier d’en-tête avant les autres sinon GLEW va s’en plaindre. Afin de linker le programme avec GLEW, vous devez ajouter ‘-lGLEW‘ au makefile.

#include "math_3d.h"

Dans ce tutoriel nous commençons à utiliser des structures d’aides telles que les vecteurs. Nous étofferons ce fichier d’en-tête au fur et à mesure de nos avancées.

GLenum res = glewInit();
if (res != GLEW_OK)
{
    fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
    return 1;
}

Ici nous initialisons GLEW et vérifions les erreurs éventuelles. Cela doit être fait après l’initialisation de GLUT.

Vector3f Vertices[1];
Vertices[0] = Vector3f(0.0f, 0.0f, 0.0f);

Nous créons un tableau contenant une structure Vector3f (type défini dans math_3d.h) et initialisons XYZ à zéro. Ainsi le point apparaîtra au milieu de l’écran.

GLuint VBO;

Nous allouons un GLuint en variable globale au programme pour stocker l’identificateur du Vertex Buffer Object.

Vous verrez par la suite que la majorité (si pas tous) des objets OpenGL sont représentés par une variable de type GLuint.

glGenBuffers(1, &amp;VBO);

OpenGL définit plusieurs fonctions glGen* pour créer des objets de différents types.

Elles prennent souvent deux paramètres :

  • le premier spécifie le nombre d’objets à créer ;
  • le second est l’adresse d’un tableau de GLuint pour stocker les identificateurs alloués par le pilote (faites attention à avoir un tableau assez grand pour récupérer le résultat).

Les futurs appels à cette fonction ne génèreront pas les mêmes identificateurs à moins qu’ils aient d’abord été détruits avec glDeleteBuffers.

Notez qu’à ce stade vous ne spécifiez pas ce que vous avez l’intention de faire avec les tampons et donc ils peuvent être considérés comme « génériques ». Leur spécification est la tâche de la fonction suivante.

glBindBuffer(GL_ARRAY_BUFFER, VBO);

OpenGL a une manière plutôt unique d’utiliser les identificateurs.

Dans de nombreuses bibliothèques, l’identificateur est simplement passé à une fonction correspondante et l’action est effectuée sur cet identificateur. En OpenGL, nous attachons l’identificateur à un nom de cible et effectuons ensuite les commandes sur cette cible. Ces commandes affectent l’identificateur attaché jusqu’à ce qu’un autre soit attaché à sa place ou que la fonction ci-dessus soit appelée avec 0 comme identificateur.

La cible GL_ARRAY_BUFFER signifie que le tampon va contenir un tableau de sommets.

Une autre cible utile est GL_ELEMENT_ARRAY_BUFFER qui signifie que le tampon contient les indices des sommets d’un autre tampon.

>

D’autres cibles sont aussi disponibles et nous verrons leur utilité dans de futurs tutoriels.

glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

Après avoir attaché notre objet, nous le remplissons de données.

L’appel ci-dessus prend le nom de la cible (le même que celui utilisé pour l’attachement), la taille des données en octets, l’adresse du tableau de sommets et un indicateur indiquant le modèle d’utilisation de ces données.

Comme nous n’allons pas changer le contenu du tampon, nous spécifions GL_STATIC_DRAW. L’opposé serait GL_DYNAMIC_DRAW.

Bien que ce ne soit qu’un conseil pour OpenGL, il est bon de réfléchir à l’indicateur correct à utiliser. Le pilote peut s’appuyer dessus pour des stratégies d’optimisation (comme la meilleure place en mémoire pour stocker le tampon).

glEnableVertexAttribArray(0);

Dans le tutoriel sur les shaders, vous verrez que les attributs de sommet utilisés par le shader (position, normale …) ont un indice associé, qui vous permet de créer un lien ente les données du programme C/C++ et le nom de l’attribut au sein du shader.

De plus vous pouvez aussi activer chaque indice d’attribut de sommet.

Dans ce tutoriel, nous n’utilisons pas encore de shader mais la position du sommet que nous avons chargé dans le tampon est traitée comme l’indice 0 d’attribut de sommet dans le pipeline fixe (qui devient actif s’il n’y a pas de shader lié).

Vous devez activer chaque attribut de sommet sinon les données associées ne seront pas accessibles dans le pipeline.

glBindBuffer(GL_ARRAY_BUFFER, VBO);

Ici, nous attachons encore notre tampon comme nous nous préparons à appeler la fonction de dessin.

Dans ce petit programme, nous n’avons qu’un seul tampon de sommet donc faire cet appel à chaque frame est redondant, mais dans les programmes plus complexes il y a de multiples tampons pour stocker vos divers modèles, et vous devez mettre à jour l’état du pipeline avec le tampon que vous avez l’intention d’utiliser.

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

Cet appel dit au pipeline comment interpréter les données dans le tampon.

Le premier paramètre spécifie l’indice de l’attribut.Dans notre cas, nous savons que c’est zéro par défaut, mais quand nous commencerons à utiliser les shaders, nous aurons besoin soit de le spécifier explicitement, soit de le demander.

Le second paramètre est le nombre de composantes dans l’attribut (trois pour X, Y et Z).

Le troisième paramètre est le type de données de toutes les composantes.

Le paramètre suivant indique si nous voulons que notre attribut soit normalisé avant son utilisation dans le pipeline. Dans notre cas, nous voulons que nos données passent sans être modifiées.

Le cinquième paramètre (appelé « stride ») est le nombre d’octets entre deux instance de l’attribut dans le tampon. Quand il n’y a qu’un seul attribut (exemple : le tampon contient uniquement des positions) et que les données sont contiguës, nous passons zéro. Si nous avons un tableau de structures qui contiennent une position et une normale (chacune étant un vecteur de trois flottants), nous passerons la taille de la structure en octets (6 * 4 = 24).

Le dernier paramètre est utile dans le cas de l’exemple précédent. Nous devons spécifier le décalage à l’intérieur de la structure où le pipeline va trouver notre attribut. Dans le cas de la structure avec la position et la normale, le décalage de la position est zéro alors que le décalage de la normale est 12.

glDrawArrays(GL_POINTS, 0, 1);

Enfin, nous appelons la fonction de dessin de la géométrie.

Toutes les commandes que nous avons vues jusqu’ici sont importantes, mais elle ne font que préparer le terrain pour l’appel de cette fonction. C’est là que le GPU commence réellement son travail. Il va combiner les paramètres de la fonction de dessin avec l’état qui a été construit jusqu’à maintenant et rendre le résultat sur l’écran.

OpenGL fournit plusieurs types de fonctions de dessin et chacune est propre à un cas particulier. En général il est possible de les diviser en deux catégories :

  • les dessins ordonnés ;
  • les dessins indicés.

Les dessins ordonnés sont les plus simples. Le GPU parcourt le tampon de sommets, sommet par sommet et les interprète selon la topologie spécifiée dans la fonction de dessin. Par exemple, si vous spécifiez GL_TRIANGLES alors les sommets 0 à 2 deviennent le premier triangle, 3 à 5 le second …

Si vous voulez que le même sommet apparaisse dans plus d’un triangle, vous devez le spécifier deux fois dans le tampon de sommets, ce qui est un gachis de place.

Les dessins indicés sont plus complexes et impliquent un tampon supplémentaire : le tampon d’indices. Le tampon d’indices contient les indices des sommets dans le tampon de sommets. Le GPU scanne le tampon d’indices et de la même manière que dans la description précédente les indices 0 à 2 forment le premier triangle …

Si vous voulez que le même sommet apparaisse dans deux triangles, vous avez juste à spécifier son indice deux fois dans le tampon d’indices. Le tampon de sommets a besoin de n’en contenir qu’une seule copie.

Les dessins indicés sont plus communs dans les jeux, car la majorité des modèles sont créés à partir de triangles qui représentent une surface (peau d’une personne, mur de château …) avec beaucoup de partages de sommets entre eux.

Dans ce tutoriel, nous utilisons la fonction de dessin la plus simple : glDrawArrays. C’est un dessin ordonné où il n’y a pas de tampon d’indices.

Nous spécifions la topologie à « points » ce qui signifie que chaque sommet est un point.

Le paramètre suivant est l’indice du premier sommet à dessiner. Dans notre cas, nous voulons commencer au début du tampon donc nous spécifions zéro mais ça nous permet de stocker plusierus modèles dans le même buffer et choisir celui à afficher selon le décalage dans le buffer.

Le dernier paramètre est le nombre de sommets à dessiner.

glDisableVertexAttribArray(0);

C’est une bonne pratique de désactiver chaque attribut de sommet quand il n’est pas immédiatement utilisé.

Le laisser activé quand un shader ne l’utilise pas est un moyen sûr de trouver des problèmes.

Résultat

OGLdev02

Remerciements

Merci à Etay Meiri de me permettre de traduire ses tutoriels.

OGLdev 01 – Ouvrir une fenêtre

Introduction

Cet article a l’objectif très simple de vous faire ouvrir une fenêtre permettant le rendu OpenGL et servant de support pour les tutoriels suivants.

Contexte

La spécification d’OpenGL ne définit pas de fonctions particulières pour manipuler des fenêtres. Les systèmes modernes de fenêtrage supportant OpenGL incluent généralement un sous-système fournissant la liaison entre un contexte OpenGL et le système de fenêtrage. Dans le système X Window, cette interface est appelée GLX. Microsoft fournit WGL (prononcé Wigueul) pour Windows et MacOS a CGL. Travailler directement avec ces interfaces afin de créer une fenêtre pour gérer l’affichage est généralement un travail fastidieux, c’est pourquoi nous allons utiliser une bibliothèque de haut niveau qui fera abstraction de ces détails.

La bibliothèque que nous allons utiliser ici est appelée « OpenGL utility library », ou GLUT.Elle fournit des fonctions simplifiées pour la gestion de fenêtres ainsi qu’une gestion évènementielle, une gestion des E/S et quelques autres services. De plus, GLUT est multiplateforme, ce qui facilite le portage.

Les alternatives à GLUT sont, entre autres, SDL ou GLFW.

Explication du code

glutInit(&argc, argv);

Cet appel initialise GLUT.
Les paramètres peuvent être fournis directement depuis la ligne de commande et inclure des options utiles du type « -sync » ou « -gldebug » qui désactive la nature asynchrone de X et vérifie automatiquement les erreurs GL et les affiche (respectivement).

glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);

Ici nous configurons quelques options de GLUT.
GLUT_DOUBLE active le « double buffering » (dessiner dans un tampon en arrière-plan pendant qu’un autre tampon est affiché).
GLUT_RGBA définit le format des pixels du tampon de couleurs où sont appliqués les rendus (par exemple : l’écran).
On active généralement ces deux options ainsi que d’autres comme nous le verrons plus tard.

glutInitWindowSize(1024, 768);
glutInitWindowPosition(100, 100);
glutCreateWindow("Tutorial 01");

Ces appels définissent les paramètres de la fenêtre et la créent.Vous avez aussi la possibilité de spécifier le titre de la fenêtre.

glutDisplayFunc(RenderSceneCB);

Comme nous travaillons dans un système fenêtré, les interactions avec le programme vont principalement se faire au travers de callbacks sur des évènements. GLUT prend en charge les interactions avec le système de fenêtres sous-jacent et nous fournit quelques callbacks.
Ici nous en utilisons juste un : le callback « principal » pour effectuer le rendu d’une image. Cette fonction est appelée continuellement par la boucle interne de GLUT.

glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

C’est notre première rencontre avec le concept d’état en OpenGL.
L’idée d’un état est que le rendu est une tâche tellement complexe qu’elle ne peut être traitée dans un seul appel de fonction avec quelques paramètres (et une fonction correctement conçue ne devrait jamais recevoir beaucoup de paramètres).
Il faut spécifier les shaders, tampons et divers indicateurs qui affectent la manière dont le rendu s’effectue. De plus vous voudrez souvent garder la même configuration au travers de plusieurs opérations de rendu (exemple : si vous ne désactivez jamais le test de profondeur, il n’y a pas lieu de le spécifier à chaque rendu). C’est pourquoi la plus grande partie de la configuration des opérations de rendu s’effectue en définissant des indicateurs et des valeurs dans la machine à états d’OpenGL. Les rendus eux-mêmes sont habituellement limités aux quelques paramètres relatant du nombre de sommets à dessiner et leur décalage de départ.
Après l’appel d’une fonction de changement d’état, cette nouvelle configuration reste inchangée jusqu’au prochain appel de cette même fonction avec une valeur différente. L’appel ci-dessus définit la couleur qui sera utilisée lors du vidage du tampon d’image (décrit plus tard). La couleur a quatre composantes (RGBA) et chaque composante est normalisée entre 0.0 et 1.0.

glutMainLoop();

Cet appel donne le contrôle à GLUT qui lance sa boucle interne. Dans cette boucle GLUT écoute les évènements du système de fenêtres et les passe aux callbacks que nous avons définies. Dans notre cas, GLUT va seulement appeler la fonction enregistrée comme callback d’affichage (RenderSceneCB) pour nous donner l’occasion de dessiner une image.

glClear(GL_COLOR_BUFFER_BIT);
glutSwapBuffers();

La seule chose que nous faisons dans notre fonction de rendu est de vider le tampon d’image (en utilisant la couleur spécifiée au-dessus – essayez de la changer). Le second appel dit à GLUT d’échanger les rôles du « backbuffer » et du « frontbuffer ». Dans le prochain appel du callback de rendu, nous rendrons dans l’actuel « frontbuffer » et l’actuel « backbuffer » sera affiché.

Résultat

OGLdev01

Remerciements

Merci à Etay Meiri de me permettre de traduire ses tutoriels.