Geometria (Rivin päävektori vs. sarakkeen päävektori)

Geometria

Aiemmin tällä oppitunnilla olemme selittäneet, että vektorit (tai pisteet) voidaan kirjoittaa matriisina (yksi rivi, kolme saraketta). Huomaa kuitenkin, että olisimme voineet kirjoittaa ne myös matriisina (kolme riviä, yksi sarake). Teknisesti nämä kaksi tapaa ilmaista pisteet ja vektorit matriisina ovat täysin päteviä, ja toisen tavan valitseminen on vain konventiokysymys.

Vektori kirjoitettuna matriisina: \( V=\begin{bmatrix}x & y & z\end{bmatrix}\)

Vektori kirjoitettuna matriisina: \(V=\begin{bmatrix}x\\\y\z\end{bmatrix}\)

Ensimmäisessä esimerkissä ( matriisi) olemme ilmaisseet vektorimme tai pisteemme niin sanotussa rivi-suuruusjärjestyksessä: vektori (tai piste) on kirjoitettu kolmen luvun rivinä. Toisessa esimerkissä sanomme, että pisteet tai vektorit on kirjoitettu sarake-duuri järjestyksessä: kirjoitamme vektorin tai pisteen kolme koordinaattia pystysuoraan, sarakkeena.

Muista, että ilmaisemme pisteet ja vektorit matriiseina kertoaksemme ne muunnosmatriiseilla (yksinkertaisuuden vuoksi työskentelemme pikemminkin matriisien kanssa). Olemme myös oppineet, että voimme kertoa matriisit vain silloin, kun vasemman matriisin sarakkeiden lukumäärä ja oikean matriisin rivien lukumäärä ovat samat. Toisin sanoen matriisit ja voidaan kertoa keskenään, mutta matriisit ja eivät. Huomaa, että jos kirjoitamme vektorin matriisina, voimme kertoa sen matriisilla (olettaen, että tämä matriisi on kertolaskun oikealla sisäpuolella), mutta jos kirjoitamme tämän vektorin matriisina, emme voi kertoa sitä matriisilla. Tätä havainnollistetaan seuraavissa esimerkeissä. Kertomiseen osallistuvien matriisien sisäiset ulottuvuudet (3 ja 3) ovat samat (vihreällä), joten tämä kertolasku on pätevä (ja tuloksena on muunnettu piste kirjoitettuna matriisina):

$$* = \begin{bmatrix}x & y & z\end{bmatrix}x & z\end{bmatrix} * \begin{bmatrix}c_{00}&c_{01}&{c_{02}}\\c_{10}&c_{11}&{c_{12}}\\c_{20}&c_{21}&{c_{22}}\\\end{bmatrix} =\begin{bmatrix}x’&y’&z’\end{bmatrix}$$

The kertolaskuun osallistuvien matriisien sisäiset ulottuvuudet (1 ja 3) eivät ole samat (punaisella), joten tämä kertolasku ei ole mahdollinen:

$$* \rightarrow \begin{bmatrix}x\\\ y\\z\end{bmatrix} * \begin{bmatrix} c_{00}&c_{01}&{c_{02}}\\ c_{10}&c_{11}&{c_{12}}\\ c_{20}&c_{21}&{c_{22}}\\\ \end{bmatrix}$$

Mitä siis tehdään? Ratkaisu tähän ongelmaan ei ole vektorin tai pisteen kertominen matriisilla, vaan matriisin M kertominen vektorilla V. Toisin sanoen siirretään piste tai vektori kertolaskun sisällä oikealle:

$$* \rightarrow \begin{bmatrix} c_{00}&c_{01}&{c_{02}}\\ c_{10}&c_{11}&{c_{12}}\\ c_{20}&c_{21}&{c_{22}}\\ \end{bmatrix} * \begin{bmatrix}x\\\y\\z\end{bmatrix} = \begin{bmatrix}x’\\\y’\\\z’\end{bmatrix}$$

Huomaa, että tämän operaation tulos on muunnettu piste kirjoitettuna matriisin muotoon. Saamme siis aluksi pisteen ja lopuksi muunnetun pisteen, jonka haluamme. Ongelma ratkaistu. Yhteenvetona voidaan todeta, että kun tavan mukaan päätämme ilmaista vektorit tai pisteet rivi-suuruusjärjestyksessä (), meidän on laitettava piste kertolaskun vasemmalle puolelle ja piste oikealle puolelle kertolaskun merkin sisälle. Tätä kutsutaan matematiikassa vasemmanpuoleiseksi tai esikertoimeksi. Jos päätät sen sijaan kirjoittaa vektorit sarake-duuri -järjestyksessä (), matriisin on oltava kertolaskun vasemmalla puolella ja vektorin tai pisteen oikealla puolella. Tätä kutsutaan oikeaksi tai jälkikertolaskuksi.

Meidän on oltava tarkkana siitä, miten näitä termejä todellisuudessa käytetään. Esimerkiksi Maya-dokumentaatiossa sanotaan, että ”matriisit ovat Mayassa post-multiplikoituja. Esimerkiksi, jos haluat muuttaa pisteen P objektiavaruudesta maailma-avaruuteen (P’), sinun täytyy jälkikerrata worldMatrixilla. (P’ = P x WM)”, mikä on hämmentävää, koska kyseessä on itse asiassa pre-multiplikaatio, mutta tässä tapauksessa puhutaan matriisin sijainnista pisteeseen nähden. Tämä on itse asiassa terminologian virheellistä käyttöä. Olisi pitänyt kirjoittaa, että Mayassa pisteet ja vektorit ilmaistaan rivin päävektoreina ja että ne siis esikerrataan (eli piste tai vektori esiintyy kertolaskussa ennen matriisia).

Seuraavassa taulukossa on yhteenveto näiden kahden konvention eroista (missä P, V ja M tarkoittavat vastaavasti pistettä, vektoria ja matriisia).

Rivi-suuruusjärjestys

\(P/V=\begin{bmatrix}x & y & z\end{bmatrix}\)

Vasemmalla tai pre-kertolasku

P/V * M

Sarake-pääjärjestys

\(P/V=\begin{bmatrix}x \\\ y \\\ z\end{bmatrix}\)

Oikea tai post-kertolasku

M * P/V

Nyt kun olemme oppineet näistä kahdesta konventiosta, saatat kysyä ”eikö siinä ole kyse vain asioiden kirjoittamisesta paperille?”.”. Tiedämme, miten lasketaan kahden matriisin A ja B tulo: kerrotaan jokainen A:n nykyisellä rivillä oleva kerroin B:n nykyisessä sarakkeessa olevilla vastaavilla elementeillä ja lasketaan tulos yhteen. Sovelletaan tätä kaavaa käyttäen kahta konventiota ja verrataan tuloksia:

Rivin suuruusjärjestys

$${ \begin{bmatrix}x & y & z\end{bmatrix}x & y & z\end{bmatrix } * \begin{bmatrix}a & b & c \\\d & e & f \\\g & h & i\end{bmatrix} }$$

$${ \begin{array}{l}x’ = x * a + y * d + z * g\\\\y’ = x * b + y * e + z * h\\\z’ = x * c + y * f + z * i\end{array} }$$

Sarakkeen pääjärjestys

$$${ \begin{bmatrix} a & b & c \\\d & e & e & f \\\g & h & i \end{bmatrix} * \begin{bmatrix}x\\y\\z\end{bmatrix} }$$

$${\begin{array}{l}x’ = a * x + b * y + c * z\\\y’ = d * x + e * y + f * z\\\z’ = g * x + h * y + i * z\end{array} }$$

Pisteen tai vektorin kertomisen matriisilla pitäisi antaa sama tulos riippumatta siitä, käytämmekö rivi- vai sarakkeellista järjestystä. Jos käytät 3D-sovellusta pisteen kiertämiseen tietyllä kulmalla z-akselin ympäri, odotat pisteen olevan tietyssä asennossa kiertämisen jälkeen riippumatta siitä, mitä sisäistä konventiota kehittäjä on käyttänyt pisteiden ja vektoreiden esittämiseen. Kuten yllä olevasta taulukosta kuitenkin näkyy, rivin pää- ja sarakkeen pääpisteen (tai vektorin) kertominen samalla matriisilla ei selvästikään antaisi samaa tulosta. Jotta pääsisimme takaisin jaloillemme, meidän pitäisi itse asiassa transponoida sarake-duuri-kertolaskussa käytetty matriisi varmistaaksemme, että x’, y’ ja z’ ovat samat (jos sinun täytyy muistaa, mitä matriisin transponointi on, katso luku Matriisioperaatiot). Tässä on, mitä saamme:

