GLSL Partikel

Aus DGL Wiki
Wechseln zu: Navigation, Suche

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ört. Da die Leistung der Grafikkarten in den letzten Jahren immer größer wurde und auch die Flexibilität immer mehr zunahm, liegt es nahe ein Partikelsystem direkt in GLSL zu programmieren. 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ören 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ährend es bei konventionellen Partikelsystemen am Sinnvollsten 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 verarbeiten und außerdem Positionen noch Physikdaten gespeichert werden müssen, sollte von Anfang an der Speicherkonsum auf der Grafikkarte mit eingeplant werden. Die Anzahl der Komponenten sollte sich möglicherweise durch vier teilen lassen. Zudem sollte beim Speichern statt 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 sinnvoll 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 her gerendert wird, spricht mach auch von einem Ping-Pong-Buffer.

Damit keine getrennten Buffer für Position und Geschwindigkeit verwendet werden müssen, sind Multiple Render Targets von Vorteil. Diese sind nicht schwerer zu verwenden als normale Framebufferobjekte. Eine Einschränkung ist, dass nur Render Targets mit gleichen Bitanzahlen kombiniert werden können. So lässt sich ein 1x float32 mit einem 2x float16 und einem RGBA8 Render target kombinieren. Ein RGBA_float16 Target z.B. lässt sich nur mit einem RG_float32 und sich selbst kombinieren.

Weiterhin ist es sehr sinnvoll 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 Programm Pseudocode um einen Überblick über die Initialisierung zu schaffen:

 Optional (3 Zeilen):
 Eine RGB_Float16 Textur für HDR Rendering mit der OpenGL-Auflösung erzeugen.
 Ein Renderbufferobjekt für den Z-Buffer erstellen.
 Beides zu einem Framebufferobjekt hinzufügen.
 2x2 RGBA_Float16 Texturen mit 1024x1024 erstellen.
 2 mit Initialisierungsdaten füllen
 Ein weiteres Framebufferobjekt erzeugen und die zwei übrigen daran binden.
 Laden weiterer Texturen 
 Shader laden  
 Hauptschleife
    Aktivieren des Partikelphysikshaders und des Rendertargets
    Rendern der Physikdaten in den zweiten Buffer
    Buffer tauschen.
    Die gerenderten Daten in ein VBO übertragen oder später per Vertextexturefetch nutzen.
    Normales Rendertaget auswählen
    Rest der Scene Rendern
    Partikel mit Partikelshader als Pointsprites rendern.

Probleme bei der Implementation

Während die Implementation der Framebufferobjekte 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 sinnvolle 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 abarbeiten können. Ansonsten fallen zwar mehr Funktionsaufrufen an, dafür wird eine Menge RAM auf der Grafikkarte gespart. Dieser Weg 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 gerendert werden können. Möglicherweise gibt es hier eine weitere 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 Pixelshader 3.0 kompatiblen Karten den Texturelookup im Vertexshader nicht unterstützen, werde ich als erstes diesen Weg zeigen, da er am einfachsten ist. Um eventuelle Probleme mit Float16 zu umgehen wird hier durchgängig float32 benutzt. Die Partikelanzahl beträgt 2^16. Mehr Partikel sollten aber kein großes Problem sein.

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ält:

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 Ping-Pong-Rendering 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 sinnvoller, 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. Prinzipiell wäre es auch möglich die ModelViewProjectionMatrix zu umgehen, so dass die eventuell vorhandene Perspektive nicht stört. 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, zeige ich hier nur wie die Ein- und Ausgabe funktioniert. Die Ausgabevariablen gl_FragData[] sind vec4-Vektoren. gl_FragData[0] entspricht dabei gl_FragColor. Hier ist ein Partikelphysikshader der im Prinzip nichts tut:

uniform sampler2D Texture0;
uniform sampler2D Texture1; 
void main(){
	vec4 data0 = texture2D(Texture0,vec2(gl_TexCoord[0]));
	vec4 data1 = texture2D(Texture1,vec2(gl_TexCoord[0]));
 
	// ...Daten modifizieren...
 
	gl_FragData[0] = data0;
	gl_FragData[1] = data1;
}

Im Rendershader muss die eigentliche Position (auch Normalvektoren und Farben) per Texturelookup ausgelesen werden. Um Bandbreite zu sparen sollten diese Variablen Werte möglichst eng zusammengepackt werden. Um Latenzen zu verstecken sollte man die Ergebnisse der Lookups nach Möglichkeit erst benutzen, wenn andere Dinge bereits berechnet wurden.

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 kann 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);
}

Links