Autor Wątek: Animacja szkieletowa - od zera  (Przeczytany 3723 razy)

Offline Avaj

  • Użytkownik

# Lipiec 05, 2010, 12:22:14
Chciałbym sobie zaimplementować od podstaw animację szkieletową, głównie żeby się jej nauczyć przy okazji. W tym celu stworzyłem sobie małą scenkę w Blenderze, która wygląda o tak:

http://screenup.pl/?l=06GHXHN

jak widać, obrócona jest tylko jedna kość, Bone.002, o 90 stopni wokół osi Z

w bindpose kości są po prostu równolegle do Bone.001, prosto do góry

napisałem eksporter, który wyeksportowałby to co mnie interesuje:

http://screenup.pl/?l=RYNF4OD

jak widać, kwaterniony się zgadzają - wszystkie kości są w spoczynku, za wyjątkiem kości nr. 2 która jest obrócona wokół osi Z o 90 stopni (a nawet -90 stopni, ale to nie ma znaczenia).


Zagadka, co teraz? Jak mając te rotacje, informację które kości wpływają na które wierzchołki oraz pozycje tych kości, dojść do modelu, żeby wyglądał tak jak powinien?

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Lipiec 05, 2010, 12:58:37
Dla każdej kości zbudować macierz, która realizowała by następującą serię przekształceń: bind pose (object space) -> pozycja w przestrzeni danej kości -> pozycja z uwzględnieniem animacji (object space)

Offline Avaj

  • Użytkownik

# Lipiec 05, 2010, 14:01:26
bind pose -> pozycja w przestrzeni danej kości = macierz translacji(-pozycja_kości)

wtedy tą macierz mnożę przez macierz obrotu w bone-space i potem znowu dodaję z powrotem pozycję kości i tą macierz już mogę użyć do przekształcenia wierzchołka, czy tak?

a w którym momencie uwzględniam parenta? bo jak widać kości-dzieci kości Bone.002 nie mają w swojej macierzy uwzględnionych tych poprawek, więc w którymś momencie będę musiał się przejechać od parenta w dół.

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Lipiec 05, 2010, 14:48:32
bind pose -> pozycja w przestrzeni danej kości = macierz translacji(-pozycja_kości)
Bardzo niejasno to napisałeś.


Masz dane:
- Bx - macierz transformująca z przestrzeni kości rodzica x do przestrzeni kości x (ułożenie w bind pose),
- Ax - macierz transformująca z przestrzeni kości x do przestrzeni rodzica x (ułożenie już takie, jak w danej klatce)

Jeżeli kość 1 jest rodzicem 2, a kość 2 jest rodzicem 3, to wierzchołki kości 3 musisz poddać następującemu przekształceniu:
p' = p * B2 * B3 * A3 * A2 * A1

Notacja wierszowa, rzecz jasna. Kość 1 nie ma rodzica, więc macierz A1 będzie przekształcała z przestrzeni kości 1 do przestrzeni świata.
Oczywiście trzeba wszystkie te macierze spąć w jedną i przekazać do vertex shadera (i to samo wyznaczyć dla każdej kości).
Równie oczywiście, nie ma sensu do shadera przekazywać macierzy 4x4 - część odpowiedzialną za projekcję można pominąć.
Efektem jest macierz transformującqa z bind pose do animowanej pozycji w przestrzeni świata. Pozostaje już tylko domnożyć przez macierz view-projection i gotowe.

Offline Avaj

  • Użytkownik

# Lipiec 09, 2010, 17:51:00
Wygląda na to, że działa. Ładnie przechodzi od jednego ułożenia do drugiego, liczone w vertex shaderze. Teraz pytanie, może nawet ostatnie:

Jakie macierze przechowywać na później, a które są potrzebne tylko tymczasowo. Na moje oko dla każdej kości będę pamiętał "spiętą" macierz B, zaś dla każdej klatki kluczowej a w niej dla każdej kości będę pamiętał spiętą macierz A. Czy tak?

(piszę macierze dla uproszczenia, prawdopodobnie dam kwaterniony bo się łatwiej interpoluje)

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Lipiec 09, 2010, 18:45:25
Jakie macierze przechowywać na później, a które są potrzebne tylko tymczasowo. Na moje oko dla każdej kości będę pamiętał "spiętą" macierz B, zaś dla każdej klatki kluczowej a w niej dla każdej kości będę pamiętał spiętą macierz A. Czy tak?

