Obracam obiekt w dwóch osiach, więc dlaczego wciąż skręca się wokół trzeciej osi?

38

Widzę pytania, które pojawiają się dość często, które dotyczą tego podstawowego problemu, ale wszystkie są uwikłane w szczegóły danej funkcji lub narzędzia. Oto próba stworzenia kanonicznej odpowiedzi, do której możemy skierować użytkowników, kiedy to się pojawi - z dużą ilością animowanych przykładów! :)


Powiedzmy, że tworzymy aparat z perspektywy pierwszej osoby. Podstawową ideą jest to, że należy ziewać, aby spojrzeć w lewo i w prawo, a wysokość, aby spojrzeć w górę i w dół. Piszemy więc trochę takiego kodu (na przykładzie Unity):

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    // Yaw around the y axis using the player's horizontal input.        
    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f);

    // Pitch around the x axis using the player's vertical input.
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f);
}

albo może

// Construct a quaternion or a matrix representing incremental camera rotation.
Quaternion rotation = Quaternion.Euler(
                        -Input.GetAxis("Vertical") * speed,
                         Input.GetAxis("Horizontal") * speed,
                         0);

// Fold this change into the camera's current rotation.
transform.rotation *= rotation;

I to w większości działa, ale z czasem widok zaczyna się krzywić. Wygląda na to, że kamera obraca się wokół swojej osi obrotu (z), chociaż powiedzieliśmy, aby obracała się tylko na x i y!

Animowany przykład kamery z widokiem z pierwszej osoby przechylającej się na boki

Może się to również zdarzyć, jeśli próbujemy manipulować obiektem przed kamerą - powiedzmy, że jest to kula ziemska, którą chcemy obrócić, aby się rozejrzeć:

Animowany przykład kuli ziemskiej przechylającej się na boki

Ten sam problem - po chwili biegun północny zaczyna błądzić w lewo lub w prawo. Dajemy wkład w dwie osie, ale uzyskujemy ten mylący obrót w trzeciej. I dzieje się tak, czy zastosujemy wszystkie nasze obroty wokół lokalnych osi obiektu, czy globalnych osi świata.

W wielu silnikach zobaczysz to również w inspektorze - obracaj obiekt na świecie, a nagle liczby zmieniają się na osi, której nawet nie dotknęliśmy!

Animowany przykład pokazujący manipulowanie obiektem obok odczytu jego kątów obrotu.  Kąt Z zmienia się, mimo że użytkownik nie manipulował tą osią.

Czy to błąd silnika? Jak powiedzieć programowi, że nie chcemy, aby zwiększał rotację?

Czy ma to coś wspólnego z kątami Eulera? Czy zamiast tego powinienem stosować ćwiartki, macierze obrotu lub wektory podstawowe?

DMGregory
źródło
1
Bardzo pomocna była również odpowiedź na następujące pytanie. gamedev.stackexchange.com/questions/123535/…
Travis Pettry

Odpowiedzi:

51

Nie, to nie jest błąd silnika lub artefakt określonej reprezentacji obrotu (mogą się one również zdarzyć, ale ten efekt dotyczy każdego systemu, który reprezentuje obroty, włącznie z czwartorzędami).

Odkryłeś prawdziwy fakt na temat działania rotacji w przestrzeni trójwymiarowej i odbiega ona od naszej intuicji dotyczącej innych transformacji, takich jak tłumaczenie:

Animowany przykład pokazujący, że zastosowanie obrotu w innej kolejności daje różne wyniki

Kiedy tworzymy rotacje na więcej niż jednej osi, otrzymujemy wynik nie tylko całkowitą / netto wartość, którą zastosowaliśmy do każdej osi (jak można się spodziewać w tłumaczeniu). Kolejność, w jakiej stosujemy obroty, zmienia wynik, ponieważ każdy obrót przesuwa osie, na których stosowane są następne obroty (jeśli obraca się wokół lokalnych osi obiektu) lub relację między obiektem a osią (jeśli obraca się wokół świata osie).

Zmiana relacji osi w czasie może mylić naszą intuicję co do tego, co każda z osi ma „robić”. W szczególności niektóre kombinacje obrotu w odchyleniu i skoku dają taki sam wynik, jak w przypadku obrotu!

Animowany przykład pokazujący sekwencję lokalnego pitch-yaw-pitch daje taki sam wynik jak pojedynczy lokalny roll

Możesz sprawdzić, czy każdy krok obraca się prawidłowo wokół żądanej osi - nie ma usterki silnika ani artefaktu w naszym zapisie, który koliduje z naszym wejściem lub zgaduje go z drugiej strony - sferyczny (lub nadprzestrzenny / czwartorzędowy) obrót oznacza po prostu nasze zawijanie transformacji wokół siebie. Mogą być lokalnie ortogonalne, dla małych rotacji, ale gdy się gromadzą, okazuje się, że nie są globalnie ortogonalne.

Jest to najbardziej dramatyczne i wyraźne dla zakrętów o 90 stopni, jak te powyżej, ale osie wędrujące pełzają również przy wielu małych obrotach, jak pokazano w pytaniu.

