Ce cours a pour ambition de vous faire comprendre comment une image de jeu-vidéo est produite par le processeur et la carte graphique d’un ordinateur.
Seront présentées les différentes étapes qui permettent de transformer une scène virtuelle en une image concrète, ainsi qu’un certain nombre de techniques et de caractéristiques liées au rendu graphique.
À la suite de ce cours, des termes hermétiques présents dans les menus de jeux et moteurs 3D tel que anisotropic filtering, mipmap, depth buffer ou fragment shader vous sembleront plus familiers.
Vous comprendrez également pourquoi on constate parfois des bugs de transparence ou des chutes de framerate dans certaines conditions.
Ce cours a été conçu de façon à rester relativement accessible, ainsi, il ne présente pas de lignes de code.
Il a également été pensé pour être le plus complet possible, néanmoins, il n’est pas totalement exhaustif, dû à mes propres limitation en tant que non-spécialiste, tout particulièrement sur les parties les plus techniques, le rendu temps réel étant en constante évolution.
Le viewport correspond à la zone d’affichage finale.
Il est défini par une résolution et une profondeur de couleurs (bits/couche).
La résolution est conditionnée par la taille de l’écran, de la fenêtre ou du fichier à générer.
Voici les résolutions les plus courantes utilisées dans le jeu-vidéo :
Les développeurs de jeux-vidéo doivent s’adapter aux plates-formes ciblées.
Même si depuis un bon moment les moteurs permettent de redimensionner aisément les graphismes à n’importe quelle résolution, connaître les usages courants des plateformes permet d’optimiser au mieux les ressources afin d’obtenir une bonne qualité d’image sans pour autant surcharger la mémoire et les unités de calcul.
De nombreux jeux choisissent délibérément une direction artistique basée sur le pixel art, se calquant souvent sur les vieilles consoles.
Connaître leur spécificité (résolution, nombre de couleurs permises) permet d’obtenir des rendus plus cohérents et adaptés.
Le viewport peut alors être réduit à la taille cible, les graphismes générés dans cet espace virtuel de petite dimension puis ragrandit en fin de cycle pour s’adapter à nos écrans en haute définition.
Le terme pixel perfect correspond à une image qui sera grossie par un multiplicateur entier.
Par exemple :
Voici un autre exemple (cliquez pour agrandir) :
Dans une image matricielle, la couleur de chaque pixel est définie par une taille mémoire.
Les anciens systèmes fonctionnaient avec un nombre limité de couleurs. Chaque pixel, était donc codé sur seulement quelques bits qui n’indiquaient pas directement les composants rouge, vert et bleu du pixel de l’écran, mais un identifiant en lien avec une palette prédéfinie.
Le Dithering, ou diffusion d’erreur, est une méthode qui consiste à assembler plusieurs pixels sous la forme de tramages.
Ce procédé permet de simuler de nouvelles teintes à partir de couleurs limitées.
Cette méthode était fréquemment utilisée sur les anciens systèmes malgré leurs résolutions limitées (le manque de couleurs posant plus de problèmes que la basse résolution des écrans).
De nombreux algorithmes existent, permettant de répartir les pixels sous la forme de différents motifs.
Cette technique est encore parfois utilisée pour certaines teintes mal gérées par les écrans SDR.
Le SDR (Standard Dynamic Range) est le format le plus répandu, utilisé par la quasi-totalité des écrans et des cartes graphiques.
Il est défini par des pixels comportant 3 canaux : rouge, vert et bleu, chacun codés en 8 bits.
Chaque pixel pèse ainsi 3 octets (24 bits).
Les moniteurs HDR (High Dynamic Range) dont la particularité est de proposer une dynamique de luminosité plus importante que les écrans classiques nécessitent une profondeur de couleur supérieure à la normale.
En effet, afin de conserver les détails présents dans les zones les plus sombres et plus claires de l’image, 256 nuances par couche ne suffisent plus.
Aujourd’hui, plusieurs normes existent, codées en 10 ou 12 bits par couche.
Le HDR10, ses évolutions (HDR10+ et HDR10+ Adaptative) ainsi que le HLG, proposent des couleurs en 10 bits (1024 nuances) par couche (soit un total de 30 bits par pixel).
La norme Dolby Vision et son évolution Dolby Vision IQ proposent quant à elles des couleurs en 12 bits (4096 nuances) par couche (soit un total de 36 bits par pixel).
Pour fonctionner correctement et donner l’illusion du mouvement, une image animée doit être mis à jour un nombre suffisant de fois chaque seconde.
Cette fluidité permet également d’offrir à l’utilisateur un retour visuel rapide et confortable de ses actions.
Cette fréquence (nombre d’images par secondes) est directement lié aux caractéristiques de l’œil et du cerveau humain
Par exemple, une abeille recevant environ 200 images par secondes verra une suite d’images fixes là ou nous percevons une image animée.
Ainsi, les « défauts » de nos sens nous donnent l’occasion de simuler des images animées crédibles à l’aide de technologies adaptées.
La norme varie grandement selon les médias :
Ces normes n’ont pas été choisies au hasard :
Lorsque l’on parle d’images produites par un ordinateur ou CG/CGI (Computer Graphics Images), il faut bien faire la différence entre celles qui ont été produites en précalculé et celles qui sont générées en temps réel.
Ce procédé consiste à produire des rendus et de les enregistrer sous la forme de fichiers, ces images pouvant éventuellement être assemblées afin de produire une vidéo.
Utilisé entre-autre dans le cinéma (effets spéciaux, animation 3D) afin de garantir une qualité de rendu maximale, ce procédé nécessite parfois plusieurs secondes, minutes ou même heures pour générer une seule image.
Ces temps de rendus à durée indéfinie sont pris en compte dans l’organisation des tâches des équipes en étalant les rendus sur l’ensemble de la production afin de ne pas exploser les délais de production.
Ce procédé est utilisé dans les jeux-vidéos et différents logiciels (CAO, modélisation, divertissement…).
Ces images sont produites à la volée avec la contrainte d’être générées suffisamment rapidement afin de garantir une bonne fluidité d’animation.
Par exemple, pour obtenir une cadence de 60 images par secondes, l’intervalle de rendu ne doit pas excéder les 16,6 ms (en réalité, ce temps devra même être un peu plus court, car l’ordinateur ne se contente pas que de calculer les graphismes).
Bien que la puissance des machines ne cesse de croître, ces délais exigents restent toujours un défi lorsqu’il s’agit de produire des rendus de qualité.
L’optimisation est donc nécessaire, voire indispensable afin de ne pas faire chuter la fréquence de rendu, même lors des scènes les plus spectaculaires.
Les moteurs et APIs graphiques sont justement conçus dans cette optique, offrant aux développeurs des outils qui leur permettent de jouir d’une optimisation maximale.
Malgré cela, les performances peuvent varier considérablement entre le code d’un débutant et d’un professionnel spécialisé dans l’optimisation graphique.
Il n’est d’ailleurs pas rare de n’obtenir un framerate décent qu’en fin de production d’un jeu-vidéo, obligeant aux équipes de travailler sur du matériel surpuissant.
Ancien système
En lien avec les moniteurs à canon à électron (tubes cathodiques)
Scanlines
50Hz, 60Hz
Deux images sont interlacée en une seule, une ligne sur deux.
Système actuel
Chaque image est indépendante de la précédente.
Produire des images en temps réel est un défi qui repose sur le hardware de la machine.
Or, ce dernier est constitué de multiples composants.
Voici les éléments les plus importants à prendre en compte dans une réflexion de code optimisé :
Obtenir des bonnes performances nécessite une synergie maîtrisée de ces composants, sachant que le développeur n’a pas un accès complet à chacun d’entre eux. Il devra comprendre leur fonctionnement interne et automatisé afin d’optimiser son code.
Ainsi, les ralentissements constatés ne sont pas toujours dus au manque de puissance du matériel, mais à une mauvaise gestion de ce dernier.
Par exemple, les processeurs passent souvent beaucoup plus de temps à « attendre » que des instructions leur soient livrées plutôt qu’à les exécuter.
Ces délais sont généralement dus à des appels dans la mémoire vive ou pire encore au disque dur.
Le terme « rammer » vient d’ailleurs du fait que sur les machines dont la RAM est quasi saturée, le système ne cesse de décharger une partie de son contenu sur le disque dur pour recharger une autre
Les APIs (Application Programing Interface) graphiques sont des bibliothèques de programmation 2D et/ou 3D proposant des fonctions normalisées destinées à produire des images qui pourront directement être affichées à l’écran ou enregistrées sous la forme de fichiers.
Mis à part les APIs Software, les APIs graphiques exécutent des instructions à la fois côté CPU et GPU, ces dernières étant transmises au pilote graphique qui se chargera d’échanger avec la carte graphique.
Ils sont donc plus bas niveau qu’un moteur de rendu, mais plus haut niveau que les drivers.
À noter qu’il est pertinent de concevoir la carte graphique comme un second ordinateur qui travaille en parallèle du CPU avec lequel il est nécessaire de communiquer comme s’il se trouvait sur un réseau local.
Le choix de l’API se fera par le développeur selon le matériel, système d’exploitation et/ou logiciel cible, ainsi que sa maîtrise de la bibliothèque.
Les projets multi-plateforme nécessitent souvent l’utilisation de plusieurs APIs, obligeant le développeur à réécrire l’interface Moteur-API graphique à plusieurs reprises.
Par exemple, une équipe souhaitant faire tourner leur jeu sur PC, XBox Series, Playstation 5, Nintendo Switch, mobile et tablettes peuvent faire ce choix :
Avant et durant l’avènement des cartes graphiques, les fonctions graphiques étaient exclusivement gérées par le CPU.
De part le fait qu’elles sont programmées de façon classique en assembleur ou en code de bas niveau, ces instructions sont appelées Software.
Les premiers épisodes de Doom et de Quake d’ID Software, dont le code est disponible en open source et documenté dans des livres, sont des exemples remarquables de jeux capables de fonctionner sans carte graphique.
Avec OpenGL, DirectX fait partie des APIs graphiques les plus connues et utilisées.
Néanmoins, DirectX ne se limite pas aux graphismes, car elle embarque des modules d’entrées (DirectInput, XInput), de son (DirectSound, XAudio, XAct, DirectMusic), de réseau (DirectPlay) et bien d’autres.
La partie graphique de DirectX reste pourtant une référence avec notamment Direct3D.
Appartenant à Microsoft, DirectX est naturellement présent sur les systèmes Windows et XBox, néanmoins, une version Linux a été annoncée.
DirectX est très apprécié des développeurs, tout particulièrement dans le domaine du jeu-vidéo, pour sa simplicité de programmation, sa documentation étoffée, sa communauté et sa puissance.
DirectX utilise le langage HLSL pour programmer ses shaders.
Initialement développée par Silicon Graphics, OpenGL est l’API graphique la plus ancienne encore présente sur le marché.
Comme son nom l’indique, OpenGL (Open Graphic Library) est une bibliothèque graphique open source et gratuite accessible à tous.
Reprise par le groupe Khronos en 2000, OpenGL n’a cessée d’évoluer pour se stabiliser en 2017 sur la version 4.6.
Depuis, le groupe se focalise sur l’API Vulkan, initialement baptisée OpenGL-Next.
Compatible avec de très nombreuses plate-formes, OpenGL est probablement la bibliothèque la plus utilisée dans le monde, qu’il s’agisse d’applications multimédia, 3D ou de jeux-vidéos.
Des variations de la bibliothèque ont d’ailleurs été conçues pour du matériel spécifique tel que OpenGL|ES pour les appareils mobiles ou OpenGL|SC pour les appareils embarqués.
Tout comme DirectX, OpenGL est appréciée des développeurs, également pour sa simplicité de programmation, sa documentation étoffée, sa communauté et sa puissance.
OpenGL utilise le langage GLSL pour programmer ses shaders.
OpenGL|ES est une version allégée d’OpenGL destinée à être exécutée sur des appareils mobiles.
Cette architecture permet un compromis intéressant entre qualité d’affichage et économie d’énergie.
WebGL est une bibliothèque basée sur OpenGL|ES 2.0, destinée à être utilisée dans une application HTML5.
Selon le matériel employé, les fonctions seront exécutées en OpenGL|ES (mobile) ou OpenGL (ordinateur).
Autrefois monopolisé par la technologie Flash, les jeux sur navigateurs fonctionnent aujourd’hui en WebGL au sein d’une balise <canvas>
.
Initialement appelé OpenGL-Next, Vulkan est une API de nouvelle génération développée par les équipes d’OpenGL : Khronos Group.
Vulkan se démarque radicalement de ses concurrents en proposant un langage de très bas niveau offrant aux développeurs la possibilité de maîtriser au mieux la carte graphique, quitte à complexifier drastiquement leur code source.
En effet, certaines actions à priori simples nécessitent parfois plus d’une centaine de lignes de code afin de fonctionner correctement !
Vulkan n’est donc pas à mettre entre toutes les mains, pourtant, confié à des experts, l’API peut véritablement faire la différence en termes de performances et d’économie d’énergie.
Un autre atout de Vulkan est sa portabilité.
Pour le moment, Vulkan fonctionne sur Windows, Mac, Linux, Android, IOS et Nintendo Switch.
Contrairement à OpenGL/OpenGL|ES, Vulkan reste un seul et même langage utilisable sur ordinateur et appareil mobile.
Ainsi, par exemple, la partie graphique d’un logiciel ou d’un jeu destiné à être porté sur plusieurs plateformes ne nécessite que d’un seul code sources plutôt que d’un code PC et d’un code mobile.
...
...
...
...
...
...
...
...
Puissance de 2 Calcul de poids facile²
Latin multum in parvo (Bezucoup dans peux) Complêt -> +33.33% de la taille originale ...
DXT1 DXT5 BC4...
Alpha binaire passant de la transparence totale à l’opacité totale à partir d’un seuil donné. Ce procédé permet de continuer à considérer l’objet comme non-transparent et utiliser le Depth Buffer.
Latin multum in parvo (Bezucoup dans peux) Complêt -> +33.33% de la taille originale ...
Développé en 1971 par Henri Gouraud, chercheur en informatique français
Développé en 1973 par Bùi Tường Phong, informaticien vietnamien
Rendu physique réaliste Développé en 2004 par trois chercheurs en informatique américains Probes ...
Color ramp ...
Un buffer, ou mémoire tampon en français, est un espace mémoire réservé par le système. Ici, nous parlons de buffers graphiques correspondant aux pixels d’une image.
Le buffer d’affichage (display buffer) correspond à la zone mémoire de l’image affichée dans le viewport à chaque rafraîchissement de l’écran.
Il s’agit de la version finale d’une frame.
Aussi appelé Depth buffer
Camera clip near / clip far
Depth (16, 32, 64 bits)
Lorsque deux faces sont superposées, la carte graphique peut d’une image à l’autre calculer un objet avant l’autre ou inversement. Ce comportement induit un bug visuel faisant scintiller les deux faces de façon aléatoire.
Le double buffering est une technique qui permet d’éviter d’afficher des images en cours de construction lors du rafraîchissement naturel de l’écran.
Le double buffering permet également de récupérer des informations de l’image précédente pour composer la nouvelle image.
À noter que le triple buffering, même si beaucoup plus rare, existe également, offrant de potentielles données supplémentaires au développeur.
Double, et triple buffer
Pour chaque passe de rendu, il est nécessaire de sélectionner les objets qui seront transmis à la carte graphique.
Le moteur tentera donc d’éliminer le maximum d’objets non-nécessaires au rendu.
Cette étape ne fait pas encore intervenir la carte graphique.
Sont exclus de la liste de rendu tous les objets qui se trouvent hors du champ de vision.
Ce dernier est défini par la position et l’orientation de la caméra active, son champ de vision (FOV), les proportions d’affichage (Largeur/Hauteur) ainsi que les paramètres near clip et far clip.
Afin de s’assurer que les objets en périphérie de la zone de rendu ne soient pas éliminés, on les inscrit dans une bounding box, boîte virtuelle délimitant l’objet suivant les trois dimensions.
Si l’une des face de cette boîte entre dans le champ de la caméra, l’objet est ajouté à la liste de rendu.
Afin de s’assurer que les objets en périphérie de la zone de rendu ne soient pas éliminés, on les inscrit dans une bounding box, boîte virtuelle délimitant l’objet suivant les trois dimensions, et on vérifie qu’au moins l’un de ses 8 sommets est présent dans le champ de vision.
Néanmoins, certains environnements comptent parfois plusieurs dizaines de milliers d’objets et, avant de tous les tester, il est possible d’en éliminer une bonne partie en utilisant un système de hiérarchisation de la scène.
Ainsi, la scène est préalablement découpée en différents secteurs, voire sous-secteurs, définis par une bouding box. Seuls les objets appartenant aux secteurs présents dans le champ visuel seront traités.
Afin d’améliorer encore l’optimisation, il est possible de placer des boites virtuelles nommées Portals qui permettent de faire le lien entre différents secteurs.
Ainsi, non seulement, ne sont conservés que les secteurs visibles à travers ces Portals, mais aussi que les objets présents dans cette nouvelle zone d’affichage restreinte.
Si les Portals fonctionnent comme des fenêtres, les Anti-portals fonctionnent plutôt comme des obstacles qui permettent d’exclure tout objet qui se trouverait derrière eux.
À noter que seul les objets dont la bounding box est totalement présente au sein de l’Anti-portal sont éliminés.
Les objets opaques et transparents ne suivent pas exactement les mêmes étapes de rendu.
Ces différences de traitement nécessitent donc de classer les objets précédemment sélectionnés en deux catégories bien distinctes.
Les objets opaques (ou avec transparence binaire on/off (alpha test)) doivent être calculés en premier..
Ils ne nécessitent pas d’être classés dans un ordre particulier et seront très rapide à produire pour la carte graphique..
Ils utilisent le Z-buffer en lecture et en écriture.
Les objets transparents (alpha progressif, matériaux semi-transparents ou blend modes particuliers) sont rendus dans un second temps..
Il est nécessaire de les classer par profondeur afin de produire un ordre d’affichage cohérent..
Ils utilisent le Z-buffer en lecture, mais pas en écriture.
Les objets transparents doivent donc être classés, le plus généralement en calculant l’éloignement de leur pivot face à la caméra.
Les objets les plus lointains sont présents en début de liste, donc dessinés en premier et les plus proches en fin de liste.
Le système permute le buffer de rendu final et le buffer d’affichage.
Pour plus de détail, voir la technique double buffer.
Cette étape consiste à remplir le buffer d’une couleur unie (souvent du noir).
Même si cette opération n’est pas nécessaire pour le buffer d’affichage, ce dernier étant généralement totalement recouvert par du nouveau contenu, elle est souvent appliquée de part son faible coût.
Un autre buffer, le Z-buffer est également réinitialisé. Contrairement au buffer d’affichage, cette étape n’est pas facultative.
Le terme « passe de rendu » reste relativement flou, car il induit le plus souvent plusieurs draw calls (parfois plusieurs milliers).
Il doit plutôt être compris comme un processus de rendu plus ou moins automatisé.
Ainsi, selon le moteur utilisé, il est possible de définir autant de passes de rendu que voulu.
Par exemple, dans le jeu Portal de Valve, chaque portail présent dans la scène entraîne une passe supplémentaire (contrainte dans la zone du portail).
Voici un exemple un peu classique de passes de rendu d’un jeu vidéo :
Modification des paramètres des vertex :
Division de chaque triangle en 4 puissance N triangles, N étant le nombre d’itérations.
Groupement des triangles en « îlots » distincs.
Article « Writing a custom rendering shader using geometry program »
Suppression des faces orientées dos à la caméra.
Source = Couleur du pixel calculé
Destination = Couleur du pixel précédemment écrit dans le buffer
Blend = Source × Paramètre1 + Destination × Paramètre2
SrcColor × SrcAlpha + DestColor * OneMinusSrcAlpha
SrcColor × SrcAlpha + DestColor * One
SrcColor × DstColor + DestColor * Zero
SrcColor × DstColor + DestColor * SrcColor
Renderdoc est un outil extrêmement puissant qui permet d’analyser chaque étape d’un rendu graphique.
Le principe consiste à prendre une « capture d’écran » d’une image d’un jeu qui sera enregistrée sous la forme d’un fichier .rdc.
Ce fichier contiens l’ensemble des textures et modèles utilisés dans ce rendu ainsi que l’ensemble des étapes de rendu.
Nvidia texture tools est un outil qui permet de visualiser et convertir des images aux différents formats supportés par les APIs graphiques.
L’outil permet également de générer des MIP maps automatiques ou personnalisées.
Shadertoy est un outil en ligne qui permet d’écrire, consulter et partager des shaders codés en HLSL.