Lekcja 39. Wstęp do symulacji fizycznych
Autor: Mysha
Oryginał: Physical Simulation Engine (Erkin Tunca & Jeff Molofee)
Źródła: http://nehe.gamedev.net/data/lessons/vc/lesson39.zip

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.

Zawartość

Projekt:

* class Vector3D --> Klasa, która będzie reprezentować wektor3D albo punkt3D w przestrzeni.

Siła i ruch:

* 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ą.

Używanie sił:

* 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:

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'.

Siła i ruch:

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.

class Mass
{
public:
    float m;         // Masa.
    Vector3D pos;         // Położenie w przestrzeni.
    Vector3D vel;         // Prędkość.
    Vector3D force;         // Siła przyłożona do tej bryły.
    Mass(float m)         // Konstruktor.
    {
        this->m = m;
    }
(...)    

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.

(...)
    void applyForce(Vector3D force)
    {
        this->force += force;         // Siła zewnętrzna zostaje dodana do sił działających na bryłę.
    }
    void init()         // Ta metoda zeruje wartość siły.
    {
        force.x = 0;
        force.y = 0;
        force.z = 0;
    }
(...)    

Jest kilka elementów do wykonania podczas symulacji:

  1. Wyzerowanie siły (zobacz metodę init())
  2. Nadanie sił zewnętrznych
  3. Zwiększenie kroku czasowego przez "zmianę w czasie"

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)':

(...)
    void simulate(float dr)
    {
        vel += (force / m) * dt;         // Zmień prędkość.
        // Zmiana jest proporcjonalna do przyspieszenia (force / m) i zmiany w czasie.
        pos += vel * dt;         // Zmień położenie.
        // Zmiana położenia jest zwiększana o iloczyn prędkości i kroku czasowego.
    }
};

Jak symulacja powinna działać:

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.

class Simulation
{
public;
    int numOfMasses;         // Liczba brył w kontenerze.
    Mass** masses;         // Bryły są reprezentowane przez tablicę wskaźników.
    Simulation(int numOfMasses, float m)         // Konstruktor tworzy kilka brył o masie 'm'.
    {
        this->numOfMasses = numOfMasses;
        masses = new Mass*[numOfMasses];         // Tworzy tablicę wskaźników.
        for(int a=0; a<anumOfMasses; ++a)         // Przechodzi przez każdy wskaźnik w tablicy.
            masses[a] = new Mass(m);         // Tworzy wskaźnik na bryłę i umieszcza go w tablicy.
    }
    virtual void release()         // Usuwa storzoną bryłę.
    {
        for(int a=0; a<numOfMasses; ++a)         // Usunie wszystkie.
        {
            delete(masses[a]);
            masses[a] = NULL;
        }
        delete(masses);
        masses = NULL;
    }
    Mass* getMass(int index)
    {
        if(index<0 || index>=numOfMasses)         // Jeżeli 'index' nie jest w tablicy.
            return NULL         // Wtedy zwróć NULL.
        return masses[index];         // Pobierz bryłę pod podanym indeksem.
    }
(...)

Symulacja przebiega w następujących trzech krokach:

  1. 'init()' ustawia siły na zero
  2. 'solve()' przykłada siły
  3. 'simulate(float dr') zmiana położenia brył pod wpływem zmieniającego się czasu (ang. iterate masses by the change in time)

(...)
    virtual void init()         // Ta metoda wywoła 'init()' każdej bryły.
    {
        for(int a=0; a<numOfMasses; ++a)         // Przejdzie przez wszystkie bryły.
            masses[a]->init();         // Wywołaj metodę 'init()' każdej bryły.
    }
    virtual void solve()         // Nie zaimplementowane ponieważ nie chcemy żadnych siły w tym prostym pojemniku.
    {         // W zaawansowanych pojemnikach, ta metoda będzie przeciążona i niektóre siły będą działały na bryły.
    }
    virtual void simulate(float dt)         // Zmiana położenia brył pod wpływem zmieniającego się czasu.
    {
        for(int a=0; a>numOfMasses; ++a)         // Przejdzie przez wszystkie bryły.
            masses[a]->simulate(dt);         // Uzyskaj nowe położenie i nową prędkość.
    }
(...)

Procedura symulacji jest upakowana w jedną metodę:

(...)
    virtual void operate(float dt)         // Aby zakończyć procedurę symulacji.
    {
        init();         // Krok 1: Zresetuj siły na zero.
        solve();         // Krok 2: Przyłóż siły.
        simulate(dt);         // Krok 3: Zmiana położenia brył pod wpływem zmieniającego się czasu.
    }
};

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:

  1. Bryłę poruszającą się ze stałą prędkością
  2. Bryłę poruszającą się pod wpływem siły grawitacji
  3. Bryłę połączoną z nieruchomym punktem za pomocą sprężyny

Obsługiwanie symulacji przez aplikację:

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:

void Update(DWORD miliseconds)         // Aktualizacja ruchu odbywa się tu.
{
(...)

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.

(...)
    float dt = miliseconds / 1000.0f;         // Zamienia milisekundy na sekundy.
    dt /= slowMotionRatio;         // Dzieli 'dt' przez 'slowMotionRatio' i otrzymuje nowe 'dt'.
    timeElpased += dt;         // Zwiększa czas, który upłynął.
(...)

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.

(...)    
    float maxPossible_dt = 0.1f;         // Powiedzmy, że maksymalna możliwa wartość 'dt' to 0,1 sekundy.
        // Potrzebujemy tego, więc nie podajemy mało precyzyjnej wartości.
    int numOfIterations = (int)(dt / maxPossible_dt) + 1;         // Obliczamy ilość powtórzeń, które muszą być wykonane podczas aktualizacji zależącej od 'maxPossible_dt' i 'dt'.
    if(numOfIterations != 0)         // Uniknij dzielenia przez zero.
        dt = dt / numOfIterations;         // 'dt' powinno być aktualizowane zgodnie z 'numOfIterations'.
    for(int a=0; a<numOfIterations; ++a)         // Potrzebujemy powtórzyć symulację tyle razy ile wynosi 'numOfIterations'.
    {
        constantVelocity->operate(dt);         // Zmień 'constantVelocity' pod wpływem 'dt'.
        motionUnderGravitation->operate(dt)         // Zmień 'motionUnderGravitation' pod wpływem 'dt'.
        massConnectedWithSpring->operate(dt);         // Zmień 'massConnectedWithSpring' pod wpływem 'dt'.
    }
}

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":

class ConstantVelocity : public Simulation
{
public:
    ConstantVelocity() : Simulation(1, 1.0f)         // Konstruktor na początku konstruuje klasę bazową podając jako argumenty jedną bryłę o masie jednego kilograma.
    {
        masses[0]->pos = Vector3D(0.0f, 0.0f, 0.0f);         // Bryła zostaje utworzona i jej położenie jest ustawione na początek układu.
        masses[0]->vel = Vector3D(1.0f, 0.0f, 0.0f);         // Ustawia prędkość bryły na (1.0f, 0.0f, 0.0f) m/s.
    }
};

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).

Używanie sił:

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:

class MotionUnderGravitation : public Simulation
{
    Vector3D gravitation;         // Przyspieszenie ziemskie.
        // Konstruktor najpierw konstruuje klasę bazową z jedną bryłą o wadze jednego kilograma.    
    MotionUnderGravitation(Vector3D gravitation) : Simulation(1, 1.0f)
    {         // 'Vector3D Gravitation' to przyspieszenie ziemskie.
        this->gravitation = gravitation;         // Ustaw grawitację tej klasy.
        masses[0]->pos = Vector3D(-10.0f, 0.0f, 0.0f);         // Ustaw położenie bryły.
        masses[0]->vel = Vector3D(10.0f, 15.0f, 0.0f);         // Ustaw prędkość bryły.
    }
(...)

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.

(...)
        // Siła grawitacji będzie przyłożona jeśli będziemy potrzebowali metody 'solve'.
    virtual void solve()
    {
        // Doda siłę do wszystkich brył (aktualnie mamy jedną, ale możemy powiększyć ich ilość w przyszłości).
        for(int a=0; a<numOfMasses; ++a)            
        // Siła grawitacji jest równa F = m * g (masa pomnożona przez przyspieszenie ziemskie).
            masses[a]->applyForce(gravitation * masses[a]->m);
    }
};

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.

class MassConnectedWithSpring : public Simulation
{
public:
    float springConstant;         // Im większe 'springConstant', tym sprężyna twardsza.
    Vector3D connectionPos;         // Położenie punktu, w którym zaczepiona jest sprężyna.
        // Konstruktor najpierw konstruuje klasę bazową.
    MassConnectedWithSpring(float springConstant) : Simulation(1, 1.0f)    
    {
        this->springConstant = springConstant;         // Ustaw 'springConstant'.
        connectionPos = Vector3D(0.0f, -5.0f, 0.0f);         // Ustaw 'connectionPos'.
        // Ustaw położenie bryły. Dziesięć metrów w prawo od 'connectionPos'.
        masses[0]->pos = connectionPos + Vector3D(10.0f, 0.0f, 0.0f);    
        masses[0]->vel = Vector3D(0.0f, 0.0f, 0.0f);         // Ustaw prędkość bryły na zero.
    }
(...)

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.

(...)
    virtual void solve()         // Siła sprężyny będzie przyłożona.
    {
        for(int a=0; a<numOfMasses; ++a)         // Będziemy przykładali siły dla każdej bryły.
        {
        // Znajdź wektor od położenia bryły do 'connectionPos'.
            Vector3D springVector = masses[a]->pos - connectionPos;
        // Nadaj siłę nawiązując do sławnego wzoru na siłę sprężyny.
            masses[a]->applyForce(-springVector * springConstant);    
        }
    }
};

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ą