Zeichnen Sie viele Kacheln mit OpenGL auf moderne Weise

35

Ich arbeite mit einem Team an einem kleinen Kachel- / Sprite-basierten PC-Spiel, und es treten Leistungsprobleme auf. Das letzte Mal, dass ich OpenGL verwendet habe, war um 2004, also habe ich mir selbst beigebracht, wie man das Kernprofil verwendet, und ich bin ein wenig verwirrt.

Ich muss in der Nähe von 250-750 48x48 Kacheln jeden Frame auf den Bildschirm zeichnen, sowie vielleicht rund 50 Sprites. Die Kacheln ändern sich nur, wenn ein neues Level geladen wird, und die Sprites ändern sich ständig. Einige der Kacheln bestehen aus vier 24x24-Teilen, und die meisten (aber nicht alle) Sprites haben die gleiche Größe wie die Kacheln. Viele der Kacheln und Sprites verwenden Alpha-Blending.

Im Moment mache ich das alles im Sofortmodus, von dem ich weiß, dass es eine schlechte Idee ist. Trotzdem, wenn eines unserer Teammitglieder versucht, es auszuführen, erhält es sehr schlechte Bildraten (~ 20-30 fps), und es ist viel schlimmer, wenn es mehr Kacheln gibt, besonders wenn viele dieser Kacheln von der Art sind, wie sie sind werden in Stücke geschnitten. Das alles lässt mich glauben, dass das Problem in der Anzahl der getätigten Draw Calls liegt.

Ich habe mir ein paar mögliche Lösungen ausgedacht, aber ich wollte sie von einigen Leuten leiten lassen, die wissen, wovon sie sprechen, damit ich meine Zeit nicht auf etwas Dummes verschwenden kann:

FLIESEN:

  1. Wenn ein Level geladen ist, ziehe alle Kacheln einmal in einen Framebuffer, der mit einer großen Huptextur verbunden ist, und zeichne einfach in jedem Frame ein großes Rechteck mit dieser Textur.
  2. Legen Sie alle Kacheln beim Laden des Levels in einen statischen Vertex-Puffer und zeichnen Sie sie auf diese Weise. Ich weiß nicht, ob es eine Möglichkeit gibt, Objekte mit unterschiedlichen Texturen mit einem einzigen Aufruf von glDrawElements zu zeichnen, oder ob ich das überhaupt tun möchte. Vielleicht einfach alle Kacheln in eine riesige Textur legen und lustige Texturkoordinaten in der VBO verwenden?

SPRITES:

  1. Zeichnen Sie jedes Sprite mit einem separaten Aufruf von glDrawElements. Dies scheint eine Menge Texturwechsel mit sich zu bringen, von denen mir gesagt wurde, dass sie schlecht sind. Sind Texturarrays hier vielleicht nützlich?
  2. Benutze irgendwie einen dynamischen VBO. Gleiche Texturfrage wie Nummer 2 oben.
  3. Sprites zeigen? Das ist wahrscheinlich albern.

Ist eine dieser Ideen sinnvoll? Gibt es eine gute Implementierung, über die ich nachsehen könnte?

Nic
quelle
Wenn sich Kacheln weder bewegen noch ändern und im gesamten Level gleich aussehen, sollten Sie zunächst den Frame Buffer verwenden. Es wird am effizientesten sein.
Zacharmarz
Versuchen Sie es mit einem Texturatlas, damit Sie nicht die Textur wechseln müssen, sondern alles andere gleich bleibt. Wie ist ihre Framerate?
user253751

Antworten:

25

Die schnellste Methode zum Rendern der Kacheln besteht darin, die Eckendaten in ein statisches VBO mit Indizes zu packen (wie glDrawElements angibt). Das Schreiben in ein anderes Bild ist völlig unnötig und erfordert nur viel mehr Speicher. Textur Schalt ist sehr teuer, so dass Sie wahrscheinlich alle Fliesen in einen so genannten packen wollen Texture Atlas und jedes Dreieck geben in der VBO der richtigen Texturkoordinaten. Auf dieser Basis sollte es kein Problem sein, je nach Hardware 1000 oder sogar 100000 Kacheln zu rendern.

Der einzige Unterschied zwischen Tile-Rendering und Sprite-Rendering besteht wahrscheinlich darin, dass Sprites dynamisch sind. Um die beste und dennoch leicht zu erreichende Leistung zu erzielen, können Sie die Koordinaten für die Sprite-Scheitelpunkte einfach in ein Stream-Draw-VBO für jeden Frame einfügen und mit glDrawElements zeichnen. Packe auch alle Texturen in einen Texturatlas. Wenn sich Ihre Sprites nur selten bewegen, können Sie auch versuchen, ein dynamisches VBO zu erstellen und zu aktualisieren, wenn sich ein Sprite bewegt. Dies ist jedoch ein völliger Overkill, da Sie nur einige Sprites rendern möchten.

