Da wir im dritten Shader-Tutorial 2D und Pseudo-3D-Effekte behandelt haben, möchte ich immer mal wieder entsprechende Effekte als einzelne Tutorials präsentieren. Einerseits, weil das viel Spaß machen kann, andererseits, weil bekanntlich Übung den Meister macht.
Im heutigen Effekt geht es um Warping. Darunter versteht man Neigungen und Verzerrungen. Mit solchen Effekten lassen sich Wasser, Lava oder – wie im hiesigen Beispiel – surreale Effekte erzeugen. Das Resultat sieht dann so aus:
Hierbei handelt es sich nur um eine von vier Farbvarianten, die ich im Code eingebaut habe. D. h. wer das nachbaut, kann per Tastendruck durchschalten, sich für eine Variante entscheiden oder etwas ganz anderes kreieren. Außerdem ist der Effekt so aufgebaut, dass man ihn im Shader mit vielen Parametern steuern und verändern kann.
sh_warping01
Den Shader habe ich sh_warping01
genannt. Im Vertex-Shader muss nichts geändert werden, der GameMaker-Code reicht völlig. Den Fragment-Shader habe ich recht ausführlich kommentiert, weshalb ich auf zusätzliche Erklärungen weitestgehend verzichte.
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | // Warping-Effekt uniform float time; uniform vec2 resolution; uniform int version; // Farbwerte vec3 color1; vec3 color2; vec3 color3; vec3 color4; /** * Generiert eine Pseudo-Zufallszahl basierend auf einer Eingabe-Koordinate. * Die Funktion verwendet ein einfaches Verfahren, um die Zufallszahl zu berechnen, * indem sie das Skalarprodukt mit einer Konstanten durchführt und das Ergebnis durch 29873.549 moduliert. * Das Ergebnis liegt im Bereich von 0 bis 1. * * @param co Die Eingabe-Koordinate zur Generierung der Zufallszahl. * @return Eine Pseudo-Zufallszahl zwischen 0 und 1. */ float random(vec2 co) { return mod(sin(dot(co.xy, vec2(27.183, 57.481))) * 29873.549, 1.0); } /** * Generiert gewarptes Fraktalrauschen basierend auf einer Eingabe-Koordinate. * Die Funktion verwendet das „random" Verfahren, um Pseudo-Zufallszahlen für die Eckpunkte eines Gitters zu generieren, * und interpoliert diese Werte basierend auf dem Bruchteil der Eingabe-Koordinate. * Das Ergebnis ist ein gewarptes Fraktalrauschen mit einer glatten Übergangsfunktion. * * @param p Die Eingabe-Koordinate zur Generierung des gewarpten Fraktalrauschens. * @return Das gewarpte Fraktalrauschen im Bereich von 0 bis 1. */ float warpNoise(in vec2 p) { vec2 ip = floor(p); vec2 u = fract(p); float a = random(ip); float b = random(ip + vec2(1.0, 0.0)); float c = random(ip + vec2(0.0, 1.0)); float d = random(ip + vec2(1.0, 1.0)); vec2 u2 = u * u * (3.0 - 2.0 * u); float res = mix(a, b, u2.x) + (c - a) * u2.y * (1.0 - u2.x) + (d - b) * u2.x * u2.y; return res * res; } /** * Generiert ein Fraktalrauschen mit Warping-Effekt. * Verwendet multiple Oktaven mit anpassbarer Amplitude, Frequenz und Persistenz. * * @param c Die Koordinaten, für die das Rauschen berechnet werden soll. * @return Der Wert des Fraktalrauschens an den gegebenen Koordinaten. */ float fieryFractal(vec2 c) { float f = -0.20; float amplitude = 1.1; float frequency = 1.31; float lacunarity = 2.0; float persistence = 0.525; int octaves = 4; for (int i = 0; i < octaves; i++) { f += amplitude * warpNoise(c * frequency); frequency *= lacunarity; amplitude *= persistence; } return f; } /** * Generiert eine Textur mit einem fließenden, geschmolzenen Effekt. * Verwendet das „fieryFractal" Fraktalrauschen für die Berechnung der Struktur. * * @param p Die Koordinaten, für die das Muster berechnet werden soll. * @param scale Der Skalierungsfaktor für das Muster. * @return Ein Vektor mit zwei Werten: das Ergebnis des Musters und die Länge eines internen Vektors. */ vec2 moltenFlow(vec2 p, float scale) { float t = time * 0.5; vec2 q = vec2( fieryFractal(p + vec2(1.0) * sin(t)), fieryFractal(p + vec2(1.0) * cos(t)) ); vec2 r = vec2( fieryFractal(p + scale * q + vec2(1.7, 9.2) + 0.15 * time), fieryFractal(p + scale * q + vec2(8.3, 2.8) + 0.126 * time) ); return vec2( fieryFractal(p + scale * r), length(q) ); } void main() { if (version == 1) { color1 = vec3(1.000000, 0.039216, 0.039216); // Knallrot color2 = vec3(1.000000, 0.258824, 0.031373); // Orange color3 = vec3(0.956863, 0.549020, 0.380392); // Hellbraun color4 = vec3(1.000000, 0.764706, 0.396078); // Gelb } else if (version == 2) { color1 = vec3(0.258824, 0.737255, 0.901961); // Hellblau color2 = vec3(0.921569, 0.482353, 0.435294); // Hellrot color3 = vec3(0.050980, 0.576471, 0.415686); // Dunkelgrün color4 = vec3(0.972549, 0.905882, 0.301961); // Hellgelb } else if (version == 3) { color1 = vec3(1.000000, 0.003922, 0.376471); // Knallrosa color2 = vec3(0.003922, 0.835294, 0.886275); // Hellblau color3 = vec3(0.960784, 0.960784, 0.960784); // Weiß color4 = vec3(0.003922, 0.262745, 0.823529); // Dunkelblau } else if (version == 4) { color1 = vec3(1.000000, 0.105882, 0.000000); // Orangerot color2 = vec3(1.000000, 0.882353, 0.027451); // Gelb color3 = vec3(0.188235, 0.956863, 0.647059); // Hellgrün color4 = vec3(0.027451, 0.325490, 0.678431); // Blau } vec2 c = 750.0 * gl_FragCoord.xy / resolution.xy; // Skalierung / Zoom float scale = 0.015; // Änderung der Position über die Zeit vec2 offset = vec2(0.0, 0.6) * time; // Verschiebungseffekt in x- und y-Richtung // Berechnung der Flussmuster und des Hauptwerts vec2 q = moltenFlow(c * scale, 1.0).xy; float f = moltenFlow(c * scale + offset, 1.0).x; // Mischen der Farben basierend auf den Werten vec3 col = mix(color1, color2, clamp(f * f * 4.0, 0.0, 1.0)); col = mix(col, color3, clamp(q.y, 0.0, 1.0)); col = mix(col, color4, clamp(length(q), 0.0, 1.0)); // Setzen der Ausgabefarbe gl_FragColor = vec4((0.2 * f * f * f + 0.6 * f * f + 0.5 * f) * col, 1.0); } |
Das Ganze sieht auf den ersten Blick wesentlich kompliziert aus, als es ist. Die meisten einfachen Änderungen kann man in den Funktionen fieryFractal
und main
vornehmen.
In der Funktion fieryFractal
haben die sechs Variablen am Anfang einen sehr großen Einfluss auf das Aussehen. Bei octaves
sollte man darauf achten, dass der Effekt rechenintensiver wird, je größer diese Zahl ist.
Das GM-Objekt
Normalerweise müsste ich, aufgrund des Shader-Tutorials, das Objekt nicht mehr zeigen. Durch das Umschalten und der version
-Variable halte ich es dennoch für angebracht. Ich schalte die Version mit dem Nummernblock durch, ihr könnt euch natürlich für andere Tasten, Maustaste oder Mausrad entscheiden. Natürlich lässt es sich auch zeitlich steuern.
Create-Event
1 2 3 | time = 0; time_add = 0.02; version = 1; |
Step-Event
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | if (keyboard_check_pressed(vk_numpad1)) { version = 1; } else if (keyboard_check_pressed(vk_numpad2)) { version = 2; } else if (keyboard_check_pressed(vk_numpad3)) { version = 3; } else if (keyboard_check_pressed(vk_numpad4)) { version = 4; } |
Draw-Event
1 2 3 4 5 6 7 8 | time += time_add; shader_set(sh_warping01); shader_set_uniform_i(shader_get_uniform(sh_warping01,"version"), version); shader_set_uniform_f(shader_get_uniform(sh_warping01,"resolution"), display_get_gui_width(), display_get_gui_height()); shader_set_uniform_f(shader_get_uniform(sh_warping01,"time"), time); draw_surface_ext(application_surface, 0, 0, 1, 1, 0, c_white, 0); shader_reset(); |
Wichtig: Die Variable version
übergeben wir per shader_set_uniform_i
, da es sich um einen Integer handelt. Bisher haben wir Variablen nur mit shader_set_uniform_f
übergeben.
Übrigens: Ich habe den Effekt so gestaltet, dass er langsam nach oben scrollt. So kann man ihn gleich für einen Textscroller, etwa Abspann oder Credits, nutzen.
Hier noch ein Beispiel, wo dieser Effekt – und viele weitere – verwendet wurde:
Weiterführende Links
Shader-Programmierung 1: Grundlagen und Sprites
Textscroller
Skripte für Textformatierung
Bitmap-Fonts im GameMaker