Dessiner beaucoup de tuiles avec OpenGL, à la manière moderne

35

Je travaille sur un petit jeu PC à base de tuiles / sprites avec une équipe de personnes et nous rencontrons des problèmes de performances. La dernière fois que j'ai utilisé OpenGL, c'était autour de 2004. Je me suis donc appris à utiliser le profil principal, et je me trouve un peu confus.

Je dois dessiner environ 250-750 dalles 48x48 sur l’écran à chaque image, ainsi qu’environ 50 images-objets. Les tuiles ne changent que lorsqu'un nouveau niveau est chargé et les sprites changent tout le temps. Certaines des tuiles sont composées de quatre pièces 24x24, et la plupart (mais pas toutes) des images-objets ont la même taille que les tuiles. Beaucoup de tuiles et d'images-objets utilisent le mélange alpha.

En ce moment, je fais tout cela en mode immédiat, ce qui, je le sais, est une mauvaise idée. Néanmoins, quand l'un des membres de notre équipe essaie de l'exécuter, il obtient des taux de trame très mauvais (~ 20-30 ips), et c'est bien pire quand il y a plus de tuiles, en particulier lorsque beaucoup de ces tuiles sont du genre sont coupés en morceaux. Tout cela me fait penser que le problème est le nombre d'appels de tirage effectués.

J'ai pensé à quelques solutions possibles à ce problème, mais je voulais les présenter à des personnes qui savent de quoi elles parlent, pour que je ne perde pas mon temps en une chose stupide:

CARRELAGE:

  1. Lorsqu'un niveau est chargé, dessinez toutes les tuiles une fois dans un tampon d'images associé à une grande texture de klaxon, puis dessinez un grand rectangle avec cette texture sur chaque image.
  2. Placez toutes les tuiles dans un tampon de sommets statique lorsque le niveau est chargé et dessinez-les de cette façon. Je ne sais pas s'il existe un moyen de dessiner des objets avec différentes textures avec un seul appel à glDrawElements, ou si c'est même quelque chose que j'aimerais faire. Peut-être que juste mettre toutes les tuiles dans une grande texture géante et utiliser des coordonnées de texture amusantes dans le VBO?

SPRITES:

  1. Dessinez chaque image-objet avec un appel séparé à glDrawElements. Cela semble impliquer beaucoup de changement de texture, ce qui me dit que c'est mauvais. Les tableaux de texture sont-ils utiles ici?
  2. Utilisez un VBO dynamique en quelque sorte. Même question de texture que le numéro 2 ci-dessus.
  3. Point sprites? C'est probablement idiot.

Certaines de ces idées sont-elles raisonnables? Y a-t-il une bonne mise en œuvre quelque part sur laquelle je pourrais regarder?

Nic
la source
Si les tuiles ne bougent ni ne changent et si elles ont la même apparence de niveau entier, vous devez utiliser la première idée - tampon d'image. Ce sera le plus efficace.
zacharmarz
Essayez d’utiliser un atlas de texture pour ne pas avoir à changer de texture, mais à garder le même. Maintenant, comment est leur framerate?
user253751

Réponses:

25

