Konsepter:3D grafikk

Fra CodeWiki

Gå til: navigasjon, søk

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

Perspektiv

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.

Identitetsmatrise

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.

Matriseoversikt

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:

Roteringsmatrise for X aksen

Deretter gjelder de samme reglene for Y og Z

Roteringsmatrise for Y aksen Roteringsmatrise for Z aksen

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:

Perspektivmatrise

Vær obs på dette Limes Z

Med andre ord kan near aldri være null.

Se også

OpenGL

Direct3D

XNA

Personlige verktøy
dataprogrammering
generelt