Sie können sich einen kleinen Prototyp ansehen, den ich in C ++ mit OpenGL: Particulate erstellt habe

Ich rendere ungefähr 10000 Punkt-Sprites mit einer durchschnittlichen Geschwindigkeit von 400 fps auf einem normalen Computer (Quad Core bei 2,66 GHz). Es ist CPU-begrenzt, was bedeutet, dass die Grafikkarte noch mehr rendern kann. Beachten Sie, dass ich hier keine Texturatlanten verwende, da ich nur eine einzige Textur für die Partikel habe. Die Partikel werden mit GL_POINTS gerendert und die Shader berechnen dann die tatsächliche Quad-Größe, aber ich denke, es gibt auch einen Quad-Renderer.

Oh, und ja, es sei denn, Sie haben ein Quadrat und verwenden Shader für das Textur-Mapping, dann ist GL_POINTS ziemlich albern. ;)

Marco
quelle
Die Sprites ändern ihre Position und die verwendete Textur, und die meisten von ihnen tun dies in jedem Frame. Auch Sprites und werden sehr oft erstellt und zerstört. Sind dies Dinge, die ein Stream Draw VBO verarbeiten kann?
Nic
2
Stream Draw bedeutet im Grunde: "Diese Daten an die Grafikkarte senden und nach dem Zeichnen verwerfen". Sie müssen die Daten also bei jedem Frame erneut senden, und das bedeutet, es spielt keine Rolle, wie viele Sprites Sie rendern, welche Position sie haben, welche Texturkoordinaten oder welche Farbe. Senden Sie jedoch alle Daten auf einmal und lassen Sie die GPU sie VIEL schneller verarbeiten als im Sofortmodus.
Marco
Das alles macht Sinn. Lohnt es sich, dafür einen Indexpuffer zu verwenden? Die einzigen Eckpunkte, die wiederholt werden, sind zwei Ecken aus jedem Rechteck, oder? (Meines Wissens sind Indizes der Unterschied zwischen glDrawElements und glDrawArrays. Stimmt das?)
Nic
1
Ohne Indizes können Sie GL_TRIANGLES nicht verwenden, was normalerweise schlecht ist, da diese Zeichenmethode die mit der garantiert besten Leistung ist. Außerdem ist die Implementierung von GL_QUADS in OpenGL 3.0 veraltet (Quelle: stackoverflow.com/questions/6644099/… ). Dreiecke sind das native Netz jeder Grafikkarte. Sie "verwenden" also 2 * 6 Byte mehr, um 2 Vertex-Shader-Ausführungen und vertex_size * 2 Byte zu speichern. Man kann also allgemein sagen, dass es IMMER besser ist.
Marco
2
Der Link zu Particulate ist tot. Könnten Sie bitte einen neuen bereitstellen?
SWdV
4

Selbst bei dieser Anzahl von Draw-Anrufen sollte die Leistung nicht abnehmen. Der unmittelbare Modus ist zwar langsam, aber nicht so langsam (als Referenz kann sogar das alte Quake mehrere tausend Anrufe im unmittelbaren Modus pro Frame verwalten, ohne zu fallen so schlecht runter).

Ich vermute, dass hier etwas Interessanteres vor sich geht. Das erste, was Sie tun müssen, ist, einige Zeit in die Profilerstellung Ihres Programms zu investieren. Andernfalls besteht ein enormes Risiko für eine Neugestaltung, basierend auf einer Annahme, die zu einem Leistungsgewinn von Null führen kann. Sehen Sie sich einmal so grundlegende Dinge wie GLIntercept an und finden Sie heraus, wohin Ihre Zeit geht. Basierend auf den Ergebnissen werden Sie in der Lage sein, das Problem mit einigen wirklichen Informationen über Ihre primären Engpässe anzugehen .

Maximus Minimus
quelle
Ich habe einige Profilerstellungen durchgeführt, obwohl dies umständlich ist, da die Leistungsprobleme nicht auf demselben Computer wie die Entwicklung auftreten. Ich bin etwas skeptisch, dass das Problem anderswo liegt, da die Probleme definitiv mit der Anzahl der Kacheln zunehmen und die Kacheln buchstäblich nichts anderes tun, als gezeichnet zu werden.
Nic
Wie wäre es dann mit Zustandsänderungen? Gruppieren Sie Ihre undurchsichtigen Kacheln nach Status?
Maximus Minimus
Das ist eine Möglichkeit. Dies verdient auf jeden Fall mehr Aufmerksamkeit von meiner Seite.
Nic
2

