OpenglES2.0 for Android:纹理映射
前言
纹理映射又叫做纹理贴图,是将纹理空间中的纹理像素映射到屏幕空间中的像素的过程。就是把一幅图像贴到三维物体的表面上来增强真实感,
可以和光照计算、图像混合等技术结合起来形成许多非常漂亮的效果 (百度百科)。简单来说,纹理就是一个图形或者照片,我们可以将它们
加载到Opengl中用以美化我们绘制的物体。
前期准备
我们现在准备实现这样一个功能:将一张图片贴到一个正方形中 。我们在以前画矩形的那节代码的基础上进行实现纹理贴图。这里我们新建一个项目
OpenglESRectangle ,然后将画矩形的相关代码copy过来~~,然后还记得我们绘制圆变成椭圆的问题吗,我们在这里给矩形也加入正交投影,以便来绘制
一个正方形。此时目录结构如下 :
因为加入了正交投影比起原先绘制矩形那一节的代码有些变动,发生变动的文件有 MyRender.java , Square.java ,simple_vertex_shader.glsl ,此时这三个文件代码如下:
package com.cumt.shape; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import com.cumt.openglesrectangle.R; import com.cumt.utils.ShaderHelper; import com.cumt.utils.TextResourceReader; import android.content.Context; import android.opengl.GLES20; import android.opengl.Matrix; public class Square { private Context context; //float类型的字节数 private static final int BYTES_PER_FLOAT = 4; // 数组中每个顶点的坐标数 static final int COORDS_PER_VERTEX = 2; /*------------------第一步: 修改顶点数据-------------------------*/ //矩形顶点坐标 static float squareCoords[] = { //以三角形扇的形式绘制 -0.5f, 0.5f , // top left 0.5f, 0.5f , // top right 0.5f, -0.5f , // bottom right -0.5f, -0.5f }; // bottom left private FloatBuffer vertexBuffer; //------------第一个是顶点着色器的变量名,第二个是片段着色器的变量名 private static final String A_POSITION = "a_Position"; private static final String U_COLOR = "u_Color"; private static final String U_MATRIX = "u_Matrix"; //------------获得program的ID的含义类似的 private int uColorLocation; private int aPositionLocation; private int uMatrixLocation; private int program;//保存program的id /*------------------第二步: 修改顶点个数-------------------------*/ private static final int POSITION_COMPONENT_COUNT = 4; float[] projectionMatrix = new float[16];//变换矩阵 public Square(Context context) { this.context = context; vertexBuffer = ByteBuffer .allocateDirect(squareCoords.length * BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); // 把坐标们加入FloatBuffer中 vertexBuffer.put(squareCoords); // 设置buffer,从第一个坐标开始读 vertexBuffer.position(0); getProgram(); uColorLocation = GLES20.glGetUniformLocation(program, U_COLOR); aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION); uMatrixLocation = GLES20.glGetUniformLocation(program, U_MATRIX); GLES20.glVertexAttribPointer(aPositionLocation, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, vertexBuffer); GLES20.glEnableVertexAttribArray(aPositionLocation); } //获取program private void getProgram(){ //获取顶点着色器文本 String vertexShaderSource = TextResourceReader .readTextFileFromResource(context, R.raw.simple_vertex_shader); //获取片段着色器文本 String fragmentShaderSource = TextResourceReader .readTextFileFromResource(context, R.raw.simple_fragment_shader); //获取program的id program = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource); GLES20.glUseProgram(program); } //设置正交投影矩阵 public void projectionMatrix(int width,int height){ final float aspectRatio = width > height ? (float) width / (float) height : (float) height / (float) width; if(width > height){ Matrix.orthoM(projectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); }else{ Matrix.orthoM(projectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); } } //以GL_LINE_LOOP方式绘制 public void draw(){ GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0); GLES20.glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f); /*------------------第三步: 修改绘制方式-------------------------*/ GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, POSITION_COMPONENT_COUNT); } }
package com.cumt.render; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import com.cumt.shape.Square; import android.content.Context; import android.opengl.GLSurfaceView.Renderer; import android.util.Log; import static android.opengl.GLES20.glClear; import static android.opengl.GLES20.glClearColor; import static android.opengl.GLES20.glViewport; import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT; public class MyRender implements Renderer { private Context context; public MyRender(Context context){ this.context = context; } //定义矩形对象 Square square; public void onSurfaceCreated(GL10 gl, EGLConfig config) { Log.w("MyRender","onSurfaceCreated"); // TODO Auto-generated method stub //First:设置清空屏幕用的颜色,前三个参数对应红绿蓝,最后一个对应alpha glClearColor(1.0f, 1.0f, 1.0f, 0.0f); square = new Square(context); } public void onSurfaceChanged(GL10 gl, int width, int height) { Log.w("MyRender","onSurfaceChanged"); // TODO Auto-generated method stub //Second:设置视口尺寸,即告诉opengl可以用来渲染的surface大小 glViewport(0,0,width,height); square.projectionMatrix(width, height); } public void onDrawFrame(GL10 gl) { Log.w("MyRender","onDrawFrame"); // TODO Auto-generated method stub //Third:清空屏幕,擦除屏幕上所有的颜色,并用之前glClearColor定义的颜色填充整个屏幕 glClear(GL_COLOR_BUFFER_BIT); square.draw(); } }
//simple_vertex_shader.glsl uniform mat4 u_Matrix; attribute vec4 a_Position; void main() { gl_Position = u_Matrix * a_Position; }
此时运行效果如下 :
我们要贴的图如下 (512 * 512 ) 要注意opengl es2.0中纹理不必须是正方形,但每个纬度应该是2的幂 ):
我们要将该图片贴到上面的矩形中
正式开始
下面让我们正式开始做纹理贴图 ,看下过程 :
纹理工具类
package com.cumt.utils; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.opengl.GLES20; import android.opengl.GLUtils; import android.util.Log; public class TextureHelper { public static final String TAG = "TextureHelper"; public static int loadTexture(Context context,int resourceId){ /* * 第一步 : 创建纹理对象 */ final int[] textureObjectId = new int[1];//用于存储返回的纹理对象ID GLES20.glGenTextures(1,textureObjectId, 0); if(textureObjectId[0] == 0){//若返回为0,,则创建失败 if(LoggerConfig.ON){ Log.w(TAG,"Could not generate a new Opengl texture object"); } return 0; } /* * 第二步: 加载位图数据并与纹理绑定 */ final BitmapFactory.Options options = new BitmapFactory.Options(); options.inScaled = false;//Opengl需要非压缩形式的原始数据 final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(),resourceId, options); if(bitmap == null){ if(LoggerConfig.ON){ Log.w(TAG,"ResourceId:"+resourceId+"could not be decoded"); } GLES20.glDeleteTextures(1, textureObjectId, 0); return 0; } GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureObjectId[0]);//通过纹理ID进行绑定 /* * 第三步: 设置纹理过滤 */ //设置缩小时为三线性过滤 GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR_MIPMAP_LINEAR); //设置放大时为双线性过滤 GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); /* * 第四步: 加载纹理到Opengl并返回ID */ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); bitmap.recycle(); GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); return textureObjectId[0]; } }
这里要说下纹理过滤 的内容,纹理过滤应用通常有两种情况,一种是GL_TEXTURE_MIN_FILTER ,即纹理被缩小时,比如一个512 x 512的纹理贴到一个平行于xy平面的正方形上,最后该正方形在屏幕上只占256 x 256的像素矩阵,这种情况下一个象素对应着多个纹理单元。另一种是GL_TEXTURE_MAG_FILTER,即纹理被放大时。下面简要说下Opengl的纹理过滤模式
, Opengl纹理过滤模式如下 :
----------------------------------------------------------------------------------------------------------------------------------------------------------------
GL_NEAREST 最近邻过滤
GL_NEAREST_MIPMAP_NEAREST 使用MIP贴图的最近邻过滤
GL_NEAREST_MIPMAP_LINEAR 使用MIP贴图级别之间插值的最近邻过滤
GL_LINEAR 双线性过滤
GL_LINEAR_MIPMAP_NEAREST 使用MIP贴图的双线性过滤
GL_LINEAR_MIPMAP_LINEAR 三线性过滤 (使用MIP贴图级别之间插值的双线性过滤
)
---------------------------------------------------------------------------------------------------------------------------------------------------------------
在缩小和放大情况下,Opengl支持不同的过滤模式,如下所示:
---------------------------------------------------------------------------------------------------------------------------------------------------------------
缩小情况:
GL_NEAREST
GL_NEAREST_MIPMAP_NEAREST
GL_NEAREST_MIPMAP_LINEAR
GL_LINEAR
GL_LINEAR_MIPMAP_NEAREST
GL_LINEAR_MIPMAP_LINEAR
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
放大 情况 :
GL_NEAREST
GL_LINEAR
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
最近邻过滤:这个比较简单,每个像素的纹理坐标,并不是刚好对应一个采样点时,按照最接近的采样点进行采样。当放大纹理时,锯齿会比较明显,
每个纹理单元都显示为一个小方块。当缩小时,会丢失许多细节。
双线性过滤:使用双线性插值平滑像素之间的过渡。放大时,锯齿看起来会比最近邻过滤少许多,看起来更加平滑。
MIP贴图 :当缩小到一定程度时,使用双线性过滤会失去太多细节,还可能引起噪声以及物体移动过程中的闪烁,为了克服这些问题就有了MIP贴图技术。
关于MIP贴图 ,维基上有更详细解释 :https://en.wikipedia.org/wiki/Mipmap
三线性过滤 :用于消除每个MIP贴图级别之间的过渡,得到一个更为平滑的图像。
新的着色器
在创建新的着色器之前,我们先明确一下Opengl的二维纹理坐标
对于一个二维的纹理,它有着自己的坐标空间,如上图所示。其有两个维度,S 与 T ,范围都是 0 到 1 。
顶点着色器代码如下 :
//texture_vertex_shader.glsl 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; }
其中 a_TextureCoordinates 用于接收纹理的坐标数据,v_TextureCoordinates用于将纹理数据传递给片段着色器,因为纹理有两个分量 S与 T 所以使用vec2类型。
片段着色器如下:
//texture_fragment_shader.glsl precision mediump float; uniform sampler2D u_TextureUnit; varying vec2 v_TextureCoordinates; void main() { gl_FragColor = texture2D(u_TextureUnit, v_TextureCoordinates); }
texture2D是着色器语言内置的纹理采样函数,用于根据指定的纹理坐标就行纹理采样,返回值类型为vec4 。
纹理坐标
//矩形顶点坐标 与 纹理坐标 static float squareCoords[] = { //以三角形扇的形式绘制 //x y s t -0.5f, 0.5f , 0 , 0 , // top left 0.5f, 0.5f , 1 , 0 ,// top right 0.5f, -0.5f , 1 , 1 ,// bottom right -0.5f, -0.5f , 0 , 1}; // bottom left
我们绘制的矩形的左上角对应着纹理的 (0,0 ) 大家注意这个映射关系,也就是我们拿出一张图其左上角的纹理坐标为 (0,0)而不是 (0,1)。
前面我们已经完成了纹理工具类,下面只需要使用它,然互将数据传入着色器。此时Square类代码如下 (Square.java):
package com.cumt.shape; import static android.opengl.GLES20.GL_TEXTURE0; import static android.opengl.GLES20.GL_TEXTURE_2D; import static android.opengl.GLES20.glActiveTexture; import static android.opengl.GLES20.glBindTexture; import static android.opengl.GLES20.glUniform1i; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.FloatBuffer; import com.cumt.openglesrectangle.R; import com.cumt.utils.ShaderHelper; import com.cumt.utils.TextResourceReader; import com.cumt.utils.TextureHelper; import android.content.Context; import android.opengl.GLES20; import android.opengl.Matrix; public class Square { private Context context; //float类型的字节数 private static final int BYTES_PER_FLOAT = 4; // 数组中每个顶点的坐标数 static final int COORDS_PER_VERTEX = 2; /*------------------修改顶点数据 ,加入顶点对应的纹理坐标-------------------------*/ //矩形顶点坐标 与 纹理坐标 static float squareCoords[] = { //以三角形扇的形式绘制 //x y s t -0.5f, 0.5f , 0 , 0 , // top left 0.5f, 0.5f , 1 , 0 ,// top right 0.5f, -0.5f , 1 , 1 ,// bottom right -0.5f, -0.5f , 0 , 1}; // bottom left private FloatBuffer vertexBuffer; private static final int VERTEX_COUNTS = 4;//顶点坐标数 private static final int POSITION_COMPONENT_COUNT = 2; //一个顶点坐标含有的元素个数 private static final int TEXTURE_COORDIANTES_COMPONENT_COUNT = 2; //一个纹理坐标含有的元素个数 //因为我们的顶点数据和纹理坐标数据放在了一起 ,所以在使用glVertexAttribPointer等函数时,其中的stride参数就需要传入了, //用于高速着色器应该如何读取坐标值 ,比如这里我们的着色器读取坐标时,设置从位置 0开始读,读取x , y后就会跳过 s t 接着读取 x y //这就是通过传入stride参数实现的 private static final int STRIDE = (POSITION_COMPONENT_COUNT + TEXTURE_COORDIANTES_COMPONENT_COUNT) * BYTES_PER_FLOAT; //------------第一个是顶点着色器的变量名,第二个是片段着色器的变量名 private static final String A_POSITION = "a_Position"; private static final String U_MATRIX = "u_Matrix"; private static final String A_TEXTURE_COORDINATES = "a_TextureCoordinates";//纹理 private static final String U_TEXTURE_UNIT = "u_TextureUnit";//纹理 private int aPositionLocation; private int uMatrixLocation; private int uTextureUnitLocation; private int aTextureCoordinates; private int program;//保存program的id private int texture; float[] projectionMatrix = new float[16];//变换矩阵 public Square(Context context) { this.context = context; vertexBuffer = ByteBuffer .allocateDirect(squareCoords.length * BYTES_PER_FLOAT) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); // 把坐标们加入FloatBuffer中 vertexBuffer.put(squareCoords); // 设置buffer,从第一个坐标开始读 vertexBuffer.position(0); getProgram(); aPositionLocation = GLES20.glGetAttribLocation(program, A_POSITION); uMatrixLocation = GLES20.glGetUniformLocation(program, U_MATRIX); aTextureCoordinates = GLES20.glGetAttribLocation(program, A_TEXTURE_COORDINATES); uTextureUnitLocation = GLES20.glGetAttribLocation(program, U_TEXTURE_UNIT); texture = TextureHelper.loadTexture(context, R.drawable.umei); // Set the active texture unit to texture unit 0. glActiveTexture(GL_TEXTURE0); // Bind the texture to this unit. glBindTexture(GL_TEXTURE_2D, texture); // Tell the texture uniform sampler to use this texture in the shader by // telling it to read from texture unit 0. glUniform1i(uTextureUnitLocation, 0); //传入顶点坐标和纹理坐标 GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT, false, STRIDE, vertexBuffer); GLES20.glEnableVertexAttribArray(aPositionLocation); //设置从第二个元素开始读取,因为从第二个元素开始才是纹理坐标 vertexBuffer.position(POSITION_COMPONENT_COUNT); GLES20.glVertexAttribPointer(aTextureCoordinates, TEXTURE_COORDIANTES_COMPONENT_COUNT, GLES20.GL_FLOAT, false, STRIDE, vertexBuffer); GLES20.glEnableVertexAttribArray(aTextureCoordinates); } //获取program private void getProgram(){ //获取顶点着色器文本 String vertexShaderSource = TextResourceReader .readTextFileFromResource(context, R.raw.texture_vertex_shader); //获取片段着色器文本 String fragmentShaderSource = TextResourceReader .readTextFileFromResource(context, R.raw.texture_fragment_shader); //获取program的id program = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource); GLES20.glUseProgram(program); } //设置正交投影矩阵 public void projectionMatrix(int width,int height){ final float aspectRatio = width > height ? (float) width / (float) height : (float) height / (float) width; if(width > height){ Matrix.orthoM(projectionMatrix, 0, -aspectRatio, aspectRatio, -1f, 1f, -1f, 1f); }else{ Matrix.orthoM(projectionMatrix, 0, -1f, 1f, -aspectRatio, aspectRatio, -1f, 1f); } } //以GL_LINE_LOOP方式绘制 public void draw(){ GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, VERTEX_COUNTS); } }
为了方便观察,将MyRender类onSurfaceCreated方法中设置的颜色更改为 glClearColor(0.5f,0.5f,0.5f, 1.0f);
运行一下 :