====== Qt e OpenGL - Le texture tridimensionali ====== Nel [[tutorial_qt:qt_opengl_04_texture_2d|precedente articolo]] abbiamo cominciato a parlare delle texture OpenGL, analizzando un esempio in cui abbiamo disegnato un cubo rotante con un numero disegnato su ciascuna faccia. Il numero era una texture bidimensionale (sempre la stessa!). Immagino siate rimasti non poco perplessi nel vedere lo stesso numero su tutte le facce del cubo! Perciò ora ci occuperemo di dare ad ogni faccia un numero diverso. La flessibilità delle API OpenGL ci pemettono di ottenere questo risultato in più modi: il più immediato potrebbe essere la creazione di sei texture. Un altro metodo è descritto nel progetto di esempio [[http://doc.qt.io/qt-5/qtopengl-cube-example.html|Cube OpenGL ES 2.0]] disponbile nella documentazione delle Qt, in cui le facce del cubo sono disegnate in successione in un'unica immagine e con le coordinate texture si specifica quale porzione di quest'immagine disegnare su ciascuna faccia. In questo articolo otterremo lo stesso risultato creando invece un vettore di texture. A tal proposito nelle OpenGL 3 è disponibile uno strumento ad hoc, chiamato [[https://www.opengl.org/wiki/Array_Texture|texture 2D array]]. Per evitare problemi di compatibilità, ci serviremo invece di una texture tridimensionale, disponibile anche in versioni precedenti delle API OpenGL: la sostanza è la stessa, cambiano leggermente le funzioni per accedere ai valori delle texture ed i relativi parametri. ~~READMORE~~ ===== Cosa sono le texture tridimensionali ===== Abbiamo già visto che le texture bidimensionali sono dei vettori //di due dimensioni//, analoghe come struttura dei dati alle immagini bitmap. Caricando un'immagine bitmap su una texture, ciascun elemento della texture conterrà i byte associati ai colori di un pixel. Le texture tridimensionali sono invece vettori di //tre dimensioni// su cui possiamo caricare, ad esempio, più immagini in successione. E questo è quello che faremo nel seguito, caricandovi in successione le sei immagini che vogliamo disegnare sulle facce del cubo. ===== Cosa cambia negli shader ===== Partiamo dal codice sorgente del [[tutorial_qt:qt_opengl_04_texture_2d|precedente articolo]], apportando progressivamente le modifiche necessarie. Modifichiamo prima di tutto gli shader. Nel vertex shader dovremo aggiungere l'attributo texNum che ci permetterà di definire la texture che vogliamo sovrapporre sulla faccia che stiamo disegnando. Tale attributo viene passato al fragment shader tramite la variabile varying outTexNum. // vertex shader attribute highp vec3 vertex; attribute mediump vec2 texCoord; attribute mediump float texNum; uniform highp mat4 matrix; varying mediump vec2 outTexCoord; varying mediump float outTexNum; void main() { gl_Position = matrix * highp vec4(vertex, 1.0); outTexCoord = texCoord; outTexNum = texNum; } Nel fragment shader la texture sarà caricata nella variabile texture di tipo sampler3D. // fragment shader uniform highp sampler3D texture; varying mediump vec2 outTexCoord; varying mediump float outTexNum; void main() { mediump vec2 flippedVertexCoord = vec2(outTexCoord.x, 1.0 - outTexCoord.y); gl_FragColor = texture3D( texture, vec3(flippedVertexCoord, outTexNum) ); } Merita spendere due parole in più sulla funzione texture3D. I tre argomenti sono variabili float il cui valore di norma è compreso tra 0 e 1. Poiché caricheremo nella texture sei immagini, se impostiamo i parametri di interpolazione della texture a GL_NEAREST, la n-esima immagine potrà essere individuata con un valore numerico compreso tra $(n-1)/6$ e $n/6$. Poiché invece utilizzeremo GL_LINEAR (per evitare la sgranatura delle immagini), utilizzeremo i valori centrali di ciascun intervallo, individuando quindi l'n-esima immagine con il valore $(2n - 1) / 12$ (il punto medio degli estremi dell'intervallo visto prima). ===== Chiamata diretta delle API OpenGL ===== Il primo passo da compiere è modificare il VBO associato ai vertici, aggiungendo l'attributo texNum struct VertexData { QVector3D coord; QVector2D texCoord; float texNum; }; Quindi, nel metodo initializeGL(), modifichiamo il vettore contenente i dati da caricare sul VBO // *** VBO dati vertici *** // vettore contenete i dati associati ai vertici float l = 1.0 / 2.0; float DTextNum = 1.0f / 6.0f; float textNum0 = DTextNum * 0.50f; VertexData vertexData[] = { // faccia 0 - 1 {QVector3D(-l, -l, l), QVector2D(0.0f, 0.0f), textNum0}, {QVector3D( l, -l, l), QVector2D(1.0f, 0.0f), textNum0}, {QVector3D(-l, l, l), QVector2D(0.0f, 1.0f), textNum0}, {QVector3D( l, l, l), QVector2D(1.0f, 1.0f), textNum0}, // faccia 1 - 2 {QVector3D( l, -l, l), QVector2D(0.0f, 0.0f), textNum0 + 1.0f * DTextNum}, {QVector3D( l, -l, -l), QVector2D(1.0f, 0.0f), textNum0 + 1.0f * DTextNum}, {QVector3D( l, l, l), QVector2D(0.0f, 1.0f), textNum0 + 1.0f * DTextNum}, {QVector3D( l, l, -l), QVector2D(1.0f, 1.0f), textNum0 + 1.0f * DTextNum}, // faccia 2 - 6 {QVector3D( l, -l, -l), QVector2D(0.0f, 0.0f), textNum0 + 5.0f * DTextNum}, {QVector3D(-l, -l, -l), QVector2D(1.0f, 0.0f), textNum0 + 5.0f * DTextNum}, {QVector3D( l, l, -l), QVector2D(0.0f, 1.0f), textNum0 + 5.0f * DTextNum}, {QVector3D(-l, l, -l), QVector2D(1.0f, 1.0f), textNum0 + 5.0f * DTextNum}, // faccia 3 - 5 {QVector3D(-l, -l, -l), QVector2D(0.0f, 0.0f), textNum0 + 4.0f * DTextNum}, {QVector3D(-l, -l, l), QVector2D(1.0f, 0.0f), textNum0 + 4.0f * DTextNum}, {QVector3D(-l, l, -l), QVector2D(0.0f, 1.0f), textNum0 + 4.0f * DTextNum}, {QVector3D(-l, l, l), QVector2D(1.0f, 1.0f), textNum0 + 4.0f * DTextNum}, // faccia 4 - 3 {QVector3D(-l, -l, -l), QVector2D(0.0f, 0.0f), textNum0 + 2.0f * DTextNum}, {QVector3D( l, -l, -l), QVector2D(1.0f, 0.0f), textNum0 + 2.0f * DTextNum}, {QVector3D(-l, -l, l), QVector2D(0.0f, 1.0f), textNum0 + 2.0f * DTextNum}, {QVector3D( l, -l, l), QVector2D(1.0f, 1.0f), textNum0 + 2.0f * DTextNum}, // faccia 5 - 4 {QVector3D(-l, l, l), QVector2D(0.0f, 0.0f), textNum0 + 3.0f * DTextNum}, {QVector3D( l, l, l), QVector2D(1.0f, 0.0f), textNum0 + 3.0f * DTextNum}, {QVector3D(-l, l, -l), QVector2D(0.0f, 1.0f), textNum0 + 3.0f * DTextNum}, {QVector3D( l, l, -l), QVector2D(1.0f, 1.0f), textNum0 + 3.0f * DTextNum} }; Dopodiché carichiamo i dati contenuti nel vettore come già visto // genera il VBO glGenBuffers(1, &m_vertexDataVBO); // attiva il VBO glBindBuffer(GL_ARRAY_BUFFER, m_vertexDataVBO); // carica nella memoria della GPU i valori contenuti in vertexData glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW); // disattiva il VBO glBindBuffer(GL_ARRAY_BUFFER, 0); Per quanto riguarda la texture, i primi comandi sono analoghi a quelli già visti, con la sostituzione di GL_TEXTURE_2D con GL_TEXTURE_3D // *** texture *** // generiamo la texture glGenTextures( 1, &m_texture); // bind della texture glBindTexture(GL_TEXTURE_3D, m_texture); // Parametri per la gestione della texture negli shader // Ripete l'immagine se necessario glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_REPEAT); // quando l'area è più grande della texture, interpola linearmente l'originale glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // quando l'area è più piccola della texture, interpola linearmente l'originale glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); Per la definizione della texture, disegneremo prima di tutto un'immagine, di modo da averne i dati dimensionali. // creiamo un'immagini QImage img = drawImage("1"); Definiamo quindi la texture senza caricare alcun dato per il momento (da notare l'argomento finale NULL) // carichiamo i bit che compongono l'immagine glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA, img.width(), img.height(), 6, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL ); Passiamo quindi a definire le singole texture con glTexSubImage3D glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, img.width(), img.height(), 1, GL_RGBA, GL_UNSIGNED_BYTE, img.bits() ); for( int i=1; i < 6; ++i){ QImage img = drawImage(QString::number(i+1)); glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, i, img.width(), img.height(), 1, GL_RGBA, GL_UNSIGNED_BYTE, img.bits() ); } L'ultima modifica riguarda l'attivazione di texNum nel metodo paintGL() glEnableVertexAttribArray( m_texNumPos ); glVertexAttribPointer( m_texNumPos, 1, // numero di elementi per vertice nel nostro caso 1 GL_FLOAT, // tipo di elemento GL_FALSE, stride, (const void *) (sizeof(QVector3D) + sizeof(QVector2D)) ); Compiliamo il codice e finalmente il nostro cubo ha tutti i numeri al posto giusto! {{ :tutorial_qt:opengltut04bis-01.png |Su ogni faccia del cubo è applicata una texture diversa}} Trovate il codice riepilogativo all'indirizzo [[http://ingegnerialibera.altervista.org/blog-file/opengltut04-01.zip|ingegnerialibera.altervista.org/blog-file/opengltut04bis-01.zip]]. ===== La classe QOpenGLTexture ===== Nella creazione dell'istanza QOpenGLTexture specifichiamo che si tratta di una texture tridimensionale con il comando // *** texture *** m_texture = new QOpenGLTexture( QOpenGLTexture::Target3D ); Come fatto in precedenza, disegniamo una prima immagine per avere le relative dimensioni. QImage img = drawImage("1"); Le dimensioni di un'immagine ci servono per definire la dimensione complessiva della texture tridimensionale e allocare la quantità di memoria necessaria m_texture->setFormat( QOpenGLTexture::RGBA8_UNorm ); m_texture->setSize( img.width(), img.height(), 6); m_texture->allocateStorage(); La classe QOpenGLTexture non ha metodi che permettano di disegnare porzioni limitate della texture, così come avvenuto nel caso precedente con il comando glTexSubImage3D. Perciò questa volta disegneremo prima le sei immagini in un vettore, e a seguire caricheremo in un'unica soluzione il contenuto del vettore nell'oggetto QOpenGLTexture // numero di byte che compongono l'immagine size_t imgSize = img.width()*img.height()*4; // array di destinazione - uchar -> 1 byte uchar fullImg[6][imgSize]; memcpy( fullImg, img.bits(), imgSize ); for( int i=1; i < 6; ++i){ QImage img = drawImage(QString::number(i+1)); memcpy( fullImg[i], img.bits(), imgSize ); } // carichiamo i byte che compongono l'immagine m_texture->setData( 0, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, fullImg ); Per impostare i parametri necessari per la gestione della texture non usiamo il comando glTexParameteri, ma ci serviamo di metodo specifici della classe QOpenGLTexture // Parametri per la gestione della texture negli shader // Ripete l'immagine se necessario m_texture->setWrapMode(QOpenGLTexture::Repeat); // quando l'area è più grande della texture, interpola linearmente l'originale m_texture->setMagnificationFilter(QOpenGLTexture::Linear); // quando l'area è più piccola della texture, interpola linearmente l'originale m_texture->setMinificationFilter(QOpenGLTexture::Linear); Nel metodo paintGL(), per caricare la texture è sufficiente usare il metodo bind() m_texture->bind(); Il risultato finale è identico a quello già visto in precedenza. {{ :tutorial_qt:opengltut04bis-02.png |Lo stesso risultato del paragrafo precedente ottenuto usando la classe QOpenGLTexture}} Il codice riepilogativo è disponibile all'indirizzo [[http://ingegnerialibera.altervista.org/blog-file/opengltut04-01.zip|ingegnerialibera.altervista.org/blog-file/opengltut04bis-02.zip]]. ===== Potrebbero interessarti anche... ===== Un elenco degli altri articoli disponibili sull'argomento: