Modul 1.4 Graphics pipeline, transformasjoner

WebGL og grunnleggende 3D transformasjoner

Dette avsnittet beskriver hvordan transformasjonsmatrisene brukes til å gjengi (rendre) 3D-geometri i form av primitiver/trekanter, som settes sammen til objekter/modeller, på en 2D skjerm.

Innhold:

Normaliserte koordinatverdier

3D modellene består av et sett primitiver som igjen består av et sett vertekser. En verteks tilsvarer et punkt i rommet, spesifisert vha. koordinatverdiene x, y og z. I tillegg kan man assosiere en farge, teksturkoordinat og normalvektor til en verteks – en verteks kan med andre ord inneholde mer enn bare koordinatverdiene x, y, z.

Man kan tegne primitiver/trekanter direkte på «canvaset» ved å bruke såkalte normaliserte koordinatverdier, dvs. verdiene til x,y og z i området [-1, 1]. Disse verdiene er uavhengig av skjerm/canvas-størrelse og er egentlig koordinatsystemet som OpenGL ES forholder seg til.

Fra NDC til skjermkoordinater

Før OpenGL ES kan tegne noe på skjermen kreves det at alle koordinatverdier er innenfor dette området (dvs. [-1, 1]). Bruker vi slike verdier direkte (som hittil) fungerer ting fint. Dette er imidlertid svært begrensende og derfor ikke ønskelig. I tillegg må det være mulig å posisjonere, skalere og rotere modellene.

Det må være mulig å bruke hvilke koordinatverdier som helst for å definere en modell. Deretter må disse transformeres slik at det ender opp i området [-1, 1].

Dette koordinatsystemet vil deretter avbildes («mappes») til canvaset, som tilsvarer det vi kaller «viewport». Normalt vil hele canvaset, dvs. det grønne området i figuren over, tilsvare viewporten.

Når trekanten i figuren over har koordinatverdier i dette området vil den vises på skjermen/canvaset som vist til høyre i figuren. Merk at figuren inkluderer koordinataksene – disse må evt. tegnes vha. egne linjer om de skal vises.

Oppsummert: Dersom man oppgir alle vertekskoordinatene (x,y og z) som normaliserte koordinatverdier, dvs. i området [-1, 1], kan de tegnes direkte på skjermen uten bruk av transformasjoner.

Videre skal vi se hvordan vi kan definere 3D-modeller med vilkårlige koordinatverdier. Vi vil se hvordan modeller defineres i sitt eget, lokale, koordinatsystem og deretter «plasseres» i det «globale koordinatsystemet». Ved hjelp av ulike transformasjoner, kamera og projeksjon vil modellens koordinater til slutt ende opp med normaliserte koordinatverdier som kan gjengis på skjermen med 3D-effekt.

Vi ønsker f.eks. å kunne bruke koordinatverdier som illustrert i figuren under og allikevel få den frem på skjermen som vist under:

Bruk av vilkårlige koordinatverdier

Her er trekanten definert av verteksene [-10, -10, 0], [10, -10, 0] og [0, 10, 0]. Koordinatverdiene transformeres (T) deretter slik at de kan fremstilles på skjermen som en trekant som vist til høyre i figuren. Legg merke til at vi ser trekanten i perspektiv – dette vil også bli forklart her.

I WebGL defineres verteksene til figurer/modeller i forhold til et høyrehånds koordinatsystem. Figuren under viser dette i forhold til «skjermen».

Koordinatsystemet til WebGL (Matsuda et al., 2013)

Her ser vi at negativ z går «innover» i skjermen mens positiv z går utover. «Øyet» i figuren representerer det «virtuelle» kamera som er med på å bestemme hva som vises på skjermen. Dette peker i utgangspunktet mot negativ z.

Steg for å lage et WebGL program

Kort oppsummert vil et typisk WebGL/Javascript program gjøre følgende for å få tegnet en 3D-modell:

  • Definere verteksene til modellen i et tenkt lokalt koordinatsystem. Det er som regel fordelaktig å sentrere modellen om origo.
  • Lage en modellmatrise som inneholder en, eller en kombinasjon, av følgende transformasjoner: translasjon, rotasjon og skalering. Vha. modellmatrisa "plasseres" modellen i det globale koordinatsystemet.
  • Lage en viewmatrise som spesifiserer hvilken del av «scenen» som skal vises på skjermen. Man tenker seg at man plasserer et kamera i det globale systemet. Det som kameraet «ser» er det som ender opp på skjermen.
  • Lage en projeksjonsmatrise. Denne bestemmer hvordan 3D-verden/scenen skal projiseres på skjermen. I tillegg er den med på å avgrense hva som vises.
  • Multiplisere sammen modell- og viewmatrisene til en modelview-matrise.
  • Sende modelview-matrisa og projeksjonsmatrisa til verteksshaderen.
  • Verteksshaderen bruker disse til å transformere hver enkelt verteks. Resten av «jobben», deriblant sammenstilling av primitiver/trekanter, rasterisering m.m. utføres av GPU’en og fragmentshaderen.

Mer om graphics pipeline

OpenGL ES sin «graphics pipeline» viser hvilke steg som inngår for å få definerte modeller og primitiver til å vises på skjermen. Den viser også hvilke transformasjoner som utføres fra verteksene leveres fra WebGL/Javascript-programmet til det endelige bildet kan vises på skjermen.

Figuren er basert på tilsvarende fra (Munshi et al., 2009)

Shader

Når vi bruker OpenGL ES & WebGL er vi nødt til å forholde oss til shadere. Shadere er programmer, skrevet i sitt eget språk - GLSL, som kjører direkte på maskinens GPU. Man legger som regel shaderkoden som en del av html- eller Javascript-koden.

Verteksshaderen vil normalt transformere hver verteks vha. diverse 4x4 matriser, normalt en såkalt modelview-matrise og en projeksjons-matrise. Disse sendes normalt til verteksshaderen slik at de er tilgjengelig når verteksshaderen transformerer verteksene. Disse matrisene vil bli omtalt mer etter hvert.

Videre skal vi se på et eksempel som tegner en enkel trekant med koordinatverdier (x, y, z) i området -2.5 - +2.5 som transformeres og projiseres slik at den til slutt vises på skjermen.

Kodeeksempel

Vi tar utgangspunkt i et kodeeksempel som tegner en enkel trekant. Verteksene legges i et verteksbuffer. Legg merke til koordinatverdiene til de tre verteksene som utgjør trekanten – disse ligger i området [-2.5, 2.5]. Vi skal også hvordan vi bruker transformasjoner for å rotere og flytte trekanten. Vi ser først på html-fila, index.html:

Tilhørende Javascript-kode, helloTriangle.js:

Programmet bruker et position-array og et color-array, som begge legges inn i hvert sitt Float32Array-buffer. Bufferen brukes til å tegne en enkel trekant på html-canvaset, eller egentlig på canvasets viewport. Viewport tilsvarer den delen av skjermen, hele html5-canvaset i dette tilfellet, som den endelige tegninga vises på. Man kan alternativt definere en mindre del av canvaset som viewport. Kommer tilbake til dette senere.

Posisjonsbufret

Posisjonsbufret opprettes ved hjelp av tre posisjoner siden det er en enkel trekant som skal tegnes – se initBuffers() metoden. Disse har koordinatverdiene:

const positions = new Float32Array([
   0.0,        height/2,   0.0,    // X Y Z
   -width/2,   -height/2,  0.0,    // X Y Z
   width/2,    -height/2,  0.0     // X Y Z
]);

Fargebufret

Vi bruker et eget buffer for fargene. Merk at det må være like mange farger som posisjoner. Hver verteks består dermed av en posisjon og en farge. Fargebufret opprettes vha. følgende array:

const colors = new Float32Array([
   1, 0.3, 0, 1,   //R G B A
   1, 0.3, 0, 1,   //R G B A
   1, 0.3, 1, 1,   //R G B A
]);

Disse tre verteksene (posisjon+farge) vil bli sendt, en og en, til verteksshaderen etter at drawArrays() er utført, se draw() metoden. I draw() koples posisjon- og fargeattributtene til tilsvarende buffer. Deretter opprettes objektet cameraMatrixes via initCamera(). Dette inneholder view- og projeksjonsmatrisa. Deretter opprettes modellmatrisa som flytter modellen litt til venstre og litt opp, samt roterer den 30 grader. Vha. modellmatrisa og view-matrisa opprettes modelview-matrisa. Modelview- og projeksjonsmatrisa sendes så til verteksshaderen før vi kaller på drawArrays().

Modellmatrisa gjør at modellen flyttes litt (-3) til venstre og litt (1) opp. View-matrisa sørger for å plassere kameraet i [0,0,10] orientert mot origo. Kamera må peke mot modellen for at den skal vises på skjermen. Disse to matrisene slås sammen til en modelview-matrise. Her er det viktig at matrisene multipliseres i korrekt rekkefølge. Kodelinja

modelviewMatrix = viewMatrix.multiply(modelMatrix);

betyr at matrisene slås sammen slik:

modelviewMatrix = viewMatrix * modelMatrix

dvs. at rad1 i viewMatrix multipliseres med kolonne1 i modelMatrix for å beregne første element (e11) i modelviewMatrix osv.

Projeksjonsmatrisa er med på å begrense hvilken del av 3D-verden som skal vises på skjermen samt å sørge for at alle verteksene omgjøres til såkalte «clip-koordinater». Disse matriseoperasjonene vil bli forklart nærmere i neste avsnitt.

I verteksshaderen er følgende parametre definert:

#version 300 es
in vec3 aVertexPosition;
in vec4 aVertexColor;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
out lowp vec4 vColor;
void main(void) {
   gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(aVertexPosition, 1.0);
   vColor = aVertexColor;
   gl_PointSize = 10.0;    //Merk: Kun i bruk når man tegner POINTS
}

Den første, aVertexPosition av type vec3 representerer verteksen som kommer fra WebGL/Javascript-koden. Denne endres for hver verteks. Den andre, aVertexColor, representerer fargeverdien. De to neste er 4x4 matriser som også kommer fra WebGL/Javascript-koden men har samme verdi for alle vertekser (derfor nøkkelordet uniform). Parametret vColor er en «output»-variabel som lar oss videresende aVertexColor til fragmentshaderen (husk: fargeverdien interpoleres mellom verteks- og fragmentshaderen).

Videre ser vi at den innebygde variabelen gl_Position settes lik aVertexPosition multiplisert med de to nevnte matrisene (NB! Må gjøres i korrekt rekkefølge, mer etter hvert).

Legg også merke til at aVertexPosition gjøres om slik at homogene koordinater benyttes. Dette gjøres vha. vec4(aVertexPosition, 1) slik at den «homogene verteksen» blir (x,y,z,1). Se eget notat om vektorer og matriser som forklarer mer om homogene koordinater.

Variabelen gl_Position er en GLSL spesifikk variabel. Hovedoppgaven til verteksshaderen er å gi denne variabelen en verdi. Som regel settes den lik produktet av verteksen, modellview- og projeksjonsmatrisene. Verteksen sine koordinater (x,y,z,w) er nå gjort om til såkalte "clip-koordinater". Se figur under.

Graphics pipeline og transformasjoner

I WebGL snakker man gjerne om følgende transformasjoner:

  • Modelltransformasjon
  • View-transformasjonen
    • De to foregående slås ofte sammen til en transformasjon (matrise): «Modelview»- transformasjonen
  • Projeksjonstransformasjonen

Disse transformasjonene utføres vha. 4x4 matriser.

I tillegg utføres (automatisk, vi trenger egentlig ikke tenke på dette):

  • «Perspective division», dvs. gjøre om fra homogene koordinater tilbake til kartesiske koordinater. Dette gjøres ved å dele x,y og z på den 4. koordinatverdien, w.
  • Viewport-transformasjon.

Disse punktene forklares nærmere i etterfølgende kapitler.

Verteksshaderen

Normalt utføres både modelview-transformasjonen og projeksjonstransformasjonen i verteksshaderen vha. matrisemultiplikasjon. Dette ser vi også i kodeeksemplet over.

Både viewport-transformasjonen og «perspective division» utføres (automatisk) i steget etter verteksshaderen, dvs. i steget "Sammenstilling av primitiver".

Vi ser dette i figuren under, som er et utsnitt av tidligere vist graphics pipeline.

Utsnitt av graphics pipeline

Verteksshaderen utfører vertekstransformasjonen ved å multiplisere verteksen med både modelview- og projeksjonsmatrisa. Dette betyr at shaderen må ha tilgang på disse matrisene. Normalt leveres disse fra «klientprogrammet» (WebGL/Javascript) før verteksene sendes til shaderen.

Etter at verteksen er multiplisert med modelview-matrisa sier vi at verteksene er transformert til «øye-koordinater». Hvorfor det kalles «øye-koordinater» vil bli tydeligere når vi ser mer på view-transformasjon.

Den transformerte verteksen multipliseres så med projeksjonsmatrisa og er da transformert til «clip-koordinater». Den transformerte verteksen tilordnes til slutt i verteksshaderen til gl_Position.

Sammenstilling av primitiver (trekanter)

I neste steg av pipelinen, «Sammenstilling av primitiver», vil koordinatene som er tilordnet gl_Position, som fortsatt består av homogene koordinater, divideres med w.

Verteksen er nå omgjort til såkalte NDC (Normalized Device Coordinates). Det vil si at alle vertekser som har koordinatverdier (x, y og z) i området [-1, 1] vil være med videre i pipelinen. Vertekser med koordinater utenfor dette området vil «clippes» vekk – de er ikke med videre. Primitiver som er delvis innenfor deles i to. Tenk på dette som om frustumet strekker seg fra -1 til +1 i x,y og z-retning.

Deretter utføres «viewport»-transformasjonen (dette gjøres ikke ved hjelp av en matrise), dvs. at NDC mappes til det området på skjermen som skal vise resultatet.

Beregning av dybde; z-buffer

Basert på z-verdien til NDC beregnes det en z-/dybdeverdi, i området 0-1, per fragment. Verdien legges i z-/dybde-bufret og er med på å bestemme om nye fragmenter skal vises eller ikke.

Se også (OpenGL Wiki, Vertex Transformation, 2012) for mer info.

Rasterisering

Etter «viewport»-transformasjonen utføres rasterisering som betyr å fylle primitivene (trekantene) med nødvendige fragmenter/piksler.

Modellene/trekantene er nå brutt ned til et sett fragmenter. Et fragment er en «kandidat» til å bli en piksel i skjermbufret, ikke alle fragmenter ender opp som piksler i skjermbufret. Hvert fragment sendes nå til fragmentshaderen (kalles pikselshader i Direct3D-terminologi).

Fragmentshaderen

Dette er, som verteksshaderen, et program som kjøres av GPUen. Her settes eller beregnes fargen til hver enkelt piksel som utgjør det indre av trekanten. Fargeverdien, vColor, til "innkommende" fragment er en interpolert (utjevnet) verdi som er basert på trekantenes verteksfarger.

Fragmentshaderen mottar en farge som et parameter, her har vi kalt den vColor (et in-parameter). I sin enkleste form vil fragmentshaderen kun videresende denne ved å sette out-parametret fragColor lik in-parametret vColor. Alternativt kan den f.eks. gjøre ulike lysberegninger som påvirker endelig farge på fragmentet. Uansett må fragmentshaderen videresende minst en fargeverdi, her vColor. I praksis betyr dette som regel å sette fragColor=vColor.

Alternativt kunne man latt vær å bruke verteksfarger og i stedet la alle pikslene få samme farge vha. en uniform-parameter i fragmentshaderen. Mer om dette senere.

Oppsummering

Hele prosessen kan forenklet illustreres slik:

Forenklet graphics pipeline

Transformasjonene som utføres av verteksshaderen

La oss nå se mer detaljert på de ulike transformasjonene som verteksshaderen utfører.

Modelltransformasjonen

Når man opererer med mange modeller er det enklere å definere hver enkelt 3D modell i forhold til et lokalt koordinatsystem (model space) i stedet for et globalt/scene koordinatsystem. Tenk deg at du skal arrangere en miniatyrscene for et filmopptak hvor det inngår modeller som biler, hus, tog, trær osv. Modellene som skal fotograferes må bygges. I stedet for å bygge disse direkte i miniatyrverdenen vil man typisk bygge disse i et verksted (e.l.) før man plasserer dem i scenen.

På tilsvarende måte konstrueres 3D modeller i WebGL. Modellene defineres i forhold til et tenkt lokalt koordinatsystem (model space) og gjerne sentrert rundt origo. Deretter "plasseres modellen" i det "globale" koordinatsystemet (world space) ved hjelp av en koordinattransformasjon / matrisemultiplikasjon. Se (Luna, 2006), (Angel E. & David S., 2015) og (Matsuda et al., 2013, vedlegg G).

Når vi i Javascript-koden spesifiserer en modell, f.eks. en kube, oppgir vi verteksene i et tenkt lokalt koordinatsystem. Verteksene sine koordinater kalles da modellkoordinater. Vha. en modelltransformasjon kan vi posisjonere og orientere kuben i det globale koordinatsystemet (world space).

Hver enkelt modell har sin egen modelltransformasjon. Når alle modellene er transformert inn i det globale koordinatsystemet er alle i samme koordinatsystem. En modelltransformasjon består normalt av en eller flere av følgende i kombinasjon:

  • Translasjon (forflytning)
  • Rotasjon
  • Skalering

Det fine med dette er at man kan definere modellene uavhengig av hverandre for deretter å gjøre en modelltransformasjon slik at modellen plasseres i det globale systemet sammen med andre modeller.

Dersom man f.eks. definerer et hus spesifiserer man alle verteksene i modellkoordinater for deretter å gjøre nødvendige transformasjoner. Dette kan f.eks. bety å skalere huset opp eller ned, rotere og evt. flytte det dit man måtte ønske i forhold til det globale systemet. Se figuren under.

Modelltransformasjonen: translasjon og skalering

Den sammensatte transformasjonen representeres av en 4x4 matrise som multipliseres med alle verteksene.

Legg merke til at dersom man setter modellmatrisa lik identitetsmatrisa vil modellen «plasseres direkte» i det globale systemet.

Modellmatrisa = Identitetsmatrisa, dvs. ingen transformasjon.

Dette betyr at dersom man ikke angir en modellmatrise, eller den settes lik identitetsmatrisa, utføres ingen rotasjon, skalering eller translering av opprinnelig modell.

View-transformasjonen

Det finnes egentlig ikke noe kameraobjekt i WebGL/OpenGL ES. Vi tenker oss i stedet at vi plasserer et kamera et sted i det globale koordinatsystemet og orienterer det mot objektene/modellene som vi ønsker skal vises på skjermen. Se figuren under.

Plassering av «virtuelt» kamera

Vha. view-transformasjonen kan vi posisjonere og orientere dette «virtuelle» kameraet der vi måtte ønske. I utgangspunktet, dvs. dersom view-matrisa = identitetsmatrisa, står kameraet i origo og ser langs negativ z-akse. Dette betyr at kameraet i utgangspunktet ikke nødvendigvis vil se alle modeller/figurer. For at kameraet skal se modeller som er bak eller ved siden av kameraet må enten modellene flyttes foran kameraet eller så må kameraet flyttes og roteres slik at det ser modellene.

