myCSharp.de - DIE C# und .NET Community (https://www.mycsharp.de/wbb2/index.php)
- Knowledge Base (https://www.mycsharp.de/wbb2/board.php?boardid=68)
-- Artikel (https://www.mycsharp.de/wbb2/board.php?boardid=69)
--- [Artikel] Zeichnen Optimieren / Schnelles zeichnen (https://www.mycsharp.de/wbb2/thread.php?threadid=28527)


Geschrieben von dr4g0n76 am 21.11.2006 um 23:15:
  [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.

C#-Code:
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:

C#-Code:
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].


Geschrieben von dr4g0n76 am 22.11.2006 um 02:05:
 
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.


Geschrieben von dr4g0n76 am 22.11.2006 um 02:10:
 
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.


Geschrieben von VizOne am 22.11.2006 um 08:38:
 
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


Geschrieben von norman_timo am 22.11.2006 um 10:07:
 
Hallo dr4g0n76,

Du verwendest in Deiner Zeichenroutine

C#-Code:
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:

C#-Code:
Rectangle rectShadow = new Rectangle(new Point(this.Location.X + 15, this.Location.Y + 15),this.Size);

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

C#-Code:
if (m_szTextSize.IsEmpty)
            {
                m_szTextSize = g.MeasureString(this.m_sText, this.m_Font);
            }

-> Nur bei Textänderungen berechnen lassen

C#-Code:
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


Geschrieben von dr4g0n76 am 22.11.2006 um 12:09:
 
@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?


Geschrieben von dr4g0n76 am 22.11.2006 um 14:31:
  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.


Geschrieben von dr4g0n76 am 22.11.2006 um 15:44:
  QuadInvalidate v1.0
Jetzt funktioniert der Algorithmus.


Geschrieben von dr4g0n76 am 22.11.2006 um 16:37:
  QuadInvalidate v1.1
Ich habe den Algorithmus jetzt extrahiert. Ein Aufruf von z.B.

C#-Code:
this.m_QuadInvalidator.Invalidate(this, this.m_Rectangle.Rectangle);

reicht,

um im

C#-Code:
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.

C#-Code:
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.


Geschrieben von dr4g0n76 am 22.11.2006 um 16:58:
  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.


Geschrieben von dr4g0n76 am 24.11.2006 um 19:40:
  Versteckte Elemente
Hiermit können versteckte Elemente untersucht werden:


Geschrieben von dr4g0n76 am 24.11.2006 um 19:40:
 
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...


© Copyright 2003-2020 myCSharp.de-Team | Impressum | Datenschutz | Alle Rechte vorbehalten. | Dieses Portal verwendet zum korrekten Betrieb Cookies. 29.02.2020 10:06