Le moyen le plus rapide de rendre les tuiles est de compresser les données de sommet dans un VBO statique avec des index (comme l'indique glDrawElements). L'écrire dans une autre image est totalement inutile et ne nécessite que beaucoup plus de mémoire. Le changement de texture est TRÈS coûteux, vous voudrez donc probablement regrouper toutes les tuiles dans un " Atlas de textures" et donner à chaque triangle du VBO les bonnes coordonnées de texture. Sur cette base, le rendu de 1000, voire de 100 000 tuiles ne devrait pas poser de problème, selon votre matériel.

La seule différence entre le rendu par mosaïque et le rendu par sprite est probablement que les sprites sont dynamiques. Donc, pour des performances optimales et faciles à réaliser, vous pouvez simplement mettre les coordonnées des sommets des sprites dans un flux dessiner VBO chaque image et dessiner avec glDrawElements. Emballez également toutes les textures dans un Atlas de textures. Si vos images-objets se déplacent rarement, vous pouvez également essayer de créer un VBO dynamique et de le mettre à jour quand une image-objet se déplace, mais c'est une surexploitation totale ici, car vous ne voulez que restituer certaines images-objets.

Vous pouvez regarder un petit prototype que j'ai fait en C ++ avec OpenGL: Particulate

Je rend environ 10000 points sprites, je suppose, avec une moyenne de 400 fps sur une machine habituelle (Quad Core @ 2.66GHz). C’est un processeur limité, ce qui signifie que la carte graphique pourrait générer encore plus de rendu. Notez que je n'utilise pas d'atlas de texture ici, car je n'ai qu'une seule texture pour les particules. Les particules sont rendues avec GL_POINTS et les shaders calculent alors la taille réelle du quad, mais je pense qu’il existe également un Quad Renderer.

Oh, et oui, à moins que vous ayez un carré et que vous utilisiez des shaders pour le mappage de texture, GL_POINTS est assez ridicule. ;)

Marco
la source
Les sprites changent de position et de texture, et la plupart le font à chaque image. En outre, les sprites et être créés et détruits très souvent. Est-ce que ces choses qu'un VBO Draw peut gérer?
Nic
2
Draw draw signifie en gros: "Envoyez ces données à la carte graphique et supprimez-les après le dessin". Vous devez donc renvoyer les données à chaque image, ce qui signifie que le nombre d'images-objets que vous restiturez, leur position, les coordonnées de texture ou la couleur importe peu. Mais envoyer toutes les données en même temps et laisser le GPU le traiter est BEAUCOUP plus rapide que le mode immédiat, bien sûr.
Marco
Tout cela a du sens. Vaut-il la peine d’utiliser un tampon d’index pour cela? Les seuls sommets qui seront répétés sont deux coins de chaque rectangle, non? (D'après ce que je comprends, les indices sont la différence entre glDrawElements et glDrawArrays. Est-ce exact?)
Nic
1
Sans index, vous ne pouvez pas utiliser GL_TRIANGLES, ce qui est généralement mauvais, car cette méthode de dessin est celle avec les meilleures performances garanties. De plus, l'implémentation de GL_QUADS est obsolète dans OpenGL 3.0 (source: stackoverflow.com/questions/6644099/… ). Les triangles sont le maillage natif de toute carte graphique. Ainsi, vous "utilisez" 2 * 6 octets de plus pour enregistrer 2 exécutions de vertex shader et vertex_size * 2 octets. Donc, vous pouvez généralement dire que c'est TOUJOURS mieux.
Marco
2
Le lien vers Particulate is dead ... Pourriez-vous en fournir un nouveau s'il vous plaît?
SWdV
4

Même avec ce nombre d'appels de tirage au sort, vous ne devriez pas voir ce genre de baisse de performances - le mode immédiat peut être lent mais il n'est pas si lent (pour référence, même le vieil ancien Quake peut gérer plusieurs milliers d'appels en mode immédiat par image sans tomber si mal).

Je soupçonne qu'il se passe quelque chose de plus intéressant. La première chose à faire est d’investir un peu de temps dans le profilage de votre programme, sinon vous courez un risque énorme de réarchitecture sur la base d’une hypothèse qui pourrait entraîner un gain de performance nul. Alors, parcourez même quelque chose d'aussi basique que GLIntercept et voyez où va votre temps. Sur la base des résultats, vous serez en mesure de résoudre le problème avec des informations réelles sur ce que sont vos principaux goulots d’étranglement.

Maximus Minimus
la source
J'ai fait du profilage, bien que cela soit gênant parce que les problèmes de performances ne se produisent pas sur la même machine que le développement. Je suis un peu sceptique quant au fait que le problème est ailleurs car les problèmes augmentent nettement avec le nombre de carreaux, et les carreaux ne font littéralement rien sauf être dessinés.
Nic
Qu'en est-il des changements d'état alors? Regroupez-vous vos carreaux opaques par État?
Maximus Minimus
C'est une possibilité. Cela mérite certainement plus d'attention de ma part.
Nic
2

D'accord, depuis que ma dernière réponse a un peu échappé des mains, voici une nouvelle qui est peut-être plus utile.


A propos de la performance 2D

Tout d'abord quelques conseils d'ordre général: la 2D n'est pas exigeante pour le matériel actuel, même un code largement non optimisé fonctionnera. Cela ne signifie pas pour autant que vous deviez utiliser le mode intermédiaire, assurez-vous au moins de ne pas modifier les états lorsque cela est inutile (par exemple, ne liez pas une nouvelle texture avec glBindTexture lorsque la même texture est déjà liée, une vérification du processeur est considérable. plus rapide qu'un appel glBindTexture) et de ne pas utiliser quelque chose d'aussi totalement faux et stupide que glVertex (même glDrawArrays sera bien plus rapide, et ce n'est pas plus difficile à utiliser, ce n'est pas très "moderne" cependant). Avec ces deux règles très simples, le temps de trame devrait être au moins 10 ms (100 fps). Maintenant, pour obtenir encore plus de vitesse, la prochaine étape logique est le traitement en lots, par exemple, en regroupant autant d'appels de dessin en un, pour cela, vous devriez envisager de mettre en œuvre des atlas de texture, afin que vous puissiez minimiser la quantité de liaisons de texture et ainsi augmenter le nombre de rectangles que vous pouvez dessiner avec un seul appel. Si vous n'êtes pas à environ 2ms (500fps), vous faites quelque chose de mal :)


Cartes de tuiles

Implémenter le code de dessin pour les cartes de tuiles consiste à trouver un équilibre entre flexibilité et rapidité. Vous pouvez utiliser des VBO statiques mais cela ne fonctionnera pas avec des mosaïques animées ou vous pouvez simplement générer les données de sommet de chaque image et appliquer les règles que j'ai expliquées ci-dessus, ce qui est très flexible mais de loin moins rapide.

Dans ma réponse précédente, j'avais introduit un modèle différent dans lequel le fragment shader prenait soin de l'ensemble de la texturation, mais il a été souligné qu'il nécessitait une recherche de texture dépendante et pouvait donc ne pas être aussi rapide que les autres méthodes. (L'idée est essentiellement de ne charger que les indications de pavé et de calculer les coordonnées de texture dans le fragment shader, ce qui signifie que vous pouvez dessiner la carte entière avec un seul rectangle)


