GLSL Partikel: Unterschied zwischen den Versionen
Oc2k1 (Diskussion | Beiträge) (→Implementation per Texturlookup im Vertexshader) |
Oc2k1 (Diskussion | Beiträge) (→Haupschleife) |
||
Zeile 124: | Zeile 124: | ||
<cpp> | <cpp> | ||
glViewport(0, 0, screenwidth,screenheight); | glViewport(0, 0, screenwidth,screenheight); | ||
− | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); | + | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //nur wenn keine andere Scene gerendert wurde |
glUseProgramObjectARB(partikelren); | glUseProgramObjectARB(partikelren); | ||
Version vom 17. August 2006, 19:08 Uhr
Bitte haben Sie etwas Geduld und nehmen Sie keine Änderungen vor, bis der Artikel hochgeladen wurde. |
Inhaltsverzeichnis
Einleitung
So mal wieder was schreiben wo von ich keine Ahnung hab... (ironisch gemeint. Ich hab nur noch kein fertiges Testprogramm und es nirgends richtig dokumentiert)
Sicher habt die meisten zumindest schon mal von Partikelsystemen gehöhrt. Da die Leistung der Grafikkarten in den letzten Jahren immer größer wurde und auch die Flexibilität immer größer wurde liegt es nahe ein Partikelssystem direkt in GLSL zu programieren. Dadurch, dass keine Daten mehr zu der Grafikkarte transportiert werden müssen und die CPU von den Berechnungen entlastet wird, sollten um eine Größenordnung mehr Partikel möglich sein.
Im gegensatz zu den klassischen Partikelsystemen wo alle Partikel unabhängig von einander sind, gibt es auch Partikelsysteme, bei denen Partikel mit einander verbunden sind. Dazu gehöhren unter anderem auch Seile und Kleidung. Der übergang zu echten Physiksimulation ist fließend. Der wahrscheilich größte unterschied ist, das ein Partikelsystem nur ein grafischer Effekt ist und keinen Einfluss auf die restliche Physik nimmt.
Grundlagen für die Implementierung
Wärend es bei konventionellen Partikelsystemen am sinvollsten war, die berechneten Daten per VBO oder Vertexarray in die Grafikkarte zu laden. Müssen bei einem GLSL-Partikelsystem, alle Daten in Texturen gespeichert werden. Da die GPU in der Lage ist mehr als die 10fache Menge an Partikeln zu verabeiten und außer den Positionen noch Physikdaten gespeichtert werden müssen, muss von Anfang an der Speicherkonsum auf der Grafikkarte mit geplant werden. Die Anzahl der Komponenten sollte sich möglicherweise durch vier teilen lassen. Zudem sollte beim Speichern stat float32 möglichst nur float16 genutzt werden. Ein Beispiel wäre ein einfaches Partikelsystem: Für drei Positionskomponenten, eine Zeitkomponente, drei Geschwindigkeitskomponenten und eine noch freie Komponente z.B. für Masse, werden 8 Werte benötigt. Bei einer Verwendung von float16 und 2^20 Partikeln, werden ganze 16MB GPU Ram verbraucht. Da es nicht möglich oder sinvoll ist in eine aktive Textur zu schreiben, wird eine zweite Textur benötigt, die die selbe Größe hat. So verbraucht unser Partikelsystem 32 MB GPU Ram. Da immer zwischen diesen zwei Buffern hin und hergerendert wird, spricht mach auch von einem Ping-Pongbuffer.
Damit keine getrennten Buffer für Position und Geschwindigkeit verwendet werden müssen, sollten Multiplerendertargets verwendet werden. Diese sind nicht schwerer zu verwenden als normale Frambufferobjekte. Eine Einschränkung ist, dass nur Rendertargets mit gleichen Bitanzahlen kombiniert werden können. So lässt sich ein 1x float32 mit einem 2x float16 und einem RGBA8 Rendertarget kombinieren. Ein RGBA_float16 Target z.B. lässt sich nur mit einem RG_float32 und sich selbst kombinieren.
Weiterhin ist es sehr sinvoll einen float16 Frambuffer zu verwenden, da so nachträglich durch die Blende die Helligkeit des Bildes optimal angepasst werden kann (Siehe fehlendes HDR tutorial....)
Nun erst einmal ein Program im Pseudocode um einen Überblick über die Initialisierung zu schaffen:
Optional (3 Zeilen): Eine RGB_Float16 Textur für HDR Rendering mit der OpenGLauflösung erzeugen. Ein Rendebufferobjekt für den Z-Buffer erstellen. Beides zu einem Frambufferobjekt hinzufügen. 2x2 RGBA_Float16 Texturen mit 1024x1024 erstellen. 2 mit initialisierungsdaten füllen Ein weiteres Frambufferobjekt erzeugen und die zwei übrigen daran binden. Laden weitere Texturen Shader Laden Haupschleife Aktivieren des Partikelphysikshaders und des Rendertargets Rendern der Physikdaten in den zweiten Buffer Buffer tauschen. Die gerderten Daten in ein VBO übertragen oder später per Vertexttexturefetch nutzen. Nortmales Rendertaget auswählen Rest der Scene Rendern Partikel mit Partikelshader als Pointsprites rendern.
Probleme bei der Implementation
Wärend sich die Implementation der Frambufferobjekte sehr sauber ist und das Ping-Pong rendering an sich kein Problem darstellen sollte, macht das Nutzen der Pixeldaten als Vertexdaten richtig Stress. Die einzige halbwegs saubere Möglichkeit ist die Verwendung eines Pixelbufferobjektes. Jedoch hat dies den großen Nachteil, das es ein großer Speicherfresser ist, da alle Daten kopiert werden. Wenn unsere 2^20 Vertices auf Float32 aufgebläht werden, lösen sich weitere 32MB auf. NUn gibt es nur zwei sinvolle Möglichkeiten:
Es wird ein relativ kleines PBO benutzt, welchen nur 2^16 Vertices abarbeitet, der Vorteil ist, dass einige Grafikkarten diese kleineren VBOs etwas schneller abbarbeiten können. Ansonsten fallen zwar mehr Funktionsaufrufen an, dafür wird eine menge Ram auf der Grafikkarte gespart. Dieser Web bereitet einem besonders nette Überraschungen beim coden, da die konzepte der FBOs irgendwie nicht zu den PBOs passen.
Die zweite Möglichkeit erscheint auf dem erstem Blick noch sauberer, läuft jedoch nicht auf ATI Karten: Es wird ein kleines VBO mit Indexkoordinaten gebildet, welches einen kleinen Teil der Textur mit den Vertexdaten indiziert. Dieser Bereich lässt sich mit zwei Uniformvariablen verschieben, so das ohne kopieren der Daten alle Partikel gerender werden können. Möglicher weise gibt es hier eine weiter Beschränkung auf Float32 texturen.
In zukunft sollte dieses Problem mit den "uerbuffers" oder "superbuffers" gelöst werden. Leider scheinen sich da Nvidia und ATI nicht einigen zu können.
Implementation per Texturlookup im Vertexshader
Auch wenn besitzer von ATI Karten jetzt dumm gucken, weil die Pixelshader3.0 kompatiblen Karten den Texturelookup im Vertexshader nicht unterstützen, werde ich als erstes diesen Weg zeigen, da er am einfachsten ist. UM eventuellen Problemen mit Float16 zu umgewhen wird hier durchgängig float32 benutzt.
Initialisierung
Für ein Partikelsystem welches mit 8 Floats pro Partikel auskommt. Kann man so die nötigen Texturen und Framebufferobjekte erzeugt werden. Zusätzlich wird noch ein VBO mit Vertexdaten notwendig, welches Texturkoordinaten für jeden Texel der Textur enthällt:
for (i=0;i<4;i++){ glBindTexture (GL_TEXTURE_2D,partikelbuffer[i]); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexImage2D (GL_TEXTURE_2D, 0,GL_RGB32F_ARB , 256, 256, 0, GL_RGBA , GL_FLOAT, dummy[i]); //dummy[] sollte Zeiger auf Speicherbereiche mit Initialisierungsdaten enthalten //oder NULL enthalten. } glGenFramebuffersEXT (2, partikel_framebuffer); glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, partikel_framebuffer[0]); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_COLOR_ATTACHMENT0_EXT,GL_TEXTURE_2D,partikelbuffer[2], 0); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_COLOR_ATTACHMENT1_EXT,GL_TEXTURE_2D,partikelbuffer[3], 0); glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, partikel_framebuffer[1]); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_COLOR_ATTACHMENT0_EXT,GL_TEXTURE_2D,partikelbuffer[0], 0); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,GL_COLOR_ATTACHMENT1_EXT,GL_TEXTURE_2D,partikelbuffer[1], 0); for (i=0;i<256; i++ )for (j=0;j<256; j++ ){ data[i*2 + j* 512]= i/256.0; data[i*2 + j* 512 +1]= j/256.0;} glGenBuffersARB( 1, &vertex); glBindBufferARB( GL_ARRAY_BUFFER_ARB, vertex ); glBufferDataARB( GL_ARRAY_BUFFER_ARB, 65536 *2 * sizeof(float), data, GL_STATIC_DRAW_ARB );
Anschließend sollten die Shader geladen werden. Benutzt für loadShader() eure eigene Funktion zum shader laden oder schaut im erstem GLSL Tutorial nach wie man einen shaderloader schreibt. Anschließen sollten die Uniformvariablen der Texturen gesetzt werden.
partikelphy = loadShader("partikelphy.vert","partikelphy.frag"); glUseProgramObjectARB(partikelphy); glUniform1iARB(glGetUniformLocationARB(partikelphy, "Texture0"),0); glUniform1iARB(glGetUniformLocationARB(partikelphy, "Texture1"),1); partikelren = loadShader("partikelren.vert","partikelren.frag"); glUseProgramObjectARB(partikelren); glUniform1iARB(glGetUniformLocationARB(partikelren, "Texture0"),0);
Haupschleife
Damit das Pingpongrendering leicht gesteuert werden kann sollte eine Variable immer zwischen dem Wert 0 und 1 hin und her wechseln. Als erstes der Teil, der die Partikelphysik in den Pingpongbuffer rendert:
pass = (pass+1)%2; glViewport(0, 0, 256,256); glUseProgramObjectARB(partikelphy); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, partikelbuffer[pass * 2]); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, partikelbuffer[ pass * 2 + 1]); glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, partikel_framebuffer[pass]); glBegin(GL_QUADS); glTexCoord2f(0,0); glVertex2f(0,0); glTexCoord2f(0,1); glVertex2f(0,1); glTexCoord2f(1,1); glVertex2f(1,1); glTexCoord2f(1,0); glVertex2f(1,0); glEnd(); glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, 0);
Nun noch der Teil, der die Partikel als Points rendert. Gegebenfalls sind Poinsprites sinvoller, zumal sie mit GLSL 1.2 noch ein wenig flexibler werden.
glViewport(0, 0, screenwidth,screenheight); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //nur wenn keine andere Scene gerendert wurde glUseProgramObjectARB(partikelren); glEnableClientState(GL_VERTEX_ARRAY); glBindBufferARB( GL_ARRAY_BUFFER_ARB, vertex); glVertexPointer(2, GL_FLOAT, 0 , (char *) NULL); glDrawArrays(GL_POINTS, 0, 65536); glDisableClientState(GL_VERTEX_ARRAY);
Shader
Es werden zwei Vertex und Pixelshader gebraucht.
Der Partikelphysikvertexshader muss nur die Variablen an den Pixelshader weiter geben. Prinzipell wäre es auch möglich die ModelViewProjectionMatrix zu umgehen, so dass die eventuell vorhandene Perspektive nicht stöhrt. Auch die Texturkoordinaten ließen sich wegrationalisieren. Damit alles leicht verständlich bleibt hier die gewohnte Variante:
void main(void){ gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; gl_TexCoord[0] = gl_MultiTexCoord0; }
Da die Berechnungen im Partikelsystem immer individuell sind, zweige ich hier nur wie die ein und Ausgabe funktioniert. Die Ausgabevariablen gl_FragData[] sind vec4 Vektoren. gl_FragData[0] entspringt dabei gl_FragColor. Hier ist ein Partikelphysikshader der im Prinzip nichts tut:
uniform sampler2D Texture0; uniform sampler2D Texture1; void main(){ gl_FragData[0] = texture2D(Texture0,vec2(gl_TexCoord[0])); gl_FragData[1] = texture2D(Texture1,vec2(gl_TexCoord[0])); }
Im Rendershader muss die eigendliche Position (auch Normalvektoren und Farben) per Texturelookup ausgelesen werden. Um Bandbreite zu sparen sollten diese Variablen Werte möglichst weit zusammen gepackt werden. Um Latenzen zu verstecken sollte man die Ergebnisse der Lookups nach Möglichkeit erst benutzen, wenn man andere Dinge berechnet hat.
uniform sampler2D Texture0; void main(void){ vec4 look = texture2D(Texture0, vec2(gl_Vertex)); look.w = 1.0; gl_Position = gl_ModelViewProjectionMatrix * look; gl_TexCoord[0] = gl_MultiTexCoord0; }
Der Pixelshader zum rendern der Partikel kan sehr einfach sein. Sollte jedoch später ein wenig mehr machen als nur weiße Punkte zu zeichnen:
void main(){ gl_FragColor = vec4(1.0,1.0,1.0,1.0); }