2D-Sprites effizient mit OpenGL rendern



  • Also, ich möchte für ein Spiel 2D-Sprites (mit oder ohne Alphakanal) möglichst effizient mit OpenGL rendern, habe aber von OpenGL noch wenig Ahnung und wollte fragen, wie ich das am besten anstelle.

    Mein erster Ansatz war, für jedes Sprite temporäre VBOs mit jeweils Daten für genau ein Quad zu erstellen, glDrawArrays aufrufen und die VBOs wieder zerstören.
    Das funktioniert, ist aber ziemlich langsam.

    Man sagt, dass man mit Drawcalls sparsam umgehen sollte, also war meine nächste Idee, die ganzen draw()s nicht sofort auszuführen, sondern die Koordinaten in einer pro-Textur-Liste zwischenzuspeichern und erst am Ende abzuarbeiten. Damit kann ich dann mehrere Quads mit der gleichen Textur an verschiedenen Stellen am Bildschirm gleichzeitig rendern. Damit später in Auftrag gegebene Sprites nicht von anderen verdeckt werden können, korrigiere ich den Z-Wert nach jedem draw() etwas nach unten.

    Das ist viel flotter und ich habe es für einen Schritt in die richtige Richtung gehalten - bis mir aufgefallen ist, dass wenn ein teilweise transparentes Sprite, das weiter "oben" sein sollte, zuerst gerendert wird, alles dahinter überhaupt nicht mehr angezeigt wird. Ist ja auch zu erwarten, schließlich haben die Sprites hinten einen höheren Z-Wert.

    Gibt es eine Lösung für das Problem? Oder ganz andere Ansätze, die größeren Erfolg versprechen?



  • Wenn es bei dir ein Transparenz-Konzept gibt, dann gibt es auch ein "über" bzw. "unter". Sortiere deine Sprites danach und zeichne von unten nach oben, dann klappt das mit der Transparenz auch. (Und dann kannste den Depth-Test auch gleich deaktivieren.)

    ;2D schrieb:

    und die VBOs wieder zerstören.

    😮 why? 😕
    Ansonsten für optimale Performance braucht man mehr Informationen. Hast du für den Hintergrund z.B. eine Tilemap? Sind deine Sprites alle gleich groß? Sind sie animiert?



  • cooky451 schrieb:

    ;2D schrieb:

    und die VBOs wieder zerstören.

    😮 why? 😕

    Soll ich nicht? Ich dachte, da die Buffer sowieso jedes Mal mit neuen Daten befüllt werden müssen, macht das Anlegen/Zerstören wahrscheinlich auch nicht mehr viel aus. Einer meiner Probleme ist im Moment, dass ich keine Ahnung habe, wie teuer verschiedene OpenGL-Operationen sind.

    cooky451 schrieb:

    Sortiere deine Sprites danach und zeichne von unten nach oben, dann klappt das mit der Transparenz auch. (Und dann kannste den Depth-Test auch gleich deaktivieren.)

    Wie kann man das aber mit den verschiedenen Listen vereinen? Ich zeichne ja erst die Liste für Textur 1, dann für Textur 2 usw. Innerhalb einer einzelnen Liste sind sie schon nach Z sortiert (garantiert mir OpenGL eigentlich, dass Dreiecke weiter vorne im Buffer zuerst gezeichnet werden?), aber insgesamt nicht. Die einzige Lösung, die mir spontan einfällt, wäre nur eine einzige Textur zu haben, ist aber auch nicht praktikabel.

    Hast du für den Hintergrund z.B. eine Tilemap?

    Im Moment schon. Idealerweise würde ich aber gerne bei etwas ankommen, das auch für den "allgemeinen Fall" gut funktioniert, so dass ich den Code für das nächste Spiel unbesehen übernehmen kann, auch wenn das dann vielleicht keine Tilemaps benutzt.

    Sind deine Sprites alle gleich groß?

    Nein, die meisten haben unterschiedliche Größen.

    Sind sie animiert?

    Im Moment nur die Spielfigur, das soll sich aber bald ändern 🙂



  • Nun, wenn du Performance willst, musst du schon etwas Arbeit rein stecken. Für den Hintergrund würde ich z.B. empfehlen einen einzigen VBO mit allen Bildschirm/Textur-Koords zu erstellen und den einfach abhängig von der Position zu rendern. Solange deine Level nicht extrem groß sind (da muss man sich dann was überlegen, ist aber eher unüblich), sollte das ziemlich effizient sein, weil nur ein Draw-Call. Dann ist der Hintergrund schon mal weg, wird als erstes gezeichnet. Dann brauche wahrscheinlich noch andere Objekte. Nun, ich kann dir jetzt leider keine Patentlösung anbieten, aber ich denke du solltest versuchen VBOs so selten wie möglich zu löschen, und am besten auch relativ selten zu verändern. (Denk dran je nach dem mit _DRAW/_STREAM anzulegen.) Ich denke wenn du die Position relativ von einer uniform machst (was man mit "echten" Objekten im 3D-Raum ja eh macht), sollte das wesentlich effizienter sein, als dauernd VBOs auszutauschen. Ich hoffe das hilft dir etwas. Wie du das mit den Listen machst musst du sehen, gesagt sei aber dass es durchaus performant sein kann jeden Frame eine neue (sortierte) Liste für alles anzulegen, falls das möglich ist.
    Und Instancing solltest du dir auch mal angucken, damit geht viel bei sowas.
    Und Geometry-Shader könnten auch hilfreich sein, aber das würde ich nicht machen wenn es nicht nur um den Spaß an der Sache geht, ich denke wenn du das oben relativ gut hinbekommst, sollten ~300 FPS mit einer GTX 460 schon recht locker drin sein. 😉



  • cooky451 schrieb:

    Nun, wenn du Performance willst, musst du schon etwas Arbeit rein stecken. Für den Hintergrund würde ich z.B. empfehlen einen einzigen VBO mit allen Bildschirm/Textur-Koords zu erstellen und den einfach abhängig von der Position zu rendern.

    Ah, auf die Idee mit einer Uniform für die Kameraposition bin ich gar nicht gekommen. Für den Boden wäre das eine gute Lösung, aber für z.B. Wände wohl schon nicht mehr, weil die ja bewegliche Objekte verdecken könnten, die dann schon vorher gezeichnet werden müssten.

    Wie du das mit den Listen machst musst du sehen, gesagt sei aber dass es durchaus performant sein kann jeden Frame eine neue (sortierte) Liste für alles anzulegen, falls das möglich ist.

    Nichts einfacher als das, denn ich möchte die Sprites genau in der Reihenfolge gerendert haben, wie ich draw() aufgerufen habe. Das einzige Problem ist, dass ich immer wieder die Textur zwischendrin ändern müsste.

    Und Instancing solltest du dir auch mal angucken, damit geht viel bei sowas.

    Kurz habe ich mir es schon angeschaut, aber mit fehlt noch irgendwie der Aha-Effekt... das sieht zunächst wie das gleiche in grün aus. Normalerweise füttert mir OpenGL für jedes Dreieck die jeweiligen Werte aus den VBOs, bei Instancing bekomme ich dann stattdessen eine Instance-ID und muss mir dann irgendwie selbst die Daten herschaffen (vermutlich aus UBOs?)? Hat das Vorteile?

    Wenn ich es mir so überlege, würde es mir auch schon viel nützen, wenn ich mehrere Texturen im Shader zur Verfügung hätte und die über Index dynamisch ansprechen könnte. Geht das 😕



  • Natürlich kannst du mehrere Texturen auf einmal binden. Ich glaube OpenGL 4 garantiert dir bis zu 80 Texturen die man gleichzeitig benutzen kann. http://www.opengl.org/sdk/docs/man/xhtml/glActiveTexture.xml

    ;2D schrieb:

    aber für z.B. Wände wohl schon nicht mehr, weil die ja bewegliche Objekte verdecken könnten, die dann schon vorher gezeichnet werden müssten.

    Hmjo, da geht das nicht so leicht. Trotzdem kannst du eigentlich für alles eine Kameraposition + Objektposition in den Shader schieben (uniform), dann musst du die VBOs gar nicht mehr anfassen. Das ist sicherlich effizienter.

    ;2D schrieb:

    Das einzige Problem ist, dass ich immer wieder die Textur zwischendrin ändern müsste.

    Das könnte ein Problem sein. Aber wenn es bei dir eh ein "über" und ein "unter" gibt stellt sich die Frage wie und wo du das speicherst - denn letztlich kannst du da natürlich auch einfach die "tiefe" als Z-Koordinate angeben und den Depth-Puffer alles machen lassen. (Denk dran, wieder also uniform! Also vec3 position statt vec2 position.)

    ;2D schrieb:

    Kurz habe ich mir es schon angeschaut, aber mit fehlt noch irgendwie der Aha-Effekt

    Du hast bis jetzt ja auch die VBOs statt Uniforms verändert, da wundert mich das nicht. 😃 Also, halten wir mal fest: VBOs für so etwas ändern => Doofe Idee. Man steckt also die Position des Objekts in den Shader (als Uniform) und rechnet die entgültigen Positionen der Vertexe im Shader aus. Wenn du jetzt z.B. 16 Tauben zeichnen willst, heißt das, dass du 16 mal die Position einer Taube im Shader setzen, und dann einen Draw-Call machen musst. Das ist, insbesondere wenn das Tauben-Modell sehr simpel ist, extrem ineffizient. Wenn du jetzt allerdings ein Array im Shader hättest, in das du die Positionen aller 16 Tauben auf einmal schreibst, und dann alles mit eine Draw-Call renderst, wäre das schon wesentlich besser. (Nur so zum Vergleich, das ist natürlich ein Extrem: Damit bin ich bei mir von ~2.5k Würfeln mit 30FPS auf ~25k Würfel mit ~75 FPS gekommen, und ich glaube später ging da sogar noch mehr.)
    Wie genau du die Positionsdaten mehrer Objekte auf einmal in den Shader bekommst, ist nicht genau festgelegt. Dafür gibt es mehrere Möglichkeiten, aber ich denke einfach ein 8-32 großes Array dafür zu nehmen und dann halt immer x-viele Objekte auf einmal zu zeichnen sollte für dich schon locker ausreichen.


Anmelden zum Antworten