Laden...

[Artikel] Zeichnen Optimieren / Schnelles zeichnen

Erstellt von dr4g0n76 vor 17 Jahren Letzter Beitrag vor 17 Jahren 51.343 Views
dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren
[Artikel] Zeichnen Optimieren / Schnelles zeichnen

Ich habe hier im Forum jetzt einige Stunden verbracht, um zu sehen, wie man eigentlich in .NET einen Zeichenvorgang optimieren kann.

Es gibt hier einfach zu viele Threads zu diesem Thema. Aber nirgendwo gebündelt, wie man etwas optimiert. Oder ich hab einfach die falschen Stichworte benutzt. Man möge es mir hiermit nachsehen.

Da ich auch gerade dieses Problem habe, kam ich auf die Idee, erst mal alle Methoden von

Graphics

und

Control

durchzusuchen.

Wenn man die APIs hinzunimmt sind natürlich auch BitBlt und Co mit von der Partie.

Natürlich sind auch die Implementierungen der Algorithmen nicht zu verachten.
Bei z.B. 1000 Shapes kann es sinnvoll (ist es sicher auch) sein, zu überprüfen, was und ob überhaupt neu gezeichnet werden muss.
Auf die Algorithmen möchte ich später eingehen.

Was ich jetzt möchte ist hier diskutieren, wie man Zeichenvorgänge optimieren kann.

Dabei gilt folgende Bedingung:

Es wird nicht mit OpenGL oder DirectX gezeichnet, sondern nur mit GDI.

Folgende Möglichkeiten habe ich bisher gefunden:

Methoden von Graphics:

IsVisible: hiermit kann geprüft werden, ob der Punkt/Rechteck usw. des Objekts überhaupt sichtbar ist.
IsVisibleClipEmpty: Überprüfen, ob im sichtbaren Clipbereich überhaupt was zu sehen ist
IsClipEmpty: Wie Clip-Empty auch für unsichtbaren Bereich
Clip: Region in der überhaupt gezeichnet wird
ClipBounds: Region umschliessendes Rechteck (Begrenzungen)
CompositingMode: enum SourceCopy/SourceOver
CompositingQuality: Qualitätsniveau während des Zusammensetzens
BeginContainer: Speichert GrafikContainer mit aktuellem Zustand
EndContainer: GrafikContainer der wieder hergestellt werden soll
Flush: Erzwingt zeichnen aller anstehenden Grafikoperationen, es wird nicht gewartet bis alles gezeichnet wurde.
InterpolationMode: Gibt an, wie z.B. Grafik beim verkleinern neu berechnet wird.
PageScale: Skalierung zwischen globalen Einheiten und Seiteneinheiten
PageUnit: Maßeinheiten (Page)
PixelOffsetMode: Der Pixeloffsetmodus bestimmt den Offset von Pixeln bei der Darstellung
TextRenderingHint: Angeben ob Text z.B. mit Antialiasing wiedergegeben wird.

Wie ihr sicher gemerkt habt, hab ich dabei versucht diese Befehle so weit wie möglich mit meinen eigenen Worten zu beschreiben.
Ansonsten hab ich einfach die Beschreibung von Microsoft übernommen.

Ich will einfach dahin kommen z.B. 1000 eigens definierte Objekte relativ schnell zu zeichnen.

Wenn sich der Algorithmus und die Implementation darum kümmern was neu gezeichnet werden muss und ggf. einige Optimierungsschalter gesetzt werden, sollte
es doch nicht so schwierig sein das ganze im Millisekunden Bereich zu zeichnen, oder was meint ihr?

Ich versuche also 1000 Grafik-Objekte zu zeichnen.

Diese sind von einer ganz einfachen Klasse CShape abgeleitet.


using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Drawing.Drawing2D;
namespace Test
{
    class CShape
    {
        private Point m_ptLocation = Point.Empty;

        private bool m_bNeedsUpdate = false;
        public bool NeedsUpdate
        {
            get { return this.m_bNeedsUpdate; }
            set { this.m_bNeedsUpdate = value; }
        }

        public virtual Point Location
        {
            get { return this.m_ptLocation; }
            set { this.m_ptLocation = value; }
        }

        public virtual void Draw(Graphics g)
        {
        }

    }
}

Ob die Methode NeedsUpdate benötigt wird, ist noch fraglich. Lassen wir die einfach mal drin.

Und so sieht die Methode CRectangle aus:


using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
namespace Test
{
    class CRectangle:CShape
    {
        Random m_Random = new Random(DateTime.Now.Millisecond);
        private Rectangle m_Rectangle = Rectangle.Empty;
        public Size Size = new Size(150, 40);
        private Color m_Color = Color.Blue;
        private Brush m_Shadow = new SolidBrush(Color.FromArgb(128,0,0,0));
        private Brush m_ColorBrush = null;
        private Pen m_BorderPen = new Pen(new SolidBrush(Color.Black),2);
        private Brush m_BlackBrush = new SolidBrush(Color.Black);
        private SizeF m_szTextSize = SizeF.Empty;
        private Font m_Font = new Font("Arial", 15);

        private string m_sText = "Das ist ein test";
        public CRectangle()
        {
            this.m_Random       = new Random(this.GetHashCode());
            this.m_Rectangle    = new Rectangle(this.Location, this.Size);
            int red             = m_Random.Next(0, 255);
            int green           = m_Random.Next(0, 255);
            int blue            = m_Random.Next(0, 255);
            this.m_Color        = Color.FromArgb(red, green, blue);
            this.m_ColorBrush   = new SolidBrush(this.m_Color);
        }

        public override Point Location
        {
            get{ return this.m_Rectangle.Location; }
            set{ this.m_Rectangle.Location = value; }
        }

        public override void Draw(Graphics g)
        {
            Rectangle rectShadow = new Rectangle(new Point(this.Location.X + 15, this.Location.Y + 15),this.Size);
            g.FillRectangle(m_Shadow, rectShadow);
            g.FillRectangle(m_ColorBrush, this.m_Rectangle);
            g.DrawRectangle(m_BorderPen, this.m_Rectangle);

            if (m_szTextSize.IsEmpty)
            {
                m_szTextSize = g.MeasureString(this.m_sText, this.m_Font);
            }
            g.DrawString(this.m_sText, this.m_Font, this.m_BlackBrush, new PointF(this.Location.X + this.Size.Width / 2 - m_szTextSize.Width / 2, this.Location.Y + this.Size.Height / 2 - m_szTextSize.Height / 2));
        }

        public Rectangle Rectangle
        {
            get { return this.m_Rectangle; }
        }

        public bool Hit(Point pt)
        {
            return this.m_Rectangle.Contains(pt);
        }


        public Color Color
        {
            get { return this.m_Color; }
            set { this.m_Color = value; }
        }
    }
}

PS: Weitere Hinweise gibt es in "Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte und Schnelle GDI(+) Grafik - wie? [Parallax Scrolling].

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren

Ich habe hier mal das Testprojekt angehängt.

Wichtig ist in diesem Falle nicht, die Interna von CRectangle oder CShape wesentlich zu verändern, sondern vorrangig zu prüfen, was muss neu gezeichnet werden und wann.

Hupps. Die Stoppuhr muss ja noch resetted werden.

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren

Mal so zwischendrin:

Nein, es muss niemand hierauf antworten. Ich werde hier einfach versuchen mit der Zeit alles zu optimieren und hier meine Ergebnisse aufzuschreiben.

Erwähnenswert wäre vielleicht noch, dass in die Zeichenroutine eine Stopwatch eingebaut ist. Wer möchte kann die Messdaten auch eine Datei schreiben.

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

1.373 Beiträge seit 2004
vor 17 Jahren

Hallo,

Ich habe mich zwar nicht im Detail mit deiner Implementierung beschäftigt, aber ich möchte darauf hinweisen, dass es sehr gute bestehende Techniken gibt, effizient nicht sichtbare Teile beim zeichnen auszuschließen.

Dieses Konzept wird im Snippet "Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte von ErfinderDesRades umgesetzt.

Als weiteres Beispiel so etwas umzusetzen sein hier Quadtrees genannt.

Viele Grüße,
Andre

4.506 Beiträge seit 2004
vor 17 Jahren

Hallo dr4g0n76,

Du verwendest in Deiner Zeichenroutine

public override void Draw(Graphics g)

noch viel zu viel Rechenkram (meiner Meinung nach). Folgende Instruktionen würde ich auslagern, und nur bei Bedarf rechnen lassen:

Rectangle rectShadow = new Rectangle(new Point(this.Location.X + 15, this.Location.Y + 15),this.Size);

-> nur bei Größenänderungen rechnen lassen

if (m_szTextSize.IsEmpty)
            {
                m_szTextSize = g.MeasureString(this.m_sText, this.m_Font);
            }

-> Nur bei Textänderungen berechnen lassen

new PointF(this.Location.X + this.Size.Width / 2 - m_szTextSize.Width / 2, this.Location.Y + this.Size.Height / 2 - m_szTextSize.Height / 2)

-> Ebenfalls nur bei Größenänderung rechnen lassen.

Dann hättest Du in Deiner Zeichenroutine im Wesentlichen nur Draw Anweisungen, so wenig wie möglich new, und so wenig Berechnungen wie möglich zu tun.

Aber das ist nur meine Persönliche Meinung (bzw. Erfahrung). Eventuell lassen sich feste Bereiche (Rectangles) definieren, die dann mittels Graphics.SetClip() und mittels Graphics.ResetClip() in den Grafikoperationen einsetzen lassen. -> Aber Quadtrees (hab ich mal in grafische Datenverarbeitung gehört) sind deutlich eleganter, und wenn ichs richtig verstanden hab, auch ein wenig komplizierter. Einfache Dinge lassen sich aber sicherlich mit Rectangle-Bereiche auch "begrenzen".

