Android OpenGL基础3——纹理的使用

系列文章

纹理概念

纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节。为了能让纹理映射到物体(通常是物体某个面)上,我们需要指定这个面分别对应纹理的哪个部分,这样面的每个顶点就关联这一个纹理坐标,用来标明从纹理的那个部分采样(片段着色器采集片段颜色),然后在其他片段上惊醒片段插值。

纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。

纹理环绕方式

一个纹理能用来渲染的的范围通常是从(0,0)到(1,1),当我们把纹理坐标设置在一个纹理可渲染的范围之外的时候(纹理不够大的时候),就涉及到文理的环绕方式(怎么填充)设置了。OpenGL默认采用的环绕方式是GL_REPEAT,其他环绕方式如下:

  • GL_REPEAT,默认的环绕方式,重复纹理图像;
  • GL_MIRROR_REPEAT,镜像重复纹理图像;
  • GL_CLAMP_TO_EDGE,超出默认纹理坐标范围时,纹理边缘拉伸效果;
  • GL_CLAMP_TO_BORDER,超出默认纹理坐标范围是,纹理边缘填充效果(填充色为制定填充色);

设置纹理环绕代码示例:

1
2
3
4
5
6
7
8
9
10
// 第一个参数,表示纹理目标,这里目标是2D纹理,第二个参数表示纹理坐标轴,2D纹理有S周和T周,3D的还有个R轴,第三个表示纹理的环绕模式,这里采用的时MIRROR_REPEAT
glTxtParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRROR_REPEAT);
glTxtParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRROR_REPEAT);
// 指定为 GL_CLAMP_TO_BORDER 这种环绕方式时,需要额外设置一个边框颜色属性
// 1. 先设置为GL_CLAMP_TO_BORDER
glTxtParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
// 2. 设置一个额外的颜色
float borderColor[] = {1.0f, 1.0f, 1.0f, 1.0f};
glTxtParameterfl(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

纹理过滤

纹理过滤解决了什么问题呢,由于纹理坐标不依赖于分辨率,它可以是任意的浮点值,所以OpenGL需要知道怎么将纹理像素映射到纹理坐标中,这种判断如何通过纹理坐标来获取纹理像素的过程,就是纹理过滤,纹理过滤有很多种,这里主要解释最常用的两种:

  • GL_NEAREST,即邻近过滤(Nearest Neighbor Filtering),是OpenGL默认的纹理过滤方式,这种方式下,OpenGL会选出纹理像素中心点里纹理坐标点最近的一个作为样本颜色;
  • GL_LINEAR,即线性过滤((Bi)linear Filtering),这种方式下,会基于纹理坐标附近的纹理像素做一个插值运算,拿计算出来的结果作为样本颜色(这个纹理坐标附近纹理像素在样本颜色中的占比有其中心点据纹理坐标距离的远近决定的,越近占比月高)。

当在一个很大的物体上应用一个很小的纹理时,此时采用上面两种过滤方式会有不同的结果表现:

  • 采用GL_NEAREST,会产生颗粒状图案,有点像素风;
  • 采用GL_LINEAR,会产生更平滑的图案。

当我们有放大或缩小需求的时候,我们可以动态的设置纹理过滤选项,设置方式就是通过下面的函数调用:

1
2
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 放大是采用GL_LINEAR
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);// 缩小时采用GL_NEAREST

多级渐远纹理

多级渐远纹理的出现我认为是为了让纹理的使用更贴近现实生活的一种产物,二者用了这种方式后,让纹理应用既符合实际,又提高了性能。具体来说,使用了多级渐远纹理后,距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。

显然,使用多级渐远纹理也会碰到过度生硬、断层等不真实的问题,解决方式就是采用类似纹理过滤的那种方案,多级渐远纹理的纹理过滤选项有以下4(种:

过滤方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样

纹理生成

上面介绍了纹理的一些设置选项:纹理环绕、纹理过滤、多级渐远纹理等概念,现在说说纹理到底怎样应用的,一般纹理的使用需要包括以下几个过程:(纹理图片)加载、创建、绑定、配置、应用等。

纹理加载

纹理加载就是把要当成纹理的图片加载到OpenGL程序中使用的过程,说白了就是图片文件的解析与加载过程(从磁盘到内存),最终要得到的时纹理图片的可用的二进制图片数据,这几假设为:data。

纹理创建与绑定

OpenGL创建对象的方式几乎都相同,先定一个地址,然后将该地址传递过去,纹理的创建也不例外:

1
2
3
4
5
6
7
8
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

// 采用glTexImage2D函数来生成2D纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
// 调用glGenerateMipmap来自动生成多级渐远纹理
glGenerateMipmap(GL_TEXTURE_2D);

函数解释:

  • 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
  • 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
  • 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
  • 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
  • 下个参数应该总是被设为0(历史遗留的问题)。
  • 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
  • 最后一个参数是真正的图像数据。

当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。

纹理应用

纹理图片加载好后,纹理对象创建和绑定后,最终要完成纹理的应用,纹理的应用离不开顶点坐标,所以我们需要在顶点属性指定的属性中增加纹理坐标的属性,然后通过顶点属性指针指定顶点属性的解析方式,最后将这些属性通过顶点着色器传递给片段着色器。

得到顶点属性后,我们还需要片段着色器中能接受到前面创建的纹理对象,在片段着色其中如何接受这个纹理对象呢。GLSL中定义了一个采样器的概念。所以我们可以在片段着色器中定义统一变量形式的一个采用器,然后在把纹理对象赋值给这个采样器即可。

最后在片段着色器中,利用GLSL内建的texture函数来采用颜色并作为片段颜色进行输出,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#version 300 es
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
// 第一个参数表示采用器,第二个参数表示纹理坐标,传递这两个参数后,通过texture得到混合后的颜色
FragColor = texture(ourTexture, TexCoord);
}

纹理单元

上面的片段着色器中,sampler2D是统一变量,但并没有类似的glUniform方法来设置这个值,而是通过glUniformi来设置,调用glUniformi方法后,可以给纹理采样器分配一个位置值,这样的位置值称为纹理单元,一个采样器中至多可以存在(0~15)共16个纹理单元;在绑定纹理之前,需要先激活纹理单元,有时候没有看到激活纹理单元,那是因为GL_TEXTURE0这个纹理单元默认是激活的,无需手动激活。

1
2
glActiveTexture(GL_TEXTURE0);// 激活第一个纹理单元
glBindTexture(GL_TEXTURE_2D, texture);// 绑定纹理对象

纹理混合

纹理混合指的是对两个纹理按一定的比例进行插值,并将最后结果作为输出的一种方式,通常利用GLSL内建的mix(texture1, texture2)来达到这个目的。既然是纹理混合,所以定需要多个纹理对象,所以片段着色器中相应的就要添加多个采样器。由于存在多个采样器,就需要指定不同纹理间混合的具体设置。这里介绍几个纹理相关的GLSL内建函数。

  • fragColor = texture(sampler2D, textureCoor),通过采样器和纹理坐标来得到纹理贴图;
  • fragColor = texture(sampler2D, textureCoor) * vec4(colorValue, 1.0),通过纹理贴图和顶点颜色来得到最终的贴图;
  • mix(texture(ourTexture1, ourTexCoord),texture(ourTexture2, ourTexCoord), 0.2),通过两个纹理混合得到最终的纹理贴图;

在上面的说明中,两个纹理贴图最终的效果都是倒置的,这是因为OpenGL要求y轴0.0坐标是在图片的底部的,但是图片的y轴0.0坐标通常在顶部。也就是OpenGL的坐标系和图片坐标系不一致导致的,这就需要进行转换,具体可以有下面几种方式:

  1. 加载图片的时候直接对纹理图片进行翻转;
  2. 翻转顶点信息中的纹理坐标,即将顶点信息中的纹理坐标表示Y周的值都用1去减(纹理坐标的值都在0到1之间);
  3. 翻转Shader顶点信息中的纹理坐标(这样不用更改源输顶点信息),同样是用1去减原来的Y纹理坐标;
  4. 反其道而行之,既然纹理反了,我们就把顶点坐标也反过来,这样就“反反得正”了,由于可见部分的顶点坐标都是在(-1,1)范围内,这里直接去顶点坐标信息中y轴值的相反数即可(区别2、3中的直接采用1去减

完整的程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
void BasicTextureSample::Init() {
LOGCATI("BasicTextureSample::Init");
if (m_ProgramObj != 0) {
return;
}
char vShader[] =
"#version 300 es\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"layout (location = 2) in vec2 aTexCoord;\n"
"out vec3 ourColor;\n"
"out vec2 ourTexCoord;\n"
"void main()\n"
"{\n"
" gl_Position=vec4(aPos.x, -aPos.y, aPos.z, 1.0f);\n"
" ourColor=aColor;\n"
" ourTexCoord=aTexCoord;\n"
"}\n";

char fShader[] =
"#version 300 es\n"
"precision mediump float;\n"
"out vec4 fragColor;\n"
"in vec3 ourColor;\n"
"in vec2 ourTexCoord;\n"
"// uniform sampler2D ourTexture;\n"
"uniform sampler2D ourTexture1;\n"
"uniform sampler2D ourTexture2;\n"
"void main()\n"
"{\n"
"// 下面这个是一个纯纹理贴图\n"
"// fragColor = texture(ourTexture, ourTexCoord)\n"
"// 下满这个是纹理和顶点颜色混合贴图\n"
"// fragColor = texture(ourTexture, ourTexCoord) * vec4(ourColor, 1.0);\n"
"// 下面这个采用两个纹理混合得到片段颜色\n"
" fragColor = mix(texture(ourTexture1, ourTexCoord),texture(ourTexture2, ourTexCoord), 0.2);\n"
"}\n";
m_ProgramObj = GLUtils::CreateProgram(vShader, fShader, m_VertexShader, m_FragmentShader);

// 定义顶点 及 属性数据
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 // 左上
};

unsigned int indices[] = {
0, 1, 3, // 第一个三角形
1, 2, 3 // 第二个三角形
};

glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
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);
// 开始顶点属性指针的设置
// position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
(void *) 0);
glEnableVertexAttribArray(0);
// color
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
(void *) (3 * sizeof(float)));
glEnableVertexAttribArray(1);
// texture
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
(void *) (6 * sizeof(float)));
glEnableVertexAttribArray(2);

// 生成纹理
glGenTextures(1, &texture1);
// 激活纹理
glActiveTexture(texture1);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, texture1);
// 为纹理设置环绕方式
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_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

LOGCATI("width:%d, height:%d, format:%d", m_textureImg[0].width, m_textureImg[0].height,
m_textureImg[0].format);
// 这里官网示例采用的时 GL_RGB,但我如果采用GL_RGB会发现显示不了上面的效果。原因应该是图片的加载方式不同,在 MainActivity
// 的 loadRgbaImage方法中,指定了图片的格式为 RGBA,如果如果采用GL_RGB,格式上就不匹配了,导致显示异常
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_textureImg[0].width, m_textureImg[0].height,
0, GL_RGBA, GL_UNSIGNED_BYTE, m_textureImg[0].ppPlane[0]);
glGenerateMipmap(GL_TEXTURE_2D);

// 生成纹理
glGenTextures(1, &texture2);
// 激活纹理
glActiveTexture(texture2);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, texture2);
// 为纹理设置环绕方式
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_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

LOGCATI("width:%d, height:%d, format:%d", m_textureImg[1].width, m_textureImg[1].height,
m_textureImg[1].format);
// 这里官网示例采用的时 GL_RGB,但我如果采用GL_RGB会发现显示不了上面的效果。原因应该是图片的加载方式不同,在
// MainActivity的 loadRgbaImage方法中,指定了图片的格式为 RGBA,如果如果采用GL_RGB,格式上就不匹配了,
// 导致显示异常
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_textureImg[1].width, m_textureImg[1].height,
0, GL_RGBA, GL_UNSIGNED_BYTE, m_textureImg[1].ppPlane[0]);
glGenerateMipmap(GL_TEXTURE_2D);
glUseProgram(m_ProgramObj);
GLUtils::setInt(m_ProgramObj, "ourTexture1", 0);
GLUtils::setInt(m_ProgramObj, "ourTexture2", 1);

}

void BasicTextureSample::Draw(int screenW, int screenH) {
LOGCATI("BasicTextureSample::Draw");
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

glUseProgram(m_ProgramObj);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// glDrawElements(GL_LINES, 6, GL_UNSIGNED_INT, 0); // 绘制线
}

void BasicTextureSample::Destroy() {
if (m_ProgramObj) {
glDeleteProgram(m_ProgramObj);
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
m_ProgramObj = GL_NONE;
}
}

void BasicTextureSample::LoadImage(NativeImage *pImage) {

LOGCATE("BasicTextureSample::LoadImage pImage = %p", pImage->ppPlane[0]);
if (pImage) {
m_textImg.width = pImage->width;
m_textImg.height = pImage->height;
m_textImg.format = pImage->format;
NativeImageUtil::CopyNativeImage(pImage, &m_textImg);
}
}

void BasicTextureSample::LoadMultiImageWithIndex(int index, NativeImage *pImage) {
if (pImage) {
if (m_textureImg[index].ppPlane[0]) {
NativeImageUtil::FreeNativeImage(&m_textureImg[index]);
}

m_textureImg[index].width = pImage->width;
m_textureImg[index].height = pImage->height;
m_textureImg[index].format = pImage->format;
NativeImageUtil::CopyNativeImage(pImage, &m_textureImg[index]);
}
}