上一篇咱们咱们要对 GLSL 语言有了根底的理解,咱们当初应该对于 GLSL Shader 脚本起码能了解其中的语法含意,不过这显然是不够的。对于后面几个章节都能够说是对 Android OpenGL ES 的入门,本篇呢就说一下在咱们的理论工作中遇到的场景下 (诸如相机滤镜是怎么实现的、视频如何应用 OpenGL 渲染等等实际性问题) 来介绍 纹理
纹理
通过后面的内容咱们曾经理解到,咱们能够为每个顶点增加色彩来减少图形的细节,从而创立出乏味的图像。然而,如果想让图形看起来更实在,咱们就必须有足够多的顶点,从而指定足够多的色彩。这将会产生很多额定开销,因为每个模型都会需要更多的顶点,每个顶点又需要一个色彩属性。
这个时候咱们就能够应用 纹理(Texture)
。
纹理基本概念
纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理)
,它能够用来增加物体的细节;你能够设想纹理是一张绘有砖块的纸,无缝折叠贴合到你的 3D 的房子上,这样你的房子看起来就像有砖墙表面了。因为咱们能够在一张图片上插入十分多的细节,这样就能够让物体十分精密而不必指定额定的顶点。
上面你会看到之前教程的那个三角形贴上了一张砖墙图片。
纹理坐标
为了可能把纹理映射 (Map) 到三角形上,咱们须要指定三角形的每个顶点各自对应纹理的哪个局部。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来表明该从纹理图像的哪个局部采样(译注:采集片段色彩)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
纹理坐标在 x 和 y 轴上,范畴为 0 到 1 之间(留神咱们应用的是 2D 纹理图像)。应用纹理坐标获取纹理色彩叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。上面的图片展现了咱们是如何把纹理坐标映射到三角形上的。
咱们为三角形指定了 3 个纹理坐标点。如上图所示,咱们心愿三角形的左下角对应纹理的左下角,因而咱们把三角形左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中地位所以咱们把它的纹理坐标设置为(0.5, 1.0);同理右下方的顶点设置为(1, 0)。咱们只有给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。
纹理坐标看起来就像这样:
// 纹理坐标
const GLfloat m_texture_coors[6] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f// 上
};
2D 纹理的坐标系是从 左下角为原点向上为 t 轴, 向右为 s 轴的坐标系 。 这个坐标系的 y 方向和 GL 坐标系相同,所以默认按顶点坐标系方向输出的图像是倒置的
。
如果想要让纹理正向显示,咱们须要做的一步工作便是将输出的纹理坐标或者顶点坐标进行 y 轴方向的倒置。
这些咱们会在上面的示例中看到
加载图片
咱们下面理解了纹理的坐标系之后,那么要做的第一件事天然是把它们加载到咱们的利用中。
纹理图像可能被贮存为各种各样的格局,每种都有本人的数据结构和排列,所以咱们如何能力把这些图像加载到利用中呢?
咱们这里采纳 Android 上的 API,
BitmapFactory.decodeResource(getResources(), R.mipmap.wall)
而后把失去的 Bitmap 对象传入 Native 进行解决
AndroidBitmapInfo info; // create a AndroidBitmapInfo
int result;
// 获取图片信息
result = AndroidBitmap_getInfo(env, bitmap, &info);
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {LOGE("Player", "AndroidBitmap_getInfo failed, result: %d", result);
return 0;
}
LOGD("Player", "bitmap width: %d, height: %d, format: %d, stride: %d", info.width, info.height,
info.format, info.stride);
// 获取像素信息
unsigned char *data;
result = AndroidBitmap_lockPixels(env, bitmap, reinterpret_cast<void **>(&data));
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {LOGE("Player", "AndroidBitmap_lockPixels failed, result: %d", result);
return 0;
}
size_t count = info.stride * info.height;
LOGE("Player", "count: %d", count);
unsigned char *resultData = (unsigned char *) malloc(count * sizeof(unsigned char));;
memcpy(resultData, data, count);
// 像素信息不再应用后须要解除锁定
result = AndroidBitmap_unlockPixels(env, bitmap);
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {LOGE("Player", "AndroidBitmap_unlockPixels failed, result: %d", result);
}
最初失去 unsigned char *resultData 指向图片的内存地址
咱们能够应用
stb_image.h
库。这是另一个更好的解决方案,stb_image.h
库是一个反对多种风行格局的图像加载库。对于stb_image.h
库的更多应用办法详见 StbImage
创立纹理
和之前生成的 OpenGL 对象一样,纹理也是应用 ID 援用的。让咱们来创立一个:
GLuint m_texture_id = 0;
glGenTextures(1, &m_texture_id);
glGenTextures 函数首先须要输出生成纹理的数量,而后把它们贮存在第二个参数的 GLuint
数组中(咱们的例子中只是独自的一个GLuint
)
生成纹理之后还须要 激活纹理
, 绑定纹理
,让之后任何的纹理指令都能够配置以后绑定的纹理:
// 激活指定纹理单元
//glActiveTexture(GL_TEXTURE0);
// 绑定纹理 ID 到纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
当初纹理曾经绑定了,咱们能够应用后面载入的图片数据生成一个纹理了。纹理能够通过 glTexImage2D 来生成:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
函数很长,参数也不少,所以咱们一个一个地解说:
- 第一个参数指定了纹理指标(Target)。设置为 GL_TEXTURE_2D 意味着会生成与以后绑定的纹理对象在同一个指标上的纹理(任何绑定到 GL_TEXTURE_1D 和 GL_TEXTURE_3D 的纹理不会受到影响)。
- 第二个参数为纹理指定多级渐远纹理的级别,如果你心愿独自手动设置每个多级渐远纹理的级别的话。这里咱们填 0,也就是根本级别。
- 第三个参数通知 OpenGL 咱们心愿把纹理贮存为何种格局。咱们的图像只有
RGB
值,因而咱们也把纹理贮存为RGB
值。 - 第四个和第五个参数设置最终的纹理的宽度和高度。咱们之前加载图像的时候贮存了它们,所以咱们应用对应的变量。
- 下个参数应该总是被设为
0
(历史遗留的问题)。 - 第七第八个参数定义了源图的格局和数据类型。咱们应用 RGB 值加载这个图像,并把它们贮存为
char
(byte) 数组,咱们将会传入对应值。 - 最初一个参数是真正的图像数据。
当调用 glTexImage2D 时,以后绑定的纹理对象就会被附加上纹理图像。
PS
目前只有根本级别 (Base-level) 的纹理图像被加载了,如果要应用多级渐远纹理
,咱们必须手动设置所有不同的图像(一直递增第二个参数)。或者,间接在生成纹理之后调用 glGenerateMipmap。这会为以后绑定的纹理主动生成所有须要的多级渐远纹理。对于多级渐远纹理
咱们后文会再介绍
应用纹理
下面咱们曾经加载了纹理,那么咱们来看看怎么应用
首先一些初始化坐标
// 顶点坐标
const GLfloat m_vertex_coors[9] = {
-0.5f, -0.5f, 0.0f,// 左下
0.5f, -0.5f, 0.0f,// 右下
0.0f, 0.5f, 0.0f// 左上
};
// 纹理坐标
const GLfloat m_texture_coors[6] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f// 上
};
接着是 Vertex Shader
const char *BitmapDrawer::GetVertexShader() {
return "attribute vec4 aPosition; \n"// 顶点坐标
"attribute vec2 aCoordinate; \n"// 纹理坐标
"varying vec2 vCoordinate; \n"// 输入纹理坐标
"void main() \n"
"{ \n"
"gl_Position = aPosition; \n"
"vCoordinate = aCoordinate; \n"
"} \n";
}
比照前文的绘制三角形的 Vertex Shader,咱们这里多了 纹理坐标
而后 Fragment Shader
const char *BitmapDrawer::GetFragmentShader() {
return "precision mediump float; \n"// 配置精度
"uniform sampler2D uTexture; \n"
"varying vec2 vCoordinate; \n"
"void main() \n"
"{ \n"
"gl_FragColor = texture2D (uTexture, vCoordinate); \n"
"} \n";
}
最初是咱们前文讲过的 OpenGL 的 GLSL 应用流程了,
PS
咱们这里我只贴上不同于绘制三角形的代码
具体代码可查看位于文末的 Github 地址
void BitmapDrawer::InitVarHandler() {
// 获取变量句柄
m_vertex_pos_handler = glGetAttribLocation(m_program_id, "aPosition");
m_texture_pos_handler = glGetAttribLocation(m_program_id, "aCoordinate");
m_texture_handler = glGetUniformLocation(m_program_id, "uTexture");
// 生成纹理
if (m_texture_id == 0) {glGenTextures(1, &m_texture_id);
LOGI(TAG, "Create texture id : %d, %x", m_texture_id, glGetError())
}
ActivateTexture();
// 绑定纹理数据
if (cst_data != NULL) {
glTexImage2D(GL_TEXTURE_2D, 0, // level 个别为 0
GL_RGBA, // 纹理外部格局
m_origin_width, m_origin_height, // 画面宽高
0, // 必须为 0
GL_RGBA, // 数据格式,必须和下面的纹理格局放弃始终
GL_UNSIGNED_BYTE, // RGBA 每位数据的字节数,这里是 BYTE: 1 byte
cst_data);// 画面数据
// 开释资源
free(cst_data);
cst_data = NULL;
}
}
void BitmapDrawer::ActivateTexture() {
GLenum type = GL_TEXTURE_2D;
// 激活指定纹理单元
// glActiveTexture(GL_TEXTURE0 + index);
// 绑定纹理 ID 到纹理单元
glBindTexture(type, m_texture_id);
// 将流动的纹理单元传递到着色器外面
// glUniform1i(texture_handler, index);
// 配置边缘过渡参数
glTexParameterf(type, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(type, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
// 绘制
void BitmapDrawer::DoDraw() {
// 启用顶点的句柄
glEnableVertexAttribArray(m_vertex_pos_handler);
glEnableVertexAttribArray(m_texture_pos_handler);
// 设置着色器参数
// glUniformMatrix4fv(m_vertex_matrix_handler, 1, false, m_matrix, 0);
glVertexAttribPointer(m_vertex_pos_handler, 3, GL_FLOAT, GL_FALSE, 0, m_vertex_coors);
glVertexAttribPointer(m_texture_pos_handler, 2, GL_FLOAT, GL_FALSE, 0, m_texture_coors);
// 开始绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
}
最终后果如下图
下面的代码中有几处咱们之前没有接触过的中央,还有存有纳闷的中央,上面咱们一一解释
// 配置边缘过渡参数
glTexParameterf(type, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(type, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
纹理过滤与盘绕
纹理过滤
下面咱们说了纹理坐标相干的内容,然而纹理坐标不依赖于分辨率 (Resolution),它能够是任意浮点值,所以 OpenGL 须要晓得怎么将纹理像素(Texture Pixel,也叫 Texel,译注 1) 映射到纹理坐标。当你有一个很大的物体然而纹理的分辨率很低的时候这就变得很重要了。你可能曾经猜到了,OpenGL 也有对于纹理过滤 (Texture Filtering) 的选项。纹理过滤有很多个选项,然而当初咱们只探讨最重要的两种:GL_NEAREST 和 GL_LINEAR。
PS
Texture Pixel 也叫 Texel,你能够设想你关上一张.jpg
格局图片,一直放大你会发现它是由有数像素点组成的,这个点就是纹理像素;留神不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL 以这个顶点的纹理坐标数据去查找纹理图像上的像素,而后进行采样提取纹理像素的色彩。
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是 OpenGL 默认的纹理过滤形式。当设置为 GL_NEAREST 的时候,OpenGL 会抉择中心点最靠近纹理坐标的那个像素。下图中你能够看到四个像素,加号代表纹理坐标。左上角那个纹理像素的核心间隔纹理坐标最近,所以它会被抉择为样本色彩:
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标左近的纹理像素,计算出一个插值,近似出这些纹理像素之间的色彩。一个纹理像素的核心间隔纹理坐标越近,那么这个纹理像素的色彩对最终的样本色彩的奉献越大。下图中你能够看到返回的色彩是邻近像素的混合色:
那么这两种纹理过滤形式有怎么的视觉效果呢?让咱们看看在一个很大的物体上利用一张低分辨率的纹理会产生什么吧(纹理被放大了,每个纹理像素都能看到):
GL_NEAREST 产生了颗粒状的图案,咱们可能清晰看到组成纹理的像素,而 GL_LINEAR 可能产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR 能够产生更实在的输入,但有些开发者更喜爱 8 -bit 格调,所以他们会用 GL_NEAREST 选项。
当进行放大 (Magnify) 和放大 (Minify) 操作的时候能够设置纹理过滤的选项,比方你能够在纹理被放大的时候应用邻近过滤,被放大时应用线性过滤。咱们须要应用 glTexParameter* 函数为放大和放大指定过滤形式。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
纹理盘绕
纹理坐标的范畴通常是从 [0, 0] 到[1, 1],超过 [0.0, 1.0] 的范畴是容许的,而对与超出范围的内容要如何显示,这就取决于纹理的盘绕形式(Wrapping mode)。在 OpenGL 默认的行为是反复这个纹理图像(GL_REPEAT)。
下图从左到右顺次是这三种成果:
盘绕形式(Wrapping) | 形容 |
---|---|
GL_REPEAT | 对纹理的默认行为。反复纹理图像 |
GL_MIRRORED_REPEAT | 但每次反复图片是镜像搁置的 |
GL_CLAMP_TO_EDGE | 纹理坐标会被束缚在 0 到 1 之间,超出的局部会反复纹理坐标的边缘,产生一种边缘被拉伸的成果。 |
咱们通过 glTexParameteri 指定 Texture 的盘绕模式,能够看见它是能够在 S,T 两个方向上独立设置的。
// 设置 Texutre 的盘绕模式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
GL_CLAMP_TO_EDGE
GL_REPEAT
GL_MIRRORED_REPEAT
纹理单元
上面咱们来看这段代码
// 激活指定纹理单元
// glActiveTexture(GL_TEXTURE0 + index);
// 绑定纹理 ID 到纹理单元
glBindTexture(type, m_texture_id);
// 将流动的纹理单元传递到着色器外面
// glUniform1i(texture_handler, index);
你可能会奇怪为什么 // glUniform1i(texture_handler, index);
这句代码被正文了,而且在咱们的 Fragment Shader 中显著有个 sampler2D
变量是个 uniform,咱们却不必 glUniform 给它赋值。应用 glUniform1i,咱们能够给纹理采样器调配一个地位值,这样的话咱们可能在一个片段着色器中设置多个纹理。一个纹理的地位值通常称为一个纹理单元 (Texture Unit)。 一个纹理的默认纹理单元是 0,它是默认的激活纹理单元,所以教程后面局部咱们没有调配一个地位值。
纹理单元的次要目标是让咱们在着色器中能够应用多于一个的纹理。通过把纹理单元赋值给采样器,咱们能够一次绑定多个纹理,只有咱们首先激活对应的纹理单元。就像 glBindTexture 一样,咱们能够应用 glActiveTexture 激活纹理单元,传入咱们须要应用的纹理单元:
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
激活纹理单元之后,接下来的 glBindTexture 函数调用会绑定这个纹理到以后激活的纹理单元,纹理单元 GL_TEXTURE0 默认总是被激活,所以咱们在后面的例子里当咱们应用 glBindTexture
的时候,无需激活任何纹理单元。
PS
OpenGL 至多保障有 16 个纹理单元供你应用,也就是说你能够激活从 GL_TEXTURE0 到 GL_TEXTRUE15。它们都是按程序定义的,所以咱们也能够通过 GL_TEXTURE0 + 8 的形式取得 GL_TEXTURE8,这在当咱们须要循环一些纹理单元的时候会很有用。
多级渐远纹理
下面咱们队一个三角形或者矩形物体应用了纹理,然而设想一下当咱们领有很多个这样的物体,,每个物体上都有纹理。有些物体会很远,但其纹理会领有与近处物体同样高的分辨率。因为远处的物体可能只产生很少的片段,OpenGL 从高分辨率纹理中为这些片段获取正确的色彩值就很艰难,因为它须要对一个跨过纹理很大部分的片段只拾取一个纹理色彩。在小物体上这会产生不实在的感觉,更不用说对它们应用高分辨率纹理节约内存的问题了。
OpenGL 应用一种叫做多级渐远纹理 (Mipmap) 的概念来解决这个问题,它简略来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背地的理念很简略:距观察者的间隔超过肯定的阈值,OpenGL 会应用不同的多级渐远纹理,即最适宜物体的间隔的那个。因为距离远,解析度不高也不会被用户留神到。同时,多级渐远纹理另一加分之处是它的性能十分好。让咱们看一下多级渐远纹理是什么样子的:
手工为每个纹理图像创立一系列多级渐远纹理很麻烦,幸好 OpenGL 有一个 glGenerateMipmaps 函数,在创立完一个纹理后调用它 OpenGL 就会承当接下来的所有工作了。前面的教程中你会看到该如何应用它。
在渲染中切换多级渐远纹理级别 (Level) 时,OpenGL 在两个不同级别的多级渐远纹理层之间会产生不实在的僵硬边界。就像一般的纹理过滤一样,切换多级渐远纹理级别时你也能够在两个不同多级渐远纹理级别之间应用 NEAREST 和 LINEAR 过滤。为了指定不同多级渐远纹理级别之间的过滤形式,你能够应用上面四个选项中的一个代替原有的过滤形式:
过滤形式 | 形容 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 应用最邻近的多级渐远纹理来匹配像素大小,并应用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 应用最邻近的多级渐远纹理级别,并应用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,应用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间应用线性插值,并应用线性插值进行采样 |
就像纹理过滤一样,咱们能够应用 glTexParameteri 将过滤形式设置为后面四种提到的办法之一:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
PS
一个常见的谬误是,将放大过滤的选项设置为多级渐远纹理过滤选项之一。这样没有任何成果,因为多级渐远纹理次要是应用在纹理被放大的状况下的:纹理放大不会应用多级渐远纹理,为放大过滤设置多级渐远纹理的选项会产生一个 GL_INVALID_ENUM 错误代码。
片段 (Frament Shader) 纹理取样
咱们再回顾一下下面的代码,
// 顶点坐标
const GLfloat m_vertex_coors[9] = {
-0.5f, -0.5f, 0.0f,// 左下
0.5f, -0.5f, 0.0f,// 右下
0.0f, 0.5f, 0.0f// 左上
};
// 纹理坐标
const GLfloat m_texture_coors[6] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f// 上
};
在应用 glTexImage2D 把图片数据绑定纹理数据之后,咱们到了 Fragment Shader 代码中用 GLSL 的内置函 数 texture2D 对顶点生成的光栅化几何图形进行逐片段涂色操作,
gl_FragColor = texture2D (uTexture, vCoordinate);
至于 texture2D
函数我开始也了解了很久,然而咱们能够先用 PS 关上咱们提供的超过的美图,而后放大到最大,咱们就看到了上面的图片 (太大了放不下,这是部分图),咱们看到超过的美图是由一个个带有色彩的方格组成的,这些方格(不带有色彩的) 就对应着咱们后面强调的 光栅化,
咱们能够设想一下,纹理是一张绘有图片的纸,无缝折叠贴合到你的几何图形上,这样你的几何图形看起来就像有图片表面了。因为咱们能够在一张图片上插入十分多的细节,这样就能够让物体十分精密而不必指定额定的顶点。
咱们只须要指定 每个顶点坐标对应的纹理坐标,对纹理进行取样操作,咱们就能以很少的顶点用纹理来增加物体的细节,这也是咱们之后进行图片解决的根底。
小结
Github 代码