Ich frage mich, wie das Zeichnen einfacher Geometrie mit Texturen so viel Leistung verschlingen kann (unter 60 fps). Selbst meine gute Grafikkarte (GTX 960) kann "nur" problemlos bis zu 1000 Sprites zeichnen. Die Texturen, die ich verwende, bestehen alle aus 2 Texturen und dürfen eine Größe von 512 x 512 nicht überschreiten. Ich filtere sogar GL_NEAREST
nur mit.
Die Sprites selbst sind zufällig in der Größe generiert. Es gibt also keine 1000 Vollbild-Quads, was kein wirklicher Anwendungsfall wäre.
Ich zeichne meine Sprites gestapelt, was bedeutet, dass ich einen dynamischen Scheitelpunktpuffer und einen statischen Indexpuffer habe. Ich aktualisiere den Scheitelpunktpuffer jedes Bild mit glBufferSubData
einmal und zeichne dann alles mit `` glDrawElements`. Ich habe ungefähr 5 verschiedene Texturen, die ich einmal pro Frame binde, was zu 5 Draw Calls führt. Zum Rendern verwende ich nur einen Shader, der gebunden ist, sobald die Anwendung gestartet wird.
Ich habe also 5 Texturbindungen, 5 Zeichenaufrufe und ein Vertex-Puffer-Update pro Frame, was nicht wirklich so viel ist.
Hier ist ein Beispiel mit einer Textur:
val shaderProgram = ShaderProgram("assets/default.vert", "assets/default.frag")
val texture = Texture("assets/logo.png")
val sprite = BufferSprite(texture)
val batch = BufferSpriteBatch()
val projView = Matrix4f().identity().ortho2D(0f, 640f, 0f, 480f)
fun setup() {
glEnable(GL_TEXTURE)
//glColorMask(true, true, true, true)
//glDepthMask(false)
glUseProgram(shaderProgram.program)
texture.bind()
batch.begin()
for(i in 1..1000)
batch.draw(sprite)
batch.update()
}
fun render() {
glClear(GL_COLOR_BUFFER_BIT)
stackPush().use { stack ->
val mat = stack.mallocFloat(16)
projView.get(mat)
val loc = glGetUniformLocation(shaderProgram.program, "u_projView")
glUniformMatrix4fv(loc, false, mat)
batch.flush()
}
}
Die batch.draw()
Methode legt die Scheitelpunktdaten der Sprites in einem CPU-seitigen Puffer ab und batch.update()
lädt alles mit auf die GPU hoch glBufferSubData
. Das Einrichten des Spritebatch sieht folgendermaßen aus:
glBindBuffer(GL_ARRAY_BUFFER, tmpVbo)
glBufferData(GL_ARRAY_BUFFER, vertexData, GL_STATIC_DRAW)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glEnableVertexAttribArray(2)
glVertexAttribPointer(0, 2, GL_FLOAT, false, 24 * sizeof(Float), 0)
glVertexAttribPointer(1, 4, GL_FLOAT, false, 24 * sizeof(Float), 2.toLong() * sizeof(Float))
glVertexAttribPointer(2, 2, GL_FLOAT, false, 24 * sizeof(Float), 6.toLong() * sizeof(Float))
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, tmpEbo)
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW)
Ich habe zuerst mein Programm profiliert, aber das Aktualisieren der Scheitelpunktpuffer und der gesamten Geometrie dauert ungefähr 10% der Gesamtzeit pro Frame. Das Austauschen der Puffer nimmt jedoch den Rest der 90% -Rahmenzeit in Anspruch.
Ich frage mich also, wie können so große AAA-Spiele ihre Szenen mit Millionen von Eckpunkten rendern, wenn das Zeichnen von Pixeln eine so zeitaufwändige Aufgabe ist? Ich weiß, dass es viele Optimierungen im Code gibt, aber immer noch.
Antworten:
Ihre GPU kann wahrscheinlich sogar 100.000 Sprites ohne Probleme rendern, aber Sie müssen es intelligent machen. Sprites und andere Geometrien müssen einer GPU in Stapeln bereitgestellt werden, die nach derselben Textur, demselben Shader und demselben Mischmodus gruppiert sind.
Große AAA-Spiele minimieren die an die GPU ausgegebenen Draw Calls. Zeichenaufrufe sind normalerweise teuer , daher werden viele ähnliche Zeichenvorgänge zusammengefasst und stapelweise an die GPU gesendet. Jede neue Änderung des Shader-, Textur- oder Mischmodus während des Renderns führt zu einem separaten Zeichenaufruf. Außerdem werden Texturatlanten verwendet, um Zeichnungsaufrufe zu reduzieren (viele Bilder auf einer einzelnen Textur).
quelle
Die Art und Weise, wie Sie Ihre Sprites stapeln, ist möglicherweise nicht optimal. Wenn Sie
glDrawElements()
einen Stapel mehrerer Sprites rendern, kann dies nur bedeuten, dass Sie 4 Scheitelpunkte pro Quad in Ihrem VBO speichern (andernfalls sehe ich nicht, wieglDrawElements()
allein mehrere Sprites gleichzeitig gerendert werden können. Ich könnte mich irren, in In diesem Fall können Sie mich gerne korrigieren.Die richtige Lösung hängt auch von Ihrem Anwendungsfall ab - es ist nicht unbedingt dasselbe für ein Partikelsystem im Vergleich zum allgemeinen 2D-Rendering für ein Spiel.
Die Sache ist: Wir brauchen keine Indizes und wir müssen auch nicht 4 Scheitelpunktpositionen pro Quad speichern.
Durch die Verwendung von weniger Speicher würden wir die Datenmenge reduzieren, die pro Frame aktualisiert werden soll, und die Anzahl der Speicherzugriffe verringern, die als langsam angenommen werden sollten.
Was ich tun würde, ist instanziiertes Rendern .
Grundsätzlich kann Ihr Problem als Rendern eines einzelnen Quad-Netzes beschrieben werden, jedoch 1000-mal mit jeweils unterschiedlichen Einstellungen (einschließlich Transformationen und erforderlichen Informationen für Textur-Lookups).
Wenn Sie wissen, dass Ihre Quads immer zum Bildschirm zeigen, können Sie es sich sogar leisten, weniger Informationen an die GPU zu senden (z. B. Positionen wie vec2, Rotationen als einzelner Schwimmer usw.).
Hier ist ein sehr sehr grober Pseudocode für das instanziierte Rendern. Ausführliche Tutorials und Erklärungen zu dieser Technik sind weit verbreitet, und ich empfehle dringend, einige nachzuschlagen.
Eine andere Technik, die Sie untersuchen könnten, ist Point Sprites .
Bei Punkt-Sprites wird ein einzelner Scheitelpunkt pro Sprite "gezeichnet". Jeder Scheitelpunkt wird dann zu einem Bildschirmraum-Quad erweitert, und Sie können sein Erscheinungsbild mithilfe des Fragment-Shaders anpassen, indem Sie normalisierte Koordinaten innerhalb dieses Quad geben (z. B. Textur-Lookups durchführen).
Die Größe des Bildschirmraum-Quad kann im Vertex-Shader (wo Sie ihn auch durch z teilen können) beschrieben werden, wie im verlinkten Artikel erläutert.
Eine Reihe anderer Dinge, die Sie ausprobieren und profilieren sollten:
Wenn sich Ihr Anwendungsfall dafür eignet, möchten Sie möglicherweise einen Compute Shader verwenden, um das VBO direkt über die GPU zu aktualisieren (dies ist etwas aufwändiger und es gibt großartige Online-Ressourcen, die dies besser erklären als ich).
quelle