játékfejlesztés.hu
FórumGarázsprojectekCikkekSegédletekJf.hu versenyekKapcsolatokEgyebek
Legaktívabb fórumozók:
Asylum:    5444
FZoli:    4892
Kuz:    4455
gaborlabor:    4449
kicsy:    4304
TPG:    3402
monostoria:    3284
DMG:    3172
HomeGnome:    2919
Matzi:    2519

Pretender:    2498
szeki:    2440
Seeting:    2306
Geri:    2188
Orphy:    1893
Joga:    1791
Bacce:    1783
MaNiAc:    1735
ddbwo:    1625
syam:    1491
Matematikai alapok számítógépes grafikához I. 2014.02.10 00:17


Előkészületek

A tanultakat C++ nyelven fogjuk implementálni, amihez MinGW g++ fordítót használunk. Konkrét verziószámot nem tudok mondani, de ha letöltitek azt a csomagot, amit a CodeBlocks IDE-hez ajánlanak, akkor nem lesz probléma. Mivel ez a segédlet főként matematikai témájú, ezért a konkrét megvalósítás miértjeibe nem kívánok mélyen belemenni. Ebből adódóan a C++ nyelv legalább alapszintű ismerete elvárt az olvasótól. A cikksorozat “OpenGL friendly”, ennek ellenére az itt szerzet tudás DirectX esetében is felhasználható. Akkor vágjunk is bele!

1. Néhány hasznos információ

1.1 Mi az a Pi?

A pi nem csak a számítógépes grafikában, de a matematika számos egyéb területén is gyakran használt állandó. Ez egy rendkívül hosszú irracionális szám, melyet úgy kapunk meg, hogy a kör kerületét elosztjuk annak átmérőjével. Mivel az eredmény állandó, ezért definiálhatjuk ezt a számot egy valós konstansként, vagy egyszerűen csak C++ makrók segítségével:

Kód:
#define PI 3.14159265


Nekem az a tapasztalatom, hogy ennél nagyobb pontosságra általában nincs szükség.


1.2 (Közbezárt) Szögek mértékegységei

Két mértékegységet fogunk használni szögek mérésére: a radiánt és a fokot. A fok, mint mértékegység nem hiszem, hogy különösebb bemutatásra szorul bárkinek is, hiszen ez a hétköznapokban is elterjedt eszköz. A félkör 180, teljes kör pedig 360 fok. Ezzel szemben a radián a matematikában gyakrabban használt mérték, és a mért szöghöz tartozó átmérő és körív hányadosát jelenti. A félkör pi radián, míg a teljes kör 2*pi radián.

A két mérték közötti átváltások a következők:

Kód:
FOK = RAD * (180/PI)
RAD = FOK * (PI/180)


Implementálva:

Kód:
template <class T>
inline T toDegree(T rad) { return rad*((T)180.0/(T)PI); }

template <class T>
inline T toRadian(T angle) { return angle*((T)PI/(T)180.0); }



1.3 Az abszolútérték-függvény

Az abszolútérték függvény egy olyan függvény, ami minden számhoz hozzárendeli a szám abszolút értékét. Ha a szám értéke nagyobb vagy egyenlő, mint 0, akkor ez az érték maga a szám. Minden más esetben pedig a szám ellentettjét produkálja.

Implementálva:

Kód:
template <class T>
inline T abs(T a) { return a > T(0) ? a : -a; }


Megjegyzés: Elegendő vizsgálni a bemenetet a ‘>’ relációval is, ugyanis nulla ellentettje önmaga.


1.4 A cikksorozatban használt műveleti tulajdonságok fogalma

Két műveleti tulajdonságról fogunk említést tenni ebben a cikksorozatban, a kommutativitásról és az asszociativitásról.

Nézzük először a kommutativitást, más néven felcserélhetőséget:

Legyen T egy tetszőleges számhalmaz,
Legyen ∘ egy tetszőleges művelet, amely értelmezett T-n.

Ha ∀a,b∈T : a∘b = b∘a, akkor azt mondjuk, hogy a ∘ művelet kommutatív.

Vagyis, ha az adott művelet operandusai felcserélhetőek, akkor a művelet kommutatív. Példa: A természetes számok halmazán értelmezett szorzás művelet kommutatív, ugyanis: legyen mondjuk a:=3, és b:=5. Ekkor 3*5=15 és 5*3=15.

Most vizsgáljunk meg egy másik fontos tulajdonságot, az asszociativitást:

Legyen T egy tetszőleges számhalmaz,
Legyen ∘ egy tetszőleges művelet, amely értelmezett T-n.

Ha ∀a,b,c∈T : (a∘b)∘c = a∘(b∘c), akkor azt mondjuk, hogy a ∘ művelet asszociatív.

Vagyis, az adott művelet asszociatív, ha szabadon zárójelezhető. Példa: Az egész számok halmazán értelmezett összeadás művelet asszociatív, ugyanis: legyen mondjuk a:=5, b:=-12 és c:=7. Ekkor (5+ (-12)) + 7 = 0 és 5 + (-12+7) = 0.



2. Vektorok


2.1 Mik a vektorok?

A vektorok rendkívül hasznos eszközök a számítógépes játékfejlesztésben és grafikában. Egy vektor nem más, mint egy irányított szakasz, melynek nagysága is iránya van. Felhasználási módjuk igen gazdag, pl. vektorok segítségével leírhatunk egy tetszőleges pontot a térben (vagy síkban), meghatározhatunk velük felületeket, fények beesését, stb. Vektorokat n dimenzióban értelmezhetünk, ha n∈ℕ : n ≥ 2.

Mi most háromdimenziós vektorokkal fogunk foglalkozni, ennek megfelelően definiáljuk a vektorokat tetszőleges valós, és rendezett számhármasoknak:

Kód:
template <class T>
class Vec3
{
public:

// [ide jön a vektor műveleteinek és tulajdonságainak implementációja]

protected:
    T v[3];
};


A fentiek szerint a mi vektorunk 3 értékkel rendelkezik, melyeket a Descartes-féle koordináta rendszer tengelyeinek megfelelően x, y és z-nek nevezünk. A fenti implementációban x == v[0], y == v[1] és z == v[2].

Ezek az értékek tehát a koordináta rendszerben, az origótól (0,0,0) számított távolságot jelentik az egyes tengelyek mentén. Az alábbiakban definiálni és implementálni fogjuk a vektorokon értelmezett műveleteket és tulajdonságokat.


2.2 Implementációs séma

Mivel a vektorokat osztályként definiáljuk és kezeljük a saját rendszerünkben, ezért illik betartani az általános implementációs előírásokat. Ennek megfelelően a Vec3 osztály többféle konstruktorral is rendelkezik (részletek a kommentekben):

Kód:
/// KONSTRUKTOROK
Vec3() {v[0]=0; v[1]=0; v[2]=0;}                                                // Default konstruktor
Vec3(T x, T y=0, T z=0) {v[0]=x; v[1]=y; v[2]=z;}                      // Init konstruktor
Vec3(const Vec3& other) {v[0]=other.v[0]; v[1]=other.v[1]; v[2]=other.v[2];}    // Copy konstruktor

// Inicializálás tömbbel (vagy init list C++11 esetében)
Vec3(const T arr[]) {v[0]=arr[0]; v[1]=arr[1]; v[2]=arr[2];}

// Megjegyzés: memcpy(), memset() használata véleményem szerint itt annyira nem indokolt.


Itt pedig az adattagok kényelmes hozzáférését biztosítjuk:

Kód:
/// ELÉRÉS
// Lekérdezések
T x() const {return v[0];}
T y() const {return v[1];}
T z() const {return v[2];}
T operator () (int i) const {return v[i];}
T operator [] (int i) const {return v[i];}

// Értékadások
T& x() {return v[0];}
T& y() {return v[1];}
T& z() {return v[2];}
T& operator() (int i) {return v[i];}
T& operator[] (int i) {return v[i];}
void operator= (const T arr[]) {v[0]=arr[0]; v[1]=arr[1]; v[2]=arr[2];}



