image: fire.webp:73:0

Feueranimation

»Demos« nannte man in der 90er Jah­ren kleine Pro­gramme, die spek­ta­ku­läre Gra­fi­ken und Musik auf dem Bildschirm dar­stell­ten und dabei die Grenzen der verfügbaren Hardware ausreizten. Die Demoszene ist eine Sub­kul­tur, die sich um die Ent­wick­lung 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 Work­space sollte jetzt ungefähr so aussehen:

Dieses Tutorial geht davon aus, dass du bereits die Pixelflow Canvas-Er­wei­te­rung und das pixelflow_canvas rubygem installiert hast. Falls nicht, findest du die Anleitung hier in Abschnitt 1: »Pixelflow Canvas installieren«.

1. Farbverlauf erstellen

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, wo­bei 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:

  • Schwarz bis Dunkelrot (Farben 0 bis 15)
  • Dunkelrot bis Orange (Farben 16 bis 31)
  • Orange bis Gelb (Farben 32 bis 47)
  • Gelb bis Weiß (Farben 48 bis 63)

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 Ter­mi­nal ruby fire.rb eingibst. Da standardmäßig die VGA-Palette ver­wen­det 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
Hinweis: Achte auf die drei Punkte im Bereich (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:

2. Animations-Loop

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:

  • In Zeile 3 legen wir die Größe für unsere Animation fest (256x128 Pixel).
  • In Zeile 5 wird »Double Buffering« aktiviert, damit wir dem Bildschirm nicht beim Zeichnen zuschauen müssen, sondern nur das fertige Ergebnis sehen, wenn wir fertig sind. Dafür ist es wichtig, dass wir in Zeile 22 den Befehl flip() aufrufen, damit der Bildschirm aktualisiert wird.
  • In Zeile 16 beginnt eine Endlosschleife, die die Animation Bild für Bild zeichnet. Da das Programm sich nun nicht mehr von selbst beendet, musst du es manuell abbrechen, indem du StrgC drückst.

Das Ergebnis sieht jetzt noch relativ un­spek­ta­ku­lä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.

3. Filterkernel

Filterkernel werden für verschiedene Effekte ver­wen­det, 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 ver­wen­det.

Hier siehst du ein paar Beispiele:

Identity
0 0 0
0 1 0
0 0 0
Box blur
1 1 1
1 1 1
1 1 1
Edge detection
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 ver­wen­det einen spe­ziellen 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.

4. Feueranimation

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, wo­bei 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:

Hast du eine Idee, woran es liegen könnte, dass wir in den hellen und dunklen Be­rei­chen falsche Farben bekommen?

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
In Ruby können wir statt der normalen 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

5. Zusammenfassung

In diesem Tutorial haben wir eine kleine Feueranimation in Ruby programmiert, die auf einem Farbverlauf basiert und einen spe­ziellen Filterkernel ver­wen­det, um die Farben zu animieren. Wir haben gesehen, wie Filterkernel in der Bildbearbeitung ver­wen­det werden und wie sie in der Pro­gram­mier­ung ein­ge­setzt 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:

  • Versuche, andere Muster zu zeichnen, um die Quelle des Feuers zu verändern.
  • Experimentiere mit verschiedenen Farbverläufen und Filterkernels, um andere Effekte (z. B. einen Raucheffekt) zu erzielen.