1. 程式人生 > IOS開發 >OpenGL學習(六)-- 基礎紋理

OpenGL學習(六)-- 基礎紋理

我的 OpenGL 專題學習目錄,希望和大家一起學習交流進步!


一、紋理綜述

物理世界中,視域內的顏色會發生快速的變化。你可以看到很多物體表面都會呈現出豐富的顏色,並且在狹小的面積上產生多彩的變化。要捕捉細節如此豐富的色彩變化是非常辛苦和縝密的工作(你需要有效地辨別每個線性色彩變化區域中的每個三角形)。如果能找到一張圖片,然後把它“粘”到物體表面上,就像貼牆紙一樣,那就簡單多了。這就是 紋理對映(texture mapping)紋理貼圖(簡稱紋理) 是通過拍攝或者繪製的一張圖片,OpenGL 支援一維、二維、三維、立方體對映紋理。以及快取紋理,同時還支援陣列紋理。

二、認識函式

畫素包裝

影象儲存空間 = 影象的高度 * 影象寬度 * 每個畫素的位元組數

1、改變或者恢復畫素的儲存方式

我們可以使用下列函式改變或者恢復畫素的儲存方式:

void glPixelStorei(GLenum pname,GLint param);

void glPixelStoref(GLenum pname,GLfloat param);

這倆函式用途是一樣的,只不過函式名一個是 i 結尾,一個是 f 結尾,區別只是第二個引數的型別,i 的是 GLintf 的是 GLfloat。 舉例來說,如果我們想要改成緊密包裝畫素資料,就這樣呼叫:

    glPixelStorei(GL_UNPACK_ALIGNMENT,1
); 複製程式碼

引數 1:GL_UNPACK_ALIGNMENT,指定 OpenGL 如何從資料快取 區中解包影象資料。

引數 2: 針對 GL_UNPACK_ALIGNMENT 設定的值 類似的,我們可以使用 GL_PACK_ALIGNMENT 來告訴 OpenGL 如何將畫素緩衝區中讀取並放置到一個使用者指定的記憶體緩衝區的資料進行包裝。

2、將顏色快取區的內容作為畫素圖直接讀取

我們無法直接將一個畫素圖繪製到顏色緩衝區中,但可以使用下面的函式將將顏色快取區的內容作為畫素圖直接讀取: void glReadPixels(GLint x,GLint y,GLSizei width,GLSizei height,GLenum format,GLenum type,const void * pixels);

引數說明

  • 引數1:x,矩形左下角的窗⼝座標
  • 引數2:y,矩形左下角的窗⼝座標
  • 引數3:width,矩形的寬,以畫素為單位
  • 引數4:height,矩形的高,以畫素為單位
  • 引數5:formatOpenGL 的畫素格式,參考 表 1-1
  • 引數6:type,解釋引數 *pixels 指向的資料,告訴 OpenGL 使⽤快取區中的什麼資料型別來儲存顏⾊分量量,畫素資料的資料型別,參考表 1-2
  • 引數7:pixels,指向圖形資料的指標

void glReadBuffer(GLenum mode); —> 指定讀取的快取

void glWriteBuffer(GLenum mode); —> 指定寫⼊的快取

表 1-1 OpenGL 畫素格式

常量 描述
GL_RGB 描述紅、綠、藍順序排列的顏色
GL_RGBA 按照紅、綠、藍、Alpha順序排列的顏色
GL_BGR 按照藍、綠、紅順序排列顏色
GL_BGRA 按照藍、綠、紅、Alpha順序排列顏色
GL_RED 每個畫素只包含了一個紅色分量
GL_GREEN 每個畫素只包含了一個綠色分量
GL_BLUE 每個畫素只包含了一個藍色分量
GL_RG 每個畫素依次包含了一個紅色和綠色的分量
GL_RED_INTEGER 每個畫素包含了一個整數形式的紅色分量
GL_GREEN_INTEGER 每個畫素包含了一個整數形式的綠色分量
GL_BLUE_INTEGER 每個畫素包含了一個整數形式的藍色分量
GL_RG_INTEGER 每個畫素依次包含了一個整數形式的紅色、綠色分量
GL_RGB_INTEGER 每個畫素包含了一個整數形式的紅色、藍色、綠色分量
GL_RGBA_INTEGER 每個畫素包含了一個整數形式的紅色、藍色、綠色、Alpha分量
GL_BGR_INTEGER 每個畫素包含了一個整數形式的藍色、綠色、紅色分量
GL_BGRA_INTEGER 每個畫素包含了一個整數形式的藍色、綠色、紅色、Aplha分量
GL_STENCIL_INDEX 每個畫素只包含一個模板值
GL_DEPTH_COMPONENT 每個畫素只包含一個深度值
GL_DEPTH_STENCIL 每個畫素包含一個深度值和一個模板值

