Übergeben einer Werteliste an den Fragment-Shader

74

Ich möchte eine Liste von Werten an einen Fragment-Shader senden. Es handelt sich um eine möglicherweise große Liste (einige Tausend Elemente lang) von Floats mit einfacher Genauigkeit. Der Fragment-Shader benötigt zufälligen Zugriff auf diese Liste und ich möchte die Werte der CPU in jedem Frame aktualisieren.

Ich denke über meine Möglichkeiten nach, wie dies getan werden könnte:

  1. Als einheitliche Variable vom Array-Typ ("uniform float x [10];"). Aber hier scheint es Grenzen zu geben, da das Senden von mehr als ein paar hundert Werten auf meiner GPU sehr langsam ist und ich auch die Obergrenze im Shader fest codieren müsste, wenn ich dies lieber zur Laufzeit ändern möchte.

  2. Aktualisieren Sie dann als Textur mit Höhe 1 und Breite meiner Liste die Daten mit glCopyTexSubImage2D.

  3. Andere Methoden? Ich habe in letzter Zeit nicht mit allen Änderungen in der GL-Spezifikation Schritt gehalten. Vielleicht gibt es eine andere Methode, die speziell für diesen Zweck entwickelt wurde?

Ville Krumlinde
quelle
1
Ich bin kein Experte für GLSL, aber aus Neugier, warum / wie würden Sie Tausende von Parametern für einen Shader verwenden?
George Profenza
5
@ GeorgeProfenza Ich würde nicht sagen, dass es sich um tausend separate Parameter handelt, sondern um einen einzelnen Parameter, der eine Wertetabelle enthält. Der Shader würde einen Wert in dieser Liste nachschlagen, bei dem der Index von gl_FragCoord und anderen Faktoren abhängt.
Ville Krumlinde

Antworten:

139

Derzeit gibt es 4 Möglichkeiten, dies zu tun: Standard-1D-Texturen, Puffertexturen, einheitliche Puffer und Shader-Speicherpuffer.

1D Texturen

Mit dieser Methode glTex(Sub)Image1Dfüllen Sie eine 1D-Textur mit Ihren Daten. Da Ihre Daten nur ein Array von Floats sind, sollte Ihr Bildformat sein GL_R32F. Sie können dann mit einem einfachen texelFetchAufruf im Shader darauf zugreifen . texelFetchNimmt Texelkoordinaten (daher der Name) und schaltet alle Filter aus. Sie erhalten also genau ein Texel.

Hinweis: texelFetchist 3.0+. Wenn Sie frühere GL-Versionen verwenden möchten, müssen Sie die Größe an den Shader übergeben und die Texturkoordinate manuell normalisieren.

Die Hauptvorteile sind hier Kompatibilität und Kompaktheit. Dies funktioniert auf GL 2.1-Hardware (unter Verwendung der Notation). Und Sie nicht haben zu verwenden GL_R32FFormate; Sie könnten GL_R16Fhalbe Schwimmer verwenden. Oder GL_R8wenn Ihre Daten für ein normalisiertes Byte angemessen sind. Größe kann viel für die Gesamtleistung bedeuten.

Der Hauptnachteil ist die Größenbeschränkung. Sie können nur eine 1D-Textur mit der maximalen Texturgröße verwenden. Auf Hardware der GL 3.x-Klasse sind dies rund 8.192, aber garantiert nicht weniger als 4.096.

Einheitliche Pufferobjekte

Dies funktioniert so, dass Sie in Ihrem Shader einen einheitlichen Block deklarieren:

layout(std140) uniform MyBlock
{
  float myDataArray[size];
};

Sie greifen dann wie ein Array im Shader auf diese Daten zu.

Zurück im C / C ++ / etc-Code erstellen Sie ein Pufferobjekt und füllen es mit Gleitkommadaten. Anschließend können Sie dieses Pufferobjekt dem MyBlockeinheitlichen Block zuordnen. Weitere Details finden Sie hier.

Die Hauptvorteile dieser Technik sind Geschwindigkeit und Semantik. Die Geschwindigkeit hängt davon ab, wie Implementierungen einheitliche Puffer im Vergleich zu Texturen behandeln. Texturabrufe sind globale Speicherzugriffe. Einheitliche Pufferzugriffe sind in der Regel nicht; Die einheitlichen Pufferdaten werden normalerweise in den Shader geladen, wenn der Shader bei seiner Verwendung beim Rendern initialisiert wird. Von dort ist es ein lokaler Zugang, der viel schneller ist.

Semantisch ist dies besser, da es sich nicht nur um ein flaches Array handelt. Für Ihre spezifischen Bedürfnisse float[]spielt es keine Rolle , ob Sie nur eine benötigen . Wenn Sie jedoch eine komplexere Datenstruktur haben, kann die Semantik wichtig sein. Betrachten Sie beispielsweise eine Reihe von Lichtern. Lichter haben eine Position und eine Farbe. Wenn Sie eine Textur verwenden, sieht Ihr Code zum Abrufen der Position und Farbe für ein bestimmtes Licht folgendermaßen aus:

vec4 position = texelFetch(myDataArray, 2*index);
vec4 color = texelFetch(myDataArray, 2*index + 1);

Mit einheitlichen Puffern sieht es genauso aus wie jeder andere einheitliche Zugriff. Sie haben Mitglieder benannt, die aufgerufen werden können positionund color. Alle semantischen Informationen sind also da; Es ist einfacher zu verstehen, was los ist.

Auch hierfür gibt es Größenbeschränkungen. OpenGL erfordert, dass Implementierungen mindestens 16.384 Bytes für die maximale Größe einheitlicher Blöcke bereitstellen. Das heißt, für Float-Arrays erhalten Sie nur 4.096 Elemente. Beachten Sie erneut, dass dies das Minimum ist, das für Implementierungen erforderlich ist. Einige Hardware bietet viel größere Puffer. AMD bietet beispielsweise 65.536 Geräte für die Hardware der DX10-Klasse an.

Puffertexturen

Dies ist eine Art "Super 1D Textur". Sie ermöglichen Ihnen effektiv den Zugriff auf ein Pufferobjekt von einer Textureinheit aus . Obwohl sie eindimensional sind, handelt es sich nicht um 1D-Texturen.

Sie können sie nur ab GL 3.0 oder höher verwenden. Und Sie können nur über die texelFetchFunktion darauf zugreifen .

Der Hauptvorteil ist hier die Größe. Puffertexturen können im Allgemeinen ziemlich gigantisch sein. Während die Spezifikation im Allgemeinen konservativ ist und mindestens 65.536 Bytes für Puffertexturen vorschreibt, erlauben die meisten GL-Implementierungen, dass sie in der Größe der Mega- Bytes liegen. In der Tat wird die maximale Größe normalerweise durch den verfügbaren GPU-Speicher und nicht durch Hardwarebeschränkungen begrenzt.

Außerdem werden Puffertexturen in Pufferobjekten gespeichert, nicht in undurchsichtigeren Texturobjekten wie 1D-Texturen. Dies bedeutet, dass Sie einige Pufferobjekt-Streaming-Techniken verwenden können , um sie zu aktualisieren.

Der Hauptnachteil ist hier die Leistung, genau wie bei 1D-Texturen. Puffertexturen sind wahrscheinlich nicht langsamer als 1D-Texturen, aber sie sind auch nicht so schnell wie UBOs. Wenn Sie nur einen Schwimmer von ihnen ziehen, sollte dies kein Problem sein. Wenn Sie jedoch viele Daten daraus abrufen, sollten Sie stattdessen ein UBO verwenden.

Shader-Speicherpufferobjekte

OpenGL 4.3 bietet eine andere Möglichkeit, dies zu handhaben: Shader-Speicherpuffer . Sie ähneln einheitlichen Puffern. Sie geben sie mit einer Syntax an, die fast identisch mit der von einheitlichen Blöcken ist. Der Hauptunterschied besteht darin, dass Sie ihnen schreiben können. Natürlich ist das für Ihre Bedürfnisse nicht nützlich, aber es gibt andere Unterschiede.

Shader-Speicherpuffer sind konzeptionell eine alternative Form der Puffertextur. Daher sind die Größenbeschränkungen für Shader-Speicherpuffer viel größer als für einheitliche Puffer. Das OpenGL-Minimum für die maximale UBO-Größe beträgt 16 KB. Das OpenGL-Minimum für die maximale SSBO-Größe beträgt 16 MB . Wenn Sie also über die Hardware verfügen, sind diese eine interessante Alternative zu UBOs.

Stellen Sie nur sicher, dass Sie sie als deklarieren readonly, da Sie ihnen nicht schreiben.

Der potenzielle Nachteil ist hier wiederum die Leistung im Vergleich zu UBOs. SSBOs funktionieren wie eine Bildlade- / Speicheroperation über Puffertexturen. Im Grunde ist es (sehr schöner) syntaktischer Zucker um einen imageBufferBildtyp. Daher werden Lesevorgänge von diesen wahrscheinlich mit der Geschwindigkeit von Lesevorgängen von a ausgeführt readonly imageBuffer.

