Wer ein Reto-Spiel entwickeln will, möchte i. d. R. auch, dass es so aussieht. Neben klassischen Pixelgrafiken und Reduzierung der Farbpalette denkt man früher oder später über einen CRT-Effekt nach. In GameMaker lässt sich das mit wenigen Zeilen Shader-Code bewältigen.
Was ist CRT?
Der Begriff CRT-Effekt bezieht sich auf visuelle Effekte oder Ästhetiken, die typischerweise mit CRT-Bildschirmen (Cathode Ray Tube) verbunden sind. CRTs waren früher die dominierende Technologie für Computermonitore und Fernseher, bevor sie von flacheren Bildschirmen wie LCDs und LEDs abgelöst wurden. Kurz gesagt: Bei dem Effekt versucht man auf einem flachen Bildschirm so zu tun, als wäre er ein Röhrenmonitor.
Einige der charakteristischen Merkmale des CRT-Effekts sind:
Scanlines
CRT-Bildschirme erzeugen Bilder, indem sie einen Elektronenstrahl über die Bildschirmoberfläche bewegen, der jedes Pixel zeilenweise beleuchtet. Dies kann zu sichtbaren horizontalen Linien führen, die als Scanlines bezeichnet werden.
Pixelartefakte
Die individuellen Phosphorpunkte auf einem CRT-Bildschirm können leicht erkennbar sein, was zu einer körnigen Textur führt. Dies kann insbesondere bei niedrigeren Auflösungen oder stark vergrößerten Bildern sichtbar sein.
Bildverzerrung und Geometrieabweichungen
CRTs neigen dazu, subtile Verzerrungen in Form von Krümmungen oder Verzerrungen am Bildschirmrand aufgrund ihrer physikalischen Eigenschaften zu erzeugen.
Farbbluten
Aufgrund der Art und Weise, wie CRTs Bilder erzeugen, können Farben leicht überlappen oder ausbluten, was zu einem weicheren und manchmal unklaren Erscheinungsbild führen kann.
Zumindest optional ist es eine sehr gute Wahl, den Effekt für nostalgische Momente anzubieten. In den Weiten des Internets findet man sehr viele Ansätze, um einen solchen Effekt zu realisieren. Häufig sind diese jedoch viel zu extrem. Die Krümmung und Störungen sind so stark, dass das Spielen auf Dauer keinen Spaß macht. Deshalb habe ich für meine Demo NO RUST einen eigenen Shader geschrieben.
Den Effekt kannst du am Anfang und am Ende der Demo sehen:
Shader-Code
Mein Shader heißt im GameMaker schlicht sh_crt
. Dabei verwende ich ausschließlich den Fragment-Shader. Wenn du einen neuen Shader nutzt, kannst du den Vertex-Shader so lassen, wie er ist.
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 | // Definiere die Texturkoordinaten für den aktuellen Pixel varying vec2 v_vTexcoord; // Uniform-Variablen für Zeit, Transparenz und Bildschirmauflösung uniform float time; uniform float alpha; uniform vec2 resolution; // Konstante, die die Intensität des CRT-Effekts definiert #define intensity 6.4 // Funktion, die die Scanlines für den CRT-Effekt erzeugt vec3 scanline(vec2 coord, vec3 screen) { // Subtrahiere eine Sinuswelle von der Farbe des Bildschirms, um den Scanline-Effekt zu erzeugen screen.rgb -= sin((coord.y + (time * 29.0))) * 0.02; return screen; } // Funktion, die die Verzerrungen des CRT-Effekts simuliert vec2 crt(vec2 coord, float bend) { // Zentriere die Koordinaten um den Ursprung coord = (coord - 0.5) * 2.0; // Skaliere die Koordinaten coord *= 1.1; // Verzerre die Koordinaten basierend auf dem Biegefaktor coord.x *= 1.0 + pow((abs(coord.y) / bend), 2.0); coord.y *= 1.0 + pow((abs(coord.x) / bend), 2.0); // Bringe die Koordinaten wieder zurück in den normalisierten Bereich coord = (coord / 2.0) + 0.5; return coord; } void main() { // Speichere die Texturkoordinaten des aktuellen Pixels vec2 uv = v_vTexcoord.xy; // Verformte Koordinaten durch die CRT-Funktion mit der Intensität des Effekts vec2 crtCoords = crt(uv, intensity); // Verwerfe den Pixel, wenn die Koordinaten außerhalb des Texturbereichs liegen if (crtCoords.x < 0.0 || crtCoords.x > 1.0 || crtCoords.y < 0.0 || crtCoords.y > 1.0) discard; // Lese die Farbe des Pixels aus der Textur vec3 color = texture2D(gm_BaseTexture, crtCoords).rgb; // Berechne die Bildschirmkoordinaten und wende den Scanline-Effekt an vec2 screenSpace = crtCoords * resolution; color = scanline(screenSpace, color); // Setze die finale Farbe des Pixels mit der Transparenz gl_FragColor = vec4(color, alpha); } |
Damit es nicht so extrem ist, beschränkt sich der Effekt darauf, die Texturkoordinaten zu verformen und Scanlines hinzuzufügen. Die Intensität kann entsprechend eingestellt werden, im Beispiel liegt sie bei 6,4.
Objekt
Nun müssen wir den Effekt nur noch in einem Objekt anwenden. Hierfür habe ich ein eigenes Objekt mit dem Namen o_pp_crt
.
Event Create
1 2 3 | time = 0; time_add = 0.01; alpha = 1; |
Event Draw-GUI
1 2 3 4 5 6 7 8 9 10 | time += time_add; draw_set_alpha(alpha); draw_set_color(c_black); draw_rectangle(0, 0, room_width, room_height, 0) shader_set(sh_crt); shader_set_uniform_f(shader_get_uniform(sh_crt,"resolution"), display_get_gui_width(), display_get_gui_height()); shader_set_uniform_f(shader_get_uniform(sh_crt,"time"),time); shader_set_uniform_f(shader_get_uniform(sh_crt,"alpha"),alpha); draw_surface_ext(application_surface, 0, 0, 1, 1, 0, c_white, alpha); shader_reset(); |
Alpha habe ich separat, weil ich das in der Demo noch extra steuere. Die Variable und die entsprechende Zeile im Draw-GUI-Event kannst du auch weglassen, wenn du sie nicht brauchst.
Weiterführende Links
Shader-Programmierung 1: Grundlagen und Sprites
Shader-Effekt: Warping
Shader-Effekt: Interferenz-Effekt