shader Zufallsgenerator
Inhaltsverzeichnis
Shadername
Zurück zur Shadersammlung
Beschreibung | Autor | Version |
---|---|---|
Pseudozufallszahlen im Shader mit GL_EXT_gpu_shader4 | Coolcat | 1.0 |
Bilder und Videos
Voraussetzungen
Die Extension GL_EXT_gpu_shader4 wird zwingend benötigt, ohne volle 32bit Integer-Arithmetik lassen sich keine sinnvollen Pseudozufallszahlen erzeugen.
Funktionsweise
Die Extension GL_EXT_gpu_shader4 macht unter anderem 32bit Integer-Arithmetik im Shader verfügbar. Dies erlaubt es einen lineareren Kongruenzgenerator zu verwenden um beliebig viele Pseudozufallszahlen direkt im Shader zu erzeugen. Die Pseudozufallszahlen in C/C++/Delphi werden übrigens auf die gleiche Weise erzeugt. Im wesentlichen braucht man nur eine Multiplikation, eine Addition und einmal Modulo um von einer Zufallszahl zur nächsten zu kommen.
s := (a * s + c) mod m
Wählt man gute Werte für a, c und m erhält man mit diesen Operationen halbwegs gleich verteilte Pseudozufallszahlen. Für m wählt man häufig einfach 2^32 um den natürlichen Integer-Überlauf auszunutzen. Die so erzeugten Zahlen sind natürlich deterministisch. Verwendet man den gleichen Seed s, erhält man immer die gleiche Zahlenfolge. Man muss also dafür sorgen, dass der Shader für jeden Vertex (bzw. Pixel) unterschiedliche Seeds bekommt. Dafür gibt es verschiedene Lösungen. Beispielsweise lässt sich dies über ein in jedem Frame neu mit Zufallszahlen initialisiertes TextureBufferObject (TBO) realisieren. Optional könnte man mittels Transform-Feedback die nach Beendigung des Vertexshaders veränderten Seeds auch wieder zurück in ein TBO schreiben. Dies würde eine Neuinitialisierung durch die CPU ersparen.
In vielen Fällen möchte man aber gerade den Determinismus ausnutzen und reproduzierbare Zahlenfolgen erzeugen. Denkbar wäre zum Beispiel eine Kombination der Vertexkoordinaten, etwa:
ivec3 position = ivec3(gl_Vertex);
seed = unsigned int(position.x * (2 + position.y) * (5 + position.z));
Genauso könnte der Seed könnte also auch aus einem zufällig gesetztem Uniform kommen oder vielleicht auch einer Kombination aus gl_VertexID und einem Uniform. Die Möglichkeiten sind unbegrenzt.
Bemerkungen
Man sollte nicht auf die Idee kommen die Seedwerte als float zwischen zu speichern. Die Konvertierung in float sorgt dafür das die erzeugten Zufallszahlen sehr schnell degenerieren und nicht mehr gleich verteilt sind.
Gute Werte für a, c und m findet man z.B. bei Wikipedia. Man sollte darauf achten, dass der Zufallsgenerator, der die Textur für die Ausgangswerte initialisiert nicht den gleichen lineareren Kongruenzgenerator verwendet. Da die erzeugten Zahlen deterministisch sind, würde man sonst für alle Vertices/Pixel die gleichen, jeweils nur um eine Generation verschobenen, Zahlen erhalten.
Code
Basis-Shader
Dies ist der Basis-Shader mit zwei einfachen Hilfsfunktionen.
#extension GL_EXT_gpu_shader4 : require
// ...
uniform usamplerBuffer tboRandomSeed;
unsigned int seed;
// ...
/* return pseudorandom float with values between 0 and 1. */
float random() {
seed = (seed * 1103515245u + 12345u);
return float(seed) / 4294967296.0;
}
/* return pseudorandom vec3 with values between -1 and 1. */
vec3 random3() {
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 (result / 2147483648.0) - vec3(1,1,1);
}
void main()
{
// seed source is a TextureBufferObject
seed = texelFetchBuffer(tboRandomSeed, gl_VertexID).x;
float rnd = random();
vec3 rnd3 = random3();
// ...
}
Reproduzierbares Zufallswirbel-Feld
Ein reproduzierbares Zufallswirbel-Feld für ein GPU-Partikelsystem. Für jede ganzzahlige Position wird ein reproduzierbarer Pseudozufallsvektor erzeugt. Für jeden Partikel wird berechnete Vektor der umliegenden 8 Ganzzahl-Positionen interpoliert. Da das Feld in jedem Frame identisch ist ergibt sich eine stetige Verwirbelung. Man könnte die gesamte Berechnung auch für mehrere Initialseeds (via Uniform) durchführen und zwischen diesen interpolieren. Dies würde es erlauben, dass sich das Wirbel-Feld laufend langsam ändert.
#extension GL_EXT_gpu_shader4 : require
// ...
vec3 randomVec(unsigned int seed, ivec3 pos) {
seed *= unsigned int(pos.x * (2 + pos.y) * (5 + pos.z));
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 (result / 2147483648.0) - vec3(1,1,1);
}
vec3 noiseField(unsigned int seed, vec3 position) {
vec3 f = floor(position);
ivec3 min = ivec3(f);
ivec3 max = min + 1;
f = position - f;
vec3 g = 1.0 - f;
vec3 result = vec3(0,0,0);
result += randomVec(seed, ivec3(min.x, min.y, min.z)) * g.x * g.y * g.z;
result += randomVec(seed, ivec3(min.x, min.y, max.z)) * g.x * g.y * f.z;
result += randomVec(seed, ivec3(min.x, max.y, min.z)) * g.x * f.y * g.z;
result += randomVec(seed, ivec3(min.x, max.y, max.z)) * g.x * f.y * f.z;
result += randomVec(seed, ivec3(max.x, min.y, min.z)) * f.x * g.y * g.z;
result += randomVec(seed, ivec3(max.x, min.y, max.z)) * f.x * g.y * f.z;
result += randomVec(seed, ivec3(max.x, max.y, min.z)) * f.x * f.y * g.z;
result += randomVec(seed, ivec3(max.x, max.y, max.z)) * f.x * f.y * f.z;
return result;
}
void main()
{
// ...
unsigned int seed = 12345u;
velocity += timeElapsed * noiseField(seed, position*0.2);
// ...
}