En verden av 3D-grafikk kan være veldig skremmende å komme inn på. Enten du bare vil lage en interaktiv 3D-logo, eller designe et fullverdig spill, hvis du ikke kjenner prinsippene for 3D-gjengivelse, sitter du fast ved å bruke et bibliotek som trekker ut mange ting.
Å bruke et bibliotek kan være akkurat det rette verktøyet, og JavaScript har en fantastisk åpen kildekode i form av three.js . Det er noen ulemper ved å bruke ferdige løsninger, skjønt:
Selv om du bestemmer deg for å bruke et grafikkbibliotek på høyt nivå, kan du bruke det mer effektivt når du har grunnleggende kunnskap om tingene under panseret. Biblioteker kan også ha avanserte funksjoner, for eksempel ShaderMaterial
i three.js
. Å kjenne prinsippene for grafikkgjengivelse lar deg bruke slike funksjoner.
Målet vårt er å gi en kort introduksjon til alle nøkkelbegrepene bak gjengivelse av 3D-grafikk og bruk av WebGL til å implementere dem. Du vil se det vanligste som er gjort, som viser og flytter 3D-objekter i et tomt rom.
De endelig kode er tilgjengelig for deg å gaffel og leke med.
Det første du trenger å forstå er hvordan 3D-modeller er representert. En modell er laget av et maske av trekanter. Hver trekant er representert med tre hjørner for hvert av hjørnene i trekanten. Det er tre mest vanlige egenskaper knyttet til hjørner.
Posisjon er den mest intuitive egenskapen til et toppunkt. Det er posisjonen i 3D-rommet, representert med en 3D-vektor med koordinater. Hvis du vet de nøyaktige koordinatene til tre punkter i rommet, vil du ha all informasjonen du trenger for å tegne en enkel trekant mellom dem. For å få modeller til å se bra ut når de er gjengitt, er det et par ting til som må leveres til gjengiveren.
Tenk på de to modellene ovenfor. De består av de samme toppunktposisjonene, men ser likevel helt annerledes ut når de gjengis. Hvordan er det mulig?
Foruten å fortelle rendereren hvor vi vil at et toppunkt skal være plassert, kan vi også gi det et hint om hvordan overflaten er skrå i den nøyaktige posisjonen. Hintet er i form av overflatens normale på det spesifikke punktet på modellen, representert med en 3D-vektor. Følgende bilde skal gi deg en mer beskrivende titt på hvordan det håndteres.
Venstre og høyre overflate tilsvarer henholdsvis venstre og høyre ball i forrige bilde. De røde pilene representerer normaler som er spesifisert for et toppunkt, mens de blå pilene representerer gjengirens beregninger av hvordan normalen skal se etter alle punktene mellom toppunktene. Bildet viser en demonstrasjon for 2D-plass, men det samme prinsippet gjelder i 3D.
Det normale er et hint for hvordan lys vil belyse overflaten. Jo nærmere en lysstråles retning er normal, jo lysere er poenget. Å ha gradvise endringer i normal retning forårsaker lysgradienter, mens det å ha brå endringer uten endringer i mellom forårsaker overflater med konstant belysning over dem, og plutselige endringer i belysning mellom dem.
Den siste viktige egenskapen er teksturkoordinater, ofte referert til som UV-kartlegging. Du har en modell og en tekstur som du vil bruke på den. Teksturen har forskjellige områder på seg, som representerer bilder som vi vil bruke på forskjellige deler av modellen. Det må være en måte å markere hvilken trekant som skal representeres med hvilken del av teksturen. Det er her teksturkartlegging kommer inn.
For hvert toppunkt markerer vi to koordinater, U og V. Disse koordinatene representerer en posisjon på teksturen, med U som representerer den horisontale aksen, og V den vertikale aksen. Verdiene er ikke i piksler, men i prosent i bildet. Det nederste venstre hjørnet av bildet er representert med to nuller, mens det øverste høyre er representert med to.
En trekant er bare malt ved å ta UV-koordinatene til hvert toppunkt i trekanten, og bruke bildet som er fanget mellom disse koordinatene på teksturen.
Du kan se en demonstrasjon av UV-kartlegging på bildet ovenfor. Den sfæriske modellen ble tatt og kuttet i deler som er små nok til å bli flatet ut på en 2D-overflate. Sømmene der kuttene ble gjort er markert med tykkere linjer. En av lappene er uthevet, slik at du fint kan se hvordan ting stemmer overens. Du kan også se hvordan en søm gjennom midten av smilet plasserer deler av munnen i to forskjellige flekker.
Trådrammene er ikke en del av teksturen, men bare lagt over bildet slik at du kan se hvordan ting kartlegges sammen.
Tro det eller ei, dette er alt du trenger å vite for å lage din egen enkle modellaster. De OBJ-filformat er enkel nok til å implementere en parser i noen få kodelinjer.
Filen viser toppunktposisjoner i et v
format, med en valgfri fjerde flottør, som vi vil ignorere, for å holde ting enkelt. Vertex-normaler er representert på samme måte med vn
. Til slutt er teksturkoordinater representert med vt
, med en valgfri tredje flottør som vi skal ignorere. I alle tre tilfeller representerer flottørene de respektive koordinatene. Disse tre egenskapene er samlet i tre matriser.
Ansikter er representert med grupper av hjørner. Hvert toppunkt er representert med indeksen til hver av egenskapene, hvor indeksene starter ved 1. Det er forskjellige måter dette er representert på, men vi vil holde oss til f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
format, som krever at alle tre egenskapene skal oppgis, og begrenser antall hjørner per ansikt til tre. Alle disse begrensningene gjøres for å holde lasteren så enkel som mulig, siden alle andre alternativer krever litt ekstra triviell behandling før de er i et format som WebGL liker.
Vi har stilt mange krav til fillasteren vår. Det høres kanskje begrensende ut, men 3D-modelleringsapplikasjoner har en tendens til å gi deg muligheten til å sette disse begrensningene når du eksporterer en modell som en OBJ-fil.
Den følgende koden analyserer en streng som representerer en OBJ-fil, og oppretter en modell i form av en rekke ansikter.
function Geometry (faces) [] // Parses an OBJ file, passed as a string Geometry.parseOBJ = function (src) { var POSITION = /^vs+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var NORMAL = /^vns+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var UV = /^vts+([d.+-eE]+)s+([d.+-eE]+)/ var FACE = /^fs+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)(?:s+(-?d+)/(-?d+)/(-?d+))?/ lines = src.split('
') var positions = [] var uvs = [] var normals = [] var faces = [] lines.forEach(function (line) { // Match each line of the file against various RegEx-es var result if ((result = POSITION.exec(line)) != null) { // Add new vertex position positions.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = NORMAL.exec(line)) != null) { // Add new vertex normal normals.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = UV.exec(line)) != null) { // Add new texture mapping point uvs.push(new Vector2(parseFloat(result[1]), 1 - parseFloat(result[2]))) } else if ((result = FACE.exec(line)) != null) { // Add new face var vertices = [] // Create three vertices from the passed one-indexed indices for (var i = 1; i <10; i += 3) { var part = result.slice(i, i + 3) var position = positions[parseInt(part[0]) - 1] var uv = uvs[parseInt(part[1]) - 1] var normal = normals[parseInt(part[2]) - 1] vertices.push(new Vertex(position, normal, uv)) } faces.push(new Face(vertices)) } }) return new Geometry(faces) } // Loads an OBJ file from the given URL, and returns it as a promise Geometry.loadOBJ = function (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(Geometry.parseOBJ(xhr.responseText)) } } xhr.open('GET', url, true) xhr.send(null) }) } function Face (vertices) this.vertices = vertices function Vertex (position, normal, uv) new Vector3() this.normal = normal function Vector3 (x, y, z) function Vector2 (x, y) 0 this.y = Number(y)
Geometry
strukturen inneholder de nøyaktige dataene som trengs for å sende en modell til grafikkortet for prosessering. Før du gjør det, vil du sannsynligvis ha muligheten til å flytte modellen rundt på skjermen.
Alle punktene i modellen vi lastet er i forhold til koordinatsystemet. Hvis vi vil oversette, rotere og skalere modellen, er alt vi trenger å gjøre å utføre denne operasjonen på koordinatsystemet. Koordinatsystem A, i forhold til koordinatsystem B, er definert av senterets posisjon som en vektor p_ab
, og vektoren for hver av aksene, x_ab
, y_ab
, og z_ab
, som representerer retningen til den aksen. Så hvis et punkt beveger seg med 10 på x
akse for koordinatsystem A, deretter - i koordinatsystemet B - vil det bevege seg i retning av x_ab
multiplisert med 10.
All denne informasjonen lagres i følgende matriseform:
x_ab.x y_ab.x z_ab.x p_ab.x x_ab.y y_ab.y z_ab.y p_ab.y x_ab.z y_ab.z z_ab.z p_ab.z 0 0 0 1
Hvis vi vil transformere 3D-vektoren q
, må vi bare multiplisere transformasjonsmatrisen med vektoren:
q.x q.y q.z 1
Dette får punktet til å bevege seg med q.x
langs den nye x
akse, av q.y
langs den nye y
akse, og av q.z
langs den nye z
akser. Til slutt får det poenget til å bevege seg i tillegg med p
vektor, som er grunnen til at vi bruker en som det siste elementet i multiplikasjonen.
Den store fordelen med å bruke disse matrisene er det faktum at hvis vi har flere transformasjoner å utføre på toppunktet, kan vi slå dem sammen til en transformasjon ved å multiplisere matrisen deres før transformasjonen av toppunktet selv.
Det er forskjellige transformasjoner som kan utføres, og vi tar en titt på de viktigste.
Hvis ingen transformasjoner skjer, så p
vektor er en nullvektor, x
vektor er [1, 0, 0]
, y
er [0, 1, 0]
og z
er [0, 0, 1]
. Fra nå av vil vi referere til disse verdiene som standardverdiene for disse vektorene. Å bruke disse verdiene gir oss en identitetsmatrise:
1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1
Dette er et godt utgangspunkt for kjedetransformasjoner.
Når vi utfører oversettelse, vil alle vektorene bortsett fra p
vektor har standardverdiene. Dette resulterer i følgende matrise:
1 0 0 p.x 0 1 0 p.y 0 0 1 p.z 0 0 0 1
Å skalere en modell betyr å redusere mengden som hver koordinat bidrar til posisjonen til et punkt. Det er ingen ensartet forskyvning forårsaket av skalering, så p
vektor beholder standardverdien. Standardaksevektorene skal multipliseres med deres respektive skaleringsfaktorer, noe som resulterer i følgende matrise:
s_x 0 0 0 0 s_y 0 0 0 0 s_z 0 0 0 0 1
Her s_x
, s_y
, og s_z
representerer skaleringen på hver akse.
Bildet over viser hva som skjer når vi roterer koordinatrammen rundt Z-aksen.
Rotasjon gir ingen ensartet forskyvning, så p
vektor beholder standardverdien. Nå blir ting litt vanskeligere. Rotasjoner får bevegelse langs en bestemt akse i det opprinnelige koordinatsystemet til å bevege seg i en annen retning. Så hvis vi roterer et koordinatsystem med 45 grader rundt Z-aksen, beveger vi oss langs x
aksen til det opprinnelige koordinatsystemet forårsaker bevegelse i en diagonal retning mellom x
og y
akse i det nye koordinatsystemet.
For å gjøre det enkelt, viser vi deg bare hvordan transformasjonsmatriser ser ut for rotasjoner rundt hovedaksene.
Around X: 1 0 0 0 0 cos(phi) sin(phi) 0 0 -sin(phi) cos(phi) 0 0 0 0 1 Around Y: cos(phi) 0 sin(phi) 0 0 1 0 0 -sin(phi) 0 cos(phi) 0 0 0 0 1 Around Z: cos(phi) -sin(phi) 0 0 sin(phi) cos(phi) 0 0 0 0 1 0 0 0 0 1
Alt dette kan implementeres som en klasse som lagrer 16 tall, og lagrer matriser i en kolonne-stor ordre .
function Transformation () { // Create an identity transformation this.fields = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] } // Multiply matrices, to chain transformations Transformation.prototype.mult = function (t) { var output = new Transformation() for (var row = 0; row <4; ++row) { for (var col = 0; col < 4; ++col) { var sum = 0 for (var k = 0; k < 4; ++k) { sum += this.fields[k * 4 + row] * t.fields[col * 4 + k] } output.fields[col * 4 + row] = sum } } return output } // Multiply by translation matrix Transformation.prototype.translate = function (x, y, z) // Multiply by scaling matrix Transformation.prototype.scale = function (x, y, z) // Multiply by rotation matrix around X axis Transformation.prototype.rotateX = function (angle) // Multiply by rotation matrix around Y axis Transformation.prototype.rotateY = function (angle) // Multiply by rotation matrix around Z axis Transformation.prototype.rotateZ = function (angle)
Her kommer nøkkeldelen av å presentere objekter på skjermen: kameraet. Det er to viktige komponenter til et kamera; nemlig posisjonen og hvordan den projiserer observerte gjenstander på skjermen.
Kameraposisjonen håndteres med ett enkelt triks. Det er ingen visuell forskjell mellom å flytte kameraet en meter fremover, og å flytte hele verden en meter bakover. Så naturlig gjør vi sistnevnte ved å bruke matrisens inverse som en transformasjon.
Den andre nøkkelkomponenten er måten observerte objekter projiseres på linsen. I WebGL ligger alt som er synlig på skjermen i en boks. Boksen spenner mellom -1 og 1 på hver akse. Alt synlig er innenfor den boksen. Vi kan bruke den samme tilnærmingen til transformasjonsmatriser for å lage en projeksjonsmatrise.
Den enkleste projeksjonen er ortografisk projeksjon . Du tar en boks i rommet, som angir bredde, høyde og dybde, med forutsetningen om at sentrum er i nullposisjon. Deretter endrer projeksjonen størrelsen på boksen slik at den passer inn i den tidligere beskrevne boksen der WebGL observerer objekter. Siden vi vil endre størrelsen på hver dimensjon til to, skalerer vi hver akse med 2/size
, hvorved size
er dimensjonen til den respektive aksen. En liten advarsel er det faktum at vi multipliserer Z-aksen med en negativ. Dette gjøres fordi vi vil snu retningen til den dimensjonen. Den endelige matrisen har denne formen:
2/width 0 0 0 0 2/height 0 0 0 0 -2/depth 0 0 0 0 1
Vi vil ikke gå gjennom detaljene for hvordan denne projeksjonen er designet, men bare bruke endelig formel , som er ganske mye standard nå. Vi kan forenkle det ved å plassere projeksjonen i nullposisjon på x- og y-aksen, slik at høyre / venstre og øvre / nedre grense er lik width/2
og height/2
henholdsvis. Parameterne n
og f
representerer near
og far
klippeplaner, som er den minste og største avstanden et punkt kan være å fange av kameraet. De er representert av parallelle sider av frustum i bildet ovenfor.
En perspektivprojeksjon blir vanligvis representert med en synsfelt (vi bruker den vertikale), størrelsesforholdet og de nærmeste og fjerne flyavstandene. Denne informasjonen kan brukes til å beregne width
og height
, og deretter kan matrisen opprettes fra følgende mal:
2*n/width 0 0 0 0 2*n/height 0 0 0 0 (f+n)/(n-f) 2*f*n/(n-f) 0 0 -1 0
For å beregne bredde og høyde kan følgende formler brukes:
height = 2 * near * Math.tan(fov * Math.PI / 360) width = aspectRatio * height
FOV (synsfelt) representerer den vertikale vinkelen som kameraet fanger med objektivet. Sideforholdet representerer forholdet mellom bildebredde og høyde, og er basert på dimensjonene på skjermen vi gjengir til.
Nå kan vi representere et kamera som en klasse som lagrer kameraposisjonen og projeksjonsmatrisen. Vi trenger også å vite hvordan vi skal beregne inverse transformasjoner. Å løse generelle matriseinversjoner kan være problematisk, men det er en forenklet tilnærming for vårt spesielle tilfelle.
function Camera () { this.position = new Transformation() this.projection = new Transformation() } Camera.prototype.setOrthographic = function (width, height, depth) { this.projection = new Transformation() this.projection.fields[0] = 2 / width this.projection.fields[5] = 2 / height this.projection.fields[10] = -2 / depth } Camera.prototype.setPerspective = function (verticalFov, aspectRatio, near, far) { var height_div_2n = Math.tan(verticalFov * Math.PI / 360) var width_div_2n = aspectRatio * height_div_2n this.projection = new Transformation() this.projection.fields[0] = 1 / height_div_2n this.projection.fields[5] = 1 / width_div_2n this.projection.fields[10] = (far + near) / (near - far) this.projection.fields[10] = -1 this.projection.fields[14] = 2 * far * near / (near - far) this.projection.fields[15] = 0 } Camera.prototype.getInversePosition = function () { var orig = this.position.fields var dest = new Transformation() var x = orig[12] var y = orig[13] var z = orig[14] // Transpose the rotation matrix for (var i = 0; i <3; ++i) { for (var j = 0; j < 3; ++j) { dest.fields[i * 4 + j] = orig[i + j * 4] } } // Translation by -p will apply R^T, which is equal to R^-1 return dest.translate(-x, -y, -z) }
Dette er den siste delen vi trenger før vi kan begynne å tegne ting på skjermen.
Den enkleste overflaten du kan tegne er en trekant. Faktisk består de fleste tingene du tegner i 3D-rom, av et stort antall trekanter.
Det første du må forstå er hvordan skjermen er representert i WebGL. Det er et 3D-rom som strekker seg mellom -1 og 1 på x , Y , og med akser. Som standard dette med akse brukes ikke, men du er interessert i 3D-grafikk, så du vil aktivere den med en gang.
Med det i tankene er det følgende tre trinn som kreves for å tegne en trekant på denne overflaten.
Du kan definere tre hjørner, som representerer trekanten du vil tegne. Du serialiserer dataene og sender dem til GPU (grafikkbehandlingsenhet). Med en hel modell tilgjengelig, kan du gjøre det for alle trekanter i modellen. Toppunktposisjonene du gir er i det lokale koordinatområdet til modellen du har lastet inn. Enkelt sagt, posisjonene du oppgir er de nøyaktige fra filen, og ikke den du får etter å ha utført matrisetransformasjoner.
Nå som du har gitt hjørnene til GPUen, forteller du GPUen hvilken logikk du skal bruke når du plasserer hjørnene på skjermen. Dette trinnet vil bli brukt til å bruke matrisetransformasjonene våre. GPU-en er veldig god til å multiplisere mange 4x4-matriser, så vi vil utnytte denne muligheten.
I det siste trinnet vil GPU rastere den trekanten. Rasterisering er prosessen med å ta vektorgrafikk og bestemme hvilke piksler på skjermen som må males for at det vektorgrafikkobjektet skal vises. I vårt tilfelle prøver GPU å bestemme hvilke piksler som ligger i hver trekant. For hver piksel vil GPU spørre deg hvilken farge du vil at den skal males.
Dette er de fire elementene som trengs for å tegne hva du vil, og de er det enkleste eksemplet på a grafikkrørledning . Det som følger er en titt på hver av dem, og en enkel implementering.
Det viktigste elementet for et WebGL-program er WebGL-konteksten. Du kan få tilgang til den med gl = canvas.getContext('webgl')
, eller bruke 'experimental-webgl'
som en reserve, i tilfelle den nåværende brukte nettleseren ikke støtter alle WebGL-funksjoner ennå. canvas
vi refererte til er DOM-elementet på lerretet vi ønsker å tegne på. Konteksten inneholder mange ting, blant annet er standard framebuffer.
Du kan løst beskrive en framebuffer som en hvilken som helst buffer (objekt) du kan tegne på. Som standard lagrer standard frambuffer fargen for hver piksel på lerretet som WebGL-konteksten er bundet til. Som beskrevet i forrige avsnitt, når vi tegner på rammebufferen, ligger hver piksel mellom -1 og 1 på x og Y akser. Noe vi også nevnte er det faktum at WebGL som standard ikke bruker med akser. Denne funksjonaliteten kan aktiveres ved å kjøre gl.enable(gl.DEPTH_TEST)
. Flott, men hva er en dybdetest?
Aktivering av dybdetesten gjør at en piksel kan lagre både farge og dybde. Dybden er med koordinaten til den pikselet. Etter at du tegner til en piksel på en viss dybde med , for å oppdatere fargen på den pikselet, må du tegne med a med posisjon som er nærmere kameraet. Ellers blir trekningsforsøket ignorert. Dette muliggjør illusjonen av 3D, siden tegning av objekter som ligger bak andre objekter, vil føre til at disse gjenstandene blir okkludert av objekter foran dem.
Eventuelle trekk du utfører blir værende på skjermen til du ber dem om å bli ryddet. For å gjøre det, må du ringe gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
. Dette fjerner både farge- og dybdebufferen. For å velge fargen som de ryddede pikslene er satt til, bruk gl.clearColor(red, green, blue, alpha)
.
La oss lage en gjengivelse som bruker et lerret og fjerner det på forespørsel:
function Renderer (canvas) Renderer.prototype.setClearColor = function (red, green, blue) { gl.clearColor(red / 255, green / 255, blue / 255, 1) } Renderer.prototype.getContext = function () { return this.gl } Renderer.prototype.render = function () gl.DEPTH_BUFFER_BIT) var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) loop() function loop () { renderer.render() requestAnimationFrame(loop) }
Ved å feste dette skriptet til følgende HTML, får du et lyseblått rektangel på skjermen
requestAnimationFrame
N
kall fører til at sløyfen blir ringt opp igjen så snart forrige ramme er ferdig gjengitt og all hendelsesbehandling er ferdig.
Det første du må gjøre er å definere hjørnene du vil tegne. Du kan gjøre det ved å beskrive dem via vektorer i 3D-rommet. Etter det vil du flytte dataene til GPU RAM ved å opprette en ny Vertex Buffer Object (FEB).
TIL Bufferobjekt generelt er et objekt som lagrer en rekke minnebiter på GPUen. Det å være en VBO betegner bare hva GPU kan bruke minnet til. Bufferobjekter du oppretter, vil vanligvis være VBO-er.
Du kan fylle VBO ved å ta alle 3N
hjørner som vi har og lage en rekke flyter med 2N
elementer for toppunktposisjon og toppunkt normale VBOer, og Geometry
for teksturkoordinatene VBO. Hver gruppe på tre flottører, eller to flyter for UV-koordinater, representerer individuelle koordinater for et toppunkt. Deretter overfører vi disse gruppene til GPU, og toppunktene våre er klare for resten av rørledningen.
Siden dataene nå er på GPU-RAM, kan du slette dem fra RAM for generell bruk. Det vil si, med mindre du senere vil endre det, og laste det opp igjen. Hver modifikasjon må følges av en opplasting, siden modifikasjoner i JS-gruppene våre ikke gjelder for VBO-er i selve GPU-RAM.
Nedenfor er et kodeeksempel som gir all den beskrevne funksjonaliteten. Et viktig notat å gjøre er det faktum at variabler som er lagret på GPUen ikke blir søppel. Det betyr at vi må slette dem manuelt når vi ikke lenger vil bruke dem. Vi vil bare gi deg et eksempel på hvordan det gjøres her, og vil ikke fokusere på det konseptet videre. Slette variabler fra GPU er bare nødvendig hvis du planlegger å slutte å bruke bestemt geometri gjennom hele programmet.
Vi har også lagt til serialisering i Geometry.prototype.vertexCount = function () { return this.faces.length * 3 } Geometry.prototype.positions = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.position answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.normals = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.normal answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.uvs = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.uv answer.push(v.x, v.y) }) }) return answer } //////////////////////////////// function VBO (gl, data, count) { // Creates buffer object in GPU RAM where we can store anything var bufferObject = gl.createBuffer() // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, bufferObject) // Write the data, and set the flag to optimize // for rare changes to the data we're writing gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW) this.gl = gl this.size = data.length / count this.count = count this.data = bufferObject } VBO.prototype.destroy = function () { // Free memory that is occupied by our buffer object this.gl.deleteBuffer(this.data) }
klasse og elementer innenfor den.
VBO
gl
datatypen genererer VBO i bestått WebGL-kontekst, basert på matrisen som er sendt som en andre parameter.
Du kan se tre anrop til createBuffer()
kontekst. De bindBuffer()
samtale oppretter bufferen. De ARRAY_BUFFER
anrop ber WebGL-tilstandsmaskinen bruke dette spesifikke minnet som gjeldende VBO (bufferData()
) for alle fremtidige operasjoner, til det ikke blir fortalt noe annet. Etter det setter vi verdien av den nåværende VBO til de oppgitte dataene, med deleteBuffer()
.
Vi tilbyr også en ødeleggelsesmetode som sletter bufferobjektet vårt fra GPU RAM ved hjelp av function Mesh (gl, geometry) { var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() }
.
Du kan bruke tre VBO-er og en transformasjon for å beskrive alle egenskapene til et nett, sammen med dets posisjon.
Geometry.loadOBJ('/assets/model.obj').then(function (geometry) { var mesh = new Mesh(gl, geometry) console.log(mesh) mesh.destroy() })
Som et eksempel, her er hvordan vi kan laste en modell, lagre egenskapene i masken og deretter ødelegge den:
attribute
Det som følger er den tidligere beskrevne totrinnsprosessen med å flytte punkter til ønskede posisjoner og male alle individuelle piksler. For å gjøre dette skriver vi et program som kjøres på grafikkortet mange ganger. Dette programmet består vanligvis av minst to deler. Den første delen er en Vertex Shader , som kjøres for hvert toppunkt, og gir ut hvor vi blant annet skal plassere toppunktet på skjermen. Den andre delen er Fragment Shader , som kjøres for hver piksel som en trekant dekker på skjermen, og sender ut fargen som pikselet skal males til.
La oss si at du vil ha en modell som beveger seg til venstre og høyre på skjermen. I en naiv tilnærming kan du oppdatere posisjonen til hvert toppunkt og sende den til GPUen. Den prosessen er kostbar og langsom. Alternativt vil du gi et program for GPU å kjøre for hvert toppunkt, og gjøre alle disse operasjonene parallelt med en prosessor som er bygget for å gjøre akkurat den jobben. Det er rollen som en toppunkt skyggelegging .
En vertex shader er den delen av gjengivelsesrørledningen som behandler individuelle hjørner. Et anrop til toppunktskyggen mottar et enkelt toppunkt og sender ut et enkelt toppunkt etter at alle mulige transformasjoner til toppunktet er brukt.
Shaders er skrevet i GLSL. Det er mange unike elementer til dette språket, men det meste av syntaksen er veldig C-aktig, så det bør være forståelig for folk flest.
Det er tre typer variabler som går inn og ut av en toppunktskygge, og alle tjener en bestemt bruk:
uniform
- Dette er innganger som har spesifikke egenskaper til et toppunkt. Tidligere beskrev vi posisjonen til et toppunkt som et attributt, i form av en treelementvektor. Du kan se på attributter som verdier som beskriver ett toppunkt.uniform
- Dette er innganger som er de samme for hvert toppunkt i samme gjengivelsesanrop. La oss si at vi ønsker å kunne flytte modellen vår ved å definere en transformasjonsmatrise. Du kan bruke et varying
variabel for å beskrive det. Du kan også peke på ressurser på GPUen, som teksturer. Du kan se på uniformer som verdier som beskriver en modell, eller en del av en modell.attribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 model; uniform mat4 view; uniform mat4 projection; varying vec3 vNormal; varying vec2 vUv; void main() { vUv = uv; vNormal = (model * vec4(normal, 0.)).xyz; gl_Position = projection * view * model * vec4(position, 1.); }
- Dette er utganger som vi overfører til fragment shader. Siden det potensielt er tusenvis av piksler for en trekant av hjørner, vil hver piksel motta en interpolert verdi for denne variabelen, avhengig av posisjon. Så hvis en toppunkt sender 500 som utgang, og en annen 100, vil en piksel som er i midten mellom dem motta 300 som inngang for den variabelen. Du kan se på variasjoner som verdier som beskriver overflater mellom hjørner.La oss si at du vil lage en toppunktskygge som mottar en posisjons-, normal- og uv-koordinat for hvert toppunkt, og en posisjon, visning (omvendt kameraposisjon) og projeksjonsmatrise for hvert gjengitt objekt. La oss si at du også vil male individuelle piksler basert på UV-koordinatene og normene. 'Hvordan ville den koden se ut?' spør du kanskje.
main
De fleste elementene her skal være selvforklarende. Det viktigste å merke seg er at det ikke er noen returverdier i varying
funksjon. Alle verdier som vi ønsker å returnere er tildelt, enten til gl_Position
variabler, eller til spesielle variabler. Her tildeles vi til vec4
, som er en firedimensjonal vektor, hvor den siste dimensjonen alltid skal settes til en. En annen merkelig ting du kanskje legger merke til, er måten vi konstruerer en vec4
på ut av posisjonsvektoren. Du kan konstruere en float
ved å bruke fire vec2
s, to varying
s, eller en hvilken som helst annen kombinasjon som resulterer i fire elementer. Det er mange tilsynelatende merkelige støpegods som gir perfekt mening når du er kjent med transformasjonsmatriser.
Du kan også se at her kan vi utføre matrisetransformasjoner ekstremt enkelt. GLSL er spesielt laget for denne typen arbeid. Utgangsposisjonen blir beregnet ved å multiplisere projeksjons-, visnings- og modellmatrisen og bruke den på posisjonen. Utgangsnormalen er bare transformert til verdensrommet. Vi forklarer senere hvorfor vi har stoppet der med de normale transformasjonene.
Foreløpig vil vi holde det enkelt, og gå videre til å male individuelle piksler.
TIL fragment shader er trinnet etter rasterisering i grafikkrørledningen. Den genererer farge, dybde og andre data for hver piksel av objektet som males.
Prinsippene bak implementering av fragment shaders ligner veldig på vertex shaders. Det er imidlertid tre store forskjeller:
attribute
utganger, og varying
innganger er erstattet med gl_FragColor
innganger. Vi har nettopp gått videre i rørledningen, og ting som er utdata i toppunktskyggen er nå innganger i fragmentskyggen.vec4
, som er en #ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec2 clampedUv = clamp(vUv, 0., 1.); gl_FragColor = vec4(clampedUv, 1., 1.); }
. Elementene representerer henholdsvis rød, grønn, blå og alfa (RGBA) med variabler i området 0 til 1. Du bør holde alfa på 1, med mindre du gjør gjennomsiktighet. Åpenhet er imidlertid et ganske avansert konsept, så vi vil holde oss til ugjennomsiktige objekter.Med det i bakhodet kan du enkelt skrive en skyggelegging som maler den røde kanalen basert på U-posisjonen, den grønne kanalen basert på V-posisjonen, og setter den blå kanalen til maksimum.
clamp
Funksjonen function ShaderProgram (gl, vertSrc, fragSrc) { var vert = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vert, vertSrc) gl.compileShader(vert) if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(vert)) throw new Error('Failed to compile shader') } var frag = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(frag, fragSrc) gl.compileShader(frag) if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(frag)) throw new Error('Failed to compile shader') } var program = gl.createProgram() gl.attachShader(program, vert) gl.attachShader(program, frag) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)) throw new Error('Failed to link program') } this.gl = gl this.position = gl.getAttribLocation(program, 'position') this.normal = gl.getAttribLocation(program, 'normal') this.uv = gl.getAttribLocation(program, 'uv') this.model = gl.getUniformLocation(program, 'model') this.view = gl.getUniformLocation(program, 'view') this.projection = gl.getUniformLocation(program, 'projection') this.vert = vert this.frag = frag this.program = program } // Loads shader files from the given URLs, and returns a program as a promise ShaderProgram.load = function (gl, vertUrl, fragUrl) { return Promise.all([loadFile(vertUrl), loadFile(fragUrl)]).then(function (files) { return new ShaderProgram(gl, files[0], files[1]) }) function loadFile (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(xhr.responseText) } } xhr.open('GET', url, true) xhr.send(null) }) } }
bare begrenser alle flyter i et objekt til å være innenfor de angitte grensene. Resten av koden skal være ganske grei.
Med alt dette i bakhodet er det bare å implementere dette i WebGL.
Det neste trinnet er å kombinere skyggen i et program:
ShaderProgram.prototype.use = function () { this.gl.useProgram(this.program) }
Det er ikke mye å si om hva som skjer her. Hver skyggelegger får tildelt en streng som en kilde og blir samlet, hvoretter vi sjekker om det var kompileringsfeil. Deretter lager vi et program ved å koble disse to skyggene. Til slutt lagrer vi tips til alle relevante attributter og uniformer for ettertiden.
Sist, men ikke minst, tegner du modellen.
Først velger du skyggeprogrammet du vil bruke.
Transformation.prototype.sendToGpu = function (gl, uniform, transpose) gl.uniformMatrix4fv(uniform, transpose Camera.prototype.use = function (shaderProgram) { this.projection.sendToGpu(shaderProgram.gl, shaderProgram.projection) this.getInversePosition().sendToGpu(shaderProgram.gl, shaderProgram.view) }
Deretter sender du alle kamerarelaterte uniformer til GPUen. Disse uniformene endres bare én gang per kameraendring eller bevegelse.
VBO.prototype.bindToAttribute = function (attribute) { var gl = this.gl // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, this.data) // Enable this attribute in the shader gl.enableVertexAttribArray(attribute) // Define format of the attribute array. Must match parameters in shader gl.vertexAttribPointer(attribute, this.size, gl.FLOAT, false, 0, 0) }
Til slutt tar du transformasjonene og VBO-ene og tildeler dem henholdsvis uniformer og attributter. Siden dette må gjøres for hver VBO, kan du opprette databindingen som en metode.
drawArrays()
Deretter tilordner du en rekke tre flyter til uniformen. Hver uniformstype har en annen signatur, så dokumentasjon og mer dokumentasjon er vennene dine her. Til slutt tegner du trekantsamlingen på skjermen. Du forteller tegningsanropet TRIANGLES
fra hvilket toppunkt du skal starte, og hvor mange hjørner du skal trekke. Den første parameteren som ble sendt, forteller WebGL hvordan den skal tolke spissen av toppunktene. Bruker POINTS
tar tre og tre hjørner og tegner en trekant for hver triplett. Bruker Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) }
ville bare tegne et poeng for hvert toppunkt. Det er mange flere alternativer, men det er ikke nødvendig å oppdage alt på en gang. Nedenfor er koden for tegning av et objekt:
Renderer.prototype.setShader = function (shader) { this.shader = shader } Renderer.prototype.render = function (camera, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
Gjengiveren må utvides litt for å imøtekomme alle ekstraelementene som må håndteres. Det skal være mulig å legge ved et skyggeprogram og å gjengi en rekke objekter basert på gjeldende kameraposisjon.
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Geometry.loadOBJ('/assets/sphere.obj').then(function (data) { objects.push(new Mesh(gl, data)) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) loop() function loop () { renderer.render(camera, objects) requestAnimationFrame(loop) }
Vi kan kombinere alle elementene som vi har for å endelig tegne noe på skjermen:
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); gl_FragColor = vec4(brown, 1.); }
Dette ser litt tilfeldig ut, men du kan se de forskjellige flekkene på sfæren, basert på hvor de er på UV-kartet. Du kan endre skyggen for å male objektet brunt. Bare sett fargen for hver piksel til å være RGBA for brun:
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); gl_FragColor = vec4(brown * lightness, 1.); }
Det ser ikke veldig overbevisende ut. Det ser ut til at scenen trenger noen skyggeeffekter.
Lys og skygger er verktøyene som lar oss oppfatte formen på objekter. Lys kommer i mange former og størrelser: spotlights som skinner i en kjegle, lyspærer som sprer lys i alle retninger, og mest interessant, solen, som er så langt unna at alt lyset det skinner på oss utstråler, i det hele tatt og formål, i samme retning.
Sollys høres ut som det er det enkleste å implementere, siden alt du trenger å gi er retningen der alle stråler spres. For hver piksel du tegner på skjermen, sjekker du vinkelen lyset treffer objektet under. Det er her overflatenormaler kommer inn.
Du kan se alle lysstrålene som flyter i samme retning og treffer overflaten under forskjellige vinkler, som er basert på vinkelen mellom lysstrålen og overflaten normal. Jo mer de sammenfaller, jo sterkere er lyset.
Hvis du utfører et punktprodukt mellom de normaliserte vektorene for lysstrålen og overflatenormalen, får du -1 hvis strålen treffer overflaten perfekt vinkelrett, 0 hvis strålen er parallell med overflaten, og 1 hvis den lyser den fra motsatt side. Så alt mellom 0 og 1 skal ikke tilføre noe lys, mens tall mellom 0 og -1 gradvis skal øke mengden lys som treffer objektet. Du kan teste dette ved å legge til et fast lys i skyggekoden.
#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); float ambientLight = 0.3; lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
Vi setter solen til å skinne i retning fremover til venstre nedover. Du kan se hvor glatt skyggen er, selv om modellen er veldig tagget. Du kan også merke hvor mørkt nederst til venstre er. Vi kan legge til et nivå av omgivende lys, som vil gjøre området i skyggen lysere.
#ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }
Du kan oppnå den samme effekten ved å introdusere en lysklasse som lagrer lysretningen og omgivelseslysintensiteten. Deretter kan du endre fragmentskyggen for å imøtekomme det tillegget.
Nå blir skyggen:
function Light () { this.lightDirection = new Vector3(-1, -1, -1) this.ambientLight = 0.3 } Light.prototype.use = function (shaderProgram) { var dir = this.lightDirection var gl = shaderProgram.gl gl.uniform3f(shaderProgram.lightDirection, dir.x, dir.y, dir.z) gl.uniform1f(shaderProgram.ambientLight, this.ambientLight) }
Deretter kan du definere lyset:
this.ambientLight = gl.getUniformLocation(program, 'ambientLight') this.lightDirection = gl.getUniformLocation(program, 'lightDirection')
I skyggeprogrammklassen legger du til nødvendige uniformer:
Renderer.prototype.render = function (camera, light, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() light.use(shader) camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }
I programmet legger du til en samtale til det nye lyset i gjengiveren:
var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
Sløyfen vil da endre seg litt:
sampler2D
Hvis du har gjort alt riktig, bør det gjengitte bildet være det samme som det var i det siste bildet.
Et siste trinn å vurdere ville være å legge til en faktisk tekstur i modellen vår. La oss gjøre det nå.
HTML5 har god støtte for innlasting av bilder, så det er ikke nødvendig å gjøre sprø bildeparsering. Bildene sendes til GLSL som sampler2D
ved å fortelle skyggen hvilken av de innbundne teksturene som skal prøves. Det er et begrenset antall teksturer man kan binde, og grensen er basert på maskinvaren som brukes. A #ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; uniform sampler2D diffuse; varying vec3 vNormal; varying vec2 vUv; void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
kan spørres etter farger på bestemte posisjoner. Det er her UV-koordinater kommer inn. Her er et eksempel der vi byttet ut brunt med fargede farger.
this.diffuse = gl.getUniformLocation(program, 'diffuse')
Den nye uniformen må legges til listen i skyggeprogrammet:
function Texture (gl, image) { var texture = gl.createTexture() // Set the newly created texture context as active texture gl.bindTexture(gl.TEXTURE_2D, texture) // Set texture parameters, and pass the image that the texture is based on gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) // Set filtering methods // Very often shaders will query the texture value between pixels, // and this is instructing how that value shall be calculated gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) this.data = texture this.gl = gl } Texture.prototype.use = function (uniform, binding) binding = Number(binding) Texture.load = function (gl, url) { return new Promise(function (resolve) { var image = new Image() image.onload = function () { resolve(new Texture(gl, image)) } image.src = url }) }
Til slutt implementerer vi teksturbelastning. Som tidligere sagt gir HTML5 fasiliteter for innlasting av bilder. Alt vi trenger å gjøre er å sende bildet til GPU:
sampler2D
Prosessen er ikke mye forskjellig fra prosessen som brukes til å laste inn og binde VBO-er. Hovedforskjellen er at vi ikke lenger binder til et attributt, men snarere binder teksturindeksen til en heltallsuniform. Mesh
type er ikke mer enn en peker forskjøvet til en tekstur.
Nå er det bare å utvide function Mesh (gl, geometry, texture) { // added texture var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.texture = texture // new this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.texture.use(shaderProgram.diffuse, 0) // new this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } Mesh.load = function (gl, modelUrl, textureUrl) { // new var geometry = Geometry.loadOBJ(modelUrl) var texture = Texture.load(gl, textureUrl) return Promise.all([geometry, texture]).then(function (params) { return new Mesh(gl, params[0], params[1]) }) }
klasse, for å håndtere teksturer også:
var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Mesh.load(gl, '/assets/sphere.obj', '/assets/diffuse.png') .then(function (mesh) { objects.push(mesh) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }
Og det endelige hovedmanuset ville se slik ut:
function loop () { renderer.render(camera, light, objects) camera.position = camera.position.rotateY(Math.PI / 120) requestAnimationFrame(loop) }
Selv animering kommer lett på dette punktet. Hvis du ville at kameraet skulle snurre rundt objektet vårt, kan du gjøre det ved å bare legge til en linje med kode:
void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = lightness > 0.1 ? 1. : 0.; // new lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }
Lek gjerne med skyggeleggere. Hvis du legger til en kodelinje, blir denne realistiske belysningen til noe tegneserieaktig.
|_+_|
Det er så enkelt som å fortelle belysningen å gå inn i ytterpunktene, basert på om den krysset en angitt terskel.
Det er mange informasjonskilder for å lære alle triks og komplikasjoner i WebGL. Og det beste er at hvis du ikke finner et svar som er relatert til WebGL, kan du se etter det i OpenGL, siden WebGL er ganske mye basert på en delmengde av OpenGL, med noen navn som endres.
I ingen spesiell rekkefølge er her noen gode kilder for mer detaljert informasjon, både for WebGL og OpenGL.
WebGL er et JavaScript-API som legger til innfødt støtte for gjengivelse av 3D-grafikk i kompatible nettlesere, gjennom en API som ligner på OpenGL.
Den vanligste måten å representere 3D-modeller er gjennom en rekke hjørner, som hver har en definert posisjon i rommet, normalt på overflaten som toppunktet skal være en del av, og koordinerer på en tekstur som brukes til å male modellen. Disse toppunktene blir deretter satt i grupper på tre, for å danne trekanter.
En vertex shader er den delen av gjengivelsesrørledningen som behandler individuelle hjørner. Et anrop til toppunktskyggen mottar et enkelt toppunkt og sender ut et enkelt toppunkt etter at alle mulige transformasjoner til toppunktet er brukt. Dette gjør det mulig å bruke bevegelse og deformasjoner på hele gjenstander.
En fragment shader er den delen av gjengivelsesrørledningen som tar en piksel av et objekt på skjermen, sammen med egenskapene til objektet i den pikselposisjonen, og kan generere farge, dybde og andre data for det.
Alle toppunktposisjoner til et objekt er i forhold til dets lokale koordinatsystem, som er representert med en 4x4 identitetsmatrise. Hvis vi flytter koordinatsystemet, ved å multiplisere det med transformasjonsmatriser, beveger objektets hjørner seg med det.