Bildanimation (Arduino Esplora, Part Teil 3)


Die Adafruit GFX Bibliothek gibt uns die Möglichkeiten Pixel für Pixel auf das TFT zu schreiben, womit sich dann auch ein Bild zusammen setzen lässt. Bei größeren Bilder sollte klar sein, dass der Bildaufbau mit 16MHz nur langsam abläuft. Als Ziel ist jedoch eine Darstellung zur Laufzeit zu verändert, wie z.B. eine Runde Analoge Anzeige.


Relativ schnell stellte sich heraus, dass die Umsetzung einer solchen Anzeige zwar einfach ist, aber ab einer bestimmten Größe zu langsam gerendert wird. Alternative und einfacher ist die das Verwenden von bereits fertigen Bildern in 16 mal 16 Format. Zugegeben ist eine Analoge Anzeige mit dieser Auflösung sehr grob und auf Dauer nicht zu friedend stellend. Eine Low Pixel Figur wiederum würde passen und das kombiniert mit den Tasten, könnte die Figur auch über den Bildschirm gesteuert werden. An dieser Stelle erinnerte ich mich wieder an den Anfang von Octoawesome von Tom Wendel, der in seinen ersten folgen ähnliche Schritte unternahm ein Spiel zu entwickeln mit C# und Windows Forms (später wurde die Windows form Oberfläche abgelöst  durch MonoGame).

Technische Umgebung und Anforderung
Kommen wir zu den Bildern die zunächst auf ein Format gebracht werden müssen, die möglichst wenig Ressourcen verbrauchen. Mit dem Format 16x16 muss jeder Pixel eine Farbe zugewiesen werden. Der Bildschirm unterstützt 16Bit, womit zwei Byte pro Pixel anfallen würden. Das wären dann 512 Bytes für ein Bild, womit dann eine Sinnvolle Animation mit 2,5 Kilobyte SRAM nicht sinnvoll wäre.
An der Stelle sollten man sich vor Augen halten, wie viele Farbabstufungen 16Bit haben. Und dann schaut man nochmal auf die Anforderung. Daraus stellen sich die Fragen:
Wie viele Farben werden benötigt?
Wie viele Pixel braucht meine Figur?


Auf die Hälfte und weniger reduzieren
Anstatt zwei Byte als Pixelfarbinformation zu hinterlegen, wird dies auf ein Byte reduziert. Der Byte Wert trägt später nur die Nummer aus einer Farbpallette. Mit einer entsprechenden Mapping Funktion, wird dann später die Farbe für den Pixel abgerufen. Durch diesen Vorgang reduziert sich das Bild von 16x16 von 512 auf 256 Bytes. Die Figur selbst benötigt in der Breite nur zehn Pixel, womit der Speicher verbrauch sich auf 160 Bytes weiter reduziert.

 uint16_t mapNumberToColor(byte c) {  
  uint16_t result = ST7735_RED;  
  switch(c) {  
   case(1):{ result = ST7735_BLACK; break; }  
   case(2):{ result = 0xF590; break; } // haut  
   case(3):{ result = 0x81E1; break; } // braun  
   case(4):{ result = 0xC2C2; break; } // hell braun  
   case(5):{ result = 0x8300; break; } // braun gelb  
   case(6):{ result = 0x5406; break; } // gruen  
   case(7):{ result = 0x32A4; break; } // dunkel gruen  
   case(8):{ result = 0xAE91; break; } // hell gruen  
   case(9):{ result = 0x2146; break; } // dunkel grau blau  
   case(10):{ result = 0x31E9; break; } // grau blau  
   case(11):{ result = 0x84B6; break; } // hell blau  
   default: {  
    result = 0;  
    break;  
   }  
  }  
    
  return result;  
 }  

Bild Editor in Arbeit
Kommen wir zum Erstellen eines Bildes. Ein Bild Byte für Byte zu schreiben ist so spaßig wie einen Film auf Indisch mit Kantonesischen Untertitel zu schauen. Hier für stelle ich demnächst ein kleines Tool zur Verfügung, mit denen ihr Farbpixel für Farbpixel euer Bild Zeichnen und nach Fertigstellung die byte Kette in den Sketch kopieren könnt.




Keine Metadaten
Die Byte Kette selbst enthält keine Metadaten. Das bedeutet, dass das Bild nicht weist wie Breit und wie hoch sie ist. Diese Informationen müssen vom Programmcode festgelegt werden.

 void drawFigurArray(int relationX, int relationY, byte figureArray[], boolean mirror, boolean clearColor) {  
  int index = 0;  
  for(int y = 0; y < 16; y++) {  
   for(int x = 0; x < 10; x++) {  
    if(clearColor) {  
      EsploraTFT.drawPixel(relationX+x, relationY+y, mapNumberToColor(figureArray[1]));  
    }  
    else {  
     int indexTarget = index;  
     if(mirror) {  
      indexTarget = index - x + (10 - x) - 1;  
     }  
   
     EsploraTFT.drawPixel(relationX+x, relationY+y, mapNumberToColor(figureArray[indexTarget]));  
     index++;  
    }  
   }  
  }  
 }  

