纹理是什么
纹理(TEXTURE),即物体表面的样子。在计算机的世界中,我们能够绘制的仅仅是一些非常基础的形状,比如点、线、三角形,这些基础显然是无法将一个现实世界中的物体很好的描述在屏幕上的。通常我们通过纹理映射将物体表面图片贴到物体的几何图形上面,完成贴图的过程,将物体从现实世界中模拟到虚拟世界中。
纹理的基础单元是纹素(Texel,即texture element或texture pixel的合成字),亦如屏幕的基础单元是像素。屏幕上有自己的坐标系,纹理也有,即纹理坐标,一个维度称为 s,另一个维度称为 t,其范围都在 [0,1]之间。纹理坐标上,是纹素。
如上图所示,在 OpenGL的二维世界中,本质上,纹理就是一个二维数组(图像数据),而纹素就是这个二维数组中的值。
纹理映射到屏幕
接下来,首先我们讨论下图像数据是如何从纹理坐标下被投射到屏幕上的。
坐标映射
上面我们就讲过纹理坐标系。两个维度,范围都在[0,1]之间。无论是使用纯色渲染,还是图片渲染,实际上都是在进行颜色渲染,最终,都需要将那块颜色映射到归一化坐标系中。而归一化坐标系中两个维度都是在[-1,1]之间。
由于这种坐标系的不一致,就需要将纹理坐标系中的图片映射到归一化坐标系中。这里,首先我们会遇到两个问题:
- 将纹理坐标系中的哪个区域块取出来进行映射。
-
取出来的区域块又该如何如何对应到归一化坐标系中通过顶点定义的外形中。
如上图所示,我们可以得到以下几点:
- 要选取图片中的哪个区域是由我们通过纹理顶点坐标定义的。上图中,我们通过定义(0,0),(0,1),(1,1),(1,0),实际上选取了整个图片部分作为贴片。
- 要进行贴图的部分定义好了,它所要贴到的地方,实际上和我们定义的顶点坐标按照定义顺序一一对应(映射)。即,我们定义的第一个纹理顶点坐标,对应我们定义的第一个顶点坐标。
最终,我们可以得到:
为了明显起见,这里再给出一张图(改变顶点定义的顺序):
纹理过滤(Texture Filtering)
纹理坐标和归一化坐标通常并不能实现一比一的映射关系,纹理可能会比归一化坐标系中的几何图形大,也可能更小。这样就带来了这样的问题:在不能实现一比一映射的时候,该如何在纹理坐标中采样显示到归一化坐标中。这就是纹理过滤,实际上是在处理放大和缩小纹理的操作。
常见的过滤方式有两种:最邻近过滤和双线性过滤。
对于最邻近过滤,顾名思义,是为每个片段选择最近的纹素;而双线性过滤,使用双线性插值平滑像素之间的过滤。如下图所示,显然线性过滤会有更好的效果,但是也会消耗更多的计算能力:
我们可以看到,对于双线性过滤这种方式,特别是在进行缩小采样的时候,当缩小比例过大,会丢掉很多的细节,因为不管这个比例如何,它只会选取周围的四个点(即四个纹理元素)。为了解决这样的问题,可以使用 MIP 贴图的方式解决。关于 MIP 更多信息可以参考这里
纹理环绕(Texture Wrapping)
上面坐标中,我们指定的纹理坐标范围均是在 [0,1]之间,但是如果超出了这个范围, 此时刻,我们选择的纹理区域大于了实际图片大小,纹理区域空白部分该如何处理,就是纹理环绕问题。下图参考
综上,我们了解了纹理坐标及其映射。下面,我们可以进行实际地编码工作了。
在程序中使用纹理
通常,我们在程序中使用纹理的步骤是这样的:
- 定义顶点以及渲染程序。其中,在 glsl 程序中使用的 attribute 、uniform、varying 区别可参考
private static final String VERTEX_SHADER =
"uniform mat4 uMVPMatrix;" +
"attribute vec4 vPosition;" +
"attribute vec2 a_texCoord;" +
"varying vec2 v_texCoord;" +
"void main() {" +
" gl_Position = uMVPMatrix * vPosition;" +
" v_texCoord = a_texCoord;" +
"}";
private static final String FRAGMENT_SHADER =
"precision mediump float;" +
"varying vec2 v_texCoord;" +
"uniform sampler2D s_texture;" +
"void main() {" +
" gl_FragColor = texture2D(s_texture, v_texCoord);" +
"}";
private static final float[] TEX_VERTEX = { // in clockwise order:
0, 1f, // top left
1f, 0, // bottom right
0, 0, // bottom left
};
private static final float[] VERTEX = { // in counterclockwise order:
-1, -1f, 0, // bottom left
1, 1, 0, // top right
-1, 1, 0, // top left
};
- 加载图片,创建 texture 纹理,然后将图片指定在纹理上。
GLES20.glGenTextures(1, texNames, 0);
mTexName = texNames[0];
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.cat);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexName);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
- 指定纹理参数,主要包括纹理过滤和纹理环绕方式。
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_MIRRORED_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_MIRRORED_REPEAT);
- 为几何顶点指定相应的纹理坐标中的顶点
mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texCoord");
GLES20.glEnableVertexAttribArray(mTexCoordHandle);
GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0,
mTexVertexBuffer);
- 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,3);
完整代码MyRender.java
需要注意的一点,上述代码中我们定义的顺序。实际上,在计算机中图片都是原点在左上角,x轴向右延伸,y轴向下延伸,如下图所示。
小结
本节中,我们对 OpenGL 中的纹理进行了解和应用。在现实世界中物体有太多的细节,纹理给我们提供了一种方式将这些细节通过图片的方式贴在屏幕上的几何物体上。这对于二维平面来说,从效果上来说可能不具有太多吸引力,因为我们的图片本来就是二维的,但是 OpenGL 也能对其进行一些变换(剪切、翻转、平移等等),而且这些变换是运行在 GPU 上。下面,我们将进入三维的世界。
参考链接
Texture Mapping
Texture Mapping
OPENGL ES 2.0:纹理
Mipmap
Textures objects and parameters