Les extensions OpenGL en VBA et VB6

Thierry GASPERMENT

Utilisez les extensions OpenGL en VB.
Cet article est destiné aux développeurs VBA et VB6 expérimentés qui souhaitent approfondir OpenGL.

Nous mettrons en pratique les extensions avec l'utilisation des VBO (vertex buffer objects), des textures 3D, et des Shaders.

Les sources sont proposées en téléchargement pour Access et VB6, mais le tutoriel peut également être appliqué avec Word, Excel, ou PowerPoint.

Commentez cet article : Commentez

- Titre : Les extensions OpenGL en VBA et VB6

- Auteur : Thierry GASPERMENT

- Parution : 15 novembre 2009

- Mise à jour : 20 mars 2019

- 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 ® 2009 Thierry GASPERMENT. 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. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- I. Introduction

- II. Les extensions

- III. glBindBuffer ou glBindBufferARB ?

- IV. Vérifier la compatibilité de la carte graphique

- V. Préparation de l'environnement de développement VB

- VI. Les Vertex Buffer Objects

- VI-A. Préparation des tableaux de données

- VI-B. Création des buffers

- VI-C. Utilisation des buffers

- VI-D. Modification des buffers

- VII. Les Textures 3D

- VII-A. Utilité des textures 3D

- VII-B. Préparation de l'environnement de développement VB

- VII-C. Génération de la texture

- VII-D. Application de la texture

- VIII. Les Shaders

- VIII-A. Préparation de l'environnement de développement VB

- VIII-B. Shader et Program

- VIII-C. Les types de Shaders

- VIII-D. Mon premier Shader

- VIII-E. Génération du programme exécutable par la carte graphique

- VIII-F. Utilisation du programme

- IX. Conclusion

- X. Téléchargements

I - Introduction

Au cours du tutoriel précédent, nous avons découvert la programmation en 3D avec OpenGL.

Dans ce nouvel article, nous allons apprendre à utiliser les extensions d'OpenGL en VB.

II - Les extensions

La bibliothèque OpenGL livrée avec Windows ne contient que les fonctions de la version 1.1.
Les fonctions des versions suivantes sont accessibles sous forme d'extensions.

Vous trouverez, en plus des différentes versions, des extensions :

- en cours de validation (EXT) ;

- validées pour une future version (ARB) ;

- spécifiques à un matériel (NV, IBM, ATI…).

Ces extensions sont regroupées dans les différents modules du Pack de module VB pour OpenGL:

Module

Extensions

ModOpenGL_1_2 à ModOpenGL_4_6

Versions 1.2 à 4.6.

ModOpenGL_ARB

Extensions ARB, validée.

ModOpenGL_EXT

Extensions EXT, en attente de validation.

ModOpenGL_OTHER

Extensions spécifiques : ATI, IBM, NVIDIA…

ModOpenGL_WGL

Extensions WGL spécifiques à Windows

Afin de pouvoir utiliser ces modules, il faut, avant toute chose, redéfinir les fonctions pour qu'elles pointent vers les fonctions d'extensions.

Pour ce faire, dans chacun de ces modules, vous trouverez une fonction RemapVBToGL.
Cette fonction doit être exécutée après la création d'un contexte OpenGL.

Par exemple pour utiliser les fonctions des Vertex Buffer Objects (VBO) de la version 1.5 :

- importez le module ModOpenGL_1_5 ;

- après l'initialisation de freeGLUT, appelez la fonction ModOpenGL_1_5.RemapVBToGL.

Les fonctions du module ModOpenGL_1_5 (par exemple glBindBuffer) peuvent ensuite être utilisées.

Les déclarations de ces fonctions d'extensions ont été traduites en VB à partir des fichiers d'en-tête de Glew, écrits en langage C++.

Toutes les fonctions n'ont pas été testées.

III - glBindBuffer ou glBindBufferARB ?

Vous pouvez trouver certaines fonctions avec le suffixe ARB ou EXT, et également la même fonction sans suffixe.

Par exemple :

- glBindBuffer est présente dans la version 1.5, module ModOpenGL_1_5 ;

- glBindBufferARB est présente dans les extensions ARB, module ModOpenGL_ARB.

En fonction du driver de la carte graphique, l'une ou l'autre des deux fonctions peut être supportée :

- la fonction glBindBuffer si le driver est compatible avec la version 1.5 ;

- la fonction glBindBufferARB si le driver est compatible avec l'extension GL_ARB_vertex_buffer_object (mais pas forcément avec la version 1.5).

Utilisez de préférence la fonction sans suffixe.
Si celle-ci n'est pas supportée, la fonction RemapVBToGL recherchera automatiquement la fonction avec un suffixe ARB ou EXT.

Ainsi un programme qui utilise la fonction glBindBuffer fonctionnera sur les cartes compatibles avec la version 1.5, et également avec les cartes compatibles avec l'extension GL_ARB_vertex_buffer_object.

IV - Vérifier la compatibilité de la carte graphique

Toutes les cartes graphiques (ou plutôt leur pilote) ne sont pas compatibles avec toutes les extensions et versions.

Pour vérifier la version d'OpenGL supportée, utilisez la fonction glGetString avec le paramètre GL_VERSION.

Pour vérifier les extensions supportées, utilisez la fonction glGetString avec le paramètre GL_EXTENSIONS.

La fonction glGetString renvoie un entier de type Long (ou LongPtr) qui est un pointeur vers une chaîne de caractères.

Pour obtenir la chaîne de caractères, utilisez la fonction LongToString du module ModOpenGLTools.

Exemple : tester si la version d'OpenGL est au moins égale à 1.5

dim lVersion as String
lVersion = LongToString(glGetString(GL_VERSION))
If val(lVersion) < 1.5 then
MsgBox "Version 1.5 non supportée!"
End If

Exemple : tester si l'extension GL_ARB_vertex_buffer_object est supportée

dim lExtensions as String
lExtensions = LongToString(glGetString(GL_EXTENSIONS))
If InStr(lExtensions,"GL_ARB_vertex_buffer_object") = 0 then
MsgBox "VBO non supportés!"
End If

Il est également possible de tester directement le support d'une fonction en recherchant l'adresse mémoire de la fonction. Celle-ci est égale à zéro si la fonction n'est pas supportée :

Exemple : tester le support de la fonction glBindBuffer

If wglGetProcAddress("glBindBuffer") = 0 Then
MsgBox "glBindBuffer non supporté!"
End If

V - Préparation de l'environnement de développement VB

Référez-vous au premier tutoriel pour télécharger les modules nécessaires et créer une fenêtre Glut.

Vous devez importer les modules ModOpenGLTools, ModOpenGLFreeGlut, ModOpenGL_1_1.
Ces modules sont dans le Pack de module VB pour OpenGL.

Vous devez également copier la librairie FreeGlut au côté de votre application.

Voici pour rappel le code pour une fenêtre GLUT qui affiche un cube coloré :

Rappel du code d'affichage d'un cube

Option Explicit

' Temps écoulé pour calcul du frame rate
Private gElapsedTime As Long
' Nombre d'affichages pour calcul du frame rate
Private gDisplayCount As Long
' RotationX
Private gRotateX As Double
' RotationY
Private gRotateY As Double
' Zoom
Private gZoom As Double

' Fonction principale
Function FonctionOpenGL()
' Chargement de freeglut
If Not LoadFreeGlut(CurrentProject.Path) Then
MsgBox "Impossible de charger la bibliothèque freeglut"
Exit Function
End If
' Initialisation de la bibliothèque
glutInit 0&, ""
' Initialisation du mode d'affichage
glutInitDisplayMode GLUT_RGBA Or GLUT_DOUBLE Or GLUT_DEPTH
' Création d'une fenêtre
glutCreateWindow "Tutoriel fenêtre GLUT"
' Définition de l'option de sortie de boucle
glutSetOption GLUT_ACTION_ON_WINDOW_CLOSE, GLUT_ACTION_GLUTMAINLOOP_RETURNS
' Fonction d'affichage
glutDisplayFunc AddressOf CallBackDraw
' Fonctions de rappel clavier
glutSpecialFunc AddressOf CallBackSpecial
' Fonction de rappel molette de souris
glutMouseWheelFunc AddressOf CallBackMouseWheel
' Appel de la fonction d'initialisation
InitScene
' Fonction d'attente
glutTimerFunc 1000 / 50, AddressOf CallBackTimer, 0
' Boucle principale
glutMainLoop
End Function

' Fonction d'affichage
Public Sub CallBackDraw()
' Appel de la fonction de rendu
Call Render
' Echange les buffers
glutSwapBuffers
' Calcul et affichage du frame rate
gDisplayCount = gDisplayCount + 1
If gDisplayCount = 100 Then
'glutSetWindowTitle "Frame rate : " & format(gDisplayCount / (glutGet(GLUT_ELAPSED_TIME) - gElapsedTime) * 1000, "0,0")
gDisplayCount = 0
gElapsedTime = glutGet(GLUT_ELAPSED_TIME)
End If
End Sub

' Fonction d'attente
Public Sub CallBackTimer(ByVal value As Long)
' Rappel de la fonction d'attente
glutTimerFunc 1000 / 50, AddressOf CallBackTimer, 0
' Rafraichir l'affichage si une fenêtre openGL est active
If glutGetWindow <> 0 Then glutPostRedisplay
End Sub

' Gestion du clavier
Public Sub CallBackSpecial(ByVal key As Long, ByVal X As Long, ByVal y As Long)
Select Case key
Case GLUT_KEY_LEFT
gRotateY = gRotateY - 10
Case GLUT_KEY_RIGHT
gRotateY = gRotateY + 10
Case GLUT_KEY_UP
gRotateX = gRotateX - 10
Case GLUT_KEY_DOWN
gRotateX = gRotateX + 10
End Select
End Sub

' Gestion de la molette de la souris
Public Sub CallBackMouseWheel(ByVal wheel As Long, ByVal direction As Long, ByVal X As Long, ByVal y As Long)
gZoom = gZoom + direction
End Sub

' Initialisation de la scène
Public Sub InitScene()
' Tests de profondeur
glEnable GL_DEPTH_TEST
glDepthFunc GL_LEQUAL
' Initialisation des variables de visualisation
gRotateX = 0
gRotateY = 0
gZoom = 10
End Sub

' Rendu de la scène
Public Sub Render()
' Passage en matrice de projection
glMatrixMode GL_PROJECTION
' Initialisation de la matrice
glLoadIdentity
' Définition de la perspective
gluPerspective 45, 1, 0.1, 100
' Passage en matrice de modélisation-visualisation
glMatrixMode GL_MODELVIEW
' Initialisation de la matrice
glLoadIdentity
' Position de la caméra
gluLookAt 0, 0, gZoom, 0, 0, 0, 0, 1, 0
glRotated gRotateX, 1, 0, 0
glRotated gRotateY, 0, 1, 0
' Vide les buffers couleur et profondeur
glClear GL_COLOR_BUFFER_BIT Or GL_DEPTH_BUFFER_BIT
' Sauvegarde la matrice
glPushMatrix
' Rotation du cube
glRotated 30, 1, 1, 1
' Début de la primitive pour le cube
glBegin GL_QUADS
' Face du haut = vert
Call glColor3d(0, 1, 0)
Call glVertex3d(1, 1, -1)
Call glVertex3d(-1, 1, -1)
Call glVertex3d(-1, 1, 1)
Call glVertex3d(1, 1, 1)
' Face du bas = orange
Call glColor3d(1, 0.5, 0)
Call glVertex3f(-1, -1, -1)
Call glVertex3f(1, -1, -1)
Call glVertex3f(1, -1, 1)
Call glVertex3f(-1, -1, 1)
' Face de derrière = rouge
Call glColor3d(1, 0, 0)
Call glVertex3f(1, -1, -1)
Call glVertex3f(-1, -1, -1)
Call glVertex3f(-1, 1, -1)
Call glVertex3f(1, 1, -1)
' Face de devant = jaune
Call glColor3d(1, 1, 0)
Call glVertex3f(-1, -1, 1)
Call glVertex3f(1, -1, 1)
Call glVertex3f(1, 1, 1)
Call glVertex3f(-1, 1, 1)
' Face de gauche = violet
Call glColor3d(1, 0, 1)
Call glVertex3f(-1, 1, 1)
Call glVertex3f(-1, 1, -1)
Call glVertex3f(-1, -1, -1)
Call glVertex3f(-1, -1, 1)
' Face de droite = bleu
Call glColor3d(0, 0, 1)
Call glVertex3f(1, 1, -1)
Call glVertex3f(1, 1, 1)
Call glVertex3f(1, -1, 1)
Call glVertex3f(1, -1, -1)
glEnd
' Restaure la matrice
glPopMatrix
End Sub