Sprite Animation
Den Sketch den ich zur Verfügung stelle, kann die Farbnummern übersetzen aus dem Byte Array und so mit ein Bild auf den TFT schreiben. Hier fehlt nur die Sequenz abfolge der verwendeten Sprites als Animation. Würde man die paar Bilder direkt hintereinander abspielen, würden diese zu schnell ablaufen. Mit 'delay' lässt sich dieses Problem provisorisch lösen, führt jedoch zu Problemen mit dem späteren Abfragen der Taster. Deshalb werden zwei Variable vom Typ long und int im Funktionsvariable angelegt. Der mit dem Typ long (hier gametime benannt), wird pro Methoden Loop durchlauf, um einen hochgezählt. Der zweite Wert von Type int Mit einer Modulo Abfrage wird dann immer der vierte Durchlauf verwendet, um den Sequenz Bildindex der Animation um einen fortzusetzen oder von vorne abzuspielen.
Die Figur kann sich in vier Richtungen bewegen, womit dann auch vier verschiedene Animationsabläufe sich abbilden. Für Links und Rechts können die selben Bilder verwendet werden, da für die gegenteilige Richtung gespiegelt werden kann.
UPDATE: Zuvor war an der Methode der Parameter 'walk' übergeben worden, der jetzt entfällt. Und 'default' führt jeweils nochmal das zweite Sprite aus, womit dann die Figur weniger flimmern sollte.

 void drawFigure(int directionX, int directionY, int relationX, int relationY) {  
   
  if(gameTime % 4 > 0) {  
   if(animStep > 2) {animStep = 0;}  
   else {animStep++;}  
  }  
   
   if(directionX == 0 && directionY == 1) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureFrontLeft, false, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureFrontMiddle, false, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureFrontLeft, true, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureFrontMiddle, false, false); break; }  
    }  
   }  
   else if(directionX == -1 && directionY == 0) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureSideLeft, false, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, false, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureSideRight, false, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, false, false); break; }  
    }  
   }  
   else if(directionX == 0 && directionY == -1) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureBackLeft, false, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureBackMiddle, false, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureBackLeft, true, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureBackMiddle, false, false); break; }  
    }  
   }  
   else if(directionX == 1 && directionY == 0) {  
    switch(animStep){  
     case(0): { drawFigurArray(relationX, relationY, spriteFigureSideLeft, true, false); break; }  
     case(1): { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, true, false); break; }  
     case(2): { drawFigurArray(relationX, relationY, spriteFigureSideRight, true, false); break; }  
     default: { drawFigurArray(relationX, relationY, spriteFigureSideMiddle, true, false); break; }  
    }  
   }  
 }  

Die Animation sollte nur dann abgespielt werden, wenn eines der Tasten gedrückt wurde. Im zweiten Teil der Blogpost Reihe wurde nur ein Kreis bewegt und wurde von der Methode in der folgenden If Abfrage ausgeführt.

 // Wenn sich X oder Y Position unterscheiden, dann den zu bewegenden Punkt neu zeichnen.  
  if(lastPosX != lastPosXtemp || lastPosY != lastPosYtemp) {  
   drawPoint(lastPosXtemp, lastPosYtemp, false);  
   drawPoint(lastPosX, lastPosY, true);  
  }  

Der Inhalt der If Abfrage Zeichnete einen Kreis, der durch die Sprite Animation ersetzt wird. Hier werden nun die Joystick eingaben auf ihre Ausrichtung geprüft bevor die Figur mit den richtigen Sprite Bilder gezeichnet werden.

 // Wenn sich X oder Y Position unterscheiden, dann den zu bewegenden Punkt neu zeichnen.  
  if(lastPosX != lastPosXtemp || lastPosY != lastPosYtemp) {  
   
   int directX = 0;  
   int directY = 0;  
   
   if(lastPosXtemp > lastPosX) { directX = -1; }  
   if(lastPosXtemp < lastPosX) { directX = 1; }  
   if(lastPosYtemp > lastPosY) { directY = -1; }  
   if(lastPosYtemp < lastPosY) { directY = 1; }  
   
   drawFigure(directX, directY, lastPosX, lastPosY);  
  }  

So das sollte erstmal alles sein für diesen Post. Im nächsten Post kommt eine Beschreibung zum Bild Editor, dass für das Erstellen der Sprites erleichtert.

Nächster Post: Eigene Sprites erstellen (Arduino Esplora, Part 3.1)

Kommentare

Beliebte Posts aus diesem Blog

Arduino Control (Teil 5) - PWM Signal einlesen

RC Fahrtenregler für Lego Kettenfahrzeug

Angular auf dem Raspberry Pi