前面几篇博客,我们将Android中利用OpenGL ES 2.0绘制各种形体,并在上一篇博客中专门讲了GLSL语言。但是我们看到的基于OpenGL开发的应用和游戏,可不仅仅是那些规则形体和一些简单的色彩构成,而是各种不规则的形体构成了现实世界或者卡通世界的人和事物,他们都是外面穿着漂亮“衣服”的。本篇博客就是来讲解这些“衣服”的基础的。这些衣服就是纹理贴图。
什么是纹理贴图
一般来说,纹理是表示物体表面的一幅或几幅二维图形,也称纹理贴图(texture)。当把纹理按照特定的方式映射到物体表面上的时候,能使物体看上去更加真实。当前流行的图形系统中,纹理绘制已经成为一种必不可少的渲染方法。在理解纹理映射时,可以将纹理看做应用在物体表面的像素颜色。在真实世界中,纹理表示一个对象的颜色、图案以及触觉特征。纹理只表示对象表面的彩色图案,它不能改变对象的几何形式。更进一步的说,它只是一种高强度的计算行为。——百度百科
比如我们在利用OpenGL做游戏的时候,加载了一个人物模型进来了,这个人物模型上是没有色彩的。我们需要给它绘上需要的色彩才行。但是这些色彩从哪里来呢?我们不可能像之前处理球体那样,根据顶点取生成需要的色彩,那样对于我们给这个人物模型绘色的工作量实在太大了。这个时候我们就需要用到纹理贴图的技术了——把一个纹理(对于2D贴图,可以简单的理解为图片),按照所期望的方式显示在诸多三角形组成的物体的表面。
纹理映射原理
启用纹理映射后,如果想把一幅纹理映射到相应的几何图元,就必须告诉GPU如何进行纹理映射,也就是为图元的顶点指定恰当的纹理坐标。纹理坐标用浮点数来表示,范围一般从0.0到1.0,左上角坐标为(0.0,0.0),右上角坐标为(1.0,0.0),左下角坐标为(0.0,1.0),右下角坐标为(1.0,1.0),如下图所示:
左图为纹理图和纹理坐标,右图为顶点图和顶点坐标。
将纹理映射到右边的两个三角形上(也就是一个矩形),需要将纹理坐标指定到正确的顶点上,才能使纹理正确的显示,否则显示出来的纹理会无法显示,或者出现旋转、翻转、错位等情况。
将右图顶点按照V2V1V4V3传入,以三角形条带方式绘制,则纹理坐标应按照V2V1V4V3传入。如果按照V3V4V1V2传入,会得到一个旋转了180度的纹理。如果按照V4V3V2V1传入,则会得到一个左右翻转的纹理。
显示图片
根据纹理映射原理,结合之前绘制正方形的经验,我们可以根据以下步骤利用OpenGL ES显示一张图片:
第一步,修改着色器
首先,我们需要修改我们的着色器,将顶点着色器修改为:
attribute vec4 vPosition;
attribute vec2 vCoordinate;
uniform mat4 vMatrix;
varying vec2 aCoordinate;
void main(){
gl_Position=vMatrix*vPosition;
aCoordinate=vCoordinate;
}
可以看到,顶点着色器中增加了一个vec2变量,并将这个变量传递给了片元着色器,这个变量就是纹理坐标。接着我们修改片元着色器为:
precision mediump float;
uniform sampler2D vTexture;
varying vec2 aCoordinate;
void main(){
gl_FragColor=texture2D(vTexture,aCoordinate);
}
片元着色器中,增加了一个sampler2D的变量,sampler2D我们在前一篇博客GLSL语言基础中提到过,是GLSL的变量类型之一的取样器。texture2D也有提到,它是GLSL的内置函数,用于2D纹理取样,根据纹理取样器和纹理坐标,可以得到当前纹理取样得到的像素颜色。
第二步,设置顶点坐标和纹理坐标
根据纹理映射原理中的介绍,我们将顶点坐标设置为:
private final float[] sPos={
-1.0f,1.0f, //左上角
-1.0f,-1.0f, //左下角
1.0f,1.0f, //右上角
1.0f,-1.0f //右下角
};
相应的,对照顶点坐标,我们可以设置纹理坐标为:
private final float[] sCoord={
0.0f,0.0f,
0.0f,1.0f,
1.0f,0.0f,
1.0f,1.0f,
};
第三步,计算变换矩阵
按照上步设置顶点坐标和纹理坐标,大多数情况下我们得到的一定是一张拉升或者压缩的图片。为了让图片完整的显示,且不被拉伸和压缩,我们需要向绘制等腰直角三角形一样,计算一个合适的变换矩阵,传入顶点着色器,代码如下:
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0,0,width,height);
int w=mBitmap.getWidth();
int h=mBitmap.getHeight();
float sWH=w/(float)h;
float sWidthHeight=width/(float)height;
if(width>height){
if(sWH>sWidthHeight){
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight*sWH,sWidthHeight*sWH, -1,1, 3, 7);
}else{
Matrix.orthoM(mProjectMatrix, 0, -sWidthHeight/sWH,sWidthHeight/sWH, -1,1, 3, 7);
}
}else{
if(sWH>sWidthHeight){
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -1/sWidthHeight*sWH, 1/sWidthHeight*sWH,3, 7);
}else{
Matrix.orthoM(mProjectMatrix, 0, -1, 1, -sWH/sWidthHeight, sWH/sWidthHeight,3, 7);
}
}
//设置相机位置
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, 7.0f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
//计算变换矩阵
Matrix.multiplyMM(mMVPMatrix,0,mProjectMatrix,0,mViewMatrix,0);
}
mMVPMatrix即为我们所需要的变换矩阵。
第四步,显示图片
然后我们需要做的,就和之前绘制正方形一样容易了。和之前不同的是,在绘制之前,我们还需要将纹理和纹理坐标传入着色器:
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT|GLES20.GL_DEPTH_BUFFER_BIT);
GLES20.glUseProgram(mProgram);
onDrawSet();
GLES20.glUniformMatrix4fv(glHMatrix,1,false,mMVPMatrix,0);
GLES20.glEnableVertexAttribArray(glHPosition);
GLES20.glEnableVertexAttribArray(glHCoordinate);
GLES20.glUniform1i(glHTexture, 0);
textureId=createTexture();
//传入顶点坐标
GLES20.glVertexAttribPointer(glHPosition,2,GLES20.GL_FLOAT,false,0,bPos);
//传入纹理坐标
GLES20.glVertexAttribPointer(glHCoordinate,2,GLES20.GL_FLOAT,false,0,bCoord);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
}
public abstract void onDrawSet();
public abstract void onDrawCreatedSet(int mProgram);
private int createTexture(){
int[] texture=new int[1];
if(mBitmap!=null&&!mBitmap.isRecycled()){
//生成纹理
GLES20.glGenTextures(1,texture,0);
//生成纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture[0]);
//设置缩小过滤为使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
//设置放大过滤为使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
//设置环绕方向S,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
//设置环绕方向T,截取纹理坐标到[1/2n,1-1/2n]。将导致永远不会与border融合
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
//根据以上指定的参数,生成一个2D纹理
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
return texture[0];
}
return 0;
}
这样我们就可以显示出我们需要显示的图片,并且保证它完整的居中显示而且不会变形了,如下图: