Nøkkelord: Graphics pipeline, view frustum, primitiver, interpolering, backface culling, indeksbuffer.
Tegne modeller bestående av flere vertekser
En av de enkleste modeller vi kan fremstille er en enkel trekant bestående av tre vertekser. Verteksene består (minst) av en [x,y,z]-posisjon. Etter hvert skal vi se at man også kan knytte farge, normalvektor (for lysberegning) og/eller en teksturkoordinat til verteksene.
Posisjonene angis i forhold til et tenkt lokalt koordinatsystem. Modellen plasseres deretter i et tenkt globalt koordinatsystem ved hjelp av en transformasjon. Transformasjonen utføres ved å multiplisere verteksene med en modellmatrise. Se [22] og [1, s. 159].
Grunnen til at man spesifiserer modellen i et lokalt koordinatsystem er at det er enklere å spesifisere verteksene til en modell i forhold til et lokalt koordinatsystem (for eksempel sentrert om origo) for deretter flytte, rotere eller skalere modellen vha. en modelltransformasjon. Dette betyr at man kan spesifisere hver enkelt modell uavhengig av hverandre for deretter å tilpasse og plassere disse inn i det globale koordinatsystemet.
Videre vil man, vha. et tenkt (/virtuelt), kamera bestemme hva som skal projiseres på skjermen. Kameraet plasseres i det globale koordinatsystemet og orienteres mot objektet slik at det vises på skjermen (canvaset).
I tillegg angis en projeksjon som avgrenser det kameraet ser vha. et såkalt frustum. Alt som faller innenfor dette vil bli med på det som vises på skjermen mens figurer som faller utenfor ikke kommer med.
Dette betyr at to andre matriser må opprettes nemlig en synsmatrise (view matrix) og en projeksjonsmatrise (projection matrix). Verteksene til trekanten må egentlig multipliseres med en kombinert modell, view og projeksjonsmatrise. Når dette er på plass vil trekanten vises på skjermen i forhold kameraets posisjon og angitte projeksjon.
Disse tre matrisene er avgjørende for å kunne generere 3D grafikk vha. WebGL.
Komplett pipeline
Figuren under viser en mer komplett «graphics pipeline». Her er også teksturminne og et ekstra steg etter fragmentshaderen (dette består igjen av flere steg) tatt med.
Vertekser sendes vha. et verteksbuffer, en og en, til verteksshaderen. Normalt vil verteksshaderen utføre diverse transformasjoner, dvs. matrisemultiplikasjoner, på verteksene før de sendes videre i pipelinen ved at gl_Position settes lik transformert verteks.
I neste steg sammenstilles verteksene slik at rasterisering kan utføres. Dette betyr at systemet beregner hvilke piksler som må til for, f.eks., å få tegnet og fylt en trekant.
Pikslene kalles nå for «fragmenter» siden det, som vi skal se, ikke er gitt at alle ender opp i framebufret. For hvert fragment kjøres nå fragmentshaderen. Fragmentet, som enten har fått farge via et parameter (som vist tidligere) eller har fått en farge basert på verteksfarger (som vi skal se etter hvert), kan manipulere og endre fargen men til slutt må fragmentshaderen sette fragColor lik en fargeverdi.
gl_Position er et definert GLSL parameter som må settes i verteksshaderen. Fragmentshaderen har et liknende parameter, her kalt fragColor, som må settes i fragmentshaderen. Navnet på dette out-parametret bestemmes av utvikleren.
I neste steg (fragment/pikseloperasjoner) utføres bl.a. dybdetest. Denne testen kan resultere i at et fragment forkastes fordi det i følge dybdeparametret (z) vil bli liggende bak et annet fragment. Et fragment er altså en kandidat til å ende opp som en piksel i framebufret.
WebGL Primitiver
3D grafikk utformes vha. grunnleggende former som punkter, linjer og trekanter og kalles gjerne primitive objekter eller bare primitiver. Selv store komplekse 3D modeller er oppbygd vha. slike primitiver. Verteksene som utgjør modellen kan for eksempel tegnes som punkter, knyttes sammen som linjer, et sett med selvstendige trekanter eller som en sekvens av trekanter.
WebGL (og OpenGL ES) støtter følgende primitiver:
- gl.POINTS, tegne et punkt per verteks
- gl.LINES, danne usammenhengende linjer.
- gl.LINE_STRIP, sammenhengende linjer.
- gl.LINE_LOOP, som LINESTRIP men det tegnes linje mellom siste og første verteks
- gl.TRIANGLES, tegner separate trekanter.
- gl.TRIANGLE_STRIP, tegner en serie sammenhengende trekanter.
- gl.TRIANGLE_FAN, tegner en «vifte».
Disse er illustrert under:
Første parameter til gl.drawArrays() bestemmer hvilken type primitiv som brukes. Vi har sett på bruk av POINTS og TRIANGLES.
Punkter
Vha. gl.POINTS kan man tegne punkter av en gitt størrelse. Hver verteks utgjør et punkt og størrelsen kan bestemmes vha. et shaderparameter, gl_PointSize.
Linjer
Verteksene danner usammenhengende linjer. v0 og v1 gir en linje, v2 og v3 en annen linje osv.
Bruker man gl.LINE_STRIP vil de to første verteksene danne en linje, verteks nr 3 koples til verteks nr 2 og danner dermed en ny linje. Verteks 4 koples til v3 osv.
Bruk av gl.LINE_LOOP tilsvarer gl.LINE_STRIP men i tillegg tegnes det en linje mellom siste og første verteks (vn->v0)
Trekanter
Trekanter kan tegnes på flere måter:
TRIANGLES:
Verteksene kan tegnes som uavhengige trekanter, dvs. hver trekant er spesifisert vha. tre vertekser. Her er det verdt å merke seg at rekkefølgen man oppgir verteksene i har betydning. Disse kan enten oppgis med klokka (CW = ClockWise) eller mot klokka (CCW = CounterClockWise). Mer om dette etter hvert.
TRIANGLE_STRIP:
Dersom gl.TRIANGLE_STRIP brukes vil de tre første verteksene danne en trekant mens hver nye verteks vil danne en ny trekant sammen med to av verteksene fra den forrige trekanten.
I en slik triangle strip er det viktig at alle trekanter som utgjør «stripen» har trekanter bestående av vertekser angitt i samme rekkefølge, dvs. enten mot klokka (CCW) eller med klokka. Rekkefølgen man angir verteksene til den første trekanten bestemmer hvordan rekkefølgen til verteksene til alle de andre trekantene oppfattes.
Dersom vi ser på figuren over er verteksene til første trekant angitt i rekkefølgen: v0, v1, v2. Den andre trekanten er da definert av v2, v1, v3, den tredje trekanten er definert av v2, v3, v4 mens den siste er definert av v4, v3, v5.
TRIANGLE_FAN:
Dette er et alternativ til triangel strip. De tre første verteksene utgjør den første trekanten mens hver nye verteks «koples» til den første verteksen i den første trekanten slik at det oppstår en slags vifte-formasjon.
I figuren ser vi at v0,v1 og v2 danner første trekant. På samme måte som triangle strips vil verteksene til påfølgende trekanter være definert i samme rekkefølge (CW eller CCW, her CCW). Den andre trekanten er derfor definert av v0, v2,v3, den tredje av v0, v3, v4 osv.
Backface culling
Ved hjelp av backface culling kan vi få WebGL til å automatisk fjerne mange av trekantene som uansett ikke vil vises i det endelige bildet. Som regel ser man ikke alle sidene til en 3D modell – vi ser kun det som kameraet peker mot. Trekantene som utgjør baksiden kan derfor ofte fjernes.
Dersom culling skal fungere må WebGL vite hva som er frem og bak på trekantene og hvilke som er vendt mot kameraet.
En trekant har to sider – en forside og en bakside. Vi kaller et polygon (her trekant) som har forsiden mot kamera et fremovervendt polygon (front facing polygon) mens et polygon som har forsiden fra kamera kalles et bakovervendt polygon (back facing polygon). Rekkefølgen verteksene er angitt i, dvs. CW eller CCW, er med på å bestemme hva som er forsiden og hva som er baksiden på trekanten.
Dette bestemmes av følgende metodekall:
gl.frontFace(gl.CCW); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK);
Det første kallet indikerer at trekanter med vertekser angitt mot klokka er «front-facing». Dette er også standard. Det andre kallet gjør at culling tas i bruk. Som standard er culling ikke i bruk og må evt. aktiveres vha. gl.enable(gl.CULL_FACE). Det tredje indikerer at det er «back facing» trekanter som skal culles (/fjernes, ikke tegnes).
Figuren illustrerer dette. Til venstre: Når kameraet peker mot trekanten der verteksene er definert mot klokka vil det se mot forsiden av trekanten og den vil tegnes på skjermen. I høyre del av figuren er kameraet flyttet til baksiden og trekanten vil dermed ikke vises på skjermen (den culles).
Det er imidlertid ikke alltid at man kan bruke culling.
Dersom man tegner en kube og tillater at man skal kunne bevege seg inne i kuben (dvs. flytte kameraet inn i kuben) vil veggene sett fra innsiden forsvinne/culles.
Dersom man bruker delvis gjennomsiktige objekter/modeller kan man ikke bruke culling. I tilfeller der man bruker ugjennomsiktige modeller, som det ikke skal være mulig å «gå inn i» vil culling kunne brukes.
Figuren under viser et ugjennomsiktig 3D objekt fra oversiden. Her vil bakovervendte polygoner/trekanter kunne fjernes siden de uansett ikke skal vises på skjermen.
Dette betyr at alle trekantene som måtte inngå i baksiden av figuren ikke behandles videre (rasterisering m.m.) og kan dermed fjernes.
Kameraet vil fortsatt se det samme siden forsiden (front facing polygons) uansett dekker over polygonene på baksiden (back facing polygons).
Man kan i mange tilfeller anta at omtrent halvparten av polygonene i en scene vender fra kameraet som betyr at man kan forkaste omtrent halvparten av trekantene – dette har stor betydning i forhold til ytelsen.
Knytte farge til verteksene
Hittil har alle pikslene fått samme farge. I dette avsnittet skal vi se hvordan man kan knytte farge til hver verteks. Vi vil se at dersom vi knytter ulike farger til en trekants vertekser vil fargene interpoleres («utjevnes») mellom verteksene. Dersom vi angir tre vertekser med fargene rød, grønn og blå vil resultatet bli som følger:
For å få til dette må det knyttes fargeverdier til verteksene i tillegg til at shaderne må utvides med et ekstra in/out-parameter i shaderene.
Verteksfarge knyttes til verteksene ved at verteksbufret utvides til også å inneholde fargeinformasjon i form av 4 ekstra float-verdier. Disse vil da inneholde en rød, grønn, blå og alpha-verdi (RGBA) – hver i området 0 til 1.
Verteksshaderen tar i så fall, for hver verteks, imot både et fargeparameter (aVertexColor) og posisjonsparameter (aVertexPosition).
Fargeparametret kan videresendes til fragmentshaderen vha. et out-parameter, f.eks. slik:
#version 300 es
in vec3 aVertexPosition; // Innkommende verteksposisjon.
in vec4 aVertexColor; // Innkommende verteksfarge.
out lowp vec4 vColor; // NB! Bruker out.
void main(void) {
gl_Position = vec4(aVertexPosition, 1.0);
vColor = aVertexColor; // Setter vColor = innkommende verteksfarge.
}
Tilsvarende in-parameter må defineres i fragmentshaderen, samme navn og type, slik:
#version 300 es
precision mediump float;
in lowp vec4 vColor; // NB! Interpolert fargeverdi.
out vec4 fragColor;
void main(void) {
fragColor = vColor; // fragColor = Interpolert fargeverdi.
}
Vi ser at samme in/out-parameter er deklarert både i verteks- og i fragmentshaderen. I steget mellom verteksshaderen og fragmentshaderen utføres rasterisering, dvs. det beregnes hvilke piksler/fragmenter som trengs. Fragmentshaderen vil, for hvert fragment, kjøres og motta en interpolert fargeverdi på vColor. Dersom det tegnes en trekant, der trekantes tre vertekser er gitt forskjellige farger, vil pikslene som trengs for å tegne trekanten få ulike interpolerte fargeverdier. Se neste avsnitt.
Mer om interpolering
Anta at vi, i stedet for en trekant, tegner en linje (gl.LINES) spesifisert av to vertekser som har fargene rød (1.0, 0.0, 0.0) og blå (0.0, 0.0, 1.0). Vi ser bort fra alfaverdien her.
Etter at fargeverdiene til linjas vertekser er tilordnet vColor i verteksshaderen vil graphics pipeline beregne hvilke fragmenter som trengs for å tegne linja. Den vil også beregne hvilken RGB-verdi hvert fragment skal ha. Denne fargeverdien videresendes til fragmentshaderen, og mottas av in-parametret vColor.
Dette kan illustreres slik:
Her ser man hvordan fragmentene mellom to vertekser, som definerer en linje, får ulike farger basert på verteksenes farger (rød og blå). Denne prosessen kalles «interpolering» og skjer automatisk i rasteriseringssteget.
Fargene knyttet til verteksene kan enten legges i samme buffer som posisjonsverdiene (x, y og z) eller de kan legges i et eget buffer. Bruk av ett buffer vil normalt være mer effektivt.
Mer om verteks og indeksbuffer
WebGL har to metoder som kan brukes til å utføre «tegning», disse er:
- gl.drawArrays()
- gl.drawElements()
I tillegg bruker man gl.clear() for å renske skjermen før man tegner vha. en eller begge de nevnte metodene.
I forhold til ytelse bør man generelt forsøke å gjøre færrest mulig kall til gl.drawArrays() eller gl.drawElements(). Det er mer effektivt å gjøre et kall for å tegne 200 trekanter i stedet for å ha 100 kall som hver tegner 2 trekanter.
Metoden gl.drawArrays() bruker verteksbufret direkte mens gl.drawElements() bruker et indeksbuffer i tillegg til et verteksbuffer for å unngå dobbeltlagring av verteksdata siden flere trekanter i en figur kan dele samme verteks.
Bruk av gl.drawArrays()
Prototypen til metoden ser slik ut:
void drawArrays(GLenum mode, GLint first, GLsizei count);
Metoden tegner primitivene definert av bufret som er bundet vha. gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer) der vertexBuffer er opprettet vha. gl.createBuffer(). Første argument til gl.drawArrays() spesifiserer hvilken type primitiv som skal tegnes. Dette kan f.eks. være gl.TRIANGLES. Den bruker da verteksdataene som er gjort tilgjengelig.
Det andre argumentet i gl.drawArrays(), first, indikerer hvilken indeks i verteksarrayet som skal brukes som første indeks (og verteks). Det siste argumentet, count, spesifiserer antall vertekser som skal brukes.
Oppsummert må man før kall på gl.drawArrays() gjøre følgende:
- Opprett et verteksbuffer vha. gl.createBuffer(). Dette gir et objekt av type WebGLBuffer.
- Binde bufret til gl.ARRAY_BUFFER vha. gl.bindBuffer(gl.ARRAY_BUFFER, buffer).
- Last verteksdata inn i bufret vha. gl.bufferData().
- Kople shaderparametre, f.eks. a_Position, til bufferobjektet vha. gl.vertexAttribPointer()
- «Enable» verteksarrayet vha. gl.enableVertexAttribArray()
Dersom modellen består av mange primitiver med delte vertekser kan det være lurt å bruke gl.drawElements() i stedet for gl.drawArrays().
Bruk av indekser og gl.drawElements()
Vi har sett hvordan bruk av triangelstrips & fans er med på å minimalisere antall vertekser som brukes til å definere en modell. Her skal vi se hvordan man kan bruke verteksbuffer sammen med et indeksbuffer for å i enda større grad gjenbruke vertekser.
Metoden drawElements() bruker et verteksbuffer i tillegg til et indeksbuffer som bestemmer hvordan verteksene sammenstilles til primitiver. Dette betyr også at verteksene kan ligge i vilkårlig rekkefølge i bufret siden det er indeksene som bestemmer rekkefølgen. Det er nå enkelt å gjenbruke en verteks ved at samme indeks brukes flere ganger i indeksarrayet. Dette kalles også «indeksert tegning».
Indeksbufret bindes vha. bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indeksBuffer) metoden.
Anta at et rektangel er definert av fire vertekser; v0, v1, v2 og v3. Dette kan tegnes indeksert vha. indeksene 0, 1, 2, 2, 1, 3. Se figuren under.
I dette eksemplet er det minimale besparelser å bruke indekser. I tilfeller der man har 100, 1000- eller millioner av trekanter vil det derimot kunne være veldig mange delte vertekser og bruk av indekser vil da kunne være rasjonelt.
Når man bruker indekser og gl.drawElements() må verteksbufret settes opp som forklart i forrige avsnitt. I tillegg må følgende gjøres:
- Opprette et indeksbuffer (av type WebGLBuffer) vha. gl.createBuffer().
- Binde indeksbufret til gl.ELEMENT_ARRAY_BUFFER vha. bindBuffer()
- Laste indeksdata inn i indeksbufret vha. gl.bufferData().
Prototypen til gl.drawElements() ser slik ut:
void drawElements(GLenum mode, GLsizei count, GLenum type, GLintptr offset);
Det første argumentet er det samme som gl.drawArrays(), f.eks. gl.TRIANGLES. Det andre argumentet, count, indikerer hvor mange indekser som ligger i indeksbufret. Det tredje argumentet, type, indikerer datatypen til indeksene som ligger i indeksbufret. Verdien til denne kan enten være gl.UNSIGNED_BYTE, gl.UNSIGNED_SHORT eller gl.UNSIGNED_INT. Det siste argumentet, offset, indikerer første indeksverdi (eller egentlig en byte offset) i indeksbufret.
Ingen kommentarer:
Legg inn en kommentar