OpenGL 紋理基礎與索引

前言

OpenGL的紋理實際上運用十分廣泛,是OpenGL中的重點。如果你有看過Android底層的繪制原理,能夠發(fā)現實際上,一般的ui界面,Android把會把像素點當作紋理數據繪制在屏幕上。

因此還是有必要稍微學習一下OpenGL的紋理。本文講述的是OpenGL的紋理基礎。

如果在本文遇到什么問題,請在http://m.itdecent.cn/p/9c58cd895fa5這里聯系本人

正文

紋理介紹

從前面幾節(jié)OpenGL我們可以清楚,OpenGL可以結合頂點數組對象,頂點緩存對象,使用OpenGL命令,生成各種圖形。但是,有沒有想過,如果需要圖像看起來十分真實,就需要足夠多的頂點,指定足夠多的顏色。這樣會產生許多額外的開銷。

因此,誕生了紋理。紋理是一個2D圖片(甚至也有1D,3D的紋理),他可以添加物體細節(jié)??梢韵胂?,實際上OpenGL可以建立了一個模型,但是還需要很多圖像的細節(jié),為了添加細節(jié),把這個帶著圖像的紙貼在模型上。

除了圖像以外,紋理也可以用來存儲大量的數據,可以把數據發(fā)送到著色器上。

比如說,我們可以把一個磚塊的紋理貼在上兩章的三角形上。


image.png

為了把紋理正確的映射到模型上,我們必須要知道紋理映射的坐標,指定三角形每個對應上紋理的哪個部分。

紋理坐標系

下面是上圖的紋理坐標:


紋理坐標系.png

能看到這個紋理坐標和之前的OpenGL的頂點坐標系不太一樣,這邊的坐標系是以左下角為原點。

記得,這坐標系和頂點坐標系思路上不太一樣。頂點坐標系相當于在一個三維空間中,找個位置構建一個2d/3d的模型。

但是紋理坐標系,一旦紋理加載了圖片之后,想要獲取到完整的圖片,就要x軸(0,1)和y軸(0,1)整個的區(qū)域才能把這個圖片讀取出來。

舉個例子:
如上圖,我們希望添的圖片左下角能對應三角形的左下角,右下角對應右下角,頂部對應三角形的中心。
那么我們可以創(chuàng)建一個如下的紋理坐標系:

float texCoords[] = {
    0.0f, 0.0f, // 左下角
    1.0f, 0.0f, // 右下角
    0.5f, 1.0f // 上中
};

但是,注意到沒有,這是個正方形的區(qū)域,如果遇到長方形的圖片怎么辦。OpenGL其實有自己的策略。這個稍后會繼續(xù)談到。

紋理環(huán)繞方式

注意到紋理坐標系的問題,這里就有OpenGL提供的幾種解決這個問題的思路。

紋理坐標一般在(0,0),(1,1)浮動,如果我們紋理坐標設置到了坐標之外,那么會發(fā)生什么?OpenGL會默認重復這個圖像。

但是實際上OpenGL除了這些之外還提供了其他的方式:

環(huán)繞方式 描述
GL_REPEAT 對紋理的默認行為。重復紋理圖像。
GL_MIRRORED_REPEAT 和GL_REPEAT一樣,但每次重復圖片是鏡像放置的。
GL_CLAMP_TO_EDGE 紋理坐標會被約束在0到1之間,超出的部分會重復紋理坐標的邊緣,產生一種邊緣被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的坐標為用戶指定的邊緣顏色。
image.png

那么反過來說,如果小于當前的坐標呢?
OpenGL將會截取一部分的圖像貼在整個模型上面。之后會在實戰(zhàn)演練中見識到現象。

OpenGL在設置紋理的時候,可以設置紋理單元的繪制參數,調用如下方法:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);

這個glTexParameteri,是設置紋理之前的做的參數設置,相當于構建構建一個紋理繪制的坐標之類的參數。
第一個參數是指:我們要繪制2d紋理,第二個參數是指:我們要設定的紋理軸中的行為,第三個參數是指:當這個紋理軸上的紋理超過了紋理坐標的范圍,的表現形式。

