Mitte der 1990er Jahre entstand in der Demoszene ein wunderschöner Effekt, den man “2D Blobs” nannte. Er wirkt, wie tanzende Wassertropfen auf einer Scheibe, die sich immer wieder vereinen und anschließend trennen. Diesen Effekt können wir als Shader in GameMaker Studio nachprogrammieren.
Der Effekt sieht so aus:
Du kannst bei den 2D-Blobs im Prinzip alles anpassen. In meiner kleinen Demo “NO RUST” gibt es einen ähnlichen Effekt, der aber etwas simpler gestrickt ist.
Das Prinzip
Die Idee hinter den Blobs basiert auf der Metaball-Technik. Dies ist eine Methode, um weiche, organische Formen zu erzeugen, die miteinander verschmelzen können.
Stell Dir vor, jeder Blob ist wie ein kleiner “Magnet”, der eine Kraft ausstrahlt. Diese Kraft ist in der Mitte des Blobs am stärksten und wird schwächer, je weiter Du davon weg bist. Wenn Du mehrere dieser Blobs hast, summieren sich ihre Kräfte in der Nähe zueinander.
Wenn zwei Blobs nahe genug beieinander sind, addieren sich ihre Kräfte. Dadurch sieht es so aus, als würden sie miteinander verschmelzen und eine größere Form bilden.
Im Grunde genommen funktioniert die Metaball-Technik so: Jeder Blob beeinflusst die Umgebung um sich herum. Wenn die “Kraft” eines oder mehrerer Blobs an einem Punkt auf dem Bildschirm stark genug ist, siehst Du dort die Form des Blobs. Andernfalls bleibt der Bereich unsichtbar oder leer.
Das Ergebnis sind glatte, organisch wirkende Formen, die wie Tropfen aussehen, die zusammenlaufen, wenn sie sich nahekommen.
Diese Technik verwendet eine Funktion (in unserem Fall die meta()
-Funktion), die für jeden Punkt auf dem Bildschirm die Entfernung zu einem Blob berechnet.
Jeder Blob hat eine gewisse “Kraft” oder “Einfluss” auf die Pixel um sich herum. Diese Kraft nimmt mit der Entfernung ab, was durch die Division des Blob-Radius durch die Entfernung (kRadius / d(p, b)
) erreicht wird.
Bewegung der 2D-Blobs
Die Bewegung der 2D-Blobs wird durch eine Kombination von Sinus- und Cosinus-Funktionen realisiert. Das ergibt eine fließende, oszillierende Bewegung in alle Richtungen. Die Zeitvariable time
sorgt dafür, dass die 2D-Blobs animiert sind und sich dynamisch im Raum bewegen.
Jede Blob-Position wird durch eine individuelle Berechnung festgelegt, die von der Zeit abhängt. Dadurch bewegen sich alle Blobs auf unterschiedliche Art und Weise, was das Bild lebendiger macht.
Farbverlauf
Für den Farbverlauf der Blobs wird die mix()
-Funktion verwendet, um eine Mischung zwischen zwei Farben (im Beispiel Gold und Orange) zu erzeugen. Diese Mischung hängt von der Blob-“Intensität” ab, die sich nach der Nähe des Pixels zum Blob-Zentrum richtet.
In der Nähe des Blob-Kerns ist die Intensität hoch, wodurch eine goldene Farbe entsteht, während an den Rändern die Intensität abnimmt und die Farbe zu Orange wechselt.
Farbmischung und Hintergrund
Der Hintergrund ist ein dunkler Farbton (#4C4A4B), der einen guten Kontrast zu den leuchtenden Blobs bietet. Du kannst ihn ändern oder – über das GM-Objekt – sogar ein Bild verwenden.
Schließlich wird die Blob-Farbe mithilfe der mix()
-Funktion mit dem Hintergrund gemischt, um eine weiche Kante und Übergänge zu erzeugen.
Shader sh_2d_blobs
Nach so viel Theorie kommen wir endlich zum Code. Da die Berechnungen ein wenig komplexer sind als sonst, habe ich ihn detaillierter kommentiert.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | uniform float time; // Animierte Zeitvariable für die Bewegung uniform vec2 resolution; // Auflösung des Viewports varying vec2 v_vTexcoord; // Texturkoordinaten des Fragments #define kLowThres 1.2 // Schwellenwert, ab dem die Blobs sichtbar werden #define kRadius 0.06 // Radius der Blobs #define pi 3.1415926 // Pi-Wert zur Berechnung der Bewegungen // Berechnung der Distanz zwischen zwei Punkten im 2D-Raum float d(vec2 p1, vec2 p2) { return sqrt(pow(p1.x - p2.x, 2.0) + pow(p1.y - p2.y, 2.0)); } // Metaball-Funktion zur Berechnung der Stärke eines Blobs an einem bestimmten Punkt float meta(vec2 p, vec2 b) { return kRadius / d(p, b); } void main(void) { // Transformation der Texturkoordinaten in den Bereich [-1, 1] unter Berücksichtigung des Seitenverhältnisses vec2 p = (2.0 * v_vTexcoord - 1.0) * vec2(resolution.x / resolution.y, 1.0); float c = 0.0; // Variable zur Speicherung der "Kraft" aller Blobs an der aktuellen Pixelposition vec4 mixedColor = vec4(0.0); // Variable zur Speicherung der endgültigen Blob-Farbe // Schleife zur Berechnung der Position und Einfluss aller 10 Blobs for (float i = 0.0; i < 10.0; i++) { // Bewegung der Blobs durch Kombination von Sinus- und Cosinus-Funktionen float xMovement = sin(time * 0.5 + i * pi / 5.0) * 0.75 + cos(time * 0.2 + i * pi / 3.0) * 0.25; float yMovement = cos(time * 0.4 + i * i * pi / 6.0) * 0.5 + sin(time * 0.3 + i * pi / 4.0) * 0.35; vec2 b = vec2(xMovement, yMovement); // Berechnete Position des i-ten Blobs c += meta(b, p); // Summiere den Einfluss des Blobs auf die Position des aktuellen Pixels } // Farbverlauf für die Blobs: Vom goldenen Kern zu einem orangefarbenen Rand if (c > kLowThres) { // Wenn die Blob-Intensität über dem Schwellenwert liegt float intensity = (c - kLowThres) / (c); // Normierung der Intensität für den Farbverlauf vec3 color = mix(vec3(1.0, 0.84, 0.0), vec3(1.0, 0.5, 0.0), intensity); // Farbverlauf von Gold zu Orange mixedColor = vec4(color, 1.0); // Setze die gemischte Farbe } // Hintergrundfarbe in einem dunklen Grauton (#4C4A4B) vec4 background = vec4(0.298, 0.290, 0.294, 1.0); // Mische die Blob-Farbe und den Hintergrund sanft vec4 finalColor = mix(background, mixedColor, mixedColor.a); // Setze die endgültige Fragmentfarbe gl_FragColor = finalColor; } |
Objekt o_2d_blobs
Natürlich brauchen wir noch ein Objekt, um den Effekt einzubinden. Außerdem brauchen wir zwei Events im Objekt.
Even Create
1 2 | time = 0; time_add = 0.05; // je höher, desto schneller |
Event Draw
1 2 3 4 5 6 | time += time_add; shader_set(sh_2d_blobs); shader_set_uniform_f(shader_get_uniform(sh_2d_blobs,"resolution"), display_get_gui_width(), display_get_gui_height()); shader_set_uniform_f(shader_get_uniform(sh_2d_blobs,"time"),time); draw_rectangle(0, 0, room_width, room_height, 0); shader_reset(); |
Wir projizieren den Effekt auf ein Rechteck, könnten aber auch eine andere Form oder gar einen Sprite verwenden.
Das war es auch schon wieder. Nun kannst Du den Effekt nach Deinen Vorstellungen anpassen.
Weiterführende Links
Shader-Programmierung 1: Grundlagen und Sprites
Shader-Effekt: Tunnel mit Maussteuerung
Shader-Effekt: CRT
Bildeinblendung per Shader
GameMaker Quicktipp: Probleme mit Shaderunterstützung