4.QOpenGLWidget-對三角形進行紋理貼圖、紋理疊加

NQian發表於2020-10-19

在上章3.QOpenGLWidget-通過著色器來渲染漸變三角形,我們為每個頂點新增顏色來增加圖形的細節,從而建立出有趣的影像。但是,如果想讓圖形看起來更真實,我們就必須有足夠多的頂點,從而指定足夠多的顏色。這將會產生很多額外開銷。

所以使用紋理(Texture)。紋理是一個2D圖片(甚至也有1D和3D的紋理),你可以想象紋理是一張繪有磚塊的紙,無縫摺疊貼合到你的3D的房子上,這樣你的房子看起來就像有磚牆外表了.
下面你會看到之前教程的那個三角形貼上了一張磚牆圖片:
  • 除了影像以外,紋理也可以被用來儲存大量的資料,這些資料可以傳送到著色器上,比如傳輸大量RGB資料顯示一幅畫面

  為了能夠把紋理對映(Map)到三角形上,我們需要指定三角形的每個頂點各自對應紋理的哪個部分。這樣每個頂點就會關聯著一個紋理座標(Texture Coordinate),用來標明該從紋理影像的哪個部分取樣(譯註:採集片段顏色)。之後在圖形的其它片段上進行片段插值(Fragment Interpolation)。

紋理座標在x和y軸上,範圍為0到1之間(注意我們使用的是2D紋理影像)。使用紋理座標獲取紋理顏色叫做取樣(Sampling)。紋理座標起始於(0, 0),也就是紋理圖片的左下角,終始於(1, 1),即紋理圖片的右上角。

紋理座標看起來就像這樣:
float texCoords[] = { 
 0.0f, 0.0f, // 左下角 
 1.0f, 0.0f, // 右下角
 0.5f, 1.0f // 上中 
};
對紋理取樣的解釋非常寬鬆,它可以採用幾種不同的插值方式。所以我們需要自己告訴OpenGL該怎樣對紋理取樣。
 
1.QOpenGLTexture紋理物件介紹
 在QT中,通過QOpenGLTexture類封裝了一個OpenGL紋理物件,QOpenGLTexture可以很容易地使用OpenGL紋理和它們提供的無數特性和目標,這取決於你的OpenGL實現的能力。
 
QOpenGLTexture紋理的範圍是從(0, 0)到(1, 1),如果超過範圍後,opengl預設是重複紋理影像,當然也可以通過setWrapMode(CoordinateDirection direction, WrapMode mode)函式來重新設定,setWrapMode函式引數定義如下:
void QOpenGLTexture::setWrapMode(CoordinateDirection direction, WrapMode mode);
//direction:座標方向,紋理的座標系統和xyz座標系統一樣,s對應x,t對應y,r對應z(3D紋理時才設定z)
//mode:紋理模式,Repeat(超出部分重複紋理)MirroredRepeat(超出部分映象重複紋理)ClampToEdge(超出部分顯示紋理臨近的邊緣顏色值)、

 QOpenGLTexture放大縮小的過濾方式是通過 setMinMagFilters(Filter minificationFilter, Filter magnificationFilter)函式實現,比如:

 m_texture->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Nearest);
//引數1:設定縮小方式 ,引數2:設定放大方式
//設定縮小和放大的方式,縮小圖片採用LinearMipMapNearest線性過濾,並使用多級漸遠紋理鄰近過濾,放大圖片採用:Nearest鄰近過濾

 

具體可以設定的引數有:
  • Nearest :  鄰近過濾,速度快,可能有鋸齒,等同於opengl中的GL_NEAREST
  • Linear :  線性過濾,將最接近的2*2個顏色,計算出一個插值,速度慢,畫面好,等同於opengl中的GL_LINEAR
  • //下面4個多級漸遠紋理引數只能用在縮小方式引數1上面 
  • NearestMipMapNearest :  使用最鄰近的多級漸遠紋理來匹配畫素大小,並使用鄰近插值進行紋理取樣,等同於GL_NEAREST_MIPMAP_NEAREST
  • NearestMipMapLinear :  在兩個最匹配畫素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行取樣,等同於GL_NEAREST_MIPMAP_LINEAR
  • LinearMipMapNearest :  使用最鄰近的多級漸遠紋理級別,並使用線性插值進行取樣,等同於GL_LINEAR_MIPMAP_NEAREST
  • LinearMipMapLinear :  在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行取樣,GL_LINEAR_MIPMAP_LINEAR
縮小之多級漸遠紋理
當紋理大於渲染螢幕時,使用紋理縮小演算法(minifying)來渲染螢幕,就可以設定NearestMipMapNearest 等4個引數,比如在一個場景中,由於遠處的物體只佔有很少的片段(近大遠小,非常遠的物體看起來就像一個點),OpenGL使用高解析度紋理為這些片段後去正確的顏色值是很困難的,它需要對一個跨過紋理很大部分的片段只拾取一個顏色,比如一個物體太遠,只佔有1個畫素值,而該物體對應的紋理是個高解析度圖片,那麼到底選用圖片中哪個畫素值?
OpenGL使用一種叫做多級漸遠紋理(Mipmap)的概念來解決這個問題,它簡單來說就是將一個影像生成一系列的紋理影像,後一個紋理影像是前一個的二分之一,直到生成只有1個畫素大小的圖片為止,如下圖所示:
然後繪製物體時,把攝像機到物體的距離與闕值作比較,在不同的距離空間內選用不同的紋理影像。由於距離遠,解析度不高也不會被使用者注意到。
所以多級漸遠紋理只應用於紋理被縮小的情況下。
 
2.原始碼實現
具體程式碼如下所示:
#include "myglwidget.h"
#include <QtDebug>

//GLSL3.0版本後,廢棄了attribute關鍵字(以及varying關鍵字),屬性變數統一用in/out作為前置關鍵字
#define GL_VERSION  "#version 330 core\n"

#define GLCHA(x)    #@x         //加單引號
#define GLSTR(x)     #x            //加雙引號
#define GET_GLSTR(x) GL_VERSION#x

const char *vsrc = GET_GLSTR(

    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aColor;
    layout (location = 2) in vec2 aTexCoord;

    out vec3 ourColor;
    out vec2 TexCoord;

    void main()
    {
        gl_Position = vec4(aPos, 1.0);
        ourColor = aColor;
        TexCoord = aTexCoord;
    }

);

const char *fsrc =GET_GLSTR(

    out vec4 FragColor;

    in vec3 ourColor;
    in vec2 TexCoord;

    uniform sampler2D ourTexture;

    void main()
    {
        FragColor = texture(ourTexture, TexCoord);
    }
);


myGlWidget::myGlWidget(QWidget *parent):QOpenGLWidget(parent)
{

}


void myGlWidget::paintGL()
{
   // 繪製
   // glViewport(0, 0, width(), height());
   glClear(GL_COLOR_BUFFER_BIT);


   // 渲染Shader
   vao.bind();       //繫結啟用vao
   m_texture->bind();
   glDrawArrays(GL_TRIANGLES, 0, 3);    //繪製3個定點,樣式為三角形

   m_texture->release();
   vao.release();       //解綁
}

void myGlWidget::initializeGL()
{

   // 為當前環境初始化OpenGL函式
   initializeOpenGLFunctions();

   glClearColor(1.0f, 1.0f, 1.0f, 1.0f);    //設定背景色為白色


   //初始化紋理物件
   m_texture  = new QOpenGLTexture(QOpenGLTexture::Target2D);
   m_texture->setData(QImage(":wall1"));
   m_texture->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Nearest);
   //設定縮小和放大的方式,縮小圖片採用LinearMipMapLinear線性過濾,並使用多級漸遠紋理鄰近過濾,放大圖片採用:Nearest鄰近過濾
