Journal Programmation 3D à travers les âges : OpenGL 1.1 (1997-2003)

Posté par  (site web personnel) . Licence CC By‑SA.
22
16
sept.
2025

Sommaire

Salut 'nal,

Si tu suivi mon journal précédent, tu as maintenant les éléments de contexte pour programmer en 3D !

On va passer à la pratique avec un exemple OpenGL 1.1 (cf. spécification), qui correspond au code typique des années fin 90-début 2000. Mais qui, magie de la rétrocompatibilité, tourne encore très bien aujourd'hui.

logo OpenGL

Installer une "glu" de fenêtrage : SDL3

OpenGL s'occupe très peu du système de fenêtrage. Eh oui, ce n'est qu'une API de dessin : gérer les événements clavier-souris-fenêtre de l'OS est en dehors de son périmètre.

C'est ainsi que chaque système fournit sa bibliothèque maison pour ça :
- sous X11 : Mesa fournit l'API GLX directement dans la bibliothèque principale libGL. Cette fusion entre les deux est d'ailleurs une erreur de conception, qui sera évitée dans la future bibliothèque OpenGL ES pour l'embarqué ;
- sous Wayland (et récemment X11) : le Khronos Group fournit l'API "universelle" EGL ;
[…]
- les autres OS : ne supportent en réalité pas nativement EGL, on y utilise p.ex. ça ou ça ;
- et en général avec Vulkan : universel mais rien à voir 😛.

Vous l'avez compris, ce fatras nous éloigne du sujet… on va donc faire comme des milliers de codeurs avant nous : utiliser une bibliothèque de "glu" multi-plateforme ! (Free)GLUT et GLFW sont encore très utilisées…

Mais nous préférerons SDL 3 car elle supporte toutes les combinaisons ET nous aidera plus tard pour Vulkan.

logo SDL

Elle n'est juste pas encore empaquetée dans les distributions les plus courantes, alors installons-la rapidement :

Prérequis

Debian/Ubuntu :

sudo apt install build-essential git make \
pkg-config cmake ninja-build gnome-desktop-testing libasound2-dev libpulse-dev \
libaudio-dev libjack-dev libsndio-dev libx11-dev libxext-dev \
libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev libxss-dev libxtst-dev \
libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev libgles2-mesa-dev \
libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev

Fedora/RHEL :

sudo dnf install gcc git-core make cmake \
alsa-lib-devel pulseaudio-libs-devel pipewire-devel \
libX11-devel libXext-devel libXrandr-devel libXcursor-devel libXfixes-devel \
libXi-devel libXScrnSaver-devel dbus-devel ibus-devel \
systemd-devel mesa-libGL-devel libxkbcommon-devel mesa-libGLES-devel \
mesa-libEGL-devel vulkan-devel wayland-devel wayland-protocols-devel \
libdrm-devel mesa-libgbm-devel libusb1-devel libdecor-devel \
pipewire-jack-audio-connection-kit-devel

Compiler

git clone https://github.com/libsdl-org/SDL --branch release-3.2.20
cd SDL
cmake -S . -B build
cmake --build build -j `nproc`

Installer

cmake --install build --prefix $HOME/sdl3220
# configurer
sed -i 's!/usr/local!'"$HOME"'/sdl3220!g' $HOME/sdl3220/lib/pkgconfig/sdl3.pc
echo 'export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$HOME/sdl3220/lib"' >> $HOME/.profile
echo 'export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$HOME/sdl3220/lib/pkgconfig"' >> $HOME/.profile
# (pour éviter un redémarrage)
source $HOME/.profile

Tester

pkg-config --modversion sdl3
# (devrait vous renvoyer :)
3.2.20

La base de tout : la croix

Nous allons afficher une croix plate interpolée en 2D, composée de 4 points reliés : rouge, vert, bleu, blanc.

croix

et utilisant ce code source C.

Pour le récupérer et me suivre pendant que je détaille, faites sur votre Linux :

git clone https://github.com/Tarnyko/suave_code_samples
cd suave_code_samples/tree/master/C/opengl-vulkan/
editor sdl3-gl1.c

Points & Coordonnées

Le repère orthonormé par défaut d'OpenGL s'étend de -1.0 à +1.0 sur tous les axes. Cela se change, mais nous conviendra très bien pour commencer 😉.
Si on place les 4 points plutôt vers le bord :

Repère avec points

on choisit 0.8,-0.8 comme coordonnées symétriques :

Repère avec points marqués

ça se matérialise facilement en code :

#define LINES 2

static const GLfloat vertex_arr[LINES * 4] = {   //  2 * 4 = 8 valeurs flottantes
    -0.8f,  0.8f,    0.8f, -0.8f,                // point ROUGE, point VERT
    -0.8f, -0.8f,    0.8f,  0.8f,                // point BLEU, point BLANC
};

(chacune des 2 lignes étant constituée de 2 points eux-mêmes définis par 2 coordonnées [X,Y], cela nous donne 2 x 2 x 2 = 8 coordonnées)

Couleurs

Pour les couleurs, bien qu'on puisse utiliser des flottants également, j'ai préféré la définition RGBA32/RGBA8888 beaucoup plus usitée (y compris chez les artistes).

Si un canal couleur est défini par un entier 8-bits, c'est-à-dire "2 exposant 8", c'est-à-dire une fourchette de 0 à 255…
…et qu'une couleur complète est définie par 4 entiers "ROUGE-VERT-BLEU-OPACITÉ" dans cet ordre précis…
… cela donne le code :

static const GLubyte color_arr[LINES * 8] = {    // 2 * 8 = 16 valeurs entières
    255, 0,   0, 255,      0, 255,   0, 255,     // ROUGE, VERT
      0, 0, 255, 255,    255, 255, 255, 255,     // BLEU, BLANC (= tout au max)
};

Index

Nous venons donc de définir 2 tableaux ci-dessus :
- vertex_arr : "points", alias vertex
- color_arr : couleurs

Il nous à les associer avec un tableau d'index associatifs nommé index_arr :

static const GLuint index_arr[LINES * 2] = {     // 2 * 2 = 4 points
    0, 1,                                        // point 1, point 2
    2, 3,                                        // point 3, point 4
};

Code

Voici la partie réellement technique de l'API !

On crée d'abord une fenêtre de taille 800x600 à l'aide de SDL :

window = SDL_CreateWindow([...], _width, _height, SDL_WINDOW_OPENGL);    // _width = 800 ; _height = 600

puis à chaque itération se produisant aussi que possible (on n'a pas encore de limiteur de FPS, ce qui n'est pas si grave sans mouvement), on redessine sur l'intégralité de la fenêtre via ce bloc :

glViewport(0, 0, _width, _height);                                       // contexte (au départ : 800x600)
 [...]
 case SDL_EVENT_WINDOW_RESIZED: { _width  = e.window.data1;              // mis à jour si redimensionnement !
                                  _height = e.window.data2; }

_width et _height sont des variables globales qu'on a déjà utilisées pour créer la fenêtre ; on les refournit ici à glViewport() pour dessiner sur exactement la même taille, c'est-à-dire l'intégralité du fond de la fenêtre
(ce n'est pas obligatoire : un contexte OpenGL peut cohabiter avec d'autres éléments sur la même fenêtre, typiquement des widgets comme ici par exemple).

Elles sont globales ici, car on les autorise à être modifiées par un événement externe : SDL_EVENT_WINDOW_RESIZED déclenché par la souris de l'utilisateur, qui mettra à jour _width et _height avant la prochaine itération.
De cette manière, notre croix sera redimensionnée en même temps que la fenêtre (sans SDL, on devrait écrire un code beaucoup plus long et spécifique pour gérer cette partie "système". Vous voyez l'intérêt de la "glu" ?)

Finalement le dessin commence ! D'abord on dessine un fond noir sans rien :

glClearColor(0, 0, 0, 255);    // NOIR
glClear(GL_COLOR_BUFFER_BIT);  // ràz avec la couleur de glClearColor()

puis on fournit nos 2 tableaux vertex_arr & color_arr à OpenGL côté CPU/RAM, en indiquant comment les interpréter :

glEnableClientState(GL_VERTEX_ARRAY);                      // active fonction : "vertices CPU/RAM"
glVertexPointer(2, GL_FLOAT, 0, vertex_arr);               // POINTS : groupes de 2 flottants (= [x,y])

glEnableClientState(GL_COLOR_ARRAY);                       // active fonction : "couleurs CPU/RAM"
glColorPointer(4, GL_UNSIGNED_BYTE, 0, color_arr);         // COULEURS : groupes de 4 entiers 0-255 (= RGBA32)

Finalement, on fournit le dernier tableau index_arr pour lancer le dessin des 2 lignes :

glDrawElements(GL_LINES, 4, GL_UNSIGNED_INT, index_arr);

et on rafraîchit simultanément le contexte et la fenêtre :

SDL_GL_SwapWindow(window);

dessin final

Compiler & exécuter

C'est le moment de tester !

gcc -std=c11 sdl3-gl1.c `pkg-config --cflags --libs sdl3` -lGL
./a.out
a.out

Remarques

Ce code fonctionne partout ; même sur Windows 95 ou RedHat 5/Mandrake 6 (1998) en mode logiciel 😉.

Il est par contre extrêmement rétro. Et pas juste dans sa forme, mais sa fonctionnalité :

  • plutôt que stocker les informations de vertices et couleurs sur le GPU (qui sait le faire depuis environ 30 ans avec les VBOs), il les stocke uniquement en RAM centrale et les transfère à chaque itération au GPU ;
  • il effectue un dessin côté CPU en mode immédiat plutôt que d'utiliser un shader stocké sur le GPU ;

Ces innovations apparaîtront respectivement dans OpenGL 1.5 (2003) et 2.0 (2004).

Concrètement en les utilisant pas, nous empêchons les GPU récents d'optimiser/paralléliser en retenant beaucoup d'état et de données dans notre programme. La programmation 3D récente utilise beaucoup plus d'appels et d'extensions dédiés au GPU lui-même.


Voilà ! Pour élargir ce tutoriel, seriez-vous plutôt intéressés par :
1. faire bouger la croix (ce qui nécessitera de créer un limiteur de FPS) ;
2. afficher 3 triangles formant une pyramide en 3D (ce qui nécessitera d'ajouter le repère [Z]) ?
… ou directement de passer à du code plus moderne avec shader et VBO 😉?

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.