Ved hjelp av view-matrisa transformeres verteksene til modellen(e) motsatt i forhold til hvor du ønsker å flytte kamera (man kan tenke seg at kameraet står i origo og at modellene flyttes i forhold til dette).

Dersom man tenker seg at kameraet skal flyttes +5 enheter langs z-aksen vil det være det samme som om vi flyttet modellen(e) -5 enheter langs z-aksen. Modell- og view-transformasjonene er egentlig to sider av samme sak. Dette vil bli nærmere forklart i avsnittet om modelview-transformasjonen.

View-transformasjonen består egentlig av en kombinasjon av en translasjon og en rotasjon.

Etter view-transformasjonen er koordinatene transformert til øyekoordinater (og modellen er i «view space»).

Ved hjelp av funksjonen mat4.lookAt(parametre) kan man generere en View-matrise. Parametrene til denne funksjonen omfatter bl.a. kameraets posisjon og punktet det peker/ser mot.

Bruk av glMatrix-biblioteket (bruker også vec3-klassen):

let v3Eye = vec3.fromValues(7.0, 6.0, 16.0);  //Eksempler
let v3Look = vec3.fromValues(0.0, 0.0, 0.0);
let v3Up = vec3.fromValues(0.0, 1.0, 0.0);
let viewMatrix = mat4.create();
mat4.lookAt(viewMatrix, v3Eye,  v3Look , v3Up);

Bruk av cuon-matrix.js (Matsuda et al., 2013) biblioteket:

let viewMatrix = new Matrix4();
viewMatrix.setLookAt(eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);

Eye-koordinatene tilsvarer kameraets posisjon. Look- koordinatene tilsvarer punktet kameraet peker mot mens Up-koordinatene definerer hva som er opp/ned på kameraet (som regel upX=0, upY=1, upZ=0).

Når man spesifiserer viewmatrisa er det selvfølgelig viktig at kameraet peker mot modellen(e), hvis ikke vil den ikke vises på skjermen. Som vi straks skal se vil også projeksjonsmatrisa være med på avgrense hva som ender opp på skjermen.

Modelview-transformasjonen

Modell- og viewtransformasjonene er i praksis to like operasjoner i forhold til hvordan den endelige scenen blir seende ut. Det viktige er forholdet mellom kameraet og modellen(e) i scenen. Anta at et kamera og en modell som begge er plassert i origo og at kameraet ser mot negativ z-akse. Dersom kameraet skal «se» modellen må enten modellen flyttes mot negativ z eller kameraet mot positiv z. Begge operasjonene er i prinsippet samme ting.

Enten flytter man modellen i den ene retninga eller så flytter man kameraet i motsatt retning.

Begrepet «modelview» betyr at modell- og viewtransformasjonsmatrisene slås sammen til en matrise.

I enkelte tilfeller kan det være praktisk å håndtere disse som to separate matriser men de må uansett slås sammen for å få utført korrekt transformasjoner på verteksene. I praksis vil man ofte slå sammen disse to matrisene i Javascript-koden og sende resultatmatrisa (altså «modelview-matrisa») til verteksshaderen. Alternativt kan begge sendes inn til shaderen som da får ansvaret for å multiplisere disse sammen.

I tidligere versjoner av både standard OpenGL og ES-versjonen var det ikke støtte for programmerbare shadere («fixed function pipeline») og her opererte man kun med modelview-matrisa.

Projeksjonstransformasjonen

Denne transformasjonen skjer etter modelview-transformasjonen. Transformasjonen sørger for å projisere den delen av 3D-verdene/scenen som kameraet peker mot til en 2D-flate (skjermen). Den bestemmer også hvordan «viewvolumet» ser ut og begrenser dette til en «enhetskube» som strekker seg fra -1 til 1 i alle tre akser (x,y,z).

En vanlig analogi innen 3D grafikk er å sammenlikne modell-, view- og projeksjonstransformasjonene med det å ta et bilde med et kamera. Modelltransformasjonen tilsvarer det å plassere modellene i scenen. View-transformasjonen tilsvarer å plassere og orientere kameraet mot modellene(e) mens projeksjonstransformasjonen tilsvarer det å velge en linse til kameraet. Kameralinsa påvirker hva kameraet ser og til en viss grad hvordan modellene vil se ut.

Det finnes to typer projeksjoner:

  • Ortografisk projeksjon
  • Perspektivprojeksjon

Disse er omtalt under.

Ortografisk projeksjon

Ortografisk projeksjon kalles også parallell projeksjon siden parallelle linjer fortsatt er parallelle etter projeksjonen. Ortografisk projeksjon betyr også at størrelsen til modeller ikke påvirkes av avstanden til kameraet - man får dermed ingen dybdefølelse. Man får med andre ord mindre realisme i scenen som fremvises. 3D effekten blir mindre om ikke fraværende. Ortografisk projeksjon kan imidlertid være nyttig i CAD (Computer Aided Design) - systemer eller om man ønsker å lage 2D grafikk i WebGL.

Når ortografisk projeksjon brukes vil «view-volumet» være en rektangulær boks. Primitiver (trekanter) som faller innenfor volumet vil projiseres på «near clip plane» (se figur under) og vises i den endelige scenen. Primitiver som er delvis innenfor vil «klippes»/deles opp. Resten vil ikke vises, dvs. verteksene til primitiver som faller utenfor blir ikke sendt videre i «pipelinen».

Man kan f.eks. bruke mat4.ortho(…) funksjonen, definert i glMatrix, til å generere en ortografisk matrise:

mat4.ortho(projectionMatrix, left, right, bottom, top, near, far);

Dette vil gi en ortografisk projeksjonsmatrise, projectionMatrix, som vil sørge for at alle primitiver (/modeller) som ligger innafor volumet, skissert over, projiseres på «near plane» og dermed vil vises på skjermen.

Perspektivprojeksjon

Når man bruker perspektivprojeksjon vil modeller som er langt unna kamera virke mindre enn de som er nærmere kamera. Dette gir en mer realistisk fremstilling og dermed en bedre 3D illusjon siden det er slik våre øyne fungerer – ting som er langt unna virker mindre enn det som er rett ved siden av oss.

View-volumet som genereres ved perspektivprojeksjon kalles et frustum som ser ut som en avkappet pyramide.

View frustum

Vertekser (og dermed primitiver og objekter) som ligger helt eller delvis innenfor denne pyramiden representerer det som kameraet ser. Denne transformasjonen sørger dermed for to ting:

  • hvordan objekter projiseres til 2D (skjermen)
  • hvilke objekter eller deler av objekter som skal utelates i det endelige bildet

Etter transformasjonen vil aktuelle vertekser være projisert på near clip plane.

Det er to typer funksjoner man kan bruke til å generere en projeksjonsmatrise:

  • perspective() / setPerspective()
  • frustum()

Den første av disse har følgende parametre:

glMatrix-biblioteket:

let projectionMatrix = mat4.create();
mat4.perspective(projectionMatrix,fovy, aspect, near, far);

cuon-matrix.js:

let projectionMatrix = new Matrix4();
projectionMatrix.setPerspective(fovy, aspect, near, far);

Parametrene er som følger:

  • fovy: vertikal “field of view”. Merk: cuon-matrix sin setPerspective bruker grader, mens gl-matrix sin perspective() bruker radianer.
  • aspect: bredde til viewport / høyde til viewport (eks. canvas.width / canvas.height)
  • near: near clip plane (f.eks. 0.1)
  • far: far clip plane (f.eks. 1000)
  • projectionMatrix: resultatmatrisa etter kall på denne funksjonen

Se også figur under:

Matrisa kan beregnes slik:

Perspektivmatrise beregnet vha. setPerspective() (Matsuda et al., 2013)

Man kan også bruke frustum() funksjonen til å generere en perspektivmatrise. Denne har parametrene (glMatrix):

mat4.frustum(projectionMatrix, left, right, bottom, top, near, far);

Left, right, top og bottom tilsvarer bredde og høyde på near-plane, dvs størrelsen på området som 3D-verden skal projiseres ned på. frustum() er mer generisk enn perspective() og perspective() er implementert vha. frustum(). Cuon-matrix biblioteket har en tilsvarende funksjon.

Matrisa kan beregnes slik:

Perspektivmatrise beregnet vha. frustum() (Sellers et al., 2014)

Vi har nå en projeksjonsmatrise. Denne sendes inn til verteksshaderen, på samme måte som modelviewmatrisa. Se kodeeksemplet over.

Perspektivprojeksjon består egentlig av en kombinasjon av skalering og translasjon.

Oppsummert

Hele prosessen kan oppsummeres i følgende figur:

Figuren viser alle transformasjoner som utføres fra modellen spesifiseres vha. modellkoordinater til modellen ender opp på skjermen i skjermkoordinater. Modellkoordinatene multipliseres med modellmatrisa slik at de transformeres til "world" koordinater. Disse multipliseres med view-matrisa og gir "øye"-koordinater (NB! som regel utføres dette i et steg ved at modellkoordinatene multipliseres med modelview-matrisa). Disse multipliseres med projeksjonsmatrisa og verteksene er i "clip"-koordinater som også er det som verteksshaderen leverer fra seg. Resten håndteres av "pipelinen": Neste steg er å konvertere fra homogene koordinater (verteksshaderen opererer med homogene koordinater) til kartesiske koordinater ved å dele på w. Vi har nå normaliserte koordinater (NDC). Disse gjøres til slutt om til skjermkoordinater vha. "viewport"-transformasjonen.

Se Vedlegg1 for komplett gjennomgang og talleksempel.

Mer om clipping og culling