在紋理中存在著s,t,r三種坐標軸:


image.png

一般繪制2D,就在s和t軸上繪制。s我們可以類比x軸,t可以類比y軸。

如果我們選擇了GL_CLAMP_TO_BORDER,如果當前的圖像不是正方形,圖像紋理將會把圖像壓縮到(0,0),(1,1)之間,剩下的部分會用黑色填充。我們可以傳遞一個float數組作為邊緣的顏色值。

float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

紋理過濾

紋理過濾實際上是對紋理中圖像像素的處理策略。

紋理坐標不依賴分辨率,它可以是任意的浮點值,所以OpenGL需要知道紋理像素怎么映射到紋理坐標系中。當你有一個很大的物體但是紋理分辨率很低的時候就很重要了。

因此OpenGL提供了很多紋理過濾的方式,其中兩種十分重要,這兩種方式也會在OpenCV中涉及到這塊東西的講解:

  • 1.鄰近過濾(GL_NEAREST)
  • 2.線性過濾(GL_LINEAR)

鄰近過濾

鄰近過濾是OpenGL默認的方式。當設置GL_NEAREST,OpenGL會挑選中心點最接近紋理坐標的像素值。


鄰近過濾.png

線性過濾

線性過濾會基于紋理坐標附近紋理像素值,做一個插值計算,計算出近似周邊的像素值。一個紋理像素中心距離紋理坐標越近,其貢獻越大。這就有點像一個掩碼操作,通過一個核算出一個相關性。

下圖你能看到返回的是一個周邊像素的混合色:


線性過濾.png

通過兩者的比較我們不難發(fā)現,線性過濾的圖像會柔和一點,而鄰近過濾的圖像就會增加對比度一點。

實際上這個操作和OpenCV的filter過濾器有這同工異曲之妙。當我們放大像素就有如下的效果:


image.png

能夠發(fā)現GL_NEAREST更加偏向像素風格,每個顆粒會很大。而GL_LINEAR會平滑一點。

OpenCV對應的api

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

多級漸遠紋理

假設一下,當一個空間有上千物體,就需要上千紋理。有些紋理很遠,其紋理難以和近的紋理難以有一個分辨率。由于遠處的物體可能只產生很少的片段,OpenGL從高分辨率紋理中為這些片段獲取正確的顏色值就很困難,因為它需要對一個跨過紋理很大部分的片段只拾取一個紋理顏色。在小物體上這會產生不真實的感覺,更不用說對它們使用高分辨率紋理浪費內存的問題了。

為了解決這個問題,OpenGL使用一種叫做多級漸遠紋理(Mipmap)的概念來解決。

簡單來說,他是一系列的紋理圖像,后者是前者的1/2。其原理很簡單,當距離觀察者超過一定的閾值,OpenGL會使用不同的多級漸遠紋理來處理,選擇最適合當前距離的紋理。

由于距離遠,解析度不高也不會被用戶注意到。同時,多級漸遠紋理另一加分之處是它的性能非常好。讓我們看一下多級漸遠紋理是什么樣子的:


多級漸遠紋理

手工創(chuàng)建多個紋理比較復雜,因此OpenGL提供了glGenerateMipmaps方法設置一系列多級漸遠紋理.

在渲染過程中,切換多級漸遠紋理級別(Level)時,OpenGL在兩個不同級別的多級漸遠紋理層之間會產生不真實的生硬邊界,就像普通的紋理過濾一樣,切換多級漸遠紋理級別時你也可以在兩個不同多級漸遠紋理級別之間使用NEAREST和LINEAR過濾。

切換紋理方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最近鄰的多級漸遠紋理來匹配像素大小,并使用近鄰插值進行紋理過濾
GL_LINEAR_MIPMAP_NEAREST 使用最近鄰的多級漸遠紋理來匹配像素大小,并且使用線性插值起進行紋理過濾。
GL_NEAREST_MIPMAP_LINEAR 在兩個最匹配像素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行采樣
GL_LINEAR_MIPMAP_LINEAR 在兩個鄰近的多級漸遠紋理之間使用線性插值,并使用線性插值進行采樣

