Android OpenGL ES(三)-平面图形

上一章Android OpenGL ES(二)-正交投影
的学习,我们已经能够画正常的图片图形了,这章我们会继续来绘制正方形和圆的这样的平面图形和绘制纹理。

平面图形


前两章,其实我们已经明白了绘制平面图形的套路了。
接下来我们按照套路继续画其他的图形。

正方形

因为OpenGL只提供给我们画三角形的方式,所以想要正方形的话,其实就是画两个三角形拼在一起。

一:使用GL_TRIANGLE_STRIP的方式

  • 绘制方式-三角形带
    GL_TRIANGLE_STRIP.png

    如上图。使用GLES20.GL_TRIANGLE_STRIP可以在定义3个点的确定三角形的情况下,每多一个点,就多绘制一个三角形。这种方式需要注意数组中点的顺序。

1. 修改矩阵的数组。将其改成正方形
按照上图中的顺序定制我们的矩阵数组。

 private static float SQUARE_COLOR_COORDS[] = {
            //Order of coordinates: X, Y, Z, R,G,B,
            -0.5f, 0.5f, 0.0f, 1.f, 0f, 0f,  //  0.top left RED
            -0.5f, -0.5f, 0.0f, 0.f, 0f, 1f, //  1.bottom left Blue
            0.5f, 0.5f, 0.0f, 1f, 1f, 1f,   //  3.top right WHITE
            0.5f, -0.5f, 0.0f, 0.f, 1f, 0f,  //  2.bottom right GREEN
    };

2. 调用onDrawFrame内调用我们的代码

  //在OnDrawFrame中进行绘制
    @Override
    public void onDrawFrame(GL10 gl) {
        super.onDrawFrame(gl);

        //传递给着色器
        GLES20.glUniformMatrix4fv(uMatrix, 1, false, mProjectionMatrix, 0);

        //1.使用三角形带的方式
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0, VERTEX_COUNT);
    }

3. 结果

正方形.png

二:使用GL_TRIANGLES_FAN的方式

将传入的顶点作为扇面绘制,ABCDEF绘制ABC、ACD、ADE、AEF四个三角形.

1. 更新正方形的矩阵数组

//0,1,2 是一个三角形 //0,2,3 是一个三角形
private static float SQUARE_COLOR_COORDS[] = {
            //Order of coordinates: X, Y, Z, R,G,B,
            -0.5f, 0.5f, 0.0f, 1.f, 0f, 0f,  //  0.top left RED
            0.5f, 0.5f, 0.0f, 1f, 1f, 1f,   //  1.top right WHITE
            0.5f, -0.5f, 0.0f, 0.f, 1f, 0f,  //  2.bottom right GREEN
            -0.5f, -0.5f, 0.0f, 0.f, 0f, 1f, //  3.bottom left Blue
    };

2. 更新绘制的方法

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, VERTEX_COUNT);

3. 结果

正方形2.png

三:使用GL_TRIANGLES和顶点矩阵数组加位置矩阵数组的方式

这种方法就是根据我们的数组,自己来定义绘制的顺序来,完成绘制两个三角形完成正方形的任务。

1. 更新数组的数据

  //正方形的点1
    private static float SQUARE_COLOR_COORDS[] = {
            //Order of coordinates: X, Y, Z, R,G,B,
            -0.5f, 0.5f, 0.0f, 1.f, 0f, 0f,  //  0.top left RED
            -0.5f, -0.5f, 0.0f, 0.f, 0f, 1f, //  1.bottom right Blue
            0.5f, 0.5f, 0.0f, 1f, 1f, 1f,   //  3.top right WHITE
            0.5f, -0.5f, 0.0f, 0.f, 1f, 0f,  //  2.bottom left GREEN
    };

    /*
     创建一个遍历的点的顺序.
    1,0,2,1 一个三角形
    1,2,3,1 另一个三角
    */
    private static short SQUARE_INDEX[] = {
            1 , 0, 2, 1, 2, 3
    };

2. 添加indexBuffer
我们同样需要为我们新添加的位置数组的分配内存,让OpenGL来读取。

//        /*
//        新增-为位置添加内存空间
//         */
        mIndexBuffer = ByteBuffer
                .allocateDirect(SQUARE_INDEX.length * Constant.BYTES_PER_SHORT)
                .order(ByteOrder.nativeOrder())
                .asShortBuffer()
                .put(SQUARE_INDEX);
        mIndexBuffer.position(0);

3. 修改绘制的方法

  //使用indexBuffer的方式
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, SQUARE_INDEX.length, GLES20.GL_UNSIGNED_SHORT, mIndexBuffer);

4. 结果

indexBuffer正方形.png

正方形小节

这里我们一共使用三种方式进行绘制

  • GL_TRIANGLES
    将传入的顶点作为单独的三角形绘制,ABCDEF绘制ABC,DEF两个三角形
  • GL_TRIANGLE_FAN
    将传入的顶点作为扇面绘制,ABCDEF绘制ABC、ACD、ADE、AEF四个三角形
  • GL_TRIANGLE_STRIP
    将传入的顶点作为三角条带绘制,ABCDEF绘制ABC,BCD,CDE,DEF四个三角形

绘制圆形。是通过绘制切分的三角形来形成的。三角形切分的越细,越接近圆。

1.更新代表圆形的矩阵数组

  • 计算绘制圆所需要的点
    因为是通过切分的方式来构成圆。所以我们需要先确定需要一个圆需要多少个点来绘制。

      /*
      需要的点的个数等于 1(圆心)+切分圆的点数+1(为了闭合,切分圆的起点和终点,需要重复一次)
       */
      private int getCircleVertexNum(int numbersRoundCircle) {
          return +1 + numbersRoundCircle + 1;
      }
    

    通过上面画正方形的经历,我们知道了GL_TRIANGLE_FAN绘制扇形的顺序。所以我们需要
    a. 先传入一个圆形。
    b. 然后按照我们的切分点开始绘制若干个三角形。最后一个三角形闭合,
    c. 还需要重复一次起点和终点。

  • 计算绘制圆上切分点的坐标


    image.png

按照上图和我们的切分点,计算每一个点的坐标,放到数组里面。构造出来的数组属性是X,Y,Z,R,G,B

private float[] createCircleCoords(Circle circle, int numbersRoundCircle) {
      //先计算总共需要多少个点
      int needNumber = getCircleVertexNum(numbersRoundCircle);
      //创建数组
      float[] circleColorCoord = new float[needNumber * TOTAL_COMPONENT_COUNT];
      //接下来给每个点分配数据

      //对每一组点进行赋值
      for (int numberIndex = 0; numberIndex < needNumber; numberIndex++) {
          int indexOffset = numberIndex * TOTAL_COMPONENT_COUNT;

          if (numberIndex == 0) {   //第一个点。就是圆心
              //位置
              circleColorCoord[indexOffset] = circle.center.x;
              circleColorCoord[indexOffset + 1] = circle.center.y;
              circleColorCoord[indexOffset + 2] = circle.center.z;

              //下面是颜色。给一个白色
              circleColorCoord[indexOffset + 3] = 1.f;
              circleColorCoord[indexOffset + 4] = 1.f;
              circleColorCoord[indexOffset + 5] = 1.f;
          } else if (numberIndex < needNumber - 1) {    //切分圆的点
              //需要根据半径。中心点。来结算
              int angleIndex = numberIndex - 1;
              float angleRadius = (float) (((float) angleIndex / (float) numbersRoundCircle) * Math.PI * 2f);
              float centerX = circle.center.x;
              float centerY = circle.center.y;
              float centerZ = circle.center.z;
              float radius = circle.radius;
              float tempX = (float) (centerX + radius * Math.cos(angleRadius));
              float tempY = (float) (centerY + radius * Math.sin(angleRadius));
              float temp = centerZ + 0;

              //位置

              circleColorCoord[indexOffset] = tempX;
              circleColorCoord[indexOffset + 1] = tempY;
              circleColorCoord[indexOffset + 2] = temp;

              //下面是颜色。给一个白色
              circleColorCoord[indexOffset + 3] = (float) (1.f* Math.cos(angleRadius));
              circleColorCoord[indexOffset + 4] = (float) (1.f* Math.sin(angleRadius));
              circleColorCoord[indexOffset + 5] = 1.f;
          } else { //最后一个点了。重复数据中的二组的位置
              //位置.index为1的点
              int copyTargetIndex = 1;
              //复制点
              circleColorCoord[indexOffset] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT];
              circleColorCoord[indexOffset + 1] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 1];
              circleColorCoord[indexOffset + 2] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 2];

              circleColorCoord[indexOffset + 3] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 3];
              circleColorCoord[indexOffset + 4] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 4];
              circleColorCoord[indexOffset + 5] = circleColorCoord[copyTargetIndex * TOTAL_COMPONENT_COUNT + 5];
          }

      }
      return circleColorCoord;
  }

这样就更新好我们的矩阵数组了。

2. 绘制

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, VERTEX_COUNT);

3. 结果

circle.png

小节

画圆就是熟练的应用了正方形的经验。

纹理


除了平面图形。我们还能绘制我们自己的2D纹理。

OpenGL中的纹理可以用来表示图像。照片甚至数学算法生成的分形数据。
每个二维的纹理都由许多小的纹理元素(text1)组成。它们是小块的数据。

理解纹理的坐标

每个二维的纹理都有自己的坐标空间。其范围是从一个拐角(0,0)到另外一个拐角(1,1)。一个纬度叫做S,而另一个拐角叫做T.

二维纹理坐标.png

对比Android系统的Y轴
android系统中的y轴也是向下的。但是纹理坐标是向上的。

纹理的大小
在标准的OpenGL ES 2.0中,纹理不必是正方形。但是每个纬度都应该是2的幂。POT纹理适用于各种情况。
纹理也有一个最大值,但是会根据不同的实现而变化。

理解纹理过滤模式

当我们渲染表面上绘制一个纹理时,那个纹理的纹理元素可能无法精确的映射到OpenGL生成的片段上。由两种情况:缩小或者放大。通过纹理过滤(texture filtering),来控制产生的效果。

  • 放大
    一个纹理放到多个片段时,就会放大。
  • 缩小
    当我们尽力把几个纹理元素放到一个片段时,缩小发生
最近邻过滤
最近邻过滤.png
  • 缺点
    放大时,锯齿相当的明显。
双线性过滤

双线性过滤会进行插值。


双线性过滤.png
  • 优点
    适合于放大的情况
  • 缺点
    不适合缩小。因为OpenGL的双线性过滤只给每个片段使用四个纹理元素。我们会失去很多细节。
MIP 贴图

可以生成一组优化过的不同大小的纹理。当生成这组纹理的时候。OpenGL会使用所有的纹理元素生成每个级别的纹理,当过滤纹理时,还要确保所有的纹理元素能被使用。在渲染时,会更具每个片段的纹理元素数量为每个片段选择最合适的级别。

  • 缺点
    会占用很多内存,但是有点
  • 优点
    同时渲染也会更快。是因为在较小的界别的纹理在GPU的纹理缓存中占用较少的空间。
三线性过滤

如果OpenGL在不同的MIP贴图级别中来回切换。当我们用双线性过滤使用MIP贴图时,再起渲染的场景中,在不同级别的切换时,就会看到明显的跳跃。我们可以切换到三线性过滤。告诉OpenGL 两个最邻近的MIP贴图级别之间也要插值。这样每个片段总共要使用8个纹理元素插值。有助于消除每个MIP贴图级别中间的过渡。得到一个更平滑的图像。

过滤模式总结

过滤模式.png

纹理绘制代码

1. 更新着色器
  • 顶点着色器
    在顶点着色器中添加 attribute的location a_TextureCoordinates(纹理的坐标)属性和varying型的变量v_TextureCoordinates
    attribute vec4 a_Position;
    //添加了一个 a_TextureCoordinates ,因为他有两个分量。S坐标和T  坐标,所以定义为vec2.
    attribute vec2 a_TextureCoordinates;
    uniform mat4 u_Matrix;
    //然后把坐标传递给被插值的varying
    varying vec2 v_TextureCoordinates;
    
    void main(){
      gl_Position=u_Matrix*a_Position;
      v_TextureCoordinates=a_TextureCoordinates;
    }
    
  • 片段着色器
    在片段着色器需要添加sampler2D采样器u_TextureUnit,并应用v_TextureCoordinates纹理坐标系
    precision mediump float;
    
    //在片元着色器这里添加这个 sampler2D 表示我们要添加2D贴图
    uniform sampler2D u_TextureUnit;
    varying vec2 v_TextureCoordinates;
    
    void main(){
        //渲染2D纹理,交给fragColor
        gl_FragColor=texture2D(u_TextureUnit,v_TextureCoordinates);
    }
    
2. 更新代码
  • 更新矩阵数组
    在这里,我们把OpenGL代表屏幕的X,y坐标和代表纹理的S.T坐标都放到数组中。
    这里需要注意的是,我们从上面知道。Android屏幕的Y坐标是向下的和而纹理中的T坐标是向上的,所以表达同一个点的Y坐标T坐标是相反的!

     //顶点的坐标系
      private static float TEXTURE_COORDS[] = {
              //Order of coordinates: X, Y,S,T
              -1.0f, 1.0f, 0.0f, 0.0f,
              -1.0f, -1.0f, 0.0f, 1.0f, //bottom left
              1.0f, 1.0f, 1.0f, 0.0f, // top right
              1.0f, -1.0f, 1.0f, 1.0f, // bottom right
      };
    
  • 将数组传递给OpenGL

     private static final String A_COORDINATE =   "a_TextureCoordinates";
      private static final String U_TEXTURE = "u_TextureUnit";
    
      //取到这个属性值和应用。偏移到我们的ST坐标使用。
     int aCoordinate = GLES20.glGetAttribLocation(mProgramObjectId, A_COORDINATE);
          mVertexFloatBuffer.position(COORDS_PER_VERTEX);
          GLES20.glVertexAttribPointer(
                  aCoordinate,
                  COORDS_PER_ST,
                  GLES20.GL_FLOAT, false,
                  STRIDE,
                  mVertexFloatBuffer);
    GLES20.glEnableVertexAttribArray(aCoordinate);
       uTexture = GLES20.glGetUniformLocation(mProgramObjectId, U_TEXTURE);
    //生成纹理ID
     mTextureId = createTexture();
    
  • 生成纹理ID
    纹理Id就相当于纹理内容在内存中索引。我们后面可以通过这个id,继续操作我们绑定的纹理。
    这里需要注意的是,图片的解码,需要将scale属性为false。如果不做这个操作的话,解码出来的图片,是放大了很多倍的。而我们的ST坐标,只能对应到一个很小的部分。
    这里的代码也是经典的流程。注意注释中提到的调用的流程。

     //使用mip贴图来生成纹理,相当于将图片复制到openGL里面?
      private int createTexture() {
           final Bitmap mBitmap;
          BitmapFactory.Options options = new BitmapFactory.Options();
          options.inScaled = false;
          //加载Bitmap
          mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher, options);
          //保存到textureObjectId
          int[] textureObjectId = new int[1];
          if (mBitmap != null && !mBitmap.isRecycled()) {
              //生成一个纹理,保存到这个数组中
              GLES20.glGenTextures(1, textureObjectId, 0);
              //绑定GL_TEXTURE_2D
              GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectId[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);
    
              //回收释放
              mBitmap.recycle();
              //因为我们已经复制成功了。所以就进行解除绑定。防止修改
              GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
              return textureObjectId[0];
          }
          return 0;
    }
    
  • 绘制
    onDrawFrame方法中,重新激活和绑定。然后调用画出定义的矩阵就可以了

     //绑定和激活纹理
          //因为我们生成了MIP,放到了GL_TEXTURE0 中,所以重新激活纹理
          GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
          //重新去半丁纹理
          GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
    
    
          //设置纹理的坐标
          GLES20.glUniform1i(uTexture, 0);
    
          //绘制三角形.
          //draw arrays的几种方式 GL_TRIANGLES三角形 GL_TRIANGLE_STRIP三角形带的方式(开始的3个点描述一个三角形,后面每多一个点,多一个三角形) GL_TRIANGLE_FAN扇形(可以描述圆形)
          GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, VERTEX_COUNT);
    
    
  • 结果


    image.png

纹理小节

  • 这里需要注意的还是纹理的整体过程。后续的使用中,还是会重复使用这一的流程。
  • 纹理的Id,在这里就是代表我们复制到内存中的bitmap
  • 绘制的时候,重新绑定绘制就可以了

总结

总结一下,我们从这第一章节的内容了解到了下面这些使用的知识点:

  1. 绘制正方形的多种方式和绘制圆的方式。熟悉了GL的绘制方法。
  2. 纹理的基础概念
  3. 绘制一个纹理的基本套路

下一章开始,我们会进入Android的相机和OpenGL的结合。
相机部分结束之后,才会到三维图形的部分。

整体的代码位置:https://github.com/deepsadness/OpenGLDemo5

系列文章地址
Android OpenGL ES(一)-开始描绘一个平面三角形
Android OpenGL ES(二)-正交投影
Android OpenGL ES(三)-平面图形
Android OpenGL ES(四)-为平面图添加滤镜
Android OpenGL ES(五)-结合相机进行预览/录制及添加滤镜
Android OpenGL ES(六) - 将输入源换成视频
Android OpenGL ES(七) - 生成抖音照片电影

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容