2.3 Vektorműveletek

A vektorok rendkívül érdekes és hasznos műveletekkel rendelkeznek. Kezdjük az egyszerűbbektől kezdve az összetettebbek iránya felé haladva.

2.3.1 Értékadás és ekvivalencia

Ha egy vektornak értékül adunk egy másikat, a tartalmazott koordináták rendszerint átveszik a másik vektor koordinátáinak értékét:

Kód:
    // Értékadás
    Vec3<T>& operator= (const Vec3<T>& other)
    {
        if (this != &other) {v[0]=other.v[0]; v[1]=other.v[1]; v[2]=other.v[2];}
        return *this;
    }


Két vektor pedig akkor egyenlő, ha koordinátáik rendszerint megegyeznek:

Kód:
    // Ekvivalencia
    inline bool operator== (const Vec3& other) const
    {return v[0]==other.v[0] && v[1]==other.v[1] && v[2]==other.v[2];}



2.3.2 Általános műveletek

Általános műveletek alatt itt az összeadást, kivonást és a negálást értjük. Az összeadás és kivonás kommutatív műveletek, és úgy hajthatóak végre, ha a két vektor koordinátáit rendszerint összeadjuk, vagy kivonjuk egymásból:

Legyen a,b∈ℝł. a:=(x1, y1, z1) és b:=(x2, y2, z2).
Ekkor a±b=(x1±x2, y1±y2, z1±z2).

A műveletek eredménye egy új vektor lesz. Implementálva:

Kód:
    // Összeadás
    inline void operator+= (const Vec3& other)
    {v[0]+=other.v[0]; v[1]+=other.v[1]; v[2]+=other.v[2];}
    inline Vec3 operator+ (const Vec3& other) const
    {return Vec3(v[0]+other.v[0], v[1]+other.v[1], v[2]+other.v[2]);}

    // Kivonás
    inline void operator-= (const Vec3& other)
    {v[0]-=other.v[0]; v[1]-=other.v[1]; v[2]-=other.v[2];}
    inline Vec3 operator- (const Vec3& other) const
    {return Vec3(v[0]-other.v[0], v[1]-other.v[1], v[2]-other.v[2]);}


Ha egy vektort negálunk, akkor minden koordinátáját negáljuk:

Kód:
    // Negálás
    inline Vec3 operator- () const
    {return Vec3(-v[0], -v[1], -v[2]);}



2.3.3 Szorzás skalárral

Matematikai értelemben skalár alatt egy általános, egyedül álló számra gondolunk. Vektorokat összeszorozhatunk egy tetszőleges skalárral és az eredmény egy olyan vektor lesz, melynek minden koordinátája egyenként meg van szorozva az adott skalárral:

Legyen a∈ℝł és c∈ℝ. a:=(x, y, z).
Ekkor c*a=(c*x, c*y, c*z).

Ez a művelet is kommutatív, de mivel különböző típusú operandusokról beszélünk, mindkét oldalról implementálnunk kell:

Kód:
/// OSZTÁLYON BELÜLI IMPLEMENTÁCIÓ (jobb oldalról történő szorzás)

    // Szorzás skalárral
    inline void operator*= (const T& a)
    {v[0]*=a; v[1]*=a; v[2]*=a;}
    inline Vec3 operator* (const T& a) const
    {return Vec3(v[0]*a, v[1]*a, v[2]*a);}

    // Osztás skalárral (a teljesség igényével)
    inline void operator/= (const T& a)
    {v[0]/=a; v[1]/=a; v[2]/=a;}
    inline Vec3 operator/ (const T& a) const
    {return Vec3(v[0]/a, v[1]/a, v[2]/a);}

/// OSZTÁLYON KÍVÜLI IMPLEMENTÁCIÓ (bal oldalról történő szorzás)

// Szorzás skalárral bal oldalról
template <class T>
inline Vec3<T> operator* (const T& a, const Vec3<T>& v)
{return Vec3<T>(v[0]*a, v[1]*a, v[2]*a);}


