OpenGL ES 之 2D 紋理介紹和使用

一、紋理概念

紋理可以簡單理解爲物體表面的圖案,OpenGL ES 3.0 中紋理有:2D紋理、2D紋理數組、3D紋理和立方圖紋理。一個紋理的單獨數據元素稱爲“紋素”(texture pixel 紋理像素的縮寫)。

1. 紋理的座標系

在這裏插入圖片描述

2. 2D紋理

2D紋理是最基本和常用的紋理,可以把2D紋理想象爲一個圖像數據的二維數組。2D紋理的紋理座標用一對2D座標 (s, t) 指定,有時也稱作 (u, v) 座標。

紋理圖像座標的左下角爲 (0.0, 0.0),右上角爲 (1.0, 1.0)。在 [0.0, 1.0] 區間之外的座標也是允許的,在區間外的紋理讀取行爲由紋理包裝模式定義。

3. 2D紋理數組

2D紋理數組常用於存儲2D圖像的一個動畫,數組的每個切片表示紋理動畫的一幀。2D紋理數組使用與3D紋理一樣的紋理座標 (s, t, r),其中 r 座標選擇 2D 紋理數組中要使用的切片,(s, t) 座標選擇的方法和 2D 紋理完全一致。

二、紋理對象和紋理加載

1. 創建紋理

首先我們需要創建一個紋理對象,紋理對象就是一個容器,包含渲染所需的紋理數據,例如圖像數據、過渡模式、包裝模式等。

在 OpenGL ES 中,紋理對象使用一個無符號的整數表示,該整數即紋理對象的一個句柄/ID。

/**
 * @param n 指定要生成的紋理對象數量
 * @param textures 一個保存n個紋理對象ID的無符號整數數組
 */
void glGenTextures(GLsizei n, GLuint *textures);

2. 刪除紋理

紋理對象在不再需要時必須刪除,這一步驟通常在應用程序關閉或者遊戲級別改變時完成。

/**
 * @param n 要刪除的紋理對象數量
 * @param textures 一個保存n個紋理對象ID的無符號整數數組
 */
void glDeleteTextures(GLsizei n, const GLuint *textures);

3. 綁定紋理

當我們生成並得到紋理對象ID後,如果想對其進行操作,就必須先綁定這個紋理對象,這樣後續的操作影響的殭屍我們綁定的這個紋理對象。

/**
 * @param target 目標紋理,即 GL_TEXTURE_2D、GL_TEXTURE_3D、GL_TEXTURE_2D_ARRAY 或 GL_TEXTURE_CUBE_MAP
 * @param texture 要綁定的紋理對象ID
 */
void glBindTexture(GLenum target, GLuint texture);

4. 解綁紋理

當我們不想再操作這個紋理對象時,可以解綁這個紋理。綁定 0 就是解綁了,同樣是使用 glBindTexture 函數,參數傳入固定的值 0 即可。

5. 加載圖像數據

用於加載2D和立方圖紋理的基本函數是 glTexImage2D。此外在 3.0 中還有多種替代方法指定 2D 紋理,如不可變紋理 glTexStorage2DglTexSubImage2D 的結合,將在後續介紹。

/**
 * @param target 目標紋理,即 GL_TEXTURE_2D、GL_TEXTURE_3D、GL_TEXTURE_2D_ARRAY 或 GL_TEXTURE_CUBE_MAP
 * @param level 指定要加載的mip級別。第一個級別爲0,後續的mip貼圖級別遞增
 * @param internalformat 紋理存儲的內部格式
 * @param width 圖像的像素寬度
 * @param height 圖像的像素高度
 * @param border 在OpenGL ES中忽略,保留是爲了兼容桌面的OpenGL,傳入0
 * @param format 輸入的紋理數據格式
 * @param type 輸入像素數據的類型
 * @param pixels 包含圖像的實際像素數據。像素行必須對齊到用glPixelStroei設置的GL_UNPACK_ALIGNMENT
 */
void glTexImage2D(
        GLenum target, GLint level, GLint internalformat,
        GLsizei width, GLsizei height, GLint border,
        GLenum format, GLenum type, const GLvoid *pixels)

關於紋理格式的介紹,可以參考:
https://blog.csdn.net/afei__/article/details/96158388

6. 相關設置

紋理的相關設置大都是通過 glTexParameter[i|f][v] 接口指定。這裏介紹兩個最常見的設置。

紋理過濾模式

紋理過濾模式,簡單的說就是當紋理需要放大和縮小時,應該怎麼處理。有關紋理過濾模式的介紹,可以參考:https://blog.csdn.net/afei__/article/details/96484772

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 縮小的情況
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大的情況

紋理座標包裝

前面介紹過,紋理座標系是 [0, 0, 1, 1],那麼當座標超過這個範圍時,應該發生怎樣的行爲。

紋理座標包裝有三種不同的模式:

  • GL_REPEAT : 重複紋理
  • GL_CLAMP_TO_EDGE : 限定讀取紋理的邊緣
  • GL_MIRRORED_REPEAT : 重複紋理並鏡像
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // s軸
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // t軸

7. 示例

創建一個RGB圖像紋理爲例:

// 紋理對象的句柄
GLuint textureId;
// 一個2*2的圖像,四個像素依次爲紅、綠、藍、黃,每個像素大小爲3個字節
GLubyte pixels[4 * 3] = {
    255, 0, 0, // Red
    0, 255, 0, // Green
    0, 0, 255, // Blue
    255, 255, 0 // Yellow
};
// 創建一個紋理對象
glGenTextures(1, &textureId);
// 綁定紋理對象
glBindTexture(GL_TEXTURE_2D, textureId);
// 加載紋理
glTexImage2D(
        GL_TEXTURE_2D, // target
        0, // level
        GL_RGB, // internalformat
        2, // width
        2, // height
        0, // border
        GL_RGB, // format
        GL_UNSIGNED_BYTE, // type
        pixels); // pixels
// 設置紋理過濾模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 設置紋理座標包裝
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

上述數據由無符號字節 RGB 三元組組成,範圍爲 [0, 255]

三、使用紋理

上面我們已經瞭解到如何創建和加載一個紋理,並得到這樣一個紋理id。那麼如何在代碼中使用這個紋理呢?通過一個實例來一步步瞭解吧。

1. 着色器腳本

頂點着色器

很簡單的一個頂點着色器,僅僅把輸入的數據再傳給片段着色器即可。

#version 300 es
layout(location = 0) in vec4 vPosition; // 輸入的頂點座標
layout(location = 1) in vec2 vTexCoor; // 輸入的紋理座標
out vec2 v_texCoor; // 輸出到片段着色器的紋理座標
void main() {
    gl_Position = vPosition;
    v_texCoor = vTexCoor;
}

片段着色器

使用紋理的真正地方,片段着色器中,使用到的內建函數 texture 作用是從紋理中指定位置讀取一個顏色的 vec4。

#version 300 es
precision mediump float; // 指定精度
uniform sampler2D s_texture; // 採樣器對象,當前綁定和激活的紋理
in vec2 v_texCoor; // 頂點着色器中輸出的紋理座標
out vec4 fragColor; // 該片段的顏色
void main() {
        fragColor = texture ( s_texture, v_texCoor ); // 在對應紋理座標處進行採樣並得到對應的顏色
}

2. 創建一個紋理對象

就使用上面的那個示例即可

GLuint loadTexture() {
    GLuint textureId;
    // 2*2 RGB data for test
    GLfloat pixels[4 * 3] = {
            1.0, 0.0, 0.0, // Red
            0.0, 1.0, 0.0, // Green
            0.0, 0.0, 1.0, // Blue
            1.0, 1.0, 0.0 // Yellow
    };
    // 創建一個紋理對象
    glGenTextures(1, &textureId);
    // 綁定紋理
    glBindTexture(GL_TEXTURE_2D, textureId);
    // 加載數據
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB, GL_FLOAT, pixels);
    // 相關設置
    glGenerateMipmap(GL_TEXTURE_2D);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 解除綁定
    glBindTexture(GL_TEXTURE_2D, 0);
    // 返回紋理id
    return textureId;
}

3. 使用這個紋理對象

我們以繪製一個矩形爲例,即繪製兩個三角形。

// 1. 創建一個程序(這裏省略了詳細代碼,可在最後的工程地址中找到)
GLuint g_program = CreateProgram(vertexShaderSource, fragmentShaderSource);
glUseProgram(g_program);

// 2. 初始化我們的頂點數據,即傳給頂點着色器的數據
// OpenGL的世界座標系是 [-1, -1, 1, 1],紋理的座標系爲 [0, 0, 1, 1]
// 又由於本例運行在安卓設備上,在安卓中 y 軸是向下的,想用正確的方向觀看圖像的話,也要注意這一點
GLfloat vertices[] = {
        // 前三個數字爲頂點座標(x, y, z),後兩個數字爲紋理座標(s, t)
        // 第一個三角形
        1.0,  1.0,  0.0,     1.0, 0.0,
        1.0,  -1.0, 0.0,     1.0, 1.0,
        -1.0, -1.0, 0.0,     0.0, 1.0,
        // 第二個三角形
        1.0,  1.0,  0.0,     1.0, 0.0,
        -1.0, -1.0, 0.0,     0.0, 1.0,
        -1.0, 1.0,  0.0,     0.0, 0.0
};

// 3. 加載紋理
GLuint textureId = loadTexture();
glActiveTexture(GL_TEXTURE0); // 激活TEXTURE0
glBindTexture(GL_TEXTURE_2D, textureId);
// 找到片段着色器中s_texture的位置,並給它賦值
GLint location = glGetUniformLocation(g_program, "s_texture");
glUniform1i(location, 0); // 因爲激活的是TEXTURE0,所以要給這個紋理賦值0

// 4. 加載頂點數據
// 給vPosition賦值,它的location是0,然後size是3,stride是5個float,起始指針是vertices
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), vertices);
glEnableVertexAttribArray(0);
// 給vTexCoor賦值,它的location是1,然後size是2,stride是5個float,起始指針是vertices+3
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), vertices + 3);
glEnableVertexAttribArray(1);

// 5. 繪製
glDrawArrays(GL_TRIANGLES, 0, 6); // 繪製兩個三角形,一共是6個點

4. 效果圖

由於放大的關係,過渡的顏色信息都是插值生成的,顏色分別爲紅、綠、藍、黃四個像素的 2*2 大小的紋理放大後的效果如下:
在這裏插入圖片描述

四、工程地址

上面沒有列出的代碼,可以在下面地址中找到:

https://github.com/afei-cn/OpenGLSample/tree/master/texturedemo

另外,這是一個可運行的 Android App 工程。上例中使用的是 C++ 的 API,還有一個使用 Java API 加載圖片紋理的例子也在上面的地址中,效果圖如下:
在這裏插入圖片描述

最後,一些 OpenGL 的相關文章: https://blog.csdn.net/afei__/article/category/8502759

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章