Rivi-duurijärjestys

$${ \begin{bmatrix}x & y & z\end{bmatrix} x & z\end{bmatrix} * \begin{bmatrix}a & b & c \\\d & e & f \\\g & h & i\end{bmatrix} }$$

$${\begin{array}{l}x’ = x * a + y * d + z * g\\\\y’ = x * b + y * e + z * h\\\z’ = x * c + y * f + z * i\end{array} }$$

Sarakkeen pääjärjestys

$$${ \begin{bmatrix} a & d & g \\\b & e & h \\\c & f & i\end{bmatrix} * \begin{bmatrix}x\\y\\z\end{bmatrix} }$$

$${\begin{array}{l}x’ = a * x + d * y + g * z\\\\y’ = b * x + e * y + h * z\\\z’ = c * x + f * y + i * z\end{array} }$$

Johtopäätöksenä voidaan todeta, että siirryttäessä rivi-suuruusjärjestyksestä sarake-suuruusjärjestykseen ei ainoastaan vaihdeta pistettä tai vektoria ja matriisia keskenään kertolaskussa, vaan myös transponoidaan matriisi, jotta voidaan taata, että molemmilla konventioilla saadaan sama tulos (ja päinvastoin).

Näistä havainnoista nähdään, että mikä tahansa sarja muunnoksia, joita sovelletaan pisteeseen tai vektoriin, kun käytetään rivi-suuruuskonventiota, voidaan kirjoittaa peräkkäisessä järjestyksessä (tai lukujärjestyksessä). Kuvitellaan esimerkiksi, että halutaan kääntää piste P matriisilla T, sitten kiertää sitä z-akselin ympäri matriisilla Rz ja sitten y-akselin ympäri matriisilla Ry. Voit kirjoittaa:

$$P’=P * T * R_z * R_y$$

Jos käyttäisit pylväs-suurnotaatiota, sinun täytyisi kutsua transformaatiota käänteisessä järjestyksessä (mikä saattaa tuntua vastoin intuitiota):

$$$P’=R_y * R_z * T * P$$$

Voi siis ajatella, että ”täytyyhän olla jokin syy suosia yhtä systeemiä toisesta”. Itse asiassa molemmat konventiot ovat oikeita ja antavat meille saman tuloksen, mutta joistakin teknisistä syistä matematiikan ja fysiikan teksteissä vektoreita käsitellään yleensä sarakevektoreina.

Transformaatiojärjestys, kun käytämme sarakkeellisia matriiseja, muistuttaa matematiikassa enemmän tapaa, jolla kirjoitamme funktioiden evaluoinnin ja komposition.

Rive-major matriisikonventio tekee matriiseista kuitenkin helpommin opittavia, mikä on syy, miksi käytämme sitä Scratchapixelissä (samoin kuin Mayassa, DirectX:ssä. Ne on myös määritelty standardiksi RenderManin spesifikaatioissa). Jotkin 3D API:t, kuten OpenGL, käyttävät kuitenkin sarake-major-käytäntöä.

Implikaatio koodauksessa:

On toinenkin mahdollisesti hyvin tärkeä näkökohta, joka on otettava huomioon, jos on valittava rivin ja sarakkeen suhteen, mutta sillä ei ole mitään tekemistä itse konventioiden kanssa ja sen kanssa, kuinka käytännöllinen toinen on toisen suhteen. Se liittyy enemmänkin tietokoneeseen ja sen toimintatapaan. Muista, että käsittelemme matriiseja. Tyypillisesti matriisin toteutus C++:ssa näyttää tältä:

class Matrix44{ … float m;};

