Introduzione a OpenGL: un tutorial per il rendering del testo 3D
Pubblicato: 2022-03-11Con la disponibilità di strumenti come DirectX e OpenGL, scrivere un'applicazione desktop che esegue il rendering di elementi 3D non è molto difficile al giorno d'oggi. Tuttavia, come molte tecnologie, a volte ci sono ostacoli che rendono difficile per gli sviluppatori che cercano di entrare in questa nicchia. Nel tempo, la corsa tra DirectX e OpenGL ha reso queste tecnologie più accessibili agli sviluppatori, insieme a una migliore documentazione e un processo più semplice per diventare uno sviluppatore esperto di DirectX o OpenGL.
DirectX, introdotto e mantenuto da Microsoft, è una tecnologia specifica per la piattaforma Windows. D'altra parte, OpenGL è un'API multipiattaforma per l'arena della grafica 3D le cui specifiche sono mantenute dal Gruppo Khronos.
In questa introduzione a OpenGL, spiegherò come scrivere un'applicazione molto semplice per il rendering di modelli di testo 3D. Utilizzeremo Qt/Qt Creator per implementare l'interfaccia utente, semplificando la compilazione e l'esecuzione di questa applicazione su più piattaforme. Il codice sorgente del prototipo creato per questo articolo è disponibile su GitHub.
L'obiettivo di questa semplice applicazione è generare modelli 3D, salvarli in un file con un formato semplice e aprirli e renderli sullo schermo. Il modello 3D nella scena renderizzata sarà ruotabile e zoomabile, per dare un migliore senso di profondità e dimensione.
Prerequisiti
Prima di iniziare, dovremo preparare il nostro ambiente di sviluppo con alcuni strumenti utili per questo progetto. La prima cosa di cui abbiamo bisogno è il framework Qt e le relative utilità, che possono essere scaricate da www.qt.io. Potrebbe anche essere disponibile tramite il gestore di pacchetti standard del sistema operativo; in tal caso, potresti provare prima con esso. Questo articolo richiede una certa familiarità con il framework Qt. Tuttavia, se non hai familiarità con il framework, non scoraggiarti a seguirlo, poiché il prototipo si basa su alcune caratteristiche abbastanza banali del framework.
Puoi anche usare Microsoft Visual Studio 2013 su Windows. In tal caso, assicurati di utilizzare il componente aggiuntivo Qt appropriato per Visual Studio.
A questo punto, potresti voler clonare il repository da GitHub e seguirlo mentre leggi questo articolo.
Panoramica di OpenGL
Inizieremo creando un semplice progetto applicativo Qt con un singolo widget di documento. Poiché è un widget essenziale, compilarlo ed eseguirlo non produrrà nulla di utile. Con Qt designer, aggiungeremo un menu "File" con quattro voci: "Nuovo...", "Apri...", "Chiudi" ed "Esci". Puoi trovare il codice che lega queste voci di menu alle loro azioni corrispondenti nel repository.
Facendo clic su "Nuovo..." dovrebbe apparire una finestra di dialogo simile a questa:
Qui, l'utente può inserire del testo, scegliere un carattere, modificare l'altezza del modello risultante e generare un modello 3D. Facendo clic su "Crea" dovrebbe salvare il modello e dovrebbe anche aprirlo se l'utente sceglie l'opzione appropriata dall'angolo in basso a sinistra. Come puoi vedere, l'obiettivo qui è convertire del testo inserito dall'utente in un modello 3D e renderizzarlo sul display.
Il progetto avrà una struttura semplice e i componenti saranno suddivisi in una manciata di file C++ e header:
createcharmodeldlg.h/cpp
I file contengono oggetti derivati da QDialog. Ciò implementa il widget di dialogo che consente all'utente di digitare testo, selezionare il carattere e scegliere se salvare il risultato in un file e/o visualizzarlo in 3D.
gl_widget.h/cpp
Contiene l'implementazione dell'oggetto derivato QOpenGLWidget. Questo widget viene utilizzato per il rendering della scena 3D.
mainwindow.h/cpp
Contiene l'implementazione del widget principale dell'applicazione. Questi file sono stati lasciati invariati poiché sono stati creati dalla procedura guidata di Qt Creator.
main.cpp
Contiene la funzione main(…), che crea il widget dell'applicazione principale e lo mostra sullo schermo.
model2d_processing.h/cpp
Contiene funzionalità di creazione di scene 2D.
modello3d.h/cpp
Contiene strutture che memorizzano oggetti del modello 3D e consentono operazioni su di essi (salvataggio, caricamento ecc.).
creatore_modello.h/cpp
Contiene l'implementazione della classe che consente la creazione di oggetti modello scena 3D.
Implementazione OpenGL
Per brevità, salteremo i dettagli ovvi dell'implementazione dell'interfaccia utente con Qt Designer e il codice che definisce i comportamenti degli elementi interattivi. Ci sono sicuramente alcuni aspetti più interessanti di questa applicazione prototipo, che non sono solo importanti ma anche rilevanti per la codifica e il rendering del modello 3D che vogliamo coprire. Ad esempio, il primo passaggio della conversione del testo in un modello 3D in questo prototipo prevede la conversione del testo in un'immagine monocromatica 2D. Una volta generata questa immagine, è possibile sapere quale pixel dell'immagine forma il testo e quali sono solo uno spazio "vuoto". Esistono alcuni modi più semplici per eseguire il rendering del testo di base utilizzando OpenGL, ma stiamo adottando questo approccio per coprire alcuni dettagli fondamentali del rendering 3D con OpenGL.
Per generare questa immagine, istanziamo un oggetto QImage con il flag QImage::Format_Mono. Poiché tutto ciò che dobbiamo sapere è quali pixel fanno parte del testo e quali no, un'immagine monocromatica dovrebbe funzionare perfettamente. Quando l'utente inserisce del testo, aggiorniamo in modo sincrono questo oggetto QImage. In base alla dimensione del carattere e alla larghezza dell'immagine, facciamo del nostro meglio per adattare il testo all'altezza definita dall'utente.
Successivamente, enumeriamo tutti i pixel che fanno parte del testo, in questo caso i pixel neri. Ogni pixel qui viene trattato come unità quadrate separate. Sulla base di ciò, possiamo generare un elenco di triangoli, calcolare le coordinate dei loro vertici e memorizzarli nel nostro file modello 3D.
Ora che abbiamo il nostro semplice formato di file modello 3D, possiamo iniziare a concentrarci sul rendering. Per il rendering 3D basato su OpenGL, Qt fornisce un widget chiamato QOpenGLWidget. Per utilizzare questo widget, tre funzioni possono essere sovrascritte:
- initializeGl() - è qui che va il codice di inizializzazione
- paintGl() - questo metodo viene chiamato ogni volta che il widget viene ridisegnato
- resizeGl(int w, int h) - questo metodo viene chiamato con la larghezza e l'altezza del widget ogni volta che viene ridimensionato
Inizializzeremo il widget impostando la configurazione dello shader appropriata nel metodo initializeGl.
glEnable(GL_DEPTH_TEST); glShadeModel(GL_FLAT); glDisable(GL_CULL_FACE);
La prima riga fa in modo che il programma mostri solo quei pixel renderizzati che sono più vicini a noi, piuttosto che quelli che sono dietro altri pixel e fuori dalla vista. La seconda riga specifica la tecnica dell'ombreggiatura piatta. La terza riga fa in modo che il programma visualizzi i triangoli indipendentemente dalla direzione a cui puntano le loro normali.
Una volta inizializzato, eseguiamo il rendering del modello sul display ogni volta che viene chiamato paintGl. Prima di sovrascrivere il metodo paintGl, dobbiamo preparare il buffer. Per fare ciò, creiamo prima un handle del buffer. Quindi leghiamo l'handle a uno dei punti di collegamento, copiamo i dati di origine nel buffer e infine diciamo al programma di annullare l'associazione del buffer:
// 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);
All'interno del metodo di sovrascrittura paintGl, utilizziamo un array di vertici e un array di dati normali per disegnare i triangoli per ogni frame:

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);
Per migliorare le prestazioni, abbiamo utilizzato Vertex Buffer Object (VBO) nella nostra applicazione prototipo. Questo ci consente di archiviare i dati nella memoria video e di utilizzarli direttamente per il rendering. Un metodo alternativo a questo consiste nel fornire i dati (coordinate dei vertici, normali e colori) dal codice di rendering:
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();
Questa può sembrare una soluzione più semplice; tuttavia, ha serie implicazioni sulle prestazioni, poiché ciò richiede che i dati viaggino attraverso il bus di memoria video, un processo relativamente più lento. Dopo aver implementato il metodo paintGl, dobbiamo prestare attenzione agli shader:
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"));
Con OpenGL, gli shader vengono implementati utilizzando un linguaggio noto come GLSL. Il linguaggio è progettato per semplificare la manipolazione dei dati 3D prima del rendering. Qui avremo bisogno di due shader: vertex shader e fragment shader. In vertex shader, trasformeremo le coordinate con la matrice di trasformazione per applicare rotazione e zoom e per calcolare il colore. Nello shader del frammento, assegneremo il colore al frammento. Questi programmi shader devono quindi essere compilati e collegati al contesto. OpenGL fornisce modi semplici per collegare i due ambienti in modo che i parametri all'interno del programma possano essere accessibili o assegnati dall'esterno:
// Get model transformation matrix QMatrix4x4 matrixVertex; ... // Calculate the matrix here // Set Shader Program object' parameters m_shaderProgram.setUniformValue(m_matrixVertex, matrixVertex);
Nel codice del vertex shader, calcoliamo la nuova posizione del vertice applicando la matrice di trasformazione sui vertici originali:
gl_Position = matrixVertex * vec4(coordVertexes, 1.0);
Per calcolare questa matrice di trasformazione, calcoliamo alcune matrici separate: scala dello schermo, traslazione della scena, scala, rotazione e centro. Troviamo quindi il prodotto di queste matrici per calcolare la matrice di trasformazione finale. Inizia traducendo il centro del modello nell'origine (0, 0, 0), che è anche il centro dello schermo. La rotazione è determinata dall'interazione dell'utente con la scena utilizzando un dispositivo di puntamento. L'utente può fare clic sulla scena e trascinarla per ruotare. Quando l'utente fa clic, memorizziamo la posizione del cursore e dopo un movimento abbiamo la seconda posizione del cursore. Usando queste due coordinate, insieme al centro della scena, formiamo un triangolo. Seguendo alcuni semplici calcoli possiamo determinare l'angolo di rotazione e possiamo aggiornare la nostra matrice di rotazione per riflettere questo cambiamento. Per il ridimensionamento, ci affidiamo semplicemente alla rotellina del mouse per modificare il fattore di ridimensionamento degli assi X e Y del widget OpenGL. Il modello viene traslato indietro di 0,5 per mantenerlo dietro il piano da cui viene renderizzata la scena. Infine, per mantenere le proporzioni naturali, dobbiamo regolare la diminuzione dell'espansione del modello lungo il lato più lungo (a differenza della scena OpenGL, il widget in cui è renderizzato può avere dimensioni fisiche diverse lungo entrambi gli assi). Combinando tutti questi, calcoliamo la matrice di trasformazione finale come segue:
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; }
Conclusione
In questa introduzione al rendering 3D OpenGL, abbiamo esplorato una delle tecnologie che consentono a ud di utilizzare la nostra scheda video per eseguire il rendering di un modello 3D. Questo è molto più efficiente rispetto all'utilizzo dei cicli della CPU per lo stesso scopo. Abbiamo usato una tecnica di shading molto semplice e abbiamo reso la scena interattiva attraverso la gestione degli input dell'utente dal mouse. Abbiamo evitato di utilizzare il bus di memoria video per trasferire i dati avanti e indietro tra la memoria video e il programma. Anche se abbiamo eseguito il rendering di una singola riga di testo in 3D, le scene più complicate possono essere renderizzate in modi molto simili.
Ad essere onesti, questo tutorial ha appena graffiato la superficie della modellazione e del rendering 3D. Questo è un argomento vasto e questo tutorial OpenGL non può affermare che questo sia tutto ciò che devi sapere per essere in grado di creare giochi 3D o software di modellazione. Tuttavia, lo scopo di questo articolo è darti una sbirciatina in questo regno e mostrare con quanta facilità puoi iniziare con OpenGL per creare applicazioni 3D.