Introducere în OpenGL: Un tutorial de redare a textului 3D
Publicat: 2022-03-11Cu disponibilitatea unor instrumente precum DirectX și OpenGL, scrierea unei aplicații desktop care redă elemente 3D nu este foarte dificilă în zilele noastre. Cu toate acestea, la fel ca multe tehnologii, uneori există obstacole care îngreunează dezvoltatorii care încearcă să intre în această nișă. De-a lungul timpului, cursa dintre DirectX și OpenGL a făcut ca aceste tehnologii să devină mai accesibile pentru dezvoltatori, împreună cu o documentare mai bună și un proces mai ușor de a deveni un dezvoltator calificat DirectX sau OpenGL.
DirectX, introdus și întreținut de Microsoft, este o tehnologie specifică platformei Windows. Pe de altă parte, OpenGL este un API multiplatform pentru arena grafică 3D a cărei specificație este menținută de grupul Khronos.
În această introducere la OpenGL, voi explica cum să scrieți o aplicație foarte simplă pentru a reda modele de text 3D. Vom folosi Qt/Qt Creator pentru a implementa interfața de utilizare, facilitând compilarea și rularea acestei aplicații pe mai multe platforme. Codul sursă al prototipului construit pentru acest articol este disponibil pe GitHub.
Scopul acestei aplicații simple este de a genera modele 3D, de a le salva într-un fișier cu un format simplu și de a le deschide și reda pe ecran. Modelul 3D din scena redată va fi rotativ și zoomabil, pentru a oferi un sentiment mai bun al profunzimii și dimensiunii.
Cerințe preliminare
Înainte de a începe, va trebui să ne pregătim mediul de dezvoltare cu câteva instrumente utile pentru acest proiect. Primul lucru de care avem nevoie este cadrul Qt și utilitățile relevante, care pot fi descărcate de pe www.qt.io. Poate fi disponibil și prin managerul de pachete standard al sistemului de operare; dacă acesta este cazul, poate doriți să încercați mai întâi cu el. Acest articol necesită o anumită familiaritate cu cadrul Qt. Cu toate acestea, dacă nu sunteți familiarizat cu cadrul, vă rugăm să nu vă descurajați să urmați, deoarece prototipul se bazează pe unele caracteristici destul de banale ale cadrului.
De asemenea, puteți utiliza Microsoft Visual Studio 2013 pe Windows. În acest caz, vă rugăm să vă asigurați că utilizați programul de completare Qt corespunzător pentru Visual Studio.
În acest moment, este posibil să doriți să clonați depozitul din GitHub și să îl urmați pe măsură ce citiți acest articol.
Prezentare generală a OpenGL
Vom începe prin a crea un proiect simplu de aplicație Qt cu un singur widget de document. Deoarece este un widget simplu, compilarea și rularea acestuia nu va produce nimic util. Cu designerul Qt, vom adăuga un meniu „Fișier” cu patru elemente: „Nou…”, „Deschide…”, „Închidere” și „Ieșire”. Puteți găsi codul care leagă aceste elemente de meniu la acțiunile lor corespunzătoare în depozit.
Făcând clic pe „Nou...” ar trebui să apară un dialog care va arăta cam așa:
Aici, utilizatorul poate introduce un text, alege un font, poate modifica înălțimea modelului rezultat și poate genera un model 3D. Făcând clic pe „Creare” ar trebui să salveze modelul și, de asemenea, ar trebui să îl deschidă dacă utilizatorul alege opțiunea corespunzătoare din colțul din stânga jos. După cum vă puteți da seama, scopul aici este de a converti un text introdus de utilizator într-un model 3D și de a-l reda pe afișaj.
Proiectul va avea o structură simplă, iar componentele vor fi împărțite într-o mână de fișiere C++ și antet:
createcharmodeldlg.h/cpp
Fișierele conțin obiect derivat QDialog. Acest lucru implementează widget-ul de dialog care permite utilizatorului să tasteze text, să selecteze fontul și să aleagă dacă să salveze rezultatul într-un fișier și/sau să îl afișeze în 3D.
gl_widget.h/cpp
Conține implementarea obiectului derivat QOpenGLWidget. Acest widget este folosit pentru a reda scena 3D.
fereastră principală.h/cpp
Conține implementarea widget-ului principal al aplicației. Aceste fișiere au fost lăsate neschimbate, deoarece au fost create de expertul Qt Creator.
principal.cpp
Conține funcția main(…), care creează widgetul principal al aplicației și îl arată pe ecran.
model2d_processing.h/cpp
Conține funcționalitatea de creare a scenei 2D.
model3d.h/cpp
Conține structuri care stochează obiecte model 3D și permit operațiunilor să lucreze asupra lor (salvare, încărcare etc.).
model_creator.h/cpp
Conține implementarea clasei care permite crearea unui obiect model de scenă 3D.
Implementarea OpenGL
Pentru concizie, vom omite detaliile evidente ale implementării interfeței cu utilizatorul cu Qt Designer și codul care definește comportamentele elementelor interactive. Există cu siguranță câteva aspecte mai interesante ale acestei aplicații prototip, care sunt nu numai importante, ci și relevante pentru codificarea și randarea modelelor 3D pe care vrem să le acoperim. De exemplu, primul pas de conversie a textului într-un model 3D în acest prototip implică conversia textului într-o imagine monocromă 2D. Odată ce această imagine este generată, este posibil să știți ce pixel al imaginii formează textul și care sunt doar spațiu „gol”. Există câteva modalități mai simple de redare a textului de bază folosind OpenGL, dar adoptăm această abordare pentru a acoperi câteva detalii esențiale ale redării 3D cu OpenGL.
Pentru a genera această imagine, instanțiem un obiect QImage cu indicatorul QImage::Format_Mono. Deoarece tot ce trebuie să știm este ce pixeli fac parte din text și care nu, o imagine monocromă ar trebui să funcționeze bine. Când utilizatorul introduce un text, actualizăm sincron acest obiect QImage. Pe baza mărimii fontului și a lățimii imaginii, facem tot posibilul să se potrivească textului la înălțimea definită de utilizator.
În continuare, enumerăm toți pixelii care fac parte din text - în acest caz, pixelii negri. Fiecare pixel aici este tratat ca unități pătrate separate. Pe baza acestui lucru, putem genera o listă de triunghiuri, calculând coordonatele vârfurilor lor și să le stocăm în fișierul nostru model 3D.
Acum că avem propriul nostru format simplu de fișier model 3D, putem începe să ne concentrăm asupra redării acestuia. Pentru redarea 3D bazată pe OpenGL, Qt oferă un widget numit QOpenGLWidget. Pentru a utiliza acest widget, trei funcții pot fi înlocuite:
- initializeGl() - aici merge codul de initializare
- paintGl() - această metodă este apelată de fiecare dată când widget-ul este redesenat
- resizeGl(int w, int h) - această metodă este apelată cu lățimea și înălțimea widget-ului de fiecare dată când este redimensionat
Vom inițializa widget-ul setând configurația shader corespunzătoare în metoda initializeGl.
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
Prima linie face ca programul să arate doar acei pixeli randați care sunt mai aproape de noi, mai degrabă decât cei care sunt în spatele altor pixeli și nu văd. A doua linie specifică tehnica de umbrire plată. A treia linie face ca programul să redea triunghiuri, indiferent de direcția către care se îndreaptă normalele lor.
Odată inițializat, redăm modelul pe afișaj de fiecare dată când este apelat paintGl. Înainte de a suprascrie metoda paintGl, trebuie să pregătim tamponul. Pentru a face asta, mai întâi creăm un handle de buffer. Apoi legăm mânerul la unul dintre punctele de legare, copiem datele sursă în buffer și, în final, îi spunem programului să dezlege tamponul:
// Get the Qt object which allows to operate with buffers QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Create the buffer handle funcs.glGenBuffers(1, &handle); // Select buffer by its handle (so we'll use this buffer // further) funcs.glBindBuffer(GL_ARRAY_BUFFER, handle); // Copy data into the buffer. Being copied, // source data is not used any more and can be released funcs.glBufferData(GL_ARRAY_BUFFER, size_in_bytes, src_data, GL_STATIC_DRAW); // Tell the program we've finished with the handle funcs.glBindBuffer(GL_ARRAY_BUFFER, 0);
În cadrul metodei de suprascriere paintGl, folosim o serie de vârfuri și o serie de date normale pentru a desena triunghiurile pentru fiecare cadru:

QOpenGLFunctions funcs(QOpenGLContext::currentContext()); // Vertex data glEnableClientState(GL_VERTEX_ARRAY);// Work with VERTEX buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hVertexes); // Use this one glVertexPointer(3, GL_FLOAT, 0, 0); // Data format funcs.glVertexAttribPointer(m_coordVertex, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Normal data glEnableClientState(GL_NORMAL_ARRAY);// Work with NORMAL buffer funcs.glBindBuffer(GL_ARRAY_BUFFER, m_hNormals);// Use this one glNormalPointer(GL_FLOAT, 0, 0); // Data format funcs.glEnableVertexAttribArray(m_coordNormal); // Shader attribute funcs.glVertexAttribPointer(m_coordNormal, 3, GL_FLOAT, GL_FALSE, 0, 0); // Provide into shader program // Draw frame glDrawArrays(GL_TRIANGLES, 0, (3 * m_model.GetTriangleCount())); // Rendering finished, buffers are not in use now funcs.glDisableVertexAttribArray(m_coordNormal); funcs.glBindBuffer(GL_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY);
Pentru o performanță îmbunătățită, am folosit Vertex Buffer Object (VBO) în aplicația noastră prototip. Acest lucru ne permite să stocăm datele în memoria video și să le folosim direct pentru randare. O metodă alternativă la aceasta implică furnizarea datelor (coordonatele vârfurilor, normalele și culorile) din codul de randare:
glBegin(GL_TRIANGLES); // Provide coordinates of triangle #1 glVertex3f( x[0], y[0], z[0]); glVertex3f( x[1], y[1], z[1]); glVertex3f( x[2], y[2], z[2]); // Provide coordinates of other triangles ... glEnd();
Aceasta poate părea o soluție mai simplă; totuși, are implicații serioase de performanță, deoarece acest lucru necesită ca datele să circule prin magistrala memoriei video - un proces relativ mai lent. După implementarea metodei paintGl, trebuie să acordăm atenție shaders:
m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Vertex, QString::fromUtf8( "#version 400\r\n" "\r\n" "layout (location = 0) in vec3 coordVertexes;\r\n" "layout (location = 1) in vec3 coordNormals;\r\n" "flat out float lightIntensity;\r\n" "\r\n" "uniform mat4 matrixVertex;\r\n" "uniform mat4 matrixNormal;\r\n" "\r\n" "void main()\r\n" "{\r\n" " gl_Position = matrixVertex * vec4(coordVertexes, 1.0);\r\n" " lightIntensity = abs((matrixNormal * vec4(coordNormals, 1.0)).z);\r\n" "}")); m_shaderProgram.addShaderFromSourceCode(QOpenGLShader::Fragment, QString::fromUtf8( "#version 400\r\n" "\r\n" "flat in float lightIntensity;\r\n" "\r\n" "layout (location = 0) out vec4 FragColor;\r\n" "uniform vec3 fragmentColor;\r\n" "\r\n" "void main()\r\n" "{\r\n" " FragColor = vec4(fragmentColor * lightIntensity, 1.0);\r\n" "}")); m_shaderProgram.link(); m_shaderProgram.bind(); m_coordVertex = m_shaderProgram.attributeLocation(QString::fromUtf8("coordVertexes")); m_coordNormal = m_shaderProgram.attributeLocation(QString::fromUtf8("coordNormals")); m_matrixVertex = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixVertex")); m_matrixNormal = m_shaderProgram.uniformLocation(QString::fromUtf8("matrixNormal")); m_colorFragment = m_shaderProgram.uniformLocation(QString::fromUtf8("fragmentColor"));
Cu OpenGL, shaders-urile sunt implementate folosind un limbaj cunoscut sub numele de GLSL. Limbajul este conceput pentru a facilita manipularea datelor 3D înainte de a fi redate. Aici, vom avea nevoie de două shadere: vertex shader și fragment shader. În vertex shader, vom transforma coordonatele cu matricea de transformare pentru a aplica rotația și zoomul și pentru a calcula culoarea. În fragment shader, vom atribui culoare fragmentului. Aceste programe shader trebuie apoi compilate și legate de context. OpenGL oferă modalități simple de a lega cele două medii, astfel încât parametrii din interiorul programului să poată fi accesați sau alocați din exterior:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
În codul vertex shader, calculăm noua poziție a vârfurilor aplicând matricea de transformare pe vârfurile originale:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
Pentru a calcula această matrice de transformare, calculăm câteva matrice separate: scara ecranului, traducerea scenei, scalarea, rotirea și centrarea. Găsim apoi produsul acestor matrici pentru a calcula matricea finală de transformare. Începeți prin a traduce centrul modelului la origine (0, 0, 0), care este și centrul ecranului. Rotația este determinată de interacțiunea utilizatorului cu scena folosind un dispozitiv de indicare. Utilizatorul poate face clic pe scenă și trage în jur pentru a se roti. Când utilizatorul dă clic, stocăm poziția cursorului, iar după o mișcare avem a doua poziție a cursorului. Folosind aceste două coordonate, împreună cu centrul scenei, formăm un triunghi. În urma unor calcule simple, putem determina unghiul de rotație și ne putem actualiza matricea de rotație pentru a reflecta această schimbare. Pentru scalare, ne bazăm pur și simplu pe rotița mouse-ului pentru a modifica factorul de scalare al axelor X și Y ale widget-ului OpenGL. Modelul este translat înapoi cu 0,5 pentru a-l menține în spatele planului din care este redată scena. În cele din urmă, pentru a menține raportul natural de aspect, trebuie să ajustam scăderea expansiunii modelului de-a lungul părții mai lungi (spre deosebire de scena OpenGL, widget-ul în care este redat poate avea dimensiuni fizice diferite de-a lungul fiecărei axe). Combinând toate acestea, calculăm matricea de transformare finală după cum urmează:
void GlWidget::GetMatrixTransform(QMatrix4x4& matrixVertex, const Model3DEx& model) { matrixVertex.setToIdentity(); QMatrix4x4 matrixScaleScreen; double dimMin = static_cast<double>(qMin(width(), height())); float scaleScreenVert = static_cast<float>(dimMin / static_cast<double>(height())); float scaleScreenHorz = static_cast<float>(dimMin / static_cast<double>(width())); matrixScaleScreen.scale(scaleScreenHorz, scaleScreenVert, 1.0f); QMatrix4x4 matrixCenter; float centerX, centerY, centerZ; model.GetCenter(centerX, centerY, centerZ); matrixCenter.translate(-centerX, -centerY, -centerZ); QMatrix4x4 matrixScale; float radius = 1.0; model.GetRadius(radius); float scale = static_cast<float>(m_scaleCoeff / radius); matrixScale.scale(scale, scale, 0.5f / radius); QMatrix4x4 matrixTranslateScene; matrixTranslateScene.translate(0.0f, 0.0f, -0.5f); matrixVertex = matrixScaleScreen * matrixTranslateScene * matrixScale * m_matrixRotate * matrixCenter; }
Concluzie
În această introducere în redarea OpenGL 3D, am explorat una dintre tehnologiile care permit ud să utilizeze placa noastră video pentru a reda un model 3D. Acest lucru este mult mai eficient decât utilizarea ciclurilor CPU în același scop. Am folosit o tehnică de umbrire foarte simplă și am făcut scena interactivă prin gestionarea intrărilor utilizatorului de la mouse. Am evitat să folosim magistrala memoriei video pentru a trece date înainte și înapoi între memoria video și program. Chiar dacă tocmai am randat o singură linie de text în 3D, scenele mai complicate pot fi redate în moduri foarte similare.
Pentru a fi corect, acest tutorial abia a zgâriat suprafața modelării și redării 3D. Acesta este un subiect vast, iar acest tutorial OpenGL nu poate pretinde că acesta este tot ce trebuie să știți pentru a putea crea jocuri 3D sau software de modelare. Cu toate acestea, scopul acestui articol este de a vă oferi o privire în acest domeniu și de a arăta cât de ușor puteți începe cu OpenGL pentru a construi aplicații 3D.