Kuten näet, matriisin 16 kerrointa tallennetaan kaksiulotteiseen liukulukumuotoiseen matriisiin (tai kaksoismuotoiseen matriisiin riippuen tarvitsemastasi tarkkuudesta. C++:n Matrix-luokkamme on template). Mikä tarkoittaa, että muistissa 16 kerrointa asetellaan seuraavasti: c00, c01, c02, c03, c10, c11, c12, c13, c20, c21, c22, c23, c30, c31, c32, c33. Toisin sanoen ne on sijoitettu muistiin vierekkäin. Katsotaan nyt, miten näitä kertoimia käytetään vektori-matriisikertoimessa, jossa vektorit kirjoitetaan rivi-suuruusjärjestyksessä: Toisin sanoen x’:n laskemiseksi tarvitsemme matriisin 16 float-matriisin 1., 5. ja 9. liukulukua. Jotta voimme laskea y’:n, meidän on käytettävä tämän matriisin 2., 6. ja 10. liukulukua. Ja lopuksi z’:tä varten tarvitsemme matriisin 3., 7. ja 11. liukuluvun. Tietojenkäsittelyn maailmassa ei ole välttämättä hyvä asia käyttää matriisin elementtejä muussa kuin peräkkäisessä järjestyksessä. Se itse asiassa mahdollisesti heikentää suorittimen välimuistin suorituskykyä. Emme mene tässä liian pitkälle yksityiskohtiin, mutta sanotaan vain, että lähintä muistia, johon prosessori pääsee käsiksi, kutsutaan välimuistiksi. Tähän välimuistiin on erittäin nopea pääsy, mutta siihen voidaan tallentaa vain hyvin rajallinen määrä tietoja. Kun suorittimen on käytettävä jotakin tietoa, se tarkistaa ensin, onko se olemassa välimuistissa. Jos on, CPU käyttää tätä tietoa heti (cache hit), mutta jos ei (cache miss), sen on ensin luotava sille merkintä välimuistiin ja kopioitava sitten tiedot keskusmuistista tähän paikkaan. Tämä prosessi vie luonnollisesti enemmän aikaa kuin silloin, kun tieto on jo olemassa välimuistissa, joten ihannetapauksessa haluamme välttää välimuistin ohituksia niin paljon kuin mahdollista. Sen lisäksi, että CPU kopioi tietyn datan keskusmuistista, se kopioi myös palan dataa, joka sijaitsee aivan sen vieressä (esimerkiksi seuraavat 24 tavua), koska laitteistosuunnittelijat ajattelivat, että jos koodisi tarvitsee käyttää esimerkiksi jonkin matriisin elementtiä, se todennäköisesti käyttää sitä seuraavia elementtejä pian sen jälkeen. Ohjelmissa tosiaan käydään usein silmukalla läpi matriisin elementtejä peräkkäisessä järjestyksessä, joten tämä oletus pitää todennäköisesti paikkansa. Sovellettuna matriisiongelmaamme matriisin kertoimien käyttäminen muussa kuin peräkkäisessä järjestyksessä voi siis olla ongelma. Jos oletetaan, että prosessori lataa välimuistiin halutun liukuluvun ja sen vieressä olevat kolme liukulukua, nykyinen toteutuksemme saattaa johtaa moniin välimuistin ohituksiin, koska kertoimet, joita käytetään x’ y’ ja z’:n laskemiseen, ovat 5 liukuluvun etäisyydellä toisistaan matriisissa. Toisaalta, jos käytetään sarakkeen suuruusjärjestyksen merkintätapaa, esimerkiksi x’:n laskeminen edellyttää pääsyä matriisin 1., 2. ja 3. elementtiin.

// column-major orderx’ = c00 * x + c01 * y + c02 * zy’ = c10 * x + c11 * y + c12 * zz’ = c20 * x + c21 * y + c22 * z

// column-major orderx’ = c00 * x + c01 * y + c02 * zy’ = c10 * x + c11 * y + c12 * zzz’ = c20 * x + c21 * y + c22 * z

// Kertoimia käytetään peräkkäisessä järjestyksessä, mikä tarkoittaa myös sitä, että hyödynnämme hyvin CPU:n välimuistitallennusmekanismia (esimerkissämme on vain 3 välimuistitallennusmatkan ohi menemistä 9:n tallennusnappulan sijaan). Yhteenvetona voidaan sanoa, että ohjelmoinnin kannalta piste- tai vektorimatriisikertolaskun toteuttaminen sarake-enemmistöjärjestystä käyttäen saattaa olla suorituskyvyltään parempi kuin rivi-enemmistöjärjestystä käyttävä versio. Käytännössä emme kuitenkaan ole pystyneet osoittamaan, että näin olisi todella ollut (kun käännät ohjelmasi käyttämällä optimointilippuja -O, -O2 tai -O3, kääntäjä voi tehdä työn puolestasi optimoimalla silmukoita moniulotteisten matriisien yli), ja olemme menestyksekkäästi käyttäneet rivi-suuruusjärjestyksen versiota ilman suorituskyvyn heikkenemistä verrattuna saman koodin versioon, jossa käytetään sarake-suuruusjärjestyksen toteutusta.