Sprites

Les sprites nécessitent beaucoup de flexibilité, ce qui rend l’optimisation très difficile, en dehors de ceux décrits dans la section "A propos des performances 2D". Et à moins que vous vouliez des dizaines de milliers d'images-objets à l'écran en même temps, cela ne vaut probablement pas la peine.

API-Beast
la source
1
Et même si vous avez des dizaines de milliers de sprites, le matériel moderne devrait fonctionner à une vitesse convenable :)
Marco
@ API-Beast attend quoi? Comment calculer les UV de texture dans le fragment shader? N'êtes-vous pas censé envoyer les UV au fragment shader?
HgMerk
0

Si tout échoue ...

Mettre en place une méthode de dessin de bascule. Ne mettez à jour que les autres sprites à la fois. Même avec VisualBasic6 et des méthodes simples bit-blit, vous pouvez activement dessiner des milliers d'images-objets par image. Peut-être devriez-vous vous pencher sur ces méthodes, car votre méthode directe consistant à dessiner des sprites semble échouer. (On dirait plutôt que vous utilisez une "méthode de rendu", mais essayez de l'utiliser comme une "méthode de jeu". Le rendu est une question de clarté et non de rapidité.)

Les chances sont, vous redessinez constamment tout l'écran, encore et encore. Au lieu de simplement redessiner uniquement les zones modifiées. C'est BEAUCOUP de frais généraux. Le concept est simple, mais pas facile à comprendre.

