În laboratorul anterior, am văzut cum putem desena obiecte pe ecran, independent de spațiul în care am definit coordonatele vârfurilor unui obiect. Mai precis, am folosit un spațiu logic, pe care l-am denumit fereastră peste spațiul 2D și în care am definit inițial coordonatele vârfurilor. Prin utilizarea transformării fereastră-poartă, am putut desena obiectele într-o poartă de afișare de pe ecran, la orice rezoluție de pixeli a acestei porți. De asemenea, am văzut cum se pot prelucra obiectele prin diferite transformări. În laboratorul anterior, am analizat doar 3 tipuri de transformări de bază: translație, modificare de scară și rotație.
Lucrul cu un spațiu 3D nu este cu mult diferit față de lucrul cu un spațiu 2D. Pentru desenarea pe ecran a unor obiecte dintr-un spațiu 3D, este necesar să ne stabilim un spațiu logic, din care să transformăm coordonatele vârfurilor în coordonatele grilei de pixeli. În grafica pe calculator, spațiul standard ales pentru acest proces este volumul unui cub, cu latura de dimensiune 2 și centrat în originea axelor de coordonate. Astfel, colțul stânga-jos-față al cubului are coordonatele $(-1, -1, -1)$ și colțul dreapta-sus-spate are coordonatele $(1, 1, 1)$. O reprezentare vizuală a acestui volum se poate găsi în imaginea următoare:
Coordonatele vârfurilor ce se regăsesc în acest volum pot fi aduse în grila de pixeli printr-o transformare fereastră-poartă, similar ca săptămâna trecută:
$$ \begin{bmatrix} x' \\ y' \\ z' \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & V_{x}\\ 0 & 1 & 0 & V_{y}\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} V_{w}/2 & 0 & 0\\ 0 & V_{h}/2 & 0\\ 0 & 0 & 1/2 & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & -1\\ 0 & 1 & 0 & -1\\ 0 & 0 & 1 & -1\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} $$
Observăm că se proiectează paralel coordonatele vârfurilor pe fața $z=-1$ a cubului. Practic, se păstrează componentele $(x, y)$ ale coordonatelor din vârfuri și putem să consideram că pentru componentele $(x, y)$ se utilizează o transformare fereastră-poartă dintr-o fereastră peste spatiul 2D.
Pentru componenta $z$, din moment ce nu se poate transforma direct în coordonatele grilei de pixeli, se normalizează între 0 și 1 și se utilizează cu scopul de valore de adâncime în procesul de rasterizare studiat în laboratorul 1 :) .
Pe ecran ajung să fie desenate doar suprafețele dintr-un triunghi ce se regăsesc în volumul cubului de latură 2, centrat în originea axelor de coordonate. Cu toate acestea, este posibil ca nu toată suprafața unui triunghi să se afle în volumul cubului, astfel că în procesul de rasterizare trebuie să se realizeze procesări suplimentare pentru a nu se desena în afara grilei de pixeli. Pentru a evita realizarea acestor calcule, putem decupa triunghiurile la limitele volumului și să utilizăm în procesul de rasterizare doar triunghiurile rezultate în urma decupării. Scenariile în care acest mecanism produce 1, 2 și 3 triunghiuri pot fi observate în imaginea de mai jos.
Cele 1, 2 sau 3 triunghiuri, rezultate după procesul de decupare, sunt utilizate în procesul de rasterizare. Exemplificarea din imagine este pentru o decupare 2D, dar ea se poate extinde la volumul unui cub, în care un triunghi se decupează la limitele volumului, în spațiul 3D.
Mai există un scenariu suplimentar, ce nu este acoperit în imagine, anume situația în care nicio parte din suprafața triunghiului nu se regăsește în interiorul volumului. În acest scenariu, nu este transmisă nicio primitivă în procesul de rasterizare.
Definirea unui spațiu finit pe care să îl normalizăm ulterior în grila de pixeli ne oferă o flexibilitate foarte mare. Dezavantajul aplicării directe a acestei abordări este că geometria obiectelor nu se regăsește întotdeauna în interiorul volumului de decupare. De fapt, de cele mai multe ori, obiectele nu se află în acest volum. Astfel, trebuie să aducem noi geometria în acesta printr-o transformare suplimentară. Această transformare poate să fie o transformare de proiecție perspectivă pentru a simula procesul de transport al luminii de pe suprafețele scenei la observator.
Tot ce „vede” ochiul uman este lumina, astfel că procesul de desenare a suprafețelor unui obiect pe ecran simulează procesul de achiziție a luminii care ajunge de pe suprafețele obiectului la ochiul uman :) . Pentru simplitate, vom considera un singur punct de convergență pentru simularea razelor de lumină. Astfel, putem să folosim o transformare perspectivă a coordonatelor vârfurilor pentru a aduce geometria în spațiul de decupare și ulterior pentru a o desena pe ecran.
Volumul proiecției perspectivă este un trunchi de piramidă, frustum. Standard, acest volum este descris cu vârful în originea axelor de coordonate și cu baza orientată de-a lungul axei $Z$, în sens negativ. Acest volum se normalizează în volumul spațiului de decupare. O reprezentare vizuală a lui se găsește în imaginea de mai jos.
Avem mai multe posibilități de a descrie forma acestui volum, dar în continuare va fi prezentată cea care apare uzual în grafica pe calculator. Pentru descrierea frustumului folosim:
Formula de calcul a proiecției perspective este:
$$ x' = \frac{x}{ -z \cdot tan(\frac{fovy}{2})\cdot aspect}\\ y' = \frac{y}{ -z \cdot tan(\frac{fovy}{2})}\\ z' = \frac{c2\cdot z-c1}{-z}\\ c1=\frac{(-2) \cdot far \cdot near}{far- near}\\ c2=\frac{(-1)\cdot(far+near)}{far - near} $$
Această formulă nu poate fi scrisă sub formă matriceală, deoarece este un sistem de ecuații neliniare. După cum vedem, în calcularea componentelor $(x', y', z')$ ale coordonatei obținute în urma transformării de proiecție perspectivă, se utilizează valoarea componentei $z$ a coordonatei pentru care se aplică transformarea. Din acest motiv, este necesar să realizăm acest proces în 2 pași:
$$ x'' = \frac{x'}{-z}\\ y'' = \frac{y'}{-z}\\ z'' = \frac{z'}{-z} $$
Pentru a nu fi nevoie să se rețină apriori valoarea componentei $z$, care poate fi obținută în urma aplicării unui lanț de transformări, astfel că transformarea de proiecție perspectivă se poate afla în interiorul unei matrici compuse dintr-un lanț de transformări, se utilizează un artificiu. Se poate observa că matricea de mai sus este construită astfel încât să păstreze în componenta $w'$ , valoarea componentei $z$, negativă:
$$ w'=0\cdot x + 0\cdot y + (-1) \cdot z + 0 \cdot 0 $$
Din acest motiv, cel de-al doilea pas de mai sus devine:
$$ x'' = \frac{x'}{w'}\\ y'' = \frac{y'}{w'}\\ z'' = \frac{z'}{w'} $$
Astfel, conform teoremei lui Thales, rezultă că:
$$ \frac{y'}{near} = \frac{y}{-z}\\ $$
$$ y'=\frac{near\cdot y}{-z}\\ $$
Considerăm că valoarea componentei $z$ este negativă, deoarece o tratăm pe post de distanță și orientarea bazei trunchiului de piramidă este din construcție de-a lungul axei $Z$, în sens negativ.
Suplimentar, presupunem că avem distanța până la limita de jos, denumită bottom și distanța până la limita de sus, denumtiă top, a feței din apropiere a frustumului, după cum se poate vedea în imaginea de mai sus. Dorim să normalizăm componenta $y$ din intervalul $[bottom, top]$ în $[-1, 1]$. Aceasta este o transformare fereastră-poartă. Împreună cu formula anterioara pentru $y'$, obținem că $y'$ este:
$$ y'=\frac{2\cdot near\cdot y}{-z \cdot (top-bottom)}\\ $$
Din construcție, avem un singur unghi de deschidere verticală a trunchiului de piramidă, astfel că avem aceeași valoare pentru limitele bottom și top:
$$ bottom= -top\\ y'=\frac{2\cdot near\cdot y}{-z \cdot 2 \cdot top}=\frac{near\cdot y}{-z \cdot top}\\ $$
Se poate observa că:
$$ \frac{top}{near}=tan(\frac{fovy}{2}) $$
Astfel, avem pentru $y'$:
$$ y' = \frac{y}{ -z \cdot tan(\frac{fovy}{2})}\\ $$
Pentru simplitate și un control ridicat, în loc să se descrie volumul frustumului prin 2 valori, una care descrie unghiul de deschidere verticală și una pentru unghiul de deschidere orizontală, se utilizează raportul de aspect al ecranului:
$$ aspect = \frac{V_{w}}{V_h}\\ x' = \frac{x}{ -z \cdot tan(\frac{fovy}{2})\cdot aspect}\\ $$
Daca formula pentru $(x, y)$ reiese din proiecția coordonatelor vârfului pe planul din apropiere al trunchiului de piramidă, formula pentru componenta $z$ este artificială și reiese exclusiv din dorința de a creea o normalizare neliniară între distanța planului din apropiere și distanța planului îndepărtat. Se dorește ca pentru obiectele din apropiere de planul apropiat, $z=-near$, să fie alocat un interval mult mai mare în spațiul de decupare, decât pentru obiectele apropiate de planul $z=-far$. Din acest motiv, formula standard aleasă inițial este:
$$ z'=\frac{c1}{-z}+c2 $$
Parametrii $c1$ și $c2$ sunt cei din formula originală. Problema acestei formule este că nu se imparte la valoarea $z$, ceea ce înseamnă că pentru $z'$ nu s-ar aplica împărțirea perspectivă. Pentru a face uniform procesul de împărțire perspectivă, se modifică formula și obținem versiunea finală:
$$ z'=\frac{c2\cdot z-c1}{-z} $$
Prin transformarea de proiecție perspectivă, descrisă mai sus, putem modifica coordonatele vârfurilor rețelei de triunghiuri pentru a le așeza în spațiul de decupare și de acolo putem desena triunghiurile pe ecran. Cu toate acestea, coordonatele obiectelor noastre nu se regăsesc întotdeauna în spațiul de vizualizare, reprezentat, cum s-a menționat mai sus, de volumul unui trunchi de piramidă cu vârful în originea axelor de coordonate și cu baza orientată de-a lungul axei $Z$, în sens negativ. În situația în care dorim să desenăm scena dintr-un alt punct de vedere, trebuie să definim conceptul de observator. Proprietățile care definesc un observator sunt:
Pe baza acestor informații, putem să creăm o transformare care modifică obiectele din scenă, din spațiul în care se află, într-un spațiu în care observatorul are poziția în originea axelor de coordonate și „privește” scena de-a lungul axei $Z$, în sens negativ. Peste acest spațiu se poate suprapune volumul de vizualizare, utilizat pentru transformarea proiecției perspective.
Transformarea de vizualizare arată în felul următor:
$$ \begin{bmatrix} {x}'\\ {y}'\\ {z}'\\ 1 \end{bmatrix} = \begin{bmatrix} \vec{R}_x & \vec{R}_y & \vec{R}_z & 0 \\ \vec{U}_x & \vec{U}_y & \vec{U}_z & 0 \\ \vec{F}_x & \vec{F}_y & \vec{F}_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} 1 & 0 & 0 & -P_x \\ 0 & 1 & 0 & -P_y \\ 0 & 0 & 1 & -P_z \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix} $$
Se poate observa că prima transformare aplicată, ultima în lanțul de înmulțiri de matrici, este o translație inversă a coordonatelor poziției observatorului în spațiul 3D, similară cu cea discutată în laboratorul anterior pentru spațiul 2D. A doua transformare aplicată este o rotație. Pentru construcția acestei matrici s-a folosit procedeul Gram–Schmidt.
Coordonatele vârfurilor din rețeaua de triunghiuri ce descrie suprafața unui obiect se declară inițial într-un spațiu convenabil creării acelui obiect.
Pentru a modifica obiectul și a-l așeza sub forma dorită în punctul final din scenă, se folosesc transformările de translație, de modificare a scării și de rotație, discutate în laboratorul anterior, adaptate pentru un spațiu 3D.
Forma matriceală a transformărilor de translație și de modificare a scării este similară cu cea a transformărilor utilizate într-un spațiu 2D. Fiecare din cele două matrici se modifică doar prin adăugarea unei noi linii și a unei noi coloane pentru a adapta cea de-a treia dimensiune ce apare într-un spațiu 3D.
$$ \begin{bmatrix} {x}'\\ {y}'\\ {z}'\\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & t_x\\ 0 & 1 & 0 & t_y\\ 0 & 0 & 1 & t_z\\ 0 & 0 & 0 &1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix} $$
$$ \begin{bmatrix} {x}'\\ {y}'\\ {z}'\\ 1 \end{bmatrix} = \begin{bmatrix} s_x & 0 & 0 &0 \\ 0 & s_y & 0 &0 \\ 0 & 0 & s_z &0 \\ 0 & 0 & 0 &1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix} $$
Mecanismul de rotație, studiat în laboratorul trecut, rotește o coordonată a unui punct într-un spațiu 2D cu un unghi a, în sens trigonometric față de originea axelor de coordonate. Acest mecanism NU poate fi adaptat direct la un spațiu 3D.
Astfel, transformarea de rotație dintr-un spațiu 2D este utilizată într-un spațiu 3D pentru a roti o coordonată într-un singur plan din spațiul 3D. Mai exact într-un plan paralel cu unul din cele 3 planuri ce trec prin originea axelor de coordonate. Există câte o matrice de transformare diferită pentru fiecare din cele 3 planuri. În situația în care dorim sa creăm o transformare de rotație într-un plan oarecare, trebuie să o obținem prin compunerea din mai multe transformări de bază.
Astfel, avem transformarea de rotație într-un plan paralel cu planul XOY:
$$ \begin{bmatrix} {x}'\\ {y}'\\ {z}'\\ 1 \end{bmatrix} = \begin{bmatrix} cos(u) & -sin(u) & 0 & 0\\ sin(u) & cos(u) & 0 & 0\\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix} $$
Putem să considerăm că această transformare este față de axa OZ.
Transformarea de rotație într-un plan paralel cu planul XOZ, sau față de axa OY este:
$$ \begin{bmatrix} {x}'\\ {y}'\\ {z}'\\ 1 \end{bmatrix} = \begin{bmatrix} cos(u) & 0 & sin(u) & 0\\ 0 & 1 & 0 & 0 \\ -sin(u) & 0 & cos(u) & 0\\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix} $$
Transformarea de rotație într-un plan paralel cu planul YOZ, sau față de axa OX este:
$$ \begin{bmatrix} {x}'\\ {y}'\\ {z}'\\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & cos(u) & -sin(u) & 0 \\ 0 & sin(u) & cos(u) & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x\\ y\\ z\\ 1 \end{bmatrix} $$
O optimizare suplimentară, pe care o putem realiza în spațiul de decupare, pe lângă cea de decupare propriu-zisă, este să excludem din procesul de rasterizare triunghiurile care sunt complet obturate de altă geometrie. Pentru a nu face o procesare avansată la nivel de rețea de triunghiuri, putem să folosim următorul artificiu:
În situția în care desenăm un model 3D etanș, care nu conține nicio gaură în suprafața lui, avem proprietatea că din orice punct am privi geometria, nu îi vom putea vedea niciodată interiorul. Astfel, avem următoarea formă de definire a unui triunghi: pe baza descrierii cu indici din vertices
, utilizată în laboratoarele anterioare, sensul în care sunt afișate vârfurile, pe baza ordinii din indici, ne determină nouă faptul că privim fațeta față sau fațeta spate a triunghiului.
Mai exact, dacă avem descrisă urmatoarea geometrie:
vector<VertexFormat> vertices { VertexFormat(glm::vec3(0, 2, 0)), VertexFormat(glm::vec3(-1, 0, 0)), VertexFormat(glm::vec3(1, 0, 0)), } vector<unsigned int> indices1 { 0, 1, 2, } vector<unsigned int> indices2 { 0, 2, 1, }
În momentul în care geometria se află în spațiul de decupare, după aplicarea tuturor transformărilor, se observă sensul dat de coordonatele vârfurilor, în ordinea în care au fost definiți indicii. În situația în care sensul pe care îl fac coordonatele, conform imaginii de mai jos, este trigonometric, considerăm că ne uităm la fațeta față a triunghiului.
Prin acest proces, în situația în care desenăm suprafața unui model 3D etanș, putem exclude din procesul de rasterizare toate triunghiurile pentru care afișăm fațetele spate, deoarece avem garanția că ele sunt obturate de restul geometriei modelului.
O privire de ansamblu a întrgului proces se poate regăsi în imaginea următoare:
transform3D.h
cu transformările de modelare descrise mai sus.transform3D.h
cu transformarea de proiecție perspectivă descrisă mai sus.transform3D.h
cu transformarea de vizualizare descrisă mai sus.DetermineTriangleFace()
pentru a identifica corect care din cele două fațete ale triunghiului, față sau spate, este afișat.Bonus: Construiți și desenați un tetraedru care să aibă indicii fiecărui triunghiuri orientați în sens trigonometric, când triunghiul este privit din exterior. Asociați culori diferite fiecărui vârf pentru a vedea mai ușor detaliile tetraedrului.