Okay, da meine letzte Antwort hier irgendwie außer Kontrolle geraten ist, handelt es sich um eine neue, die vielleicht nützlicher ist.


Über 2D-Performance

Zunächst einige allgemeine Ratschläge: 2D stellt keine Anforderungen an die aktuelle Hardware, selbst weitgehend nicht optimierter Code wird funktionieren. Das bedeutet jedoch nicht, dass Sie den Zwischenmodus verwenden sollten. Stellen Sie zumindest sicher, dass Sie den Status nicht ändern, wenn dies nicht erforderlich ist (binden Sie beispielsweise keine neue Textur mit glBindTexture, wenn dieselbe Textur bereits gebunden ist, und überprüfen Sie die CPU auf Tonnen schneller als ein glBindTexture-Aufruf) und nichts völlig Falsches und Dummes wie glVertex zu benutzen (sogar glDrawArrays werden viel schneller sein und sind nicht schwieriger zu benutzen, aber nicht sehr "modern"). Mit diesen beiden sehr einfachen Regeln sollte die Frame-Zeit mindestens 10 ms (100 fps) betragen. Um noch mehr Geschwindigkeit zu erreichen, ist der nächste logische Schritt das Batching, z. B. das Bündeln von beliebig vielen Draw-Aufrufen in einem. Hierzu sollten Sie Texturatlanten implementieren. So können Sie die Anzahl der Texturbindungen minimieren und somit die Anzahl der Rechtecke, die Sie mit einem Aufruf zeichnen können, auf eine große Menge erhöhen. Wenn du jetzt nicht auf ungefähr 2ms (500fps) bist, machst du etwas falsch :)


Karten kacheln

Durch die Implementierung des Zeichencodes für Kachelkarten wird das Gleichgewicht zwischen Flexibilität und Geschwindigkeit hergestellt. Sie können statische VBOs verwenden, aber das funktioniert nicht mit animierten Kacheln, oder Sie können einfach die Scheitelpunktdaten für jeden Frame generieren und die oben erläuterten Regeln anwenden. Das ist sehr flexibel, aber bei weitem nicht so schnell.

In meiner vorherigen Antwort hatte ich ein anderes Modell eingeführt, in dem der Fragment-Shader die gesamte Texturierung übernimmt. Es wurde jedoch darauf hingewiesen, dass dies eine abhängige Textur-Suche erfordert und daher möglicherweise nicht so schnell ist wie die anderen Methoden. (Die Idee ist im Grunde, dass Sie nur die Kachelindizes hochladen und im Fragment-Shader die Texturkoordinaten berechnen, was bedeutet, dass Sie die gesamte Karte mit nur einem Rechteck zeichnen können.)


Sprites

Sprites erfordern viel Flexibilität, was es sehr schwierig macht, sie zu optimieren, abgesehen von den im Abschnitt "Über 2D-Leistung" beschriebenen. Und wenn Sie nicht gleichzeitig zehntausende Sprites auf dem Bildschirm haben möchten, lohnt sich die Mühe wahrscheinlich nicht.

API-Biest
quelle
1
Und selbst wenn Sie zehntausende von Sprites haben, sollte moderne Hardware mit einer anständigen Geschwindigkeit laufen :)
Marco
@ API-Beast was warten? Wie berechnet man Textur-UVs im Fragment-Shader? Sollen Sie die UV-Strahlen nicht an den Fragment-Shader senden?
HgMerk
0

Wenn alle Stricke reißen...

Richten Sie eine Flip-Flop-Zeichenmethode ein. Aktualisieren Sie jeweils nur jedes andere Sprite. Selbst mit VisualBasic6 und einfachen Bit-Blit-Methoden können Sie Tausende von Sprites pro Frame aktiv zeichnen. Vielleicht sollten Sie sich diese Methoden genauer ansehen, da Ihre direkte Methode, nur Sprites zu zeichnen, anscheinend fehlschlägt. (Klingt eher so, als würden Sie eine "Rendermethode" verwenden, aber versuchen, sie wie eine "Spielemethode" zu verwenden. Beim Rendern geht es um Klarheit, nicht um Geschwindigkeit.)

Es besteht die Möglichkeit, dass Sie ständig den gesamten Bildschirm neu zeichnen. Anstatt nur die geänderten Bereiche neu zu zeichnen. Das ist eine Menge Aufwand. Das Konzept ist einfach, aber nicht leicht zu verstehen.

