»Demos« nannte man in der 90er Jahren kleine Programme, die spektakuläre Grafiken und Musik auf dem Bildschirm darstellten und dabei die Grenzen der verfügbaren Hardware ausreizten. Die Demoszene ist eine Subkultur, die sich um die Entwicklung von Demos und die Organisation von Demopartys dreht. In diesem Tutorial programmieren wir eine kleine Feueranimation in Ruby, die in der Demoszene relativ beliebt ist.
Stelle zuerst sicher, dass du keinen Ordner geöffnet hast. Um sicherzugehen, drücke einfach den Shortcut für »Ordner schließen«: StrgK und dann F. Dein Workspace sollte jetzt ungefähr so aussehen:
Der Feuereffekt basiert auf einem Farbverlauf von Schwarz über Rot, Orange und Gelb bis Weiß. Wir verwenden das Pixelflow Canvas mit einer Palette von 64 Farben, wobei kleine Farben für kalte Pixel und große Farben für heiße Pixel stehen. Wir können den Farbverlauf in 4 Abschnitte unterteilen:
Um den Farbverlauf zu programmieren, beginnen wir mit einem kleinen Programm, das uns die ersten 64 Farben ausgibt. So können wir kontrollieren, ob die Farben korrekt sind. Erstelle eine neue Datei namens fire.rb
und füge folgenden Code ein:
require 'pixelflow_canvas' Pixelflow::Canvas.new(64, 1, :palette) do (0...64).each do |i| draw_pixel(i, 0, i) end end
Starte das Pixelflow Canvas, indem du StrgShiftP oder F1 drückst und dann »Show Pixelflow Canvas« eingibst.
Führe das Programm aus, indem du im Terminal ruby fire.rb
eingibst. Da standardmäßig die VGA-Palette verwendet wird, sehen wir die ersten 64 Farben der VGA-Palette:
Diese 64 Farben sollen nun durch unseren Farbverlauf ersetzt werden. Dazu schauen wir uns den Farbverlauf im Detail an:
Im ersten Abschnitt sehen wir, dass Grün und Blau auf 0 gesetzt sind, während Rot von 0 auf 50% ansteigt. Da jeder Farbkanal einen Wert von 0 bis 255 annehmen kann, entspricht 50% einem Wert von ca. 128. Der Farbverlauf beginnt also bei Schwarz (0, 0, 0) und endet bei Dunkelrot (128, 0, 0). Der erste Abschnitt umfasst 16 Farben, wir müssen also den Rotwert in jedem Schritt um 8 erhöhen, um nach 16 Schritten bei 128 zu landen. Ändere den Code wie folgt:
require 'pixelflow_canvas' Pixelflow::Canvas.new(64, 1, :palette) do (0...16).each do |i| set_palette(i, i * 8, 0, 0) end (0...64).each do |i| draw_pixel(i, 0, i) end end
(0...16)
. Der dritte Punkt schiebt die hintere Zahl aus dem Bereich heraus, sodass die Schleife nur von 0 bis 15 läuft. Alternativ könnten wir auch (0..15)
schreiben (mit zwei Punkten).
Dein Bild sollte jetzt so aussehen:
Im zweiten Abschnitt steigt der Grünwert von 0 auf 50% an, während Rot weiter von 50% auf 100% wächst. Blau bleibt weiterhin auf 0.
Der Farbverlauf geht also von Dunkelrot (128, 0, 0) zu Orange (255, 128, 0). Füge folgenden Code direkt hinter dem set_palette
-Aufruf ein:
set_palette(i + 16, i * 8 + 128, i * 8, 0);
Dein Farbverlauf sollte nun so aussehen:
Im dritten Abschnitt steigt der Grünwert von 50% auf 100% an, während Blau von 0 auf 50% ansteigt. Rot bleibt bei 100%. Der Farbverlauf geht also von Orange (255, 128, 0) zu Gelb (255, 255, 128). Füge folgenden Code ein:
set_palette(i + 32, 255, i * 8 + 128, i * 8);
Dein Farbverlauf sollte nun so aussehen:
Im vierten Abschnitt steigt der Blauwert von 50% auf 100% an, während Rot und Grün bei 100% bleiben. Der Farbverlauf geht also von Gelb (255, 255, 128) zu Weiß (255, 255, 255). Füge folgenden Code ein:
set_palette(i + 48, 255, 255, i * 8 + 128);
Geschafft! Dein Farbverlauf sollte nun so aussehen:
Wir schreiben nun unser Programm ein bisschen um, um das Grundgerüst für eine Animation zu erhalten, bei dem immer nur am unteren Rand des Bildschirms ein helles Rechteck erscheint, das wir anschließend animieren werden, so dass es wie ein Feuer aussieht. Ändere dein Programm wie folgt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
require 'pixelflow_canvas' Pixelflow::Canvas.new(256, 128, :palette) do # Doublebuffering aktivieren set_draw_mode(:buffered) # Farbverlauf erstellen (0...16).each do |i| set_palette(i, i * 8, 0, 0) set_palette(i + 16, i * 8 + 128, i * 8, 0) set_palette(i + 32, 255, i * 8 + 128, i * 8) set_palette(i + 48, 255, 255, i * 8 + 128) end # Endlosschleife loop do # heißes Rechteck am unteren Bildschirmrand zeichnen set_color(63) fill_rect(10, 126, 245, 127) # Bild anzeigen flip() end end |
Anmerkungen:
flip()
aufrufen, damit der Bildschirm aktualisiert wird.Das Ergebnis sieht jetzt noch relativ unspektakulär aus, aber wir sehen schon das helle Rechteck am unteren Rand des Bildes (und auch wenn es noch nicht so aussieht, werden schon regelmäßig neue Frames gezeichnet):
Um einen Feuereffekt zu erzielen, verwenden wir eine Technik aus der Bildbearbeitung, die als »Filterkernel« (bzw. Convolution Matrix oder Faltungsmatrix) bezeichnet wird.
Filterkernel werden für verschiedene Effekte verwendet, z. B. Weichzeichnen, Schärfen oder Kantenerkennung. Dabei wird ein kleines Quadrat von Pixeln um einen bestimmten Pixel herum betrachtet, und die Farben dieser Pixel werden mit einem bestimmten Gewicht multipliziert und addiert. Der resultierende Wert wird dann als neuer Farbwert für den betrachteten Pixel verwendet.
Hier siehst du ein paar Beispiele:
0 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
1 | 1 | 1 |
1 | 1 | 1 |
1 | 1 | 1 |
0 | -1 | 0 |
-1 | 4 | -1 |
0 | -1 | 0 |
Im ersten Beispiel (»Identity«) wird der Pixelwert unverändert übernommen und alle umliegenden Pixel ignoriert. Im zweiten Beispiel (»Box blur«) wird der Pixelwert mit den Werten der umliegenden Pixel gemittelt, um einen Weichzeichnungseffekt zu erzielen. Im dritten Beispiel (»Edge detection«) wird der Pixelwert so berechnet, dass Kanten im Bild hervorgehoben werden.
Wird ein Filterkernel wiederholt auf ein Bild angewendet, entsteht ein Effekt, der sich über die gesamte Bildfläche ausbreitet. Der Feuereffekt verwendet einen speziellen Filterkernel, der die Farben von Pixeln nach oben bewegt und dabei abkühlt. Der Filterkernel sieht folgendermaßen aus:
0 | 0 | 0 |
1 | 0 | 1 |
0 | 2 | 0 |
Der Filterkernel ist also sehr ähnlich zu einem »Box blur«-Filter, jedoch wird hier der untere Pixel doppelt gewichtet und der obere Pixel weggelassen, so dass sich die Farbe minimal nach oben bewegt.
Wir können den Filterkernel in unserem Programm verwenden, um den Feuereffekt zu erzeugen. Dazu müssen wir den Filterkernel auf jeden Pixel anwenden und die Farben entsprechend anpassen. Füge folgenden Code über der flip()
-Zeile ein:
# Filterkernel auf jedes Pixel anwenden (0...128).each do |y| (0...256).each do |x| # Farbwerte der Nachbarpixel einsammeln c = get_pixel(x, y + 1) * 2 c += get_pixel(x - 1, y) c += get_pixel(x + 1, y) # Summe durch vier teilen c /= 4 # Pixel setzen set_pixel(x, y, c) end end
Wir gehen zeilenweise durch das Bild und in jeder Zeile betrachten wir jeden Pixel. Für jeden Pixel addieren wir die Farbwerte der umliegenden Pixel und teilen das Ergebnis durch 4, um den Mittelwert zu erhalten. Diesen Mittelwert setzen wir dann als neuen Farbwert für den betrachteten Pixel.
Deine Animation sollte nach einer kleinen Weile nun so aussehen:
Wir sehen, dass die hellen Pixel langsam nach oben wandern und dabei abkühlen, wobei sie eine andere Farbe annehmen. Das Problem ist jedoch, dass das Ergebnis zu glatt und nicht wirklich wie ein Feuer aussieht. Um das zu beheben, fügen wir noch ein paar zufällige Farbvariationen hinzu. Füge folgenden Code vor der set_pixel
-Zeile ein:
# Zufällige Variation hinzufügen c += rand(7) - 3
Da uns rand(7)
eine zufällige Zahl im Bereich von 0…6 zurückgibt, subtrahieren wir 3, um Werte von -3 bis +3 zu erhalten. Das Ergebnis wird dann auf den Mittelwert addiert, um eine zufällige Variation zu erzeugen. Das Ergebnis sieht schon vielversprechender aus:
Das Problem ist, dass wir die Farben in der Palette direkt als Index verwenden, ohne zu überprüfen, ob sie im Bereich von 0 bis 63 liegen. Wenn wir also eine Farbe von -3 oder +3 erhalten, landen wir außerhalb des gültigen Bereichs und erhalten eine falsche Farbe. Um das zu beheben, fügen wir folgenden Code vor der set_pixel
-Zeile ein:
# Wertebereich auf 0 bis 63 begrenzen c = c.clamp(0, 63)
Dein Ergebnis sollte nun so aussehen:
Wir sind nun fast fertig – wir müssen nur noch dafür sorgen, dass nicht die gesamte Luft »glüht« – wir können dies erreichen, in dem wir die zufällige Variation nur anwenden, wenn unser Pixel nicht schon ganz schwarz ist. Ändere die Zeile mit dem rand
-Aufruf wie folgt:
c += rand(7) - 3 if c > 0
if
-Syntax auch die Kurzform if
am Ende eines Ausdrucks verwenden, um den Ausdruck nur dann auszuführen, wenn die Bedingung erfüllt ist. Das ist besonders nützlich, wenn wir nur eine einfache Anweisung ausführen wollen.
Dein Feuereffekt sollte nun so aussehen:
Das gesamte Programm sieht nun so aus:
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 |
require 'pixelflow_canvas' Pixelflow::Canvas.new(256, 128, :palette) do # Doublebuffering aktivieren set_draw_mode(:buffered) # Farbverlauf erstellen (0...16).each do |i| set_palette(i, i * 8, 0, 0) set_palette(i + 16, i * 8 + 128, i * 8, 0) set_palette(i + 32, 255, i * 8 + 128, i * 8) set_palette(i + 48, 255, 255, i * 8 + 128) end # Endlosschleife loop do # heißes Rechteck am unteren Bildschirmrand zeichnen set_color(63) fill_rect(10, 126, 245, 127) # Filterkernel auf jedes Pixel anwenden (0...128).each do |y| (0...256).each do |x| # Farbwerte der Nachbarpixel einsammeln c = get_pixel(x, y + 1) * 2 c += get_pixel(x - 1, y) c += get_pixel(x + 1, y) # Summe durch vier teilen c /= 4 # Zufällige Variation hinzufügen c += rand(7) - 3 if c > 0 # Wertebereich auf 0 bis 63 begrenzen c = c.clamp(0, 63) # Pixel setzen set_pixel(x, y, c) end end # Bild anzeigen flip() end end |
In diesem Tutorial haben wir eine kleine Feueranimation in Ruby programmiert, die auf einem Farbverlauf basiert und einen speziellen Filterkernel verwendet, um die Farben zu animieren. Wir haben gesehen, wie Filterkernel in der Bildbearbeitung verwendet werden und wie sie in der Programmierung eingesetzt werden können, um Effekte zu erzielen. Die Feueranimation ist ein beliebter Effekt in der Demoszene und kann mit ein wenig Übung und Experimentieren noch weiter verbessert werden.
Hier sind ein paar Vorschläge, wie du die Animation weiter verbessern könntest: