Les textures atlas

📅 July 06, 2023

Les atlas sont un élément essentiel dans le développement de jeu 2D qui permet de gagner en performance facilement. Il a tendance à être occulté (oui j'ai mené l'enquête 🔍 en regardant les sources de jeux Steam ). Nous allons donc voir dans cet article à quoi il sert et comment l'utiliser.

Les spritesheets et tilesets

Un spritesheet (parfois appelée tilesheet) est une grande image qui contient plusieurs petites images organisées dans une grille de cases de taille toujours identique, parfois sur une seule ligne ou une seule colonne. Un tileset diffère dans l'usage. Si le spritesheet est utilisée dans l'animation d'objet et de personnage 🏃, le tileset sert plutôt à la construction de décor 🌇. Leurs avantages sont de permettre un téléchargement plus rapide et d'avoir un poids plus léger par rapport à l'utilisation d'images séparées. Leur principale limitation est que chaque case étant un rectangle de même dimension, ils ne sont pas optimisés pour le trimming.

woman walk by Kenney ℹ️ assets par Kenney : exemple d'un spritesheet d'une animation d'un personnage

Les texture atlas

Un atlas est un tilesheet amélioré. Il est optimisé pour le trimming car il permet d'avoir des images de tailles différentes à l'aide d'un fichier descripteur en xml, json ou autre.

Atlas MaxRects

<!-- generic XML with TexturePacker
x  => sprite x pos in texture
y  => sprite y pos in texture
w  => sprite width (may be trimmed)
h  => sprite height (may be trimmed)
pX => x pos of the pivot point (relative to sprite width)
pY => y pos of the pivot point (relative to sprite height)
oX => sprite's x-corner offset (only available if trimmed)
oY => sprite's y-corner offset (only available if trimmed)
oW => sprite's original width (only available if trimmed)
oH => sprite's original height (only available if trimmed)
 -->
<TextureAtlas imagePath="kenney-rect-test.png" width="4990" height="3968">
    <sprite n="bannerTowerGreen_NE.png" x="4203" y="342" w="80" h="185" pX="0.5" pY="0.84" oX="20" oY="20" oW="120" oH="215"/>
    ...
</TextureAtlas>

Un atlas permet donc de rogner automatiquement la transparence de tous ses sprites. Et si besoin, il est possible d'utiliser les données d'offset (oX et oY ici) pour repositionner une animation dans son ensemble. On peut aussi ajouter un Pivot à ses images pour mieux gérer la rotation ou le positionnement de son sprite (pas disponible avec tous les formats de fichiers, mais ça devrait être très rapide pour l'ajouter dans votre moteur)

Quoi mettre dans son atlas

Quand on crée généralement un spritesheet par animation, on va pouvoir mettre toutes ses animations dans son atlas. Selon la taille que ça prend, on pourra éventuellement en faire un par personnage, voir mettre absolument tout (persos, props, tiles, ui). Il faut trouver un bon compromis pour diminuer son overdraw et ses drawcalls et gérer un bon niveau de RAM à un instant t, en intégrant les assets qui ont le plus de chance d'être affichés en même temps.

L'exception étant les backgrounds plein écran, qui prendraient trop de place tout en étant rectangulaires, et qui sont donc inutiles à ajouter dans un atlas car on y gagnerait peu (qu'un seul drawcall).

Texture Packer

Il existe de nombreux formats pour des moteurs de jeux différents tels que Phaser, Cocos2D, Godot, Starling ou même Unity et Unreal. Bien que ces derniers permettent d'en faire depuis leurs plateformes de développement en quelques clics, je recommande d'utiliser TexturePacker :

  • Il est performant 💪
  • Il vous rend plus autonome en permettant de changer de moteur de jeu plus facilement. 🔄
  • Il est utilisable aussi en ligne de commande et peut donc être intégré à n'importe quel pipeline 🏭
  • Il exclut les doublons 🔒
  • Il permet de redimensionner individuellement ou globalement les sprites, et d'exporter plusieurs tailles d'atlas 🖼️
  • Il peut être combiné à SpriteIlluminator qui permet de créer des normal map pour gérer des effets de lumières 💡
  • Et enfin, vous pouvez créer votre propre format descripteur pour répondre à vos besoins spécifiques 🧩. Voici par exemple un format d'atlas pour ma bibliothèque Spriter.

