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.

En enkel trekant
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 (Luna, 2006) og (Angel E. & David S., 2015, 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.

View frustum
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 projeksjons- matrise. 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.
Basert på figur fra (Munshi, A. et al, 2009) |
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 gl_FragColor lik en fargeverdi.
Både gl_Position og gl_FragColor er spesielle GLSL parametre som må settes i henholdsvis verteks- og fragmentshaderen.
I neste steg (fragement/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:
WebGL (og OpenGL ES) støtter følgende primitiver:
- gl.POINTS, tegne et punkt per verteks
- gl.LINE, danne usammenhengende linjer. v0 og v1 gir en linje, v2 og v3 en annen linje osv.
- gl.LINESTRIP, sammenhengende linjer. v0 og v1 danner en linje, v2 koples til v1, v3 til v2 osv.
- gl.LINE_LOOP, som LINESTRIP men i tillegg tegnes det en linje mellom siste og første verteks (vn->v0)
- gl.TRIANGLES, tegner separate trekanter. En trekant for hver 3. verteks.
- gl.TRIANGLE_STRIP, tegner en serie sammenhengende trekanter. De tre første verteksene danner en trekant. Hver nye verteks fører til en ny trekant.
- gl.TRIANGLE_FAN, tegner en «vifte». De tre første verteksene danner en trekant. Hver nye verteks lager en ny trekant som koples til den første verteksen.
WebGL primitiver. Fra (Matsuda, K. et al, 2013). |
Punkter
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 nr2 og danner dermed en ny linje. Verteks 4 koples til v3 osv.
Bruk av gl.LINE_LOOP tilsvarer gl.LINESTRIP 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.
Triangle Strip |
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.
Triangle Fan |
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 kalles indikerer at det er «back facing» trekanter som skal culles (/fjernes, ikke tegnes).
Front & back facing polygon |
Dette er en enkel måte å optimalisere på – jo færre vertekser som videresendes i pipelinen jo mer effektivt program. Der 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:Ulike farger angitt i hver av de tre verteksene |
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 (a_Color) og posisjonsparameter (a_Position).
Fargeparametret kan videresendes til fragmentshaderen vha. et varying-parameter, f.eks. slik:
attribute vec4 a_Position; // Innkommende verteksposisjon.
attribute vec4 a_Color; // Innkommende verteksfarge.
varying vec4 v_Color; // NB! Bruker varying.
void main() {
gl_Position = a_Position; // Posisjon.
v_Color = a_Color; // Setter varying = innkommende verteksfarge.
};
Tilsvarende varying-parameter må defineres i fragmentshaderen, samme navn og type, slik:
precision mediump float;
varying vec4 v_Color; // NB! Interpolert fargeverdi.
void main() {
gl_FragColor = v_Color; // gl_FragColor = Interpolert fargeverdi.
};
Vi ser at samme varying-parameter er deklarert både i verteks- og i fragmentshaderen. I steget mellom fragment- og verteksshaderen utføres rasterisering, dvs. det beregnes hvilke piksler som skal inngå i trekanten. Siden v_Color er av type varying vil verdien interpoleres slik at hver piksel får forskjellige farger. Verdien til v_Color i fragmentshaderen er dermed ikke den samme som v_Color i verteksshaderen.
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 v_Color i fragmentshaderen vil RGB-verdiene for hvert fragment som utgjør linja beregnes og videresendes til fragmentshaderens v_Color parameter.
Dette kan illustreres slik:
Interpoleringsprosessen |
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 at et buffer vil normalt være mer effektivt. Legg også merke til bruk av «stride»-parametret i vertexAttribPointer(…). Dette angir antall bytes som hver verteks opptar dvs. antall bytes som posisjon og farge opptar til sammen.
Mer om verteks og indeksbuffer
WebGL har tre 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 drawArrays() eller 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 drawArrays() bruker verteksbufret direkte mens 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);
Det andre argumentet i 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å 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().
- «Enable» verteksarrayet vha. gl.enableVertexAttribArray()
- Kople shaderparametre, f.eks. a_Position, til bufferobjektet vha. gl.vertexAttribPointer()
Bruk av indekser og gl.drawElements()
Vi har sett hvordan bruk av triangel strips & 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.Verteks og indelsbuffer |
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.
Bruk av indeksbuffer (Anyuru, 2012) |
Når man bruker indekser og 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 gl.drawElements(GLenum mode, GLsizei count, GLenum type, GLintptr offset);
Ingen kommentarer:
Legg inn en kommentar