概念
纹理可以理解为一张 2D 图片(也有 1D 和 3D 的纹理),它可以添加到物体表面,以增加物体的表面细节,就像屋子里的墙纸一样。为了能把纹理映射到物体表面上,我们需要指定物体各个顶点对应纹理上的哪个坐标(Texture Coordinate)。

纹理坐标系以左下角为原点,横向向右为
s
轴,纵向向上为 t
轴,且不论纹理图片大小,横向纵向的最大值都是 1。
如果是 3D 纹理,还有
r
轴。事实上它们和x
,y
,z
轴是一个意思。
代码实践
public static int loadTexture(Context context, int resourceId) {
final int[] textureObjectIds = new int[1];
glGenTextures(1, textureObjectIds, 0);
if (textureObjectIds[0] == 0) {
Timber.d("Could not generate a new OpenGL texture object.");
return 0;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);
if (bitmap == null) {
Timber.d("resource Id could not be decoded");
return 0;
}
glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);
// 设置缩小的情况下过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 设置防大的情况下过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载纹理到 OpenGL,读入 Bitmap 定义的位图数据,并把它复制到当前绑定的纹理对象
GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE, 0);
return textureObjectIds[0];
}
glGenTextures
glGenTextures(int n, int[] textures, int offset)
:要使用纹理,必须要有 id。通过这个函数来生成纹理 id,n
代表生成 id 个数,textures
和 offset
一起决定生成的 id 放置的地址。
glBindTexture
通过这个函数将上一步生成的纹理对象绑定到 GL_TEXTURE_2D
这个 texture target 上,绑定后,这个 GL_TEXTURE_2D
就相当于纹理对象的代理了,之后对这个纹理对象的操作和配置都可以通过 GL_TEXTURE_2D
来进行。
- 这里涉及到一些概念。OpenGL 中至少保证有 16 个 texture unit(纹理单元),我们默认使用的是
GL_TEXTURE0
,即 0 号单元。每个 texture unit 又包含多种 texture target(纹理目标),例如上文中的GL_TEXTURE_2D
就是一种 texture target。事实上我们需要先调用glActiveTexture
来激活指定位置的 texture unit,再调用glBindTexture
来将纹理对象绑定到当前激活的 texture unit 下的某个类型 texture target 上。因为我们默认使用的就是GL_TEXTURE0
,因此当我们要使用默认的 texture unit 时就可以不用先调用glActiveTexture
。
当我们第一次调用glBindTexture
时,就决定了绑定的纹理对象类型,例如上文中我们就将纹理对象绑定到了GL_TEXUTRE0
的GL_TEXTURE_2D
类型,那么其纹理对象内部状态就被初始化为 2d texture 状态,并且该对象不能再被绑定到其他 texture target 上。- 我们上面的操作并没有关心 texture unit,而是使用默认就完了。那么什么时候我们需要使用不同的 texture unit 呢?当使用多重纹理的时候,也就是在着色器中要同时使用多个 *sampler 时。这是另一个更为复杂的话题了,这里不做讨论。
纹理过滤
纹理坐标被定义成一定是 [0~1]
,因此纹理坐标上任取一个点,不可能刚好落在纹理图片的一个像素上。那么我们就需要对这个坐标点应该显示什么颜色进行采样。常用的采样方式有两种:GL_NEAREST
和 GL_LINEAR
。
GL_NEAREST

GL_NEAREST
被称为邻近过滤,是 OpenGL 默认的过滤方式。上图中➕表示纹理坐标,OpenGL 将选择像素中心点最近的那个像素返回。
GL_LINEAR

GL_LINEAR
被称作线性过滤。上图中➕表示纹理坐标,OpenGL 将返回坐标附近像素值的加权平均值。
// 设置缩小的情况下过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// 设置放大的情况下过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
上面的代码就设置了纹理对象在放大(Magnify)和缩小(Minify)时采用的过滤方式。
texImage2D
// 加载纹理到 OpenGL,读入 Bitmap 定义的位图数据,并把它复制到当前绑定的纹理对象
GLUtils.texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
将 bitmap 图片上传到 GPU 端的,我们纹理对象绑定的 GL_TEXTURE_2D
处,这样,我们的纹理对象就会被附加上纹理图像。
glGenerateMipmap
glGenerateMipmap
用于生成多级渐进纹理。
多级渐进纹理
- 想象一下,假设我们有一个包含着上千物体的大房间,每个物体上都有纹理。有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率。由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。
- OpenGL使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。让我们看一下多级渐远纹理是什么样子的:
多级渐进纹理
解绑纹理对象
glBindTexture(GL_TEXTURE, 0);
。
着色器代码
uniform mat4 u_Matrix;
attribute vec4 a_Position;
attribute vec2 a_TextureCoordinates;
varying vec2 v_TextureCoordinates;
void main() {
v_TextureCoordinates = a_TextureCoordinates;
gl_Position = u_Matrix * a_Position;
}
顶点着色器中我们使用 attribute vec2 a_TextureCoordinates
来接受应用程序中的纹理坐标,并且将其赋值给 varying vec2 v_TextureCoordinates
来向片段着色器共享纹理坐标。
precision mediump float;
uniform sampler2D u_TextureUnit;
varying vec2 v_TextureCoordinates;
void main() {
gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates);
}
片段着色器中,我们使用跟顶点着色器中相同的 varying vec2 v_TextureCoordinates
来接受从顶点着色器传输过来的纹理坐标。我们申明一个采样器 uniform sampler2D u_TextureUnit
,采样器的作用是在纹理坐标上,按照采样规则进行像素采样,然后通过 texture2D
将像素值附加给光栅化后的片段。
Q: 这里的采样器如何定位到上文中加载的纹理对象呢?
A: 注意上文中的纹理对象,最终是绑定在了 texture unit0
的 texture targetGL_TEXTURE_2D
处。采样器在着色器中的声明sampler2D
就表明了其 texture target 是GL_TEXTURE_2D
。因此我们还需要告诉采样器,它所绑定的 texture unit 的位置。
这里面有两个重要的变量,一个是纹理坐标 a_TextureCoordinates
,一个是采样器 u_TextureUnit
。
应用层代码
1. 纹理坐标的传递
我们在 Rectangle
中定义物体的坐标和纹理的坐标,并将其中的纹理坐标取出,传递给着色器中的 a_TextureCoordinates
:
private static final String A_TEXTURE_COORDINATES = "a_TextureCoordinates";
float[] rectangleVertex = {
// 物体 (x, y),纹理 (s, t)
0f, 0f, 0.5f, 0.5f,
-0.5f, -0.8f, 0f, 0.9f,
0.5f, -0.8f, 1f, 0.9f,
0.5f, 0.8f, 1f, 0.1f,
-0.5f, 0.8f, 0f, 0.1f,
-0.5f, -0.8f, 0f, 0.9f
};
...
aTextureCoordinatesLocation = glGetAttribLocation(program, A_TEXTURE_COORDINATES);
注意到,每一个顶点坐标都需要对应一个纹理坐标,如果其中哪个坐标没有对应的纹理坐标的话,那么最后绘制的物体中所有与该顶点坐标相关的形状都不会被绘制。
注意上面的纹理坐标与所对应的顶点坐标是上下颠倒的。这是因为纹理坐标是左下角为原点,而 Android 系统则是以左上角为坐标原点,因此我们只能在应用层手动将纹理坐标上下颠倒,以正确显示图像纹理。
2. 采样器的传递
下面的代码完成了着色器中的采样器绑定到纹理对象的逻辑。
// 从编译好的着色器中将采样器变量取出
uTextureUnitLocation = glGetUniformLocation(program, U_TEXTURE_UNIT);
// 取得纹理对象 id
int texture = TextureHelper.loadTexture(context, R.drawable.image);
// 激活纹理单元 GL_TEXTURE0
glActiveTexture(GL_TEXTURE0);
// 将纹理对象绑定到当前激活纹理单元的 GL_TEXTURE_2D
glBindTexture(GL_TEXTURE_2D, texture);
// 将着色器中的采样器绑定到纹理单元 GL_TEXTURE0
glUniform1i(uTextureUnitLocation, 0);
https://github.com/fightyz/OpenGLPlayground.git
revision fe3f9efdd078deb0edb323a4fe6e91cf36acb669
网友评论