m_texture->setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat); m_texture->setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat); //建立著色器程式 program = new QOpenGLShaderProgram; program->addShaderFromSourceCode(QOpenGLShader::Vertex,vsrc); program->addShaderFromSourceCode(QOpenGLShader::Fragment,fsrc); program->link(); program->bind();//啟用Program物件 //初始化VBO,將頂點資料儲存到buffer中,等待VAO啟用後才能釋放 float vertices[] = { // 位置 // 顏色 //紋理座標 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 2.0f, 0.0f,// 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, // 左下 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 2.0f // 頂部 }; vbo.create(); vbo.bind(); //繫結到當前的OpenGL上下文, vbo.allocate(vertices, sizeof(vertices)); vbo.setUsagePattern(QOpenGLBuffer::StaticDraw); //設定為一次修改,多次使用 //初始化VAO,設定頂點資料狀態(頂點,法線,紋理座標等) vao.create(); vao.bind(); // void setAttributeBuffer(int location, GLenum type, int offset, int tupleSize, int stride = 0); program->setAttributeBuffer(0, GL_FLOAT, 0, 3, 8 * sizeof(float)); //設定aPos頂點屬性 program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float), 3, 8 * sizeof(float)); //設定aColor頂點顏色 program->setAttributeBuffer(2, GL_FLOAT, 6 * sizeof(float), 2, 8 * sizeof(float)); //設定紋理座標 //offset:第一個資料的偏移量 //tupleSize:一個資料有多少個元素,比如位置為xyz,顏色為rgb,所以是3 //stride:步長,下個資料距離當前資料的之間距離,比如右下位置和左下位置之間間隔了:3個xyz值+3個rgb值,所以填入 6 * sizeof(float) program->enableAttributeArray(0); //使能aPos頂點屬性 program->enableAttributeArray(1); //使能aColor頂點顏色 program->enableAttributeArray(2); //使能紋理座標 //解綁所有物件 vao.release(); vbo.release(); } void myGlWidget::resizeEvent(QResizeEvent *e) { }
 由於我們設定的三角形紋理座標是
  //紋理座標
2.0f, 0.0f,//   右下
0.0f, 0.0f,  // 左下
1.0f, 2.0f  //  頂部

 所以是超過了範圍(0, 0)到(1, 1),假如我們繪製mode改為QOpenGLTexture::ClampToEdge,就可以看出其實三角形是大於圖片的,修改程式碼如下:

 m_texture->setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::ClampToEdge);
 m_texture->setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::ClampToEdge);

顯示介面如下所示:

 在程式碼中,我們還儲存了上章著色器顏色渲染相關程式碼,所以我們可以把得到的紋理顏色與頂點顏色混合,來獲得更有趣的混合效果,修改fragment原始碼:

FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);

編譯並執行,如下圖所示:

3.紋理疊加

在上個原始碼實現中,我們在fragment原始碼中定義了一個uniform型別的ourTexture變數,但是我們卻沒有給它賦值就已經實現了紋理,這是因為如果有一個紋理的話,預設是啟用的.
假如有多個紋理的話,我們就需要設定其紋理位置值(也稱為一個紋理單元(Texture Unit))。然後再將對應的QOpenGLTexture繫結上.
設定如下所示:

program->setUniformValue("texture1", 0);
m_texture->bind();    //將m_texture繫結在"texture1"上
program->setUniformValue("texture2", 1);
m_texture2->bind(1);//將m_texture2繫結在"texture1"上
....

修改fragment原始碼:

#version 330 core
...

uniform sampler2D texture1;
uniform sampler2D texture2;

void main()
{
FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.7);
}

mix函式作用是將前兩個紋理引數進行融合,根據第三個引數值來進行線性插值,如果第三個值是0.0,它會返回第一個輸入;如果是1.0,會返回第二個輸入值。0.7表示返回30%的第一個輸入顏色和70%的第二個輸入顏色。

然後再加入一個我的大學圖片:

最終和磚牆疊加後的效果如下所示:

 具體原始碼如下所示:

#include "myglwidget.h"
#include <QtDebug>


//GLSL3.0版本後,廢棄了attribute關鍵字(以及varying關鍵字),屬性變數統一用in/out作為前置關鍵字
#define GL_VERSION  "#version 330 core\n"

#define GLCHA(x)    #@x         //加單引號
#define GLSTR(x)     #x            //加雙引號
#define GET_GLSTR(x) GL_VERSION#x


const char *vsrc = GET_GLSTR(

    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aColor;
    layout (location = 2) in vec2 aTexCoord;

    out vec3 ourColor;
    out vec2 TexCoord;

    void main()
    {
        gl_Position = vec4(aPos, 1.0);
        ourColor = aColor;
        TexCoord = aTexCoord;
    }

);

const char *fsrc =GET_GLSTR(

    out vec4 FragColor;

    in vec3 ourColor;
    in vec2 TexCoord;


    uniform sampler2D texture1;
    uniform sampler2D texture2;

    void main()
    {
        FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.7);
    }

);


myGlWidget::myGlWidget(QWidget *parent):QOpenGLWidget(parent)
{

}