最後 3 個格式 GL_STENCIL_INDEXGL_DEPTH_COMPONENTGL_DEPTH_STENCIL 用於對模板緩衝區和深度緩衝區直接進行讀寫。 引數 type 解釋引數 pixels 指向的資料,它告訴 OpenGL 使用緩衝區的什麼資料型別來儲存顏色分量。如下表1-2。 表1-2 畫素資料的資料型別

常量 描述
GL_UNSIGNED_BYTE 每種顏色分量都是一個 8符號整數
GL_BYTE 8符號整數
GL_UNSIGNED_SHORT 16符號整數
GL_SHORT 16符號整數
GL_UNSIGNED_INT 32符號整數
GL_INT 32符號整數
GL_FLOAT 精度浮點數
GL_HALF_FLOAT 精度浮點數
GL_UNSIGNED_BYTE_3_2_2 包裝的 RGB
GL_UNSIGNED_BYTE_2_3_3_REV 包裝的 RGB
GL_UNSIGNED_SHORT_5_6_5 包裝的 RGB
GL_UNSIGNED_SHORT_5_6_5_REV 包裝的 RGB
GL_UNSIGNED_SHORT_4_4_4_4 包裝的 RGBA
GL_UNSIGNED_SHORT_4_4_4_4_REV 包裝的 RGBA
GL_UNSIGNED_SHORT_5_5_5_1 包裝的 RGBA
GL_UNSIGNED_SHORT_5_5_5_1_REV 包裝的 RGBA
GL_UNSIGNED_INT_8_8_8_8 包裝的 RGBA
GL_UNSIGNED_INT_8_8_8_8_REV 包裝的 RGBA
GL_UNSIGNED_INT_10_10_10_2 包裝的 RGBA
GL_UNSIGNED_INT_2_10_10_10_REV 包裝的 RGBA
GL_UNSIGNED_INT_24_8 包裝的 RGBA
GL_UNSIGNED_INT_10F_11F_11F_REV 包裝的 RGBA
GL_FLOAT_32_UNSIGNED_INT_24_8_REV 包裝的 RGBA

3、載入紋理

在幾何圖形中應用文理貼圖時,第一個必要步驟就是將紋理載入記憶體。一旦被載入,這些紋理就會成為當前紋理狀態的一部分。有 3OpenGL 函式最經常用來從儲存器緩衝區中載入(比如從一個磁碟檔案中讀取)紋理資料。

void glTexImage1D (GLenum target,GLint level,GLint internalformat,GLsizei width,GLint border,const GLvoid *pixels);

void glTexImage2D (GLenum target,GLsizei height,const GLvoid *pixels);

void glTexImage3D (GLenum target,GLsizei depth,const GLvoid *pixels);
複製程式碼

這三個函式實際上是由同一個函式 glTexImage 派生出來的。

