web-dev-qa-db-fra.com

Est-ce que Graphics.DrawImage est trop lent pour les grandes images?

Je travaille actuellement sur un jeu et je souhaite un menu principal avec une image de fond.

Cependant, je trouve la méthode Graphics.DrawImage() très lente. J'ai fait des mesures. Supposons que MenuBackground soit mon image de ressource avec une résolution de 800 x 1200 pixels. Je le dessinerai sur un autre bitmap de 800 x 1200 (je convertis tout d'abord en un tampon bitmap, puis je le redimensionne et je le dessine enfin à l'écran - c'est ainsi que je gère la possibilité de résolutions de plusieurs joueurs. de quelque manière que ce soit, voir le paragraphe suivant).

J'ai donc mesuré le code suivant:

Stopwatch SW = new Stopwatch();
SW.Start();

// First let's render background image into original-sized bitmap:

OriginalRenderGraphics.DrawImage(Properties.Resources.MenuBackground,
   new Rectangle(0, 0, Globals.OriginalScreenWidth, Globals.OriginalScreenHeight));

SW.Stop();
System.Windows.Forms.MessageBox.Show(SW.ElapsedMilliseconds + " milliseconds");

Le résultat est assez surprenant pour moi - la Stopwatch mesure quelque chose entre 40 - 50 milliseconds. Et parce que l'image d'arrière-plan n'est pas la seule chose à dessiner, le menu complet prend environ 100 ms à afficher, ce qui implique un retard observable.

J'ai essayé de l'attribuer à l'objet Graphics donné par l'événement Paint, mais le résultat était 30 - 40 milliseconds - pas beaucoup changé.

Alors, cela signifie-t-il que Graphics.DrawImage() est inutilisable pour dessiner des images plus grandes? Si oui, que dois-je faire pour améliorer les performances de mon jeu?

28
Miroslav Mares

Oui, c'est trop lent.

J'ai rencontré ce problème il y a plusieurs années en développant Paint.NET (dès le début, en fait, et c'était plutôt frustrant!). Les performances de rendu étaient abominables, car elles étaient toujours proportionnelles à la taille du bitmap et non à la taille de la zone à redessiner. Autrement dit, le nombre d'images par seconde diminuait à mesure que la taille du bitmap augmentait et le nombre d'images par seconde n'augmentait jamais, pas plus que la taille de la zone non valide/redessinée lors de l'implémentation de OnPaint () et de l'appel de Graphics.DrawImage (). Un petit bitmap, disons 800x600, a toujours bien fonctionné, mais les images plus grandes (par exemple 2400x1800) étaient très lentes. (Vous pouvez de toute façon supposer, pour le paragraphe précédent de toute façon, qu'il ne se passait rien de plus, tel que la mise à l'échelle avec un filtre bicubique coûteux, ce qui aurait affecté négativement les performances.)

Il est possible de forcer WinForms à utiliser GDI à la place de GDI + et d’éviter même la création d’un objet Graphics dans les coulisses, ce qui vous permet de superposer une autre boîte à outils de rendu (Direct2D, par exemple). Cependant, ce n'est pas simple. Je le fais dans Paint.NET, et vous pouvez voir ce qui est requis en utilisant quelque chose comme Reflector dans la classe GdiPaintControl dans la DLL SystemLayer, mais pour ce que vous faites, je le considère comme un dernier recours.

