Programmierung butterweicher und flimmerfreier Animationen

Einleitung

Animationen sind für viele Hobby-Programmierer ein beliebtes Tätigkeitsfeld, insbesondere bei der Spieleprogrammierung. In diesem Artikel zeige ich Ihnen einige Grundprinzipien sowie Umgehungen von Hürden, damit Ihre Animationen völlig ruckelfrei, flimmerfrei und insbesondere von der CPU-Rechenleistung unabhängig auf jedem Rechner genau gleich schnell laufen.

Das Ganze finden Sie in der Billard-Simulation praktisch angewendet vor.

Anschauungsbeispiel

Als Beispiel wollen wir drei Planetenkreise mit einem siebenzackigen Stern in der Mitte darstellen:

Drei Planeten um einen Stern
Unsere Beispielanimation in diesem Artikel

Dabei sollen die Planeten genau 4 Sekunden für eine Umkreisung im Gegenuhrzeigersinn benötigen, während der Stern eine Umdrehung in 2 Sekunden im Uhrzeigersinn zurücklegt.

Grundtechnik bei jeder Animation: Doppelte Pufferung

Das Grundprinzip jeder flimmerfreien Animation bildet die Verwendung von mehreren Bildschirmspeichern. Microsoft BASIC unter MS-DOS bietet Ihnen dazu bei den EGA-Grafikmodi mehrere solche Bildschirmseiten an, welche Sie mit SCREEN setzen können. Das Wichtigste ist also immer das Zeichnen auf einer hintergründigen Bildschirmseite.

Die statische Grundszene

Um die Animation bezüglich Rechenzeit zu optimieren, bietet es sich an, die statischen Teile der Grafik in einer weiteren Bildschirmpufferseite zu zeichnen, so dass diese für jedes neue Bild direkt mit PCOPY umkopiert werden kann:

' Animationsdemonstration Planetenkreise mit Stern
' Ansatz 1

Pi! = 4! * ATN(1!)
SCREEN 7, , 4, 4
CIRCLE (160, 100), 118, 15, , , 5! / 6!
PAINT (160, 100), 15

Weisser Kreis auf schwarzem Hintergrund
Statische Grundszene der Beispielanimation

Bei der Billard-Simulation bildet die Bande diese statische Szene.

Die dynamischen Animationspartien

Für die bewegten Partien sollten Sie einen Programmteil schreiben, welcher Ihnen Ihre Animation zu einem beliebigen Zeitpunkt t in der momentanen Stellung hinzeichnet:

ZeichneBild:
' Kreise
wPos! = t! * .5 * Pi!
FOR i% = 0 TO 2
  w! = wPos! + CSNG(i%) * 2! * Pi! / 3!
  x% = 160 + CINT(96! * COS(w!))
  y% = 100 + CINT(80! * SIN(w!))
  CIRCLE (x%, y%), 21, 3 + i%, , , 5! / 6!
  PAINT (x%, y%), 3 + i%
NEXT i%
wPos! = t! * -Pi!
FOR i% = 0 TO 6
  w1! = wPos! + CSNG(i%) * 4! * Pi! / 7!
  x1% = 160 + CINT(66! * COS(w1!))
  y1% = 100 + CINT(55! * SIN(w1!))
  w2! = wPos! + CSNG(i% + 1) * 4! * Pi! / 7!
  x2% = 160 + CINT(66! * COS(w2!))
  y2% = 100 + CINT(55! * SIN(w2!))
  LINE (x1%, y1%)-(x2%, y2%), 3
NEXT i%
FOR i% = 0 TO 6
  w1! = wPos! + CSNG(i%) * 4! * Pi! / 7!
  x1% = 160 + CINT(54! * COS(w1!))
  y1% = 100 + CINT(45! * SIN(w1!))
  PAINT (x1%, y1%), 5 + i%, 3
NEXT i%
RETURN

Wir berechnen also in wPos! die momentane Winkelposition und zeichnen die Planetenkreise sowie den Stern entsprechend.

Hauptabspielschleife

Wir definieren zuerst die Zeit t0 sowie die Nummer des ersten Bildschirmpuffers:

n% = 0
t0! = TIMER + 10!

Anschliessend können wir unsere Animation abspielen lassen:

WHILE INKEY$ = ""
  t! = TIMER - t0!
  PCOPY 4, n%
  SCREEN , , n%, n% - 1 AND 3
  GOSUB ZeichneBild
  SCREEN , , , n%
  n% = n% + 1 AND 3
WEND
END

Zu Beginn wird die momentane Zeit t berechnet, die Grundszene in den noch nicht sichtbaren Bildpuffer n% hineinkopiert. Ausserdem wird die Arbeitsseite auf n% gesetzt, während die Sichtbarseite immer noch auf n%-1 bleibt. Insgesamt werden 4 Bildschirmseiten nacheinander verwendet. Der Grund für 4 statt nur 2 Seiten ist derjenige, dass bei vielen Grafikkarten der effektive Bildschirmseitenwechsel erst beim sog. VBlank erfolgt, also verzögert. Da aber die CPU sofort weiterarbeiten kann, könnte es bei nur 2 Bildschirmseiten vorkommen, dass das Hintergrundbild bereits wieder gelöscht ist, während es noch gebraucht wird. Mit 3 und mehr Seiten kann dieser Effekt verhindert werden.

Die fertige Animation können Sie hier herunterladen, es misst dabei auch noch die Bildaufbaurate.

Das 55 ms-Taktproblem des PC-Uhrenbausteins

Wenn Sie das Programm von vorhin laufen gelassen haben, werden Sie leicht festgestellt haben, dass die Animation trotz einer recht guten Bildaufbaurate (auf meinem Pentium III mit 550 MHz gut 62 Bilder/s) die Animation zwar flimmerfrei erscheint, aber immer noch sehr stark ruckelt. Die Ursache für dieses Problem liefert Ihnen folgendes kleine Programm:

t! = TIMER
WHILE INKEY$ = ""
  t2! = TIMER
  IF t2! <> t! THEN
    PRINT t2! - t!
    t! = TIMER
  END IF
WEND

Ausgabe:

 5.078125E-02
 5.859375E-02
 5.078125E-02
 5.859375E-02
 5.078125E-02
 .0625
 .046875

Wie Sie vielleicht aus einem PC-Systemhandbuch wissen, generiert der Uhrenbaustein genau 216=65'536 Elementarschritte (Ticks) in der Stunde, was unter anderem auch beim SOUND-Befehl (siehe Tonprogrammierung) seine Spuren hinterlassen hat: Sie geben dort die Tondauer auch als Anzahl solcher Ticks an. Der Mittelwert der obigen Werte entspricht folglich 3600×1000÷216=54,931640625 ms, was dem Mittelwert der obigen Zahlen entspricht.

Die Systemuhr, welche wir als antreibenden Zeitgeber verwenden möchten, können Sie mit einem Schrittmotor vergleichen, bei welchem sich die Antriebswelle nur ruckartig statt gleichförmig dreht.

Die Lösung für das Problem: Digitaler Tiefpassfilter

Wenn Sie als Mechaniker mit dem vorhin genannten Schrittmotor etwas antreiben wollen, wo Sie eine möglichst gleichförmige Drehbewegung brauchen, helfen Sie sich mit einem Schwungrad (Trägheit, die schnelle Geschwindigkeitswechsel verhindert) und einer elastischen Kupplung (fängt die Stösse bei den Schrittbewegungen ab) ab. Elektroniker kennen dies als sog. Tiefpassfilter, in dem sie mit einem entsprechenden RC-Glied die schnellen Spannungswechsel am Signalausgang verhindern.

Mit genau einem solchen digitalen Tiefpassfilter können Sie das Ruckeln von vorhin vollständig eliminieren.

Einfacher Tiefpassfilter-Algorithmus

Ein einfacher digitaler Tiefpassfilter ergibt folgender Algorithmus:

Sie bilden von den letzten n Zeit-Rohwerten einen arithmetischen Mittelwert, wobei Sie jedoch die Werte im Verhältnis n:n-1:n-2...3:2:1 unterschiedlich gewichten, wovon der aktuellste Zeitwert das höchste Gewicht bekommt.

In BASIC formuliert sieht das Ganze etwa so aus:

' Dämpfung: Digitaler Tiefpassfilter

INPUT "Dämpfstärke (Anzahl Werte)"; n%
DIM tRoh!(1 TO n%)

t0! = TIMER + 10!

WHILE INKEY$ = ""
  ' Alles um 1 verschieben
  FOR i% = 2 TO n%
    tRoh!(i% - 1) = tRoh!(i%)
  NEXT i%
  tRoh!(n%) = TIMER - t0!
  ' arithmetisches Mittel mit Gewichtung
  tGed! = 0!
  FOR i% = 1 TO n%
    tGed! = tGed! + CSNG(i%) * tRoh!(i%)
  NEXT i%
  tGed! = tGed! * 2! / (CSNG(n%) * CSNG(n% + 1))
  PRINT USING "Roh: ###.####### Gedämpft: ###.####### Differenz: ###.#####"; tRoh!(n%); tGed!; tGed! - tGedAlt!
  tGedAlt! = tGed!
WEND

Je mehr Werte Sie mitteln, desto stärker werden die Unregelmässigkeiten herausgefiltert, was Sie hinten bei der Differenz sehen können.

Die Gewichtungen und damit den Filter lässt sich grafisch recht hübsch darstellen:

            ^
            |
tRoh!(n%)   |######
tRoh!(n%-1) |#####
tRoh!(n%-2) |####
..          |###
tRoh!(2)    |##
tRoh!(1)    |#
            |

Daraus sollte auch die Entstehung dieser Division mit n*(n+1)/2 ersichtlich sein.

Optimierung des Berechnungsalgorithmus

Der Berechnungsalgorithmus in der jetzigen Form kann recht aufwendig werden, weshalb es sich lohnt, diesen zu optimieren. Um das Optimierungspotential zu zeigen, sollten wir einen Vergleich zweier aufeinanderfolgenden Werte machen:

            ^
            |
tRoh!(n%+1) |++++++
tRoh!(n%)   |#####-
tRoh!(n%-1) |####-
tRoh!(n%-2) |###-
..          |##-
tRoh!(2)    |#-
tRoh!(1)    |-
            |
## = bleibt unverändert
++ = wird dazuaddiert
-- = wird subtrahiert

Den vorherigen tGed!-Wert kennen wir ja noch, also genügt es, zum letzen Wert die neue Zeit n dazuzählen und dafür die einfache Summe aller Zeiten wegzuzählen. Dabei die Division n*(n+1)/2 nicht vergessen:

DivK! = 2! / (CSNG(n%) * CSNG(n% + 1))
tRohNeu! = TIMER - t0!
tGed! = tGed! + (tRohNeu! * CSNG(n%) - ts!) * DivK!

Auch die Divisionskonstante kann einmalig berechnet werden.

Als weitere Optimierung kann noch die Verschiebung aller Rohzeitwerte in der Feldvariable tRoh!() eliminiert werden, denn ts! können Sie ebenfalls mit der Formulierung

ts! = ts! + tRohNeu! - tRoh!(h%)
tRoh!(h%) = tRohNeu!
h% = h% MOD n% + 1

beseitigen, da Sie tRoh!() als reinen FIFO-Stack verwenden können. Damit sieht die neue Version des vorherigen Programms etwa so aus:

' Dämpfung: Digitaler Tiefpassfilter optimiert

INPUT "Dämpfstärke (Anzahl Werte)"; n%
DIM tRoh!(1 TO n%)

t0! = TIMER + 10!
ts! = 0!
FOR i% = 1 TO n%
  tRoh!(i%) = TIMER - t0!
  ts! = ts! + tRoh!(i%)
NEXT i%
tGed! = TIMER - t0!
h% = 1

DivK! = 2! / (CSNG(n%) * CSNG(n% + 1))
WHILE INKEY$ = ""
  tRohNeu! = TIMER - t0!
  tGed! = tGed! + (tRohNeu! * CSNG(n%) - ts!) * DivK!
  ts! = ts! + tRohNeu! - tRoh!(h%)
  tRoh!(h%) = tRohNeu!
  h% = h% MOD n% + 1

  PRINT USING "Roh: ###.####### Gedämpft: ###.####### Differenz: ###.#####"; tRoh!(n%); tGed!; tGed! - tGedAlt!
  tGedAlt! = tGed!
WEND

Genau diesen Filter müssen Sie noch in das Hauptprogramm unserer Beispielanimation einbauen:

t0! = TIMER + 10!

DIM tRoh!(63)
ts! = 0!
FOR i% = 0 TO 63
  tRoh!(i%) = TIMER - t0!
  ts! = ts! + tRoh!(i%)
NEXT i%
t! = TIMER - t0!
h% = 0

WHILE INKEY$ = ""
  tRohNeu! = TIMER - t0!
  t! = t! + (64! * tRohNeu! - ts!) / 2080!
  ts! = ts! - tRoh!(h%) + tRohNeu!
  tRoh!(h%) = tRohNeu!
  h% = h% + 1 AND 63
  PCOPY 4, n%
  SCREEN , , n%, n% - 1 AND 3
  GOSUB ZeichneBild
  SCREEN , , , n%
  n% = n% + 1 AND 3
WEND

Sie finden die fertige Version hier zum Herunterladen. Damit ist unsere Animation wirklich perfekt, wie Sie hoffentlich auf Ihrem PC-Bildschirm selber erleben können :-).

Wieviel Dämpfung ist sinnvoll?

Mit dem Parameter n haben Sie vorhin gesehen, dass Sie die Stärke der Dämpfung beeinflussen können. Diesen Wert sollten Sie in der Praxis weder zu gering noch zu gross wählen. Es hängt vor allem davon ab, wie gleichmässig der Rechenzeitverbrauch eines Bildaufbauschrittes ist. Das hier gezeigte Beispiel ist sehr regelmässig, sodass ein grosses n möglich ist, denn es wird ja immer die genau gleiche Menge an Rechenoperationen und BASIC-Zeichenbefehle aufgerufen. Bei der Billard-Simulation dagegen bedeutet die Impulsauswertung eines stattfindenden Stosses zwischen zwei Kugeln oder mit der Bande vor allem auf älteren Rechnern einen kurzfristigen Anstieg der Rechenzeit. Aus diesem Grund ist dort n sehr massvoll gewählt.

Sie können dies sehr leicht selber nachvollziehen, in dem Sie das Programm mit Pause kurz bremsen und mit Tastendruck wieder weiter laufen lassen: Bei sehr grossem n dauert es dann sichtlich eine Weile, bis sich die Animation wieder im Taktrhythmus befindet.


Wieder zurück zur Übersicht


© 2000 by Andreas Meile