Utilisez un tampon pour l'arrière-plan statique vierge. Ceci n'est jamais rendu, à moins qu'il n'y ait pas de sprites à l'écran. Ceci est constamment utilisé pour "revenir" à l'endroit où un sprite a été dessiné, pour dessiner le sprite lors du prochain appel. Vous avez également besoin d'un tampon pour "dessiner", qui n'est pas l'écran. Vous y dessinez, puis, une fois tous dessinés, vous les retournez une fois à l’écran. Cela devrait être un appel à l'écran par tous vos sprites. (Au lieu de dessiner chaque image-objet à l'écran, une à la fois, ou d'essayer de tout faire en même temps, votre fusion alpha échouera.) L'écriture en mémoire est rapide et ne nécessite pas de temps à l'écran pour "dessiner". ". Chaque appel de tirage attendra un signal de retour avant de tenter à nouveau de tirer. (Pas une v-sync, un tick matériel, ce qui est beaucoup plus lent que le temps d'attente de la RAM.)

J'imagine que c'est l'une des raisons pour lesquelles vous ne voyez ce problème que sur un seul ordinateur. Ou bien, il s’agit du rendu logiciel d’ALPHA-BLEND, que toutes les cartes ne prennent pas en charge. Vérifiez-vous si cette fonctionnalité prend en charge le matériel avant de tenter de l’utiliser? Avez-vous une solution de repli (mode non alpha-blend), s’ils ne l’ont pas? De toute évidence, vous n'avez pas de code qui limite (nombre d'éléments mélangés), car je suppose que cela dégraderait le contenu de votre jeu. (Contrairement à ce qui se passait s'il ne s'agissait que d'effets de particules, tous alpha-mélangés, ce qui explique pourquoi les programmeurs les limitent, car ils pèsent lourdement sur la plupart des systèmes, même avec le support matériel.)

Enfin, je suggérerais de limiter votre mélange alpha aux seules choses qui en ont besoin. Si tout en a besoin ... Vous n'avez pas d'autre choix que de demander à vos utilisateurs de disposer de meilleures conditions matérielles, ou de dégrader le jeu pour obtenir les performances souhaitées.

JasonD
la source
-1

Créez une feuille de sprite pour les objets et un ensemble de tuiles pour le terrain, comme vous le feriez dans un autre jeu 2D, il n'est pas nécessaire de changer de texture.

Rendu des tuiles peut être une douleur parce que chaque paire de triangle a besoin de leurs propres coordonnées de texture. Il y a une solution à ce problème cependant, ça s'appelle rendu instancié .

Tant que vous pouvez trier vos données de manière à avoir, par exemple, une liste des tuiles d'herbe et leurs positions, vous pouvez restituer chaque tuile d'herbe avec un seul appel à dessiner. Il vous suffit de fournir un tableau. du modèle au monde matrices pour chaque tuile. Le tri de vos données de cette manière ne devrait pas poser de problème, même avec le graphe de scènes le plus simple.

dreta
la source
-1: L'instanciation est une idée pire que la solution pure-shader de M. Beast. L'instanciation est plus efficace pour les performances lors du rendu d'objets de complexité moyenne (environ 100 triangles ou plus). Chaque tuile triangle ayant besoin de coordonnées de texture n'est pas un problème. Vous venez de créer un maillage avec un tas de quads lâches qui forment un tilemap.
Nicol Bolas
1
@ NicolBolas bien, je vais laisser la réponse pour apprendre
dreta
1
Pour plus de clarté, Nicol Bolas, que suggérez-vous pour gérer tout cela? Le flux de Marco dessine quelque chose? Y a-t-il un endroit où je peux voir une implémentation de cela?
Nic
@Nic: Le streaming dans les objets tampons n'est pas un code particulièrement complexe. Mais vraiment, si vous ne parlez que de 50 sorts, ce n'est rien . Les chances sont bonnes que ce soit votre dessin de terrain qui soit à l'origine du problème de performances. Il serait donc probablement suffisant de passer aux tampons statiques.
Nicol Bolas
En fait, si l'instanciation fonctionnait comme on pourrait le penser, ce serait la meilleure solution - mais comme ce n'est pas le cas, il est préférable de regrouper toutes les instances en un seul vbo statique.
Jari Komppa