OpenGL/OpenGL ES入門:紋理初探 - 常用API解析

佐籩發表於2019-05-26

系列推薦文章:
OpenGL/OpenGL ES入門:圖形API以及專業名詞解析
OpenGL/OpenGL ES入門:渲染流程以及固定儲存著色器
OpenGL/OpenGL ES入門:影象渲染實現以及渲染問題
OpenGL/OpenGL ES入門:基礎變換 - 初識向量/矩陣
OpenGL/OpenGL ES入門:紋理初探 - 常用API解析
OpenGL/OpenGL ES入門: 紋理應用 - 紋理座標及案例解析(金字塔)

什麼是紋理

在之前的幾片文章中,已經對點、線和三角形進行了渲染,也看到了如何通過計算顏色值對它們進行著色,以及在它們之間進行值操作來模擬光照效果。為了能夠達到更加真實的效果,這一篇引入紋理貼圖。

紋理只是一種能夠應用到場景中的三角形上的影象資料,它通過經過過濾的紋理單元(texel,相當於基於紋理的畫素)填充到實心區域。

初識紋理的小夥伴們可以理解為,紋理就是圖片。當然紋理遠遠不止是影象資料這麼簡單,它是大多數現代3D渲染演算法的一個關鍵因素。這裡只做簡單瞭解。

畫素

畫素包裝

影象資料在記憶體中很少以緊密包裝的形式存在。在許多硬體平臺上,處於效能考慮,一幅影象的每一行都應該從一種特定的位元組對齊地址開始。絕大多數編譯器會自動把變數和緩衝區放置在一個針對該架構對齊優化的地址上。

預設情況下,OpenGL採用4個位元組的對齊方式,這種方式適合於很多目前正在使用時的系統。

下面這個句話引用來自《OpenGL超級寶典》(第5版)
很多程式設計師會簡單地將影象寬度值乘以高度值,在乘以每個畫素的位元組數,這樣就錯誤地判斷一個影象所需的儲存器數量
例如:一幅RGB影象,包含3個分量,每個分類都儲存在一個位元組中(每個顏色通道8位),如果影象的寬度為199個畫素,那麼影象的每一行需要多少儲存空間呢?
按照上面的演算法來計算:199*3 = 597位元組
這樣也許是對的,但是作為優秀的程式設計師,可能會討厭這個數字。如果硬體本身的體系結構是4位元組排列(大部分是這樣的),那麼影象每一行的末尾都將有額外的3個空位元組進行填充(每一行600位元組),而這是為了使每一行的儲存器地址從一個能夠被4整除的地址開始。

許多未經壓縮的影象檔案格式也遵循這種慣例,然而Targa(.TGA)檔案格式則是1個位元組排列的,這樣不會浪費空間。為什麼記憶體分配意圖對於OpenGL來說這麼重要?

因為在我們想OpenGL提交影象資料或從OpenGL獲取影象資料時,OpenGL需要知道我們想要在記憶體中對資料進行怎樣的包裝或解包裝操作。

認識一下下面幾個函式:

// 改變畫素儲存方式
glPixelStorei(GLenum pname, GLint param);

// 恢復畫素儲存值方式
glPixelStoref(GLenum pname, GLint param);

// 如果我們想要改成緊密包裝畫素資料,應該像下面這樣呼叫函式
/*
引數1: 指定OpenGL如何從資料緩衝區中解包影象資料
引數2: 允許設定1(byte排列)、2(排列為偶數byte的行)、4(字word排列)、8(行從雙位元組邊界開始)
*/
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
複製程式碼

畫素圖

畫素圖在記憶體佈局上與點陣圖非常相似,但是每個畫素將需要一個以上的儲存位來表示。每個畫素的附加位允許儲存強度(亮度)或者顏色分量值。

OpenGL中,可以使用下面的函式將顏色緩衝區的內容作為畫素圖直接讀取。

// 將顏色緩衝區的內容作為畫素圖直接讀取
/*
引數1&引數2: x,y矩形左下角的視窗座標
引數3&引數4: width,height矩形的寬高,以畫素為單位
引數5: 畫素格式
引數6: 解釋引數pixels指向的資料,告訴OpenGL使用緩衝區中的什麼資料型別來儲存顏色分量,畫素資料的資料型別
引數7: pixels,指向影象資料的指標
*/
glReadPixel(GLint x, GLint y, GLSizei width, GLSizei height, GLenum format, GLenum type, const void *pixels);

/*
模式引數:GL_FRONT、GL_BACK、GL_LEFT、GL_RIGHT、GL_FRONT_LEFT、
GL_FRONT_RIGHT、GL_BACK_LEFT、GL_BACK_RIGHT或者甚至是GL_NONE中的任意一個
*/
// 指定讀取的快取
glReadBuffer(mode);
// 指定寫入的快取
glWriteBuffer(mode);
複製程式碼

畫素格式表

畫素資料的資料型別

讀取畫素

Targa影象格式是一種方便而且容易使用的影象格式,並且它既支援簡單顏色影象,也支援帶有Alpha值的影象。後面篇幅中一致使用這種格式來進行紋理操作。

/*
引數1: 將要載入的Targa檔案的檔名
引數2: 檔案寬度地址
引數3: 檔案高度地址
引數4: 檔案資料格式地址
引數5: 檔案格式地址
返回值: 如果函式呼叫成功,返回一個新定位到直接從檔案中讀取的影象資料的指標,否則返回NULL
*/
GLbyte *gltReadTGABits(const char *szFileName, GLint *iWidth, GLint *iHeight, GLint *iComponents, GLenum *eFormat);
複製程式碼

載入紋理

在幾何圖形中應用貼圖時,第一個必要步驟就是將紋理載入記憶體。一旦被載入,這些紋理就會成為當前紋理狀態的一部分。

/*
引數1: GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
引數2: 指定這個函式所載入的mip貼圖層次,預設設為0
引數3: 每個紋理單元儲存多少顏色成分(從讀取畫素圖時獲得)
引數4: width、height、depth指載入紋理的寬度、高度、深度
引數5: 允許為紋理貼圖指定一個邊界寬度,目前來說,設定為0
引數6: OpenGL 資料儲存方式,一般使用GL_UNSIGNED_BYTE
引數7: 圖片資料指標
*/
void glTexImage1D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, void *data);

void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, void *data);

void glTexImage3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data);
複製程式碼

使用顏色緩衝區

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

void glCopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);

void glCopyTexImage1D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
複製程式碼

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

更新紋理

在時間敏感的場合如遊戲或模擬應用程式中,重複載入新紋理可能會成為效能瓶頸。 如果我們不再需要某個已載入的紋理,它可以被全部替換,也可以被替換掉一部分。替換一個紋理影象常常要比直接使用glTexImage重新載入一個新紋理快的多。函式程式碼如下

void glTexSubImage1D(GLenum target, GLint level, 
                    GLint xOffset, 
                    GLsizei width, 
                    GLenum format, GLenum type, const GLvoid *data);
                    
void glTexSubImage2D(GLenum target, GLint level, 
                    GLint xOffset, GLint yOffset, 
                    GLsizei width, GLsizei height,
                    GLenum format, GLenum type, const GLvoid *data);
                    
void glTexSubImage3D(GLenum target, GLint level, 
                    GLint xOffset, GLint yOffset, GLint zOffset,
                    GLsizei width, GLsizei height, GLsizei depth,
                    GLenum format, GLenum type, const GLvoid *data);
複製程式碼

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

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

void glCopyTexSubImage1D(GLenum target, GLint level, 
                    GLint xOffset, 
                    GLint x, GLint y, 
                    GLsizei width);
                    
void glCopyTexSubImage2D(GLenum target, GLint level, 
                    GLint xOffset, GLint yOffset
                    GLint x, GLint y, 
                    GLsizei width, GLsizei height);
                    
void glCopyTexSubImage1D(GLenum target, GLint level, 
                    GLint xOffset, GLint yOffset, GLint zOffset,
                    GLint x, GLint y, 
                    GLsizei width, GLsizei height);
複製程式碼

前面說到,不存在一種對應方法來將一幅2D彩色影象作為一個3D紋理的來源。但是,我們可以使用glCopyTexSubImage3D函式,在一個三維紋理中使用顏色緩衝區的資料來設定它的一個紋理單元平面。

紋理物件

紋理物件允許我們一次載入一個以上紋理狀態(包含紋理影象)。以及在它們之間進行快速切換。紋理狀態是由當前繫結的紋理物件維護的。而紋理物件時一個無符號整數標識的。

//使用函式分配紋理物件
//指定紋理物件的數量 和 指標(指標指向一個無符號整形陣列,由紋理物件識別符號填充)。
void glGenTextures(GLsizei n, GLuint *textTures);

//繫結紋理狀態
//引數1: GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
//引數2: 需要繫結的紋理物件
void glBindTexture(GLenum target, GLunit texture);

//刪除繫結紋理物件
//紋理物件 以及 紋理物件指標(指標指向一個無符號整形陣列,由紋理物件識別符號填充)。
void glDeleteTextures(GLsizei n, GLuint *textures);

//測試紋理物件是否有效
//如果texture是一個已經分配空間的紋理物件,那麼這個函式會返回GL_TRUE,否則會返回GL_FALSE。
GLboolean glIsTexture(GLuint texture);
複製程式碼

紋理引數設定

和將一幅圖片貼在三角形的一面相比,紋理貼圖需要更多的工作,很多引數的應用都會影響渲染的規則和紋理貼圖的行為。這些紋理引數都是通過glTexParameter函式的變數來進行設定的。

/*
引數1: target,指定這些引數將要應用在那個紋理模式上,比如GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D。
引數2: pname,指定需要設定那個紋理引數
引數3: param,設定特定的紋理引數的值
*/
glTexParameterf(GLenum target, GLenum pname, GLFloat param);
glTexParameteri(GLenum target, GLenum pname, GLint param);
glTexParameterfv(GLenum target, GLenum pname, GLFloat *param);
glTexParameteriv(GLenum target, GLenum pname, GLint *param);
複製程式碼

基本過濾

根據一個拉伸或收縮的紋理貼圖計算顏色片段的過程稱為紋理過濾

使用OpenGL的紋理引數函式,可以同時設定放大和縮小過濾器。引數名為:GL_TEXTURE_MAG_FILTERGL_TEXTURE_MIN_FILTER

就目前來說,可以認為它們從兩種基本的紋理過濾器:最鄰近過濾(GL_NEAREST)和線性過濾(GL_LINEAR)中選擇。

最鄰近過濾: 最為顯著的特徵就是當紋理被拉伸到特別大時,所出現的大片斑駁畫素。它是我們能夠選擇的最簡單、最快速的過濾方法。

線性過濾: 最為顯著的特徵就是當紋理被拉伸時,所出現的“失真”圖形,但是,和最鄰近過濾模式下所呈現的斑駁狀畫素塊相比較,這種“失真”更接近事實。

OpenGL/OpenGL ES入門:紋理初探 - 常用API解析

/*
引數1: 紋理維度
引數2: 放大&縮小過濾器
引數3: 環繞模式
*/
// 為放大和縮小過濾器設定紋理過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEARST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEARST);

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

通過下面的圖片可以比較一下兩種過濾的區別:

OpenGL/OpenGL ES入門:紋理初探 - 常用API解析

紋理環繞

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

呼叫glTexParameter函式(並分別使用GL_TEXTURE_WRAP_S、GL_TEXTURE_WRAP_T或GL_TEXTURE_WRAP_R做引數),為每個座標分別設定環繞模式。

/*
引數1: 紋理維度 GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
引數2: GL_TEXTURE_WRAP_S、GL_TEXTURE_T、GL_TEXTURE_R,針對s,t,r座標
引數3: 環繞方式
*/
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
複製程式碼

OpenGL/OpenGL ES入門:紋理初探 - 常用API解析

OpenGL/OpenGL ES入門:紋理初探 - 常用API解析

相關文章