引數說明

  • target: 紋理維度 GL_TEXTURE_1DGL_TEXTURE_2DGL_TEXTURE_3D
  • Level: 指定所載入的mip貼圖層次。⼀一般我們都把這個引數設定為 0
  • internalformat: 每個紋理單元中儲存多少顏色成分。(從讀取畫素圖時獲得)
  • width、height、depth 引數: 指載入紋理的寬度、⾼度、深度。==注意!==這些值必須是 2 的整數次方。(這是因為 OpenGL 舊版本上的遺留下的⼀個要求。當然現在已經可以⽀持不是 2 的整數次方。但是開發者們還是習慣使⽤用以 2 的整數次方去設定這些引數。)
  • border 引數: 允許為紋理理貼圖指定⼀一個邊界寬度。
  • format 引數: 畫素資料的資料型別(GL_UNSIGNED_BYTE,每個顏色分量都是一個 8 位無符號整數)
  • type 引數:
  • data 引數: 指向紋理影象資料的指標

4、使用顏色緩衝區

一維和二維紋理也可以從顏色緩衝區載入資料。我們可以從顏色緩衝區讀取一幅影象,並通過下面這兩個函式將它作為一個新的紋理使用。

void glCopyTexImage1D(GLenum target,GLenum
  internalformt,GLint x,GLint border);
void glCopyTexImage2D(GLenum target,GLsizei
  height,GLint border);
複製程式碼

這倆函式的操作類似於 glTexImage,但在這裡 xy 在顏色緩衝區中指定了開始讀取紋理資料的位置。源緩衝區是通過 glReadBuffer 函式設定的。請注意,並不存在 glCopyTexImage3D,因為我們無法從 2D 顏色緩衝區獲取體積資料。

5、更新紋理

替換一個紋理影象常常要比直接使用 glTexImage 重新載入一個新紋理快得多。用於完成這個任務的函式就是 glTexSubImage,它同樣有3 個變型。

void glTexSubImage1D(GLenum target,GLint xOffset,GLenum
  format,const GLvoid *data);

void glTexSubImage2D(GLenum target,GLint yOffset,GLsizei
  width,const GLvoid *data);

void glTexSubImage3D(GLenum target,GLint
  zOffset,Glenum type,const GLvoid * data);
複製程式碼

絕大部分引數都與 glTexImage 函式的引數準確地對應。xOffset、yOffset 和 zOffset 引數指定了在原來的紋理貼圖中開始替換紋理資料的偏移量。width、height 和 depth 引數指定了“插入”到原來那個紋理中的新紋理的寬度、高度和深度。

6、插入替換紋理

下面這組函式允許我們從顏色緩衝區讀取紋理,並插入或替換原來紋理的一部分。下面這組函式都是 glCopyTexSubImage 函式的變型。

void glTexSubImage1D(GLenum target,const GLvoid * data);
複製程式碼

注意: 這裡並沒有 glCopyTexImage 函式。這是因為顏色緩衝區是 2D 的,不存在一種對應的方法來將一副 2D 彩色影象作為一個 3D 紋理的來源。但是,我們可以使用 glCopyTexSubImage3D 函式,在一個三維紋理中使用顏色緩衝區的資料來設定它的一個紋理單元平面。

7、紋理物件

1)分配紋理物件

void glGenTextures (GLsizei n,GLuint *textures);
複製程式碼

在這個函式中,我們可以指定紋理物件的 數量 n 和一個指標 *textures,這個指標指向一個無符號整型陣列(由紋理物件識別符號填充)。

2)繫結紋理狀態

我們可以把他們看成是不同的可用紋理狀態的控制代碼。為了“繫結”其中一種紋理狀態,可以呼叫下面這個函式。此後,所有的紋理載入和紋理引數設定隻影響當前繫結的紋理物件。

void glBindTexture (GLenum target,GLuint texture);
複製程式碼
  • 引數 target: GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
  • 引數 texture: 需要繫結的紋理物件

3)讀紋理位,讀取畫素

// Load a .TGA file
GLbyte *gltReadTGABits(const char *szFileName,GLint *iWidth,GLint *iHeight,GLint *iComponents,GLenum *eFormat,GLbyte *pData = NULL);
複製程式碼
  • 引數 szFileName: 紋理檔名稱
  • 引數 iWidth: 檔案寬度地址
  • 引數 iHeight: 檔案高度地址
  • 引數 iComponents: 檔案元件地址
  • 引數 eFormat: 檔案格式地址
  • 返回值: 指向影象資料的指標

4)刪除繫結的紋理物件

