Introduksjon
Denne bloggen benyttes som støtte i faget "Datamaskingrafikk" ved Uit, IVT campus Narvik. Bloggen omhandler grunnleggende OpenGL ES og WebGL. All eksempelkode er derfor basert på Javascript.
Fagområdet omfatter både to- og tredimensjonal grafikk i form av bilder og animasjoner. I vår sammenheng er vi interessert i den tekniske prosessen. Det vil si: hvordan fremstiller man 3D-grafikk ved hjelp av datamaskinen og tilhørende programvare?
Fagområdet kan deles inn i flere underemner, som for eksempel geometri (hvordan representere 3D-modeller), animasjon (bevegelse over tid) og rendring (2D-fremvisning/gjengivelse av 3D-modeller). Også bildebehandling kan ses på som et underemne av fagområdet (New World Encyclopedia, Computer Graphics).
I denne sammenheng er det geometri, animasjon og rendring som står i fokus. Bildebehandling er ikke tema i disse notatene.
Anvendelser
Det finnes mange og varierte anvendelser av datamaskingrafikk, og man kan stort sett plassere disse i en av følgende fire hovedkategorier (Angel et al., 2015):
Visning av informasjon – visualisering
Kart og geografiske informasjonssystemer (GIS), statistikk
CAD (Computer Aided Design) for arkitekter og ingeniører
Verktøy for å utvikle integrerte kretser
Simuleringer og animasjoner
Simuleringer: sanntidsfremstilling, f.eks. fly- og bilsimulatorer
Datamaskinspill
Opplæringsprogramvare
Animasjoner
Film- og TV-industrien
Reklame
Fotorealistiske bilder, med lys- og skyggelegging
VR – Virtual Reality, og AR – Augmented Reality
Brukergrensesnitt
Windows, Linux, macOS osv. Alle er basert på samme prinsipp – vinduer, menyer, ikoner m.m., dvs. grafiske elementer på skjermen.
3D-datamaskingrafikk bruker geometriske data til å rendre (fremstille) 2D-bilder. Spørsmålet er dermed: Hvordan fremstiller man 3D-modeller/objekter[1] på en 2D-flate (skjermen)?
Før vi går videre med dette, er det en del andre begreper og bakgrunnsteori som må på plass.
Vi starter med å se på hva et «grafikksystem» er, litt om grafikkortet og Graphics Processing Unit (GPU),
samt en introduksjon til begrepene scene, virtuelt kamera, shadere og graphics pipeline – og litt om ulike 3D-APIer.
Grafikksystem
Et grunnleggende datamaskingrafikksystem består i prinsippet av følgende deler (Angel et al., 2015):
Grafikksystemet i dagens datamaskiner er stort sett rasterbasert. Rastergrafikk innebærer at bildet er sammensatt av bildepunkter ordnet i rader og kolonner. Et bildepunkt, eller picture element, kalles piksel (pixel).
Skjermbufferet (framebuffer) er et minneområde som inneholder pikseldata tilsvarende et komplett bilde (en frame) klart til visning på skjermen.
Skjermbufferet er essensielt, og presisjonen avgjør detaljgraden i bildene som fremvises. Presisjonen er definert av antall bits per piksel. Én bit per piksel gir tilgang på to farger, 8 bits gir 256 farger. 24 bits dybde (8 bits per fargekomponent) er i dag vanlig og kalles gjerne true color eller RGB-systemer.
Såkalte HDR-systemer (High Dynamic Range) bruker 10 eller flere bits per fargekomponent ut mot skjermen, og typisk 16-bits flyttall internt i rendringen. Tidligere ble fargekomponentene lagret som heltall (integer), men i dag lagres de gjerne som flyttall (float) i skjermbufferet, slik at også HDR støttes.
Framebufferet inneholder mer informasjon enn bare fargen til hver piksel. Her ligger også informasjon om dybde m.m. Oppgaven til grafikksystemet er i prinsippet å omgjøre grafiske primitiver (gitt fra et program) til piksler i skjermbufferet. Denne prosessen kalles rasterisering eller scan conversion. Et grafisk primitiv kan være et punkt, en linje eller et polygon (mangekant). Et polygon er en flate beskrevet av et sett rette linjer – en trekant er dermed en type polygon.
Noen polygoner og tilhørende vertekser
Rasterisering gjør at vi kan gå fra linjer og trådmodeller (wireframes) til fylte polygoner. Primitiver er definert av et sett vertekser. En verteks (vertex) er i prinsippet et punkt i det tredimensjonale rom. Figuren under viser en kule oppbygd av polygoner (trekanter).
En kule oppbygd av trekanter
Fra et program kan man for eksempel angi tre punkter i rommet for å tegne en trekant. I tillegg angis for eksempel farge og/eller en teksturkoordinat for hver verteks. Ved hjelp av rasterisering vil nødvendige piksler genereres, med korrekt farge, slik at trekanten kan vises på skjermen.
Rasterisering er imidlertid ikke den eneste måten å lage et bilde av en 3D-scene på.
Den andre hovedtilnærmingen kalles ray tracing. Der rasterisering går «fra geometri til piksler»
går ray tracing motsatt vei: den tar for seg én piksel om gangen og sporer en stråle ut i scenen for å finne ut hva som er
synlig akkurat der. Rasterisering er rask, og det er denne teknikken dagens grafikkort og APIer i hovedsak er bygd rundt.
Ray tracing gir på sin side mer fysisk korrekt lys, skygge og speiling, men koster vesentlig mer regnekraft.
Disse notatene vil i hovedsak fokusere på rasterisering, men vi vil også se litt på ray tracing.
Mer om skjermbufferet
Skjermbufferet er som regel satt sammen av flere delbuffere:
Fargebuffer
Z-buffer (dybdebuffer)
Stensilbuffer
Fargebufferet inneholder fargeverdiene, typisk RGB (evt. RGBA), til alle pikslene. Z-bufferet, eller dybdebufferet, inneholder en dybdeverdi for hver piksel. Denne brukes for å avgjøre om en piksel ligger foran eller bak andre piksler. Stensilbufferet er ikke like sentralt som farge- og z-bufferet, men det kan for eksempel brukes til å lage skygger og speilinger. I praksis ligger ofte dybde- og stensilverdiene i samme buffer (f.eks. 24 bits dybde + 8 bits stensil).
Grafikkortet og grafikkprosessoren
I eldre datamaskiner ble gjerne grafikkprosesseringen utført av CPUen, samtidig som skjermbufferet var en del av standard minne.
Ingen spesiell støtte for grafikk
Dagens datamaskiner har typisk hovedkort eller prosessorbrikke med integrert grafikkprosessor – en Graphics Processing Unit (GPU).
Denne kan for eksempel ha en dedikert del av standard minne som den jobber mot (med assosiert skjermbuffer).
Poenget med grafikkprosessoren er å avlaste CPUen ved at den tar seg av beregningskrevende grafikkoperasjoner.
GPUer ble opprinnelig utviklet for å avlaste CPUen for grafikkoperasjoner. En GPU er egentlig en spesialisert mikroprosessor som i dag også brukes
i forbindelse med HPC (high performance computing). HPC omfatter teknikker og metoder for mange og/eller tunge beregninger.
HPC-metoder brukes i mange sammenhenger, f.eks. i forbindelse med utvinning av kryptovaluta, maskinlæring og kunstig intelligens,
værsimuleringer m.m. (Store Norske Leksikon, 2026). Det finnes ulike produsenter av GPUer som f.eks. Nvidia, AMD, Intel m.fl.
GPU-arkitekturen varierer mellom de ulike produsentene og det er derfor viktig at det finnes felles programmeringsgrensesnitt mot disse.
Eksempler er OpenGL, DirectX og Vulkan. Mer om dette senere i notatet.
Integrert løsning, GPU på hovedkortet
En GPU er, som en CPU, en mikroprosessor. Den er spesielt god til å utføre de matematiske og geometriske beregningene som trengs for å fremstille 3D-grafikk på en 2D-skjerm, fordi den kjører svært mange enkle beregninger parallelt.
Alternativt kan man legge til ekstern maskinvare i form av dedikerte grafikkort (skjermkort) med integrert grafikkprosessor. Slike kort kobles direkte til maskinens hovedkort. Skjermbufferet er i så fall et minneområde assosiert med grafikkortet, og vil typisk inneholde piksler tilsvarende et komplett bilde (en frame) for visning på skjermen.
Eksternt grafikkort
Mer om grafikkortet
Dagens grafikkort (kalles også grafikkakselerator) har gjerne direkte støtte for 3D-APIer som OpenGL, Direct3D, Vulkan og Metal. 3D-grafikk fremstilles programmatisk ved hjelp av disse APIene. Via APIet sendes kommandoer til kortets driver, som igjen sørger for at kommandoene utføres.
Grafikkprosessoren utfører kommandoene og genererer rastergrafikk som legges i skjermbufferet. Skjermbufferet oppdateres dermed via GPUen. Denne arkitekturen avlaster CPUen betraktelig. NVIDIA, AMD, Intel og Apple er eksempler på produsenter av GPUer og grafikkort.
Figuren under viser et eksempel på et grafikkort med GPU, kjøleribbe, minnebrikker og diverse tilkoplinger og kontakter. Kortet kobles typisk til hovedkortet via PCI Express (PCIe).
Eksempel på et grafikkort (med hjelp fra ChatGPT)
Forestill deg en bedrift med en ingeniøravdeling og en designavdeling. Dersom ingeniørene konstruerer en bru – dvs. gjør nødvendige beregninger, finner nødvendige materialer og lager en skisse – vil industridesignerne (satt litt på spissen) sørge for at brua ser bra ut innenfor ingeniørenes rammer. Forenklet oppsummert: beskrivelsen av brua sendes til designerne, som så bestemmer hvordan brua skal se ut. Merk at dette antakelig er en grov forenkling som mange designere vil protestere mot.
Fra vertekser til piksler
Resultatet er at beskrivelsen av brua ender opp som en pen bru.
Dette kan sammenlignes med virkemåten til et grafikkort. Brukerapplikasjoner, som for eksempel et 3D-tegneverktøy eller en WebGL-applikasjon, sender informasjon om bildet til grafikkortet. Grafikkortet bestemmer så hvordan skjermpikslene skal fylles for å skape ønsket bilde. Denne informasjonen sendes videre til skjermen.
Det er krevende å fremstille et 3D-bilde ut fra en geometrisk beskrivelse. Første steg består i å opprette en trådmodell (wireframe) bestående av for eksempel mange trekanter. Deretter utføres rasterisering, som innebærer å beregne hvilke piksler som trengs for å fylle trekantene. Så får hver piksel en farge. Fargen kan også, eller i tillegg, bestemmes av teksturer og lys-/skyggedata. I forbindelse med spill skjer denne prosessen typisk 60 ganger per sekund eller oftere. Dersom CPUen skulle utført alle disse beregningene, ville den fort blitt overbelastet.
Grafikkortet består av fire hovedkomponenter:
En kontakt for tilknytning til hovedkortet, i dag så godt som alltid PCI Express (PCIe). Eldre maskiner brukte PCI eller AGP (Advanced Graphics Port).
En egen prosessor – GPU – som bestemmer hvordan hver piksel skal se ut
Minne som holder på alle pikslene som skal fremvises, inkludert skjermbufferet
En eller flere skjermkontakter, typisk HDMI, DisplayPort eller USB-C
Hva kjennetegner et godt grafikkort?
Ytelsen til et grafikkort måles gjerne i form av antall komplette bilder per sekund det klarer å generere. På engelsk betegnes dette som frame rate og måles i fps – frames per second. Det er imidlertid flere faktorer som innvirker på fps:
Hvor raskt det kan bygge opp en trådmodell – måles i antall trekanter eller vertekser per sekund
Hvor raskt trådmodeller fylles med piksler – måles i antall piksler GPUen kan prosessere per sekund
Hvor mye og hvor rask minnebåndbredde kortet har – teksturer og buffere må leses og skrives kontinuerlig
3D-illusjonen
Teknikker for å vise 3D-grafikk med dybde på en 2D-flate, for eksempel et papirark, har blitt grundig studert og utviklet av kunstnere gjennom de siste århundrene. Teknikkene er basert på fenomener som vi observerer i vår fysiske verden. Ser man langs en rett jernbanelinje, vil kantene på linja se ut til å møtes i et «forsvinningspunkt» (vanishing point) langt der fremme. Objekter vil oppfattes som mindre jo lenger unna de er. Et hus som står veldig langt unna betrakteren vil virke lite, mens et tre som står nært vil oppfattes som stort.
«Vanishing point» og dybde
Objekter dekker til hverandre, enten helt eller delvis, avhengig av gjennomsiktighet, plassering og dybdeplassering.
Ugjennomsiktige objekter
Poenget er at dybdeinformasjon er viktig når grafikksystemet gjør om 3D-objekter til 2D-skjermgrafikk. Bruk av lys, skyggelegging og materialer kan være med på å fremheve 3D-illusjonen.
Modellrepresentasjon
Modeller kan for eksempel utformes ved hjelp av geometrisk modellering – dvs. ved å angi punkter og grafiske primitiver i en virtuell 3D-verden. Et primitiv er i denne sammenheng en enkel «figur» som en linje, en trekant eller andre typer polygoner.
En modell er igjen oppbygd av et sett med primitiver. Det viser seg at en effektiv måte å bygge opp 3D-modeller på er ved hjelp av et sett trekanter. Kuler kan for eksempel genereres utelukkende av trekanter. Jo mindre trekantene er, jo glattere vil kuleflaten fremstå. Kombineres dette med lyssetting og tekstur, er det mulig å oppnå fotorealistiske kuler med gjenskinn og skyggeeffekter. En tekstur er kort fortalt et 2D-bilde som knyttes til / legges på primitiver.
Et mesh er en samling polygoner som til sammen utgjør en 3D-modell. Jo flere polygoner (trekanter), jo mer detaljert modell. Dette betyr også høyere prosesseringskrav. Videre er det ønskelig å kunne manipulere modellene, dvs. rotere, flytte eller skalere dem. En annen viktig del av 3D-grafikken er å kunne bestemme synsvinkel – dvs. fra hvilken kant modellen skal betraktes – og hvordan modellen skal projiseres på skjermen (perspektiv eller ortografisk projeksjon – mer om dette senere).
Scene
Når man tar bilder i den virkelige verden, stilles gjerne modellene og objektene opp i en scene før vi «knipser» bildet med fotoapparatet. Vi bruker kameraet til å fange opp og generere 2D-bilder av vår fysiske 3D-verden. Tilsvarende skjer når man programmerer 3D-scener med for eksempel WebGL. Etter at modellene er laget, posisjonerer man disse i en tenkt 3D-verden. Deretter plasseres eventuelle lyskilder og et «kamera» i den tenkte verdenen. Kameraet posisjoneres slik at det fanger opp den delen av scenen som ønskes vist på skjermen. Det er viktig å skille mellom objektene og bildet av objektene. Legg merke til at objekter, lyskilder og kamera plasseres uavhengig av hverandre.
Scene og kamera
Objekter spesifiseres ved hjelp av geometriske primitiver. Geometriske primitiver spesifiseres igjen ved hjelp av vertekser. Vertekser kan, i tillegg til å inneholde en posisjon, også inneholde informasjon om farge, tekstur og normalvektor (brukes i forbindelse med lys – mer om dette senere). Man kan dermed beskrive modeller ved hjelp av et sett vertekser. En trekant spesifiseres av tre vertekser. En kule består av et stort antall trekanter.
Det virtuelle kameraet
Kameramodellen som brukes i moderne 3D-grafikksystemer, simulerer i prinsippet optiske systemer.
Figuren viser hvordan objektet (huset) i scenen projiseres på et bildeplan. Trekker man projeksjonslinjer fra objektet til kameralinsen, finner vi «bildet» av modellen på bildeplanet.
Det virtuelle kameraet
Objekter som er langt fra kameraet, vil fremstå som mindre på bildeplanet enn objekter som er nærmere kameraet.
Fordeler med denne kameramodellen er blant annet at vi skiller mellom objekter, betrakter (kameraet) og lyskilder. Alle disse spesifiseres og plasseres uavhengig av hverandre. Dette gjør det mulig å utforme bildet av objektene ved hjelp av geometriske beregninger i form av vektor- og matriseoperasjoner.
Figuren viser essensen av 3D-datamaskingrafikk. Modellene beskrives og plasseres i en tenkt 3D-verden. Deretter plasseres kamera og lyskilder uavhengig av modellene. Kameraets plassering og synsvidde bestemmer bildet av «scenen», som så omgjøres til et 2D-bilde ved hjelp av projeksjon.
3D-API
3D-APIer gir som regel programmereren følgende muligheter:
spesifisere primitiver og objekter
spesifisere betrakteren (eller kameraet)
spesifisere lyskilder
spesifisere materialegenskaper (bl.a. farge)
Både OpenGL, OpenGL ES og Direct3D er eksempler på 3D-APIer som tilbyr dette. Andre eksempler er Metal og Vulkan.
Programmereren ser grafikksystemet gjennom APIet. På den ene siden av APIet er vårt program, på den andre implementasjonen av funksjonene i APIet – dette kan være i form av programvare og/eller maskinvare.
Grafikk-API (Angel et al., 2015)
Dette fungerer ved at objektene (og verteksene) prosesseres i den rekkefølgen de genereres av applikasjonen. Omforming av vertekser til piksler i skjermbufferet utføres i en såkalt graphics pipeline, som vil bli nærmere forklart etter hvert.
3D-grafikkteknologier
Det finnes en rekke lavnivås 3D-APIer, men de mest utbredte er følgende:
Direct3D (Microsoft)
OpenGL (Khronos Group)
OpenGL ES (Khronos Group)
Vulkan (Khronos Group)
Metal (Apple)
Følgende Javascript 3D-APIer bygger på nevnte 3D-APIer:
WebGL - JavaScript-API over OpenGL ES (WebGL, 2026)
WebGPU - JavaScript-API over Vulkan, Metal og Direct3D 12 (WebGPU, 2026).
Både WebGL og WebGPU er JavaScript-APIer over henholdsvis OpenGL ES og Vulkan, Metal, Direct3D 12 (det er imidlertid også mulig å bruke WebGPU fra andre språk, som kan kjøre utenfor nettleseren). WebGPU er ikke et tema i disse notatene.
WebGL vil derimot bli mye omtalt etter hvert siden dette er gjennomgangstemaet i disse notatene.
Det kan også nevnes at Googles Android har et tilsvarende API mot OpenGL ES.
I tillegg finnes flere høyerenivås APIer, som OGRE, jMonkeyEngine og MonoGame (etterfølgeren til Microsofts XNA, se monogame.net). Java3D er et Java-API for 3D-grafikk som kjører over OpenGL eller Direct3D, men det er ikke lenger under aktiv utvikling.
For nettleseren finnes det også høynivås APIer, som Three.js (threejs.org) og Babylon.js (babylonjs.com).
Begge disse er basert på WebGL (og etter hvert også WebGPU).
Direct3D
Direct3D er en del av Microsofts DirectX-plattform. DirectX var i utgangspunktet en samling APIer for utvikling av spill,
og omfattet APIer som DirectDraw (2D), DirectMusic, DirectPlay m.fl.
Direct3D er i utgangspunktet kun tilgjengelig på Windows-plattformene, i tillegg til Xbox-konsollene.
APIet er optimalisert for 3D-grafikk til datamaskinspill. Direct3D bruker tilgjengelig maskinvarestøtte, dvs. grafikkort dersom det er tilgjengelig.
Dette betyr at den beregningskrevende koden kjører på grafikkprosessoren (GPU) – i stedet for på maskinens CPU.
Ved hjelp av Direct3D kan man utvikle både 2D- og 3D-grafikkapplikasjoner.
Direct3D gir programmereren et API mot grafikkortets drivere. Figuren under viser en oversikt over Direct3D sin plassering i forhold til applikasjon,
grafikkortdrivere og maskinvare.
Direct3D, lagdeling
Direct3D er et lavnivås API som i prinsippet gir tilgang til alle grafikkortets funksjoner. Fra og med Direct3D 12 (2015) er APIet «eksplisitt»: programmereren har mer direkte kontroll over minne og kommandokøer, på samme måte som i Vulkan og Metal.
OpenGL
OpenGL (opengl.org) er et 3D-API for utvikling av portable 2D- og 3D-applikasjoner. OpenGL ble introdusert i 1992 av Silicon Graphics Inc. (SGI) og er svært utbredt. Det brukes i forbindelse med Computer Aided Design (CAD), virtuell virkelighet, medisinske applikasjoner, ulike typer simuleringer og datamaskinspill.
Det finnes støtte for OpenGL på de fleste operativsystemplattformer, inkludert de fleste UNIX-varianter, alle aktuelle Windows-varianter og Linux. Apple har imidlertid markert OpenGL som utdatert (deprecated) i macOS fra og med versjon 10.14 (2018) og anbefaler Metal i stedet.
Det finnes språkbindinger til blant annet C, C++, Java, Python, C#, Fortran og Ada. OpenGL er leverandøruavhengig og ble i utgangspunktet administrert av OpenGL Architecture Review Board, med bred støtte fra industrien. I 2006 ble administrasjonen av OpenGL flyttet til Khronos Group (Khronos Group, About). Det kan nevnes at Microsoft var med her inntil 2003.
OpenGL bruker tilgjengelig maskinvare/grafikkort og GPU dersom dette er tilgjengelig på maskinen som kjører OpenGL-applikasjonen.
Fordelen med OpenGL er den brede støtten, den åpne arkitekturen, samt at det finnes støtte for APIet på de fleste plattformer. OpenGL er et rent grafikkbibliotek og ikke spesielt rettet mot spillutvikling (selv om det er mye brukt til dette). Siste versjon er OpenGL 4.6 fra 2017.
OpenGL ES og WebGL
En variant av OpenGL, OpenGL ES (Khronos Group, OpenGL ES), der ES står for Embedded Systems, brukes av plattformer som Android, Apple/iOS og Sony PlayStation. I tillegg ble det ved innføringen av HTML5-standarden også mulig å lage 3D-applikasjoner via WebGL, som også er basert på OpenGL ES. Programmering mot WebGL-APIet gjøres ved hjelp av JavaScript.
OpenGL ES er et subsett av standard OpenGL og finnes i flere versjoner. Figuren under viser sammenhengen mellom disse. Her ser vi at OpenGL ES 1.1 og OpenGL ES 2.0 er inkompatible versjoner. Versjon 1.1 var et subsett av OpenGL 1.5. Forskjellen på disse versjonene og de etterfølgende er at det fra og med OpenGL 2.0 / GL ES 2.0 ble innført programmerbare shadere. Dette gjør at programmereren har mye mer kontroll på hvordan det endelige 3D-bildet fremstår på skjermen. Det gjøres ved å lage såkalte «shaderprogrammer» som utføres av GPUen. Tidligere var ikke dette mulig. OpenGL ES 2.0 er et subsett av OpenGL 2.0, mens OpenGL ES 3.0 er et subsett av OpenGL 3.3.
Sammenhengen mellom OpenGL og OpenGL ES
WebGL finnes i to versjoner: WebGL 1.0 (basert på OpenGL ES 2.0) og WebGL 2.0 (basert på OpenGL ES 3.0).
WebGL 2 har vært støttet i alle store nettlesere siden 2022 og gir blant annet vertex array objects, instansiert tegning,
3D-teksturer og en nyere versjon av shaderspråket.
Vulkan
Vulkan er et lavnivås 3D-API som ble introdusert i 2016 av Khronos Group. Dette er en åpen standard som støttes av flere grafikkortprodusenter, inkludert AMD, Intel og NVIDIA.
Vulkan er designet for å gi utviklere mer direkte kontroll over GPUen og redusere overhead sammenlignet med OpenGL og Direct3D.
Vulkan støtter flere OS-plattformer, inkludert Windows, Linux og Android. Khronos Group anser i dag Vulkan som OpenGLs etterfølger.
Til tross for dette vil sannsynligvis OpenGL/ES fortsatt støttes av maskinvareprodusenter i mange år fremover.
OpenGL/ES og Vulkan bruker helt ulike programmeringsmodeller, men deler allikevel mange grunnleggende konsepter som shaderprogrammering,
transformasjoner, graphics pipeline og bruk av vertekser og teksturer.
Man kan derfor trygt fortsatt bruke OpenGL/ES i undervisningssammenheng for å lære grunnleggende 3D-konsepter.
Metal
Metal er, på samme måte som Vulkan, også et lavnivås 3D-API, men det er utviklet av Apple og er kun tilgjengelig på Apple-plattformer som macOS, iOS og iPadOS.
Dette er en proprietær løsning som er tett integrert med Apples maskinvare og operativsystemer.
Metal gir utviklere tilgang til grafikk- og databehandlingsfunksjoner på Apple-maskinvare, og er optimalisert for ytelse og effektivitet.
WebGPU
WebGPU er et relativt nytt 3D-API som er under utvikling av W3C. På samme måte som med WebGL-applikasjoner kjøres WebGPU-applikasjoner direkte i nettlesere.
De vanligste nettlesere støtter i dag WebGPU. WebGPU er designet for å gi bedre ytelse og effektivitet sammenlignet med WebGL.
På samme måte som WebGL programmeres WebGPU-applikasjoner ved hjelp av JavaScript, men WebGPU har et eget shaderspråk - WGSL (WebGPU Shading Language).
WebGPU er basert på underliggende grafikk-APIer som Vulkan, Metal og Direct3D 12.
Shadere og graphics pipeline
I disse notatene fokuserer vi på OpenGL ES, WebGL og tilhørende shaderprogrammering. WebGPU, Vulkan og/eller Metal omtales derfor ikke mer enn det som allerede er nevnt.
Videre skal vi konsentrere oss om shaderbegrepet og det vi kaller «graphics pipeline» i sammenheng med OpenGL og OpenGL ES.
OpenGL og OpenGL ES definerer en prosess for å konvertere vertekser, teksturer m.m. til et bilde som kan vises på skjermen. Prosessen er beskrevet som en graphics/rendering pipeline. Vi kan sammenligne dette med et slags «transportbånd» der verteks- og pikseloperasjoner utføres i ulike steg. Dette er vist i figuren under:
Forenklet rendering-/graphics pipeline
En shader er et program som kjøres av GPUen, og som regel består et shaderprogram av flere delprogrammer/funksjoner. Shaderspråket knyttet til OpenGL ES kalles GLSL ES og har en C-lignende syntaks.
I klientprogrammet, f.eks. WebGL/JavaScript, spesifiseres punkter/vertekser. Verteksene definerer igjen primitiver/trekanter som utgjør modellen(e) som skal tegnes på skjermen.
Primitiv og modell
Hver trekant spesifiseres ved hjelp av tre vertekser. Hver verteks har en x-, y- og z-verdi.
Verteksene legges typisk i et array/buffer og leveres til GPUen. Første steg på veien er verteksshaderen. Dette er, som nevnt, et lite program som kjører for hver enkelt verteks i modellen.
Verteksshaderen manipulerer (transformerer) verteksen i henhold til kriterier gitt av klientprogrammet. Dette kan f.eks. bety at alle vertekser flyttes litt langs x-aksen, som igjen betyr at hele modellen flyttes litt langs x-aksen. En annen mulighet er at modellen skal roteres noen grader om z-aksen – alle verteksene må da roteres om z. Hvor mye modellen skal flyttes eller roteres, bestemmes fra klientprogrammet ved å sende en transformasjonsmatrise til shaderen.
Deretter vil verteksene settes sammen til primitiver («primitive assembly»), slik at de er klare til å bli fylt med farger (piksler). Dette skjer i neste steg – her beregnes hvilke fragmenter (tenk på et fragment som en piksel) som inngår i hvert primitiv (trekant).
For hvert fragment kjøres deretter fragmentshaderen. Dette er også et lite program som kjøres av GPUen. Siden denne kjører for hvert fragment, kan man som programmerer påvirke hvert enkelt fragment (piksel) før det vises på skjermen. Resultatet fra fragmentshaderen må være en farge for aktuell piksel, som til slutt ender opp i framebufferet og på skjermen.
Figuren under illustrerer dette:
Enda mer forenklet rendering-/graphics pipeline
Når man bruker OpenGL ES 2.0 eller nyere, må man bruke verteks- og fragmentshadere for i det hele tatt å få noe frem på skjermen.
NB! En verteks er ikke det samme som et punkt på skjermen. En verteks er et (virtuelt) punkt i 3D-rommet som brukes til å definere primitiver (f.eks. trekanter). Hittil har vi sagt at primitiver enten er linjer eller trekanter (evt. mangekanter/polygoner, som støttes av OpenGL, men ikke av OpenGL ES).
I WebGL/OpenGL ES finnes det også et punktprimitiv. Dette betyr at det er mulig å tegne en enkelt verteks som et punkt. Man kan også bestemme størrelsen på punktet. Mer om dette etter hvert.
GLSL / GLSL ES
Shaderprogrammene, dvs. verteksshader og fragmentshader, er i praksis to programmer/funksjoner som kjøres av GPUen. Verteksshaderen kjører for hver verteks, mens fragmentshaderen kjører for hvert fragment (piksel).
Disse programmeres ved hjelp av et eget shaderspråk som kalles OpenGL Shading Language (GLSL). Syntaksen til GLSL er C-lignende, og språket er dermed relativt enkelt å lære dersom man har programmeringsbakgrunn. OpenGL ES har sin variant, som heter GLSL ES (Khronos Group, GLSL ES).
Vi kommer tilbake til shadere etter hvert, siden dette er en fundamental del av OpenGL ES og dermed også WebGL.
Ray tracing – et alternativ til rasterisering
"Graphics pipeline" beskrevet over er basert på rasterisering: modellen sendes gjennom pipelinen ett primitiv om gangen,
og for hvert primitiv (trekant) beregnes hvilke fragmenter (piksler) det dekker. Pipelinen «vet» dermed lite om scenen som helhet – når
fragmentshaderen kjører for én piksel, har den i utgangspunktet ingen kunnskap om de andre objektene i scenen.
Nettopp derfor er effekter som speilinger, gjennomsiktighet og skygger krevende å få til med rasterisering.
Dette er effekter som i så fall må løses med egne teknikker, som f.eks. shadow maps, environment maps o.l.
Ray tracing snur problemstillingen på hodet. I stedet for å spørre «hvilke piksler dekker denne trekanten?»,
spør man «hva ser jeg gjennom denne pikselen?». For hver piksel i bildet sendes en stråle (en ray) fra kameraet,
gjennom pikselen på bildeplanet og videre ut i scenen. Deretter beregnes hvilket objekt strålen først treffer,
og fargen til pikselen bestemmes ut fra materialet, lyset og geometrien i akkurat det treffpunktet.
Metoden er en direkte etterligning av hvordan lys oppfører seg i den fysiske verden – men baklengs. I virkeligheten går lyset fra lyskilden, spretter rundt i scenen og ender opp i øyet vårt. De aller fleste lysstrålene treffer aldri øyet, så det ville vært svært lite effektivt å simulere dem alle. Derfor sporer man i stedet strålene fra kameraet og ut i scenen, slik at man kun regner på det lyset som faktisk bidrar til bildet.
Rasterisering eller ray tracing?
Kort oppsummert:
Rasterisering er raskt og velegnet til sanntid (spill, simulatorer, VR).
Realistisk lys og speiling må tilnærmes med ulike teknikker.
Ray tracing gir tilnærmet fysisk korrekt lys, skygge, speiling og brytning nesten «gratis» som en konsekvens
av selve metoden, men er langt mer beregningskrevende.
Historisk har derfor ray tracing hørt hjemme i offline rendring, der man kan bruke sekunder eller timer på ett enkelt bilde:
film, animasjon, arkitekturvisualisering og reklame. Verktøy som Blender (Cycles), Pixars RenderMan og V-Ray er eksempler på dette.
Rasterisering har på sin side vært enerådende i sanntidsgrafikk.
Dette skillet er i ferd med å viskes ut. Fra og med NVIDIAs RTX-serie (2018) har grafikkortene fått egne
maskinvareenheter (RT-kjerner) som akselererer det mest kostbare steget i ray tracing,
nemlig å finne ut hvilket objekt en stråle treffer. Tilsvarende støtte finnes i dag også fra AMD, Intel og Apple.
På API-siden er dette eksponert gjennom blant annet DirectX Raytracing (DXR) og Vulkan Ray Tracing.
WebGL har ingen innebygd støtte for ray tracing, men er, som beskrevet over, basert på verteks- og fragmentshadere og rasterisering.
Ray tracing er per i dag (august 2026) heller ikke en del av WebGPU-standarden.
Siden OpenGL ES og WebGL er basert på rasterisering, vil vi i disse notatene fokusere på dette.
OpenGL, Direct3D eller Vulkan/Metal?
Her er det selvfølgelig delte meninger. Fordelen med OpenGL / OpenGL ES er åpenheten og støtten for mange plattformer. Ulempen er at det kan oppleves som relativt tungt å komme i gang med APIet. OpenGL er tiltenkt alle typer 3D-applikasjoner, og ikke spesielt utviklet med tanke på datamaskinspill.
Direct3D (en del av DirectX) ble i utgangspunktet utviklet med tanke på spill. Direct3D kan programmeres fra C++ og fra .NET-språk som C#. En av ulempene er at DirectX er et proprietært Microsoft-API som bare kjører på Windows og Xbox.
Vulkan, Direct3D 12 og Metal er alle lavnivå-APIer som gir høyere ytelse, men som har en høyere læringsterskel og krever betydelig mer kode enn OpenGL/ES.
Metal er på sin side proprietært og kun tilgjengelig på Apple-plattformer.
[1] Her brukes begrepene objekt og modell om hverandre, men i samme betydning – en 3D-modell beskrevet av et sett 3D-punkter/koordinater.
I denne sammenheng omtales teknologi som kombinasjon av programmeringsspråk og 3D API. Under er noen aktuelle teknologier for utvikling av 3D datamaskingrafikk:
C++ og OpenGL
Java og OpenGL (JOGL).
C++ og Direct3D
C# og Monogame (tidligere XNA)
Android, Java og Open GL ES
Javascript, WebGL, og OpenGL ES
Javascript og Three.js
Det finnes også andre varianter som for eksempel Microsofts Windows Presentation Foundation som også tilbyr et 3D API.
Dette er imidlertid ikke så omfattende som de nevnte og er tiltenkt enklere animasjoner og utvikling av 3D brukergrensesnittelementer.
C++ og OpenGL
Bruk av C++ sammen med OpenGL er kanskje den mest utbredte måten å anvende OpenGL på.
Det finnes C++ kompilatorer for de fleste operativsystemplattformer. Det samme gjelder OpenGL APIet.
En variant er å bruke C++ og Qt-biblioteket fra The Qt Company sammen med OpenGL.
Java OpenGL (JOGL)
Dette lar oss bruke Java til å programmere mot OpenGL APIet. JOGL er en referanseimplementasjon av JSR 231 (Java Bindings for OpenGL). Dette er ikke det samme som Java3D som opererer på et høyere abstraksjonsnivå.
C++ og Direct3D
Vi kan selvfølgelig kode direkte mot DirectX APIet vha. unmanaged C++. Dette krever at vi har installert DirectX SDKen. Microsoft Visual Studio er et egnet verktøy for denne type utvikling.
C# og Monogame
MonoGame.net (www.monogame.net) er et åpen kildekode-prosjekt.
Monogame er et kryssplattformverktøy som lar oss skrive C#-kode som deretter kan kompileres til aktuell plattform.
Monogame støtter per i dag (2026) følgende plattformer: Windows, macOS, Linux, Android, iOS, iPadOS, PlayStation 4, PlayStation 5, Xbox One og Nintendo Switch.
Avhengig av plattform som det kompileres til brukes OpenGL, OpenGL ES eller DirectX i bunnen.
Android og Open GL ES
Android støtter OpenGL ES som et rent Java eller Kotlin-API. Dersom man behersker Android/Java/Kotlin og samtidig er kjent med WebGL/Javascript vil overgangen til 3D-grafikk på Android-plattformen være enkel.
Introduksjon til WebGL, Javascript og Open GL ES
WebGL
Ved hjelp Javascript og WebGL-APIet kan man utvikle applikasjoner som kan generere og vise 3D-grafikk direkte i nettleseren uten bruk av spesielle plugins e.l. WebGL er basert på OpenGL ES (Khronos Group, OpenGL ES) og brukes sammen med HTML5 og Javascript.
Figuren under viser sammenhengen.
All tegning gjøres på et et HTML5 «canvas» element som tilsvarer et rektangulært område i nettleseren. HTML5 canvas-elementet ble opprinnelig tatt med i HTML-standarden for å kunne tegne 2D grafikk men brukes også til 3D.
Canvas
I 2009 etablerte Khronos-gruppen (www.khronos.org) en arbeidsgruppe for WebGL som nå består av flere nettleserprodusenter som Apple, Google, Mozilla og Opera.
Khronos-gruppen ble etablert i år 2000 og er en «non-profit» organisasjon som utvikler og vedlikeholder standarder for diverse APIer, deriblant OpenGL, OpenGL ES, WebGL, OpenCL m.m.
WebGL versjon 1.0 ble «frosset» i mars 2011 og støtte for WebGL er nå blitt standard i de fleste store nettlesere som Chrome, Firefox, Edge, Vivaldi, Safari m.fl.
Med WebGL har man tilgang til maskinvareakselerert interaktiv 3D-grafikk direkte i nettleseren – dvs. nettleseren utnytter evt. grafikkort i datamaskinen.
Vha. WebGL kan man lage fullverdige 3D spill som kjører direkte i nettleseren. Distribusjonsmodellen blir dermed svært enkel, all kode ligger på en webserver og lastes ned til nettleseren på samme måte som vanlige web-sider. Det er heller ingen «royalties» involvert – WebGL er fritt tilgjengelig.
Siden WebGL er basert på OpenGL ES, som igjen er basert på OpenGL, vil utviklere som tidligere har brukt noen av OpenGL-APIene enkelt kunne ta i bruk WebGL.
WebGL 1.0 eller WebGL 2.0?
WebGL 1.0 er basert på OpenGL ES 2.0 og benytter shaderspråket GLSL ES versjon 1.0. WebGL 2.0 ble standardisert i 2017 og er basert på OpenGL ES versjon 3.0. WebGL 2.0 støtter derfor, i tillegg til GLSL ES 1.0, også shaderspråket GLSL ES 3.0.
WebGL 1.0 APIet gir tilgang til OpenGL ES 2.0 funksjonalitet mens WebGL 2.0 APIet også gir tilgang til OpenGL ES 3.0 spesifikk funksjonalitet. WebGL 1.0 programmer vil dermed også fungere med WebGL 2.0.
Kodeeksempler knyttet til disse notatene bruker WebGL 2.0.
Komme i gang med WebGL
Det er enkelt å komme i gang med WebGL – man trenger i prinsippet kun en teksteditor til å skrive Javascript-kode og en nettleser der man kan teste koden.
Javascript-kode kan debugges direkte i nettleseren (bruk f.eks. F12 på Windows eller Option+Commannd+i på Mac).
Javascript
Javascript ble opprinnelig utviklet av Netscape for å kunne tilføre dynamiske elementer på websider og lastes ned fra webserveren. Javascriptkoden, som gjerne er integrert i html-koden, kjøres lokalt av nettleseren.
Tradisjonelt har Javascript-kode kjørt interpreterende. I nyere versjoner av nettlesere JIT-kompileres gjerne koden før den kjøres. Dette for å øke ytelsen. Javascript støtter også (en form for) objektorientering.
Javascript har en Java-liknende syntaks men er ellers svært forskjellig fra programmeringsspråket Java.
Kort introduksjon til Three.js
Three.js (http://threejs.org/) er et Javascript bibliotek som gjør livet enklere for WebGL-utviklere. Three skjuler flere av de kompliserende elementene som man må forholde seg til i "ren" WebGL, som f.eks. shadere.
Ved hjelp av noen få linjer med kode kan man f.eks. vise en roterende, teksturert og belyst kube. Tilsvarende i ren WebGL krever mange titalls, om ikke hundrevis av kodelinjer.
Før vi sier mer om html5 og canvas går vi rett på et eksempel. Åpner man følgende html-fil i nettleseren vil man se en grønn kube:
Fila index.html
Fila demo1.js
Html-fila, index.html, refererer javascriptfila, demo1.js. I main-funksjonen til demo1.js opprettes et canvas-objekt, dvs. flaten som det skal tegnes på.
Dette legges til det DOM treet som en del av html-dokumentet. Deretter opprettes et renderer-objekt som brukes til å tegne/gjengi (rendre) hele scenen.
Videre opprettes et Three scene-objekt. Three bruker en såkalt scenegraf slik at alle elementer, som figurer/objekter, kamera og lyskilder legges til en hierarkisk organisert scenegraf.
Deretter opprettes et kameraobjekt. Her brukes et perspektivkamera. Hva de ulike parametrene betyr vil bli forklart senere.
Som standard står kameraet i origo og peker "innover", mot negativ z. Kameraet flyttes litt "utover" slik at det står i [0,0,3] og peker mot origo.
Siden kuben er tegnet om origo vil den fanges opp av kameraet og vises på skjermen. Kameraets kan også settes til å peke i andre retninger.
Mer om dette senere.
Man kan enkelt legge til flere kuber e.l. i sceneobjektet som også vil bli tegnet når man kaller render() på renderer-objektet.
Selve kuben genereres vha. et geometriobjekt og et materialobjekt. Disse indikerer henholdsvis "geometrien" (form og størrelse) og materialtype, dvs. en kombinasjon av farge,
belysning og eventuell tekstur. Her brukes kun farge slik at kuben fremstår som grønn på skjermen.
WebGL: 2 og 3D grafikk direkte i nettleseren
Vi fortsetter her med "ren" WebGL, dvs. uten bruk av Three.js.
HTML5 Canvas-elementet
I Three-eksemplet over brukte vi <canvas> elementet i html-fila. Ved hjelp av <canvas> og Javascript kan man, som vist, generere og vise/gjengi («rendre») både 2D og 3D-grafikk direkte i nettleseren.
Canvas og 2D
Eksemplet under viser hvordan man tegner 2D grafikk på canvas-elementet. HTML-fila ser slik ut:
tegnrektangel/index.html
I .html fila defineres et <script> av type "module". Her importeres en funksjon, main(), fra fila helloRektangel.js.
Funksjonen kalles så for å starte programmet. Funksjonen ligger i en egen Javascript-fil, og ser slik ut:
tegnrektangel/tegnRektangel.js
Her opprettes først et nytt div-element, som vi kaller "divWrapper". Derettes opprettes et canvas element
som "pakkes inn" i et div-elementet. Canvas-elementet er det vi kan tegne både 2D og 3D grafikk på.
Det nye div-elementet legges så inn i document.body slik at det blir en del av html-dokumentet.
Deretter settes størrelse på canvaset til 960x480. Vi gir divWrapper id'en "my2DCanvas".
Deretter hentes en referanse, gl ("graphics library"), til en 2D kontekst.
Vha. denne kan man tegne enkel 2D grafikk vha. fillRect() o.l.
Som vi skal se i neste avsnitt følger vi omtrent samme oppskrift når vi skal tegne 3D grafikk vha. WebGL.
Den viktigste forskjellen er at kontekst-objektet hentes ut på følgende måte:
let gl = canvasElem.getContext('webgl2');
Den andre store forskjellen er måten man bruker gl-objektet til å kalle på WebGL/OpenGL ES-metoder/funksjoner for å få generert 3D grafikk. Dette gjøres på en helt annen måte enn hva man gjør i forhold til 2D grafikk.
Canvas og 3D
I denne gjennomgangen skal vi se hvordan man bruker WebGL-kontekst til å tegne på. I det første eksemplet, helloCanvas, setter vi kun bakgrunnsfargen.
hellocanvas/index.html
I .html fila defineres et <script> av type "module". Her importeres en funksjon, helloCanvas(), fra fila helloCanvas.js.
Funksjonen kalles så for å starte programmet. Funksjonen helloCanvas() ser slik ut:
hellocanvas/helloCanvas.js
Ved hjelp av WebGLCanvas-klassen opprettes et WebGLCanvas-objekt. WebGLCanvas-klassen setter gl lik:
this.gl = canvasElem.getContext('webgl2');
I helloCanvas() kan vi så hente ut gl fra canvas-objektet.
Videre vil gl bli brukt til å utføre WebGL/OpenGL ES kall for å få generert 3D-grafikk på canvaset.
I eksemplet settes kun bakgrunnsfargen til canvaset vha. gl.clearColor(…) og gl.clear(…).
Legg merke til at koden importerer WebGLCanvas fra WebGLCanvas.js. Denne ser slik ut:
WebGL & 3D grafikk
Hvordan fremstiller man 3D objekter på en 2D skjerm? Dette er i korte trekk hva 3D datamaskingrafikk handler om. 3D grafikk kan fremstilles på flere måter vha. WebGL.
Modeller kan tegnes ”manuelt” vha. vertekser og primitiver i et tenkt 3D rom. Foreløpig kan vi tenke på en verteks som et punkt i rommet definert av en x,y og z-verdi mens et primitiv kan være et punkt, en linje eller en trekant som er definert av henholdsvis en, to eller tre vertekser.
Det er også mulig å laste inn modeller fremstilt i andre verktøy, som for eksempel 3D Studio Max, Blender o.l. Slike modeller er på tilsvarende måte konstruert av et (stort) antall vertekser og primitiver som behandles på samme måte som ”manuelle” modeller. Deretter vil verteksene gå gjennom flere transformasjoner, via shaderne og GPUen, før de til slutt ender opp som piksler på skjermen. I første omgang vil «manuelle modeller», dvs. der vi selv definerer vertekser og primitiver, gjennomgås.
Tegne på canvaset – bruk av shader
Shadere må brukes uansett hva vi skal tegne i WebGL. Hva er så en shader? For å kunne svar på dette må man se på hele prosessen, fra vi definerer vertekser til f.eks. en trekant i WebGL/Javascript-programmet til pikslene som utgjør denne trekanten ender opp i skjermbufret og til slutt på canvaset/skjermen.
Hele prosessen omtales som en «graphics/rendering pipeline», se figuren under:
Forenklet graphics/rendering pipeline
Til venstre ser vi Javascript/WebGL-programmet der man spesifiserer verteksene til modellen som skal tegnes.
Dersom vi ser på eksemplet med en enkel trekant består den av tre vertekser. Hver verteks vil bli sent til verteksshaderfunksjonen. Dette er et eget program/funksjon som mottar verteksene og ev. gjør diverse transformasjoner på disse. Legg merke til at det som skjer fra og med verteksshaderen og utover utføres av GPUen. Transformert verteks "videresendes" fra verteksshaderfunksjonen (og sendes videre i pipelinen) ved at gl_Position settes lik (ev. transformert) verteks (aVertexPosition).
Verteksene sammenstilles (slik at de utgjør en trekant) og deretter vil det bli beregnet hvilke piksler/fragmenter (foreløpig sier vi at et fragment tilsvarer en piksel – egentlig er et fragment en «kandidat» til å bli en piksel i skjermbufret, ikke alle fragmenter ender opp som piksler i skjermbufret) som skal til for å få tegnet denne trekanten.
For hver piksel/fragment som inngår i trekanten vil fragmentshaderen bli kjørt. Denne kan manipulere hver enkelt fragment. Fragmentshaderen må gi fragColor en fargeverdi (RGBA). Denne ender så opp i skjermbufret som til slutt vises på skjermen.
Foreløpig er denne gjennomgangen noe forenklet – det er flere steg som inngår, spesielt mellom fragmentshader og framebuffer, men disse er ikke tatt med her. Her er kun det de viktigste stegene vist.
Tegne et punkt
Vi starter så enkelt som overhodet mulig - det vil si å tegne et enkelt punkt på skjermen.
Det nye er her bruk av shadere. Shaderkoden er lagt i html-fila vha. script-tagger av type "x-shader/x-vertex" eller "x-shader/x-fragment".
Disse gis også en id.
Shaderfunksjonene er skrevet i shaderprogrammeringsspråket OpenGL ES Shading Language (Khronos Group, GLSL ES, 2009).
Dette er en variant av OpenGL GLSL. Dette språket har sin egen syntaks, datatyper, nøkkelord som attributes og uniforms osv.
Vi kommer tilbake til detaljene etter hvert. Vi starter med .html fila:
hellopoint/index.html
hellopoint/hellopoint.js
I html-fila ligger kildekoden til verteks- og fragment-shaderne som programmet bruker for å tegne punktet.
Begge shaderfunksjonene består av en main() funksjon. Legg merke til at vi bruker OGSL ES versjon 3,
derfor "#version 300 es" i starten av shaderfunksjonene (dette må ligge helt fremst i koden).
Javascript-koden bruker WebGL-APIet for å tegne et enkelt punkt på canvaset. Punktet består av EN verteks.
Etter hvert vil vi tegne mer kompliserte modeller som består av mange vertekser og primitiver.
Vi vil da også bruke såkalte vertex buffers for å lagre verteksene i GPUens minne.
I eksemplet over er det kun EN verteks og det er derfor ikke nødvendig å bruke vertex buffers.
I main() opprettes et WebGLCanvas-objekt, som inneholder gl-objektet. Dette representerer WebGL-APIet og brukes,
som vi ser, "overalt" i programmet til å utføre WebGL-kall.
Fra main() kalles initBaseShaders(). Denne funksjonen leser vertex- og fragmentshaderne fra html-fila.
Ved hjelp av klassen WebGLShaders kompileres shaderkoden. Funksjonen returnerer et objekt som blant annet inneholder det
kompilerte shaderProgram-objektet fra WebGLShaders-objektet.
Deretter kalles draw(), som starter med å renske skjermen/canvaset. Deretter aktiveres nevnte shaderProgram.
Ved hjelp av funksjonene connectPositionAttribute() aktiveres aVertexPosition i verteksshaderen.
Legg merke til at aVertexPosition er et "in"-parameter av type vec3 (vektor bestående av 3 elementer, slik [x, y, z]).
vec3 er en GLSL-datatype. "in" er også et nøkkelord i GLSL versjon 3 (i eldre versjoner brukte man her "attribute" i stedet for "in").
Funksjonen connectPositionAttribute() kaller på GLSL-funksjonen gl.vertexAttrib3f(…) som levererer EN verteksposisjon, [0,0,0],
til vertekshaderen. Vertekshaderen mottar denne via parametret aVertexPosition.
I shaderen er denne av type vec3. Vi ser at denne utvides til en vec4 når vi setter gl_Position.
Det siste elementet (w) settes her lik 1 (kommer tilbake til hvorfor dette gjøres).
Størrelsen på punktet bestemmes av gl_PointSize som her hardkodes til 50.
Denne kunne alternativt blitt sendt inn til shaderen vha. gl.vertexAttrib1f(aVertexPointSize, 50.0).
I så falle må man legge til et nytt "in"-parameter i verteksshaderen, aVertexPointSize,
og sette gl_PointSize = aVertexPointSize.
I siste linje kalles drawArrays(gl.POINTS, 0, 1).
Dette trigger verteksshaderen som setter gl_Position = aVertexPosition.
Variabelen gl_Position er en spesiell GLSL-variabel og verteksshaderen har som hovedoppgave å gi denne variabelen en verdi.
Som regel vil man gjøre diverse transformasjoner (matrisemultiplikasjoner) på aVertexPosition
og tilordne resultatet til gl_Position.
I eksemplet over utføres ingen transformasjon, gl_Position settes bare lik vec4(aVertexPosition, 1.0).
Variabelen gl_PointSize er også spesiell og brukes kun i tilfeller der man tegner punkter.
Her settes gl_PointSize lik 50 og bestemmer hvor stort punktet blir.
Som nevnt kunne man alternativt sendt inn punktstørrelsen som et parameter til verteksshaderen.
Etter at verteksshaderen har kjørt for alle vertekser (her kun en) vil det automatisk beregnes hvilke piksler som trengs for å tegne punktet.
For hver piksel som inngår i punktet kjøres fragmentshaderen. Tenk foreløpig på et fragment som det samme som en piksel.
Hovedoppgaven til fragmentshaderen er å returnere fargen til fragmentet.
I eksemplet over returneres en «hardkodet» farge, som betyr at alle piksler får samme farge.
Det er også mulig å knytte en farge til verteksene slik at fragmentene får farge basert på verteksfargene.
Man kan også sende inn en enkelt farge til fragmentshaderen vha. et såkalt uniform-parameter.
Mer om dette etter hvert.
Legg meke til at fragmentshaderen har en spesiell variabel, fragColor, som er en "out"-variabel og må settes til en fargeverdi.
Dette er en vec4 (RGBA) og verdiene må være mellom 0.0 og 1.0.
I eksemplet over settes fragColor lik vec4(1.0, 0.4, 1.0, 1.0), som betyr at fargen blir lilla.
Kjører man koden over i en standard nettleser vil det gi (omtrent) følgende resultat:
Med andre ord; et stort punkt i origo.
Tegne en trekant
En trekant består av tre vertekser og man bruker typisk et verteksarray og et verteksbuffer til dette. Følgende fem steg inngår for å få sendt et verteksarray til shaderen:
Opprett et bufferobjekt
gl.createBuffer()
Binde bufret – sette gjeldende buffer
gl.bindBuffer()
Skrive data/verteksarray til bufferet
gl.bufferData()
Kopler shaderparameter til bufferobjektet
gl.vertexAttribPointer()
«Enable» tilordninga (over):
gl.enableVertexAttribArray()
Følgende kodeeksempel viser dette:
hellotrianglesimple/index.html
Vi ser at denne refererer helloTrianglesimple() som ligger i helloTrianglesimple.js:
Resultate blir som vist under.
En enkel trekant bestående av tre vertekser
I funksjonen initBuffers() opprettes et VertexBuffer-objekt vha. et Float32Array-objekt, som igjen inneholder de tre verteksposisjonene.
Bufret må «aktiveres» ved at det bindes til gl.ARRAY_BUFFER før data fra positions-arrayet kopieres til bufret vha. gl.bufferData(…).
I draw() koples aVertexPosition til data i position-bufret vha. vertexAttribPointer(…) før bufret til slutt «enables».
Funksjonen vertexAttribPointer() forteller WebGL at data (posisjoner eller farger) skal hentes fra bufret som sist ble bundet vha. bindBuffer(…).
Funksjonsprototypen til vertexAtttribPointer() ser slik ut:
Første parameter indikerer hvilken parameter i verteksshaderen som skal motta data, andre parameter angir antall float-verdier per verteks (for posisjon er denne 3 og
for farger er den typisk 4). Det tredje parametret angir datatype som brukes (her gl.FLOAT). Nest siste parameter, strideToNextPieceOfData,
er aktuell når man knytter andre verdier, som f.eks. farge eller normalvektor, til verteksene. Det siste parametret angir hvor i bufret første verteks ligger (her indeks 0).
Deretter sendes det inn en fargeverdi, uFragmentColor. Her indikerer prefikset "u" at dette er en såkalt «uniform» parameter.
I dette tilfellet betyr det at alle fragmenter får samme farge. I fragmentshaderen ser vi at gl_FragColor settes lik uFragmentColor.
I draw() funksjonen renskes skjermen og drawArrays() kalles med gl.TRIANGLES som første parameter.
Det andre parametret antyder hvor i bufret første verteks skal hentes fra (her indeks 0) mens det siste parametret indikerer hvor mange vertekser som skal tegnes.
Det første parametret, gl.TRIANGLES, antyder hvordan verteksene skal sammenstilles. For hver verteks i som ligger i verteksbufret vil verteksshaderen kjøre.
Aktuell verteks vil komme som innparameter via aVertexPosition. Videre ser vi at verteksshaderen setter gl_Position lik aVertexPosition.
Mer om bindBuffer()
Anta at vi har definert følgende vertekstabell:
let positions = [-50.0, 50.0, 0.0,
-50.0,-50.0, 0.0,
50.0,-50.0, 0.0,
50.0, 50.0, 0.0];
var myBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, myBuffer);
Etter dette vil myBuffer være gjeldende buffer. Dette betyr at påfølgende bufferoperasjoner vil utføres vha. dette, inntil det "avbindes"
eller at et annet buffer settes som gjeldende vha. et nytt bindBuffer() kall.
Det første parametret til bindBuffer() kan ha følgende to verdier:
gl.ARRAY_BUFFER
gl.ELEMENT_ARRAY_BUFFER.
Dersom bufret inneholder verteksdata brukes ARRAY_BUFFER mens ELEMENT_ARRAY_BUFFER brukes dersom det inneholder indeksdata.
En figur/modell kan bestå av mange trekanter der flere av trekantene kan dele samme verteks.
I stedet for å lagre verteksdata flere ganger (i et verteksbuffer) lagres kun indeksen, i et indeksbuffer (mer om dette senere).
Deretter brukes typisk bufferData() for å fylle bufret med verteksdata:
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
Etter dette bør man kople fra bufret vha.:
gl.bindBuffer(gl.ARRAY_BUFFER, null);
Før man kan kalle på drawArrays() må man kople shaderparametre med til aktuelle bufferobjekter.
Kople shaderparametre til bufferobjekter
Etter at bufferobjektet er opprettet må dette koples til shaderparametre / attributter.
Eksempler på shaderattributt er a_Position definert i verteksshaderen.
Et verteksshaderattributt er assosiert med et, og kun et, bufferobjekt som vist i figuren under.
Figuren viser hvordan tre ulike shaderattributt, a_Position, a_Color og a_Normal er koplet til hvert sitt bufferobjekt.
Vi kopler shaderparametre til bufferobjektet slik:
Binde til aktuelt bufferobjekt.
Setter et shaderattributtet til å peke på bufret, dvs. aktiverte buffer.
Til slutt må attributtet gjøres aktivt («enable»).
Som allerede vist over aktiveres aktuelt buffer slik:
gl.bindBuffer(gl.ARRAY_BUFFER, myBuffer);
Ved hjelp av WebGL-funksjonen vertexAttribPointer() koples shaderattributtet (f.eks. a_Position) til verteksbufret:
let floatsPerVertex = 3;
let posAttrib = gl.getAttribLocation(gl.program, 'a_Position');
gl.vertexAttribPointer(posAttrib, floatsPerVertex, gl.FLOAT, false, 0, 0);
Funksjonen gl.getAttribLocation() returnerer shaderparametrets posisjon (i form av en integer) i shaderen.
Parametrene til vertexAttribPointer(index, size, type, norm, stride, offset) har følgende betydning:
Index (her posAttrib) er en indeks som refererer til attributtet a_Position definert i verteksshaderen. Dette får sin verdi vha. gl.getAttribLocation() som vist.
Size (her floatsPerVertex = 3) indikerer antall floats per verteks.
Type (her FLOAT) indikerer datatypen til verdiene i bufret.
Norm (her false). Vi bruker stort sett alltid false her.
Stride (her 0). Denne indikerer at verteksverdiene følger direkte etter hverandre. Dersom bufret også inneholder fargeverdier i tillegg til koordinatverdier vil dette parametret indikere «avstanden» mellom koordinatverdiene til hver enkelt verteks.
Offset (her 0). Indikerer startindeksen til hvor i bufret data skal hentes fra.
Deretter må «attributtpekeren», posAttrib, gjøres aktiv, slik:
gl.enableVertexAttribArray(posAttrib);
Verteksshaderen vil nå hente posisjonsdata fra assosiert verteksbuffer som vist i figuren under:
I dette tilfellet vil shaderen hente tre verdier i slengen pga. at verdien på andre parameter til vertexAttribPointer() er satt lik 3.
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)
Vertekser sendes vha. et vertexbuffer, 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. v0 og v1 gir en linje, v2 og v3 en annen linje osv.
gl.LINE_STRIP, 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.
Disse er illustrert under:
WebGL primitiver. Fra (Matsuda, K. et al, 2013).
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.
Triangle Strip
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.
Triangle Fan
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.
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).
Front & back facing polygon
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).
Dette er en enkel måte å optimalisere på – jo færre vertekser som videresendes i pipelinen jo mer effektivt program. 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:
Ulike farger angitt i hver av de tre verteksene
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 fargeverdier 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 fragmentshaderen vil RGB-verdiene for hvert fragment
som utgjør linja beregnes og videresendes til fragmentshaderens vColor parameter.
Dette kan illustreres slik:
Interpoleringsprosessen
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 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 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 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.
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 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 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().
Kople shaderparametre, f.eks. a_Position, til bufferobjektet vha. gl.vertexAttribPointer()
Dersom modellen består av mange primitiver med delte vertekser kan det være lurt å bruke drawElements() i stedet for 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.
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».
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)
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 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().
Det første argumentet er det samme som 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 UNSIGNED_INT.
Det siste argumentet, offset, indikerer første indeksverdi (eller egentlig en byte offset) i indeksbufret.