(piszę macierze dla uproszczenia, prawdopodobnie dam kwaterniony bo się łatwiej interpoluje)
Tak, ale nie kwaterniony, tylko właśnie macierze. Sugeruję przechowywanie:
- w kościach modelu: "spętych" macierzy B - to sie liczy raz przy wczytywaniu i po krzyku,
- w jakiejś klasie Pose albo coś takiego: kwaterniony i wektory przesunięć aktualnego ułożenia kości,
- w klasie Pose (po wywołaniu Pose::Compile albo cos takiego): "spięte" macierze A dla każdej kości (najlepiej jako wektor macierzy 4x3 - można jednym rzutem wrzucić do shadera w takim surowym stanie)

Offline Avaj

  • Użytkownik

# Lipiec 09, 2010, 19:15:47
Pose::Compile odpalam raz czy co klatkę? Bo czegoś nie rozumiem. Nawet jeśli będę miał gotowe spięte macierze A to nie mogę ich tak po prostu wrzucić do shadera. Muszę znaleźć macierz pośrednią między SpiętaAPose1 a SpiętaAPose2 i dopiero wtedy wrzucać to do shadera. Mógłbyś doprecyzować czy Pose to są po prostu klatki kluczowe animacji u ciebie?

Offline kafor7

  • Użytkownik

# Lipiec 09, 2010, 21:03:52
Tak wywołujesz to co klatkę i ta funkcja powinna ci zwrócić aktualne macierze jakie mają być przesłane do VS (jeżeli masz interpolacje między klatkami to interpolujesz to odpowiednio, jeżeli nie zwracasz macierze wczytane z pliku dla każdej klatki chodź nie wierzę, że nie interpolujesz tego) Przynajmniej ja tak robiłem przy md5.
« Ostatnia zmiana: Lipiec 09, 2010, 21:05:42 wysłana przez kafor7 »

Offline Avaj

  • Użytkownik

# Lipiec 09, 2010, 21:36:51
To pewnie coś oczywistego, ale coś się w tym całym Pose zagubiłem.

@kafor7: robiłeś MD5 na VS? ja dopiero co porzuciłem md5 bo jest to mało przyjemny format do animacji w VSie

Offline kafor7

  • Użytkownik

# Lipiec 09, 2010, 21:55:13
To pewnie coś oczywistego, ale coś się w tym całym Pose zagubiłem.

@kafor7: robiłeś MD5 na VS? ja dopiero co porzuciłem md5 bo jest to mało przyjemny format do animacji w VSie

tak mam gdzies z pomocą Krzyska K jakoś działało ale strasznie nie poukładany kod miałem. Nawet działało na VS tylko mogłem używać 63 kości (przez kartę graficzną - nie pociągnie więcej uniformów)

Offline Avaj

  • Użytkownik

# Lipiec 09, 2010, 22:44:34
Dobra, widzę bez kodu się nie obejdzie:

Kod: (cpp) [Zaznacz]
    void update(float deltaTime)
    {
        timer += deltaTime;

        KeyFrame& k1 = keyframes[0];
        KeyFrame& k2 = keyframes[1];
        for (int bone=0;bone<header.bones;bone++)
        {
            glm::mat4 boneTransform;
            for (int b=0;b<=bone;b++)
            {
               glm::quat rot = glm::gtc::quaternion::mix(k1.rotations[b], k2.rotations[b], timer);
               glm::mat4 after = glm::mat4(glm::gtc::quaternion::mat3_cast(rot));

                after[3][0] = k1.translations[b].x;
                after[3][1] = k1.translations[b].y;
                after[3][2] = k1.translations[b].z;

                boneTransform = boneTransform * after;
            }
            glm::mat4 finalMatrix = boneTransform;
            finalMatrix = finalMatrix * bones[bone].B;

            boneMatrices[bone] = finalMatrix;
        }
    }

Takie coś puszczam co klatkę. boneMatrices idą już prosto do shadera. Czy da się jakoś to rozkminić bez tej pętli "for (int b=0;b<=bone;b++)", bo trochę mnie stresuje, że będzie (n^2+1)/2 mnożeń macierzy, może da się to jakoś do n zoptymalizować? ^^

Offline kafor7

  • Użytkownik

# Lipiec 09, 2010, 23:34:29
Dobra, widzę bez kodu się nie obejdzie:

Kod: (cpp) [Zaznacz]
    void update(float deltaTime)
    {
        timer += deltaTime;

        KeyFrame& k1 = keyframes[0];
        KeyFrame& k2 = keyframes[1];
        for (int bone=0;bone<header.bones;bone++)
        {
            glm::mat4 boneTransform;
            for (int b=0;b<=bone;b++)
            {
               glm::quat rot = glm::gtc::quaternion::mix(k1.rotations[b], k2.rotations[b], timer);
               glm::mat4 after = glm::mat4(glm::gtc::quaternion::mat3_cast(rot));

                after[3][0] = k1.translations[b].x;
                after[3][1] = k1.translations[b].y;
                after[3][2] = k1.translations[b].z;

                boneTransform = boneTransform * after;
            }
            glm::mat4 finalMatrix = boneTransform;
            finalMatrix = finalMatrix * bones[bone].B;

            boneMatrices[bone] = finalMatrix;
        }
    }

Takie coś puszczam co klatkę. boneMatrices idą już prosto do shadera. Czy da się jakoś to rozkminić bez tej pętli "for (int b=0;b<=bone;b++)", bo trochę mnie stresuje, że będzie (n^2+1)/2 mnożeń macierzy, może da się to jakoś do n zoptymalizować? ^^

Nie wiem skąd ten kod się wziął ci, że masz prawie n^2 mnożeń macierzy.

ja mam to mniej więcej tak:
void Update(float dt){
   static int x = 0; //odpowiada to aktualnej klatce
   time += dt;
   if(time * fps >= 1.0f){
      time = 0;
      ++x;
   }

    REP(i, liczbaKosci){
         koscWynikowa[i] = Interpolation(frameList[x][i], frameList[x + 1][i], time * fps)
}

frameList - to tablica dwuwymiarowa każdej klatki zawierającą każdą kość (odwołanie frameList[0][0] odpowiada pierwszej klatce i pierwszej kości

Offline Avaj

  • Użytkownik

# Lipiec 10, 2010, 00:03:53
Dobra, nie mam dzisiaj bani do tego, jutro na spokojnie się zastanowię. Bo macierzy 4x4 nie da się interpolować (nie miałoby to sensu), zaś same kwaterniony nie wystarczą bo zgubi mi się info o translacjach. Do jutra ^^

Offline kafor7

  • Użytkownik

# Lipiec 10, 2010, 00:07:34
Dobra, nie mam dzisiaj bani do tego, jutro na spokojnie się zastanowię. Bo macierzy 4x4 nie da się interpolować (nie miałoby to sensu), zaś same kwaterniony nie wystarczą bo zgubi mi się info o translacjach. Do jutra ^^


zapomniałem ci dodać ta funckja wygląda tak naprawdę tak :

Matrix Interpolation(Quat& q1, Vec3& v1, Quat& q2, Vec3& v2, float t);

przyjmuje 2 kwaterniony i 2 translacje oraz współczynnik interpolacji a zwraca macierz

Offline blizniak

  • Użytkownik

# Lipiec 10, 2010, 00:10:10
@Java:

jak chcesz interpolować kości między klatkami to wylicz najpierw 2 zestawy kwaterniono/wektorów (jeden dla klatki n, drugi dla n+1),
potem przeprowadź interpolację mix osobno dla quat/vec,
a potem zamień na macierze i pomnóż przez odwrotną macierz spoczynkową

(przykład jak już masz dwa szkielety - u mnie obiekty klasy SPoseTransform)
SPoseTransform SModel::updatePoseTransformSLERP(const SPoseTransform* pt1,
                                      const SPoseTransform* pt2, float a)
{
    const uint nbones = armature.bones.size();

    SPoseTransform pt;
    pt.bones.resize(nbones);
    for(uint i = 0; i < nbones; i++)
    {
        const SPoseBoneData* pb1 = &pt1->bones[i];
        const SPoseBoneData* pb2 = &pt2->bones[i];
        pt.bones[i].pos = mix(pb1->pos, pb2->pos, a);
        pt.bones[i].ort = mix(pb1->ort, pb2->ort, a);
        pt.bones[i].id = pb1->id;
    }

    for(uint i = 0; i < nbones; i++)
    {
        const SBoneData& bd = armature.bones[i];       

        mat4 rest_matrix = toMat4(bd.ort);
        rest_matrix[3] += vec4(bd.pos, 0);
        mat4 rest_matrix_inv = inverse(rest_matrix);

        // find pose bone
        const SPoseBoneData* pb = pt.getBoneByID(bd.id);
        mat4 pose_matrix = toMat4(pb->pos, pb->ort);
        _poseTransform[i] = pose_matrix * rest_matrix_inv;   
    }

    return pt;
}