Exécutez (F5) la fonction FonctionOpenGL (ou exécutez le projet sous VB6) pour afficher la fenêtre.

Utilisez :

- les touches fléchées pour faire tourner la scène ;

- la molette de la souris pour zoomer la scène.

Remplacez CurrentProject en fonction de l'application utilisée :

- CurrentProject pour Access ;

- ThisWorkBook pour Excel ;

- ThisDocument pour Word ;

- App pour VB6.

Pour VB6, remplacez également Function FonctionOpenGL par le point d'entrée Sub Main.

VI - Les Vertex Buffer Objects

Pour plus d'informations sur les Vertex Buffer Object (VBO), consultez ces articles :

- La géométrie en OpenGL : vers les VertexBufferObject ;

- Utilisation des VBO avec OpenGL 3.x.

Vous pouvez utiliser les VBO pour accélérer le rendu de scènes complexes.
Ceux-ci étant apparus avec la version 1.5, ils sont supportés par de nombreuses cartes graphiques (même sur portable ou ordinateur de bureautique).

Commençons tout de même par vérifier que la carte graphique supporte les VBO, en plaçant ce code avant l'appel à InitScene :

Teste le support des VBO

' Teste si les VBO sont supportés, soit par la version, soit l'extension ARB
If val(LongToString(glGetString(GL_VERSION))) < 1.5 And InStr(LongToString(glGetString(GL_EXTENSIONS)), "GL_ARB_vertex_buffer_object") = 0 Then
MsgBox "VBO non supportés", vbOKOnly
Exit Function
End If

Bien entendu, Exit Function est à remplacer par Exit Sub si le test est codé dans la procédure Main d'une application VB6.
Les VBO sont supportés à partir de la version 1.5, ou dans l'extension GL_ARB_vertex_buffer_object.
Si on n'est pas dans ce cas, on affiche un message et on quitte la fonction.

Importez maintenant le module ModOpenGL_1_5 qui contient les fonctions de la version 1.5.
N'oubliez pas de « mapper » ensuite les fonctions en exécutant la procédure RemapVBToGL de ce module.

Initialisation des fonctions

' Initialise les fonctions
ModOpenGL_1_5.RemapVBToGL

Nous voici prêts à programmer les VBO !

VI-A - Préparation des tableaux de données

Pour utiliser les VBO, il faut mettre les données du cube en tableaux.

Pour remplir facilement des tableaux, ajoutez la fonction BuildArray vue dans le premier tutoriel. :

Fonction pour remplir un tableau

' Fonction pour remplir un tableau
Public Function BuildArray(parray, pValues As Variant)
Dim lcpt As Long
Dim lIndice As Long
lIndice = LBound(parray)
For lcpt = LBound(pValues) To UBound(pValues)
parray(lIndice) = pValues(lcpt)
lIndice = lIndice + 1
Next
End Function

Les tableaux de données sont initialisés à la fin de la procédure InitScene :

Déclaration des tableaux

' Déclaration des tableaux
Dim CubeArray(1 To 144) As Single
Dim IndiceArray(1 To 36) As Long

CubeArray est un tableau contenant les coordonnées des vertex (les sommets) et leurs couleurs.
Il y a pour chaque sommet trois coordonnées et trois composantes de couleurs.
On définit quatre vertex par face, car chaque sommet est utilisé par plusieurs faces avec une couleur différente.
Donc le tableau CubeArray a une taille de (3 coordonnées + 3 couleurs) * 4 sommets * 6 faces = 144 vertex.

IndiceArray est un tableau d'indices qui va désigner chaque vertex du tableau CubeArray qu'il faut utiliser pour dessiner le cube.
Nous allons dessiner ce cube à base de triangles : le triangle étant l'élément de base géré par les cartes graphiques, il est le plus rapide à afficher.
Chaque face du cube est composée de deux triangles de trois sommets.
Donc le tableau IndiceArray a une taille de 6 faces * 2 triangles * 3 sommets = 36 indices.

Voici l'initialisation de ces deux tableaux :

Initialisation des tableaux

' Initialisation des tableaux
BuildArray CubeArray, Array( _
-1, -1, 1, 1, 1, 0, 1, -1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, -1, 1, 1, 1, 1, 0, _
1, 1, -1, 0, 1, 0, -1, 1, -1, 0, 1, 0, -1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, _
-1, -1, -1, 1, 0.5, 0, 1, -1, -1, 1, 0.5, 0, 1, -1, 1, 1, 0.5, 0, -1, -1, 1, 1, 0.5, 0, _
1, -1, -1, 1, 0, 0, -1, -1, -1, 1, 0, 0, -1, 1, -1, 1, 0, 0, 1, 1, -1, 1, 0, 0, _
-1, 1, 1, 1, 0, 1, -1, 1, -1, 1, 0, 1, -1, -1, -1, 1, 0, 1, -1, -1, 1, 1, 0, 1, _
1, 1, -1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, -1, 1, 0, 0, 1, 1, -1, -1, 0, 0, 1)

BuildArray IndiceArray, Array(0, 1, 2, 0, 2, 3, _
4, 5, 6, 4, 6, 7, _
8, 9, 10, 8, 10, 11, _
12, 13, 14, 12, 14, 15, _
16, 17, 18, 16, 18, 19, _
20, 21, 22, 20, 22, 23)

Petite explication de ces tableaux, avec en exemple les premières lignes qui correspondent à la face avant :

En haut de l'image le tableau CubeArray contient les coordonnées des sommets (en rouge) et leur couleur (en bleu).