L'empaquetement par polygones

L'algorithme de base considère généralement vos sprites comme des rectangles mais il est aussi possible d'utiliser un algorithme qui utilisera des polygones en définissant un certain nombre de triangles. Attention, tous les moteurs de jeux ne le proposent pas nativement, mais pas d'inquiétude pour Cocos2D et Unity 👌, et ça reste assez simple à implémenter si besoin (exemple d'implémentation d'un parser d'atlas). Sinon comme autre logiciel indépendant capable de faire ça, il existe aussi SpriterUV qui permet de paramétrer le nombre de triangles de chaque sprite indépendamment.

Dans tous les cas, on peut affiner pour trouver un bon compromis entre la surcharge CPU dû à l'augmentation de triangles et à votre gain de place dans votre atlas mais aussi à l'overdraw.

sprite rect
Rectangles: 100% overdraw, 2 triangles
sprite polygons 82 9 20
Polygones: 82.9% overdraw, 20 triangles
Overdraw GPU elapsed
Overdraw réduit avec un polygone 39µs contre 118µs pour le fichier d'origine et 47µs pour un rectangle trimmé

Le nombre de polygones est paramétrable, voici en exemple 2 autres textures : 89% / 11 triangles et 78% / 49 triangles. Je pense que celui à 11 triangles est largement suffisant.

Tests et comparatifs

