Formularea problemei
Scopul acestei serii de articole este de a arăta cum funcționează OpenGL scriind-o (foarte mult simplificată!) Clonează-te. În mod surprinzător, adesea întâlnesc oameni care nu pot depăși bariera inițială de învățare a OpenGL / DirectX. Astfel, am pregătit un ciclu scurt de șase prelegeri, după care studenții noștri dau rezultate bune.
Deci, sarcina este pusă după cum urmează: fără a folosi librăriile terților (în special cele grafice) pentru a obține aproximativ astfel de imagini:
Voi încerca să nu depășesc 500 de linii în codul final. Studenții mei au nevoie de 10 până la 20 de ore de programare pentru a începe să emită astfel de redări. Pe intrare primim un fișier text cu o plasă poligon + imagini cu texturi, ieșirea este un model randat. Nicio interfață grafică, programul lansat pur și simplu generează un fișier imagine.
Deoarece obiectivul este de a minimiza dependențele externe, îi dau elevilor doar o clasă care permite lucrul cu fișierele TGA. Acesta este unul dintre cele mai simple formate care acceptă imagini în RGB / RGBA / alb-negru. Asta este, ca punct de pornire, o modalitate ușoară de a lucra cu imagini. Rețineți că singura funcționalitate disponibilă la început (în afară de încărcarea și salvarea imaginii) este abilitatea de a seta culoarea unui pixel.
Nu există funcții pentru desenarea segmentelor de triunghi, va trebui să scrieți toate acestea manual.
Îmi dau codul sursă, pe care îl scriu în paralel cu elevii, dar nu recomand să-l folosesc, pur și simplu nu are sens.
Tot codul este disponibil pe github, aici este codul inițial pe care îl dau studenților mei.
output.tga ar trebui să arate astfel:
Algoritmul Brezenham
Scopul primei conferințe este de a face o randare într-o plasă de sârmă, pentru asta trebuie să înveți cum să atragă întinderi.
Puteți să mergeți și să citiți ce este algoritmul Brezenham. dar majoritatea studenților mei sunt blocați citind o versiune intregă imediat, așa că să scriem codul noi înșine.
Cum arata cel mai simplu cod care traseaza o linie intre doua puncte (x0, y0) si (x1, y1)?
Aparent, ceva de genul:
Codul instantaneu este disponibil pe github.
Problema cu acest cod (pe lângă eficiență) este alegerea constantei, pe care am luat-o egală cu 0,01.
Dacă dintr-o dată o considerăm egală cu .1, segmentul nostru va arăta astfel:
Putem găsi ușor pasul potrivit: este doar numărul de pixeli care trebuie să fie desenate.
Cel mai simplu cod (cu erori!) Arată cam așa:
Atenție: prima sursă de erori din acest cod pentru elevii mei este diviziunea întregului tip (x-x0) / (x1-x0).
Mai mult, dacă încercăm să desenați acest cod aici, aceste linii:
Se pare că o linie este bună, a doua cu găuri, iar a treia nu este deloc.
Gauri în unul dintre segmente datorită faptului că înălțimea sa este mai mare decât lățimea.
Elevii mei oferă de multe ori o remediere: dacă (dx> dy) altceva.
Ei bine, pomi de Crăciun!
Acest cod funcționează bine. Acesta este un fel de complexitate pe care vreau să o văd în versiunea finală a randării noastre.
Desigur, este ineficient (diviziuni multiple și altele asemenea), dar este scurt și ușor de citit.
Rețineți că nu există nici o afirmație în ea, nu există verificări pentru plecarea în străinătate, este rău.
Dar încerc să nu deranjez acest cod, deoarece este citit foarte mult, în timp ce îmi amintesc în mod sistematic despre necesitatea verificărilor.
Deci, codul anterior funcționează bine, dar poate fi optimizat.
Optimizarea este un lucru periculos, trebuie să înțelegeți clar ce cod de platformă va funcționa.
Optimizarea codului pentru o placă grafică sau doar un CPU este un lucru complet diferit.
Înainte și în timpul oricărei optimizări, codul trebuie să fie profilat.
Încercați să ghiciți ce operație este cea mai intensă din punct de vedere al resurselor aici?
Pentru teste, am desenat de la 1000000 de ori cele 3 segmente pe care le-am desenat înainte. Procesorul meu: este CPU Intel® Core ™ i5-3450 @ 3.10GHz.
Acest cod pentru fiecare pixel apelează constructorul de copie TGAColor.
Și acesta este 1.000.000 * 3 piese * aproximativ 50 de picsule pe bucată. Destul de puține provocări.
De unde începem optimizarea?
Profilul ne va spune.
Am compilat codul cu tastele g ++ -ggdb -g3 -pg -O0; apoi a fugit gprof:
Rețineți că fiecare divizie are același divizor, să o luăm în afara buclei.
Eroarea variabilă ne determină distanța față de linia ideală din pixelul nostru curent (x, y).
De fiecare dată când o eroare depășește un pixel, mărim (micșorează) y câte unul și reducem eroarea cu unul.
Și de ce avem nevoie de puncte plutitoare? Singurul motiv este o diviziune prin dx și o comparație cu .5 în corpul bucla.
Putem scăpa de punctul de flotare înlocuind variabila de eroare cu o altă, să o numim eroare2, este egală cu eroarea * dx * 2.
Aici este codul echivalent:
O altă conversație, acum este suficient să eliminați copiile inutile când se numește funcția, culoarea frontală este prin referință (sau pur și simplu prin includerea pavilionului de compilare -O3) și totul este gata. Nu o singură înmulțire, nici o singură diviziune a codului.
Timpul de funcționare a scăzut de la 2,95 secunde la 0,64.
Sârmă.
Acum, totul este gata pentru a crea un fir de randare. Imaginea codului și a modelului de testare sunt aici.
Am folosit formatul fișierului wavefront pentru a stoca modelul. Tot ce avem nevoie pentru redare este de a citi din fișier o serie de vârfuri ale formularului
v 0,608654 -0,568839 -0,416318
[. ]
acestea sunt coordonatele x, y, z, un vertex pe fiecare linie a fișierului
și fațete
f 1193/1240/1193 1180/1227/1180 1179/1226/1179
[. ]
Aici suntem interesati de primul numar dupa fiecare gol, acesta este numarul vertexului din matricea pe care am citit-o mai devreme. Astfel, această linie spune că nodurile 1193, 1180 și 1179 formează un triunghi.
Fișierul model.cpp conține cel mai simplu parser.
Noi scriem un astfel de ciclu în main.cpp și voila, redarea noastră de sârmă este gata.
Data viitoare vom desena triunghiuri 2D si vom face randarea.