Notez qu'on utilise un tableau entrelacé : c'est-à-dire qu'on alterne coordonnées et couleur.
La face avant est positionnée en z = 1, donc la troisième coordonnée est toujours 1.
La face est jaune, donc la couleur est composée de rouge et de vert => (1, 1, 0).
On définit ainsi quatre sommets, dont la couleur est jaune.

En bas à droite le tableau IndiceArray contient les indices des sommets qui composent les triangles.
Attention, le premier sommet ne commence pas à 1 mais à 0.

Le premier triangle est composé des points d'indice 0, 1 et 2 ; c'est le triangle en bas à droite.
Vous remarquez la flèche verte qui montre que les points sont définis dans le sens inverse au sens horaire.
La face avant du triangle est donc celle que l'on voit.
Il est important de faire attention à l'orientation des faces, notamment pour le culling (masquage des faces arrière par exemple pour accélérer le rendu).

Le deuxième triangle est composé des points d'indice 0, 2 et 3.
Nous utilisons donc plusieurs fois les mêmes vertex.

VI-B - Création des buffers

Chacun des tableaux va être envoyé dans un buffer.
Les identifiants de ces buffers sont stockés dans un tableau, que nous déclarons en en-tête de module :

Décalaration des buffers

' Buffers pour le cube
Dim CubeBuffers(1 To 2) As Long

Retournons ensuite à la fin de la procédure InitScene.
À la suite du code d'initialisation des tableaux, générez deux buffers :

Génération des buffers

' Crée les buffers pour le cube
glGenBuffers 2, CubeBuffers(1)

On demande la création de deux buffers, et on donne le premier buffer en paramètre :

- CubeBuffers(1) contient l'identifiant du premier buffer ;

- CubeBuffers(2) contient l'identifiant du deuxième buffer.

Ces buffers sont vides, il faut ensuite les remplir avec la fonction glBufferData.

Avant d'utiliser la fonction glBufferData, nous devons « activer » le buffer souhaité avec la fonction glBindBuffer.

Cela nous donne pour le premier buffer qui contient les données de vertex et de couleurs :

Données de vertex et couleurs

' Données de vertex / couleur
glBindBuffer GL_ARRAY_BUFFER, CubeBuffers(1)
glBufferData GL_ARRAY_BUFFER, UBound(CubeArray) * Len(CubeArray(1)), VarPtr(CubeArray(1)), GL_STATIC_DRAW
glBindBuffer GL_ARRAY_BUFFER, 0&

GL_ARRAY_BUFFER désigne un tableau de données : vertex, couleurs, normales, coordonnées de textures…

Le deuxième paramètre de la fonction glBufferData est la taille des données, en bytes.
Cette taille est calculée en multipliant le nombre de données dans le tableau par la taille d'un élément du tableau.

Le troisième paramètre de la fonction glBufferData est un pointeur vers le premier élément du tableau.
Ce pointeur est obtenu grâce à la fonction VarPtr.

Le dernier paramètre de la fonction glBufferData est le type d'utilisation des données du buffer.
GL_STATIC_DRAW désigne une utilisation du buffer en lecture seule.
Nous verrons ensuite une utilisation dynamique d'un buffer.

On procède de la même manière pour remplir le buffer d'indices, mais avec un type de tableau GL_ELEMENT_ARRAY_BUFFER (car c'est un tableau d'indices) :

Données d'indices

' Données d'indices
glBindBuffer GL_ELEMENT_ARRAY_BUFFER, CubeBuffers(2)
glBufferData GL_ELEMENT_ARRAY_BUFFER, UBound(IndiceArray) * Len(IndiceArray(1)), VarPtr(IndiceArray(1)), GL_STATIC_DRAW
glBindBuffer GL_ELEMENT_ARRAY_BUFFER, 0&

Une fois les buffers remplis, les tableaux IndiceArray et CubeArray ne sont plus utilisés.

Il ne faut pas oublier de supprimer les buffers lorsqu'ils ne sont plus utiles.

Supprime les buffers

' Supprime les buffers
glDeleteBuffers 2, CubeBuffers(1)

Pour notre application il n'est pas utile de supprimer les buffers.
Lorsqu'on ferme la fenêtre et que la fonction glutMainLoop nous rend la main, les buffers sont déjà supprimés.

Pour s'en assurer il suffit de tester après glutMainLoop si les identifiants dans le tableau CubeBuffers sont des buffers valides.

Teste les buffers

' Teste les buffers
MsgBox "Buffer 1 : " & glIsBuffer(CubeBuffers(1))
MsgBox "Buffer 2 : " & glIsBuffer(CubeBuffers(2))

Résultat

Buffer 1 : 0
Buffer 2 : 0

VI-C - Utilisation des buffers

L'utilisation des buffers est très similaire à l'utilisation des tableaux vue dans le premier tutoriel.

La différence est que les données ne sont plus stockées dans un tableau, mais dans un buffer.
Lorsqu'on appelle les fonctions, on ne donne donc plus en paramètre un tableau, mais une position dans le buffer actif.

Avant d'écrire le nouveau code de dessin du cube, supprimez les anciennes lignes situées dans la procédure Render (de glBegin à glEnd inclus).

Nous allons écrire à la place le code avec VBO.

Il faut d'abord activer les tableaux utilisés, pour notre cas on active les tableaux de vertex et de couleurs :

Activation des tableaux

' Début de dessin du cube
' Activation des tableaux
glEnableClientState GL_VERTEX_ARRAY
glEnableClientState GL_COLOR_ARRAY

Ensuite on positionne les pointeurs pour indiquer où sont les données de vertex et de couleurs.
Ces données étant dans le premier buffer, on l'active au préalable :

Position des données à utiliser

' Position des données à utiliser
glBindBuffer GL_ARRAY_BUFFER, CubeBuffers(1)
glVertexPointer 3, GL_FLOAT, (3 + 3) * 4, ByVal 0&
glColorPointer 3, GL_FLOAT, (3 + 3) * 4, ByVal 12&

Les fonctions glVertexPointer et glColorPointer méritent une explication.

1er paramètre = 3 : c'est le nombre d'informations par sommet : trois coordonnées ou trois composantes de couleur.

2e paramètre = GL_FLOAT : c'est le type des données contenues dans le buffer (GL_FLOAT = Single).