void myGlWidget::paintGL()
{
   // 繪製
    int w = width();
    int h = height();
    int side = qMin(w, h);
    glViewport((w - side) / 2, (h - side) / 2, side, side);
    glClear(GL_COLOR_BUFFER_BIT);


   // 渲染Shader
   vao.bind();       //繫結啟用vao
   m_texture->bind();

   program->setUniformValue("texture1", 0);
   m_texture->bind();
   program->setUniformValue("texture2", 1);
   m_texture2->bind(1);

   glDrawArrays(GL_TRIANGLE_FAN, 0, 4);    //繪製3個定點,樣式為三角形

   m_texture->release();
   m_texture2->release();
   vao.release();       //解綁
}

void myGlWidget::initializeGL()
{

   // 為當前環境初始化OpenGL函式
   initializeOpenGLFunctions();

   glClearColor(1.0f, 1.0f, 1.0f, 1.0f);    //設定背景色為白色


   //初始化紋理物件
   m_texture  = new QOpenGLTexture(QOpenGLTexture::Target2D);
   m_texture->setData(QImage(":wall")); //載入磚塊圖片
   m_texture->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Nearest);
   //設定縮小和放大的方式,縮小圖片採用LinearMipMapLinear線性過濾,並使用多級漸遠紋理鄰近過濾,放大圖片採用:Nearest鄰近過濾

   m_texture->setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
   m_texture->setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);


   //初始化紋理物件
   m_texture2  = new QOpenGLTexture(QOpenGLTexture::Target2D);
   m_texture2->setData(QImage(":my").mirrored()); //返回圖片的映象,設定為Y軸反向,因為在opengl的Y座標中,0.0對應的是圖片底部


   m_texture2->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Nearest);
   //設定縮小和放大的方式,縮小圖片採用LinearMipMapLinear線性過濾,並使用多級漸遠紋理鄰近過濾,放大圖片採用:Nearest鄰近過濾

   m_texture2->setWrapMode(QOpenGLTexture::DirectionS,QOpenGLTexture::Repeat);
   m_texture2->setWrapMode(QOpenGLTexture::DirectionT,QOpenGLTexture::Repeat);






   //建立著色器程式

   program = new QOpenGLShaderProgram;
   program->addShaderFromSourceCode(QOpenGLShader::Vertex,vsrc);
   program->addShaderFromSourceCode(QOpenGLShader::Fragment,fsrc);

   program->link();
   program->bind();//啟用Program物件


   //初始化VBO,將頂點資料儲存到buffer中,等待VAO啟用後才能釋放
   float vertices[] = {
   //     ---- 位置 ----       ---- 顏色 ----     - 紋理座標 -
        0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
        0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
       -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
       -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
   };

   vbo.create();
   vbo.bind();              //繫結到當前的OpenGL上下文,
   vbo.allocate(vertices, sizeof(vertices));
   vbo.setUsagePattern(QOpenGLBuffer::StaticDraw);  //設定為一次修改,多次使用


   //初始化VAO,設定頂點資料狀態(頂點,法線,紋理座標等)
   vao.create();
   vao.bind();

  // void setAttributeBuffer(int location, GLenum type, int offset, int tupleSize, int stride = 0);
   program->setAttributeBuffer(0, GL_FLOAT, 0,                  3, 8 * sizeof(float));   //設定aPos頂點屬性
   program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(float),  3, 8 * sizeof(float));   //設定aColor頂點顏色
   program->setAttributeBuffer(2, GL_FLOAT, 6 * sizeof(float),  2, 8 * sizeof(float));   //設定aColor頂點顏色


   //offset:第一個資料的偏移量
   //tupleSize:一個資料有多少個元素,比如位置為xyz,顏色為rgb,所以是3
   //stride:步長,下個資料距離當前資料的之間距離,比如右下位置和左下位置之間間隔了:3個xyz值+3個rgb值,所以填入 6 * sizeof(float)


   program->enableAttributeArray(0); //使能aPos頂點屬性
   program->enableAttributeArray(1); //使能aColor頂點顏色
   program->enableAttributeArray(2); //使能aColor頂點顏色


   //解綁所有物件
   vao.release();
   vbo.release();


}
void myGlWidget::resizeEvent(QResizeEvent *e)
{


}

 

 

 

相關文章