Vertekser og trekanter som faller innafor frustumet vil kunne ende opp på skjermen. En trekant kan ha tre posisjoner i forhold til frustumet:

  • Innenfor
  • Utenfor
  • Delvis innenfor

Når trekanten er innenfor frustumet vil den henge med til neste steg i pipelinen. Trekanter som faller (helt) utenfor ”culles” dvs. fjernes for videre behandling. Trekanter som faller delvis innenfor deles i to. Delen som faller innenfor beholdes mens den andre delen ”culles”.

Det kan nevnes at moderne grafikkort automatisk ”culler” det som faller utenfor frustumet. Men, og dette er viktig, det betyr ikke at WebGL-programmet automatisk vet hvilke objekter som ligger innafor frustumet og hvilke som ligger utenfor (dette bestemmes jo av View og projeksjonsmatrisene). Dette betyr igjen at dersom vi har en stor scene med mange objekter som skal tegnes og kameraet peker en vei vil tegnekommandoen (drawArrays()) bli utført for alle objekter uansett om de ligger innafor eller utenfor frustumet. Grafikkortet/pipelinen vil imidlertid sørge for å fjerne objekter som faller utenfor.

For mer effektiv tegning/rendring kan man allerede i WebGL-koden sørge for at det som faller utenfor frustumet ikke tegnes – dette kalles frustum culling. For enda bedre resultat kan man også sørge for ikke å tegne objekter som ligger bak andre objekter og som uansett ikke skal vises – kalles occlusion culling.

Siste del av graphics pipeline

Her omtales kort de siste blokkene i figuren (over) som viser OpenGL ES graphics pipeline.

Scissor test, Stencil test, Depth test, Blending

Den nest siste blokka kalles «Diverse fragment/piksel-operasjoner». Disse omtales kort her.

Selv om pikselfargene er beregnet og returnert fra fragmentshaderen kan fragmentet fortsatt bli forkastet pga. ulike tester som utføres i dette steget.

Scissor test:

Man kan sette et såkalt ScissorRectangle for å begrense tegning til en mindre del av skjermen/vinduet. Settes på GraphicsDevice-objektet.

Stencil test:

Bruker et spesielt buffer, et stencil buffer, og brukes i spesielle tilfeller til avanserte rendringsteknikker.

Depth test:

Ved hjelp av verteksene beregnes også dybdeinformasjon til hver enkelt fragment – hver enkelt fragment har tilknyttet dybdeinformasjon. Dette lagres i et eget buffer som kalles z-buffer eller bare dybde(depth) buffer. Dette er med på å avgjøre hvilke modeller som ligger fremst i bildet og dermed skal vises og hvilke som ikke skal med i det endelige bildet.

Alpha blending:

Dersom alpha blending benyttes vil fragmentets farge blandes med fargen til andre fragmenter (som ligger over eller under).

Frame buffer

I det siste steget i pipelinen lagres pikslene i frame bufret som også er det som til slutt vises på skjermen.

Eksempel: Fra modellkoordinater til skjermkoordinater

Se gjennomgang i Vedlegg1 . Her gjennomgås et eksempel med alle transformasjonene, med tallverdier, fra verteksene sendes inn til verteksshaderen til trekanten ender opp på skjermen.

En kube bestående av 12 trekanter og 36 vertekser

I dette avsnittet skal vi se hvordan man kan definere og tegne en kube som tegnes i perspektiv ved hjelp av view- og projeksjonsmatrisene. Sidene til kuben skal ha ulike farger og skal se omtrent slik ut.

Vi tar utgangspunkt i følgende figur for å definere nødvendige vertekser:

Her ser vi kuben og verteksposisjonene til kubens hjørner. Det er også antydet at hver side av kuben består av to trekanter. I tillegg er koordinataksene vist. Dersom kuben tegnes vha. drawArrays(gl.TRIANGLES, …) kan følgende verteksposisjonsarray benyttes:

//KUBEN:
//36 verteksposisjoner:
let cubeVertices = new Float32Array([
    //Forsiden (pos):
    -1, 1, 1,
    -1,-1, 1,
    1,-1, 1,

    -1,1,1,
    1, -1, 1,
    1,1,1,

    //Høyre side:
    1,1,1,
    1,-1,1,
    1,-1,-1,

    1,1,1,
    1,-1,-1,
    1,1,-1,

    //Baksiden (pos):
    1,-1,-1,
    -1,-1,-1,
    1, 1,-1,

    -1,-1,-1,
    -1,1,-1,
    1,1,-1,

    //Venstre side:
    -1,-1,-1,
    -1,1,1,
    -1,1,-1,

    -1,-1,1,
    -1,1,1,
    -1,-1,-1,

    //Topp:
    -1,1,1,
    1,1,1,
    -1,1,-1,

    -1,1,-1,
    1,1,1,
    1,1,-1,

    //Bunn:
    -1,-1,-1,
    -1,-1,1,
    1,-1,1,

    -1,-1,-1,
    1,-1,1,
    1,-1,-1
]);

For å få farge på kubens side angir vi også en farge per verteks vha. følgende array:

//36 verteksfarger, bruker ulike farger på hver side av kuben:
let colors = new Float32Array([
    //Forsiden:
    1.0, 0.0, 0.0, 1,
    1.0, 0.0, 0.0, 1,
    1.0, 0.0, 0.0, 1,

    1.0, 0.0, 0.0, 1,
    1.0, 0.0, 0.0, 1,
    1.0, 0.0, 0.0, 1,

    //Høyre side:
    0.0, 1.0, 0.0, 1,
    0.0, 1.0, 0.0, 1,
    0.0, 1.0, 0.0, 1,

    0.0, 1.0, 0.0, 1,
    0.0, 1.0, 0.0, 1,
    0.0, 1.0, 0.0, 1,

    //Baksiden:
    1.0, 1.0, 0.0, 1,
    1.0, 1.0, 0.0, 1,
    1.0, 1.0, 0.0, 1,

    1.0, 1.0, 0.0, 1,
    1.0, 1.0, 0.0, 1,
    1.0, 1.0, 0.0, 1,

    //Venstre side:
    0.0, 0.0, 1.0, 1,
    0.0, 0.0, 1.0, 1,
    0.0, 0.0, 1.0, 1,

    0.0, 0.0, 1.0, 1,
    0.0, 0.0, 1.0, 1,
    0.0, 0.0, 1.0, 1,

    //Topp
    1.0, 0.0, 1.0, 1,
    1.0, 0.0, 1.0, 1,
    1.0, 0.0, 1.0, 1,

    1.0, 0.0, 1.0, 1,
    1.0, 0.0, 1.0, 1,
    1.0, 0.0, 1.0, 1,

    //Bunn:
    0.5, 0.7, 0.3, 1,
    0.5, 0.7, 0.3, 1,
    0.5, 0.7, 0.3, 1,

    0.5, 0.7, 0.3, 1,
    0.5, 0.7, 0.3, 1,
    0.5, 0.7, 0.3, 1
]);

En kubeoppgave

Som vist kan man tegne en kube ved å angi 36 verteksposisjoner og farger og bruke drawArrays(gl.TRIANGLES…) til å tegne kuben. En annen mulighet er å bruke færre verteksposisjoner og drawArrays(gl.TRIANGLE_STRIP, …). Definer så få vertekser som nødvendig for å kunne tegne en kube vha. drawArrays(gl.TRIANGLE_STRIP, …)

Bruker vi indekser til å tegne kuben holder det å angi 8 vertekser i tillegg til 36 indekser.

Tegn samme kube vha. indekser og drawElements(). Bruk følgende figur som utgangspunkt for å definere kubevertekser og indekser:

Kuben består av 8 vertekser der en verteks har sin posisjon, f.eks. [-1,1,1]. Verteksene gjenbrukes vha. indekser. Hver side av kuben består av to trekanter definert av 6 indeksverdier som vist i lista til høyre i figuren. Fronten av kuben er f.eks. definert av trekantene tilsvarende indeksverdiene 0,1,2 og 0,2,3. Her angis verteks(indeksene) til trekantene mot klokka.

Modul 2.1: Lineær algebra for 3D grafikk

Modul 21 – Matematikk for 3D datamaskingrafikk

Dette er en kort introduksjon til matematikken som brukes i sammenheng med 3D datamaskingrafikk.

Nøkkelord: Lineær algebra, vektorer, matriser, transformasjoner, matrisemultiplikasjon, normalvektor, normalisering, invers matrise, transponert matrise.

Vektorer

Vektorer har en avgjørende rolle i forbindelse med bl.a. grafikk, animasjoner, kollisjonsdeteksjon og ulike fysiske simuleringer. En vektor har både en retning (direction) og en størrelse (magnitude).

Enheter som i sin natur består av både retning og størrelse kaller vi gjerne en vektorstørrelse (vector valued quantities). Eksempler er kraft (har en retning og ”styrke”), forskyvning (retning og avstand), fart (retning og hastighet).

Vi kan også bruke vektorer til å beskrive kun retning – for eksempel hvilken retning vi ser inn i en spillverden, hvilken retning en polygon har, fra hvilken retning lyset kommer m.m.

Vektorer tegnes gjerne som piler med en retning og en lengde som vist i figuren under.

To vektorer tegnet som piler med retning og lengde

I prinsippet er to vektorer med samme retning og lengde lik - i figuren er \(\vec{u}\) og \(\vec{v}\) lik siden de har samme retning og lengde. Plassering av vektorene er (i prinsippet) ikke viktig – vi kan godt flytte på \(\vec{v}\) og fortsatt si at \(\vec{u} = \vec{v}\).

For at vi skal kunne jobbe med vektorer må vi imidlertid kunne kvantifisere vektorstørrelsene (størrelse og retning). Vi plasserer dem derfor inn i et 3D koordinatsystem slik at ”halen” til vektorene starter i origo (standardposisjon).

Vi kan nå angi enhver vektor ved å spesifisere koordinatene til ”vektorhodet”, slik: \(\vec{v} = (x, y, z)\). Dette betyr at vi kan spesifisere en vektor vha. tre flyttall.

To vektorer som har samme lengde og retning kan ”plasseres” i to ulike koordinatsystemer og fortsatt være like. Dette er analogt med for eksempel temperatur – vann har samme kokepunkt enten vi spesifiserer dette i Celcius (100) eller Fahrenheit (212).

Samme vektor angitt i to ulike koordinatsystemer

På samme måte kan vi bruke ulike måleskalaer for å måle vind (m/s, km/t). Vinden blåser i en bestemt retning med en bestemt hastighet. Vi kan måle dette i forhold til ulike referanserammer (eller koordinatsystemer) men vinden har uansett samme retning og hastighet.

Dette er viktig og betyr at når vi kvantifiserer en vektor vha. koordinatverdier, er disse i forhold til en referanseramme, dvs. et koordinatsystem. I vår sammenheng (grafikk) er det ofte behov for å utnytte ulike referanserammer. Det er derfor viktig å holde rede på hvilken referanseramme/koordinatsystem en vektor er definert i. I tillegg må vi vite hvordan vi kan konvertere vektorer mellom de ulike koordinatsystemene.

Legg merke til at både vektorer og punkter kan beskrives vha. koordinatene \((x,y,z)\) i forhold til et koordinatsystem. Matematisk er de allikevel ikke det samme – et punkt representerer en lokasjon i rommet mens en vektor representerer en størrelse og en retning.

Venstrehånds- og høyrehånds koordinatsystemer

WebGL bruker et såkalt høyrehånds koordinatsystem. Dersom du vender innsiden av høyre hånd opp, strekker tommelen rett ut og vender resten av fingrene opp (90 grader i forhold til håndflaten) vil tommelen peke i positiv Z-retning, resten av fingrene peke i positiv Y-retning mens hele hånden peker i positiv X-retning (se figur under). Tilsvarende for venstrehåndssystem.

Høyrehånds koordinatsystem
Venstrehånds- og høyrehånds koordinatsystem

Dette betyr at når vi definerer 3D modeller i WebGL angir vi punktene i forhold til et høyrehåndssystem. OpenGL bruker også høyrehåndssystemet mens Direct3D bruker venstrehåndssystemet.

Grunnleggende vektoroperasjoner

To vektorer er like dersom tilsvarende komponenter er like:

La \(\vec{u} = (u_x, u_y, u_z)\) og \(\vec{v} = (v_x, v_y, v_z)\) være to vektorer:

\(\vec{u}\) og \(\vec{v}\) er like dersom \(v_x = u_x\), \(v_y = u_y\) og \(v_z = u_z\)

Vi legger sammen to vektorer slik:

La \(\vec{u} = (u_x, u_y, u_z)\) og \(\vec{v} = (v_x, v_y, v_z)\) være to vektorer. En vektoraddisjon er definert slik:

\( \vec{u} + \vec{v} = (u_x + v_x,\; u_y + v_y,\; u_z + v_z) \)
Vektoraddisjon vist geometrisk

Vi multipliserer en vektor med en skalarverdi slik:

La \(k\) være en skalarverdi, f.eks. -0,5, og \(\vec{v} = (v_x, v_y, v_z)\), f.eks. \((2,1)\). Produktet av k og \(\vec{v}\) blir da:
\( k\,\vec{v} = (k\,v_x,\; k\,v_y,\; k\,v_z) \)

Skalering av en vektor med en skalarverdi

Vi kan definere vektorsubtraksjon vha. vektoraddisjon.

\( \vec{u} - \vec{v} = \vec{u} + (-1 \cdot \vec{v}) = \vec{u} + (-\vec{v}) \)
Skalering av en vektor med en skalarverdi

Vi kan enkelt utføre grunnleggende vektoroperasjoner i Matlab eller Octave (http://www.gnu.org/software/octave/). Under ser du et eksempel på dette. I kommandovinduet til Matlab legger man inn vektorer (i form av Matlab-variabler) samt utfører aktuell operasjon. Eksemplet under viser addisjon av to vektorer.

Vi starter med å angi to vektorer:

>>u=[1 2];
>>v=[2 0.5];
>>u+v
ans = 3.0 2.5

Vi oppgir med andre ord vektorer vha. klammeparenteser og skiller verdiene med et mellomrom. Når vi skriver u+v og trykker enter beregnes svaret.

Lengde og normalisering

Lengden til en vektor beregnes slik (Pytagoras):

\[ \left\lVert \vec{u} \right\rVert = \sqrt{x^{2} + y^{2} + z^{2}} \]

I enkelte sammenhenger er vi ikke interessert i lengden men ønsker at vektoren skal ha lengden 1. Dette kalles normalisering. Vi normaliserer en vektor slik:

\[ \frac{\vec{u}}{\left\lVert \vec{u} \right\rVert} = \left( \frac{x}{\left\lVert \vec{u} \right\rVert},\; \frac{y}{\left\lVert \vec{u} \right\rVert},\; \frac{z}{\left\lVert \vec{u} \right\rVert} \right) \]

Dette betyr at x,y og z-verdiene vil få verdier mellom 0 og 1 mens lengden på vektoren blir lik 1.

Prikkprodukt

Prikkproduktet (kalles også indreprodukt eller skalarprodukt) mellom to vektorer er en form for vektormultiplikasjon som resulterer i en skalarverdi. La \(\vec{u} = (u_x, u_y, u_z)\) og \(\vec{v} = (v_x, v_y, v_z)\) være to vektorer. Prikkproduktet mellom disse er da:

\[ \vec{u} \cdot \vec{v} = u_x \cdot v_x + u_y \cdot v_y + u_z \cdot v_z \]

Prikkproduktet er altså summen av produktene av enkeltkomponentene.

Dette har tilsynelatende liten mening. Vha. cosinus-setningen kan det vises at prikkproduktet også kan beregnes slik:

\[ \vec{u} \cdot \vec{v} = \left\lVert \vec{u} \right\rVert \left\lVert \vec{v} \right\rVert \cos\theta \]

Dette kan dermed brukes til å finne vinkelen mellom to vektorer:

\[ \cos\theta = \frac{\vec{u} \cdot \vec{v}} {\left\lVert \vec{u} \right\rVert \left\lVert \vec{v} \right\rVert} \]

Matlab:

Her kan vi angi vektorene slik:

>>u=[-4 0 -1];
>>v=[1 2 3];
>>dot(u,v)
ans = -7

Her oppgir vi først to vektorer (med navn u og v). Deretter beregnes prikkproduktet som gir en skalarverdi som svar (-7).

Videre finner vi vinkelen mellom disse slik:

>> ul=sqrt(u(1,1)*u(1,1) + u(1,2)*u(1,2) + u(1,3)*u(1,3));
>> vl=sqrt(v(1,1)*v(1,1) + v(1,2)*v(1,2) + v(1,3)*v(1,3));
>> dp=dot(u,v);
>> cosFi= dp /(ul*vl);
>> acosd(cosFi)
ans = 116.9841

Kryssproduktet

Kryssproduktet mellom to vektorer evaluerer til en ny vektor. Kryssproduktet mellom to 3D- vektorer gir en ny vektor som er gjensidig ortogonal til de to opprinnelige vektorene. Dette betyr at den nye vektoren står vinkelrett på flaten definert av de to opprinnelige vektorene.

Kryssproduktet er definert slik:

\[ \vec{w} = \vec{u} \times \vec{v} = \bigl( (u_y v_z - u_z v_y),\; (u_z v_x - u_x v_z),\; (u_x v_y - u_y v_x) \bigr) \]
Kryssproduktet gir en vektor ortogonal på de to opprinnelige vektorene

Dersom du med din høyre hånd peker pekefingeren i retning av den første vektoren, \(\vec{u}\), og ringfingeren i retning av den andre vektoren, \(\vec{v}\), vil tommelen peke i \(\vec{w}\) sin retning. Dette kalles høyrehåndsregelen for kryssprodukt.

Kryssproduktet gir en vektor ortogonal på de to opprinnelige vektorene

Kryssproduktet er ikke-kommutativt, dvs. \(\vec{u} \times \vec{v}\) er ikke det samme som \(\vec{v} \times \vec{u}\). Det kan imidlertid vises at \(\vec{u} \times \vec{v} = -\vec{v} \times \vec{u}\)

Kryssproduktet kan for eksempel brukes til å finne normalvektorer som er aktuell i forbindelse med lysberegning i WebGL.

Matlab:

Vi starter med å angi to vektorer:

>>u=[2 1 3];
>>v=[2 0 0];
>>w=cross(u,v)
w = 0 6 -2
>> y=cross(v,u)
y = 0 -6 2

Her beregnes kryssproduktet mellom u og v samt v og u. Vi ser at \(\vec{u} \times \vec{v} = -\vec{v} \times \vec{u}\).

Punkter

Vi kan også bruke en vektor til å spesifisere en 3D-lokasjon i rommet – vi kaller dette en posisjonsvektor eller bare punkt. I dette tilfellet er det lokasjonen til ”vektorhodet” som er av interesse – ikke lengden eller retninga.

Vektorer, Javascript & WebGL

Det finnes flere gode Javascript-biblioteker som inneholder vektor-klasser som vi kan bruke.

Noen eksempler:

  • Sylvester (Sylvester, 2012)
  • WebGL-mjs (WebGL-mjs, 2010)
  • glMatrix (glMatrix)
  • cuon-matrix.js (Matsuda, K. et al, 2013)

I følgende eksempler bruker vi Sylvester-bibilioteket fra Javascript-koden.

Vektorsum

Anta at vi skal summere vektorene \(\vec{u} = (5, 3, 0)\) og \(\vec{v} = (4, -2, 0)\).

let u = Vector.create([5.0, 3.0, 0.0]);
let v = Vector.create([4.0, -2.0 , 0.0]);
let sumV = u.add(v);

Dette vil gi \(v_{\text{sum}} = (9, 1, 0)\).

Vektorsubtraksjon

let u = Vector.create([5.0, 3.0, 0.0]);
let v = Vector.create([4.0, -2.0 , 0.0]);
let vdif = u.subtract(v);

Dette vil gi \(v_{\text{dif}} = (1, 5, 0)\).

Skalering av vektorer

Skalering av en vektor betyr at vi multipliserer vektoren med en skalarverdi.

let a = Vector.create([9.0, 1.0, 0.0]);
let fscale = 2.0;
let vprod = a.multiply(fscale);

Dette vil gi \(v_{\text{prod}} = (18, 2, 0)\).

Multipliserer man vektoren med et tall mindre enn 1 nedskalerer man vektoren.

Finne normalen mellom to vektorer

En normalvektor er en vektor som står vinkelrett (ortogonal) på en flate. En normal representerer en retning noe som betyr at posisjonen ikke er viktig. I forbindelse med grafikk er det ofte aktuelt å beregne normalvektorer i forbindelse med lysberegning.

Man kan for eksempel finne normalen til en flate representert av to vektorer som har samme ”startpunkt”. Se figuren under.

Normalen til en flate representert av to vektorer med samme startpunkt

Vi kan bruke kryssproduktet til å beregne normalvektoren til en flate definert av vektorene v1 og v2. Eksempel:

let v1 = Vector.create([0.0, 2.0, 0.0]);
let v2 = Vector.create([3.0, 1.0 , 0.0]);
let w = v1.cross(v2);   //= v1 x v2

Dette betyr at man multipliserer vektorene slik: \(w = v1 \times v2\) som gir vektoren \([0,0,-6]\) som resultat.

\[ \begin{aligned} \vec{w} = \vec{v1} \times \vec{v2} &= \bigl( (v1_y\,v2_z - v1_z\,v2_y),\; (v1_z\,v2_x - v1_x\,v2_z),\; (v1_x\,v2_y - v1_y\,v2_x) \bigr) \\ &= \bigl( (2 \cdot 0 - 0 \cdot 1),\; (0 \cdot 3 - 0 \cdot 0),\; (0 \cdot 1 - 2 \cdot 3) \bigr) \\ &= \underline{\underline{(0, 0, -6)}} \end{aligned} \]

Dette stemmer også med høyrehåndsregelen for kryssprodukt (se punkt over om kryssprodukt). Det motsatte, dvs: w = v2.cross(v1) gir \(w = [0,0,6]\), altså en vektor som peker i motsatt retning.

Normalisering av en vektor

Ofte er det behov for å skalere vektorer til enhetsvektorer. Dette betyr at vektorverdiene (x,y og z) skaleres til verdier mellom 0 og 1 men der forholdet mellom dem fortsatt er det samme som i den opprinnelige vektoren. Lengden på vektoren blir lik 1. Dette gjøres ved å dividere enkeltkomponentene i vektoren med lengden på vektoren (se tidligere beskrivelse). I Javascript (med Sylvester-biblioteket, se over) kan man gjøre følgende:

let v3 = Vector.create([5.0, 2.0 , 1.0]);
let v3unit = v3.toUnitVector();
let length = v3unit.distanceFrom([0,0,0]);

Legg merke til hvordan lengden på vektoren kan beregnes vha. distanceFrom() metoden. Her beregenes lengden fra [0,0,0] for å sjekke at vektoren faktiska har lengde = 1.

Finne vinkelen mellom to vektorer – prikkprodukt

Vi kan bruke prikkproduktet til å finne vinkelen mellom to vektorer. Prikkproduktet kan for eksempel brukes til å beregne kulebaner, refleksjonsvinkler eller lysintensitet. Formelen for prikkproduktet ble vist i et tidligere kapittel. Javscript (m/Sylvester):

let u1 = Vector.create([0.0, 5.0, 0.0]);
let v1 = Vector.create([5.0, 5.0, 0.0]);
let dp1 = u1.dot(v1);                 //=25
let u1L = u1.distanceFrom([0,0,0,]);  //Lengden til u1
let v1L = v1.distanceFrom([0,0,0,]);  //Lengden til v1
let cosFi = dp1 / (u1L * v1L);        //Vinkelen mellom dem!
let angle = toDegrees(Math.acos(cosFi));

. . .

function toDegrees (angle) {
    return angle * (180 / Math.PI);
}

Dette vil gi angle lik 45 grader som er vinkelen mellom vektor u1 og v1.

Matriser og transformasjoner

Innenfor området 3D datamaskingrafikk er matriser fundamentale. Matriser brukes til ulike transformasjoner som å flytte (translere), rotere eller skalere 3D modeller samt til å simulere kamera og gjøre ulike projeksjoner.

Dersom man skal få et bedre ”grep” om, samt utnytte mulighetene i forbindelse med «3D-programmering», er det er viktig å forstå grunnleggende matrisematematikk.

De nevnte Javascript-bibliotekene (Sylvester m.fl., se over) har mange ferdige klasser og funksjoner som vi kan bruke til dette.

Essensen er at vi bruker matriser til å transformere posisjonsvektorer (punkter). Multipliserer vi en posisjonsvektor med en matrise får vi en ny og transformert posisjonsvektor. 3D modeller beskrives vha. et sett punkter (vertekser) i 3D rommet. Dersom vi transformerer alle punktene til modellen vha. samme matrise har vi transformert hele modellen.

En matrise er en samling med (flyt)tall organisert i rader og kolonner. Produktet av antall rader og kolonner gir matrisas dimensjon. Tallene i matrisa kalles elementer. Vi angir elementene i matrisa vha. en indeks, slik; \(M_{ij}\) der \(i\) angir rad og \(j\) kolonne for element \(M\).

Man indekserer for eksempel elementene i en (4x4) matrise som vist under:

\[ \begin{bmatrix} M_{11} & M_{12} & M_{13} & M_{14} \\ M_{21} & M_{22} & M_{23} & M_{24} \\ M_{31} & M_{32} & M_{33} & M_{34} \\ M_{41} & M_{42} & M_{43} & M_{44} \end{bmatrix} \]

Under er det vist noen eksempler på matriser:

\[ A = \begin{bmatrix} 3{,}5 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0{,}5 & 0 \\ 2 & -5 & 3 & 1 \end{bmatrix} \qquad B = \begin{bmatrix} 3 & 1 \\ 2 & 1 \\ 1 & 0 \end{bmatrix} \qquad \vec{u} = \begin{bmatrix} u_1 & u_2 & u_3 & u_4 \end{bmatrix} \qquad \vec{v} = \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} \]

Matrise A er en 4x4 matrise, B en 3x2, u en 1x4 og v en 3x1 matrise. Her er u og v spesielle matriser som består av henholdsvis en rad og en kolonne. Disse kalles gjerne rad og kolonnevektorer siden de kan brukes til å representere vektorer på matriseform (vi kan derfor velge om vi ønsker å bruke notasjonen \((x,y,z)\) eller \([x, y, z]\)).

Matriser kan enten være basert på radvektorer eller kolonnevektorer. I en radvektor-basert matrise kan vi se på hver rad som en vektor mens i en kolonnevektorbasert matrise er hver kolonne en vektor.

To matriser er like dersom alle elementene i begge matriser er like. Vi adderer matriser (av samme dimensjon) ved å addere enkeltelementene. Vi multipliserer en matrise med en skalarverdi ved å multiplisere hver enkelt element med skalarverdien. Subtraksjon uttrykkes vha. addisjon, slik: \(A - B = A + (-1 \cdot B) = A + (-B)\).

Matrisemultiplikasjon

I dette avsnittet skal vi se på hvordan man kan multiplisere matriser med hverandre og vektorer med matriser.

Matrise * matrise

Dette brukes for eksempel i de tilfeller der man skal utføre sammensatte transformasjoner som for eksempel en translasjon (forflytning) etterfulgt av en rotasjon.

Dersom A en en \(m \times n\) matrise og B er en \(n \times p\) matrise er produktet AB definert og gir en \(m \times p\) matrise, C. I C er \(m_{ij}\) lik prikkproduktet av radvektor \(i\) i A og kolonnevektor \(j\) i B, dvs.

\[ C_{ij} = \vec{u}_{\text{rad}i} \cdot \vec{v}_{\text{col}j} \]

Eksempel:

\[ A = \begin{bmatrix} -1 & 5 & -4 \\ 3 & 2 & 1 \end{bmatrix} \qquad B = \begin{bmatrix} 2 & 1 & 0 \\ 0 & -2 & 1 \\ -1 & 2 & 3 \end{bmatrix} \] \[ AB = \begin{bmatrix} (-1,5,-4)(2,0,-1) & (-1,5,-4)(1,-2,2) & (-1,5,-4)(0,1,3) \\ (3,2,1)(2,0,-1) & (3,2,1)(1,-2,2) & (3,2,1)(0,1,3) \end{bmatrix} = \begin{bmatrix} 2 & -19 & -7 \\ 5 & 1 & 5 \end{bmatrix} \]

Produktet AB er nå definert som en 2*3 matrise (multipliserer antall rader i den første ganger antall kolonner i den andre).

Legg merke til at BA ikke er definert siden antall kolonner i B ikke matcher antall rader i A.

Matlab:

Vi angir matrisene slik:

>>A=[-1 5 -4 ; 3 2 1];
>>B=[2 1 0 ; 0 -2 1 ; -1 2 3];
>>A*B

Ans =

     2   -19    -7
     5     1     5

Vektor * matrise

Dette er egentlig bare et spesialtilfelle av vanlig matrise-multiplikasjon.

Anta følgende multiplikasjon:

\[ \vec{u}B = \begin{bmatrix} x & y & z \end{bmatrix} \begin{bmatrix} v_{11} & v_{12} & v_{13} \\ v_{14} & v_{15} & v_{16} \\ v_{17} & v_{18} & v_{19} \end{bmatrix} \]

Observer at dette resulterer i en 1*3 radvektor:

\[ \vec{u}B = \begin{bmatrix} \vec{u} \cdot \vec{v}_{\text{col}1} & \vec{u} \cdot \vec{v}_{\text{col}2} & \vec{u} \cdot \vec{v}_{\text{col}3} \end{bmatrix} \]

Identitetsmatrisa

Dette er en kvadratisk matrise som har verdien 1 i diagonalen og 0 ellers.

\[ M \cdot I = I \cdot M = M \]

Multiplikasjon med en identitetsmatrise gir ingen endring på opprinnelig matrise.

Inverse matriser

Den inverse av en kvadratisk matrise er definert som \(A^{-1}\) og har følgende egenskap:

\[ A^{-1} \cdot A = I \]

der \(I\) er identitesmatrisa. For at en kvadratisk matrise skal ha en tilsvarende invers må den ha visse egenskaper.

Dersom man har en rotasjonsmatrise og inverterer den vil den inverse gi en rotasjon i motsatt retning, den inverse av en skaleringsmatrise som skalerer opp en modell vil skalere modellen ned igjen osv.

Eksempel:

Den inverse av følgende matrise (som er en «translasjonsmatrise»)

