转载注明:http://www.scottcgi.com/?p=81

纹理,在openGL中,可以理解为加载到显卡显存中的图片。Android设备在2.2开始支持openGL ES2.0,从前都是ES1.0 和 ES1.1的版本。简单来说,openGL ES是为了嵌入设备进行功能剪裁后的openGL版本。ES2.0是和1.x版本不兼容的,区别和兼容性参见android 官方文档。

首先,android使用openGL提供了特殊的view作为基础叫做GLSurfaceView。我们的view需要继承GLSurfaceView。如下简单示例:

?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 public class MyGLSurfaceView extends GLSurfaceView {        public MyGLSurfaceView(Context context) {          super (context);          setFocusableInTouchMode( true );            // Tell the surface view we want to create an OpenGL ES 2.0-compatible          // context, and set an OpenGL ES 2.0-compatible renderer.          this .setEGLContextClientVersion( 2 );            this .setRenderer( new MyRenderer());      }   }

并没有什么特别之处,android view的渲染操作需要实现一个render接口,GLSurfaceView的渲染接口为android.opengl.GLSurfaceView.Renderer。我们需要实现接口的方法。

?
1 2 3 4 5 6 7 8 9 public class MyRenderer implements Renderer {   public void onDrawFrame(GL10 gl) {}   public void onSurfaceChanged(GL10 gl, int width, int height) {}   public void onSurfaceCreated(GL10 gl, EGLConfig config) {}   }

接口实现3个方法,对应绘制,绘制区域变化,区域创建。需要说明的是参数GL10 gl是openGL es1.x版本的对象。这里我们不会使用到。还有一点就是,onDrawFrame方法的调用是有系统调用的,不需要手动调用。系统会以一定的频率不断的回调。

 

接下来我们进入ES2.0的使用,先上代码:

?
1 2 3 4 5 6 7 8 9 public void onSurfaceCreated(GL10 gl, EGLConfig config) {      GLES20.glEnable(GLES20.GL_TEXTURE_2D);      // Active the texture unit 0      GLES20.glActiveTexture(GLES20.GL_TEXTURE0);        loadVertex();      initShader();      loadTexture(); }

绘制区域创建的时候,我们设置了启用2D的纹理,并且激活了纹理单元unit0。什么意思呢,说起来话长,以后慢慢说。简单说一下,记住openGL是基于状态的,就是很多状态的设置和切换,这里启用GL_TEXTURE_2D就是一个状态的开启,表明openGL可以使用2D纹理。

那神马是激活纹理单元,这个和硬件有点关系,openGL要显卡会划分存储纹理的存储区域不止一个区域。这里是使用区域 unit 0,多重纹理绘制可以开启多个,这个以后说。接下来,调用了三个函数,载入顶点,初始化着色器,载入纹理。

第一,载入顶点,openGL绘制图形是根据顶点以后链接起来的。为什么要这样,其实这样很强大是一种设计吧。顶点可以暂时简单理解为含有位置信息的坐标点。展开代码如下:

?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 private void loadVertex() {      // float size  = 4      this .vertex = ByteBuffer.allocateDirect(quadVertex.length * 4 )                              .order(ByteOrder.nativeOrder())                              .asFloatBuffer();        this .vertex.put(quadVertex).position( 0 );        // short size = 2      this .index = ByteBuffer.allocateDirect(quadIndex.length * 2 )                             .order(ByteOrder.nativeOrder())                             .asShortBuffer();        this .index.put(quadIndex).position( 0 ); }   private FloatBuffer vertex; private ShortBuffer index;   private float [] quadVertex = new float [] {          - 0 .5f, 0 .5f, 0 .0f, // Position 0          0 , 1 .0f, // TexCoord 0            - 0 .5f, - 0 .5f, 0 .0f, // Position 1          0 , 0 , // TexCoord 1            0 .5f , - 0 .5f, 0 .0f, // Position 2          1 .0f, 0 , // TexCoord 2            0 .5f, 0 .5f, 0 .0f, // Position 3          1 .0f, 1 .0f, // TexCoord 3 };   private short [] quadIndex = new short [] {          ( short )( 0 ), // Position 0          ( short )( 1 ), // Position 1          ( short )( 2 ), // Position 2            ( short )( 2 ), // Position 2           ( short )( 3 ), // Position 3           ( short )( 0 ), // Position 0 };

FloatBuffer,ShortBuffer是封装了本地数据结构的封装对象。是的,这个2个对象里面的数据不被java虚拟机管理,相当于C语言的存储方式。具体的介绍可以参看这里(想了解的猛击)。 quadVertex的数据就是一个矩形的坐标,和纹理坐标。一两句话很难解释清楚,这里涉及到openGL的几个经典的坐标系,下次说。概括的说,openGL的坐标是单位化的,都是0.0-1.0的浮点型,屏幕的中心点是(0,0)。而纹理的坐标左下角是(0,0)。 这里的quadVertex是在屏幕中大概花了一个矩形贴了一个图片, position0 是左上点,以后左下,右下,右上的顺序,纹理坐标同理。

quadIndx神马意思呢,就是这刚才的这些顶点索引排列。这里一个矩形也就4个顶点,每个顶点3个位置坐标,2个纹理坐标。也就是说一个顶点有5个float数据。至于为什么顶点为什么这么排列下次说,是2个三角形合成了一个矩形,几句话很难解释清楚。

所以说,这段代码就是把矩形的位置和纹理坐标,存储到本地数据,准备后面使用而已。

 

第二,初始化着色器。这个着色器就是ES2.0的特色,又叫可编程着色器,也是区别于ES1.x的本质。这里只做简单的介绍。可编程着色器是一种脚本,语法类似C语言,脚本分为顶点着色器和片段着色器,分别对应了openGL不同的渲染流程。如下:

顶点着色器:

?
01 02 03 04 05 06 07 08 09 10 11 12 uniform mat4 u_MVPMatrix;   attribute vec4 a_position; attribute vec2 a_texCoord;   varying vec2 v_texCoord;     void main()   {      gl_Position = a_position;      v_texCoord  = a_texCoord;     }

片段着色器:

?
1 2 3 4 5 6 7 8 9 precision lowp float;         varying vec2 v_texCoord;                       uniform sampler2D u_samplerTexture;   void main()                                          {                                                       gl_FragColor = texture2D(u_samplerTexture, v_texCoord); }

这里记住一句话,顶点着色器,会在顶点上执行;片段着色器会在像素点上执行。刚才的矩形就有4个顶点,每个顶点都会应用这个脚本。也就是说,顶点是位置相关信息,片段是色彩纹理相关信息。

这个2段脚本都是文本,需要编译,链接,等等一些操作才能被ES2.0所使用。过程就像C语言的编译运行过程。openGL 提供了相关函数去做这些事情。如下:

?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 private void initShader() {      String vertexSource   = Tools.readFromAssets( "VertexShader.glsl" );      String fragmentSource = Tools.readFromAssets( "FragmentShader.glsl" );        // Load the shaders and get a linked program      program = GLHelper.loadProgram(vertexSource, fragmentSource);        // Get the attribute locations      attribPosition = GLES20.glGetAttribLocation(program, "a_position" );      attribTexCoord = GLES20.glGetAttribLocation(program, "a_texCoord" );        uniformTexture = GLES20.glGetUniformLocation(program, "u_samplerTexture" );        GLES20.glUseProgram(program);      GLES20.glEnableVertexAttribArray(attribPosition);      GLES20.glEnableVertexAttribArray(attribTexCoord);      // Set the sampler to texture unit 0      GLES20.glUniform1i(uniformTexture, 0 ); }

可以看到,顶点和片段一起构成一个program,它可以被openGL所使用,是一个编译好的脚本程序,存储在显存。 GLES20.glGetAttribLocation 和 GLES20.glGetUniformLocation 这句话是神马作用呢。简单说就是,java程序和着色器脚本数据通信的。把就像参数的传递一样,这样脚本就能根据外界的参数变化,实时的改变openGL流水线渲染的处理流程。

以下是我封装的载入着色器的辅助方法:

?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public static int loadProgram(String vertexSource, String fragmentSource) {      // Load the vertex shaders      int vertexShader = GLHelper.loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);        // Load the fragment shaders      int fragmentShader = GLHelper.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);        // Create the program object      int program = GLES20.glCreateProgram();        if (program == 0 ) {          throw new RuntimeException( "Error create program." );      }        GLES20.glAttachShader(program, vertexShader);      GLES20.glAttachShader(program, fragmentShader);        // Link the program      GLES20.glLinkProgram(program);        int [] linked = new int [ 1 ];        // Check the link status      GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linked, 0 );        if (linked[ 0 ] == 0 ) {          GLES20.glDeleteProgram(program);          throw new RuntimeException( "Error linking program: " +  GLES20.glGetProgramInfoLog(program));      }        // Free up no longer needed shader resources      GLES20.glDeleteShader(vertexShader);      GLES20.glDeleteShader(fragmentShader);        return program; }
?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static int loadShader( int shaderType, String source) {        // Create the shader object      int shader = GLES20.glCreateShader(shaderType);        if (shader == 0 ) {          throw new RuntimeException( "Error create shader." );      }        int [] compiled = new int [ 1 ];        // Load the shader source      GLES20.glShaderSource(shader, source);        // Compile the shader      GLES20.glCompileShader(shader);        // Check the compile status      GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0 );        if (compiled[ 0 ] == 0 ) {          GLES20.glDeleteShader(shader);          throw new RuntimeException( "Error compile shader: " + GLES20.glGetShaderInfoLog(shader));      }        return shader; }

为什么openGL的很多操作目标都是int类型的,因为openGL只会在显存生成或绑定地址,返回id,以后用id相当于句柄去改变它的内部状态。

 

第三,就是载入纹理了。载入纹理,就是把图片的数据上传到显存,以后在使用它。请注意纹理图片的长和宽最好是2的N次方,不然不一定能绘制出来。

?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 static int [] loadTexture(String path) {       int [] textureId = new int [ 1 ];         // Generate a texture object       GLES20.glGenTextures( 1 , textureId, 0 );         int [] result = null ;         if (textureId[ 0 ] != 0 ) {             InputStream is = Tools.readFromAsserts(path);             Bitmap bitmap;           try {               bitmap = BitmapFactory.decodeStream(is);           } finally {               try {                   is.close();               } catch (IOException e) {                   throw new RuntimeException( "Error loading Bitmap." );               }           }             result = new int [ 3 ];           result[TEXTURE_ID] = textureId[ 0 ]; // TEXTURE_ID           result[TEXTURE_WIDTH] = bitmap.getWidth(); // TEXTURE_WIDTH           result[TEXTURE_HEIGHT] = bitmap.getHeight(); // TEXTURE_HEIGHT             // Bind to the texture in OpenGL           GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[ 0 ]);             // Set filtering           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_NEAREST);             GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);           GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);             // Load the bitmap into the bound texture.           GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0 , bitmap, 0 );             // Recycle the bitmap, since its data has been loaded into OpenGL.           bitmap.recycle();         } else {           throw new RuntimeException( "Error loading texture." );       }         return result;   }

代码一目了然,这里使用了android的工具类吧bitmap直接转换成openGL纹理需要的格式了。过程是,先生成一个纹理的id在显卡上的,以后根据id上传纹理数据,以后保存这个id就可以操作这个纹理了。至于纹理的一些过滤特性设置,下次再说。

 

现在貌似就剩下绘制了,准备好了顶点信息,顶点对应的纹理坐标。初始化了着色器,上传了纹理图片。接下来就已把他们合起来绘制了。

?
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void onDrawFrame(GL10 gl) {      // clear screen to black      GLES20.glClearColor( 0 .0f, 0 .0f, 0 .0f, 0 .0f);      GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);        vertex.position( 0 );      // load the position      // 3(x , y , z)      // (2 + 3 )* 4 (float size) = 20      GLES20.glVertexAttribPointer(attribPosition,                                   3 , GLES20.GL_FLOAT,                                   false , 20 , vertex);        vertex.position( 3 );      // load the texture coordinate      GLES20.glVertexAttribPointer(attribTexCoord,                                    2 , GLES20.GL_FLOAT,                                    false , 20 , vertex);        GLES20.glDrawElements(GLES20.GL_TRIANGLES, 6 , GLES20.GL_UNSIGNED_SHORT, index); }

我尽力保持了代码的简单,openGL的基于状态体现,bind这个函数无处不在,这里bindTexture就是通知openGL使用那个id的纹理图片。接下来的操作就是针对bind的图片的。绘制就需要让openGL知道绘制神马。所以这里需要用到vertex这个本地数据容器,里面装在的是顶点和纹理坐标信息。 GLES20.glVertexAttribPointer就是把顶点数据,按照openGL喜欢的格式上传到显卡存储。draw方法的调用,是在前面应用了纹理id的情况下,所以绘制纹理坐标的时候,会使用上传的纹理图片。

是的,每次都需要把数据上传到openGL,毕竟显存和内存不是同一个地方,openGL采用了客户端-服务端的设计模式。当然使用VBO等技术可以把数据缓存在显存,提高运行性能。这个以后再说吧。

这是运行截图,可以看到纹理是反的,这是我故意的设置。android中如果顶点坐标和纹理坐标一致,那么图片绘制出来就是反过来的。因为绘制是屏幕坐标系,和openGL坐标系的Y是反过来的。openGL几个坐标系,和屏幕坐标系的关系以后再说。

更多相关文章

  1. 专利敲诈——微软对Android痛下杀手的背后
  2. 【Android开发学习43】OpenGL ES教程VI之纹理贴图(原文对照)
  3. 【更新】Google 与微软开始口水战
  4. 微软一年通过Android获得几十亿美元收入,没错,是微软!
  5. android纹理图片的加载与修改
  6. 不仅是微软和诺基亚,谁都无法 fork Android,因为它就没法 fork
  7. Android:微软的金钱机器(更新)
  8. OpenGL ES教程VI之纹理贴图(原文对照)
  9. 【Android开发学习15】Android OpenGL ES 纹理映射之glDrawArray

随机推荐

  1. Android: 获得API level
  2. Android用Spinner做日期 年月日
  3. android 动画2
  4. 第四课-Log的使用
  5. 播放系统铃音 android
  6. Android登录界面开发及响应;页面跳转;传参
  7. Android 学习记录-调试输出
  8. Android touch event
  9. Android(安卓)6.0中的新技术总结
  10. android使用HTTP协议读取数据