Ob das Lesen über das Laden / Speichern von Bildern durch Pufferbilder schneller oder langsamer als Puffertexturen ist, ist derzeit unklar.

Ein weiteres potenzielles Problem besteht darin, dass Sie die Regeln für den nicht synchronen Speicherzugriff einhalten müssen . Diese sind komplex und können Sie sehr leicht stolpern.

Nicol Bolas
quelle
1
Vielleicht habe ich nicht allzu viel Einblick in die GPU-Architektur, aber passt die Aussage "UBOs sind kein globaler Speicher, der bei der Initialisierung in Shader geladen wird" wirklich zu einer Größe von mindestens 65 KByte?
Christian Rau
3
@ ChristianRau: Sicher. GPUs haben viele Speicherpuffer, von denen einige ziemlich groß sind. Und da es sich um eine Uniform handelt (und daher eine feste Größe hat), wird jede von bis zu 4 separaten Threads geteilt. Und Sie müssen sie nur hochladen, wenn Sie entweder das von Ihnen verwendete Programm oder den einheitlichen Puffer ändern . Das Beenden eines Scheitelpunkts / Fragments und das Starten eines neuen muss also nicht geändert werden. Bei einem Fragment-Shader-Prozess können Sie die Kopie selbst mit 30 SIMDs nur 20 Mal kopieren. Unabhängig davon, wie viele Fragmente gerendert werden.
Nicol Bolas
9
Dies ist eine fantastische Antwort, danke. Ich hätte stundenlang gegoogelt, um all diese Informationen woanders zu finden.
Ville Krumlinde
Dies war sehr nützlich und schließlich die Übersicht, die jeder zum Datenproblem benötigt. Es gelten viele interessante Grenzen. Normale Texturen sind wahrscheinlich aufgrund der Verwendung von Textureinheiten auf Grafikkarten eingeschränkt.
Robetto
8

Dies klingt nach einem guten Anwendungsfall für Texturpufferobjekte . Diese haben nicht viel mit normalen Texturen zu tun und ermöglichen Ihnen grundsätzlich den Zugriff auf den Speicher eines Pufferobjekts in einem Shader als einfaches lineares Array. Sie ähneln 1D-Texturen, werden jedoch nicht gefiltert und nur über einen Ganzzahlindex aufgerufen. Dies klingt nach dem, was Sie tun müssen, wenn Sie es als Werteliste bezeichnen. Und sie unterstützen auch viel größere Größen als 1D-Texturen. Denn es ist die Aktualisierung können Sie dann die Standardpufferobjektmethoden verwenden ( glBufferData, glMapBuffer, ...).

Andererseits benötigen sie GL3 / DX10-Hardware, um verwendet zu werden, und ich denke, sie wurden sogar in OpenGL 3.1 zum Kern gemacht. Wenn Ihre Hardware / Ihr Treiber dies nicht unterstützt, ist Ihre zweite Lösung die Methode Ihrer Wahl, verwenden Sie jedoch eher eine 1D-Textur als eine Breite x 1 2D-Textur. In diesem Fall können Sie auch eine nicht flache 2D-Textur und etwas Indexmagie verwenden, um Listen zu unterstützen, die größer als die maximale Texturgröße sind.

Aber Texturpuffer passen perfekt zu Ihrem Problem, denke ich. Für genauere Einblicke können Sie auch in die entsprechende Erweiterungsspezifikation schauen .

EDIT: Als Antwort auf Nicol's Kommentar zu einheitlichen Pufferobjekten können Sie hier auch einen kleinen Vergleich der beiden suchen . Ich tendiere immer noch zu TBOs, kann aber nicht wirklich begründen, warum, nur weil ich sehe, dass es konzeptionell besser passt. Aber vielleicht kann Nicol einer Antwort mehr Einblick in die Sache geben.

Christian Rau
quelle
Danke, das sieht so aus, als könnte es das sein, wonach ich gesucht habe. Meine AMD-GPU unterstützt diese Erweiterung, jetzt muss ich nur noch abschätzen, wie weit verbreitet diese in der Benutzerbasis ist ...
Ville Krumlinde
@VilleKrumlinde Es sollte von jeder GL3 / DX10-Hardware unterstützt werden, zumindest auf der Hardwareseite.
Christian Rau
Sie können auch Uniform Buffers verwenden . Sie können im Allgemeinen nicht so groß sein wie Texturpuffer, aber sie sind im Shader viel einfacher zu erreichen und zu bearbeiten. Und ein bisschen schneller für den Speicherzugriff.
Nicol Bolas
@NicolBolas Auf welche Weise sind sie im Shader leichter zugänglich und verwendbar (außer beim Ersetzen texture...durch ...[...])? Sind sie wirklich schneller als TBOs? Einige Einblicke wären schön, da ich mit keinem der beiden Erfahrungen habe. Vielleicht können Sie eine Antwort hinzufügen, die UBOs als Alternative erklärt?
Christian Rau
6

Eine Möglichkeit wäre, einheitliche Arrays zu verwenden, wie Sie bereits erwähnt haben. Eine andere Möglichkeit besteht darin, eine 1D- "Textur" zu verwenden. Suchen Sie nach GL_TEXTURE_1D und glTexImage1D. Ich persönlich bevorzuge diesen Weg, da Sie die Größe des Arrays im Shader-Code nicht wie gesagt fest codieren müssen und opengl bereits über integrierte Funktionen zum Hochladen / Zugreifen auf 1D-Daten auf der GPU verfügt.

edvaldig
quelle
2

Ich würde sagen, wahrscheinlich nicht Nummer 1. Sie haben eine begrenzte Anzahl von Registern für Shader-Uniformen, die je nach Karte variieren. Sie können GL_MAX_FRAGMENT_UNIFORM_COMPONENTS abfragen, um Ihr Limit herauszufinden. Auf neueren Karten läuft es zu Tausenden, zB hat ein Quadro FX 5500 anscheinend 2048. (http://www.nvnews.net/vbulletin/showthread.php?t=85925). Es hängt davon ab, auf welcher Hardware es ausgeführt werden soll und welche anderen Uniformen Sie möglicherweise auch an den Shader senden möchten.

Nummer 2 könnte je nach Ihren Anforderungen funktionieren. Entschuldigen Sie die Unbestimmtheit hier, hoffentlich kann Ihnen jemand anderes eine genauere Antwort geben, aber Sie müssen genau angeben, wie viele Texturaufrufe Sie in älteren Shader-Modellkarten tätigen. Es hängt auch davon ab, wie viele Texturlesevorgänge Sie pro Fragment ausführen möchten. Abhängig von Ihrem Shader-Modell und den Leistungsanforderungen möchten Sie wahrscheinlich nicht erneut versuchen, Tausende von Elementen pro Fragment zu lesen. Sie können Werte in RGBAs einer Textur packen und so 4 Lesevorgänge pro Texturaufruf erhalten. Bei wahlfreiem Zugriff ist dies jedoch möglicherweise nicht hilfreich.

Ich bin mir bei Nummer 3 nicht sicher, aber ich würde vorschlagen, vielleicht UAV (ungeordnete Zugriffsansichten) zu betrachten, obwohl ich denke, dass dies nur DirectX ist, ohne anständiges openGL-Äquivalent. Ich denke, es gibt eine nVidia-Erweiterung für openGL, aber Sie beschränken sich dann wieder auf eine ziemlich strenge Mindestspezifikation.

Es ist unwahrscheinlich, dass die Übergabe von Tausenden von Datenelementen an Ihren Fragment-Shader die beste Lösung für Ihr Problem darstellt. Wenn Sie mehr Details zu dem, was Sie erreichen möchten, angegeben haben, erhalten Sie möglicherweise alternative Vorschläge.

Hybrid
quelle
Vielen Dank. Ich würde wahrscheinlich einen einzelnen Lesevorgang von diesem Array pro Fragment durchführen. Es ist eine Liste von Werten, die den endgültigen Fragmentwert beeinflussen, und wenn ich es so betrachte, ist vielleicht eine 1d-Textur der natürliche Weg. Das einzige Problem ist, dass ich lieber eine exakte Ganzzahlsuche anstelle einer Gleitkomma-Texturkoordinate hätte.
Ville Krumlinde
Für einen einzelnen Lesevorgang pro Fragment klingt es so, als wäre eine Textur definitiv der richtige Weg. Solange Sie die nächstgelegene Filterung für Ihre 1D-Textur verwenden, erhalten Sie immer einen Wert, der mit dem Wert übereinstimmt, den Sie in Ihre Textur eingegeben haben (natürlich vorbehaltlich von Gleitkomma- und Texturformat-Genauigkeitsfehlern)
Hybrid
1
@VilleKrumlinde Wenn Sie einen einfachen Array-Zugriff mit einem Integer-Index wünschen, sind TBOs der richtige Weg (zumindest auf GL3 / DX10-Hardware). Schau dir meine Antwort an.
Christian Rau