\[ \begin{bmatrix} 1 & 0 & 0 & x \\ 0 & 1 & 0 & y \\ 0 & 0 & 1 & z \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

vil være:

\[ \begin{bmatrix} 1 & 0 & 0 & -x \\ 0 & 1 & 0 & -y \\ 0 & 0 & 1 & -z \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Ting blir mer utfordrende når vi har sammensatte transformasjoner – å finne den inverse blir ikke lenger så enkelt. Inversen kan regnes ut på ulike måter som vi ikke skal gå inn på her siden det normalt vil omfatte en hel forelesning i lineær algebra. Se f.eks. (Weisstein, E.W., 2011) for mer informasjon om inverse matriser.

Transponert matrise

Den transponerte matrisen \(A^{T}\) er definert ved at rader i den opprinnelige matrisen \(A\) blir kolonner i den transponerte matrisen. Etter hvert vil vi se at «transpornering» og invertering av matriser brukes til å transformere normalvektorer.

2D transformasjoner

Før vi går inn på 3D transformasjoner skal vi så på grunnleggende 2D transformasjoner.

2D-skalering vha. en matrise

Anta at vi har et 2D punkt angitt som \((x,y)\) som skaleres med faktorene \(a\) og \(b\). Dette betyr at det skalerte punktet blir \((ax, by)\).

Dette kan settes opp som en matrisemultiplikasjon slik:

\[ \begin{bmatrix} a & 0 \\ 0 & b \end{bmatrix} \bullet \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} ax \\ by \end{bmatrix} \]

Vektoren bak = tegnet tilsvarer nå det skalerte punktet.

2D-rotasjoner vha. matriser

Man kan rotere et punkt \((x,y)\) med en vinkel \(\theta\) til et punkt \((x', y') = (x\cos\theta - y\sin\theta,\; x\sin\theta + y\cos\theta)\). Se også figur under.

Teorien bak dette er som følger:

Bruker addisjonsformlene for sinus og cosinus (angle sum formulas):

\[ \begin{aligned} \sin(\alpha + \theta) &= \sin\alpha \cos\theta + \cos\alpha \sin\theta \\ \cos(\alpha + \theta) &= \cos\alpha \cos\theta - \sin\alpha \sin\theta \end{aligned} \]

Enkel pytagoras tilsier (se figur under):

\[ \begin{aligned} x &= r\cos\alpha \\ y &= r\sin\alpha \\ \end{aligned} \]

Bruker addisjonsformlene til å sette opp tilsvarende uttrykk for x' og y':

\[ \begin{aligned} x' &= r\cos(\alpha + \theta) = r\cos\alpha \cos\theta - r\sin\alpha \sin\theta \\ y' &= r\sin(\alpha + \theta) = r\sin\alpha \cos\theta + r\cos\alpha \sin\theta \end{aligned} \]

Setter inn x og y i formlene:

\[ \begin{aligned} x' &= x\cos\theta - y\sin\theta \\ y' &= x\sin\theta + y\cos\theta \end{aligned} \]

Forklaring: Ved å erstatte \(r\cos\alpha\) med \(x\) og \(r\sin\alpha\) med \(y\) kan vi sette \(x' = r\cos(\alpha + \theta)\) og \(y' = r\sin(\alpha + \theta)\). Dette gir uttrykkene for \(x'\) og \(y'\) vist over.

2D-rotasjon av et punkt om origo

Vi kan nå sette dette opp som en matrisemultiplikasjon slik:

\[ \begin{bmatrix} x' \\ y' \end{bmatrix} = \begin{bmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{bmatrix} \bullet \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} x\cos\theta - y\sin\theta \\ x\sin\theta + y\cos\theta \end{bmatrix} \]

2D-translasjon vha. en matrise

Translasjon (forflytning) av et punkt \(p = (x,y)\) tilsvarende en gitt vektor \(v = (s,t)\) innebærer en vektorsummering:

\[ (x,y) + (s,t) = (x+s,\; y+t) \]

Dette bryter med de andre transformasjonene (skalering og rotasjon) som enkelt lar seg uttrykke og kombinere vha. matrisemultiplikasjon:

  • Skalering: \(p' = M_{\text{scale}} \cdot p\)
  • Rotasjon: \(p' = M_{\text{rotation}} \cdot p\)
  • Translasjon: \(p' = v + p\) (der \(v\) er translasjonssvektoren).

Translasjon håndteres med andre ord ulikt de andre transformasjonene.

For å løse dette brukes såkalte homogene koordinater. Dette innebærer at man legger til en ekstra koordinatverdi, \(w\). 2D punktet angis dermed slik: \((x,y,w)\). Dette betyr at tilsvarende kartesiske koordinater blir \((x/w, y/w)\). Dersom \(w = 0\) representerer dette et punkt i uendeligheten og bør unngås dersom punktet representerer en lokasjon. Settes \(w = 1\) oppnår vi det vi ønsker:

En translasjon tilsvarende

\[ (x,y,1) + (s,t,0) = (x+s,\; y+t,\; 1) \]

kan nå representeres som en matrise slik:

\[ \begin{bmatrix} 1 & 0 & s \\ 0 & 1 & t \\ 0 & 0 & 1 \end{bmatrix} \bullet \begin{bmatrix} x \\ y \\ 1 \end{bmatrix} = \begin{bmatrix} x+s \\ y+t \\ 1 \end{bmatrix} \]

Vi kan nå bruke matrisemultiplikasjon til å translere punkter. Matrisene for skalering og rotasjon utvides på samme måte til 3x3 matriser slik at de tre typene transformasjoner kan håndteres på en ensartet måte.

Dette betyr at man kan lage sammensatte transformasjoner ved å multiplisere matrisene med hverandre – noe som går bra siden alle typer transformasjoner kan representeres av 3x3 matriser.

Eksempel:

Dersom man har en translasjonsmatrise \(M_t\) og en rotasjonsmatrise \(M_r\) slås disse sammen vha. matrisemultiplikasjon, \(M = M_t \cdot M_r\). Matrisen \(M\) kan nå brukes til å transformere vektorene. Eksempel: \(V_{\text{trans}} = v \cdot M\).

Transformasjonsmatrisene for 3D

Her vil vi presentere de grunnleggende matrisene som brukes av OpenGL ES og WebGL til å skalere, rotere og translere (forflytte) 3D objekter i rommet. Translasjon i 3D fungerer på samme måte som i 2D ved å bruke homogene koordinater \((x,y,z,w)\).

Skalering fungerer også tilsvarende skalering for 2D. Når det gjelder rotasjon må man i tillegg angi aksen som objektet skal roteres om (x,y eller z).

For å kunne kombinere alle typer transformasjoner vha. matrisemultiplikasjon er også skalering- og rotasjonsmatrisene utvidet til 4x4 matriser.

Skalering

Anta at vi ønsker å skalere en modell til det dobbelte av den opprinnelige modellen. Følgende matrise er definert og brukes til skalering av punkter:

\[ S = \begin{bmatrix} S_x & 0 & 0 & 0 \\ 0 & S_y & 0 & 0 \\ 0 & 0 & S_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Setter man \(S_x = 2\), \(S_y = 2\) og \(S_z = 2\) skaleres modellen i alle retninger til det dobbelte. Se figuren under:

Modell skalert med faktor 2 i alle retninger
Skalerer med faktor 2

Rotasjoner

Følgende matrise er definert og brukes til rotasjon rundt x-aksen:

\[ R_x = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Følgende matrise er definert og brukes til rotasjon rundt y-aksen:

\[ R_y = \begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Følgende matrise er definert og brukes til rotasjon rundt z-aksen:

\[ R_z = \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Translasjon og bruk av homogene koordinater

Som vi ser bruker OpenGL ES 4x4 matriser. Dette er (tilsvarende som for 2D) nødvendig for at matrisene skal kunne kombineres med translasjon (forflytning). Poenget er at vi ønsker samme format på matrisene for alle typer transformasjoner slik at det er enkelt å kombinere disse.

Translasjonsmatrisa er definert som følger:

\[ T = \begin{bmatrix} 1 & 0 & 0 & b_x \\ 0 & 1 & 0 & b_y \\ 0 & 0 & 1 & b_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

der \(b\) tilsvarer forflytning i x,y,z retning.

Eksempel:

Anta at vi har et kvadrat avgrenset av minimumspunktet \((-8,2,0)\) og maksimumspunktet \((-2, 8, 0)\). Anta videre at vi ønsker å translere dette 12 enheter på x-aksen, -10 enheter på y-aksen og 0 på z-aksen. Translasjonsmatrisa blir som følger:

\[ T = \begin{bmatrix} 1 & 0 & 0 & 12 \\ 0 & 1 & 0 & -10 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Gjør om punktene til homogene koordinater ved å setter \(w = 1\). Dette gir:

\[ \begin{bmatrix} 1 & 0 & 0 & 12 \\ 0 & 1 & 0 & -10 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \bullet \begin{bmatrix} -8 \\ 2 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 4 \\ -8 \\ 0 \\ 1 \end{bmatrix} \] \[ \begin{bmatrix} 1 & 0 & 0 & 12 \\ 0 & 1 & 0 & -10 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \bullet \begin{bmatrix} -2 \\ 8 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 10 \\ -2 \\ 0 \\ 1 \end{bmatrix} \]

Resultatet illustrert:

Kvadrat før og etter translasjon
Translasjon

Bruk av 4x4 matriser gjør det mulig å kombinere transformasjonsmatrisene på en ensartet måte ved hjelp av matrisemultiplikasjon. Skal man gjøre en rotasjon og en translasjon lager man to matriser og multipliserer disse. Dette gir en ny matrise som representerer den totale transformasjonen. Legg merke til at rekkefølgen på matrisemultiplikasjonen er avgjørende (\(A \cdot B\) er ulik \(B \cdot A\)).

Sammensatte transformasjoner

Som tidligere antydet kan man eksempelvis kombinere en translasjon med en rotasjon – vi lager da en translasjonsmatrise og en rotasjonsmatrise. Vi kombinerer disse vha. en matrisemultiplikasjon.

Legg merke til at rekkefølgen av slike matrisemultiplikasjoner er avgjørende;

\[ A \cdot B \neq B \cdot A \quad \text{(der A og B er matriser).} \]

Eksempel: Anta at punktet P skal transleres tilsvarende [12,-10,0] og roteres 35 grader om X-aksen.

Her lages to matriser som slås sammen før resultatmatrisa multipliseres med punktet P. Matrisa \(M_{\text{translate\_rotateX}}\) tilsvarer her produktet av \(M_{\text{translate}}\) og \(M_{\text{rotateX}}\). Punktet P vil i dette tilfellet først transleres og deretter roteres. Resultatet er punktet \(P_{\text{transformed}}\).

Legg merke til at matrisene kombineres (multipliseres) til en ny matrise før denne multipliseres med punktet. Den nye matrisen representerer begge transformasjonene.

\[ P = \begin{bmatrix} 3 \\ 4 \\ 5 \\ 1 \end{bmatrix} \] \[ M_{\text{translate}} = \begin{bmatrix} 1 & 0 & 0 & 12 \\ 0 & 1 & 0 & -10 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \] \[ M_{\text{rotateX}} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos 35 & -\sin 35 & 0 \\ 0 & \sin 35 & \cos 35 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \] \[ \begin{aligned} M_{\text{translate\_rotateX}} &= M_{\text{translate}}\,M_{\text{rotateX}} = \begin{bmatrix} 1 & 0 & 0 & 12 \\ 0 & 1 & 0 & -10 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos 35 & -\sin 35 & 0 \\ 0 & \sin 35 & \cos 35 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \\[6pt] &= \begin{bmatrix} 1 & 0 & 0 & 12 \\ 0 & 0.819 & -0.57357 & -10 \\ 0 & 0.57357 & 0.819 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{aligned} \] \[ P_{\text{transformed}} = M_{\text{translate\_rotateX}}\,P = \begin{bmatrix} 1 & 0 & 0 & 12 \\ 0 & 0.819 & -0.57357 & -10 \\ 0 & 0.57357 & 0.819 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 3 \\ 4 \\ 5 \\ 1 \end{bmatrix} = \begin{bmatrix} 15 \\ -9.5912 \\ 6.3900 \\ 1 \end{bmatrix} \]

Alternativet er at punktet først multipliseres med translasjonsmatrisa for deretter å multipliseres med rotasjonsmatrisa. Dette vil også fungere men dersom vi skal transformere en modell som består at mange punkter vil dette være lite effektivt da det krever (omtrent) dobbelt så mange matrisemultiplikasjoner i forhold til om matrisene først multipliseres sammen til en ny kombinert matrise. I det første tilfellet må det utføres \(N \cdot 2\) vektor/matrise- multiplikasjoner mens det i det andre tilfellet må utføres \(N\) vektor/matrise- multiplikasjoner og en matrise/matrise-multiplikasjon (der \(N\) = antall punkter).

Mer om sammensatte transformasjoner

Rotasjonsmatrisa som ble presentert i forrige avsnitt sørger for rotasjon om origo. Det er imidlertid ikke alltid at modellen vi operer med er sentrert om origo.

Tannhjul som ikke er sentrert om origo roteres om origo

Figuren viser hva som skjer dersom vi roterer tannhjulet når dette ikke er sentrert om origo – det roteres om origo.

Dersom man ønsker at tannhjulet skal roteres om sitt eget sentrum flyttes (transleres) tannhjulet til origo for deretter gjøre rotasjonen og til slutt flytte tannhjulet tilbake. Den komplette sammensatte transformasjonen kan representeres av følgende matrise:

\[ M = M_{\text{trans}}^{-1} \cdot M_{\text{rot}} \cdot M_{\text{trans}} \]
Tannhjulet transleres til origo, roteres, og flyttes tilbake

Dersom man tegner modellen sentrert om origo slipper man å flytte modellen til origo før rotasjon. Det er derfor ofte lurt å tegne (f.eks. i et tegneverktøy) modellene slik at de er sentrert om origo.

Javascript, WebGL og transformasjonsmatrisene

Som vist over finnes det et sett med ferdigdefinerte transformasjonsmatriser som håndterer translasjoner, rotasjoner og skalering av punkter i rommet.

Her ser vi på hvordan man ved hjelp av eksisterende Javascript-funksjoner og klasser kan generere matrisene i Javascript/WebGL kode. Alternativet er å «håndkode» matrisene (som arrays) – noe som vil oppleves som veldig tungvint.

Her brukes Javascript-biblioteket cuon-matrix.s (Matsuda, K. et al, 2013).

Etter at matrisen er generert sendes den til verteksshaderen som bruker den til å transformere verteksene som «passerer» shaderen. Verteksshaderen er et program som kjøres av GPUen. Her utføres blant annet vektor/matrise-multiplikasjoner. Shadere vil bli mer omtalt etter hvert.

Translasjonsmatrisa:

Denne sørger for å flytte et punkt i forhold til verdiene på \(b_x\), \(b_y\) og \(b_z\).

\[ T = \begin{bmatrix} 1 & 0 & 0 & b_x \\ 0 & 1 & 0 & b_y \\ 0 & 0 & 1 & b_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Multipliserer vi et punkt med denne matrisa vil punktet forflytte seg tilsvarende verdien på disse. Dersom \(p_1 = (1,2,3)\) og vi setter \(b_x = 2\), \(b_y = 2\) og \(b_z = 2\) og multipliserer med matrisa får vi den transformerte vektoren \(p_2 = (3,4,5)\).

let bx = 2, by=2, bz=2;
let modelMatrix = new Matrix4();

modelMatrix.setTranslate(bx, by, bz);

Matrisen modelMatrix vil nå se slik ut:

\[ T = \begin{bmatrix} 1 & 0 & 0 & 2 \\ 0 & 1 & 0 & 2 \\ 0 & 0 & 1 & 2 \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

Dersom man studerer modelMatrix-objektet vil vi se at det består av et array, elements, med plass til 16 elementer med indeks f.o.m. 0 t.o.m. 15. I forhold til matrisa over tilsvarer de 4 (indeks 0-3) første elementene 1. kolonne i matrisa, de fire neste (indeks 4-7) 2. kolonne osv. Elements-tabellen vil i dette tilfellet se slik ut:

[1,0,0,0,0,1,0,0,0,0,1,0,2,2,2,1]

Det samme gjelder for de andre matrisene som omtales under.

Rotasjonsmatriser:

Her har vi tre matriser, en for rotasjon rundt hver av aksene. Det er også mulig å rotere rundt en vilkårlig vektor.

Eksempel på hvordan man generere en slik matriser vha. Javascript-funksjoner:

let angle = 60;   //i grader
let modelMatrix = new Matrix4();

modelMatrix.setRotate(angle, 0, 1, 0);

Her oppretter vi en rotasjonsmatrise, modeMatrix, som vil rotere punkter 60 grader om y-aksen. På samme måte kan vi lage matriser for rotasjon rundt Y og Z-aksen evt. vilkårlig vektor vha. de tre siste parametrene til setRotate().

Skaleringsmatrise:

Vi oppretter en skaleringsmatrise slik:

let scaleX = 0.5, scaleY = 0.5, scaleZ = 0.5;   //halvverer i alle retninger
let modelMatrix = new Matrix4();

modelMatrix.setScale(scaleX, scaleY, scaleZ);

Legg merke til at forskjellen i forhold til translasjon er at vektorkomponentene multipliseres med verdiene i skaleringsmatrisa. Ved translasjon adderes vektorkomponentene til matriseverdiene.