3e paramètre = (3 + 3) * 4 : c'est la taille de données entre deux sommets.
Il y a 3 coordonnées + 3 composantes de couleur, que l'on multiplie par la taille d'un élément du tableau.
Ce 3e paramètre, nommé stride est très important dans ce cas où le tableau est entrelacé.
Si on avait utilisé un tableau par type de données, ce paramètre serait égal à zéro.

4e paramètre = 0& ou 12& : c'est le décalage des informations, en bytes.
Les vertex commencent au début du tableau donc avec un décalage de 0, alors que les coordonnées commencent après les trois premiers éléments, donc un décalage de 3*4=12 (car chaque élément occupe quatre bytes).

Notez l'utilisation de ByVal pour le dernier paramètre afin de bien passer la valeur en paramètre.

À ce stade on a défini un buffer de données avec GL_ARRAY_BUFFER.
Ce buffer serait suffisant si on n'utilisait pas de buffer d'indices : on utiliserait alors la fonction glDrawArrays pour dessiner.

Mais ici nous utilisons un buffer d'indices, c'est le deuxième buffer que nous activons :

Utilisation des indices

' Utilisation des indices
glBindBuffer GL_ELEMENT_ARRAY_BUFFER, CubeBuffers(2)

Et enfin on peut exécuter la commande de dessin :

Dessine le cube

' Dessine le cube
glDrawElements GL_TRIANGLES, 36, GL_UNSIGNED_INT, ByVal 0&

Cette fonction demande l'affichage :

- de triangles (GL_TRIANGLES) ;

- au nombre de 12 (composés de 36 indices) ;

- à partir de données d'indices de type Long (GL_UNSIGNED_INT) ;

- et sans décalage dans le buffer d'indices (ByVal 0&).

Enfin, il faut désactiver l'utilisation des tableaux et buffers :

Désactive les tableaux et buffers

' Désactivation des tableaux
glDisableClientState GL_VERTEX_ARRAY
glDisableClientState GL_COLOR_ARRAY
' Désactive les buffers
glBindBuffer GL_ARRAY_BUFFER, 0&
glBindBuffer GL_ELEMENT_ARRAY_BUFFER, 0&

Voici le cube dessiné avec les VBO :

VI-D - Modification des buffers

Il est également possible de modifier dynamiquement le contenu des buffers.

Pour cela on récupèrera la position des données en mémoire avec la fonction glMapBuffer.

Avant toute chose, il faut définir le type de buffer afin qu'il soit modifiable.

GL_STATIC_DRAW est utilisé pour un buffer statique, donc jamais modifié.
GL_STREAM_DRAW est utilisé pour un buffer que l'on modifie une fois par affichage.
GL_DYNAMIC_DRAW est utilisé pour un buffer dynamique que l'on modifie souvent, plusieurs fois par affichage.

Remplacez GL_STATIC_DRAW par GL_STREAM_DRAW pour le tableau CubeArray :

Données de vertex et de couleurs modifiables

glBufferData GL_ARRAY_BUFFER, UBound(CubeArray) * Len(CubeArray(1)), VarPtr(CubeArray(1)), GL_STREAM_DRAW

Juste avant l'utilisation du buffer d'indices, on va boucler sur les données pour modifier dynamiquement leur couleur.

Il nous faut donc un pointeur (lPtr), un compteur (lCpt) et un tableau de composantes de couleur (lColor) :

Déclaration

#If VBA7 Then
Dim lPtr As LongPtr
Dim lCpt As LongPtr
#Else
Dim lPtr As Long
Dim lCpt As Long
#End If
Dim lColor(1 To 3) As Single

Les variables lPtr et lCpt sont des pointeurs et doivent donc être déclarés en fonction de l’environnement d’exécution (LongPtr ou Long).

On lit ensuite la position des données du buffer :

Position des données

lPtr = glMapBuffer(GL_ARRAY_BUFFER, GL_READ_WRITE)

Puis on boucle sur les données, on les lit et on les modifie :

Boucle de traitement des données

' Boucle sur les données
For lCpt = 1 To 24
' Lecture des données de couleur
RtlMoveMemory lColor(1), ByVal lPtr + (lCpt - 1) * (3 + 3) * 4 + 3 * 4, 3 * 4
' Atténuation de la couleur
lColor(1) = lColor(1) - 0.005
lColor(2) = lColor(2) - 0.005
lColor(3) = lColor(3) - 0.005
' Écriture des nouvelles données de couleur
RtlMoveMemory ByVal lPtr + (lCpt - 1) * (3 + 3) * 4 + 3 * 4, lColor(1), 3 * 4
Next

On boucle de 1 à 24, car on a 24 sommets.
Le tableau lColor reçoit les composantes de couleurs.
Pour lire le buffer, on utilise la fonction RtlMoveMemory (déclarée dans le module ModOpenGLTools).
Cette fonction permet de copier des zones de la mémoire.
Notez l'utilisation du ByVal lorsqu'on utilise un pointeur.

Enfin on libère le buffer avec glUnmapBuffer :

Libère le buffer

' Libère le buffer
glUnmapBuffer GL_ARRAY_BUFFER

L'exécution de ce projet affiche un cube dont les couleurs sont progressivement atténuées :

Il est bien sûr possible de modifier également les coordonnées des sommets.
Il suffit de modifier les données de vertex (décalage 0 au lieu de 3*4) à la place des données de couleurs.

On obtient ainsi des objets dont la géométrie peut varier, tout en conservant une grande rapidité d'exécution.

VII - Les Textures 3D

On se représente assez naturellement une texture 2D par un rectangle.
Une texture 3D présente une dimension supplémentaire : on peut donc se la représenter par un cube (ou un pavé).

Ces textures sont d'ailleurs également appelées Volume Texture.
De ce fait, ces textures occupent un espace mémoire relativement important.

Par exemple, une texture 3D cubique de 64 pixels de côté sans canal alpha occupe en mémoire : 64 * 64 * 64 * 3 = 786 432 octets = 768 Ko.

VII-A - Utilité des textures 3D