Verwenden Sie einen Puffer für den jungfräulichen statischen Hintergrund. Dies wird niemals selbst gerendert, es sei denn, auf dem Bildschirm befinden sich keine Sprites. Dies wird ständig verwendet, um "zurückzusetzen", wo ein Sprite gezeichnet wurde, um das Sprite beim nächsten Aufruf zu entzeichnen. Sie benötigen auch einen Puffer zum "Zeichnen", bei dem es sich nicht um den Bildschirm handelt. Sie zeichnen dort, und wenn Sie alle gezeichnet haben, klappen Sie das Bild einmal auf den Bildschirm. Das sollte ein Screen-Call für alle deine Sprites sein. (Im Gegensatz dazu, jedes Sprite einzeln auf dem Bildschirm zu zeichnen oder zu versuchen, alles auf einmal zu machen, schlägt die Alpha-Überblendung fehl.) Das Schreiben in den Speicher ist schnell und erfordert keine Bildschirmzeit zum Zeichnen ". Jeder Draw-Call wartet auf ein Return-Signal, bevor er erneut versucht, zu zeichnen. (Kein V-Sync, ein tatsächlicher Hardware-Tick, der viel langsamer ist als die Wartezeit, die der RAM hat.)

Ich stelle mir vor, dass dies ein Grund dafür ist, dass Sie dieses Problem nur auf einem Computer sehen. Oder es wird auf das Software-Rendering von ALPHA-BLEND zurückgegriffen, das nicht von allen Karten unterstützt wird. Prüfen Sie, ob diese Funktion von der Hardware unterstützt wird, bevor Sie versuchen, sie zu verwenden? Haben Sie ein Fallback (Nicht-Alpha-Mischmodus), wenn sie es nicht haben? Offensichtlich haben Sie keinen Code, der die Grenzen (Anzahl der gemischten Dinge) festlegt, da ich davon ausgehe, dass dies Ihren Spielinhalt beeinträchtigen würde. (Im Gegensatz dazu, wenn es sich nur um Partikeleffekte handelt, die alle Alpha-gemischt sind, und daher, warum Programmierer sie einschränken, da sie auf den meisten Systemen selbst mit Hardware-Unterstützung eine hohe Belastung darstellen.)

Zuletzt würde ich vorschlagen, das, was Sie Alpha-Blending sind, nur auf Dinge zu beschränken, die es brauchen. Wenn alles es braucht ... Sie haben keine andere Wahl, als von Ihren Benutzern bessere Hardwareanforderungen zu fordern oder das Spiel für die gewünschte Leistung zu verschlechtern.

JasonD
quelle
-1

Erstellen Sie ein Sprite-Blatt für Objekte und ein Geländeset wie in anderen 2D-Spielen. Es ist nicht erforderlich, die Texturen zu wechseln.

Das Rendern von Kacheln kann schmerzhaft sein, da jedes Dreieckspaar seine eigenen Texturkoordinaten benötigt. Für dieses Problem gibt es eine Lösung, die als instanziiertes Rendern bezeichnet wird .

Solange Sie Ihre Daten so sortieren können, dass Sie beispielsweise eine Liste mit Grasplättchen und deren Positionen haben, können Sie jedes Grasplättchen mit einem einzigen Zeichenaufruf rendern. Sie müssen lediglich ein Array bereitstellen von Modell zu Weltmatrizen für jedes Plättchen. Das Sortieren Ihrer Daten auf diese Weise sollte auch mit dem einfachsten Szenendiagramm kein Problem sein.

dreta
quelle
-1: Instanzen sind eine schlechtere Idee als die reine Shader-Lösung von Mr. Beast. Das Instanziieren ist für die Leistung am besten geeignet, wenn Objekte mit mittlerer Komplexität (etwa 100 Dreiecke) gerendert werden. Jede Dreieckskachel, die Texturkoordinaten benötigt, ist kein Problem. Sie erstellen einfach ein Netz mit einer Reihe loser Quads, die zufällig eine Tilemap bilden.
Nicol Bolas
1
@NicolBolas in Ordnung, ich werde die Antwort für das Lernen verlassen
dreta
1
Aus Gründen der Klarheit, Nicol Bolas, was ist Ihr Vorschlag, wie Sie mit all dem umgehen sollen? Marcos Stream-Draw-Ding? Kann ich irgendwo eine Implementierung davon sehen?
Nic
@Nic: Streaming zum Puffern von Objekten ist kein besonders komplexer Code. Aber wirklich, wenn Sie nur über 50 Trotz reden, ist das nichts . Die Chancen stehen gut, dass es Ihre Geländezeichnung ist, die das Leistungsproblem verursacht hat. Daher wäre ein Wechsel zu statischen Puffern wahrscheinlich gut genug.
Nicol Bolas
Wenn die Instanzen so funktionieren würden, wie wir es uns vorstellen, wäre dies die beste Lösung - aber da dies nicht der Fall ist, ist es der richtige Weg, alle Instanzen in eine einzige statische VBO zu backen.
Jari Komppa