Az elhajított kő pályája mindig egy parabolát ír le, ha eltekintünk a közegellenállástól. Ennek matematikája nem vészesen bonyolult és könnyen fordítható programkódra, ami kifejezetten hasznos akciójátékok készítésében, ahol a lövedékek pályájának számítása szükséges. Hogyan tudom előre jósolni, hol esik le, meddig repül, milyen pályán a golyó. És ami talán még fontosabb: Milyen szögben vagy milyen sebességgel kell elhajítanom az objektumot, hogy eltaláljam az ellenfelet. Ezen jelenség fizikáját, matematikáját és szimulációját fogjuk áttekinteni.
Az alábbiakban nagy szükség lesz a 2D és 3D vektorok és műveleteik stabil ismeretére.
Hajítás elmélet
Először csak 2 dimenziós síkkal foglalkozunk. Később fogjuk áttekinteni a megoldást a 2D-s és 3D-s tér bármely pontjára.
Nézzük meg először, hogy milyen információkkal tudjuk jellemezni egy objektum elhajításának folyamatát. Az alábbi magyarázatok tartalmazzák a jelöléseket, amiket a képletekben használok valamint, hogy a lenti kódrészletekben milyen néven hivatkozok fizikai mennyiségekre
A hajítás kezdetét leíró információk:
- : Kezdőpont. (Kódban:
Vector2 startPoint
) - : Kezdősebesség (Kódban:
Vector2 velocity
)
A rendszerre állandó:
- : Gravitációs gyorsulás. Állandó, lefelé irányuló (Kódban:
float gravity
) Mennyivel változik a hajított test sebességének függőleges komponense másodpercenként? - D: Közegellenállás. (Legtöbb esetben elhanyagoljuk) (Kódban:
float drag
)
Az elhajított test állapota egy adott időpillanatban:
- : Hajítás óta eltelt idő (Kódban:
float fullTime
) - : Végpont pozíciója. (Kódban:
Vector2 targetPoint
) - : Kezdőponttól való elmozdulás. (Kódban:
Vector2 distanceVector
) - : Jelenlegi sebesség (Kódban:
Vector2 velocity
)
A maximum emelkedésre vonatkozó információk:
- : A görbe legfelső pontjának pozíciója (Kódban:
Vector2 positionAtTop
) - : A sebességvektor a görbe tetején (Kódban:
Vector2 velocityAtTop
) - : A görbe legfelső pontjáig való eljutás ideje (Kódban:
float timeAtTop
)
A következőkben csak olyan idealizált esetekkel foglalkozunk, ahol a kezdőpont egyszerűség kedvéért mindig az origóban van, így a vektor és a pont megegyezik. Ha egy megoldandó feladatban a kezdőpont egyezik az origóval, akkor egy egyszerű eltolással el lehet végezni a szükséges transzformációt:
Vector2 startPoint, // Kezdőpont
Vector2 targetPoint; // Célpont
Vector2 distanceVector; // Elmozdulás
// ...
Vector2 distanceVector = targetPoint - startPoint; // Ha a targetPoint ismert
Vector2 targetPoint = startPoint + distanceVector; // Ha a distanceVector ismert
Talán meglepő lehet, hogy a súly vagy a tömeg nem része a szükséges információknak. Ez nem tévedés. Ha a közegellenállás elenyésző, akkor a tömeg irreleváns. (Akkor is így van ez, ha számolunk közegellenállást, de csak olyan egyszerűsített fizikai modellt alkalmazunk, amiben nem jelenik meg a tömeg. A Unity fizika is így működik és mi is így fogunk tenni ebben a cikkben.)
Galilei óta tudjuk mindezt, aki a legenda szerint a Pisai ferde toronyról hajigált le ágyugolyókat, hogy bizonyítsa, hogy minden tárgyat egyenlő mértékben gyorsít a föld gravitációja. Aki nem hiszi, nézze meg az Apollo 15 David Scott parancsnokának kísérletét a Hold légüres terében.
Szögek és vektorok
Egy vektor megkapható, annak hosszából és irányából. Ez persze igaz a sebességvektorokra is. A sebességvektor hossza a sebesség maga, tehát az a (float
) szám, hogy hány egységnyi utat tesz meg a tárgy másodpercenként. Az irányt pedig megadhatjuk különböző módokon: Lehet egy vízszintessel bezárt szög vagy pedig egy egység hosszú irányvektor.
Ha az irány egy egységvektor, akkor nincs más dolgunk, mint ezt összeszorozni a sebességgel (hossz-szal) és ebből megkapjuk a teljes sebességvektort.
Vector2 direction; // Vektor irány. Ez kötelezően egy EGYSÉG HOSSZ vektor
float speed; // Sebessége mértéke (nem vektor)
// ...
Vector2 velocity = direction * speed;
(Az angol velocity
kifejezést a sebességvektorra fogjuk most használni míg a speed
szót csak annak irány nélküli float
hosszára. Ez a nevezéktan eléggé bevett a grafikus programozásban.)
Ha a vektor irányát egy szögérték adja meg, akkor egy nagyon kicsit komplikáltabb a dolgunk, de erre lesz szükségünk a későbbiekben leggyakrabban, ezért tekintsük át.
float angleDeg; // Víszintessel bezárt szög FOKBAN
float speed; // Sebesség mértéke (nem vektor)
// ...
float angleRad = angleDeg * Mathf.Deg2Rad; // Átszámítás radiánba
float vx = speed * Mathf.Cos(angleRad); // Ugyanis a Mathf szöfüggvényei, mint a Cos
float vy = speed * Mathf.Sin(angleRad); // és a Sin csak radiánon vannak értelmezve
Vector2 velocity = new(vx, vy); // Sebességvektor összállt
Ha nem tiszta, miért használtuk a szinusz és koszinusz függvényeket, akkor erről a A derékszögű háromszög-ekkel foglalkozó fejezetben olvashatsz bővebben. A későbbiekben úgy tekintjük, hogy a fokok mindig radiánban értendőek.
Ha épp szétbontani szeretnénk egy sebességvektor, hosszra és szögre, az sem bonyolult:
Vector2 velocity ; // Sebességvektor
float angleDeg = Mathf.Atan(velocity.y / velocity.x);
float speed = velocity.magnitude;
A későbbiekben az alábbi jelöléseket és nevezéket fogjuk használni:
- : Sebesség (Kódban:
Vector2 velocity
) - : Sebességvektor vízszintes komponense (Kódban:
float vx
) - : Sebességvektor függőleges komponense (Kódban:
float vx
) - α: hajítás vízszintessel bezárt szöge (Kódban:
Vector2 angleRad
) - : Sebesség mértéke (vektor abszolútértéke, hossza,
magnitude
-ja) (Kódban:float speed
)
Ha a sebesség egy speciális pillanatra vonatkozik, akkor ezt egy megkülönböztető szimbólum fogja jelezni. Például a nulla mindig a kezdőponthoz fog kötődni: , , , ,
A fenti példákban sebességvektorokkal foglalkoztunk, ugyanis ezekről lesz szó a továbbiakban, de fontos kiemelni, hogy a fenti műveletek bármilyen vektoron értelmezettek. Például a gravitáció egy gyorsulás, ami másféle fizikai vektormennyiség. Jelen cikkben a gravitáció gyorsulásvektora mindig lefelé mutat, ahogy azt a valóságban megszokhattuk.
Egzakt és szimulált megoldás
Tegyük fel, hogy ismerjük egy elhajított kő kezdeti sebességvektorát. Ez már elég is nekünk, hogy kiszámítsuk a kő pozícióját minden időpillanatban. (Emlékezzünk a kezdő pozíció most nem kell, hisz azt az origóba tettük). Az a bizonyos “minden időpillanat” egy kicsikét nagy szám a számítógépünk számára. Ehhez végtelen időre és memóriára is szükség lenne. Nyugodtan lehetünk azonban gazdaságosabbak. És mondhatjuk azt, hogy csak minden századmásodpercben számolunk pozíciót mondjuk 5 másodpercen keresztül. Na ez már smafu egy modern gépnek.
Ahogy a A FixedUpdate és a gyorsuló mozgás fejezetben tárgyaltuk, ezt nem egy túl bonyolult dolog elvégezni, ha ismerjük a gyorsuló mozgás szimulálásának módját.
public void FixedUpdate()
{
transform.position += velocity * Time.fixedDeltaTime; // Új pozíció
velocity += (gravity * Vector2.down) * Time.fixedDeltaTime; // Gyorsulás lefelé
currentVelocity *= 1 - drag * Time.fixedDeltaTime; // Akár közekellenás
}
Ez a módszer viszont mivel a folytonos fizikai folyamatot felbontja apró időegységekre, egy idő után egyre pontatlanabb eredményt ad. Ez nem feltétlenül probléma. Az összes játékmotor fizikája is hasonlóan működik valós időben. Azonban ha tökéletes fizikailag megfelelő eredményt akarunk kapni, akkor felcsaphatjuk a Négyjegyű függvénytáblázatunkat, és megnézhetjük, hogy milyen képlet adja vissza egy elhajított test pozícióját időpillanatban. Ezt nevezzük egzakt megoldásnak. Amikor nem apró lépések sorozata hanem egy matematikailag pontos számítás vezet el a végeredményhez. És ezt fogjuk megvizsgálni a következő fejezetben. Azonban előtte beszéljük át a szimulált és egzakt megoldások előnyeit és hátrányait egymással szemben.
Mivel az egzakt megoldás csak egyetlen számítás, egy képlet behelyettesítés és nem kell apránként végig-menni akár többszáz, -ezer vagy -millió apró lépésen, ezért sokkal kevesebb processzorkapacitást igényel. Emellett ez adja a fizikailag pontos eredményt is míg a szimulált megoldás mindig pontatlan lesz, és ez a pontatlanság az eltelt idővel folyamatosan nő. Azért használunk legtöbb esetben apró lépésekre osztott, szimulált megoldást mégis, mert:
- Real Time szimulációnál, mint pl. videojátékok úgyis kis időközönként vissza kell jelezni a felhasználónak. Így is úgy is sok apró számítást végzünk. Nem spórolunk így semmi időt, hisz minden részeredményre úgyis szükségünk lesz.
- Játékok esetén a felhasználói interakcióból következően folyamatosan kiszámíthatatlan módon változik a rendszer.
- Egy egyszerű fizikai számítás nagyon hamar olyan bonyolulttá tud válni, hogy nem csak nehezen tudjuk matematikai képlettel leírni. Pl.: Ha a hajítás kiszámításába belevesszünk légellenállást is, akkor már csak integrálszámítással juthatunk el egzakt megoldáshoz.
- Sok esetben nem csak nehéz egzakt megoldást adni arra, hogy számoljuk ki egy rendszer későbbi állapotát, hanem lehetetlen a jelen matematikai tudásunk mellett. Pl.: Ha három egymás körül keringő gravitációs pont pozícióját szeretnénk kiszámolni valamennyi idő múlva, akkor meg vagyunk lőve. Erre nem létezik ismert pontos megoldás, csak szimulált. (Ez a három test probléma.)
Szóval a szimulált megoldás nem csak egyszerűbb, és praktikusabb a legtöbb esetben, de gyakran az egyetlen lehetőségünk is. Az apró pontatlanság, amit ez eredményez pedig általában nem a játékfejlesztők problémája, inkább a NASA mérnökeié, akik aszteroidák pályáját próbálják jósolni 100 évre előre. Mi könnyen tudunk élni vele.
Pozíció kiszámítása adott időpillanatra
Azonban egy egyszerű hajítás esetén létezik egyszerű egzakt megoldás. Ebben a fejezetben ezt tekintjük át:
Ha ismert a hajítási sebességvektor és szeretnénk megkapni a kezdőponttól való eltávolodás x és y koordinátáját akkor használhatjuk a következőket:
Először is a vízszintes eltávolodás a legkönnyebb, hisz az nem más, mint egy egyenes vonalú egyenletes mozgás a kezdeti sebességvektor vízszintes komponensével.
Az alábbi két képlet írható fel tehát:
Ha pedig a nem ismert önmagában, csak az szög és a , azaz a sebesség mértéke, akkor:
Függőleges esetben a képeltek kissé bonyolultabbak, hisz ott egyenletesen gyorsuló mozgás történik, ezért az annak megfelelő képeteket alkalmazzuk:
És végül mindez kóddal kifejezve:
public static Vector2 GetPositionAtTime
(Vector2 startVelocity, float gravity, float fullTime)
{
float x = startVelocity.x * fullTime;
float y = startVelocity.y * fullTime - 0.5f * gravity * fullTime * fullTime;
return new(x,y);
}
public static Vector2 GetPositionAtTime
(float speed, float startAngleRad, float gravity, float fullTime)
{
float vx = speed * Mathf.Cos(startAngleRad);
float vy = speed * Mathf.Sin(startAngleRad);
float x = vx * fullTime;
float y = vy * fullTime - 0.5f * gravity * fullTime * fullTime;
return new(x,y);
}
Sebesség kiszámítása adott időpillanatra
A képletek igen egyszerűek. Ne bonyolítsuk ezt túl! Vízszintes tengelyen a sebesség nem változik, függőlegesen pedig egyenletesen csökken a gravitációs gyorsulás mértékében.
public static Vector2 GetVelocityAtTime(
Vector2 startVelocity, float gravity, float fullTime) =>
new(startVelocity.x, startVelocity.y - gravity * fullTime);
Pálya minden pontjának kiszámítása egyszerre
A fentiek előre elvégezhetők bármilyen hosszú időre előre nézve is, ha mondjuk ki akarjuk rajzolni az előre jelzett pályát. Íme az én megoldásom, ahol egy listát töltök fel a pálya pontjaival:
(Azért használtam listát, amit paraméterben kap meg a függvény, mert így a felhasználó újra tudja használni ugyanazt a listát és ezáltal nem szükséges folyamatos memóriafoglalás a lekérdezéshez.)
Megoldás szimulált esetben:
public static void GetPathSimulated(
Vector2 startPoint, Vector2 startVelocity,
float gravity, float drag, float fullTime, float timeStep,
List<Ray> path) // Eredmény ebbe a listába kerül beírásra
{
if (timeStep <= 0) return;
path.Clear();
path.Add(new(startPoint, startVelocity));
Vector2 currentPoint = startPoint;
Vector2 currentVelocity = startVelocity;
float currentTime = 0;
for (int i = 0; currentTime < fullTime; i++)
{
float timeLeft = fullTime - currentTime;
if (timeLeft < timeStep)
timeStep = timeLeft;
currentPoint += currentVelocity * timeStep;
currentVelocity += gravity * timeStep * Vector2.down;
currentVelocity *= 1 - drag * timeStep;
currentTime += timeStep;
path.Add(new(currentPoint, currentVelocity));
}
}
És minden pontra egzakt megoldást adva:
public static void GetPathExact(
Vector2 startPoint, Vector2 startVelocity,
float gravity, float fullTime, float timeStep,
List<Vector2> path) // Eredmény ebbe a listába kerül beírásra
{
if (timeStep <= 0) return;
path.Clear();
path.Add(startPoint);
float currentTime = 0;
for (int i = 0; currentTime < fullTime; i++)
{
float timeLeft = fullTime - currentTime;
if (timeLeft < timeStep)
timeStep = timeLeft;
currentTime += timeStep;
Vector2 currentPoint =
GetPositionAtTime(startPoint, startVelocity, gravity, currentTime);
path.Add(currentPoint);
}
}
Hajítási idő kiszámítása a sebességvektor ismeretében
Legegyszerűbb megkapni a hajítási időt a vízszintes sebesség alapján, mivel ezen a tengelyen a mozgás egyenes vonalú egyenletes. Ehhez csak a vízszintes sebességkomponenst kell kiszámolni, ami ahogy korábban láttuk megkapható a teljes sebesség és a hajítási szög koszinuszának szorzataként.
public static float GetFullTime(
float distanceX, float startAngleRad, float startSpeed)
{
float vx = startSpeed * Mathf.Cos(startAngleRad);
return Mathf.Abs(distanceX) / vx;
}
Célzás
Trükkösebb dolgunk van, ha visszafelé kell gondolkodnunk. Azt tudjuk, hova szeretnénk eljuttatni a lövedéket, de nem ismerjük a kezdő sebességvektort, amivel el kell indítanunk az objektumot. Magyarul fogalmazva ezt hívjuk célzásnak.
Ha ismert a gravitációs gyorsulás mértéke és az elmozdulásvektor (a becélzott tárgy távolsága), akkor a végtelen lehetséges pálya és hozzá végtelen lehetséges kezdő sebességvektor létezik, ami eljuttatja a lövedéket a kívánt célba. Tehát még egy információra szükség lesz:
Vagy azt kell tudni, milyen szögben dobjuk el a tárgyat vagy pedig azt milyen sebességgel és ebből a másik, ismeretlen érték megkapható. Ezt úgy is mondhatjuk, a probléma már csak egy szabadsági fokkal rendelkezik.
Próbáld ki ezt az alábbi példán! Állítsd be a fix kezdősebességet vagy fix hajítási szöget, mozgasd a kezdő (piros) és célpontot (zöld), és figyeld meg hogy változik a másik szabad paraméter!
Mind ehhez a hajítás vagy ballisztikus pálya alábbi két egyenletét tudjuk felhasználni:
Azt, hogy hogyan kaptuk meg a fenti képleteket itt nem tárgyaljuk.
Célzás: Sebesség kiszámítása a vízszintessel bezárt szög ismeretében
Az első képlet azt adja meg, hogy egy megadott szögben és megadott kezdősebességgel elhajított test, ismert gravitációs gyorsulás mellett adott vízszintes pozícióhoz milyen magasságérték tartozik. Az egyenlet átrendezve -ra a következőképp fest:
Ez természetesen csak akkor ad eredményt, ha a gyökvonás alatti rész nem negatív. Ellenkező esetben nem létezik semmilyen kezdősebesség, ami képes arra, hogy el tudjuk találni a becélzott pontot. Ez akkor lehetséges, ha a célpont az kezdő ponttól szögből húzott egyenes felett helyezkedik el. (Pl.: Ha 45 fokos szögben kell dobnunk, akkor nem fogjuk eltalálni a fejünk feletti célpontot bármilyen erős is a dobásunk.)
Kiszámításához ezért kódban Try
szerkezetet használtam. Ekkor a függvény visszatérése egy bool
, és azt adja meg, hogy sikerült-e megtalálni a keresett értéket, amit sikeres esetben egy kimenő (out
) paraméteren keresztül kap meg a metódus hívója.
(Az out
paraméterről bővebben: Függvények több kimenő adattal)
Az alábbi függvény csupán a fenti képlet leírása C# programnyelven:
public static bool TryGetStartSpeed(
Vector2 distanceVector,
float gravity,
float startAngleRad,
out float startSpeed) // Kimenő paraméter
{
float dy = distanceVector.y;
float dx = distanceVector.x;
float cosAlpha = Mathf.Cos(startAngleRad);
float tanAlpha = Mathf.Tan(startAngleRad);
float root = gravity * dx * dx /
(2 * cosAlpha * cosAlpha * (dx * tanAlpha - dy));
if (root < 0)
{
startSpeed = 0;
return false; // Nincs eredmény
}
startSpeed = Mathf.Sqrt(root);
return root > 0;
}
Ebből sebességvektort a következő metódus adhat:
public static bool TryGetStartVelocity(
Vector2 distanceVector,
float gravity,
float startAngleRad,
out Vector2 startVelocity) // Kimenő paraméter
{
if (!TryGetStartSpeed(distanceVector, gravity,
startAngleRad, out float startSpeed))
{
startVelocity = Vector2.zero;
return false; // Nincs eredmény
}
float v0x = startSpeed * Mathf.Cos(startAngleRad);
float v0y = startSpeed * Mathf.Sin(startAngleRad);
startVelocity= new(v0x, v0y);
return true;
}
Célzás: Vízszintessel bezárt szög kiszámítása a sebesség ismeretében
Ekkor a másik egyenlethez kell folyamodnunk, amiben ráismerhetnek a matematikát kedvelők a másodfokú egyenlet megoldóképletére:
A fenti egyenletnek ezért lehet nulla vagy két megoldása is: Ha a gyökvonás alatti rész negatív akkor természetesen nincs egyetlen fizikailag valós megoldás sem, egyébként viszont kettő is. Ez igaz a valódi hajításra nézve is. Gondoljuk végig: Ha ismert a hajítás sebessége, akkor két szög is megadhatja azt a pályát, ami eltalálja a célpontot, ám ha a célpont túl távol és magasan van, akkor semmilyen szögben hajítva nem tudjuk eltalálni azt. Mivel nem biztos, hogy kapunk eredményt, kódban most is Try
szerkezetet használunk:
Ha a képlet plusz-mínusz (±) műveleténél a hozzáadást választjuk, akkor egy magasabb, ha a kivonást, akkor egy alacsonyabb pályát kapunk. Ennek kiválasztásáért az alábbi kódban egy bool
paraméter felel: low
(Alacsonyabb pályát akarjuk-e megkapni?)
public static bool TryGetStartAngle(
Vector2 distanceVector,
float gravity,
float startSpeed,
bool low,
out float startAngleRad) // Kimenő paraméter
{
float dy = distanceVector.y;
float dx = distanceVector.x;
float v2 = startSpeed * startSpeed;
float v4 = v2 * v2;
float x2 = dx * dx;
float underRoot = v4 - gravity * (gravity * x2 + 2 * dy * v2);
if (underRoot < 0)
{
startAngleRad = 0;
return false; // Nincs eredmény
}
float root = Mathf.Sqrt(underRoot);
if (low)
startAngleRad = Mathf.Atan((v2 - root) / (gravity * dx));
else
startAngleRad = Mathf.Atan((v2 + root) / (gravity * dx));
return true;
}
Ebből sebességvektort a következő metódus ad:
public static bool TryGetStartVelocity(
Vector2 distanceVector,
float gravity,
float startSpeed,
bool low,
out Vector2 startVelocity) // Kimenő paraméter
{
if (!TryGetStartAngle(distanceVector, gravity,
startSpeed, low, out float startAngleRad))
{
startVelocity = Vector2.zero;
return false; // Nincs eredmény
}
float v0x = startSpeed * Mathf.Cos(startAngleRad);
float v0y = startSpeed * Mathf.Sin(startAngleRad);
startVelocity= new(v0x, v0y);
return true;
}
Görbe legfelső pontja
Először is ki kell számolnunk mennyi idő kell addig, hogy a függőleges sebességünk nulla legyen adott gravitációs gyorsulás mellett:
Ebből a pozíció és sebesség a korábbiak alapján számolható.
public static void GetMaxPoint(
Vector2 startVelocity, float gravity,
out Vector2 positionAtTop, out Vector2 velocityAtTop, out float timeAtTop)
{
timeAtTop = startVelocity.y / gravity;
float height = startVelocity.y * timeAtTop -
0.5f * gravity * timeAtTop * timeAtTop;
positionAtTop = new(startVelocity.x * timeAtTop, height);
velocityAtTop = new(startVelocity.x, 0);
}
Átalakítás 3D-be
Korábban csak két dimanzióban dolgoztunk, ezt az egyszerűség kedvéért tettük így. Azonban a 3D sem sokkal bonyolultabb. Ott is vízszintes és függőleges komponensekről beszélhetünk, és a hajítás ugyanúgy működik. Egyedül annyi a különbség, hogy a vízszintes komponens két tengelyen oszlik el. Ez a Unity-ben, ahogy más egyéb balkezes koordinátarendszerekben is az x, és a z tengely.
Tehát nincs más dolgunk, ha 3D-ben szeretnénk elvégezni a műveleteket, mint
- Először 2D-be alakítjuk a bemeneti 3D vektorokat
- Elvégezzük a ballisztikus számításokat 2D vektorokon
- Visszaalakítjuk az eredményt 3D-be:
Az egyes és a hármas pontra én sorban az alábbi FlattenTo2D
és az ExtendTo3D
függvényeket készítettem el. A kettes ponttal foglakozott a lecke eddigi része.
// 2D-be alakítás
public static Vector2 FlattenTo2D(Vector3 v3) =>
new (v3.GetHorizontalSize(), v3.y);
// Vízszintes komponens hossza
public static float GetHorizontalSize(Vector3 v) =>
new Vector2(v.x, v.z).magnitude;
// Visszalakítás 3D-be
public static Vector3 ExtendTo3D(this Vector2 v2, Vector3 horizontalDirection)
{
Vector2 distanceVectorH = new(horizontalDirection.x, horizontalDirection.z);
Vector2 directionH = distanceVectorH.normalized;
return new(directionH.x * v2.x, v2.y, directionH.y * v2.x);
}
// 3D vaktor vízszintessel bezárt szögének lekérdezése
public static float GetVerticalAngleRad(this Vector3 velocity) =>
Mathf.Atan(velocity.y / velocity.GetHorizontalSize());
Teljes kód
A fentiek alapján írtam egy közel 500 soros osztályt, ami tartalmaz a ballisztikus pályák számításához szükséges 46 matematikai segédfüggvényt. Alább tölthető le: