GLSL Partikel 2: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
(Die Seite wurde neu angelegt: {{Offline}} Der klassische Ansatz eines GPU Partikelsystem verwendet eine oder mehrere Texturen um die Partikel zu speichern. Zur Aktualisierung wird...)
 
(Grundlagen)
Zeile 8: Zeile 8:
  
 
== Grundlagen ==
 
== Grundlagen ==
 +
Ich mehrere benutze Texturbuffer-Objects (TBO) zum speichern der Partikeldaten:
 +
* ein 4x32bit-float Buffer zum speichern der Position und Größe des Partikels (GL_RGBA32F_ARB)
 +
* ein weiterer 4x32bit-float Buffer zum speichern der Velocity und Lebenszeit des Partikels (GL_RGBA32F_ARB)
 +
* ein 32bit-unsigned Buffer zum speichern des aktuellen Zufallsseed (GL_LUMINANCE32UI_EXT)
 +
 
=== Update-Schritt ===
 
=== Update-Schritt ===
 +
Eine Grafikkarte kann nicht aus einem Buffer lesen und gleichzeitig auch hinein schreiben. Für benötigen also jeden Buffer doppelt: Einen Inputbuffer aus dem als VBO oder TBO lesen, während wir in den Outputbuffer via Transform-Feedback schreiben. Nach jedem Updateschritt werden die Rollen der Buffer getauscht.
 +
 +
Zunächst interpretieren wir den Position-Inputbuffer als Vertexbuffer und rendern sämtliche Partikel. Der Vertexshader ist trivial, denn die eigentliche Arbeit verrichtet der Geometryshader. Der Fragmentshader wird nicht benötigt und daher mittels {{INLINE_CODE|glEnable(GL_RASTERIZER_DISCARD_NV)}} abgeschaltet.
 +
 +
'''Vertexshader''':
 +
<glsl>void main() {
 +
gl_Position = gl_Vertex;
 +
}</glsl>
 +
 +
'''Geometryshader''':
 +
<glsl>#extension GL_EXT_gpu_shader4 : enable
 +
#extension GL_EXT_geometry_shader4: enable
 +
 +
uniform samplerBuffer tboVelocity;
 +
uniform usamplerBuffer tboRandom;
 +
uniform float timeElapsed;
 +
 +
varying out vec4 velocity_out;
 +
varying out unsigned int seed;
 +
 +
const vec3 wind = vec3(1.0, 1.0, 2.0);
 +
 +
vec3 constField() {
 +
return timeElapsed * wind;
 +
}
 +
 +
vec3 dampField(vec3 velocity, float strength) {
 +
return -velocity*strength*timeElapsed;
 +
}
 +
 +
vec3 noiseField(float strength) {
 +
vec3 result;
 +
seed = (seed * 1103515245u + 12345u); result.x = float(seed);
 +
seed = (seed * 1103515245u + 12345u); result.y = float(seed);
 +
seed = (seed * 1103515245u + 12345u); result.z = float(seed);
 +
return timeElapsed * strength * ((result / 4294967296.0) - vec3(0.5,0.5,0.5));
 +
}
 +
 +
void main() {
 +
// get all particle data
 +
vec3 position = gl_PositionIn[0].xyz;
 +
float size = gl_PositionIn[0].w;
 +
 +
vec4 tmp = texelFetchBuffer(tboVelocity, gl_PrimitiveIDIn);
 +
vec3 velocity = tmp.xyz;
 +
float lifetime = tmp.w;
 +
 +
// update lifetime and discard dead particles
 +
lifetime -= timeElapsed;
 +
if (lifetime <= 0.0) { return; }
 +
 +
// update position and size
 +
position += velocity * timeElapsed;
 +
size += 0.3 * size * timeElapsed;
 +
 +
// read current random seed
 +
seed = texelFetchBuffer(tboRandom, gl_PrimitiveIDIn).x;
 +
 +
// update velocity by applying force fields
 +
velocity += dampField(velocity, 0.5);
 +
velocity += constField();
 +
velocity += noiseField(1.0);
 +
 +
// write vertex
 +
gl_Position = vec4(position, size);
 +
velocity_out = vec4(velocity, lifetime);
 +
EmitVertex();
 +
}</glsl>
 +
 +
Da wir GL_POINTS rendern ist ein Primitiv einen Vertex groß. Aus diesem Grund eignet sich die eingebaute Variable {{INLINE_CODE|gl_PrimitiveIDIn}} perfekt als Arrayindex. Via TexelFetch können wir also Velocity, Lebenszeit und aktuellen Seed auslesen.
 +
 +
Ist die Lebenszeit eines Partikels abgelaufen, brechen wir die Verarbeitung dieses Partikels ab. Die Funktion {{INLINE_CODE|EmitVertex()}} wird also nicht aufgerufen und der Partikel somit unterdrückt. Der entscheidende Vorteil von Transform-Feedback an dieser Stelle ist nun, das beim schreiben in den Outputbuffer <u>keine</u> Lücke entsteht. Es tritt also keine Fragmentierung auf. Am Ende des Updatevorgangs befinden sich alle Partikel aufgeräumt am Anfang des Buffers. Mit einem Query lässt sich auch die Anzahl der in den Outputbuffer geschriebenen Partikel auslesen, beim nächsten Frame wissen wir also exakt wie viele Partikel wir rendern müssen.
 +
 +
Bezüglich des verwendeten Zufallsgenerators in der Funktion {{INLINE_CODE|noiseField(float)}} sollte man bei Bedarf den Artikel [[GLSL_noise|GLSL Noise]] konsultieren. Der Rest des Update-Shaders sollte eigentlich klar sein.
 +
 
=== Emitter ===
 
=== Emitter ===
 +
 
== Erweiterungen ==
 
== Erweiterungen ==
 
Nun wollen wir das bestehende Partikelsystem erweitern. Es sollte klar sein das die hier beschriebenen Dinge sehr viel Rechenleistung benötigen. Diese Aktionen müssen also möglicherweise über mehrere Frames verteilt werden.
 
Nun wollen wir das bestehende Partikelsystem erweitern. Es sollte klar sein das die hier beschriebenen Dinge sehr viel Rechenleistung benötigen. Diese Aktionen müssen also möglicherweise über mehrere Frames verteilt werden.

Version vom 25. Februar 2009, 00:23 Uhr

Hinweis: Dieser Artikel wird gerade Offline bearbeitet!

Bitte haben Sie etwas Geduld und nehmen Sie keine Änderungen vor, bis der Artikel hochgeladen wurde.

(weitere Artikel)
WIP Offline.jpg

Der klassische Ansatz eines GPU Partikelsystem verwendet eine oder mehrere Texturen um die Partikel zu speichern. Zur Aktualisierung wird ein Quad in Größe dieser Textur gerendert. Der Fragmentshader liest die alten Daten aus einem Backbuffer und schreibt die aktualisierten Partikel in den Framebuffer. Dieses Verfahren ist an sich sehr effizient, hat aber einige Nachteile:

  • Es ist sehr kompliziert effizient neue Partikel von der CPU zu emittieren. Die CPU muss immer wissen wo sich aktive Partikel in der Textur befinden. Sind die Lebensdauern der Partikel unterschiedlich (oder gar zufällig) wird dies zu einem echten Problem.
  • Da die Partikelanordnung wie gerade beschrieben fragmentiert, muss beim rendern der Partikel immer die vollständige Textur verarbeitet werden, auch wenn eigentlich nur wenige Partikel tatsächlich aktiv sind. Außerdem ist zum rendern ein großes VBO mit Dummy-Partikeln erforderlich, die eigentlich keine Daten enthalten.
  • Sollen die Partikel zum Beispiel nach der Entfernung zur Kamera sortiert werden, ist dies sehr aufwendig. Auf der GPU verwendet man üblicherweise Odd-Even-Mergesort oder Bitonic-Sort. Beide Algorithmen vergleichen jeweils zwei Elemente und führen dann ggf. einen Tausch (Swap) dieser Elemente aus. Im Fragmentshader lässt sich jedoch die Position eines Fragments nicht mehr nachträglich ändern. Eine Implementierung im Fragmentshader erfordert also, dass jeder Vergleich zweimal durchgeführt wird. Das bedeutet auch, dass jeder Texturzugriff zweimal durchgeführt werden muss. Zudem erfordern beide Algorithmen sehr viele Passes. Für eine Million Partikel (1024²) sind so etwa 210 Passes erforderlich. Dies ist auch bei einer Verteilung über mehrere Frames nicht mehr sinnvoll in Echtzeit durchführbar.

Durch die Benutzung der Features aktueller Grafikhardware wie dem Geometryshader, Transform-Feedback, Instancing sowie diverser weiterer Features des Shader Model 4.0 lässt sich hier viel optimieren. Dieser Artikel setzt die Kenntnis dieser Funktionen voraus. Desweiteren sollte man natürlich wissen wie ein (state-preserving) Partikelsystem funktioniert, also zum Beispiel etwas mit den Begriffen Emitter und Forcefield anfangen können.

Grundlagen

