上一节讲到了绘制三角形,我想绘制一个图片要怎么做呢?使用纹理。 贴瓷砖大家应该都见过吧,纹理和瓷砖的原理差不多,把瓷砖(图片)的四个角对齐,贴到墙上(两个三角形绘制的矩形)上即可。
1、纹理
纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
通俗来说,纹理就是包含着一张或者多张图片信息的一个OpenGL对象,这张图片可以是一维、二维或者三维的,并且拥有对应的图片格式。
2、纹理坐标
如果我们要绘制一个矩形,矩形的坐标如下:
GLfloat verticesCoords[] = {
-0.6f, 0.6f, 0.0f, // Position 0
-0.6f, -0.6f, 0.0f, // Position 1
0.6f, -0.6f, 0.0f, // Position 2
0.6f, 0.6f, 0.0f, // Position 3
};
GLushort indices[] = {0, 1, 2, 0, 2, 3};
长方形坐标如图所示:
要想瓷砖贴得好,纹理的坐标也应该和上边对应。
首先说明下,纹理坐标系和顶点坐标系不一样,顶点坐标系是-1到1之间,和一般的坐标系没有区别。但纹理坐标系的取值范围是从0到1,没有负值。相当于纹理坐标系类似于一般坐标系的第一象限。不过由于android上opengl es的bug,纹理坐标系真实情况如下:
现在长方形四个顶点的坐标分别是:左上角、左下角、右下角、右上角,那纹理坐标也要按这个方位来,所以纹理坐标的值即是:
GLfloat textureCoords[] = {
0.0f, 0.0f, // TexCoord 0
0.0f, 1.0f, // TexCoord 1
1.0f, 1.0f, // TexCoord 2
1.0f, 0.0f // TexCoord 3
};
还有个小点值得注意,纹理坐标轴不再是x、y、z轴之类的,它的轴名字为s、t、r轴,和x、y、z轴相对应,和他们一样的理解就行
3、纹理环绕
纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分),但OpenGL提供了更多的选择:
| 环绕方式 | 描述 |
|---|---|
GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
不同的环绕方式效果图如下:
用如下代码指定环绕方式:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
4、纹理过滤
纹理的终极原理一开始就说了,和贴瓷砖一样,四个角对齐,贴上去即可。
还记得OpenGL是什么 一文中对图形渲染管线的描述吗,其中有个步骤叫光栅化,就是将图形分成一个个小格子,然后片段着色器取颜色,绘制这个小格子。
纹理也一样,片段着色器从纹理中读取相应坐标点内的颜色值,绘制到一个个小格子中,即一个个像素点内。
当你有一个很大的物体但是纹理的分辨率很低的时候怎么办(具体情况参见图片放大),这时候纹理过滤就派上用场了。打个比方,屏幕上相信的两个像素要显示的纹素一样,这时候要怎么显示。
纹理过滤有很多个选项,但是现在我们只讨论最重要的两种:GL_NEAREST和GL_LINEAR。
GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
这两种纹理过滤的效果如图所示:
5、纹理绘制
纹理代表着一张图片或一个视频,要把这个图片或纹理的像素数据赋给纹理对象。那android中如何在c++中取像素数据呢?
AndroidBitmapInfo info;
void* pixel;
if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
LOGI("get bitmap info failed");
return;
}
AndroidBitmap_lockPixels(env, bitmap, &pixel);
auto context = MyGlRenderContext::getInstance();
LOGI("bitmap width = %d, height = %d", info.width, info.height);
context->setBitmapData(pixel, info.width, info.height);
AndroidBitmap_unlockPixels(env, bitmap);
接下来,我们要生成纹理并配置纹理:
glGenTextures(1, &m_TextureId);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
纹理生成完之后,我们要把图片像素配置到纹理上
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height,
0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
现在我们来看看此时的顶点着色器代码:
#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
out vec2 v_texCoord;
void main()
{
gl_Position = a_position;
v_texCoord = a_texCoord;
}
顶点着色器代码中传入了两个参数,一个是顶点坐标,另一个就是纹理坐标,并且向片段着色器输出纹理坐标
片段着色器代码:
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;
void main()
{
outColor = texture(s_TextureMap, v_texCoord);
}
片段着色器中接收顶点着色器的纹理坐标,并且调用texture函数。texture函数的作用就是根据纹理坐标获取s_TextureMap代表的纹理的纹素
s_TextureMap的值怎么指定呢?如果片段着色器只有一个纹理,一般不用我们指定它的值了,系统会自动帮我们对应。一般是通过如下方法指定这个值:
GLint m_SamplerLoc = glGetUniformLocation(m_ProgramObj, "s_TextureMap");
glUniform1i(m_SamplerLoc, 0);
最后,我们要来绘制纹理了:
glUseProgram(m_ProgramObj);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), verticesCoords);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GL_FLOAT), textureCoords);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
//如果只有一个纹理,那么这里可以不指定,系统默认会为我们赋值。
glUniform1i(m_SamplerLoc, 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
可以这么理解:
- 激活纹理,注意后面的参数是 GL_TEXTURE0,它是opengl内定义的常量,表示第0个纹理
- 绑定纹理,GL_TEXTURE0 纹理对象激活了,绑定 m_TextureId 到 GL_TEXTURE0 纹理对象上
- 给片段着色器内的纹理对象 m_SamplerLoc 赋值为0,其实就是把 m_SamplerLoc 对象关联到0号纹理,即 GL_TEXTURE0
- 绘制图形,绘制矩形,纹理激活了也绑定了相应数据,所以纹理也会绘制上去。
最后,提一个小点:纹理代表着一张图片,图片是有宽高的,如果显示的宽高和图片原有的宽高不对应,图片就会拉伸变形。怎么让纹理正确显示呢? 因为纹理是显示在我们绘制的矩形上,所以矩形的比例对了,纹理的比例自然就对了。矩形的大小,又要参考屏幕宽高进行规一化处理,所以这里比较绕,大家可以自行琢磨下。
最后展示效果图: