[powrót]

Co to jest i jak implementować time-based animation ?

Najpierw omówienie tego co już wiemy :

Implementacja animacji jest prosta: wystarczy nieustająco modyfikować stan animacji (zmienne w rodzaju przesunięcie_obiektu lub kąt_obrotu_obiektu) i nieustająco przerysowywać ekran.

  1. Używając gluta: Zazwyczaj należy robić w idle (funkcji zarejestrowanej przez glutIdleFunc) instrukcje jak

      przesunięcie_obiektu += 0.5;
      kąt_obrotu_obiektu += 0.5;
      glutPostRedisplay();
    

    Wywołujemy glutPostRedisplay() aby wymusić wywołanie display w najbliższym czasie. display używa zmiennych jak przesunięcie_obiektu i kąt_obrotu_obiektu aby narysować konkretką klatkę animacji.

    W prostych przypadkach można też po prostu wykonać te instrukcje pod koniec każdego display.

  2. Używając SDLa: jeśli sami piszemy pętlę programu, mamy kod w rodzaju

    while ( !done )
    {
      while ( SDL_PollEvent( &event ) )
      {
        ... obsłuż zdarzenie event ...
      }
    }
    
    Zmienianie stanu animacji możemy zrobić w każdym przebiegu pętli, np.
    while ( !done )
    {
      while ( SDL_PollEvent( &event ) )
      {
        ... obsłuż zdarzenie event ...
      }
    
      /* Zmień stan animacji */
      przesunięcie_obiektu += 0.5;
      kąt_obrotu_obiektu += 0.5;
    
      /* Narysuj nową klatkę animacji */
      display();
    }
    
    Ponieważ wiemy że w każdym kroku pętli modyfikujemy stan animacji, możemy explicite w każdym kroku pętli wywołać display() (zamiast czekać na zdarzenie SDL_VIDEOEXPOSE). Alternatywnie, jeżeli lubimy czekać na SDL_VIDEOEXPOSE, to możemy zażądać wywołania SDL_VIDEOEXPOSE w najbliższym czasie przez wywołanie
    {
      SDL_Event event;
      event.type = SDL_VIDEOEXPOSE;
      SDL_PushEvent(&event);
    }
    
    Powyższy kod to odpowiednik glutPostRedidplay() z gluta.

Oczywiście powyższe kawałki kodu to tylko przykłady najprostszego i najbardziej typowego podejścia. Te przykłądy będą wymagać odpowiednich modyfikacji w różnych przypadkach, np. nie zawsze animacja działa w czasie całego programu: 1. użytkownik rzuca piłeczką 2. piłeczka leci, robimy animację 3. piłeczka upada, koniec animacji.

OK, to wszystko już wiemy, i implementowaliśmy w pierwszym zadaniu z PGK. Na czym polega problem ?

Problem polega na tym że tak jest źle. A dokładniej: problem polega na tym że animacja działa źle na prawie wszystkich komputerach, za wyjątkiem komputera na którym testowaliście program. Szybkość działania OpenGLa jest drastycznie różna na różnych komputerach — różne karty graficzne, różna implementacja OpenGLa itd. Na szybkim komputerze instrukcje

  przesunięcie_obiektu += 0.5;
  kąt_obrotu_obiektu += 0.5;

mogą być wywoływane 100 razy na sekundę, na wolnym komputerze tylko 10 razy na sekundę. Wszystko przez to że szybki komputer szybko wykonuje procedurę display (która zazwyczaj jest najbardziej czasożerną częścią programu). Czyli pętla programu działa szybciej, i procedura w stylu idle jest wywoływana znacznie częściej.

W rezultacie instrukcja przesunięcie_obiektu += 0.5; jest wywoływana na szybszym komputerze 10 razy częściej na sekundę, więc odpowiedni obiekt na ekranie porusza się szybciej.

Efekt: jeżeli dostosowaliście stałą o jaką w każdym kroku zmieniacie przesunięcie_obiektu (0.5 w naszych przykładach) do swojego komputera, a potem uruchomicie ten sam program na 10 razy szybszym komputerze, to stwierdzicie że obiekt porusza się 10 razy szybciej. Lub 10 razy wolniej na 10 razy bardziej wolnym komputerze.

Obejście problemu w naprawdę starym stylu: użycie delay

Jeden sposób obejścia tego problemu to napisanie pętli programu w stylu

while (!done)
{
  delay(10);
  ... zmień stan animacji ...
  ... obsłuż zdarzenia, czyli nasze "while ( SDL_PollEvent( &event ) ) ..." ...
  ... narysuj kolejną klatkę animacji, czyli nasze "display()" ...
}

Gdzie funkcja delay po prostu zawiesza program na 10 milisekund. To jest dobre rozwiązanie jeżeli wszystkie instrukcje zabierają niezauważalnie mało czasu w porównaniu z delay(10). Wtedy każdy krok pętli wykonuje się co 10 milisekund, czyli nasza instrukcja przesunięcie_obiektu += 0.5; wykonuje się na każdym komputerze około 100 razy na sekundę, więc obiekt w ciągu sekundy zawsze przesuwa się o 100 * 0.5 = 50 jednostek.

To było dobre rozwiązanie w starych dobrych czasach kiedy wszyscy byliśmy jeszcze w szkole podstawowej, mieliśmy nasze PC-ty z DOSem i programowaliśmy gry tekstowe, gdzie największym osiągnięciem animacyjnym było "przesuń literkę "D" symbolizującą smoka na środek ekranu, żeby mogła zjeść literkę "@"".

Nasz problem polega na tym że display to ciąg wywołań OpenGLa które zajmują dość sporo czasu. Musielibyśmy zrobić delay(X) z naprawdę dużym parametrem X żeby czas display zabrał "niezauważalnie mało czasu w porównaniu z delay(X)". Na przykład delay(100). Tylko że delay(100) oznacza że program zasypia na 1/10 sekundy. Więc nasz program działa na każdym komputerze (nawet bardzo szybkim) z marną szybkością maksymalnie 10 klatek na sekundę (frames per second, FPS).

Ujmując problem bardziej intuicyjnie: my nie możemy sobie pozwolić na zawieszenie programu na X milisekund: nasza gra 3D naprawdę potrzebuje tyle czasu ile tylko procesor jest w stanie jej dać. Rozwiązania które niepotrzebnie zawieszają nasz program są niedopuszczalne.

Rozwiązanie problemu: time-based animation

Podsumowując, chcemy mieć animację która

  1. Produkuje tyle klatek na sekundę ile tylko może na aktualnym komputerze (z aktualną implementacją OpenGLa itd.)
  2. Zmienia się tak samo w czasie rzeczywistym. Mówiąc normalnie: jeżeli w czasie 1 sekundy obiekt ma przesunąć się o 50 jednostek, to chcemy żeby obiekt przesunął się w czasie 1 sekundy o 50 jednostek na każdym komputerze. Mówiąc wprost: animacja ma wyglądać tak samo dla użytkownika, bez względu na to na jakim komputerze pracuje.

Jeżeli komputer jest szybki, to animacja bedzie bardziej "gładka". Na wolnym komputerze będzie bardziej "kanciasta". Na każdym komputerze użytkownik zobaczy że obiekt w ciągu sekundy przesunął się o 50 jednostek, ale na szybkim komputerze będzie to pokazane za pomocą 100 klatek animacji a na wolnym komputerze za pomocą jedynie 10 klatek.

Czyli musimy pozwolić na wywoływanie display tak szybko jak to tylko możliwe. Chcąc mieć gładką animację musimy też zmieniać stan animacji tak często jak to jest możliwe. Stąd np. rozwiązanie z wrzuceniem instrukcji przesunięcie_obiektu += 0.5; do środką procedury timer zarejestrowanej przez glutTimerFunc lub SDL_AddTimer nie jest dobre — na szybkim komputerze powodowałoby że animacja jest bardziej kanciasta niż potrzeba.

Rozwiązanie: na początku idle oblicz ilość czasu jaki upłynął od ostatniego wywołania idle. Przeskaluj wszystkie zmiany współczynników przez tą ilość. Przykład:

/* Statyczna zmienna w programie */
Uint32 last_idle_time;

...

/* Gdzieś w main() ... */

/* Zainicjuj last_idle_time */
last_idle_time = SDL_GetTicks();

/* Pętla zdarzeń */
while ( !done )
{
  while ( SDL_PollEvent( &event ) )
  {
    ... obsłuż zdarzenie event ...
  }

  /* Zmień stan animacji */
  {
    Uint32 time_now = SDL_GetTicks();
    przesunięcie_obiektu += 50.0 * (time_now - last_idle_time) / 1000.0;
    last_idle_time = time_now;
  }

  /* Narysuj nową klatkę animacji */
  display();
}

SDL_GetTicks podaje czas w milisekundach, stąd (last_idle_time - time_now) / 1000.0 to ilość sekund (zmiennoprzecinkowa, więc być może np. 0.5 sekundy) jaka upłynęła od ostatniej zmiany. Stąd wiem że przesunięcie_obiektu zmieni się o 50 jednostek w ciągu 1 sekundy — na każdym komputerze.

Kilka uwag:

Przykład

time_based_example.c. Po uruchomieniu zobaczycie że żółty prostokąt przesuwa się o długość białej kreski w czasie dokładnie 1 sekundy. Bez względu na to na jak szybkim komputerze go uruchomimy. Można sprawdzać z zegarkiem.