Konsepter:3D grafikk
Fra CodeWiki
Definisjonen av 3D grafikk er grafikk som ser ut til å ha 3 dimensjoner (X, Y og Z) Problemet med 3D grafikk på datamaskiner, er at en normal skjerm kun viser 2 dimensjoner. Det som må til er å kunne gjøre om 3 dimensjoner med data til et todimensjonalt bilde som kan vises på en todimensjonal skjerm. Det finnes hovedsaklig to metoder for å gjøre dette; med matriser, eller hardkode det.
For å forstå denne artikkelen vil det være nødvendig med litt basiske kunnskaper om trigonometri. C/C++ syntaks vil bli brukt fordi C/C++ er lingua franca innen programmering, men vil kun bli brukt eksempelvis, og det kreves ingen dyp forståelse av C++ for å forstå konseptene.
Aller først må vi ta frem noen definisjoner, som vektorer. I matematikk er en vektor et punkt med en retning, i denne artikkelen vil vi anta at en vektor er et punkt på et spesifikt sted uten noen retning, dette er egentlig det motsatte av hva en vektor er i matematikk. Vi benytter to datatyper for vektorer her: vector2 og vector3. Disse består av punktene {x, y} og {x, y, z} som er flyttallsverdier.
Vi tar for oss normal programmering først. Først så er den enkleste definisjonen for å få gjort om 3D til 2D rett og slett å dele X og Y på Z
vector2 Project(vector3 input, float width, float height) { return vector2(input.x / input.z + width / 2, input.y / input.z + height / 2); }
Men en ser her øyeblikkelig problemer, hva hvis z er null? eneste rette å gjøre, er å sjekke om z er null, og returnere null på den aksen.
vector2 Project(vector3 input, float width, float height) { if(input.z == 0) return vector2(0, 0); else return vector2(input.x / input.z + width / 2, input.y / input.z + height / 2); }
Men dersom du forsøker dette, vil du se at det kan virke riktig, men det er ikke helt riktig enda, så vi må ta tak i litt trigonometri. Se for deg synsvinkelen din som en rettvinklet trekant
Da er det bare snakk om litt trigonometri for å beregne en mer korrekt 2D versjon.
Husk at y / z = tan(α) der vi lar y være høyden til "vinduet" i dette tilfellet. Så det vi trenger å gjøre, er å multiplisere hvert punkt sin y verdi med høyde / tan(α), der α er FOV / 2 for å få et mer riktig punkt. Husk at datamaskinen regner i radianer hvor 45° er det samme som π/2
// Field of view er definert ved radianer vector2 Project(float fov, vector3 input, float width, float height) { float aspect_w = width / tan(fov / 2); float aspect_h = height / tan(fov / 2); if(input.z == 0) return vector2(0, 0); else return vector2((input.x * aspect_w) / input.z + width / 2, (input.y * aspect_h) / input.z + height / 2)); }
Dette skal gi et riktig perspektiv.
Matriser
Matriser er noe av det viktigste innen 3D grafikk, og forenkler veldig mye arbeid. Rotering, translering, skalering pluss pluss gjøres vanligvis med matriser. Matriser er kritisk å ha en god forståelse for når en driver med seriøs programmering av 3D grafikk. En matrise i 3D grafikk er som regel et 4x4 2-deminsjonalt array av flyttall. I noen tilfeller kan en møte på matriser med færre dimensjoner, 4x3 og 3x3 (brukes mye i 2D grafikk) Matrisematematikk er noe en sannsynligvis kommer borti når en jobber med Direct3D eller OpenGL. I OpenGL er det enda viktigere å kunne matrisematematikken, fordi det er nærmest forventet at en skal skrive egne matrisefunksjoner, eller hente dem fra et annet sted.
Vanligvis multipliserer en matriser med andre matriser, eller punkter. For å få en enkel forståelse for hvordan matriser fungerer, skal vi se på hvordan en får multiplisert en matrise med et punkt, for å få et nytt punkt. Dette er selve basisen for 3D grafikk og er en operasjon som nærmest alle 3D programmer gjør.
Identity matriser er en matrise der en multiplikasjon med en annen matrise vil gi den andre matrisen uendret; samme som å multiplisere med 1 med andre ord. De er satt opp med 1 diagonalt fra venstre ned til høyere.
struct matrix44 { float m00, m10, m20, m30; float m01, m11, m21, m31; float m02, m12, m22, m32; float m03, m13, m23, m33; static matrix44 Empty() { matrix44 result; result.m00 = 0; result.m10 = 0; result.m20 = 0; result.m30 = 0; result.m01 = 0; result.m11 = 0; result.m21 = 0; result.m31 = 0; result.m02 = 0; result.m12 = 0; result.m22 = 0; result.m32 = 0; result.m03 = 0; result.m13 = 0; result.m23 = 0; result.m33 = 0; return result; } static matrix44 Identity() { matrix44 result; result.m00 = 1; result.m10 = 0; result.m20 = 0; result.m30 = 0; result.m01 = 0; result.m11 = 1; result.m21 = 0; result.m31 = 0; result.m02 = 0; result.m12 = 0; result.m22 = 1; result.m32 = 0; result.m03 = 0; result.m13 = 0; result.m23 = 0; result.m33 = 1; return result; } float& operator [](int column, int row) { float* arr = &m00; return arr[row * 4 + column]; } }; vector3 MultiplyMatrixVector3(matrix &mat, vector3 &vec) { return vector3 ( inp.x * mat.m00 + inp.y * mat.m10 + inp.z * m20 + m30, inp.x * mat.m01 + inp.y * mat.m11 + inp.z * m21 + m31, inp.x * mat.m02 + inp.y * mat.m12 + inp.z * m22 + m32 ); }
Utifra denne, kan en med logikk finne ut at m00, m11 og m22 er skalering av x, y og z m30, m31 og m32 er translering av x, y og z.
Så da kan vi lage en transleringsmatrise slik:
matrix44 TranslateMatrix(float x, float y, float z) { matrix44 res = matrix44::Identity(); res.m00 = 1.0f; res.m10 = 0.0f; res.m20 = 0.0f; res.m30 = x; res.m01 = 0.0f; res.m11 = 1.0f; res.m21 = 0.0f; res.m31 = y; res.m02 = 0.0f; res.m12 = 0.0f; res.m22 = 1.0f; res.m32 = z; res.m03 = 0.0f; res.m13 = 0.0f; res.m23 = 0.0f; res.m33 = 1.0f; return res; }
og en skaleringsmatrise vil se slik ut:
matrix44 ScaleMatrix(float x, float y, float z) { matrix44 res = matrix44::Identity(); res.m00 = x; res.m10 = 0.0f; res.m20 = 0.0f; res.m30 = 0.0f; res.m01 = 0.0f; res.m11 = y; res.m21 = 0.0f; res.m31 = 0.0f; res.m02 = 0.0f; res.m12 = 0.0f; res.m22 = z; res.m32 = 0.0f; res.m03 = 0.0f; res.m13 = 0.0f; res.m23 = 0.0f; res.m33 = 1.0f; return res; }
Matriseoppsettet er satt opp for å gjøre idéen mer oversiktelig.
Og da er det på tide å ta opp den vanskeligste delen av matriser, å multiplisere en matrise med en annen matrise. Dette innebærer egentlig at alle felt må kryssmultipliseres.
Dette er enklest å forklare med kode
matrix44 MultiplyMatrix(matrix44& a, matrix44& b) { matrix44 result = matrix44::Empty(); for (int i = 0; i < 4; i++) for (int j = 0; j < 4; j++) for (int k = 0; k < 4; k++) result[i, j] += a[i, k] * b[k, j]; return result; }
Når vi nå kan multiplisere matriser, er det på tide å lære hvordan en kan rotere en modell i 3D. Da er det på tide å ta frem roteringsmatrisene. Det finnes tre stykker, som er rotering rundt X, Y og Z aksene, eller heading, pitch og roll som det noen ganger også kalles.
La oss begynne med rotering rundt X aksen.
Da titter vi nok en gang på funksjonen der vi multipliserer en vektor med matrisen. hvis vi vet at X_out = x_in * m00 + y_in * m10 + z_in * m20 + m30
Og en enkel rotering rundt X aksen er y_out = y_in * cos(v) men det er ikke alt, fordi vi må huske på at Z også skal roteres.
Så dette blir den komplette matrisen for å rotere et punkt rundt X aksen:
Deretter gjelder de samme reglene for Y og Z
Dette er den grunnleggende mekanikken bak 3D grafikk.
Skal vi for eksempel flytte et objekt 20 enheter vekk, og rotere det rundt sin egen akse, er det såpass enkelt som dette:
vector3 return_value = MultiplyMatrixVector3(MultiplyMatrix(TranslateMatrix(0, 0, 20), RotateMatrixZ(PI / 2 /* 45° */)), input_vector3);
Som kan uttrykkes enklere med
vector3 return_value = TranslateMatrix(0, 0, 20) * RotateMatrixZ(PI / 2 /* 45° */) * input_vector3
Merk at i motsetning til vanlig multiplisering, spiller det stor rolle i hvilken rekkefølge multiplikasjonene foregår, altså matrix_a * matrix_b != matrix_b * matrix_a.
Perspektiv med matriser
Det er også mulig å lage et perspektiv ved å bruke matriser. Disse er litt mer kompliserte i oppbyggingen en andre matriser. Samme kamera operasjonen brukes her, men vi er nå interessert i å vite den omvendte tangenten, så denne konstanten blir slik: f = cotan(α/2)
deretter så må x verdien multipliseres med f / (bredde / høyde) altså
m00 = cotan(α/2) / (width / height)
y må multipliseres med f
m11 = cotan(α/2)
Deretter kommer den som er litt vanskelig å forstå, nå skal vi definere clipping plane, som på z aksen skal multipliseres med (far+near)/(near-far) og så skal vi addere (2*far*near)/(near - far).
Husk z_out = x_in * m02 + y_in * m12 + z_in * m22 + m32
Grunnen til at vi multipliserer er at vi vil ha z verdiene innefor en visst område, og grunnen til at vi adderer, er fordi vi vil at de skal starte ved den definerte near verdien.
Den ferdige matrisen vil se slik ut:
Med andre ord kan near aldri være null.

