Licznik FPS (klatek na sekundę), kolejność wykonywania instrukcji, płynne rysowanie grafiki

0

Zająłem się rysowaniem animacji w Delphi. Mój podstawowy problem to brak odpowiedniej płynności, wynikający z niestabilnej ilości klatek na sekundę. Wrzucenie do Timera instrukcji w stylu "narysuj kolejną klatkę, a potem jak najszybciej załaduj ją na Canvas" odpada, bo apka renderuje te klatki z bardzo różną szybkością. Czasem wyrenderowanie jednej to kwestia 10 milisekund, czasem 18, a jeszcze innym razem 12, a takie odstępy czasowe zaburzają wrażenie płynności i wywołują wrażenie skokowości.

Wydaje mi się, że jedynym sposobem zaradzenia temu, jest wprowadzenie jakiegoś limitu klatek na sekundę. Wymyśliłem więc, że ustalę jakąś stałą przerwę pomiędzy wyświetlaniem kolejnych klatek (np. 1 klatka co 33 milisekundy powinna przełożyć się na jakieś 30 FPS). W związku z tym doszedłem do wiosku, że muszę mierzyć czas, jaki upłynął od wyświetlenie na ekranie poprzedniej klatki. Coś takiego:

  1. Wyświetl klatkę..
  2. Zacznij odmierzać czas i jednocześnie renderuj już do buffora kolejną klatkę.
  3. Gdy skończysz, sprawdź licznik. Jeżeli nabił już 33 milisekundy - wyświetl ją. Jeżeli nie - czekaj.
  4. Powtórz.

Pierwszy problem pojawił się już na etapie mierzenia czasu pomiędzy klatkami. Dla testu skleciłem sobie taki kod:

Procedure

begin
  Timer1. Enabled:= True;
  Application.ProcessMessages;

  //JAKIEŚ DALSZE INSTRUKCJE

end;

A w samym Timerze (z interwałem ustawionym na 1) miałem coś takiego:

I:= I + 1;

Niestety nie rozumiem czemu, ale procedura odpala Timer już PO wykonaniu wszystkich instrukcji w niej zawartych i kompletnie olewa narzuconą przeze mnie kolejność instrukcji...

  1. Da się temu jakoś zaradzić?
  2. Może znacie jakiś inny sposób rozwiązania tego problemu z FPSami?
  3. Może znacie jakieś inne sposoby na płynne rysowanie grafiki bez migania i zmiany prędkości animacji (przez co nie musiałbym tego mierzyć)?

Pozdrawiam.

2

Zainteresuj się OpenGL/DirectX - nigdy nie wykorzystuj kontrolek do tworzenia animacji, nie po to zostały one zaprojektowane.

0

@Patryk27: a jak chcę mieć w aplikacji animowany suwak do zakładek to mam go wykonać w OpenGL? :]

0

@Crow: może spróbuj użyć timera używającego wątku pobocznego? Przykład takiego znajduje się tutaj.

0

Zwykle wylicza się czas dt jaki upłynął między klatkami animacji i odpowiednio aktualizuje animacje o ten współczynnik(dt w milisekundach lub sekundach w drugim przypadku pamiętaj o tym żeby trzymać to w odpowiednim typie licz rzeczywistych). Wtedy uzyskasz większą płynność animacji ponieważ uwzględnisz faktyczny czas, w jednej klatce wykonasz większy krok animacji a innej mniejszy ale dla użytkownika sprawi to wrażenie większej płynności.

0

Osobiście nie polecam stosowania Application.ProcessMesssages; przy aplikacjach, które mają mają za główne zadanie kontakt z użytkownikiem (ale to jest tylko moja opinia).
Jeśli stosujesz przy czymś takim Timer może Ci w każdym takim miejscu wstrzymać algorytm, by wykonać procedurę z Timer-a.
Polecam drugi wątek i synchronize nie częściej, niż 50 razy na sek.
W załączniku przykładowa klasa do kontroli czasu (na liczbach całkowitych, dokładność do 1 ms), lecz jeszcze jej nigdy nie testowałem, może wymagać poprawek i może istnieć lepszy sposób.

0

Osobiście nie polecam stosowania Application.ProcessMesssages; przy aplikacjach, które mają mają za główne zadanie kontakt z użytkownikiem (ale to jest tylko moja opinia).

A co ma jedno do drugiego? Zadaniem tej metody jest odblokowanie kolejki komunikatów (przetworzenie zalegających komunikatów), aby odblokować interfejs; A jak wiadomo interfejs służy jedynie użytkownikowi, więc stosowanie tej metody nie jest żadnym błędem; Co nie zmienia faktu, że używając jej, trzeba wiedzieć co się robi;

Jeśli stosujesz przy czymś takim Timer może Ci w każdym takim miejscu wstrzymać algorytm, by wykonać procedurę z Timer-a.

Po to właśnie TTimer jest - ma wcinać się w kod i wykonywać to co mu zadano, w ramach głównego wątku;

W załączniku przykładowa klasa do kontroli czasu (na liczbach całkowitych, dokładność do 1 ms) [...]

Niestety nie, dlatego że używasz Sleep, a dokładność tej procedury to około 16ms (lub 10ms).

0

Co do przykładu z załącznika - dzięki za poprawkę.
Application.ProcessMessages przy bardzo szybkim kliknięciu dwóch przycisków mających za zadanie modyfikować np: tą samą zmienną globalną, może dojść do błędu
(ta sama zmienna na początku ma wartość np: 10, a po , Application.ProcessMessages może mieć 9, bo inny przycisk ją zmodyfikował). W takich przypadkach wypada zblokować inne kontrolki, by temu zapobiec, co zajmuje dodatkowy czas. A trzeba wziąć pod uwagę, że samo Application.ProcessMessages opóźnia algorytm.
Problem pytającego polega na tym, że Timer nigdy nie osiągnie interwału 1 ms (ale to chyba już wyjaśnione), oraz Application.ProcessMessages zajmuje (akurat u mnie) ok. 10 ms), co dodatkowo burzy działanie takiego algorytmu. A TTimer co jakiś czas, mimo opóźnień wywołuję procedurę jeszcze raz.

0

Jako tako rozwiązałem swój problem z klatkami przy pomocy wspomnianego przez furious programming QueryPerformanceCounter i osobnego wątku. Niestety brnąc w to danej (konkretniej przy próbach usunięcia screen tearingu i flickeringu) odkryłem, że to po prostu nie ma większego sensu. Delphi jest mało wydajny w renderowaniu grafiki i sprawia masę problemów. Już wolę przerzucić się na C++ (ucząc się go od podstaw, bo nie miałem z nim wcześniej styczności) i biblioteki OpenGL czy DirectX, niż męczyć się dalej z Delphi. Tak wiem, Delphi też obsługuje te biblioteki, ale wyraźnie nie jest do tego stworzony.

Tak czy inaczej, dzięki wszystkim za wskazówki.

0
pawel24pl:

Application.ProcessMessages przy bardzo szybkim kliknięciu dwóch przycisków mających za zadanie modyfikować np: tą samą zmienną globalną, może dojść do błędu [...]

No może dojść do błędu, ale to nie zmienia faktu, że jak się używa tej metody w sposób odpowiedzialny i prawidłowy to do błędu nie doprowadzi...

W swoim projekcie mam animowany slider do zakładek, który wykorzystuje Application.ProcessMessages podczas animacji; Po kliknięciu innej zakładki niż aktywna, zakładki przełączają się, slider startuje i jedzie do tej przed chwilą aktywowanej; Po każdym skoku slidera o 1px wywoływana jest ta metoda, aby oblokować m.in. czekający w kolejce komunikat REPAINT, czyli aby po skoku slidera, suwak ten został przemalowany; Jak nie wywołam tej metody, suwak nie będzie jechał - zostanie przemalowany jedynie po skończeniu animacji, czyli po prostu zniknie z początkowej pozycji i od razu pokaże się na końcowej;

