(...ikke pensum…)
Basert på kodeeksemplet /modul1/TriangleTrans1.html /.js gjennomgås her
nøyaktig hva som skjer fra vertekser leveres (via drawArrays()) til
verteksshaderen til fragmenter leveres til fragmentshaderen og til slutt ender opp som
piksler på skjermen.
Kodeeksemplet resulterer i følgende:
Trekanten tegnes på et «canvas» som er 400x400 piksler stort. Størrelsen på canvaset settes i .html-fila og representerer det området på skjermen som trekanten ender opp på. Man bruker gjerne begrepet «viewport» om det området på skjermen som fremviser resultatet. I utgangspunktet tilsvarer «viewporten» hele canvaset.
Alle koordinattransformasjoner som inngår kan oppsummeres slik:
I etterfølgende delkapitler forklares det stegvis, med matriseoperasjoner, hva som skjer fra verteksene sendes til fragmentshaderen til fragmenter blir generert, dvs. fra verteksene oppgis i modellkoordinater til de er transformert til skjermkoordinater.
Vi ser altså nærmere på følgende del av pipelinen:
1. Modellmatrisa
Trekanten er definert vha. av følgende vertekser:
var triangleVertices = new Float32Array([
-10, -10, 0,
0, 10, 0,
10, -10, 0
]);
Dvs. tre vertekser. Dette er trekantens opprinnelige koordinater definert i et tenkt lokalt koordinatsystem.
Disse kalles gjerne «modellkoordinater» og transformeres i verteksshaderen til
«clip-koordinater». Etter kall på drawArrays() kalles verteksshaderen for hver
av verteksene.
Verteksshaderen ser slik ut:
attribute vec3 a_Position;
uniform mat4 u_modelviewMatrix;
uniform mat4 u_projectionMatrix;
void main() {
gl_Position = u_projectionMatrix * u_modelviewMatrix * vec4(a_Position,1.0);
}
Her mottas verteksen via a_Position. Denne transformeres vha. modelview- og
projeksjonsmatrisene (NB! Rekkefølgen på matrisemultiplikasjonen er avgjørende).
Resultatet tilordnes gl_Position.
La oss anta at vi bruker identitetsmatrisa som modellmatrise:
\[ mM = \begin{bmatrix} 1.00 & 0.00 & 0.00 & 0.00 \\ 0.00 & 1.00 & 0.00 & 0.00 \\ 0.00 & 0.00 & 1.00 & 0.00 \\ 0.00 & 0.00 & 0.00 & 1.00 \end{bmatrix} \]Det betyr at trekanten hverken roteres, skaleres eller endrer posisjon i forhold til opprinnelig definert posisjon/størrelse.
2. View-matrisa
Kameraet er plassert i \([0,0,100]\) og peker mot \([0,0,0]\) / origo.
Opp-retninga til kameraet er \([0,1,0]\). Ved hjelp av lookAt() funksjonen
genereres View-matrisa:
// cuon-utils: setLookAt(eyeX, eyeY, eyeZ,
// centerX, centerY, centerZ, upX, upY, upZ)
viewMatrix.setLookAt(0, 0, 100, 0, 0, 0, 0, 1, 0);
Dette gir en view-matrise som ser slik ut:
\[ mV = \begin{bmatrix} 1.00 & 0.00 & 0.00 & 0.00 \\ 0.00 & 1.00 & 0.00 & 0.00 \\ 0.00 & 0.00 & 1.00 & -100.00 \\ 0.00 & 0.00 & 0.00 & 1.00 \end{bmatrix} \]Modell- og view-matrisa slås så sammen til modelview-matrisa:
\[ mMV = \begin{bmatrix} 1.00 & 0.00 & 0.00 & 0.00 \\ 0.00 & 1.00 & 0.00 & 0.00 \\ 0.00 & 0.00 & 1.00 & -100.00 \\ 0.00 & 0.00 & 0.00 & 1.00 \end{bmatrix} \]3. Projeksjonsmatrisa
Projeksjonsmatrisa genereres vha.
// cuon-utils: Matrix4.prototype.setPerspective = function(fovy,
// aspect, near, far)
projectionMatrix.setPerspective(45, canvas.width / canvas.height, 1, 1000);
Dette gir en projeksjonsmatrise som ser slik ut:
\[ mP = \begin{bmatrix} 2.414 & 0.000 & 0.000 & 0.000 \\ 0.000 & 2.414 & 0.000 & 0.000 \\ 0.000 & 0.000 & -1.002 & -2.002 \\ 0.000 & 0.000 & -1.000 & 0.000 \end{bmatrix} \]I verteksshaderen utføres følgende multiplikasjon:
gl_Position = mP * mMV * vec4(a_Position,1);
Uttrykket vec4(a_Position, 1) gjør a_Position om til en verteks
bestående av «homogene koordinater» der w=1.
Resultatet av denne multiplikasjonen for de tre aktuelle verteksene vil se slik ut:
\[ v_{1c} = {\footnotesize \begin{bmatrix} 2.414 & 0.000 & 0.000 & 0.000 \\ 0.000 & 2.414 & 0.000 & 0.000 \\ 0.000 & 0.000 & -1.002 & -2.002 \\ 0.000 & 0.000 & -1.000 & 0.000 \end{bmatrix} \bullet \begin{bmatrix} 1.00 & 0.00 & 0.00 & 0.00 \\ 0.00 & 1.00 & 0.00 & 0.00 \\ 0.00 & 0.00 & 1.00 & -100.00 \\ 0.00 & 0.00 & 0.00 & 1.00 \end{bmatrix} \bullet \begin{bmatrix} -10 \\ -10 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} -24.142 \\ -24.142 \\ 98.1982 \\ 100 \end{bmatrix}} \] \[ v_{2c} = {\footnotesize \begin{bmatrix} 2.414 & 0.000 & 0.000 & 0.000 \\ 0.000 & 2.414 & 0.000 & 0.000 \\ 0.000 & 0.000 & -1.002 & -2.002 \\ 0.000 & 0.000 & -1.000 & 0.000 \end{bmatrix} \bullet \begin{bmatrix} 1.00 & 0.00 & 0.00 & 0.00 \\ 0.00 & 1.00 & 0.00 & 0.00 \\ 0.00 & 0.00 & 1.00 & -100.00 \\ 0.00 & 0.00 & 0.00 & 1.00 \end{bmatrix} \bullet \begin{bmatrix} 0 \\ 10 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 0.000 \\ 24.142 \\ 98.1982 \\ 100 \end{bmatrix}} \] \[ v_{3c} = {\footnotesize \begin{bmatrix} 2.414 & 0.000 & 0.000 & 0.000 \\ 0.000 & 2.414 & 0.000 & 0.000 \\ 0.000 & 0.000 & -1.002 & -2.002 \\ 0.000 & 0.000 & -1.000 & 0.000 \end{bmatrix} \bullet \begin{bmatrix} 1.00 & 0.00 & 0.00 & 0.00 \\ 0.00 & 1.00 & 0.00 & 0.00 \\ 0.00 & 0.00 & 1.00 & -100.00 \\ 0.00 & 0.00 & 0.00 & 1.00 \end{bmatrix} \bullet \begin{bmatrix} -10 \\ -10 \\ 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 24.142 \\ -24.142 \\ 98.1982 \\ 100 \end{bmatrix}} \]De tre verteksene er nå transformert til det vi kaller «clip-koordinater». Vi ser at
gl_Position settes lik denne verdien til slutt i verteksshaderen. Som
programmerer har vi nå egentlig «gjort jobben» - vi har ikke lenger kontroll på hva som
skjer før fragmentene er generert og fragmentshaderen kjører.
Vi skal allikevel manuelt forsøke å beregne hvilke skjermpiksler de tre verteksene tilsvarer. Hvis vi går tilbake til utsnittet av pipelinen i figuren over ser vi at neste steg er at det utføres en perspektivdivisjon, dvs. x,y og z-koordinatene til verteksen, som fortsatt består av homogene koordinater, divideres med w for få verteksen tilbake til kartesiske koordinater.
Dette gir følgende resultater:
\[ v_{1ndc} = \frac{v_{1c}}{w} = \begin{bmatrix} -24.142/100 \\ -24.142/100 \\ 98.1982/100 \\ 100/100 \end{bmatrix} = \begin{bmatrix} -0.241 \\ -0.241 \\ 0.981 \\ 1 \end{bmatrix} \] \[ v_{2ndc} = \frac{v_{2c}}{w} = \begin{bmatrix} 0/100 \\ 24.142/100 \\ 98.1982/100 \\ 100/100 \end{bmatrix} = \begin{bmatrix} 0 \\ 0.241 \\ 0.981 \\ 1 \end{bmatrix} \] \[ v_{3ndc} = \frac{v_{3c}}{w} = \begin{bmatrix} 24.142/100 \\ -24.142/100 \\ 98.1982/100 \\ 100/100 \end{bmatrix} = \begin{bmatrix} 0.241 \\ -0.241 \\ 0.981 \\ 1 \end{bmatrix} \]Dette kalles Normalized Device Coordinates (NDC). Vertekser med NDC-koordinatverdier mellom -1 og 1 vil være synlig på skjermen. Alle andre faller utenfor frustumet og vil derfor heller ikke vises på skjermen.
Koordinatsystemet sett i forhold til «viewporten» ser nå slik ut:
Dette betyr egentlig at dersom de opprinnelige verteksene hadde vært oppgitt med
koordinatverdier i området [-1,1] hadde det ikke vært nødvendig med matriseoperasjonene i
verteksshaderen – man kunne satt gl_Position = a_Position direkte. Å operere
direkte med NDC-koordinater er altså mulig men sjelden ønskelig siden det bl.a. gjør det
veldig vanskelig å posisjonere ulike modeller i forhold til hverandre, man mister også
«kamerabegrepet» m.m.
4. Konvertere fra NDC til vinduskoordinater
Vi er ikke helt i mål enda. NDC-koordinatene mappes til slutt til viewporten vha. noen enkle operasjoner. Vi kaller dette vinduskoordinater (derav \(x_w, y_w, z_w\)).
\[ \begin{bmatrix} x_w \\ y_w \\ z_w \end{bmatrix} = \begin{bmatrix} (\text{width}/2)\, x_{ndc} + o_x \\[2pt] (\text{height}/2)\, y_{ndc} + o_y \\[2pt] \left( \dfrac{f - n}{2} \right) z_{ndc} + \left( \dfrac{n + f}{2} \right) \end{bmatrix} \]I formlene over er width og height bredde og høyde på viewporten, evt. det som er satt
vha. gl.viewport(x, y, width, height) funksjonen.
Verdiene til n og f tilsvarer normalt 0 og 1 og angir dybdeområdet (0 og 1 er
standardverdier og kan endres vha. gl.depthrange(n, f)).
Parametrene \(o_x\) og \(o_y\) representerer viewportens sentrum og beregnes slik:
\[ o_x = \frac{x + \text{width}}{2} \qquad o_y = \frac{y + \text{height}}{2} \]Vi kan nå regne ut faktiske skjermkoordinater for de tre verteksene. En liten sak først: Koordinatsystemet til viewporten har punkt \([0,0]\) i nedre venstre hjørne, positiv y går oppover og positiv x går mot høyre.
Setter vi inn tall får vi følgende vinduskoordinater for de tre verteksene:
\[ v_{1w} = \begin{bmatrix} 151.715 \\ 151.715 \\ 0.990 \end{bmatrix} \qquad v_{2w} = \begin{bmatrix} 200.000 \\ 248.284 \\ 0.990 \end{bmatrix} \qquad v_{3w} = \begin{bmatrix} 248.284 \\ 151.715 \\ 0.990 \end{bmatrix} \]Z-verdien angir dybde og må være innafor [0,1] for at modellen skal vises på skjermen. Se (OpenGL Wiki, Vertex Transformation, 2012) for mer info.
Kontrollerer vi disse verdiene (x og y) mot tegninga under ser vi at dette kan stemme:
NB! Størrelsen på canvaset er i kodeeksemplet satt til 400x400 og kan selvsagt endres til ønskelige verdier.
5. Videre i pipelinen: rasterisering og fragmentshader
Etter at dette er gjort vil rasterisering bli utført dvs. GPUen beregner hvilke fragmenter (piksler) som må til for å fylle trekanten. Deretter kjøres fragmentshaderen for hver enkelt fragment. Etter dette er det, som tidligere nevnt, flere andre operasjoner og tester som utføres per fragment før de evt. ender opp i skjermbufret for til slutt å bli vist frem på skjermen.
6. Mer om Viewport
Viewport representerer den delen av «canvaset» der det endelige bildet presenteres. Som regel er dette hele canvaset men det kan også avgrenses slik at man kun tegner for eksempel øvre halvdel av canvaset. Man kan se på dette som en skalering av det endelige bildet.
Dette er aktuelt dersom man, f.eks., skal lage et spill der to spillere ser hver sin del av spillet i hver sin viewport. Det kan også være aktuelt dersom man ønsker å se en modell fra flere sider samtidig ved å plassere kameraet på ulike steder for hver viewport.
I eksemplet over bruker vi et canvas som er 400x400 piksler. Størrelsen settes i .html-fila. «Viewporten» utgjør i utgangspunktet hele canvaset.
Det er også mulig å sette/endre «viewport» vha. kallet
glViewport(xStart, yStart, bredde, høyde), f.eks. slik:
gl.viewport(0,0,canvas.width,canvas.height);
Dette kallet gjøres typisk i initialiseringsdelen av programmet. I eksemplet over settes WebGL-viewporten lik tilgjengelig canvas-størrelse. Bruker vi dette i kodeeksemplet over får vi:
Som er samme resultat som uten kall på gl.viewport(). Default er altså å
bruke hele «canvaset» som «viewport». NB! Her det lagt til hjelpelinjer (er ikke en del av
koden over).
Endres f.eks. kallet til følgende vil tegninga oppta en fjerdedel av canvaset:
gl.viewport(0,0,canvas.width/2,canvas.height/2);
Resultatet vil se slik ut:
Ingen kommentarer:
Legg inn en kommentar