Les textures 3D sont très intéressantes lorsqu'on souhaite texturer un objet sans discontinuité entre les différentes faces.
Par exemple on peut texturer notre cube pour faire croire qu'il est en bois :

Regardez bien aux arêtes du cube : il n'y a pas de discontinuité, comme si le cube était taillé dans la masse.

VII-B - Préparation de l'environnement de développement VB

Préparez votre application pour afficher un cube, tel qu'il est expliqué ci-dessus dans la section V : Préparation de l'environnement de développement VB.

Les textures 3D sont apparues avec la version 1.2, elles sont supportées par de nombreuses cartes graphiques (même sur portable ou ordinateur de bureautique).

Commençons tout de même par vérifier que la carte graphique les supporte, en plaçant ce code avant l'appel à InitScene :

Teste le support des textures 3D

' Teste si les textures 3D sont supportées, soit par la version, soit l'extension EXT
If val(LongToString(glGetString(GL_VERSION))) < 1.2 And InStr(LongToString(glGetString(GL_EXTENSIONS)), "GL_EXT_texture3D") = 0 Then
MsgBox "Textures 3D non supportées", vbOKOnly
Exit Function
End If

Bien entendu, Exit Function est à remplacer par Exit Sub si le test est codé dans la procédure Main d'une application VB6.
Les textures 3D sont supportées à partir de la version 1.2, ou dans l'extension GL_EXT_texture3D.
Si on n'est pas dans ce cas, on affiche un message et on quitte la fonction.

Importez maintenant le module ModOpenGL_1_2 qui contient les fonctions de la version 1.2.
N'oubliez pas de « mapper » ensuite les fonctions en exécutant la procédure RemapVBToGL de ce module.

Initialisation des fonctions

' Initialise les fonctions
ModOpenGL_1_2.RemapVBToGL

Nous voici prêts à programmer les textures 3D !

VII-C - Génération de la texture

Déclarons tout d'abord un identifiant pour la texture, en début de module :

Déclaration d'un identifiant

' Identifiant de la texture 3D
Private gTexture As Long

Ensuite à la fin de la fonction InitScene, on génère la texture :

Génération de la texture

' Active les textures 3D
glEnable GL_TEXTURE_3D
' Génère un identifiant de texture
glGenTextures 1, gTexture
' Active cette texture
glBindTexture GL_TEXTURE_3D, gTexture
' Filtrage de la texture (pas de mipmapping => linéaire)
glTexParameteri GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR
glTexParameteri GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR
' Génère les données de la texture de bois
Dim lRayon As Single
Dim lCpt As Long
Dim x As Long, y As Long, z As Long
' Tableau de données de la texture
Dim ldata(1 To 64& * 64& * 64& * 3&) As Byte
lCpt = 1
For z = 1 To 64&
For y = 1 To 64&
For x = 1 To 64&
' Rayon du point courant
lRayon = Sqr(Abs((x - 32&) * (x - 32&) + (y - 32&) * (y - 32&)))
' Ajoute un petit décalage en fonction de la profondeur
lRayon = lRayon + z / 64
' Si lRayon mod 4 > 1 => couleur claire, sinon couleur foncée
If (lRayon Mod 4 > 1) Then
' Couleur brun clair
ldata(lCpt) = 130  'red
ldata(lCpt + 1) = 60  ' green
ldata(lCpt + 2) = 10  ' blue
Else
' Couleur brun foncé
ldata(lCpt) = 100  'red
ldata(lCpt + 1) = 45  ' green
ldata(lCpt + 2) = 25  ' blue
End If
' Avance de 3 bytes
lCpt = lCpt + 3
Next
Next
Next
' Génère la texture
glTexImage3D GL_TEXTURE_3D, 0, GL_RGB, 64&, 64&, 64&, 0, GL_RGB, GL_UNSIGNED_BYTE, VarPtr(ldata(1))

Le début du code est identique aux textures 2D :

- on active les textures avec la fonction glEnable (on active bien sûr les textures 3D) ;

- on génère un identifiant de texture avec la fonction glGenTextures ;

- on active cette texture avec la fonction glBindTexture ;

- et on définit le filtrage de texture avec la fonction glTexParameteri ; ici on applique un filtrage linéaire.

Ensuite on calcule la couleur de chaque point de la texture.
Chaque point d'une texture 2D était appelé un texel.
Pour une texture 3D, on parle de voxel.

Chaque couleur de voxel est calculée en fonction de sa position x, y et z.
On n'utilise que trois composantes de couleur car la transparence n'est pas utile.

On génère, grâce à un test sur le rayon du point, des cercles marron concentriques pour simuler les nervures du bois.

Pour que la texture ne soit pas trop parfaite, on ajoute un petit décalage en fonction de la profondeur z.

Finalement la texture est générée grâce à la fonction glTexImage3D :

- GL_TEXTURE_3D désigne bien évidemment une texture 3D ;

- GL_RGB désigne le format des données = trois composantes : rouge, vert et bleu ;

- 64&, 64&, 64& est la taille de la texture = un cube de 64 points de côté ;

- GL_UNSIGNED_BYTE désigne le type des données = Byte ;

- Le dernier paramètre désigne l'emplacement des données à utiliser : c'est un pointeur (VarPtr) vers le premier élément du tableau.

VII-D - Application de la texture

Pour appliquer la texture au cube, il faut procéder comme pour les textures 2D.
Voici le nouveau code pour dessiner le cube texturé :

Dessin du cube texturé