Co więc z tym zrobimy?

Jeśli masz już system obrotu z odchyleniem skoku, jednym z najszybszych sposobów wyeliminowania niepożądanego przechylenia jest zmiana jednego z obrotów, aby działał na globalnej lub macierzystej osi transformacji zamiast na lokalnych osiach obiektu. W ten sposób nie można uzyskać zanieczyszczenia krzyżowego między nimi - jedna oś pozostaje całkowicie kontrolowana.

Oto ta sama sekwencja pitch-yaw-pitch, która stała się rzutem w powyższym przykładzie, ale teraz stosujemy nasze odchylenie wokół globalnej osi Y zamiast obiektu

Animowany przykład kubka obracającego się z lokalnym skokiem i globalnym odchyleniem, bez problemu z rolowaniemAnimowany przykład kamery pierwszoosobowej wykorzystującej globalne odchylenie

Możemy więc naprawić kamerę pierwszoosobową za pomocą mantry „Pitch Locally, Yaw Global”:

void Update() {
    float speed = lookSpeed * Time.deltaTime;

    transform.Rotate(0f, Input.GetAxis("Horizontal") * speed, 0f, Space.World);
    transform.Rotate(-Input.GetAxis("Vertical") * speed,  0f, 0f, Space.Self);
}

Jeśli sumujesz rotacje za pomocą mnożenia, odwróć lewą / prawą kolejność jednego z mnożeń, aby uzyskać ten sam efekt:

// Yaw happens "over" the current rotation, in global coordinates.
Quaternion yaw = Quaternion.Euler(0f, Input.GetAxis("Horizontal") * speed, 0f);
transform.rotation =  yaw * transform.rotation; // yaw on the left.

// Pitch happens "under" the current rotation, in local coordinates.
Quaternion pitch = Quaternion.Euler(-Input.GetAxis("Vertical") * speed, 0f, 0f);
transform.rotation = transform.rotation * pitch; // pitch on the right.

(Konkretna kolejność będzie zależeć od konwencji mnożenia w twoim środowisku, ale lewy = więcej globalny / prawy = więcej lokalny jest częstym wyborem)

Jest to równoważne z zapamiętywaniem całkowitego odchylenia netto i skoku całkowitego, który chcesz jako zmienne zmiennoprzecinkowe, a następnie zawsze stosowanie wyniku netto naraz, konstruowanie pojedynczej czwartorzędowej orientacji czwartorzędowej lub macierzy z tych samych kątów (pod warunkiem, że trzymasz się totalPitchzaciśniętego):

// Construct a new orientation quaternion or matrix from Euler/Tait-Bryan angles.
var newRotation = Quaternion.Euler(totalPitch, totalYaw, 0f);
// Apply it to our object.
transform.rotation = newRotation;

lub równoważnie ...

// Form a view vector using total pitch & yaw as spherical coordinates.
Vector3 forward = new Vector3(
                    Mathf.cos(totalPitch) * Mathf.sin(totalYaw),
                    Mathf.sin(totalPitch),
                    Mathf.cos(totalPitch) * Mathf.cos(totalYaw));

// Construct an orientation or view matrix pointing in that direction.
var newRotation = Quaternion.LookRotation(forward, new Vector3(0, 1, 0));
// Apply it to our object.
transform.rotation = newRotation;

Korzystając z tego podziału globalnego / lokalnego, obroty nie mają szansy na łączenie się i wpływanie na siebie, ponieważ są stosowane do niezależnych zestawów osi.

Ten sam pomysł może pomóc, jeśli jest to obiekt na świecie, który chcemy obrócić. Na przykład taki jak kula ziemska, często chcielibyśmy go odwrócić i zastosować nasze odchylenie lokalnie (dzięki czemu zawsze obraca się wokół biegunów) i przechylać się globalnie (więc przechyla się w kierunku / z dala od naszego widoku, a nie w kierunku / z dala od Australii , gdziekolwiek wskazuje ...)

Animowany przykład globu pokazującego rotację o lepszych zachowaniach

Ograniczenia

Ta globalna / lokalna strategia hybrydowa nie zawsze jest właściwym rozwiązaniem. Na przykład w grze z lotem / pływaniem 3D możesz chcieć być skierowany prosto w górę / prosto w dół i nadal mieć pełną kontrolę. Ale dzięki tej konfiguracji uderzysz w blokadę gimbala - twoja oś odchylenia (globalne w górę) staje się równoległa do osi przechyłu (lokalny do przodu) i nie możesz spojrzeć w lewo ani w prawo bez skręcania.

Zamiast tego w takich przypadkach możesz użyć czystych lokalnych rotacji, tak jak to rozpoczęliśmy w powyższym pytaniu (aby twoje kontrolki czuły to samo bez względu na to, gdzie patrzysz), co początkowo pozwoli, aby trochę się wkradło - ale potem poprawiamy to.

Na przykład, możemy użyć obrotów lokalnych, aby zaktualizować nasz wektor „do przodu”, a następnie użyć tego wektora do przodu wraz z referencyjnym wektorem „do góry”, aby skonstruować naszą ostateczną orientację. (Używając na przykład metody Quitynion.LookRotation firmy Unity lub ręcznie konstruując macierz ortonormalną z tych wektorów). Kontrolując wektor w górę, kontrolujemy rolkę lub skręcenie.

Na przykład w locie / pływaniu będziesz chciał stopniowo stosować te poprawki. Jeśli jest to zbyt gwałtowne, widok może się rozpraszać. Zamiast tego możesz użyć obecnego wektora w górę odtwarzacza i podpowiedzieć go w kierunku pionowym, klatka po klatce, aż jego poziom wyrówna się. Stosowanie tego podczas tury może czasami być mniej mdłości niż skręcanie kamery, gdy elementy sterujące odtwarzacza są bezczynne.

DMGregory
źródło
1
Czy mogę zapytać, jak się masz do gifów? Ładnie wyglądają.
Bálint
1
@ Bálint Korzystam z bezpłatnego programu o nazwie LICEcap - pozwala nagrać część ekranu do gifa. Następnie przycinam animację / dodaję dodatkowy tekst podpisu / kompresuję gif za pomocą Photoshopa.
DMGregory
Czy to odpowiedź tylko dla Unity? Czy w sieci jest bardziej ogólna odpowiedź?
posfan12,
2
@ posfan12 W przykładowym kodzie użyto składni Unity dla konkretności, ale sytuacje i matematyka obowiązują w jednakowym stopniu. Czy masz trudności z zastosowaniem przykładów w swoim środowisku?
DMGregory
2
Matematyka jest już obecna powyżej. Sądząc po twojej edycji, po prostu mylisz niewłaściwe części. Wygląda na to, że używasz zdefiniowanego tutaj typu wektora . Obejmuje to sposób konstruowania macierzy obrotu z zestawu kątów, jak opisano powyżej. Zacznij od matrycy tożsamości i wywołania TCVector::calcRotationMatrix(totalPitch, totalYaw, identity)- jest to odpowiednik powyższego przykładu „Euler naraz”.
DMGregory
1

PO 16 godzinach funkcji rodzicielskich i rotacyjnych lol.

Chciałem tylko, żeby kod miał pusty, w zasadzie obracający się w kółko, a także obracający się z przodu.

Innymi słowy, celem jest obrócenie odchylenia i nachylenia bez żadnego wpływu na rzut.

Oto kilka, które faktycznie działały w mojej aplikacji

W jedności:

  1. Utwórz pusty. Nazwij to ForYaw.
  2. Daj temu dziecku puste, o nazwie ForPitch.
  3. Na koniec utwórz sześcian i przenieś go do z = 5 (do przodu). Teraz widzimy, czego staramy się unikać, czyli przekręcenie sześcianu (przypadkowe „przewrócenie” podczas ziewania i przechylania) .

Teraz skryptuj w Visual Studio, przeprowadź konfigurację i zakończ to w Aktualizacji:

objectForYaw.Rotate(objectForYaw.up, 1f, Space.World);
    objectForPitch.Rotate(objectForPitch.right, 3f, Space.World);

    //this will work on the pitch object as well
    //objectForPitch.RotateAround
    //    (objectForPitch.position, objectForPitch.right, 3f);

Zauważ, że wszystko się zepsuło, kiedy próbowałem zmienić kolejność rodzicielstwa. Lub jeśli zmienię Space.World na obiekt pitch dziecka (nadrzędny Yaw nie ma nic przeciwko obracaniu przez Space.Self). Mylące. Mógłbym zdecydowanie wyjaśnić, dlaczego musiałem przyjąć lokalną oś, ale zastosować ją w przestrzeni świata - dlaczego Space.Self rujnuje wszystko?

Jeh
źródło
Nie jest dla mnie jasne, czy ma to na celu odpowiedzieć na pytanie, czy prosisz o pomoc w zrozumieniu, dlaczego napisany kod działa w ten sposób. Jeśli to drugie, powinieneś opublikować nowe pytanie, podając link do tej strony w celu uzyskania kontekstu, jeśli chcesz. Podpowiedź me.Rotate(me.right, angle, Space.World)jest taka sama, jak me.Rotate(Vector3.right, angle, Space.Selfi twoje zagnieżdżone transformacje są równoważne przykładom „Pitch Lokalnie, Yaw Globalnie” w zaakceptowanej odpowiedzi.
DMGregory
Trochę obu. Częściowo podzielić się odpowiedzią (np. Tutaj jest dokładnie jak dla mnie zadziałało). A także przedstawić pytanie. Ta wskazówka skłoniła mnie do myślenia. Wielkie dzięki!
Jeh
Zadziwiające, że nie zostało to przyjęte. Po wielu godzinach czytania wyczerpujących odpowiedzi, które, choć edukacyjne, nadal nie działały, wystarczy prosta oś obrotu obiektów zamiast ogólnej osi Vector3.right, aby utrzymać obrót na obiekcie. Niesamowite, czego szukałem, zostało pochowane tutaj z 0 głosami i na 2. stronie Google wielu, wielu podobnych pytań.
user4779