Viele Grüße
Norman-Timo

A: “Wie ist denn das Wetter bei euch?”
B: “Caps Lock.”
A: “Hä?”
B: “Na ja, Shift ohne Ende!”

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren

@norman_timo:

Ja, damit hast Du recht. Aber ich hatte zu Beginn noch viel mehr Berechnungen drin.

Zum Beispiel das Berechnen der Textabmessungen.
Oder die Brushes: Color color = new Color(Color.Irgendwas).

Das Auslagern der meisten Berechnungen hat schon einiges gebracht.

Ich kam aber gestern auf eine ganz andere Idee.

Und zwar:

Es wird quasi ein unsichtbares Raster (Rechtecke) über den Bildschirm gelegt.
Diese können in einem List<Rectangle> -Variable-Array gespeichert werden.

In dem Quadrant in dem sich die Maus befindet wird neu gezeichnet.

QuadrantIndex = BerechnetAusMausKoordinaten(ptMouse);

List<Rectangle> invRectQuad
QuadrantRect = List[QuadrantIndex];
Invalidate(QuadrantRect)

Das macht bei mir ziemlich genau Faktor 10 aus und dieser wäre absolut ausreichend.

Wenn nur der verschobene Bereich neu gezeichnet wird, funktioniert das nicht.

Sprich: im konkreten Fall nur der Bereich des Rechtecks das verschoben wird.

Fällt jemanden noch was anderes ein?

Bin für jeden Vorschlag dankbar.

Eine andere Möglichkeit wäre noch zu überprüfen, ob Objekte sich verdecken.
Denn was macht es für einen Sinn ein Objekt zu zeichnen, das eh nicht angezeigt werden kann, weil es durch ein anderes verdeckt wird?

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren
QuadInvalidate v0.9

Hier die erste Version des Quadinvalidate.

Wie man sieht wird bei überspringen eines Quadranten nicht alles korrekt neu gezeichnet.

Was wird hier gemacht? Wie vorhin beschrieben wurde der Index des upzudatenden Quadranten bestimmt.

Hat aber den Nachteil wenn das zu bewegende grafische Element größer ist als eines der Quadrate müssen alle Quadrate in denen das Element sich befindet neu gezeichnet werden. Dies resultiert in Grafikfehlern und das Update funktioniert auch überhaupt nicht so wie gedacht.

Jetzt wird deswegen überprüft, in welchen upzudatenden Quadranten sich das/die zu bewegende(n) Element(e) befindet(befinden).

Funktioniert nicht schlecht. Aber es gibt immer noch Darstellungsfehler. Warum?

Ich denke das ist ganz einfach: weil das Element z.B. nach oben bewegt wird.
Sobald es die unteren Quadrate verlassen hat, werden diese nicht mehr gezeichnet. obwohl dies vielleicht passieren sollte.

Ich denke jetzt wäre es wichtig zu wissen, in welchen Quadraten sich das Element befunden hat um diese zusätzlich neu zu zeichnen. Dies sollte das Problem lösen.

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren
QuadInvalidate v1.0

Jetzt funktioniert der Algorithmus.

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren
QuadInvalidate v1.1

Ich habe den Algorithmus jetzt extrahiert. Ein Aufruf von z.B.


this.m_QuadInvalidator.Invalidate(this, this.m_Rectangle.Rectangle);

reicht,

um im


protected override void OnMouseMove(MouseEventArgs e)
{
        base.OnMouseMove(e);
        this.m_Rectangle.Location = e.Location;
        this.m_QuadInvalidator.Invalidate(this, this.m_Rectangle.Rectangle);
}

ein optimierteres Zeichnen hinzubekommen.

Bei mir lassen sich jetzt ohne große Zeitverzögerung bis zu 3000 Objekte zeichnen.
Vorausgesetzt das Programm läuft im Debug-Mode außerhalb des Debuggers.

Ach ja, der Invalidator muss mit z.B.


CQuadInvalidator m_QuadInvalidator = new CQuadInvalidator(10,150,150);

initialisiert werden.

10 gibt dabei die Dimension an (10 Rechtecke je X- und Y-Richtung).
Jedes dabei 150 lang und 150 hoch.

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren
Quadtree

Das unten hinzugefügte Projekt zeigt wie ein Quadtree funktionieren würde, in diesem Fall wird rekursiv das zu zeichnende/bewegende Objekt eingegrenzt, bis es gut genug aufgelöst wird.

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren
Versteckte Elemente

Hiermit können versteckte Elemente untersucht werden:

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.

dr4g0n76 Themenstarter:in
2.921 Beiträge seit 2005
vor 17 Jahren

Dieses Artikel erhält nächste Woche ein Upgrade. War ja eigentlich zuerst nie als Artikel gedacht. 😉

edit: Nächste Woche? schon lange her. Muss halt jetzt sagen: When it's done...

Seit der Erkenntnis, dass der Mensch eine Nachricht ist, erweist sich seine körperliche Existenzform als überflüssig.