Jeśli jesteś obeznany z fizyką i chciałbyś zacząć implementować kod do jej symulacji, to ta lekcja może Ci w tym pomóc. Aby cokolwiek zrozumieć powinieneś wiedzieć czym są operacje na wektorach w 3D oraz sprawnie operować pojęciami takimi jak siła i przyspieszenie.
W tej lekcji znajdziesz bardzo prosty silnik do symulacji fizyki.
* class Vector3D --> Klasa, która będzie reprezentować wektor3D albo punkt3D w przestrzeni.
* class Mass --> Klasa, która będzie reprezentować bryłę.
Jak symulacja powinna działać:
* class Simulation --> Pojemnik na symulowane bryły.
Obsługiwanie symulacji przez aplikację:
* class ConstantVelocity : public Simulation --> Klasa, która będzie reprezentować bryłę poruszająca się ze stałą prędkością.
* class MotionUnderGravitation : public Simulation --> Klasa, która będzie reprezentować bryłę poruszającą się pod wpływem grawitacji.
* class MassConnectedWithSpring : public Simulation --> Klasa, która będzie reprezentować bryłę poruszającą się pod wpływem działania przyłączonej sprężyny.
Projekt silnika fizycznego (ang. physical engine) nie zawsze jest prosty. Ale są proste zasady zależności; aplikacja opiera się na symulacji a symulacja na bibliotece matematycznej. My użyjemy właśnie tej prostej kolejności. Dla naszych celów otrzymamy pojemnik (ang. container) do symulacji ruchu brył. Symulacja będzie zawierała obiekty 'class Mass' oraz 'class Simulation'. 'class Simulation' będzie właśnie naszym pojemnikiem. Kiedy otrzymamy klasę 'Simulation', będziemy mogli rozwijać (ang. develop) naszą aplikację. Jednak wcześniej, potrzebujemy biblioteki matematycznej. Biblioteka zawiera tylko jedną klasę 'class Vector3D', używaną do reprezentacji punktów, wektorów, pozycji, prędkości i sił w przestrzeni trójwymiarowej.
*class Vector3D --> Klasa, która będzie reprezentować wektor3D albo punkt3D w przestrzeni.
Klasa 'Vector3D' jest jedynym członkiem naszej skromnej biblioteki matematycznej (ang. math library). 'Vector3D' zawiera pola 'X', 'Y', i 'Z' oraz implementuje operatory arytmetyczne dla wektorów. Dodawanie, odejmowanie, mnożenie i dzielenie, które znajdują się w 'Vector3D'. Dopóki nasza lekcja skupia się na fizyce nie będziemy zagłębiać się w detale klasy 'Vector3D'. Jeśli zaglądniesz do pliku Physics1.h zobaczysz jak prosta jest klasa 'Vector3D'.
Aby zaimplementować symulację fizyczną, powinniśmy wiedzieć czym jest bryła. Bryła jest obiektem, który posiada pozycję (ang. position) i prędkość (ang. velocity). Ma ciężar na Ziemi, Księżycu, Marsie i w każdym innym miejscu gdzie istnieje grawitacja. Ciężar jest inny w różnych miejscach, w których zmienia się grawitacja. Jednak jest jedna wspólna wartość, która jest równa w różnych warunkach. Tą wartością jest masa. Wartość masy określa "ile masy istnieje w przestrzeni". Dla przykładu książka jest bryłą, której ciężar wynosi ok. 1 kg na Ziemi, a na Księżycu ok. 0,17 kg, natomiast jej masa jest równa 1 kg gdziekolwiek. Wartość masy jest określona tak, aby była równa ciężarowi na Ziemi.
Po tym jak zrozumiałeś czym jest masa bryły, powinniśmy zainteresować się siłą (ang. force) i ruchem (ang. move). Bryła, która ma niezerową prędkość w przestrzeni, porusza się zgodnie z jej kierunkiem (prędkość jest wielkością wektorową, posiada zwrot, kierunek i wartość - przyp. tłum.). Dlatego powodem zmiany położenia jest prędkość oraz czas. Zmiana położenia zależna jest od szybkości poruszania się bryły oraz od czasu jaki upłynął. Powinieneś to zrozumieć zanim będziesz czytał dalej, jeśli nie to poświęć trochę czasu, aby pomyśleć nad zależnościami pomiędzy położeniem, prędkością i czasem.
Prędkość bryły zmienia się pod wpływem siły na nią działającej i zależy od jej kierunku. Ta skłonność jest proporcjonalna do siły i odwrotnie proporcjonalna do masy. Zmiana prędkości na jednostkę czasu nazywana jest przyspieszeniem. Im większa siła działająca na bryłę, tym większe przyspieszenie. Natomiast gdy masa bryły jest większa, zmniejsza się przyspieszenie. Przyspieszenie sformułowane jest w postaci:
przyspieszenie = siła / masa
Tutaj wyprowadzimy znane równanie:
siła = masa * przyspieszenie
(często będziemy używali tego równania)
Aby przygotować fizyczny ośrodek do symulacji, powinienieś byś świadomy tego, że potrzebujesz środowiska (ang. environment), w którym ta symulacja będzie zachodzić. Środowiskiem w tej lekcji jest po prostu pusta przestrzeń czekająca na to, aby ją wypełnić przez bryły, które stworzymy. Jednoski masy i czasu powinny zostać wybrane na samym początku. Ja zdecydowałem używać sekund jako jednostek czasu oraz metrów jako jednostek definiujących położenie. Odpowiednio: prędkość to metr na sekundę (m/s), przyspieszenie to metr na sekundę do kwadratu (m/s^2) oraz jako jednostkę masy przyjąłem kilogramy (kg).
*class Mass --> Klasa, która będzie reprezentować bryłę.
Teraz zaczniemy używać teorii! Musimy napisać klasę, która będzie reprezentować bryłę, powinna ona zawierać pola: położenie, prędkość i siła.
Chcemy przyłożyć siłę do bryły. Przykładowo w jednym czasie, może być kilka źródeł sił zewnętrznych działających na tą bryłę. Suma wektorów tych sił daje siłę wypadkową. Zanim zaczniesz dodawać siły, powinienieś je zresetować (wyzerować). Dopiero później możemy dodać siły zewnętrzne.
Jest kilka elementów do wykonania podczas symulacji:
Tutaj, zwiększanie kroku czasowego jest wykonane przy pomocy "Metody Euler'a" (ang. The Euler Method). Metoda Euler'a jest prostą metodą symulacji. Jest wiele wyrafinowanych metod, ale Euler jest wystarczająco dobry dla wielu aplikacji, wiele komputerów i gier wideo jej używa. Ta metoda przelicza prędkość i położenie bryły zgodnie z przyłożoną siłą i czasem, który upłynął. Zmiana wykonywana jest w 'void simulate(float dt)':
W symulacji, w każdym kroku poszczególny proces zajmuje miejsce. Siła ustawione są na zero, następnie przykładamy siły zewnętrzne, zmienia się położenie i prędkość. Ten cykl powtarza się tak długo jak upływa czas, zaimplementowany jest on w 'class Simulation'.
*class Simulation --> Pojemnik na symulowane bryły.
Klasa 'Simulation' zawiera masy jako pola wewnętrzne, jej rolą jest utworzenie a następnie usunięcie brył, oraz utrzymywanie funkcji symulującej.
Symulacja przebiega w następujących trzech krokach:
Procedura symulacji jest upakowana w jedną metodę:
Do teraz już mamy prostą symulację fizyczną, która opiera się ma bibliotece matematycznej. Zawiera klasy 'Mass' oraz 'Simulation', używa bardzo prostego wzorca procedury symulacji oraz używa Metody Eulera. Teraz jesteśmy gotowi rozwijać naszą aplikację, która będzie zawierała:
Zanim napiszemy konkretną symulację, powinniśmy wiedzieć jak sterować symulacją przez aplikację. W tej lekcji, silnik symulacji i aplikacja rozdzielone są w dwóch plikach. W pliku z aplikacją znajduje się funkcja:
Ta funkcja jest wywoływana w każdej ramce/klatce (ang. frame). 'DWORD miliseconds' to różnica czasu pomiędzy poprzednią ramką a ramką bieżącą. W każdym cyklu symulacja powinna się również zająć równolegle światem rzeczywistym. Aby powtórzyć symulację po prostu wywołujemy metodę 'void operate(float dt)', aby to zrobić powinniśmy znać 'dt'. Odkąd czas podajemy w sekundach najpierw powinniśmy zamienić je na milisekundy (zobacz poniższy kod). Później używamy wartości 'slowMotionRatio', która oznacza jak wolno chcemy, aby symulacja działała w porównaniu z rzeczywistym światem. Dzielimy 'dt' przez tą wartość i otrzymujemy nowe 'dt'. Teraz możemy dodać 'dt' do 'timeElpased'. 'timeElpased' jest czasem symulacji.
Teraz jesteśmy pawie gotowi, aby kierować symulacją. Jednak... jest kilka bardzo ważnych rzeczy, które powinieneś wiedzieć: 'dt' jest odpowiedzialne za precyzję. Jeżeli 'dt' jest zbyt małe, Twoja symulacja będzie niestabilna i ruch będzie źle przeliczany. Analiza stabilności jest używana w symulacjach fizyki po to aby znaleźć maksymalną wartość 'dt', którą symulacja może używać. W tej lekcji nie będziemy zagłębiali się w szczegóły i jeżeli jesteś zainteresowany tylko pisaniem gry a nie aplikacji naukowej, to możesz używać maksymalnej wartości, otrzymanej metodą prób i błędów. Dlatego właśnie symulacje fizyczne tak rzadko były używane przez starsze gry.
W kodzie poniżej możemy zdefiniować maksymalne możliwe 'dt' jako 0,1 sekundy (100 milisekund). Przy pomocy tej wartości będziemy obliczać liczbę powtórzeń które mają nastąpić podczas aktualizacji. Napiszmy wzór:
int numOfIterations = (int)(dt / maxPossible_dt) + 1;
'numOfIterations' to liczba powtórzeń, która musi nastąpić w czasie symulacji. Powiedzmy, że aplikacja działa z prędkością 20 fps'ów (ang. frames per second), co daje 'dt' = 0,05 sekundy. Później 'numOfIterations' równa się 1, symulacja będzie powtarzana raz na 0,05 sekundy. Powiedzmy 'dt' będzie równe 0,12 sekundy, wtedy 'numOfIterations' wynosi 2. Poniżej, zaraz za 'int numOfIterations = (int)(dt / maxPossible_dt) +1;' powinieneś zauważyć, że 'dt' jest przeliczane jeszcze raz. Właśnie tam 'dt' jest dzielone przez 'numOfIterations' i wynosi 'dt' = 0,12 / 2 = 0,06. 'dt' na początku było większe niż maksymalna możliwa wartość 0,1. Teraz mamy 'dt' równe 0,06 powtórzymy symulacje drugi raz i stąd będziemy mieli 0,12 na wyjściu. Przeanalizuj poniższy kod i miej pewność, że rozumiesz powyższy akapit.
Teraz zacznijmy pisać aplikację:
1. Bryła poruszającą się ze stałą prędkością
*class ConstantVelocity : public Simulation --> Klasa, która będzie reprezentować bryłę poruszająca się ze stałą prędkością.
Bryła poruszająca się ze stałą prędkością nie potrzebuje dodatkowej zewnętrznej siły. Musimy tylko stworzyć jedną bryłę i ustawić jej prędkość na (1.0f, 0.0f, 0.0f) aby poruszała się wzdłuż osi 'X' z prędkością 1 m/s. Będziemy dziedziczyć klasę 'Simulation'. Klasa "class ConstantVelocity":
Kiedy metoda 'operate(float dt)' z klasy 'ConstantVelocity' jest wywoływana, przelicza następne stany bryły. Ta metoda jest wywoływana przez główną aplikację, zanim cokolwiek zostanie narysowane w oknie. Powiedzmy, że Twoja aplikacja działa z prędkością 10 fps'ów, jeżeli taki sam czas będzie podawany do 'operate(float dt)' w każdej klatce to 'dt' będzie równe 0,1 sekundy. Kiedy symulacja wywołuje 'simulate(float dt)' wtedy nowe położenie będzie się zwiększać poprzez iloczyn 'velocity * dt':<p>
Vector3D(1.0f, 0.0f, 0.0f) * 0.1 = Vector3D(0.1f, 0.0f, 0.0f)
Po każdym powtórzeniu, bryła porusza się o 0,1 metra w prawo, po 10 klatkach przesunie się o jeden metr. Prędkość wynosi 1 m/s. Czy ten wynik jest przypadkiem? Jeżeli nie możesz odpowiedzieć na to pytanie poświęć trochę czasu, aby pomyśleć nad powyższymi zależnościami.
Kiedy uruchomisz aplikację zobaczysz, że bryła ze stałą prędkością porusza się po osi 'X'. Naciskając F2 zwiększysz czas, a przy pomocy F3 dziesięć razy go zwolnisz. Na ekranie zobaczysz linie reprezentujące koordynaty płaszczyzny, odstępy pomiędzy tymi liniami wynoszą jeden metr. Dzięki tym liniom możesz zaobserwować, że bryła porusza się jeden metr w ciągu jednej sekundy kiedy jest włączony tryb "świata rzeczywistego". W przypadku "trybu wolnego" porusza się jeden metr na dziesięć sekund. Technika opisujące powyższą metodę jest jedną z ogólniejszych, jednak aby jej używać powinieneś sztywno trzymać się ustalonych jednostek (czasu, odległości, itp. - przyp. tłum).
W symulacji ze stałymi prędkościami, nie dodawalibyśmy żadnych sił. Jednak wiemy, że jeśli jakaś siła działa na ciało, to porusza się ono ruchem przyspieszonym, więc kiedy chcemy mieć ruch przyspieszony dodajemy siły. W tym momencie dodamy siły w metodzie 'solve'. Siła wypadkowa ma wpływ na ruch:
Powiedzmy, że chcemy przyłożyć siłę o wartości jednego niutona na osi 'X'. Wtedy powinniśmy napisać:
mass->applyForce(Vector3D(1.0f, 0.0f, 0.0f));
w metodzie 'solve'. Jeśli dodać następną siłę o wartości dwóch niutonów po osi 'Y' powinieneś napisać:
mass->applyForce(Vector3D(0.0f, 2.0f, 0.0f);
w metodzie 'solve'. Możesz dodać jakąkolwiek siłę aby zmienić ruch. W następnej aplikacji zobaczymy jedną siłę zapisaną przy po mocy wzoru.
2. Bryła poruszającą się pod wpływem siły grawitacji
*class MotionUnderGravitation : public Simulation --> Klasa, która będzie reprezentować bryłę poruszającą się pod wpływem grawitacji.
Klasa 'MotionUnderGravitation' tworzy bryłę i przykłada siłę do niej. Ta siła to siła grawitacji, jest ona równa iloczynowi masy i przyspieszenia ziemskiego:
F = m * g
Na Ziemi gdy upuścimy obiekt, będzie on poruszał się z przyspieszeniem 9,81 m/s^2, dopóki nie doświadczy on innej siły niż siła grawitacji. Na Ziemi jest ona stała i równa 9,81 m/s^2 (jest niezależna od masy, wszystkie bryły spadają z tym samym przyspieszeniem.)
Klasa 'MotionUnderGravitation' posiada konstruktor:
Konstruktor pobiera 'Vector3D' grawitacji, który jest przyspieszeniem ziemskim, następnie jest on używany przez symulację do przyłożenia tej siły na obiekt.
W powyższym kodzie powinieneś zauważyć wzór F = m * g. Aplikacja tworzy 'MotionUnderGravitation' z polem 'Vector3D' o wartości "Vector3D(0.0f, -9.81f, 0.0f)". -9,81 oznacza ujemne przyspieszenie na osi 'Y' dzięki temu bryła będzie spadać. Uruchom aplikację i obserwuj co się dzieje. Wpisz 9.81 zamiast -9.81 i zaobserwuj różnicę.
3. Bryła połączoną z nieruchomym punktem za pomocą sprężyny
* class MassConnectedWithSpring : public Simulation --> Klasa, która będzie reprezentować bryłę poruszającą się pod wpływem działania przyłączonej sprężyny.
W tym przykładzie, chcemy połączyć bryłę z nieruchomym punktem przy pomocy sprężyny. Sprężyna powinna ciągnąć bryłę w kierunku punktu połączenia. W konstruktorze, klasy 'MassConnectedWithSpring' jest ustawiane położenie punktu połączenia oraz bryły.
Prędkość bryły ustawiona jest na zero i znajduje się ona w odległości dziesięciu metrów od punktu w którym zaczepiona jest sprężyna. Siła sprężyny opisana jest wzorem:
F = -k * x
'k' to współczynnik twardości/rozciągliwości (ang. stiff) sprężyny, 'X' to odległość bryły od punktu połączenia. Ujemna wartość we wzorze powoduje, że siła jest siłą ciągnącą (ang. attractive). Jeśli byłaby bez minusa to wtedy sprężyna odpychałaby, co byłoby dość dziwne i nieoczekiwane.
Siła sprężyny w powyższym kodzie jest tą samą co zdefiniowana we wzorze (F = -k * x). Tylko tutaj zamiast 'X' używamy 'Vector3D', ponieważ rozważamy przestrzeń 3D. 'springVector' podaje różnicę pomiędzy położeniem bryły a 'connectionPos'. Jeżeli wartość 'springConstant' jest większa oraz większa jest przyłożona siła to bryła oscyluje szybciej.
W tej lekcji, chciałem pokazać koncepcję silnika fizycznego, jeżeli interesujesz się fizyką to nie będziesz miał problemów ze stworzeniem własnych symulacji. Możesz spróbować bardziej skomplikowanych interakcji, dzięki temu otrzymasz bardzo atrakcyjne gry i dema. Następnym krokiem będzie opracowanie symulacji ciał, prostych mechanizmów oraz zaawansowanych metod symulacji.
Jeżeli masz jakiekolwiek pytania lub komentarze proszę skontaktuj się ze mną