Strumenti Utente



tutorial_qt:qt_opengl_04_texture_2d

Qt e OpenGL - Le texture e la classe QOpenGLTexture

In questo articolo parleremo di texture. Dopo una breve introduzione su cosa sono le texture nella programmazione openGL, vedremo come implementarle con gli strumenti standard delle OpenGL che con quelli specifici presenti all'interno delle Qt.

Continua così la panoramica sulla programmazione OpenGL e le Qt già cominciata con i precedenti articoli:

Cosa sono le texture?

Partiamo da zero definendo brevemente cosa sono le texture. Se avete avuto precedenti esperienze con la programmazione 3D probabilmente collegherete le texture alle immagini bitmap e sicuramente l'associazione è corretta. Le texture però sono uno strumento che può essere applicato anche anche per definire e caricare immagini sulla GPU . Fondamentalmente le texture sono dei vettori che possiamo usare in generale per caricare sulla GPU vettori da 1 a 3 dimensioni con un numero di elementi praticamente arbitrario (l'unico limite è connesso che le risorse hardware che abbiamo a disposizione).

Poiché un immagine bitmap è una vettore bidimensionale di byte contenenti informazioni sui colori di ciascun pixel dell'immagine, si capisce perché la texture si presti agevolmente come strumento per caricare immagini sulla GPU.

Le texture con le API OpenGL

Per apprendere l'uso delle texture bidimensionali, ridisegniamo il cubo visto nel tutorial precedente, disegnando un'immagine su ciascuna faccia. Per iniziare l'immagine sarà costituita da un numero che creeremo con la funzione drawImage

QImage GLWidget::drawImage( const QString & txt ){
    int border = 10;
    int side = qMax( fontMetrics().width(txt) + border , fontMetrics().height()  + border );
    QPixmap pixmap( side, side );
    pixmap.fill(Qt::white);
    QPainter painter;
    painter.begin(&pixmap);
    painter.setRenderHints(QPainter::HighQualityAntialiasing | QPainter::TextAntialiasing);
    painter.setFont(font());
    painter.setPen( Qt::red );
    painter.drawText(QRect(0,0,side,side), Qt::AlignCenter, txt );
    painter.end();

    return pixmap.toImage().convertToFormat( QImage::Format_RGBA8888 );
}

Con l'ultima riga di comando abbiamo convertito l'immagine nel formato RGBA, di modo che ad ogni pixel siano associati quattro byte (rosso, verde, blu e trasparenza).

Passiamo ora alla programmazione OpenGL vera e propria partendo dagli shader. Per gestire la texture bidimensionale associata all'immagine introduciamo nel fragment shader la variabile texture di tipo sampler2D: si tratta di un vettore bidimensionale di elementi vec4, la cui dimensione verrà definita al momento della creazione della texture nel programma.

Dobbiamo inoltre associare ad ogni vertice un vettore bidimensionale contenente le coordinate di mappatura dell'immagine, vale a dire i parametri sulla base dei quali “spalmare” l'immagine sulla faccia. Le coordinate (0, 0) sono associate all'angolo in basso a sinistra, (1, 1) all'angolo in alto a destra.

Ecco un semplice vertex shader con le funzionalità necessarie

// vertex shader
attribute highp vec3 vertex;
attribute mediump vec2 texCoord;

uniform highp mat4 matrix;

varying mediump vec2 outTexCoord;

void main() {
   gl_Position = matrix * highp vec4(vertex, 1.0);
   outTexCoord = texCoord;
}

E questo è il corrispondente fragment shader

// fragment shader
uniform highp sampler2D texture;

varying mediump vec2 outTexCoord;

void main() {
    mediump vec2 flippedVertexCoord = vec2(outTexCoord.x, 1.0 - outTexCoord.y);
    gl_FragColor = texture2D(texture, flippedVertexCoord);
}

Come abbiamo già visto in precedenza, aggiungiamo un attributo privato di tipo GLuint per individuare la texture che caricheremo in memoria

class GLWidget: public QOpenGLWidget {
...
private:
...
    GLuint m_texture;
...
};

Diversamente da quanto visto nei precedenti articoli, per ottimizzare le risorse di sistema introduciamo un ulteriore livello di complessità: non creiamo un VBO a parte per le coordinate di mappatura della texture, ma ne utilizziamo uno unico sia per le coordinate nello spazio che per quelle di mappatura. Dopo aver definito una struct VertexData del tipo

struct VertexData {
    QVector3D coord;
    QVector2D texCoord;
};

nel membro initializeGL() scriviamo

void GLWidget::initializeGL(){
    ...
    // vettore contenete i dati associati ai vertici
  double l = 1.0 / 2.0;
  VertexData vertexData[] = {
      // faccia 0
      {QVector3D(-l, -l,  l), QVector2D(0.0f, 0.0f)},
      {QVector3D( l, -l,  l), QVector2D(1.0f, 0.0f)},
      {QVector3D(-l,  l,  l), QVector2D(0.0f, 1.0f)},
      {QVector3D( l,  l,  l), QVector2D(1.0f, 1.0f)},
      // faccia 1
      {QVector3D( l, -l,  l), QVector2D(0.0f, 0.0f)},
      {QVector3D( l, -l, -l), QVector2D(1.0f, 0.0f)},
      {QVector3D( l,  l,  l), QVector2D(0.0f, 1.0f)},
      {QVector3D( l,  l, -l), QVector2D(1.0f, 1.0f)},
      // faccia 2
      {QVector3D( l, -l, -l), QVector2D(0.0f, 0.0f)},
      {QVector3D(-l, -l, -l), QVector2D(1.0f, 0.0f)},
      {QVector3D( l,  l, -l), QVector2D(0.0f, 1.0f)},
      {QVector3D(-l,  l, -l), QVector2D(1.0f, 1.0f)},
      // faccia 3
      {QVector3D(-l, -l, -l), QVector2D(0.0f, 0.0f)},
      {QVector3D(-l, -l,  l), QVector2D(1.0f, 0.0f)},
      {QVector3D(-l,  l, -l), QVector2D(0.0f, 1.0f)},
      {QVector3D(-l,  l,  l), QVector2D(1.0f, 1.0f)},
      // faccia 4
      {QVector3D(-l, -l, -l), QVector2D(0.0f, 0.0f)},
      {QVector3D( l, -l, -l), QVector2D(1.0f, 0.0f)},
      {QVector3D(-l, -l,  l), QVector2D(0.0f, 1.0f)},
      {QVector3D( l, -l,  l), QVector2D(1.0f, 1.0f)},
      // faccia 5
      {QVector3D(-l,  l,  l), QVector2D(0.0f, 0.0f)},
      {QVector3D( l,  l,  l), QVector2D(1.0f, 0.0f)},
      {QVector3D(-l,  l, -l), QVector2D(0.0f, 1.0f)},
      {QVector3D( l,  l, -l), QVector2D(1.0f, 1.0f)}
  };

Definite le informazioni da caricare, passiamo a creare il VBO

  // 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 creare la texture diamo invece i comandi

    // generiamo il Texture Object
    glGenTextures( 1, &m_texture);
    // bind del TO
    glBindTexture(GL_TEXTURE_2D, m_texture);
    
    
    // modifichiamo l'immagine per adattarle
    QImage img = drawImage("8").convertToFormat(QImage::Format_RGBA8888);
    // carichiamo i bit che compongono l'immagine
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.width(), img.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, img.bits() );

Per la gestione della texture nello shader possiamo definire alcune opzioni

    // Ripete l'immagine se le coordinate di mappatura sono maggiori di 1
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // quando l'area è più grande della texture, interpola linearmente l'originale
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // quando l'area è più piccola della texture, interpola linearmente l'originale
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

A questo punto dobbiamo definire l'IBO; rimandiamo a quanto già visto nel precedente articolo.

Ora disegniamo il cubo nel metodo paintGL(). Per come abbiamo caricato i dati associati ai vertici, dopo aver effettuato il binding del VBO, dovremo fare attenzione ai parametri della funzione glAttribPointer( … ), definendo stride (distanza tra l'inizio dei dati di un vertice e l'inizio dei dati del successivo) e offset (posizione dei dati che stiamo caricando nella struct VertexData)

  // Attiviamo il VBO dei vertici
  glBindBuffer(GL_ARRAY_BUFFER, m_vertexDataVBO); // attiva il VBO
  glEnableVertexAttribArray( m_vertexPos );
  GLsizei stride = sizeof(VertexData);
  glVertexAttribPointer( m_vertexPos,
                         3,                                 // numero di elementi per vertice: 3 (x,y,z)
                         GL_FLOAT,
                         GL_FALSE,
                         stride,                            // distanza inizio-inizio
                         (const void *) 0 );                // posizione dati in VertexData
  
  glEnableVertexAttribArray( m_texCoordPos );
  glVertexAttribPointer( m_texCoordPos,
                         2,                                  // numero di elementi per vertice: 2 (x,y)
                         GL_FLOAT,
                         GL_FALSE,
                         stride,                             // distanza inizio-inizio
                         (const void *) sizeof(QVector3D) ); // posizione dati in VertexData

Per attivare la texture sull'unità 0 daremo invece i comandi

  // attiviamo l'unita' 0
  glActiveTexture(GL_TEXTURE0);
  // binding del TO
  glBindTexture(GL_TEXTURE_2D, m_texture);
  // impostiamo il valore nell'unita' 0
  m_program->setUniformValue( m_texturePos, 0);

Al momento di eliminare la classe ricordiamoci di liberare la memoria occupata dalla texture con la funzione glDeleteTextures

GLWidget::~GLWidget(){
    ...
    glDeleteTextures(1, &m_texture);
    ...
}

Ed ecco il risultato finale

Alle facce del cubo è stata applicata un'immagine

Il codice sorgente completo è disponibile al link ingegnerialibera.altervista.org/blog-file/opengltut04-01.zip.

Le texture con QOpenGLTexture

Usando la classe QOpenGLTexture, la creazione della texture per un'immagine singola è decisamente agevole. Prima di tutto aggiungiamo un attributo privato di tipo QOpenGLTexture alla classe

class GLWidget: public QOpenGLWidget {
...
private:
    QOpenGLTexture * m_texture;
...
};

Nel metodo initializeGL() creiamo la texture e carichiamo l'immagine con una sola linea di codice

  m_texture = new QOpenGLTexture( drawImage("8") );

E' possibile impostare le stesse variabili di stato già viste in precedenza con i comandi

    // quando l'area è più piccola della texture, interpola linearmente l'originale
    m_texture->setMinificationFilter(QOpenGLTexture::Nearest);
    // quando l'area è più grande della texture, interpola linearmente l'originale
    m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
    // Ripete l'immagine se le coordinate di mappatura sono maggiori di 1
    m_texture->setWrapMode(QOpenGLTexture::Repeat);

Disegniamo l'immagine nel metodo paintGL() con i comandi

  // *** texture ***
  // attiviamo l'unita' 0
  glActiveTexture(GL_TEXTURE0);
  // attiviamo la texture
  m_texture->bind();
  // impostiamo il valore nell'unita' 0
  m_program->setUniformValue( m_texturePos, 0);

Il risultato finale è identico a quello del precedente paragrafo.

Al link ingegnerialibera.altervista.org/blog-file/opengltut04-02.zip trovate il codice completo.

Articoli correlati

Comments


tutorial_qt/qt_opengl_04_texture_2d.txt · Ultima modifica: 2015/02/10 09:16 da mickele

Facebook Twitter Google+ Digg Reddit LinkedIn StumbleUpon Email