Ha egy vektort skalárral szorzunk, akkor megnyújtjuk az adott vektort. Ha például kétszeresére szeretnénk növelni egy vektor hosszát (de az irányát változatlanul akarjuk hagyni), akkor elég megszorozni a vektort 2-vel. Ha a vektor felét akarjuk venni, akkor a szorzó 0.5 lesz.



2.3.4 Dot product

Az egyik érdekesebb művelet vektorok esetében a dot product, vagy más néven skaláris szorzat. Ennek a műveletnek mindkét operandusa egy-egy vektor, eredménye viszont egy skalár. Ez a skalár nem más, mint a két vektor által közbezárt szög koszinusza megszorozva a vektorok hosszának szorzatával. Ebből adódik, hogy ha a két vektor egység hosszúságú, akkor a vektorok által közbezárt szög a skaláris szorzat arkuszkoszinusza (mivel arccos = cos-1), de erről majd később. A skaláris szorzat a következőképpen határozható meg:

Legyen a,b∈ℝł. a:=(x1, y1, z1) és b:=(x2, y2, z2).
Ekkor a·b = x1*x2 + y1*y2 + z1*z2.

Implementálva:

Kód:
/// OSZTÁLYON KÍVÜL

// Dot product
template <class T>
inline T dot(const Vec3<T>& v1, const Vec3<T>& v2)
{return v1[0]*v2[0] + v1[1]*v2[1] + v1[2]*v2[2];}



2.3.5 Cross product

Egy másféle szorzás két vektor között a cross product, vagy más néven a vektoriális szorzat. A művelet mindkét operandusa vektor, és az eredmény pedig egy olyan vektor lesz, amely merőleges a művelet eredeti vektorai által meghatározott síkra. Ennek a tulajdonságnak rengeteg előnye van a számítógépes grafikában, például meg tudjuk vele határozni a felületek normál-vektorát, amelynek hatalmas szerepe van az árnyalásban. Fontos megjegyezni, hogy a vektoriális szorzat nem kommutatív, tehát nem ugyanazt kapjuk eredményül, ha felcseréljük a művelet operandusait: a,b∈ℝ3. a×b ≠ b×a. A művelet a következőképpen számolható ki:

Legyen a,b∈ℝł. a:=(x1, y1, z1) és b:=(x2, y2, z2).
Ekkor a×b=(y1*z2 - z1*y2, z1*x2 - x1*z2, x1*y2 - y1*x2).

Implementálva:

Kód:
/// OSZÁLYON KÍVÜL

// Cross product
template <class T>
inline Vec3<T> cross(const Vec3<T>& a, const Vec3<T>& b)
{
    return Vec3<T>(a.y()*b.z() - a.z()*b.y(),
                a.z()*b.x() - a.x()*b.z(),
                a.x()*b.y() - a.y()*b.x());
}



2.4 Egyéb műveletek és tulajdonságok

2.4.1 A vektor hossza

Mint említettük, egy vektornak iránya és hossza van. Egy vektor hosszát a következő módon kapjuk meg:

Legyen a∈ℝł, a:=(x, y, z).
Ekkor |a|=√m, ahol m=(x*x + y*y + z*z).

Implementálva:

Kód:
    // Vektor hossza
    inline T length() const
    {return std::sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);}



2.4.2 Vektor normalizálása

A normalizált vektor azzal a tulajdonsággal rendelkezik, hogy a hossza pontosan 1. Bármely vektort ilyen tulajdonságúra hozhatunk, ha beszorozzuk a hosszának inverzével (vagy elosztjuk a vektort a hosszúságával):

Kód:
/// OSZTÁLYON KÍVÜL

// Normalizált vektor
template <class T>
inline Vec3<T> normalized(const Vec3<T>& v)
{return Vec3<T>(v/v.length());}



2.4.3 Két vertex távolsága

Utaljunk most vertexek alatt olyan vektorokra, melyeket csak pozíciók meghatározására használunk. Két vertex távolsága kiszámítható az alábbi módszerrel:

Legyen a,b∈ℝł, a:=(x1, y1, z1) és b:=(x2, y2, z2).
Ekkor |a-b|=√m, ahol m=(x2-x1)^2 + (y2-y1)^2 + (z2-z1)^2.

Implementálva:

Kód:
/// OSZTÁLYON KÍVÜL

// Két vertex (vektorokkal prezentálva) távolsága
template <class T>
inline T distance(const Vec3<T>& a, const Vec3<T>& b)
{
    T x = b[0]-a[0];
    T y = b[1]-a[1];
    T z = b[2]-a[2];
    return std::sqrt(x*x + y*y + z*z);
}
[code]

[i]Megjegyzés: A szemfülesebbek észrevehetik, hogy két vektor távolsága a különbségvektor hossza.[/i]


[i]2.4.4 Két vektor által közbezárt szög[/i]

Korábban beszéltünk róla, hogy a skaláris szorzattal ki lehet számolni két vektor által közbezárt szöget. A művelethez szükségünk van a vektorok egységhosszú változatára, majd ezen vektorok skaláris szorzatának arkuszkoszinusza lesz a végeredmény, melyet radiánban kapunk meg.

Implementálva:

[code]
/// OSZTÁLYON KÍVÜL

// Két vektor által közbezárt szög
template <class T>
inline T angle(const Vec3<T>& a, const Vec3<T>& b)
{return std::acos(dot(normalized(a), normalized(b)));}



2.4.5 Visszaverődés

Arra is létezik eszköz, hogy egy beeső vektor visszaverődését kiszámoljuk egy adott felületről. Ehhez magára a vektorra és a felületet meghatározó normálvektorra van szükségünk. A művelet előfeltétele, hogy mindkét érték normalizálva legyen. Ezt a biztonság kedvéért a függvényen belül elvégezzük. Ezt a függvényt általában shaderekben szokták használni, mi csak a teljesség igényéért implementáljuk CPU oldalon:

Legyen x,n∈ℝł, ahol x a beeső vektor, n pedig a felületre merőleges normálvektor.
Ekkor r∈ℝł a felület által visszavert vektor és r=x-2*n*(x·n).

Implementálva:

Kód:
/// OSZTÁLYON KÍVÜL

// Visszaverődés
// R_r = R_in - 2N(R_in.N) egyenlőséget használva
template <class T>
inline Vec3<T> reflection(const Vec3<T>& in, const Vec3<T>& surface_normal)
{
    Vec3<T> N = normalized(surface_normal);
    Vec3<T> R = normalized(in);
    return (R - ((T)2*N) * R.dot(N));
}




Utószó

Remélem ezzel a cikkel sikerült tisztázni, hogy mik a vektorok és milyen műveleteket értelmezünk velük. A cikksorozat következő fejezetében vélhetőleg mátrixokkal fogunk foglalkozni, ami egy szintén érdekes és hasznos témakör. Ezen cikkben részletezett vektorosztály teljes forrása pedig itt érhető el.

Copyright
Cikk: Creative Commons - Nevezd meg! Ne módosítsd!
Forrás: Free

Seeting
mrpowhs@gmail.com

Értékelés: 5.50

Új hozzászólás
hetievela          2015.01.30 09:56
First, the absence of fat in the stomach creates a better appearance and this makes people look better, but more importantly feel better.

For more information please visit how-wiki.com

Secondly a flat belly indicates that people are healthy and take care of your body and the third with a bulging belly is not fashionable for people who need to stand out from the crowd wants to get rid of stomach fat and enjoy an impressive figure . There are many misconceptions about the best way to lose belly fat. http://www.healthcaresup.com/xtreme-muscle-pro/
Seeting          2014.02.11 11:48
Rendben.
HomeGnome          2014.02.11 11:44
A blogszemlére egy félreértés miatt nem került ki (még1x bocsi!), de szerintem nem kell innen törölni, jó kis cikk ez, és itt jobban meg is marad, mint a blogszemlén.
Seeting          2014.02.11 11:32
Ezt most már törölhetitek, csak azért küldtem be, mert azt hittem nem kerül ki a blogom a szemlére, de HG már kitette. Azóta már egyébként is frissült a cikk. Köszönöm !