' Affecte la texture
glBindTexture GL_TEXTURE_3D, gTexture
' Début de la primitive pour le cube
glBegin GL_QUADS
' Face du haut
Call glTexCoord3f(1, 1, 0):    Call glVertex3f(1, 1, -1)
Call glTexCoord3f(0, 1, 0):    Call glVertex3f(-1, 1, -1)
Call glTexCoord3f(0, 1, 1):    Call glVertex3f(-1, 1, 1)
Call glTexCoord3f(1, 1, 1):    Call glVertex3f(1, 1, 1)
' Face du bas
Call glTexCoord3f(0, 0, 0):    Call glVertex3f(-1, -1, -1)
Call glTexCoord3f(1, 0, 0):    Call glVertex3f(1, -1, -1)
Call glTexCoord3f(1, 0, 1):    Call glVertex3f(1, -1, 1)
Call glTexCoord3f(0, 0, 1):    Call glVertex3f(-1, -1, 1)
' Face de derrière
Call glTexCoord3f(1, 0, 0):    Call glVertex3f(1, -1, -1)
Call glTexCoord3f(0, 0, 0):    Call glVertex3f(-1, -1, -1)
Call glTexCoord3f(0, 1, 0):    Call glVertex3f(-1, 1, -1)
Call glTexCoord3f(1, 1, 0):    Call glVertex3f(1, 1, -1)
' Face de devant
Call glTexCoord3f(0, 0, 1):    Call glVertex3f(-1, -1, 1)
Call glTexCoord3f(1, 0, 1):    Call glVertex3f(1, -1, 1)
Call glTexCoord3f(1, 1, 1):    Call glVertex3f(1, 1, 1)
Call glTexCoord3f(0, 1, 1):    Call glVertex3f(-1, 1, 1)
' Face de gauche
Call glTexCoord3f(0, 1, 1):    Call glVertex3f(-1, 1, 1)
Call glTexCoord3f(0, 1, 0):    Call glVertex3f(-1, 1, -1)
Call glTexCoord3f(0, 0, 0):    Call glVertex3f(-1, -1, -1)
Call glTexCoord3f(0, 0, 1):    Call glVertex3f(-1, -1, 1)
' Face de droite
Call glTexCoord3f(1, 1, 0):    Call glVertex3f(1, 1, -1)
Call glTexCoord3f(1, 1, 1):    Call glVertex3f(1, 1, 1)
Call glTexCoord3f(1, 0, 1):    Call glVertex3f(1, -1, 1)
Call glTexCoord3f(1, 0, 0):    Call glVertex3f(1, -1, -1)
glEnd
' Désactive la texture
glBindTexture GL_TEXTURE_3D, 0

1. On active d'abord les textures 3D.

2. On active la texture à utiliser (glBindTexture).

3. On dessine le cube en donnant des coordonnées de texture.

La principale différence par rapport à une texture 2D est que la fonction glTexCoord3f demande trois paramètres.

Chaque vertex du cube est associé à un voxel de la texture.

Ici l'exemple est simple : on associe chaque sommet du cube à un sommet de la texture 3D.

Et enfin on désactive la texture une fois le dessin terminé.

On obtient alors le résultat attendu :

VIII - Les Shaders

Les Shaders sont de petits programmes exécutés par la carte graphique.
Ils sont très efficaces et permettent de programmer de nombreux effets très rapides.
Ces programmes sont écrits en langage GLSL, abréviation de OpenGL Shading Language.
Ce langage est proche du C, vous en trouverez les spécifications sur le site d'OpenGL : OpenGL Shading Language.

Nous n'allons pas détailler ici le langage GLSL : nous allons « juste » apprendre à intégrer ces programmes dans notre application en VB.

VIII-A - Préparation de l'environnement de développement VB

Préparez votre application pour afficher un cube, tel qu'il est expliqué ci-dessus dans la section V : Préparation de l'environnement de développement VB.

Les Shaders sont apparus avec la version 2.0, ils sont supportés par les cartes graphiques récentes, mais pas par certaines cartes graphiques des PC bureautiques.

Commençons par vérifier que la carte graphique les supporte, en plaçant ce code avant l'appel à InitScene :

Teste le support des textures 3D

' Teste si les Shaders sont supportés
If val(LongToString(glGetString(GL_VERSION))) < 2 Then
MsgBox "Shaders non supportés", vbOKOnly
Exit Function
End If

Bien entendu, Exit Function est à remplacer par Exit Sub si le test est codé dans la procédure Main d'une application VB6.
Si les Shaders ne sont pas supportés, on affiche un message et on quitte la fonction.

Importez maintenant le module ModOpenGL_2_0 qui contient les fonctions de la version 2.0.
N'oubliez pas de « mapper » ensuite les fonctions en exécutant la procédure RemapVBToGL de ce module.

Initialisation des fonctions

' Initialise les fonctions
ModOpenGL_2_0.RemapVBToGL

Nous voici prêts à programmer les Shaders !

VIII-B - Shader et Program

Il y a deux notions à connaître :

- les Shaders ;

- et les Programs.

Le principe est assez simple :

1. On crée un Shader.

2. On définit son code source en GLSL.

3. On compile le Shader

4. On crée un Program.

5. On attache le Shader au Program.

6. On génère le Program exécutable (étape de linkage).

7. On peut utiliser le Program.

On pourra activer ou désactiver un Program au cours du rendu.

La complication en VB est la gestion des chaînes de caractères.
OpenGL utilise des chaînes de caractères multioctets alors que VB utilise des chaînes Unicode.
Nous utiliserons donc la fonction StrConv pour convertir les chaînes de caractères.
De plus les paramètres sont des pointeurs, que nous obtenons avec la fonction StrPtr ou VarPtr selon les cas.

VIII-C - Les types de Shaders

Il y a trois types de Shaders :

- les Shaders de sommets (Vertex Shaders) qui agissent sur la position des Vertex ;

- les Shaders de pixels (Fragement Shaders) qui agissent sur la couleur des pixels de l'image ;

- les Shaders de géométrie (Geometry Shaders) apparus avec le Shader Model 4.0 d'OpenGL 3.

VIII-D - Mon premier Shader

Pour débuter, nous allons créer et utiliser un Shader très simple.
Celui-ci modifie la couleur de chaque pixel afin qu'il soit rouge.
Ce programme n'est pas très utile, mais a le mérite d'être simple pour un premier exemple.

Voici le code GLSL de ce Shader :

Code du Shader

void main(void)
{
gl_FragColor = vec4 ( 1.0 , 0.0 , 0.0 , 1.0 ) ;
}

Il est en effet très simple :

- pour chaque pixel dessiné, la procédure main est appelée ;

- gl_FragColor renvoie une couleur fixe (couleur rouge).

Voyons maintenant comment intégrer ce programme dans notre code VB.

VIII-E - Génération du programme exécutable par la carte graphique

Dans la procédure InitScene, générons un Shader.

Génération du Shader

'Génération du Shader
Dim lShader As Long
lShader = glCreateShader(GL_FRAGMENT_SHADER)

GL_FRAGMENT_SHADER désigne un Shader de pixel, car nous souhaitons agir sur la couleur des pixels.

Ensuite il faut définir le code source du Shader grâce à la fonction glShaderSource.
Nous écrivons ce code dans une variable lShaderString de type String.
Il est également possible d'écrire le code source dans un fichier texte et de le charger en VB.
On peut ainsi écrire des Shaders facilement réutilisables.

Code source du Shader

' Code source du Shader
Dim lShaderString As String
lShaderString = "void main(void)" & _
"{" & _
"gl_FragColor = vec4 ( 1.0 , 0.0 , 0.0 , 1.0 ) ;" & _
"}"

Ce code source doit être converti en multioctets :

Code source du Shader

' Conversion
lShaderString = StrConv(lShaderString, vbFromUnicode)

Puis on injecte le code source dans le Shader :

Injecte le code source dans le Shader

' Injecte le code source dans le Shader
glShaderSource lShader, 1, VarPtr(lShaderString), LenB(lShaderString)

On passe en paramètre le pointeur vers la chaîne de caractères : on obtient ce pointeur grâce à la fonction VarPtr.
Et en dernier paramètre, on précise la taille de la chaîne de caractères : il est important d'utiliser la fonction LenB.

Notez le deuxième paramètre égal à 1.
Ce paramètre précise le nombre de chaînes de caractères qui sont passées dans le troisième paramètre.
Ici on n'a qu'une seule chaîne de caractères.
Pour préciser plusieurs chaînes, il faut les stocker dans un tableau.
On donne alors :

- en deuxième paramètre le nombre de lignes dans ce tableau ;

- en troisième paramètre un pointeur vers le premier élément de ce tableau ;

- et en quatrième paramètre un pointeur vers le premier élément d'un tableau contenant les tailles de chaque chaîne de caractères.

Il faut maintenant compiler le Shader :

Compile le Shader

' Compile le Shader
glCompileShader lShader

Cette compilation est effectuée par la carte graphique.
On peut ensuite lire le statut de la compilation :

Statut de compilation

' Lecture du statut de compilation
Dim lStatus As Long
glGetShaderiv lShader, GL_COMPILE_STATUS, lStatus

Si lStatus contient GL_TRUE, alors la compilation s'est correctement déroulée.
Dans le cas contraire il est possible de lire le journal de compilation :

Journal de compilation

If lStatus <> GL_TRUE Then
' Taille du journal
glGetShaderiv lShader, GL_INFO_LOG_LENGTH, lSize
' Lecture du journal
lLog = Space(lSize)
glGetShaderInfoLog lShader, lSize, lSize, StrPtr(lLog)
' Écriture du journal dans la fenêtre de débogage
Debug.Print StrConv(lLog, vbUnicode)
End If

La lecture de ce journal se fait en deux temps :

1. Il faut d'abord lire la taille du journal ;

2. Ensuite on lit le journal en spécifiant sa taille.

La chaîne de caractères qui reçoit le journal doit être d'une taille suffisante, on initialise donc la variable lLog avec autant d'espaces que la taille du journal.
Puis la fonction glGetShaderInfoLog demande un pointeur vers cette variable ; on utilise ici encore la fonction StrPtr.

Finalement, on écrit le journal en prenant soin de convertir la variable au format Unicode.

Pour tester ce journal de compilation, modifiez le code source de manière à générer une erreur.

Par contre si la compilation s'est correctement déroulée, on continue le traitement en générant le programme.

On utilise un identifant de programme déclaré en en-tête de module :

Identifiant du programme

' Identifiant du programme
Private gProgram As Long

Puis on génère le programme à la suite du code précédent :

Génère le Program

Else
' Génère un identifiant de programme
gProgram = glCreateProgram
' Attache le shader au programme
glAttachShader gProgram, lShader
' Génère le programme exécutable
glLinkProgram gProgram
' Statut du liage
glGetProgramiv gProgram, GL_LINK_STATUS, lStatus
If lStatus <> GL_TRUE Then
' Taille du journal
glGetProgramiv lShader, GL_INFO_LOG_LENGTH, lSize
' Lecture du journal
lLog = Space(lSize)
glGetProgramInfoLog lShader, lSize, lSize, StrPtr(lLog)
' Écriture du journal dans la fenêtre de débogage
Debug.Print StrConv(lLog, vbUnicode)
End If
' Supprime le shader
glDeleteShader lShader
End If

glLinkProgram peut également provoquer une erreur : on récupère alors le journal de la même manière que pour la compilation du Shader.

C'est seulement lorsque cette étape de lien s'est correctement déroulée que le Program est utilisable.
On peut alors supprimer le Shader qui n'est plus utile (glDeleteShader).

VIII-F - Utilisation du programme

Il est maintenant très simple d'utiliser le programme.
Il suffit d'appeler la fonction glUseProgram afin de définir l'utilisation du programme.

Avant le dessin du cube, activez le programme :

Active le programme

' Active le programme
glUseProgram gProgram

Et après le dessin du cube, désactivez-le :

Désactive le programme

' Désactive le programme
glUseProgram 0&

Exécutez l'application : le cube est coloré en rouge.

Cet effet n'est pas spectaculaire, mais en utilisant des shaders plus complexes on peut mettre en œuvre des effets comme ceux qu'on voit souvent dans les jeux vidéo.

IX - Conclusion

Avec les extensions OpenGL on peut désormais utiliser des fonctionnalités avancées et obtenir de beaux effets, même en VB.

Pour en apprendre plus, consultez la page des tutoriels OpenGL.

Merci aux équipes Office, Jeux et VB6 pour leur aide.

Merci à jacques_jean et Claude Leloup pour leur relecture orthographique.

X - Téléchargements

Téléchargez le pack de modules VB pour OpenGL :
Pack de module VBA pour OpenGL

Téléchargez la bibliothèque dll FreeGlut :
Librairie FreeGlut

Téléchargez les démos de ce tutoriel :

1 - Les Vertex Buffer Objects
Access

2 - Les Textures 3D
Access

3 - Les Shaders
Access