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.