void glDeleteTextures (GLsizei n,const GLuint *textures);
複製程式碼

5)測試紋理物件是否有效

GLboolean glIsTexture(GLuint texture)
複製程式碼

如果這是一個以前已經分配的紋理物件名,則返回 GL_TRUE,否則返回 GL_FALSE

8、設定紋理引數

很多引數的應用都會影響渲染的規則和紋理貼圖的行為。這些紋理引數都是通過 glTexParameter 函式的變數進行設定的。

void glTexParameterf (GLenum target,GLenum pname,GLfloat param);
void glTexParameterfv (GLenum target,const GLfloat *params);
void glTexParameteri (GLenum target,GLint param);
void glTexParameteriv (GLenum target,const GLint *params);
複製程式碼
  • 引數1:target,指定這些引數將要應⽤在哪個紋理模式上,⽐如 GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
  • 引數2:pname,指定了需要設定哪個紋理引數
  • 引數3:param 或 params,用於設定特定的紋理引數的值

9、設定過濾方式

根據一個拉伸或收縮的紋理貼圖計算顏色片段的過程稱為 紋理過濾(Texture Filitering)。使用 OpenGL 的紋理引數函式,可以同時設定放大和縮小過濾器。這兩種過濾器的引數名分別是 GL_TEXTURE_MAG_FILTERGL_TEXTURE_MIN_FILTER。我們可以為它們從兩種基本的紋理過濾器 GL_NEARESTGL_LINEAR 中進行選擇,它們分別對應於 鄰近過濾線性過濾

1)鄰近過濾(GL_NEAREST):

鄰近過濾是把最鄰近的紋理單元應用到紋理座標中。如圖1,左上角那個紋理畫素的中心距離紋理座標最近,所以它會被選擇為樣本顏色:

2)線性過濾(GL_LINEAR):

如圖2,線性過濾會把這個紋理座標周圍的紋理單元的加權平均值應用到這個紋理座標上(線性插值),一個紋理畫素的中心距離紋理座標越近,那麼這個紋理畫素的顏色對最終的樣本顏色的貢獻越大。下圖中你可以看到返回的顏色是鄰近畫素的混合色:

兩種紋理過濾方式的視覺效果,當在一個很大的物體上應用一張低解析度的紋理時(紋理被放大了,每個紋理畫素都能看到):

我們可以使用下面這個函式進行過濾:

void glTexParameteri (GLenum target,GLint param);
複製程式碼

四種組合方式的過濾:

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_HEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_LINEAR);
複製程式碼

10、設定環繞方式

正常情況下,我們在 0.01.0 的範圍內指定紋理座標,使它與紋理貼圖中的紋理單元形成對映關係。如果紋理座標落在這個範圍之外,OpenGL 則根據當前紋理環繞模式(Wrapping Mode)處理這個問題。

環繞方式(Wrapping) 描述
GL_REPEAT 對紋理的預設行為。重複紋理影象。
GL_MIRRORED_REPEAT 和GL_REPEAT一樣,但每次重複圖片是映象放置的。
GL_CLAMP_TO_EDGE 紋理座標會被約束在0到1之間,超出的部分會重複紋理座標的邊緣,產生一種邊緣被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的座標為使用者指定的邊緣顏色。

當紋理座標超出預設範圍時,每個選項有不同的效果。如下圖:

環繞方式對比.png

設定紋理引數:

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAR_S,GL_CLAMP_TO_EDGE);
複製程式碼
  • 引數1:紋理維度GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D

  • 引數2:為S/T座標設定模式GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,針對 s,t,r 座標

  • 引數3:wrapMode,環繞模式GL_REPEAT、GL_CLAMP、GL_CLAMP_TO_EDGE、GL_CLAMP_TO_BORDER
  • (1) GL_REPEAT OpenGL 在紋理座標超過 1.0 的⽅向上對紋理理進⾏重複;
  • (2) GL_CLAMP 所需的紋理單元取自紋理邊界或 TEXTURE_BORDER_COLOR.
  • (3) GL_CLAMP_TO_EDGE 環繞模式強制對範圍之外的紋理座標沿著合法的紋理單元的最後一⾏或者最後一 列來進行取樣。
  • (4) GL_CLAMP_TO_BORDER 在紋理座標在 0.01.0 範圍之外的只使⽤邊界紋理單元。邊界紋理單元是作為圍繞基本影象的額外的行和列,並與基本紋理影象⼀起載入的。