OpenGL提供的api:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

GL_TEXTURE_MIN_FILTER代表縮小操作時候的紋理操作,GL_TEXTURE_MAG_FILTER代表漸遠紋理級別切換時候操作。

注意一個常見的錯誤,放大不會使用多級漸遠紋理。使用了會報錯。

紋理常規(guī)開發(fā)流程

紋理的常見實際上和VAO,VBO十分相似。都是走一個套路,通過Gen函數創(chuàng)建對象,bind函數綁定對象,最后再傳輸數據。

首先通過下面這個函數,常見一個紋理對象:

GLuint texture;
glGenTextures(1,&texture);

綁定:

glBindTexture(GL_TEXTURE_2D,texture);

設置紋理坐標的參數:

 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
    
    //    float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
    //    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
    //設置紋理過濾
    //縮小時候
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

最后再傳輸數據:

 glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,data);
 glGenerateMipmap(GL_TEXTURE_2D);

針對glTexImage2D進行講解:

  • 1.第一個參數:指定了紋理目標。設置為GL_TEXTURE_2D意味著會生成與當前綁定的紋理對象在同一個目標上的紋理(任何綁定到GL_TEXTURE_1D和GL_TEXTURE_3D的紋理不會受到影響)。

  • 2.第二個參數:第二個參數為紋理指定多級漸遠紋理的級別,如果你希望單獨手動設置每個多級漸遠紋理的級別的話。這里我們填0,也就是基本級別。

  • 3.第三個參數告訴OpenGL我們希望把紋理儲存為何種格式。我們的圖像只有RGB值,因此我們也把紋理儲存為RGB值(記住要符合圖片通道數,如果是4通道是RGBA)。

  • 4.第四個和第五個參數設置最終的紋理的寬度和高度。我們之前加載圖像的時候儲存了它們,所以我們使用對應的變量。

  • 5.下個參數應該總是被設為0(歷史遺留的問題)

  • 6.第七第八個參數定義了源圖的格式和數據類型。我們使用RGB值加載這個圖像,并把它們儲存為char(byte)數組,我們將會傳入對應值

  • 7.最后一個參數是真正的圖像數據

當調用glTexImage2D時,當前綁定的紋理對象就會被附加上紋理圖像。然而,目前只有基本級別(Base-level)的紋理圖像被加載了,如果要使用多級漸遠紋理,我們必須手動設置所有不同的圖像(不斷遞增第二個參數)?;蛘?,直接在生成紋理之后調用glGenerateMipmap。這會為當前綁定的紋理自動生成所有需要的多級漸遠紋理。

索引繪制

在這里稍微聊聊索引。實際上,在我的第一篇OpenGL學習中,發(fā)現當我們繪制一個三角形會指定3個坐標,一個矩形就需要2個三角形,6個頂點組成三角形組合起來。

但是實際上這種操作會浪費內存,而且有很多冗余的頂點數據。為了解決這種情況OpenGL引入了索引的概念。

舉一個例子:

不實用索引去繪制矩形,需要6個頂點,其中有2個頂點是冗余的。