Kod metody animującej suwak wrzuciłem tutaj - https://4programmers.net/Pastebin/6292 - zobacz sobie;

Dzięki tej metodzie, animacja jest widoczna na ekranie, a podczas jej trwania przetwarzane są wszystkie inne komunikaty; Dzięki temu podczas jej trwania np. podświetlane są inne kontrolki po najechaniu kursorem, można zamknąć formularz (i nic się złego nie stanie), można odpalać zdarzenia spod innych komponentów itd. itd.;

Jak widzisz nie jest taka zła i nieprzydatna;

A trzeba wziąć pod uwagę, że samo Application.ProcessMessages opóźnia algorytm.

Bo po to została stworzona;

Problem pytającego polega na tym, że Timer nigdy nie osiągnie interwału 1 ms (ale to chyba już wyjaśnione) [...]

Tak, nigdy nie osiągnie takiej dokładności, bo używa zegara, który ma o wiele gorszą dokładność; Na poprzednim laptopie osiągałem minimum ~10ms, a na tym co mam obecnie i drugim domowym, około 16ms; Rozdzielczość pracy najpewniej zależna jest od sprzętowego zegara, ale mogę się mylić;

[...] oraz Application.ProcessMessages zajmuje (akurat u mnie) ok. 10 ms), co dodatkowo burzy działanie takiego algorytmu.

Obliczanie czasu działania tej metody nie ma żadnego sensu, bo kolejka komunikatów może być pusta i wykona się w mgnieniu oka, jak również może się w niej znajdować mnóstwo komunikatów, które zostaną obsłużone w kilka sekund; Poza tym robisz drugi poważny błąd, jeśli czas trwania tej metody mierzysz np. za pomocą GetTickCount, która to jest równie niedokładna jak Sleep czy TTimer, bo te wszystkie elementy korzystają z tego samego, niedokładnego zegara;

A TTimer co jakiś czas, mimo opóźnień wywołuję procedurę jeszcze raz.

Nie można komparować metody ProcessMessages z klasą TTimer, bo ich przeznaczenie jest zupełnie inne; Metoda ta służy do przetworzenia oczekujących komunikatów, a wspomniana klasa do cyklicznego wykonywania kodu w ramach tego samego wątku;

To tak jakbyś powiedział, że rower jest lepszym środkiem stransportu, bo żeliwny kaloryfer słabiej grzeje;


Crow:

Niestety brnąc w to danej (konkretniej przy próbach usunięcia screen tearingu i flickeringu) odkryłem, że to po prostu nie ma większego sensu.

Według tego co wiem, rozrywania ekranu nie dasz rady wykluczyć po stronie aplikacji;

Delphi jest mało wydajny w renderowaniu grafiki i sprawia masę problemów.

Ale to nie Delphi jest temu winne, a tryb, w którym Twoja aplikacja działa i komponenty, których używasz;

Już wolę przerzucić się na C++ (ucząc się go od podstaw, bo nie miałem z nim wcześniej styczności) i biblioteki OpenGL czy DirectX, niż męczyć się dalej z Delphi. Tak wiem, Delphi też obsługuje te biblioteki, ale wyraźnie nie jest do tego stworzony.

Nie ma to żadnego logicznego podłoża - dokładnie ten sam problem będziesz miał, jeśli napiszesz aplikację w C++, która to będzie działać w trybie okienkowym i używać zwykłych komponentów; Dobry (a nawet idealny) efekt uzyskasz korzystając z Direct Draw, OpenGL czy DirectX, bez względu na język i środowisko programistyczne.

0

@furious programming

Trochę chyba za mało precyzyjnie to opisałem ;d.