下面是綜合前面的函式的一個例子:

// 將TGA檔案載入為2D紋理。
bool LoadTGATexture(const char *szFileName,GLenum minFilter,GLenum magFilter,GLenum wrapMode)
{
    GLbyte *pBits;
    int nWidth,nHeight,nComponents;
    GLenum eFormat;
    
    //1、讀紋理位,讀取畫素
    //引數1:紋理檔名稱
    //引數2:檔案寬度地址
    //引數3:檔案高度地址
    //引數4:檔案元件地址
    //引數5:檔案格式地址
    //返回值:pBits,指向影象資料的指標
    
    pBits = gltReadTGABits(szFileName,&nWidth,&nHeight,&nComponents,&eFormat);
    if(pBits == NULL)
        return false;
    
    //2、設定紋理引數
    //引數1:紋理維度
    //引數2:為S/T座標設定模式
    //引數3:wrapMode,環繞模式
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,wrapMode);
    glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,wrapMode);
    
    //引數1:紋理維度
    //引數2:線性過濾
    //引數3:wrapMode,minFilter);
    glTexParameteri(GL_TEXTURE_2D,magFilter);
    
    //3、精密包裝畫素資料
    //引數1:GL_UNPACK_ALIGNMENT,指定OpenGL如何從資料快取區中解包影象資料
    //引數2:針對GL_UNPACK_ALIGNMENT 設定的值
    glPixelStorei(GL_UNPACK_ALIGNMENT,1);
    
    //載入紋理
    //引數1:紋理維度
    //引數2:mip貼圖層次
    //引數3:紋理單元儲存的顏色成分(從讀取畫素圖是獲得)
    //引數4:載入紋理寬
    //引數5:載入紋理高
    //引數6:載入紋理的深度
    //引數7:畫素資料的資料型別(GL_UNSIGNED_BYTE,每個顏色分量都是一個8位無符號整數)
    //引數8:指向紋理影象資料的指標
    
    glTexImage2D(GL_TEXTURE_2D,0,nComponents,nWidth,eFormat,GL_UNSIGNED_BYTE,pBits);
    
    //使用完畢釋放pBits
    free(pBits);
    
    
    //只有minFilter 等於以下四種模式,才可以生成Mip貼圖
    //GL_NEAREST_MIPMAP_NEAREST具有非常好的效能,並且閃爍現象非常弱
    //GL_LINEAR_MIPMAP_NEAREST常常用於對遊戲進行加速,它使用了高質量的線性過濾器
    //GL_LINEAR_MIPMAP_LINEAR 和GL_NEAREST_MIPMAP_LINEAR 過濾器在Mip層之間執行了一些額外的插值,以消除他們之間的過濾痕跡。
    //GL_LINEAR_MIPMAP_LINEAR 三線性Mip貼圖。紋理過濾的黃金準則,具有最高的精度。
    if(minFilter == GL_LINEAR_MIPMAP_LINEAR ||
       minFilter == GL_LINEAR_MIPMAP_NEAREST ||
       minFilter == GL_NEAREST_MIPMAP_LINEAR ||
       minFilter == GL_NEAREST_MIPMAP_NEAREST)
        
        //載入Mip,紋理生成所有的Mip層
        //引數:GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
        glGenerateMipmap(GL_TEXTURE_2D);
    
    return true;
}
複製程式碼

以上的總結參考了並部分摘抄了以下文章,非常感謝以下作者的分享:

1、OpenGl的學習網站 LearnOpenGL-CN 其中的“紋理”一節

2、《OpenGL超級寶典 第5版》

3、《OpenGL程式設計指南(原書第9版)》

轉載請備註原文出處,不得用於商業傳播——凡幾多