Introduksjon til texture mapping
Hittil har vi sett hvordan farge kan knyttes til hver enkelt verteks og på den måten fargelegge polygoner/trekanter. Skal man lage et spill eller animasjon med tiltalende grafikk og figurer vil det være svært tungvint å skulle fargelegge polygonene/trekantene til modeller manuelt.
Ved hjelp av teksturer kan vi gjøre våre 3D modeller mer tiltalende ved å ”kle” dem med bilder. I WebGL kan bilder legges på flatene som utgjør 3D-modellene. Man kan for eksempel legge et bilde på et rektangel bestående av to trekanter – slike bilder kalles tekstur.
Prosessen kalles texture mapping («teksturering») og innebærer at fargen til bildets piksler tilordnes fragmentene som genereres i rasteriseringssteget i pipelinen. Pikslene som utgjør teksturbildet kalles texel (texture element) og består av en RGB(A)-verdi.
Ved å kombinere bruk av teksturer, farger og lys kan man lage ulike spesialeffekter som f.eks. blande ulike teksturer, blande en tekstur med en farge filtrert på ulike måter, simulere bølger i vann, generere en «skybox» eller «skydome», diverse partikkeleffekter m.m.
WebGL støtter flere bildeformater via texImage2D(). I praksis støtter den alle formater som nettleseren kan dekode, og omfatter formater som f.eks. png, jpg, gif, webp, bmp, svg.
Teksturkoordinater
Teksturkoordinater, som også kalles st- eller uv-koordinater, spesifiserer et (2D) punkt i teksturen.
En verteks består minimum av en xyz-posisjon. Som vist tidligere kan man også knytte en farge til en verteks i form av en RGBA-verdi. Skal man kle en 3D-modell med en tekstur knyttes en 2D-teksturkoordinat til hver verteks.
Husk at en tekstur er et 2D-objekt som legges på en 3D-modell. Teksturkoordinatene lagres sammen med posisjonsdataene for hver enkelt verteks. Dersom vi har et rektangel, bestående av to trekanter, som vi ønsker dekt med en tekstur vil verdiene til u og v-koordinatene variere mellom 0 og 1.
Figuren under viser hvordan uv-koordinatsystemet ser ut for to forskjellige 2D teksturer.
I nedre venstre hjørne er uv = 0,0 mens i øvre høyre hjørne er uv = 1,1. Dette gjelder også dersom teksturen ikke er kvadratisk.
WebGL støtter også såkalt «cube map» teksturer. En slik «kubetekstur» består egentlig av 6 enkeltteksturer – en for hver side i en kube. En kubetekstur kan f.eks. brukes til å lage refleksjoner fra omgivelsene i skinnende objekter som f.eks. en skinnende kule. Dette kalles environment mapping.
Kubeteksturer kan også brukes til å lage en såkalt «skybox» som er en teksturert kube som omslutter vår 3D-verden. Teknikken går ut på at innersiden av kuben kles med tekstur, f.eks. blå himmel med skyer. Kamera plasseres inni kuben og når kamera flyttes, flyttes også skyboksen tilsvarende. Man vil dermed aldri nå veggene i kuben uansett hvor mye man beveger kamera som igjen vil gi inntrykk av en «uendelig» horisont.
Bildestørrelser
WebGL 1.0 (OpenGL ES 2.0)
WebGL 1.0 (OpenGL ES 2.0) har begrensninger i forhold til hvilke teksturbilder som kan brukes. Her er det anbefalt at teksturbilder, som brukes i WebGL-applikasjoner, har bredde og høyde, i antall piksler, som tilsvarer 2n x 2m der n og m er ikke-negative heltall (0 eller større). Et bilde kan f.eks. være 128 x 256 (27 x 28), 256 x 256, 1024 * 512 osv. piksler stort. NPOT (non-power-of-two) fungerer imidlertid også i WebGL 1.0 under visse forutsetninger.
WebGL 2.0 (OpenGL ES 3.0)
WebGL 2 har imidlertid full støtte for NPOT (non-power-of-two) bilder, dvs. bilder med vilkårlig størrelse. Det er denne versjonen som brukes i disse notatene og tilhørende kodeeksempler.
Steg for å bruke tekstur
Følgende må gjøres for å teksturere en modell/figur:
1. Oppdatere shaderne
Både verteks og fragmentshaderne må endres slik at de kan motta og håndtere teksturdata. Her presenteres oppdaterte shaderfunksjoner:
Oppdatert verteksshader
Oppdatert fragmentshader
Legg merke til at verteksshaderen tar i mot posisjon-, farge- og teksturdata for hver verteks. De nye teksturrelaterte elementene vil bli nærmere forklart etter hvert.
2. Laste tekstur, klargjør buffer
Følgende steg inngår:
- Definere uv-koordinater for hver verteks til modellen som skal tekstureres.
- Laste aktuell bildefil som utgjør teksturen.
- Når bildet er ferdig lastet opprettes et tekstur-objekt vha. gl.createTexture(). Deretter utføres gl.bindTexture() og gl.texImage2D() for å laste teksturen til GPUen/shaderen.
- Utføre gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) for å unngå at bildet vises opp-ned.
- Sette diverse teksturparametre som f.eks. bestemmer hva som skal skje dersom bildet er mindre eller større enn flaten som skal tekstureres.
- Opprette et teksturbuffer vha. gl.createBuffer(). Utføre gl.bindBuffer() og gl.bufferData() for å fylle bufret med uv-data (på samme måte som for posisjon og farge).
3. Tegne vha. drawArrays()
Følgende steg inngår:
- Kople buffret til shader-parametret vha. gl.vertexAttribPointer(), og aktiver dette vha. gl.enableVertexAttribArray().
- Aktiver «teksturenhet», vha. gl.activeTexture(gl.TEXTURE0).
- Sett verdien til en sampler2D-uniform til et teksturenhetsnummer med gl.uniform1i().
Eksempel: Teksturering av et rektangel
Her ser vi på hvordan man kan teksturere et enkelt rektangel bestående av to trekanter. Figuren under viser prosessen:
Figuren viser opprinnelig .png fil som brukes som tekstur. Til høyre ser vi rektanglet, bestående av to trekanter, som skal «kles» med tekstur. Her er xyz-posisjon og uv-koordinater til hver verteks angitt. Legg merke til at vi ser 3D-koordinatsystemet fra «toppen», rektanglet ligger med andre ord i xz-planet. Rød linje er x-aksen, grønn er y-aksen mens blå linje er z-aksen.
Koden
Bildefilen(e) må lastes ned fra serveren før den kan brukes som tekstur. Slike filer (.png, .jpg e.l.) legges typisk i egen mappe. I eksemplet under er det antatt at .png fila ligger i en egen textures-mappe. Her brukes en egen klasse, ImageLoader, som igjen bruker Javscript-klassene Image og Promise, til nedlasting av bilder. Klassen er gjengitt under:
ImageLoader er laget slik at man kan laste ned et eller flere bilder i samme operasjon ved å bruke Promise. Klassen har en metode, nemlig load(). Her gjennomløpes urls-arrayet, som inneholder navn på hvert bilde som skal lastes ned. For hver bilde-url, opprettes et nytt Promise-objekt, som legges til arrayet promises. Hvert promise-objekt sørger for å laste ned aktuelt bilde vha. Image-klassen. Nedlasting av et bilde starter når man gir src attributtet en verdi. Etter hvert som nedlasting av hvert enkelt bilde er fullført, kalles resolve(). Dette skjer i Image sin onload().
Merk at nedlastings-koden kjøres asynkront med resten av koden i ImageLoader sin load() funksjon. Det betyr at Promise.all(promises) kan kalles før alle bildene er ferdig nedlastet. Denne funksjonen vil imidlertid vente til alle bilder er ferdig nedlastet før den kaller på onLoad(images). Kall på onLoad(images) kaller dermed tilbake til funksjonen som bruker ImageLoader.load() når ALLE bilder i urls er ferdig nedlastet.
I main() skjer følgende (legg merke til at deler av koden er utelatt - du finner komplett kode blant kodeeksemplene):
Her kalles imageLoader.load() for å laste ned alle bilder som ligger i textureUrls-arrayet (her er det bare ett - tree1.png). Når alle bilder er ferdig nedlastet kjøres callback-funksjonen knyttet til load(). De nedlastede bildene følger med i parametret textureImages. Her laster vi kun ned et bilde, og dette kan hentes ut vha. const textureImage = textureImages[0].
Vi har nå et bilde som kan brukes videre i programmet. Vi ser at bildet sendes som parameter til initSquareTextureAndBuffers(). I funksjonen initSquareTextureAndBuffers() opprettes posisjon-, farge- og teksturebuffrene:
Her opprettes et posisjonsbuffer, et fargebuffer og et teksturbuffer. Disse fylles med data fra tilsvarende array. Bruk av teksturbuffer er litt mer omfattende enn posisjon- og fargebuffer og krever litt mer forklaring:
- Kallet gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) sørger for at bildet vises riktig vei. Kallet setter en tilstand i WebGL-konteksten som gjelder for alle etterfølgende teksturopplastinger, dvs. kall på gl.texImage2D(), og gjør at bildet snus opp-ned, dvs. at øverste bilderad blir den nederste. Navnet FLIP_Y viser til at y-koordinaten inverteres (y → 1−y); speilingen skjer altså om en vannrett akse. Dette må gjøres siden teksturkoordinatsystemet som brukes av WebGL/OpenGL ES (og OpenGL) ikke matcher koordinatsystemet som brukes av Image-klassen. Sett fra WebGL har teksturen «origo» (0,0) i nedre venstre hjørne, mens Image-objektet har origo i øvre venstre hjørne. Standardverdien er false, så uten dette kallet vises teksturen opp-ned.
- Kallet gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true) har derimot ingenting med orientering å gjøre. Det gjør at WebGL multipliserer fargekanalene (R, G og B) med alfakanalen under opplastingen. En texel med verdien (1, 0, 0, 0.5) – altså halvt gjennomsiktig rød – blir dermed lagret som (0.5, 0, 0, 0.5). Dette kalles premultiplisert alfa og er en forberedelse til å tegne delvis gjennomsiktige teksturer. Selve fargeblandingen krever i tillegg at blending slås på, og at man velger en blandefunksjon som passer med premultipliserte verdier – slik kodekommentaren antyder. Det kommer vi tilbake til senere. Er teksturen helt ugjennomsiktig, slik et vanlig foto uten alfakanal er, har kallet ingen effekt: alfa er da 1 overalt, så multiplikasjonen endrer ingenting. Det er tilfellet i dette eksemplet.
- Funksjonen gl.texImage2D(...) laster også teksturen til GPUen. Denne funksjonen finnes i flere varianter men den som er brukt her er kanskje den vanligste. Parametrene betyr:
- gl.TEXTURE_2D: indikerer at det er en 2D tekstur som benyttes.
- 0: mipmap level. Omtales senere.
- gl.RGBA: internt format.
- gl.RGBA: indikerer hva hver texel (piksel) i teksturen består av (her RGBA). Her kan alternativt gl.RGB m.fl. varianter benyttes.
- gl.UNSIGNED_BYTE: indikerer størrelsen på hver «fargekanal», her brukes en unsigned byte for Rød, en for Grønn osv. Her betyr det at hver texel opptar 4 byte.
- textureImage: Teksturbildet.
- Videre settes teksturparametre vha. gl.texParameteri(). I det første kallet, gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST), indikerer vi hvilken farge pikslene (fragmentene) skal få dersom vi forstørrer modellen. Når vi «zoomer» inn modellen betyr det at hver texel dekker flere piksler. Parametret gl.NEAREST indikerer at pikslene får fargen til nærmeste texel. Dette er den enkleste og raskeste metoden. Resultatet er at jo mer man «zoomer» inn på en modell jo mer hakket blir bildet (kalles «pixelation»). Alternativt kan man bruke gl.LINEAR som vil gi en mer diffus modell jo mer man zoomer inn. Det andre kallet til gl.texParameteri() håndterer forminsking på samme måte. Alternativet til å bruke disse teknikkene er å bruke mipmapping. Se [2] for mer info.
- I neste linje kopler vi fra gjeldende tekstur vha. gl.bindTexture(gl.TEXTURE_2D, null).
- I de neste linjene opprettes et bufferobjekt som lastes med uv-data, dvs. aktuelle teksturkoordinater for alle verteksene til rektanglet.
- Til slutt returneres et Javascript-objekt med all relevant informasjon.
Tegne rektanglet
Via draw() og drawSquare() tegnes det teksturerte rektanglet:
I connectTextureAttribute() aktiveres tekstur-bufret, som koples til shaderparametret ‘aVertexTextureCoordinate’ som må være definert i verteksshaderen som vist under:
Tekstur-bufret aktiveres vha. gl.enableVertexAttribArray(). Deretter aktiveres gjeldende tekstur vha. gl.activeTexture(gl.TEXTURE0). Parametret gl.TEXTURE0 koples til aktuell tekstur. Bruk av gl.TEXTURE0 henger også sammen med «sampleren» som initieres og brukes på de neste linjene. uSampler er definert i fragmentshaderen, som ser slik ut:
Vi ser at verdien 0 sendes til shaderens uSampler-parameter vha. gl.uniform1i(textureShader.uniformLocations.sampler, 0). Denne bestemmer hvilken «teksturenhet» (texture unit) som skal benyttes (her 0). Dersom verdien til uSampler er lik 0 brukes gl.TEXTURE0. Disse to verdiene henger derfor sammen. Vi kan tenke på en «teksturenhet» som en referanse til en tekstur. Ved hjelp av gl.TEXTUREn og samplerverdien kan man blande farger fra flere teksturer – såkalt multiteksturering.
I verteksshaderen settes vTextureCoordinate lik innkommende teksturkoordinat. I fragmentshaderen er tilsvarende in-parameter også deklarert. Verdien til denne er interpolert på veien fra verteks- til fragmentshaderen. Verdien til denne brukes til å hente texels, dvs. farge, fra teksturen. Dette gjøres ved hjelp av texture() funksjonen i fragmentshaderen.
Blande farge og tekstur
Det er også mulig å blande farger fra teksturen med verteksfarger dersom disse er definert. Dette er nesten ferdig implementert i foregående kodeeksempel. Det eneste som mangler er at vi tar i bruk vColor i fragmentshaderen, slik:
Her multipliseres teksturfarge med fragmentfargen slik at disse fargene blandes. Resultatet tilordnes fragColor. Dette gir (f.eks.) følgende resultat:
Vi ser at opprinnelig tekstur/bilde nå har et tydelig grøntskjær, siden den er blandet med verteksfargene.
Bruk av flere shaderpar
I mange tilfeller er det nødvendig å bruke ulike shaderpar (dvs. en verteks- og en fragmentshader) for ulike modeller som skal tegnes. En teksturert kube er kun avhengig av posisjon og uv-koordinater mens f.eks. tegning av koordinatsystemet krever en shader som tar i mot en posisjon og en farge.
Se tilhørende kodeeksempler.
Ingen kommentarer:
Legg inn en kommentar