Cependant, la taille du bitmap que vous utilisez (800 x 1200) devrait quand même fonctionner assez bien dans GDI + sans avoir à recourir à une interop avancée, à moins que vous ne visiez un objectif aussi bas qu'un Pentium II à 300 MHz. Voici quelques conseils qui pourraient vous aider:

  • Si vous utilisez un bitmap opaque (sans alpha/transparence) dans l'appel à Graphics.DrawImage(), et surtout s'il s'agit d'un bitmap 32 bits avec un canal alpha (mais vous savez c'est opaque, ou vous ne l'êtes pas soin), puis définissez Graphics.CompositingMode sur CompositingMode.SourceCopy avant d'appeler DrawImage() (assurez-vous de rétablir la valeur d'origine après, sinon les primitives de dessin normales sembleront très moches). Cela évite beaucoup de calculs de fusion supplémentaires par pixel.
  • Assurez-vous que Graphics.InterpolationMode n'est pas défini sur quelque chose comme InterpolationMode.HighQualityBicubic. Utiliser NearestNeighbor sera le plus rapide, mais s'il y a un étirement, cela peut ne pas sembler très bon (à moins que cela s'étire exactement de 2x, 3x, 4x, etc.) Bilinear est généralement un bon compromis. Vous ne devez jamais utiliser autre chose que NearestNeighbor si la taille du bitmap correspond à la zone sur laquelle vous dessinez, en pixels.
  • Toujours dessiner dans l'objet Graphics qui vous a été attribué dans OnPaint().
  • Fais toujours ton dessin en OnPaint. Si vous avez besoin de redessiner une zone, appelez Invalidate(). Si vous souhaitez que le dessin ait lieu maintenant, appelez Update() après Invalidate(). Cette approche est raisonnable, car les messages WM_Paint (qui entraînent un appel à OnPaint()) sont des messages "de faible priorité". Tout autre traitement par le gestionnaire de fenêtres sera effectué en premier et vous risqueriez d’avoir beaucoup de sauts d’image et d’attelage autrement.
  • Utiliser un System.Windows.Forms.Timer comme minuterie de framerate/tick ne fonctionnera pas très bien. Celles-ci sont implémentées à l'aide de la variable SetTimer de Win32 et donnent lieu à des messages WM_TIMER qui entraînent ensuite la génération de l'événement Timer.Tick, et WM_TIMER est un autre message de priorité basse envoyé uniquement lorsque la file d'attente des messages est vide. Vous feriez mieux d'utiliser System.Threading.Timer, puis d'utiliser Control.Invoke() (pour vous assurer que vous êtes sur le bon fil!) Et d'appeler Control.Update().
  • En général, n'utilisez pas Control.CreateGraphics(). (corollaire de 'toujours dessiner dans OnPaint()' et 'toujours utiliser la Graphics qui vous a été donnée par OnPaint()')
  • Je recommande de ne pas utiliser le gestionnaire d'événements Paint. Au lieu de cela, implémentez OnPaint() dans la classe que vous écrivez, ce qui devrait être dérivé de Control. Dérivant d'une autre classe, par exemple PictureBox ou UserControl, n’ajoutera aucune valeur pour vous ou entraînera une surcharge supplémentaire. (BTW PictureBox est souvent mal compris. Vous ne voudrez probablement presque jamais l’utiliser.)

J'espère que cela pourra aider.

94
Rick Brewster

GDI + n'est en aucun cas un démon de la vitesse. Toute manipulation d'image grave doit généralement aller du côté natif des choses (appels pInvoke et/ou manipulation via un pointeur obtenu en appelant LockBits.) 

Avez-vous examiné XNA/DirectX/OpenGL?. Ce sont des frameworks conçus pour le développement de jeux et seront des ordres de grandeur plus efficaces et flexibles que d’utiliser un framework d’interface utilisateur comme WinForms ou WPF. Les bibliothèques offrent toutes des liaisons C #. 

Vous pouvez invoquer le code natif à l'aide de fonctions telles que BitBlt , mais il existe également une surcharge associée au franchissement de la limite de code géré.

3
Ed S.

GDI + n'est probablement pas le meilleur choix pour les jeux. DirectX/XNA ou OpenGL doivent être préférés car ils utilisent toutes les accélérations graphiques possibles et rapides.

3
Ani

Bien que cette question soit ancienne et que WinForms soit un cadre ancien, je voudrais partager ce que je viens de découvrir par accident: dessiner une bitmap dans un BufferedGraphics et le rendre ensuite au contexte graphique fourni par OnPaint est way plus rapide que de dessiner le bitmap directement dans le contexte graphique d'OnPaint - du moins sur ma machine Windows 10. 

C'est surprenant car intuitivement j'avais supposé qu'il serait un peu plus lent de copier deux fois des données (et j'ai donc pensé que cela n'est généralement justifié que si l'on souhaite effectuer une double mise en mémoire tampon manuellement). Mais évidemment, quelque chose de plus sophistiqué se passe avec l’objet BufferedGraphics.

Créez donc un BufferedGraphics dans le constructeur du contrôle qui doit héberger le bitmap (dans mon cas, je voulais dessiner un bitmap plein écran 1920x1080):

        using (Graphics graphics = CreateGraphics())
        {
            graphicsBuffer = BufferedGraphicsManager.Current.Allocate(graphics, new Rectangle(0,0,Screen.PrimaryScreen.Bounds.Width,Screen.PrimaryScreen.Bounds.Height));
        }

et l'utiliser dans OnPaint (en annulant OnPaintBackground)

    protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}

    protected override void OnPaint(PaintEventArgs e)
    {   
        Graphics g = graphicsBuffer.Graphics;

        g.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);

        graphicsBuffer.Render(e.Graphics);
    }

au lieu de définir naïvement

    protected override void OnPaintBackground(PaintEventArgs e) {/* just rely on the bitmap to fill the screen */}

    protected override void OnPaint(PaintEventArgs e)
    {   
        e.Graphics.DrawImage(someBitmap,0,0,bitmap.Width, bitmap.Height);
    }

Voir les captures d'écran suivantes pour une comparaison de la fréquence d'événements MouseMove obtenue (j'implémente un contrôle d'esquisse très simple en mode point). En haut se trouve la version où le bitmap est dessiné directement, en bas BufferedGraphics est utilisé. J'ai déplacé la souris à peu près à la même vitesse dans les deux cas.

 enter image description here

1
oliver