Ich mehrere benutze Texturbuffer-Objects (TBO) zum speichern der Partikeldaten:

  • ein 4x32bit-float Buffer zum speichern der Position und Größe des Partikels (GL_RGBA32F_ARB)
  • ein weiterer 4x32bit-float Buffer zum speichern der Velocity und Lebenszeit des Partikels (GL_RGBA32F_ARB)
  • ein 32bit-unsigned Buffer zum speichern des aktuellen Zufallsseed (GL_LUMINANCE32UI_EXT)

Update-Schritt

Eine Grafikkarte kann nicht aus einem Buffer lesen und gleichzeitig auch hinein schreiben. Für benötigen also jeden Buffer doppelt: Einen Inputbuffer aus dem als VBO oder TBO lesen, während wir in den Outputbuffer via Transform-Feedback schreiben. Nach jedem Updateschritt werden die Rollen der Buffer getauscht.

Zunächst interpretieren wir den Position-Inputbuffer als Vertexbuffer und rendern sämtliche Partikel. Der Vertexshader ist trivial, denn die eigentliche Arbeit verrichtet der Geometryshader. Der Fragmentshader wird nicht benötigt und daher mittels glEnable(GL_RASTERIZER_DISCARD_NV) abgeschaltet.

Vertexshader:

void main() {
	gl_Position = gl_Vertex;
}

Geometryshader:

#extension GL_EXT_gpu_shader4 : enable
#extension GL_EXT_geometry_shader4: enable

uniform samplerBuffer tboVelocity;
uniform usamplerBuffer tboRandom;
uniform float timeElapsed;

varying out vec4 velocity_out;
varying out unsigned int seed;

const vec3 wind = vec3(1.0, 1.0, 2.0);

vec3 constField() {
	return timeElapsed * wind;
}

vec3 dampField(vec3 velocity, float strength) {
	return -velocity*strength*timeElapsed;
}

vec3 noiseField(float strength) {
	vec3 result;
	seed = (seed * 1103515245u + 12345u); result.x = float(seed);
	seed = (seed * 1103515245u + 12345u); result.y = float(seed);
	seed = (seed * 1103515245u + 12345u); result.z = float(seed);
	return timeElapsed * strength * ((result / 4294967296.0) - vec3(0.5,0.5,0.5));
}

void main() {
	// get all particle data
	vec3 position = gl_PositionIn[0].xyz;
	float size = gl_PositionIn[0].w;

	vec4 tmp = texelFetchBuffer(tboVelocity, gl_PrimitiveIDIn);
	vec3 velocity = tmp.xyz;
	float lifetime = tmp.w;

	// update lifetime and discard dead particles
	lifetime -= timeElapsed;
	if (lifetime <= 0.0) { return; }

	// update position and size
	position += velocity * timeElapsed;
	size += 0.3 * size * timeElapsed;
	
	// read current random seed
	seed = texelFetchBuffer(tboRandom, gl_PrimitiveIDIn).x;

	// update velocity by applying force fields
	velocity += dampField(velocity, 0.5);
	velocity += constField();
	velocity += noiseField(1.0);
	
	// write vertex
	gl_Position = vec4(position, size);
	velocity_out = vec4(velocity, lifetime);
	EmitVertex();
}

Da wir GL_POINTS rendern ist ein Primitiv einen Vertex groß. Aus diesem Grund eignet sich die eingebaute Variable gl_PrimitiveIDIn perfekt als Arrayindex. Via TexelFetch können wir also Velocity, Lebenszeit und aktuellen Seed auslesen.

Ist die Lebenszeit eines Partikels abgelaufen, brechen wir die Verarbeitung dieses Partikels ab. Die Funktion EmitVertex() wird also nicht aufgerufen und der Partikel somit unterdrückt. Der entscheidende Vorteil von Transform-Feedback an dieser Stelle ist nun, das beim schreiben in den Outputbuffer keine Lücke entsteht. Es tritt also keine Fragmentierung auf. Am Ende des Updatevorgangs befinden sich alle Partikel aufgeräumt am Anfang des Buffers. Mit einem Query lässt sich auch die Anzahl der in den Outputbuffer geschriebenen Partikel auslesen, beim nächsten Frame wissen wir also exakt wie viele Partikel wir rendern müssen.

Bezüglich des verwendeten Zufallsgenerators in der Funktion noiseField(float) sollte man bei Bedarf den Artikel GLSL Noise konsultieren. Der Rest des Update-Shaders sollte eigentlich klar sein.

Emitter

Erweiterungen

Nun wollen wir das bestehende Partikelsystem erweitern. Es sollte klar sein das die hier beschriebenen Dinge sehr viel Rechenleistung benötigen. Diese Aktionen müssen also möglicherweise über mehrere Frames verteilt werden.

Partikel/Partikel-Interaktion

Sortieren nach Z

Transparente Schatten (mit Self-Shadows)