float vertices[] = {
    // 第一個三角形
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, 0.5f, 0.0f,  // 左上角
    // 第二個三角形
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

但是如果我們使用索引,只需要創(chuàng)建如下,2個數組,一個是頂點數組,一個是索引數組:

float vertices[] = {
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

unsigned int indices[] = { // 注意索引從0開始! 
    0, 1, 3, // 第一個三角形
    1, 2, 3  // 第二個三角形
};

這樣我們就能通過索引找到這些頂點,復用頂點并且繪制2個三角形。

索引的用法

同樣的配方:
生成一個Gen對象:

GLuint EBO;
glGenBuffers(1, &EBO);

綁定對象,并且輸入頂點信息:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

但是繪制頂點的方法卻是出現了變化:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glDrawElements 第一個參數指定了我們繪制的模式,這個和glDrawArrays的一樣。第二個參數是我們打算繪制頂點的個數,這里填6,也就是說我們一共需要繪制6個頂點。第三個參數是索引的類型,這里是GL_UNSIGNED_INT。最后一個參數里我們可以指定EBO中的偏移量(或者傳遞一個索引數組,但是這是當你不在使用索引緩沖對象的時候),但是我們會在這里填寫0。

我們會使用glDrawElements,繪制6個頂點,其順序是依照索引來繪制。這樣就能繪制出一個矩形。

在這里解釋一下,此時索引數組有兩個:

    1. 0,1,3
  • 2.1,2,3
    這些索引會根據之前編寫好的glVertexAttribPointer,去尋找頂點數組對象對應緩存對象的數組,找到對應的頂點信息。

因此畫出來的如下圖:


image.png

這里稍微解釋一下索引繪制的存儲原理。glDrawElements會通過GL_ELEMENT_ARRAY_BUFFER目標的EBO中獲取索引。這意味著我們每一次渲染索引都需要綁定一次索引。不過頂點數組對象,同樣可以保存索引的綁定狀態(tài),就和VBO一樣。

VAO綁定時正在綁定的索引緩沖對象會被保存為VAO的元素緩沖對象。綁定VAO的同時也會自動綁定EBO。如下圖:


索引綁定.png

當目標是GL_ELEMENT_ARRAY_BUFFER的時候,VAO會儲存glBindBuffer的函數調用。這也意味著它也會儲存解綁調用,所以確保你沒有在解綁VAO之前解綁索引數組緩沖,否則它就沒有這個EBO配置了。

因此當我們調用glDelete函數的時候,記得最后才銷毀VAO。

實戰(zhàn)演練

繪制箱子

當我們熟知了紋理以及索引繪制的基礎知識之后,開始實戰(zhàn)環(huán)節(jié)。我們嘗試著把一個箱子繪制到一個矩形中。

接下來的源碼,為了避免和前面的文章產生重復性,在閱讀了Android 的源碼之后,抽象了一個渲染引擎。這里面還是很簡單,只是做一個OpenGL渲染環(huán)境的初始化,讓我們集中本文所學習到的知識。

首先初始化環(huán)境。

 RenderEngine *engine = new RenderEngine();
    GLuint VAO;
    GLuint VBO;
    GLuint EBO;
    GLuint texture[] = {1};
    const char* vPath = "/Users/yjy/Desktop/iOS workspcae/first_opengl/Texture/vertex.glsl";
    const char* fPath = "/Users/yjy/Desktop/iOS workspcae/first_opengl/Texture/fragment.glsl";
    Shader *shader = new Shader(vPath,fPath);
    shader->compile();

接下來,靈活運用上一篇文章的知識,創(chuàng)建VAO,VBO,EBO(索引)。

//要畫矩形,因此要兩個三角形
void flushRetriangle(GLuint& VAO,GLuint& VBO,GLuint& EBO){
    float vertices[] = {
        //位置
        // 右上角          //顏色              //紋理
        0.5f, 0.5f, 0.0f,   1.0f,0.0f,0.0f,  2.0f,2.0f,
        // 右下角
        0.5f, -0.5f, 0.0f,  0.0f,1.0f,0.0f,  2.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,2.0f
    };
    
    unsigned int indices[] = { // 注意索引從0開始!
        0, 1, 3, // 第一個三角形
        1, 2, 3  // 第二個三角形
    };
    
    //分配VAO
    glGenVertexArrays(1,&VAO);
    //分配VBO
    glGenBuffers(1,&VBO);
    //分配EBO
    glGenBuffers(1,&EBO);
    
    //綁定
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER,VBO);
    
    //設置緩存數據
    glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
    //索引
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
    
    
    //告訴OpenGL怎么讀取數據
    //分成三次來讀取
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,8 * sizeof(float),(void*)0);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,8* sizeof(float),(void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,8* sizeof(float),(void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);
    

    
    //解綁
    glBindBuffer(GL_ARRAY_BUFFER,0);
    //glBindTexture(GL_TEXTURE_2D,0);
    glBindVertexArray(0);
   
}

稍微解釋一下,其核心原理,實際上就是把頂點,顏色,紋理都集中在一個地方處理,并且告訴OpenGL應該怎么讀取整個數據。

因為有三種數據要讀取,所以要告訴OpenGL讀取數據該怎么移動,從哪里讀取。并且設置3中l(wèi)ocation,放在頂點著色器中解析。

  • 1.頂點數據,是float型,每次讀取3個,每讀取完一次就移動8sizeof(float)的大小(作者是4字節(jié)大小8),從第0個位置開始讀取。
  • 2.顏色數據,是float型,每次讀取3個,每讀取完一次就移動8*sizeof(float)的大小,從第3個位置開始讀取.
  • 3.紋理坐標數據,是float型,每次讀取2個,每讀取完一次就移動8*sizeof(float)的大小,從第6個位置開始讀取.

這樣就能正確的讀取到緩存中所有的數據。

image.png

最后再設置好繪制紋理的環(huán)境參數,以及讀取紋理數據。

bool initTexture(GLuint& texture,const char* str,bool isResver){
    //設置紋理信息
    //生成和綁定
    glGenTextures(1,&texture);
    glBindTexture(GL_TEXTURE_2D,texture);
    //綁定當前的紋理對象,設置環(huán)繞,過濾的方式
    //設置S軸和T軸,環(huán)繞方式
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
    
//    float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f };
//    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
    //設置紋理過濾
    //縮小時候
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
    //切換級別
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    
    //加載圖片生成wenli數據
    int width ,height,nrChannels;
    stbi_set_flip_vertically_on_load(isResver);
    GLubyte *data = stbi_load(str, &width, &height, &nrChannels, 0);

    if(data){
        if(nrChannels == 3){
            glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data);
        }else if(nrChannels == 4){
            glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,data);
        }
        
        glGenerateMipmap(GL_TEXTURE_2D);
    }else{
        std::cout << "Failed to load texture" << std::endl;
        return false;
    }
    
    stbi_image_free(data);
    
    return true;
}

這里在讀取圖片數據的時候,我借助了stbi庫去讀取,它就是一個頭文件,直接引入就能使用了。
設置了環(huán)繞類型為GL_REPEAT類型,紋理過濾是GL_LINEAR .
注意了,這里我們需要判斷顏色通道,當在顏色通道在3的時候使用RGB,顏色通道為4的時候,使用RGBA,只有正確的設置了顏色通道才能正確的讀取到圖像數據。

關于顏色通道具體內容,可以看我的OpenCV的教程。能看到為什么Android系統時候的順序是RGBA的順序而不是OpenCV的ABGR的順序,也是因為Android繪制底層使用了OpenGL進行繪制啊。

這里是源碼的下半部分:

    flushRetriangle(VAO, VBO, EBO);

    
    initTexture(texture[0], "/Users/yjy/Desktop/opengl/container.jpg",false);

    
    if(shader&&shader->isCompileSuccess()){
        
        shader->use();
        
        engine->loop(VAO, VBO, texture, 1,shader, [](Shader* shader,GLuint VAO,

            
            //箱子
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D,texture[0]);

            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
        });
    }

最后再來看看頂點著色器

#version 330 core
layout(location = 0)in vec3 aPos;
layout(location = 1)in vec3 aColor;
layout(location = 2)in vec2 aTexCoord;

out vec2 TexCoord;

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

還有片元著色器

#version 330 core
out vec4 FragColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main(){
    FragColor = texture(ourTexture, TexCoord);
}

image.png

這樣就能看到結果圖了。

在箱子上面加一個笑臉,并且添加圖片權重

你一定很奇怪為什么sampler2D采樣器是一個uniform,我們卻不用glUniform給它賦值。