Od kilku tygodni (a koncepcyjnie to już od kilku miesięcy) próbuję sklecić bardzo prosty silnik raycastingowy. Jeżeli się z tym nie spotkałeś - jest to takie cuś, co w sposób spekulacyjny (na zasadzie domysłu) renderuje grafikę udającą trójwymiar (pseudo 3D) Jako że cały świat gry osadzony na tym silniku składa się z idealnych sześcianów, algorytm jest w stanie wydedukować jak wygląda każdy widziany przez gracza obiekt, z dowolnego kąta. Podobna technologia napędza stareńkiego Wolfensteina 3D albo późniejsze Dark Forces.

PRZYKŁAD (z poziomu przeglądarki):

http://demos.playfuljs.com/raycaster/

Od strony teoretycznej jest to dosyć proste i udało mi się rozbić poszczególne zagadnienia na odpowiednie algorytmy (wyszukiwanie obiektów, skalowanie, ustalanie orientacji względem gracza, texturowanie itd.) i sądziłem, że uda mi się przenieść to do Delphi trochę chałupniczymi metodami, tzn. bez sięgania po bardziej zaawansowane biblioteki graficzne typu OpenGL czy DIrectX. Niestety - nie wyszło.
Wstępnie przepatrzyłem sobie już obsługę OpenGL w Delphi i dla mnie (nie jestem programistą) to po prostu czarna magia. Nauczenie się czegoś takiego, to dla mnie jak nauka nowego języka programowania. Wiem, pod C++ też mnie to czeka, ale doszedłem do wniosku, że skoro już mam coś zaczynać od podstaw, to lepiej od razu przenieść się na bardziej odpowiednie środowisko.

Myślę, że nie bez powodu w trakcie swoich poszukiwań znalazłem masę takich raycastingowych silników napisanych w C++ czy Javie i ani jednego w Delphi. Najwyraźniej nie jest to najlepsze narzędzie do takich zastosowań.

0

Od strony teoretycznej jest to dosyć proste i udało mi się rozbić poszczególne zagadnienia na odpowiednie algorytmy (wyszukiwanie obiektów, skalowanie, ustalanie orientacji względem gracza, texturowanie itd.) i sądziłem, że uda mi się przenieść to do Delphi trochę chałupniczymi metodami, tzn. bez sięgania po bardziej zaawansowane biblioteki graficzne typu OpenGL czy DIrectX. Niestety - nie wyszło.

Wyjść wyszło, bo sporo już napisałeś; Jednak zabawa z FPSami w środowisku okienkowym to katorga - trzeba by być geniuszem, żeby w zwykłym okienkowym trybie osiągnąć efekt, jaki Cię interesuje;

Wstępnie przepatrzyłem sobie już obsługę OpenGL w Delphi i dla mnie (nie jestem programistą) to po prostu czarna magia.

Zapewne widziałeś obsługę OpenGL w postaci gołych funkcji (samych importów), dlatego obsługa tej biblioteki w taki sposób wydaje się być tragedią; Czasem ktoś rzeźbi w taki sposób, choć jest to dość trudne i czasochłonne;

Wiem, pod C++ też mnie to czeka, ale doszedłem do wniosku, że skoro już mam coś zaczynać od podstaw, to lepiej od razu przenieść się na bardziej odpowiednie środowisko.

Nie zmieniaj języka, jeśli nie musisz - gołe importy są w obsłudze podobnie trudne jak gołe WinAPI, więc aby zacząć coś robić w tym kierunku, trzeba sobie znaleźć bibliotekę, która oferuje API po pierwsze maskujące wywołania nieczytelnych i skomplikowanych funkcji, a po drugie, udostępniającą wygodne w obsłudze i czytelne zestawy klas;

Co nie zmienia faktu, że dobrej biblioteki dla Delphi może brakować;

Myślę, że nie bez powodu w trakcie swoich poszukiwań znalazłem masę takich raycastingowych silników napisanych w C++ czy Javie i ani jednego w Delphi. Najwyraźniej nie jest to najlepsze narzędzie do takich zastosowań.

Też tak myślę - najwięcej gotowych i rozwijanych bibliotek znajdziesz dla wymienionych języków.

1 użytkowników online, w tym zalogowanych: 0, gości: 1