template<typename T>class Vec3{public: Vec3(T xx, T yy, T zz) : x(xx), y(yy), z(zz) {} T x, y, z, w;}; template<typename T>class Matrix44{public: T m; Vec3<T> multVecMatrix(const Vec3<T> &v) {#ifdef ROWMAJOR return Vec3<T>( v.x * m + v.y * m + v.z * m, v.x * m + v.y * m + v.z * m, v.x * m + v.y * m + v.z * m);#else return Vec3<T>( v.x * m + v.y * m + v.z * m, v.x * m + v.y * m + v.z * m, v.x * m + v.y * m + v.z * m);#endif }}; #include <cmath>#include <cstdlib>#include <cstdio>#include <cstdio>#include <ctime> #define MAX_ITER 10e8 int main(int argc, char **argv){ kello_t alku = kello(); Vec3<float> v(1, 2, 3); Matrix44<float> M; float *tmp = &M.m; for (int i = 0; i < 16; i++) *(tmp + i) = drand48(); for (int i = 0; i < MAX_ITER; ++i) { Vec3<float> vt = M.multVecMatrix(v); } fprintf(stderr, ”Kellonaika %f\n”, (clock() – start) / float(CLOCKS_PER_SEC)); return 0;}

Row-major ja Column-Major Order in Computing

Täydellisyyden vuoksi mainittakoon vielä, että termejä row-major ja column-major order voidaan käyttää myös tietojenkäsittelyssä kuvaamaan tapaa, jolla moniulotteisten matriisien alkiot on sijoitettu muistiin. Rivi-suuruusjärjestyksessä moniulotteisen matriisin elementit asetetaan peräkkäin vasemmalta oikealle, ylhäältä alas. Tätä menetelmää käytetään C/C++:ssa. Esimerkiksi matriisi:

$$M = \begin{bmatrix}1&2&3\\\4&5&6\end{bmatrix}$$

voitaisiin kirjoittaa C/C++:ssa seuraavasti:

float m={{1, 2, 3}, {4, 5, 6}};

ja tämän matriisin elementit sijoitettaisiin lineaariseen muistiin vierekkäin seuraavasti:

float m={{1, 2, 3}, {4, 5, 6}};

ja tämän matriisin elementit sijoitettaisiin lineaariseen muistiin vierekkäin:

1 2 3 4 5 6

Sarakkeellisessa järjestyksessä, jota käyttävät esimerkiksi FORTRANin ja MATLABin kaltaiset kielet, matriisin elementit tallennetaan muistiin ylhäältä alaspäin, vasemmalta oikealle. Käyttämällä samaa matriisiesimerkkiä matriisin elementit tallennettaisiin (ja niitä käytettäisiin muistissa seuraavasti:

1 4 2 5 3 6

Tietäminen siitä, miten matriisin elementit on sijoitettu muistiin, on tärkeää erityisesti silloin, kun niitä yritetään käyttää osoittimien siirtämisen ja for-silmukan optimoinnin avulla (olemme selittäneet aiemmin tässä luvussa, että se voi vaikuttaa CPU:n välimuistitallennuksen suorituskykyyn). Koska tarkastelemme kuitenkin vain C/C++:a ohjelmointikielenä, sarakkeiden suuruusjärjestys (laskentaan sovellettuna) ei kiinnosta meitä kovinkaan paljon. Mainitsemme vain, mitä termit tarkoittavat tietojenkäsittelyssä, jotta tiedät, että ne saattavat kuvata kahta eri asiaa riippuen asiayhteydestä, jossa niitä käytetään. Sinun tulisi olla varovainen, ettet sekoita niitä keskenään. Matematiikan yhteydessä ne kuvaavat sitä, kohdellaanko vektoreita (tai pisteitä) koordinaattiriveinä vai sarakkeina ja toisena, ja tietojenkäsittelyn yhteydessä ne kuvaavat tapaa, jolla tietty ohjelmointikieli tallentaa ja käyttää moniulotteisen matriisimassan (jollaisia matriisit ovat) elementtejä muistissa.

OpenGL on mielenkiintoinen tapaus tässä suhteessa. Kun GL:ää alun perin luotiin, kehittäjät valitsivat rivi-major-vektorikonvention. OpenGL:ää laajentaneet kehittäjät kuitenkin ajattelivat, että pitäisi palata sarake-major-vektoriin, minkä he tekivätkin. Yhteensopivuussyistä he eivät kuitenkaan halunneet muuttaa piste-matriisikertoimen koodia ja päättivät sen sijaan muuttaa järjestystä, jossa matriisin kertoimet tallennetaan muistiin. Toisin sanoen OpenGL tallentaa kertoimet sarake-duuri -järjestyksessä, mikä tarkoittaa, että sarake-duuri -vektoria käyttävän matriisin muuntokertoimilla m03, m13 ja m23 on float-matriisissa indeksit 13, 14 ja 15, kuten rivi-duuri -vektoria käyttävän matriisin muuntokertoimilla m30, m31 ja m32.

Yhteenveto

Kahden konvention väliset erot on tiivistetty seuraavaan taulukkoon:

Row-major vector (matematiikka) Column-…päävektori (matematiikka)

\(P/V=\begin{bmatrix}x & y & z\end{bmatrix}\)

\(P/V=\begin{bmatrix}x \\\ y \\\ z\end{bmatrix}\)

Pre-kertolasku \(vM\)

Post-kertolasku \(Mv\)

Kutsujärjestys ja järjestys, jossa muunnoksia sovelletaan, ovat samat: ”Otetaan P, muunnetaan T:llä, muunnetaan Rz:llä, muunnetaan Ry:llä” kirjoitetaan \(P’=P*T*R_z*R_y\)

Kutsujärjestys on päinvastainen kuin järjestys, jossa muunnoksia sovelletaan: ”ota P, muunna T:llä, muunna Rz:llä, muunna Ry:llä” kirjoitetaan seuraavasti: \(P’=R_y*R_z*T*P\)

API: Direct X, Maya

API: OpenGL, PBRT, Blender

Matriisin rivit edustavat koordinaatiston perustoja (tai akseleita) (punainen: x-akseli, vihreä: y-akseli, sininen:z-akseli)

$${\begin{bmatrix} \color{red}{c_{00}}& \color{red}{c_{01}}&\color{red}{c_{02}}&0\\ \color{green}{c_{10}}& \color{green}{c_{11}}&\color{green}{c_{12}}&0\\ \color{blue}{c_{20}}& \color{blue}{c_{21}}&\color{blue}{c_{22}}&0\\0&0&0&1 \end{bmatrix} } $$

Matriisin sarakkeet kuvaavat koordinaatiston perusteita (tai akseleita) (punainen: x-akseli, vihreä: y-akseli, sininen:z-akseli)

$${ \begin{bmatrix} \color{red}{c_{00}}& \color{green}{c_{01}}&\color{blue}{c_{02}}&0\\ \color{red}{c_{10}}& \color{green}{c_{11}}&\color{blue}{c_{12}}&0\\ \color{red}{c_{20}}& \color{green}{c_{21}}&\color{blue}{c_{22}}&0\\0&0&0&1\end{bmatrix} }$$

Käännösarvot tallennetaan elementteihin c30, c31 ja c32.

$${\begin{bmatrix}1&0&0&0\\0&1&0&0\\0&0&1&0\\Tx&Ty&Tz&1\end{bmatrix} }$$

Käännösarvot tallennetaan elementteihin c03, c13 ja c23.

$$${\begin{bmatrix}1&0&0&Tx\\0&1&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&0&1&1&1&1&

} }$$

Transponoi matriisi niin, että sitä käytetään sarakkeen suuruusjärjestyksessä järjestettynä matriisina

Transponoi matriisi niin, että sitä käytetään rivin…major-järjestetty matriisi

Row-major matrix (Computing) Column-major matrix (Computing)

API: Direct X, Maya, PBRT

API: OpenGL

Eräs lukija esitti Stackoverflow’ssa kysymyksen, jonka mukaan yllä oleva taulukko oli sekava. Aihe on hämmentävä, ja vaikka yritimme parhaan kykymme mukaan valottaa asiaa, monet ihmiset ovat edelleen hämmentyneitä siitä. Ajattelimme, että vastauksemme Stackoverflow’ssa voisi toivottavasti tuoda toisenlaista näkemystä kysymykseen.

Sinulla on teoria (mitä teet matematiikassa kynällä ja paperilla) ja mitä teet toteutuksella (C++). Nämä ovat kaksi eri ongelmaa.

Matematiikka: voit käyttää kahta merkintätapaa, joko sarake- tai rivimerkintöjä. Rivin päävektorilla paperilla sinun täytyy kirjoittaa vektori-matriisikertolasku vM, jossa v on rivivektori (1×4) ja M sinun 4×4 matriisi. Miksi? Koska matemaattisesti voit kirjoittaa vain *, etkä toisinpäin. Vastaavasti jos käytät saraketta, vektori on kirjoitettava pystysuoraan alaspäin tai notaatiossa (4 riviä, 1 sarake). Näin ollen kertolasku matriisilla voidaan kirjoittaa vain seuraavasti: . Huomaa, että matriisi asetetaan vektorin eteen: Mv. Ensimmäistä merkintätapaa kutsutaan vasemmanpuoleiseksi eli pre-multiplikaatioksi (koska vektori on tulon vasemmalla puolella) ja toista (Mv) kutsutaan oikeanpuoleiseksi eli post-multiplikaatioksi (koska vektori on tulon oikealla puolella). Kuten näet, termit johtuvat siitä, onko vektori matriisin vasemmalla puolella (edessä eli ”pre”) vai oikealla puolella (jälkeen eli ”post”).

Nyt, jos sinun täytyy muuttaa vektoria (tai pistettä), sinun täytyy kiinnittää huomiota kertolaskujen järjestykseen, kun kirjoitat ne paperille. Jos haluat kääntää jotain matriisilla T ja sitten kiertää matriisilla R ja sitten skaalata matriisilla S, niin sarakkeellisessa maailmassa sinun täytyy kirjoittaa v’ = S * R * T * v. Rivimaisessa maailmassa sinun täytyy kirjoittaa v’ = v * T * T * R * S.

Tässä on teoriaa. Kutsutaan tätä rivi/sarakevektorikonventioksi.

Tietokone: Sitten tulee se kohta, kun päätät toteuttaa tämän C++:lla vaikkapa. Tässä on se hyvä puoli, että C++ ei pakota sinua mihinkään mistään. Voit kartoittaa matriisisi kertoimien arvot muistiin haluamallasi tavalla, ja voit kirjoittaa koodin suorittamaan matriisikertolaskun toisella matriisilla haluamallasi tavalla. Vastaavasti se, miten käytät kertoimia vektori-matriisikerrointa varten, on täysin sinun päätettävissäsi.Sinun on tehtävä selvä ero sen välillä, miten kartoitat kertoimet muistissa, ja sen välillä, mitä konventioita sinun on käytettävä matemaattisesta näkökulmasta vektoreiden esittämiseen. Nämä ovat kaksi toisistaan riippumatonta ongelmaa. Kutsutaan tätä osaa rivi/sarakkeen pääasiallista asettelua.

Voit esimerkiksi julistaa matriisiluokan vaikkapa 16 vierekkäisen liukuluvun muodostamaksi joukoksi. Se on hienoa. Jossa kertoimet m14, m24, m34 edustavat matriisin käännösosaa (Tx, Ty, Tz), joten oletat, että ”konventiosi” on rivi-major, vaikka sinua kehotetaan käyttämään OpenGL-matriisikonventiota, jonka sanotaan olevan column-major. Mahdollinen sekaannus johtuu siitä, että kertoimien kartoitus muistissa on erilainen kuin se mentaalinen esitys, jonka teet itsellesi ”column-major”-matriisista. Koodaat ”riviä”, mutta sinun sanottiin käyttävän (matemaattisesta näkökulmasta) ”saraketta”, mistä johtuu vaikeutesi saada tolkkua siitä, teetkö asiat oikein vai väärin.

Tärkeää on nähdä matriisi kolmen akselin määrittelemän koordinaatiston esityksenä ja translaationa. Se, mihin ja miten tallennat nämä tiedot muistiin, on täysin sinun päätettävissäsi. Olettaen, että koordinaatiston kolmea akselia edustavat kolme vektoria ovat nimeltään AX(x,y,z), AY(x,y,z), AZ(x,y,z), ja translaatiovektoria merkitään (Tx, Ty, Tz), niin matemaattisesti, jos käytät pylväsvektoria, sinulla on:

$$M = \begin{bmatrix} AXx & AYx & AZx & Tx\\\ AXy & AYy & AZy & Ty \\\ AXz & AYz & AZz & Tz \\\0 & 0 & 0 & 1 & 1\end{bmatrix}$$

Koordinaatiston akselit kirjoitetaan pystysuoraan. Nyt jos olet jos käytät rivi-duuri:

$$$M = \begin{bmatrix} AXx & AXy & AXz & 0\\\ AYx & AYy & AYz & 0 \\\ AZx & AZy & AZz & 0 \\\ Tx & Ty & Tz & 1\end{bmatrix}$$

Koordinaatiston akselit kirjoitetaan vaakasuoraan. Tietokonemaailmassa ongelmana on nyt se, miten nämä kertoimet tallennetaan muistiin. Ei. Voit myös kirjoittaa:

float m = { AXx, AXy, AXz, Tx, AYx, AYy, AYz, AYx, AYy, AYz, Ty, AZx, AZy, AZz, Tz, 0, 0, 0, 1};

tai: Säilytät vain 16 kerrointa muistissa eri tavoin, ja se on täysin kunnossa, kunhan tiedät, mikä tuo tapa on, jotta voit käyttää niitä sopivasti myöhemmin. Muista, että matriisilla kerrottu vektori antaa saman vektorin riippumatta siitä, käytätkö matemaattista rivi- vai sarakemerkintää. Tärkeää on siis oikeastaan se, että kerrot vektorisi (x,y,z)-koordinaatit oikeilla matriisikertoimilla, mikä edellyttää tietoa siitä, miten ”sinä” olet päättänyt tallentaa matriisikertoimen muistiin:

Vector3 vecMatMult ( Vector3 v, float AXx, float AXy, float AXz, float Tx, float AYx, float AYy, float AYz, float Ty, float AZz, float AZy, float AZz, float AZz, float Tz) { return Vector3( v.x * AXx + v.y * AYx + v.z * AZx + Tx, v.x * AXy + v.y * AYy + v.z * AZy + Ty, v.x * AXz + v.y * AZz + v.z * AZz + Tz}

Kirjoitimme tämän funktion korostaaksemme sitä tosiasiaa, että riippumatta siitä, mitä konventiota käytät, vektorin * matriisin kertolaskun tuloksena saadaan vain vektorin syöttökoordinaattien ja koordinaatiston akselikoordinaattien AX, AY ja AZ välinen kertolasku ja yhteenlasku (riippumatta käyttämästäsi merkintätavasta ja tavasta, jolla tallennat ne muistiin). Jos käytät:

float m = { AXx, AXy, AXz, 0, AYx, AYy, AYz, 0, AZx, AZy, AZz, 0, Tx, Ty, Tz, 1};

Sinun täytyy kutsua:

vecMatMult(v, m, m, m, m, …

Jos käytät:

float m = { AXx, AYx, AZx, Tx, AXy, AYy, AZy, Ty, AXz, AYz, AZz, AZz, Tz, 0, 0, 0, 0, 1};

Tarvitaan kutsua:

vecMatMult(v, m, m, m, m, m, m, m, m, m, …

Kertooko tuo sinulle, mitä konventiota käytät? Ei. Sinun täytyy vain kutsua oikeita kertoimia oikeissa paikoissa, kun teet vec * mat-kerrointa. Ja siinä kaikki, niin hämmentävältä kuin se tuntuukin.Nyt asiat ovat hieman toisin, kun on kyse mat * mat -kertolaskusta. Voit olettaa, että järjestys, jossa matriisit kerrotaan, ei ole sama. Eli R * S * T ei ole sama kuin T * S * R. Järjestyksellä on todellakin merkitystä. Nyt taas jos käytät ”rivin suuruusluokkaa” niin matemaattisesti sinun täytyy kirjoittaa:

mt11 = ml11 * mr11 + ml12 * mr21 + ml13 * mr31 + ml14 * mr41

jossa ml on vasemmanpuoleinen matriisi ja mr oikeanpuoleinen: mt = ml * mr. Huomatkaa kuitenkin, että emme ole käyttäneet hakasulkeita indekseihin, koska emme halua antaa ymmärtää, että käytämme 1D-joukkoon tallennettuja elementtejä tässä. Puhumme vain paperille kirjoitettujen matriisien kertoimista. Jos haluat kirjoittaa tämän C++:lla, kaikki riippuu siitä, miten olet tallentanut kertoimet muistiin edellä ehdotetulla tavalla.

Vastaa

Sähköpostiosoitettasi ei julkaista.