Maintenant, faisons un petit comparatif complet où l'on affiche simplement 464 images les unes sur les autres avec différentes méthodes. J'ai réalisé les tests avec un PC récent équipé d'une RTX 2060 donc les temps d'impression GPU sont très faibles. J'ai donc ajouté à mes tests l'impression GPU sur navigateur. On pourrait aller plus loin en faisant les tests sur un PC plus ancien ou sur mobile (mais j'ai besoin d'une grosse motivation là 😛) car je parie sur des différences plus notables. Dans l'idéal, on devrait aussi ajouter l'usage du CPU et le temps de parsing des PNG (qui n'est pas lisible tel quel pour le GPU). J'ai codé avec Haxe/OpenFl et Unity. OpenFL utilise OpenGl et Unity utilise DirectX 11. Le premier test Unity utilise les paramètres par défaut qui crée un polygone pour chaque image. Le dernier test Unity utilise un atlas avec les paramètres par défaut.

ID Algorithme Dimensions Taille sur le disque Temps de chargement (local / 4G / 3G)1 RAM (tex théorique2 / Browser3 / Windows4) GPU elapsed (Browser5 / Windows6) drawcalls
1 Fichiers séparés (déjà rognés) N/A 2.57Mo 660ms / 4.75s / 56s - / 59Mo / 155Mo 280ms / 5266µs 464 drawcalls
2 MaxRects Atlas maxrects 4990x3968 2.03Mo 28ms / 200ms / 12s 75Mo / 48MO / 150Mo 275ms / 3631µs 1drawcall
3 Polygones : texture ajustée Atlas Polygons 3423x4094 1.9Mo 55ms / 200ms / 11s 53Mo / 46Mo / 128Mo 178ms/2768µs 1 drawcall
4 Polygones : texture POT 4096x4096 1.95Mo 59ms / 210ms / 12s 64Mo / 52Mo / 142Mo 221ms/2785µs 1 drawcall
5 Polygones : textures POT 3 * 2048x2048 + 2048x1024 1.85Mo 75ms / 200ms / 11.4s 56Mo / 47Mo / 130Mo 192ms/2772µs 4 drawcalls
6 Polygones + Compression (ATF DXT5) 4096x4096 16.1Mo 385ms / 1s / >30s 16Mo / 38Mo / 111Mo 45ms/1649µs 1 drawcall
7 Polygones + Compression (ATF DXT5) + GZIP 4096x4096 4.9Mo 23ms / 340ms / 28s 16Mo / 38Mo / 111Mo 45ms/1649µs 1 drawcall
8 Unity Default (Tight Mesh + Compression DXT5 + GZIP) N/A 4.6Mo 128ms / - / - N/A / - / 100Mo - / 4499µs 465 drawcalls
9 Unity Atlas (Tight Mesh + max 2048 + Compression DXT5 + GZIP) 5 * 2048x2048 4.3Mo 106ms / 366ms / 30s 20Mo / 111Mo / 94Mo 45ms/1750µs 6 drawcalls

ℹ️ assets par Kenney

Conclusions et remarques

Le test étant très simple, il n'y a aucune règle inviolable énoncée ci-dessous, juste des perspectives à surveiller lorsque vous cherchez à optimiser votre jeu. L'utilisation d'un atlas étant déjà bien en soit.

  • Il y a peu de différence entre le temps GPU du test 1 et du test 2 sur navigateur. Les images ont déjà été trimmées (pas de différence dans l'overdraw donc), la seule différence étant ici le nombre de drawcalls. Il faudrait peut-être afficher 100 fois ça pour noter des différences plus flagrantes (peut être une idée pour un prochain article 👀)
  • Le mieux est d'avoir la texture la plus petite possible car les résultats du test 3 sont meilleurs que ceux des tests 4 et 5.
  • On peut avoir de meilleures performances même avec quelques drawcalls supplémentaires (cf. tests 4 et 5) si notre texture est plus petite.
  • Le natif c'est quand même 30 à 70 fois plus rapide 🚀
  • Quand c'est possible, on utilise une texture compressée car on améliore bien les performances malgré un poids de fichier plus important. La compression fera l'objet d'un prochain article 👀.
  • Je trouve que mes builds natives prennent trop de RAM 😒. J'ai probablement des choses qui ne vont pas dans mon framework car il y a quelques années je faisais tourner 8 textures de 4096x4096 pour environ 100Mo. J'ai déjà commencé à proposer un PR 💪.

⌨️ Dev Pour savoir s'il faut utiliser un atlas avec rectangles ou polygones, il faut d'abord vérifier la compatibilité de son moteur de jeu. Ensuite, cela dépend de votre projet, le mieux étant de privilégier l'empaquetage par polygones quand cela est possible. Cela dit, un maxRect pour un petit projet ça peut suffire, car l'essentiel étant ici d'être à l'aise avec la techno que l'on utilise.

Les dimensions de l'atlas

POT ou NPOT

On peut lire à gauche à droite qu'il est nécessaire d'utiliser une texture de dimensions POT. A priori, d'après les résultats du tableau ci-dessus, cela ne semble absolument pas nécessaire sur un PC moderne. D'ailleurs, l'intégralité des articles que l'on peut trouver sur internet recommandant une texture POT ont déjà 10 ans 👴 et parlent alors de supporter du matériel ancien.

Par exemple, dans cet échange sur stackexchange au sujet des textures de dimensions POT, il y est fait mention du guide des recommandations de performance de PowerVR (qui développe des accélérateurs graphiques mobiles pour Intel, Samsung...). Dans ce guide, il y a un chapitre sur les textures NPOT qui recommandent encore les textures POT car leurs accélérateurs sont optimisés pour cela. Ils avertissent également que le support des textures POT ne sont pas supportées par OpenGL ES 1.1 et avec un peu de paramétrage sur OpenGL ES 2.0 (sortie en 2007). Seulement, aujourd'hui les appareils supportent OpenGL ES 3.0 depuis 2017 sur Chrome, Opera et Firefox, et depuis 2021 pour Safari7, et très certainement à une date antérieure pour les applications natives puisque sa date de sortie est 2012. Bon malgré cela, d'après l'enquête annuelle de Steam sur le hardware de ses utilisateurs, il y aurait 7% d'utilisateurs de DirectX 8 (pas compatible NPOT). Le sondage ne dit pas si c'est exclusif ou si leur GPU fonctionne aussi avec des versions plus récentes. Peut-être est-ce pour de l'émulation de vieux jeux 👾 ?

Toutefois, j'ai voulu vérifier la performance mobile en reprenant les tests 3 et 4 et les résultats ne confirment pas les conseils de PowerVR. Toutefois, il semble que mon GPU Adreno n'utilise pas la technologie PowerVR. Enfin les relevés via le logiciel Arm Graphics Analyser sont assez fastidieux à obtenir et j'ai parfois un résultat où 4b est plus rapide que 3b en natif. Cela dit, sans pouvoir comparer la somme des appels d'API OpenGL, je ne peux pas en être certain. Il me faudrait un logiciel mieux conçu mais ni le profiler de Qualcomm, ni le profiler de Google ne fonctionne sur mon téléphone.

ID Algorithme Dimensions Scale GPU elapsed (Mobile Chrome browser8 / Android9)
3 OpenGL ES 3.2 3423x4094 100% 263ms / glTexImage2D: 23581µs, glDrawArrays: 741µs
4 OpenGL ES 3.2 4096x4096 100% 277ms / glTexImage2D: 26588µs, glDrawArrays: 552µs
3b OpenGL ES 3.2 3423x4094 50% 237ms / glTexImage2D: 23167µs, glDrawArrays: 562µs
4b OpenGL ES 3.2 4096x4096 50% 298ms / glTexImage2D: 25769µs, glDrawArrays: 783µs

Donc inutile de se prendre la tête avec ça, à moins d'avoir un besoin spécifique. 😏 On peut aussi imaginer créer dynamiquement à l'exécution du jeu une texture POT à partir d'une texture NPOT pour les seuls utilisateurs qui en auraient besoin (il suffit de regarder alors quelle version d'OpenGL ou DirextX ils utilisent). C'est ce que fait Unity plus ou moins...car ils le font à priori pour tout le monde.

Les dimensions maximales possibles

Chaque GPU a des limitations qui lui sont propres.

💻 Pour les PC, aujourd'hui on a facilement du 16384x16384.

📱 En revanche pour les mobiles, vu la grande disparité et la progression fulgurante de ces dernières années, je conseille de rester sur une texture de dimensions maximales de 2048x2048 pour toucher un public le plus large possible. Cela dit, mon téléphone moyen de gamme de 2019 supporte 4096x4096. L'iphone 6S (2016) aussi. Donc tout dépend de votre cible.

📺 Ma télé (via son navigateur) est quant à elle capable de gérer du 8192x8192 (à relativiser avec le fait qu'elle est quand même bien moins performante que mon téléphone).

🎮 A priori la Xbox360 est capable de gérer du 8192x8192.

Enfin, pour connaître la taille maximale supportée par son appareil, on peut rapidement utiliser webgl dans une page web :

var canvas = document.createElement('canvas');
var gl = canvas.getContext('experimental-webgl');
document.write(gl.getParameter(gl.MAX_TEXTURE_SIZE));

Note: désolé pour l'ordre des notes de bas de page, ça sera corrigé lors d'une prochaine mise à jour de mon site


  1. pour la simplification, j'ai relevé le temps de la fonction englobant tous les appels glTexImage2D ou glCompressedTexImage2D
  2. j'ai relevé la variable GPU Time Elapsed des fonctions d'écriture à l'aide du logiciel Intel Graphics Frame Analyser
  3. width * height * 32 / 8 / 1024 / 1024 pour les PNG | width * height * 8 / 8 / 1024 / 1024 pour les DXT5
  4. empreinte mémoire de l'application entière obtenue à l'aide du gestionnaire de tâche du navigateur SHIFT + ESC
  5. empreinte mémoire de l'application entière obtenue à l'aide du gestionnaire de tâche de windows CTRL + SHIFT + ESC
  6. j'ai utilisé la différence entre Load et DOMContentLoad dans la console du navigateur (je ne peux pas additionner le temps de chargement de chaque fichier car le navigateur gère un certain nombre de chargements en parallèle). J'ai compté le temps de chargement du fichier descripteur le cas échéant.
  7. caniuse webgl2
  8. j'ai utilisé chrome://inspect/#devices pour récupérer les performances du browser mobile sur mon pc
  9. j'ai utilisé le logiciel ARM Graphics Analyser qui n'est ni pratique ni précis car il ne fournit pas la somme de tous les appels d'API OpenGL