使用glUniform1i,我們可以給紋理采樣器分配一個位置值,這樣的話我們能夠在一個片段著色器中設置多個紋理。一個紋理的位置值通常稱為一個紋理單元(Texture Unit)。一個紋理的默認紋理單元是0,它是默認的激活紋理單元,所以教程前面部分我們沒有分配一個位置值。

紋理單元的主要目的是讓我們在著色器中可以使用多于一個的紋理。通過把紋理單元賦值給采樣器,我們可以一次綁定多個紋理,只要我們首先激活對應的紋理單元。就像glBindTexture一樣,我們可以使用glActiveTexture激活紋理單元,傳入我們需要使用的紋理單元:

glActiveTexture(GL_TEXTURE0); // 在綁定紋理之前先激活紋理單元
glBindTexture(GL_TEXTURE_2D, texture);

激活紋理單元之后,接下來的glBindTexture函數調用會綁定這個紋理到當前激活的紋理單元,紋理單元GL_TEXTURE0默認總是被激活,所以我們在前面的例子里當我們使用glBindTexture的時候,無需激活任何紋理單元。

OpenGL至少保證有16個紋理單元供你使用,也就是說你可以激活從GL_TEXTURE0到GL_TEXTRUE15。它們都是按順序定義的,所以我們也可以通過GL_TEXTURE0 + 8的方式獲得GL_TEXTURE8,這在當我們需要循環(huán)一些紋理單元的時候會很有用。

因此為了讓這個片元著色器可以讀取兩個紋理數據,此時要在片元著色器多建立一個采樣器,并且為其賦值id。并且調用glsl中的mix函數來為兩張圖片添加權重

#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ourTexture2;

void main(){
    //mix = mix(x,y,a) = x * (1-a) + y * a
    //箱子 * 0.8 + 笑臉 * 0.2
    FragColor = mix(texture(ourTexture, TexCoord),texture(ourTexture2, TexCoord),0.2);
}
    initTexture(texture[0], "/Users/yjy/Desktop/opengl/container.jpg",false);
    initTexture(texture[1], "/Users/yjy/Desktop/opengl/awesomeface.png",true);
    
    if(shader&&shader->isCompileSuccess()){
        
        shader->use();
        //設置的是紋理單元
        glUniform1i(glGetUniformLocation(shader->ID,"ourTexture"),0);
        shader->setInt("ourTexture2", 1);
        
        engine->loop(VAO, VBO, texture, 1,shader, [](Shader* shader,GLuint VAO,
                                                     GLuint* texture,GLFWwindow *window){
        
            
            //箱子
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D,texture[0]);
            
            //笑臉
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D,texture[1]);
            glBindVertexArray(VAO);
            glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
        });
    }

能夠發(fā)現,此時采樣次的id實際上和glActiveTexture激活的GL_TEXTUREN是一一對應的。


image.png

改變紋理范圍

當我們嘗試這改變整個紋理范圍,看看效果如何。

float vertices[] = {
        //位置
        // 右上角          //顏色              //紋理
        0.5f, 0.5f, 0.0f,   1.0f,0.0f,0.0f,  2.0f,2.0f,
        // 右下角
        0.5f, -0.5f, 0.0f,  0.0f,1.0f,0.0f,  2.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,2.0f
    };

當超過1的時候:


image.png

此時將會采用環(huán)繞方式,為重復,把這個圖片添加到擴大的地方。

當小于1的時候:

   //位置
        // 右上角          //顏色              //紋理
        0.5f, 0.5f, 0.0f,   1.0f,0.0f,0.0f,  0.5f,0.5f,
        // 右下角
        0.5f, -0.5f, 0.0f,  0.0f,1.0f,0.0f,  0.5f,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,0.5f
    };
image.png

能看到此時就是只有圖片的四分之一,左下角。

總結

當了解了這些差不多,也就對紋理坐標有了初步的認識。下面有一個流程圖,可以稍微闡述紋理坐標系,頂點坐標系的關系。


紋理映射過程.png

可以看見紋理的繪制的過程經歷了兩次不同的坐標系,這個必須記住了。明白了這個,掌握紋理的基礎已經不遠了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容