Ha valaki vagy Unity-ben vagy egyéb 3D motor alatt fejlesztett már játékot, szinte biztos, hogy találkozott már a quaternionok fogalmával. Viszont könnyen lehet, hogy nem érti, mire valók és miért van szükség rájuk. Ezt kívánja ez a cikk orvosolni.
Egy quaternion nem más, mint egy absztrakt matematikai objektum, aminek sok hasznos felhasználási módja van. Míg a mérnökök ezzel reprezentálják egy repülő vagy űrhajó forgását, a fizikusok a kvantummechanikában lelik hasznát. Számunkra most a számítógépes grafikában betöltött szerepe a fontos.
Egy quaternion egy 3D-s térbeli forgatást ír le.
Nem csak a Unity játékmotor, de szinte minden egyéb modern 3D-s szoftver quaterniont használ egy elforgatás reprezentálására, annak ellenére, hogy mint látni fogjuk, több lehetőség is lenne.
A magyar matematikai szakirodalomban “kvaternió”-nak nevezik a fogalmat, ám mivel ez a cikk programozóknak szól és én még sosem hallottam ezt a kifejezést informatikus szájából, így az angol megfelelőjét fogom használni itt.
Forgatás Euler koordinátákkal
Mielőtt megvizsgálnánk a quaternionok működését, nézzük meg milyen egyéb módon ábrázolhatunk egy forgatást!
Ha egy testet elforgatunk saját (lokális)
- X tengelye körül x fokkal,
- Y tengelye körül y fokkal,
- Z tengelye körül z fokkal,
akkor egy számhármassal leírható a forgatás. Ezeket nevezzük euler koordinátáknak vagy euler szögeknek.
A különböző tengelyek közti fokban leírt forgatást reprezentálhatjuk egy számhármassal, azaz vektorral: (x, y, z). A Unity-ben már van is erre a célra egy típus, a Vector3.
Ha megnézzük a Transform komponens inspector ablakát, akkor az alapján úgy gondolhatnánk, hogy a Unity is euler koordinátákat használ és ahogy a pozíciónak és a skálázásnak is, úgy az elforgatásnak is Vector3 a típusa. Ám ha kódban megpróbáljuk lekérdezni a megfelelő értékeket, láthatjuk, hogy a rotáció lokális és világkoordináta rendszerben is Quaternion típusú.
Vector3 localPosition = transform.localPosition;
Vector3 position = transform.position;
Vector3 localScale = transform.localScale;
Vector3 lossyScale = transform.lossyScale;
Quaternion localRotation = transform.localRotation;
Quaternion rotation = transform.rotation;
Az elforgatás reprezentálása egy 3D vektorral a nagy svájci matematikustól, Leonhard Eulertől származik, aki a quaternionok “felfedezése” előtt írta le módszerét. Az euler koordináták használatának előnye az egyszerűségében rejlik. Mi emberek intuitívan tudunk a térben gondolkodni velük, legalábbis jóval inkább, mint bármi mással. Ezért is jeleníti meg az inspector felületen a Unity a Quaternionokat euler koordinátákkal.
Egyéb módjai is vannak egy 3D orientáció leírásának, mint az euler szögek és a qutarnion, erre példa lehet a rotációs mátrix. Ezeket az egyéb lehetőségeket itt nem tárgyaljuk.
Miért nem használunk euler koordinátákat?
Sajnos több hátrányuk is van az euler koordinátáknak a quaternionokkal szemben.
Az egyik ilyen a lineáris interpoláció, azaz az egyenletes átmenet A állapotból B állapotba.
Ha euler koordináták x, y és z értékén interpolálunk az egyik elfordulásból a másik elfordulásba, akkor közel sem a lehető legrövidebb utat kapjuk. Ezzel szemben a quaternionoknál igen.
Lásd a lenti példakódot és hozzá tartozó videókat:
Quaternionok interpolációja
// Euler koordináták:
[SerializeField] Vector3 eA, eB;
[SerializeField, Range(0,1)]
float phase = 0;
void OnValidate()
{
// Euler koordinátákból Quaternion:
Quaternion qA =
Quaternion.Euler(eA);
Quaternion qB =
Quaternion.Euler(eB);
// Interpolált Quaternion:
transform.rotation =
Quaternion.Slerp(qA, qB, phase);
}
Euler szögek interpolációja
// Euler koordináták:
[SerializeField] Vector3 eA, eB;
[SerializeField, Range(0,1)]
float phase = 0;
void OnValidate()
{
// Interpolált euler koordináták:
Vector3 lerp =
Vector3.Lerp(eA,eB, phase);
// Euler koordinátákból Quaternion:
transform.rotation =
Quaternion.Euler(lerp);
}
Ha kételkednétek abban, hogy mindezeknek van szerepe a valódi játékfejlesztésben, itt egy példa, mi történik, ha a fejlesztő euler koordinátákon interpolál Quaternionok helyett:
Emellett euler szögeken előállíthatunk olyan elfordulást, amiben 3-számból (x, y vagy z) 2 ugyanazon tengely mentén forgat, ezáltal lesz egy tengelyünk, ami mentén nem tudunk forgatni. Ezen tengelyen ekkor csak mind a 3 szám együttes módosításával tudunk mozogni. Ezt nevezzük gimbal lock-nak. (Míg a 3D grafikában ez a jelenség kényelmetlenséget szül, addig a repülők és űrjárművek műszereiben fellépő gimbal-lock életveszélyes szituációt képes eredményezni, amire példa lehet az Apollo 13 holdkompjának esete.)
Az euler koordináták hiányosságai az animációt igen nehézkessé tudják tenni. Sajnos a Unity animációs rendszerében szintén csak euler koordinátákat tudunk animálni, ami nem csupán nem optimális, de a fenti problémák ugyanúgy megjelennek benne.
Unity alatt quaterniont és euler szögeket egymásba alakítani a következő módon lehet:
Vector3 euler = new Vector3(1, 1, 1);
Quaternion q = Quaternion.Euler(euler); // Euler -> Quaternion
Vector3 toEuler = q.eulerAngles; // Quaternion -> Euler
Quaternionok
A fogalmat először William Rowan Hamilton írta le, aki fél életét azzal töltötte, hogy megtalálja a komplex számok matematikájának 3 dimenziós megfelelőjét.
A komplex számok egyik nagy előnye, hogy egy olyan 2D transzformációt reprezentálhatunk vele, ami tartalmaz egy elforgatást és egy méretezést. Ezen transzformáció végrehajtása nem igényel semmi trigonometriát, csupán néhány összeadást és szorzást.
Ehhez hasonlóan viselkedő 3D-s matematikai objektumot keresett Hamilton. Tehát valami olyat, amivel le lehet írni egy 3D elfordulást és könnyedén műveleteket végezni velük.
A megfejtés szikrája 1843-ban egy Dublini hídon átkelve pattant ki az ír matematikus fejéből, amikor is felismerte, hogy a megoldás nem 3, hanem 4 dimenzióban lehetséges csak.
A következőkben tárgyaltak megértése nem teljes egészében szükséges ahhoz, hogy használni tudjuk a quaternionokat Unity-ben, de hasznos, ha fejlődni szeretnénk, mint 3D programozók.
Javasolt lehet kezdőknek úgy olvasni a cikket, hogy a bonyolultabb matematikai részek felett átsiklanak és inkább a definiált fogalmak megértésére és quaternionok gyakorlati használatára, a kódrészletekre koncentrálnak.
Quaternion, mint 4 dimenziós objektum
Mint mondtam, a quaternionok igazából a komplex számok négy dimenzióra történő kiterjesztései.
Eszerint a quaternionokat 4 számmal reprezentálhatjuk, Unity alatt ezek a számok a következő módon vannak elnevezve: .
Ekkor egy quaterniont így írhatunk le:
Ahol i, j, és k a komplex számokhoz hasonló “képzeletbeli” tagok, amikre igaz, hogy:
A quaternion valós része a és képzetes része az vektor.
Quaternion létrehozása Unity-ben float típusú tagokból így zajlik:
Quaternion q = new Quaternion(x, y, z, w);
Elfordulás és quaternion
Feltűnhet, hogy eggyel több számot tartalmaz egy quaternion, mint ami egy elfordulás szabadsági foka, azaz egy forgatás egyértelmű meghatározásához szükséges, egymástól független mennyiségek száma.
Mivel a quaternionok eggyel több számértéket tartalmaznak, mint az elforgatás szabadsági foka, így nem minden számnégyes ír le egy elforgatást reprezentáló quaterniont, csak amikre igaz a következő állítás:
azaz a 4 dimenziós térben (x,y,z,w) vektor hossza egy.
Az ezen feltételnek megfelelő quaternionokat nevezzük egység-quaternionoknak.
Egy Unity Quaternion típusú objektum felvehet nem egységnyi értéket is, de amikor beállítjuk azt egy Transformnak, akkor a Unity automatikusan végrehajt rajta egy normalizálást, azaz 1 egység méretűre állítja.
Tengely körüli elforgatás
Egy 3D forgatásértékre tekinthetünk úgy, mint egy tetszőleges tengely és egy akörüli skaláris elforgatás. Ha így nézzük, akkor egy 3D elforgatás a következőképp írható le 4 számmal:
Egy tengelyt meghatározó irányvektor (a quaternion képzetes része).
Elforgatás szöge (a quaternion valós része).
A quaternionra tekinthetünk eszerint is, de a különböző mennyiségek megfeleltetése egy kicsikét bonyolultabb:
Tengelyből és akörüli elforgatásból a következő módon állíthatjuk elő a megfelelő quaterniont:
Quaternion AngleAxis(float angle, Vector3 axis)
{
float halfAngle = angle * 0.5f * Mathf.Deg2Rad;
float cos = Mathf.Cos(halfAngle);
axis *= Mathf.Sin(halfAngle);
return new Quaternion(x: axis.x, y: axis.y, z: axis.z, w: cos);
}
A fenti függvényt nem szükséges saját magunknak implementálni, ugyanis létezik rá beépített Unity megoldás ugyanúgy, ahogy az inverz műveletére is:
float angle = 180; // Elfordulás fokban
Vector3 axis = Vector3.up; // Tengely
// Tengely és elfordulás quaternionná alakítása:
Quaternion q = Quaternion.AngleAxis(angle, axis);
// Quaternion szétbontása tengelyre és elfordulásra:
q.ToAngleAxis(out float toAngle, out Vector3 toAxis);
Quaternionok szorzása
Ahogy a komplex számok szorzásával egyszerűen forgathatunk 2D pontokat a 2D síkon, úgy a quaternionok használatával lineáris műveletekkel forgathatunk 3D pontokat a 3D térben.
Ebben rejlik a quaternionok nagy előnye, és ez az oka annak, hogy qaternionok lineáris interpolációja egyenletes tengely körüli forgatást ír le.
Többek közt azért is van szükségünk arra, hogy egymáshoz “adjunk” különböző elfordulásokat, hogy a Unity könnyen ki tudja számolni a GameObject hierarchiában mélyen elhelyezkedő gyerek objektumok globális rotációját maga az objektum és szülői lokális rotációiból.
Erre való a quaternionok szorzása.
Ha például A, B, C GameObjectek közül
A gyereke B és
B gyereke C,
akkor C globális elfordulása a térben magkapható A, B és C lokális rotációjának szorzataként.
Két quaternion szorzata:
Ennek megoldása:
(Itt használtuk az i, j, és k képzetes tagokra vonatkozó azonosságokat.)
Ezt a következő programkóddal tudnánk leírni:
Quaternion Multiply(Quaternion q, Quaternion p)
{
float w = -q.x * p.x - q.y * p.y - q.z * p.z + q.w * p.w;
float x = q.x * p.w + q.y * p.z - q.z * p.y + q.w * p.x;
float y = -q.x * p.z + q.y * p.w + q.z * p.x + q.w * p.y;
float z = q.x * p.y - q.y * p.x + q.z * p.w + q.w * p.z;
return new Quaternion(x,y,z,w);
}
A fenti függvényt szerencsére nem kell nekünk leimplementálni ugyanis Unity-ben van rá beépített megoldás. Két quaternion szorzásához a csillag operátort használjuk:
float angle = 180; // Elfordulás fokban
Vector3 axis = Vector3.up; // Tengely
Quaternion p = AngleAxis(angle, axis); // Máshogy is létrehozhatnánk
Quaternion q = transform.rotation; // Máshogy is létrehozhatnánk
transform.rotation = p * q; // Quaternion szorzás
Az identitás quaternion
Bármely algebrában azt az értéket, amivel egy elvégzett művelet önmagába visz, identitás-nak nevezzük.
Ahogy a hagyományos számokon végzett szorzásnak az 1 az identitása (), úgy a quaternionok szorzásának is létezik identitásértéke:
Ez könnyen lekérhető a következőképp:
Quaternion identity = Quaternion.identity;
bool testIdentity = q * identity == q; // true
Az identitás megegyezik egy Unity Quaternion alapértelmezett értékével és a (0, 0, 0) Euler szögekkel.
A szorzás iránya
Quaternionok szorzása nem kommutatív művelet, tehát .
Ez azt jelenti, hogy az hogy melyik oldalról szorzod a transform rotációját befolyásolni fogja a művelet eredményéti.
Lehet úgy gondolni a Quaternion szorzására, hogy
- ha jobb felől szorzok, akkor azzal a lokális koordinátarendszerben vett tengely körül forgatok,
- ha bal felől szorzok, akkor azzal a globális koordinátarendszerben vett tengely körül forgatok.
[SerializeField] Quaternion original;
[SerializeField] Vector3 axis;
[SerializeField] float angle;
[SerializeField] bool multiplyFromTheRight;
void OnValidate()
{
Quaternion q = Quaternion.AngleAxis(angle, axis);
if (multiplyFromTheRight)
transform.localRotation = original * q; // Forgatás lokális felfelé körül
else
transform.localRotation = q * original; // Forgatás globális felelé körül
}
void OnDrawGizmos()
{
// A beállított felfelé tengely globális verzióját sárgával,
// lokális verzióját magentával (rózsaszínnel) rajzolom ki.
// ...
Pont elforgatása egy quaternionnal
A quaternionok újabb nagy előnye abban van, hogy bármely tengely mentén képesek egy pontot elforgatni bármekkora szögben és a szükséges számítás gyors és relatíve egyszerű.
pont elforgatását quaternionnal így írhatjuk le:
Ahol a Quaternion inverze, aminek értéke , tehát felbontva:
Ennek megoldását nem írom itt le, a Unity alatt úgyis egy pontot egy quaternion-nal könnyedén el lehet forgatni a csilla operátorral:
float angle = 180; // Elfordulás fokban
Vector3 axis = Vector3.up; // Tengely
Quaternion q = Quaternion.AngleAxis(angle, axis); // Máshogy is létrehozhatnánk
Vector3 p = new Vector3(1, 1, 1); // Pont
Vector3 end = q * p; // Quaternion szorzás
Quaternionok létrehozása
Unity alatt quaternionokat többféle módon tudunk létrehozni. Ebből néhányat már láttunk.
// A valós és képzeletbeli takgokból:
Quaternion q1 = new Quaternion(x, y, z, w);
// Euler szögekből (vector3 vagy három float)
Quaternion q2 = Quaternion.Euler(Vector3.up);
Quaternion q3 = Quaternion.Euler(1.25f,2.5f,3.5f);
// Egy tengelyből (Vector3) és egy tengely körüli elforgatás szögből (float):
Quaternion q4 = Quaternion.AngleAxis(angle, axis);
// Két tengelyből:
// Ebben a példában az eredeti felefelé vektor kerül az előrefelé vektor helyére:
Quaternion q5 = Quaternion.FromToRotation(Vector3.up, transform.forward);
// Egy lokálisan előre mutató vektorból:
// Ebben az esetben a lokális felfelé a legközelebb lesz a globális felfelé irányhoz.
Quaternion q6 = Quaternion.LookRotation(relativePos);
// Egy lokálisan előre és egy felfelé mutató vektorból:
// Ebben az esetben a lokális felfelé a legközelebb lesz a beállított felfelé irányhoz.
Quaternion q7 = Quaternion.LookRotation(relativePos, Vector3.up);
Emellett “keverhetünk” quaternionokat lineáris interpolációval.
Az interpolációs függvények 3 bemenetet várnak: két quaterniont (A és B) és egy számértéket 0 és 1 közt, amit nevezzünk t-nek. Az eredmény mindig egy quaternion lesz, ami valahol A és B elfordulása közt van. Minél közelebb van a nullához a t számérték, annál jobban hasonlít az eredmény A-hoz, minél közelebb 1-hez annál közelebb áll az eredmény B-hez.
Korábban láttuk, hogy a quaternionok közti lineáris interpoláció sokkal egyenletesebb, mint ha ugyanezt euler szögeket tartalmazó vektorokkal oldanánk meg (emlékezzünk az Ördögűző című filmből szökött Fifa játékosra). Most nézzük meg, quaternionokat hogy lehet interpolálni.
Szimpla lineáris interpoláció (Lerp) a két elforgatás közt egyenes vonalat képez, és ezen a ponton fog egyenletesen végighaladni. Ezzel szemben a gömbmenti (spherical) lineáris interpoláció (Slerp) a kör mentén halad egyenletesen.
Két azonos mértékű módosítása t-nek Lerp alatt lehet, hogy más szöget fog bezárni, míg Slerp esetén biztosak lehetünk benne, hogy a t értékében történő egységnyi mértékű változás mindenhol ugyanazt a szöget fogja lefedni. Lásd a következő ábrán:
Quaternion q1 = Quaternion.Euler(10, 20, 30);
Quaternion q2 = Quaternion.Euler(50, 60, 70);
float t = Time.time % 1;
Quaternion lerped = Quaternion.Lerp(q1, q2, t);
Quaternion slerped = Quaternion.Slerp(q1, q2, t);
A Lerp művelet végrehajtása a háttérben nagyon egyszerűen zajlik, csupán a w, x, y, z float értékeket kell a párjuk közt lineárisan interpolálni. Így a függvény végrehajtása nagyon gyors lesz, hiszen csak szorzást, összeadást és kivonást igényel, mint elemi műveleteket. Ezzel szemben az Slerp trigonometriai számításokat is igényel, így sokkal lassabb a végrehajtása a Lerp-hez képest.
Vegyük viszont észre, hogy a lineáris interpoláció eredményei a köztes pontokon nem egység-quaternionok. Így, ha egy forgatást akarunk reprezentálni vele, normalizálni kell. Ezzel a legtöbb esetben nincs semmi extra dolgunk, hiszen a normalizálás automatikusan megtörténik, amikor egy transform rotációját beállítjuk.
Ezt azért fontos kiemelni, mert bár a Quaternion Lerp magában sokkal gyorsabb művelet, mint a Slerp, de a szükséges normalizálás miatt a különbség a legtöbb valós felhasználás esetén már nem túl nagy.