美文网首页
音视频开发之旅(九) OpenGL ES 绘制平面图形

音视频开发之旅(九) OpenGL ES 绘制平面图形

作者: yabin小站 | 来源:发表于2020-11-05 00:47 被阅读0次

    目录

    1. 写着色器代码
    2. 通过GLSurfaceview加载Shader并运行
    3. 遇到的问题
    4. 参考
    5. 收获

    我们前两篇介绍了OpenGL ES 基本概念GLSL及Shader的渲染流程,这篇我们开始实战,通过GLSurfaceView加载着色器,来绘制三角形、正方形和直线这些平面图形。在实践过程中遇到的问题有时候让人没有头绪,检查了一遍又一遍代码,发现流程没有问题,但屏幕就是一片漆黑。。通过近一个小时的排查,发现问题出在了这里。。。下面开始我们今天的学习时间之旅,希望对你也有帮助。

    GLSL着色器的编写

    如果对OpenGL的基本概念以及渲染流程不清晰的,建议看下前两篇文章,这些基本概念和流程要了解或者理解,否则后面实践之旅就是跳坑之旅。

    我们先通过下面重要的二张图,快速回顾下


    工欲善其事,必先利其器,如何方便的编写GLSL代码呐? AndoridStudio提供了“Support for GLSL”插件。VS Code也有比较强大的插件比如“Shader Toy
    ”和“Shader languages support for VS Code”。但是都不足之处,就是没有自动提示和补全的功能。所以编写GLSL代码是要细心。

    1.1 着色器代码的编写

    首先我们来编写下顶点着色器和片元着色器

    //vertex_shader.glsl 顶点着色器
    
    attribute vec4 a_Position;
    attribute vec4 a_Color;
    
    varying vec4 v_Color;
    
    void main() {
        v_Color = a_Color;
        gl_Position = a_Position;
    }
    

    上述代码简单语法回顾

    attribute是修饰符 只能用于顶点着色器,用于修饰可变的参数
    vec4: 浮点型向量
    
    gl_Position:内置变量
    
    varying:也是一个修饰符,用于顶点着色器和片元着色器的值传递。
    注意:必须要在顶点着色器和片元着色器都定义同名同类型同varying修饰符的变量,才能正常传递。
    
    //fragment_shader.glsl 片元着色器
    
    precision mediump float;
    
    varying vec4 v_Color;
    
    void main() {
        gl_FragColor = v_Color;
    }
    

    这里对精度precision mediump 做下说明
    用于修饰浮点型和整形,有三个等级 highp\mediump、lowp


    1.2 着色器代码的读取

    着色器代码通常放在assets目录下或者raw目录下,当然也见到过直接写在代码里。我们为了方便,采用比较通用的方式:把glsl代码文件放在了assets下,再加载前需要先把他们读到内存中,常规的文件读写

    public static String loadAsset(Resources res, String path) {
         StringBuilder stringBuilder = new StringBuilder();
         try {
             InputStream is = res.getAssets().open(path);
    
             byte[] buffer = new byte[1024];
             int count;
             while (-1 != (count = is.read(buffer))) {
                 stringBuilder.append(new String(buffer, 0, count));
             }
             String result = stringBuilder.toString().replaceAll("\\r\\n", "\n");
            return result;
         } catch (IOException e) {
             e.printStackTrace();
         }
         return "";
     }
    

    1.3 着色器的创建、设置源码、编译

    private static int loadShader(int type, String codeStr) {
            //1. 根据类型(顶点着色器、片元着色器)创建着色器,拿到着色器句柄
            int shader = GLES20.glCreateShader(type);
            Log.i(TAG, "compileShaderCode: type=" + type + " shaderId=" + shader);
    
            if (shader > 0) {
                //2. 设置着色器代码 ,shader句柄和code进行绑定
                GLES20.glShaderSource(shader, codeStr);
                //3. 编译着色器,
                GLES20.glCompileShader(shader);
    
                //4. 查询编译状态
                int[] status = new int[1];
                GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);
                Log.i(TAG, "loadShader: status[0]=" + status[0]);
                //如果失败,释放资源
                if (status[0] == 0) {
                    GLES20.glDeleteShader(shader);
                    return 0;
                }
            }
            return shader;
        }
    

    1.4 程序的创建、attach着色器、链接、使用

    public static int loadProgram(String verCode, String fragmentCode) {
            //1. 创建Shader程序,获取到program句柄
            int programId = GLES20.glCreateProgram();
            if(programId == 0){
                Log.e(TAG, "loadProgram: glCreateProgram error" );
                return 0;
            }
            //2. 根据着色器语言类型和代码,attach着色器
            GLES20.glAttachShader(programId, loadShader(GLES20.GL_VERTEX_SHADER, verCode));
            GLES20.glAttachShader(programId, loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentCode));
            //3. 链接
            GLES20.glLinkProgram(programId);
            //4. 使用
            GLES20.glUseProgram(programId);
            return programId;
        }
    

    1.5 状态查询

    1. 着色器Shader和Program创建后会拿到对应的句柄,通过检查是否大于0验证是否可用

    2. GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0);着色器编译后检查编译的状态是否大于0判断可用性。

    3. glGetProgramiv(programObjectId, GL_VALIDATE_STATUS,
      validateStatus, 0);//程序链接后,检查程序的可用性

    1.6 输入与输出

    输入:给着色器赋值
    输出:在屏幕上显示

    前面5个步骤把准备工作都做好了,那边现在面临两个问题.

    1. 我们看到顶点着色器中有两个attribute修饰的变量,如何给它们赋值?
    2. 如何把着色器再屏幕上绘制出来?

    这个环节我们就来解决这两个问题

    首先定义好顶点坐标和颜色的位数和着色器的数据

        //每个顶点坐标的个数
        private final static int COORDS_PER_VERTEX = 2;
    
        //每个顶点颜色的个数
        private final static int COLOR_PER_VERTEX = 3;
        
        // 浮点类型占用的字节数
        private final static int BYTES_PER_FLOAT = 4;
        
        //下面两个字符串常量就是GLSL顶点着色器的输入
    
        private static final String A_POSITION = "a_Position";
        private static final String A_COLOR = "a_Color";
    
        //STRIDE是一个顶点的字节偏移(顶点坐标xy+颜色rgb)
        private final int STRIDE = (COORDS_PER_VERTEX+ COLOR_PER_VERTEX )* BYTES_PER_FLOAT;
    
        private FloatBuffer mVertexData;
    
        public MyRender2() {
            //顶点数组
            float[] TRIANGLE_COORDS = {
                    0.5f, 0.5f,1f, 0.5f,0.5f,
                    -0.5f, -0.5f,0.5f, 1f,0.5f,
                    0.5f, -0.5f,0.5f, 0.5f,1f
    
            };
    
           //通过nio ByteBuffer把设置的顶点数据加载到内存
            mVertexData = ByteBuffer
                    .allocateDirect(TRIANGLE_COORDS.length * BYTES_PER_FLOAT) //需要多少字节内存
                    .order(ByteOrder.nativeOrder())//大小端排序
                    .asFloatBuffer()
                    .put(TRIANGLE_COORDS);//设置数据
        }
    

    然后给顶点着色器的变量赋值

     String vertexCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "vertex_shader.glsl");
            String fragmentCode = ShaderHelper.loadAsset(MyApplication.getContext().getResources(), "fragment_shader.glsl");
            //创建着色器程序
            programId = ShaderHelper.loadProgram(vertexCode, fragmentCode);
    
    
            int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION);
            Log.i(TAG, "drawFrame: aposition="+aPosition);
            mVertexData.position(0);
            GLES20.glVertexAttribPointer(aPosition,
                    COORDS_PER_VERTEX,//用几个偏移描述一个顶点
                    GLES20.GL_FLOAT,//顶点数据类型
                    false,
                    STRIDE,//一个顶点需要多少个字节偏移
                    mVertexData//分配的buffer
            );
    
            //开启顶点着色器的attribute
            GLES20.glEnableVertexAttribArray(aPosition);
    
            int aColor = GLES20.glGetAttribLocation(programId, A_COLOR);
            mVertexData.position(COORDS_PER_VERTEX);
            GLES20.glVertexAttribPointer(aColor,COLOR_PER_VERTEX,GL_FLOAT,false,STRIDE,mVertexData);
            GLES20.glEnableVertexAttribArray(aColor);
    

    关键API说明

    1. 获取着色器attribute一个属性:int aPosition = GLES20.glGetAttribLocation(programId, A_POSITION);_

    2. 数据的偏移:mVertexData.position(0); 因为有坐标和颜色两个变量的值,数据又是根据顶点一一设定的。

    3. 给attribute赋值:GLES20. glVertexAttribPointer(
      int indx,//attribute的句柄
      int size,//在数组中占用的位数
      int type,//数据的类型
      boolean normalized,
      int stride,//步幅 单位字节数
      java.nio.Buffer ptr // 元数据
      )

    4. 使能对应的attribute属性:GLES20.glEnableVertexAttribArray(aPosition);

    通过上面几个环节我们可以看到,我们可以看到,是如何给着色器语言中的变量赋值的。下面我们看来下如何渲染。

    我们采用GlSurfaceView,通过Render来实现,具体如下:

        //1. 设置OpenGL ES的版本
        glSView.setEGLContextClientVersion(3);
    
         //2. 给glSurfaceView设置render
        glSView.setRenderer(new MyRender2());
    
      
    public class MyRender2 implements GLSurfaceView.Renderer {
    
    @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
          //着色器的加载、赋值
        }
    
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            GLES20.glViewport(0, 0, width, height);
        }
    
        @Override
        public void onDrawFrame(GL10 gl) {
            //清屏
            GLES20.glClear(GL_COLOR_BUFFER_BIT);
            
            //绘制
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN,0,3);
        }
    }
    
    是的,在onDrawFrame中进行不断的 glDrawArrays来绘制刷新   
     public static native void glDrawArrays(
            int mode, //点、线、三角形
            int first,//顶点的第一个数据的index
            int count//顶点的总数
        );
    
    

    二、实践:用GLSurfaceView加载GLSL绘制屏幕图形

    2.1 三角形

    上面的代码中定义的就是三角形,对应顶点数据如下

    float[] TRIANGLE_COORDS = {0.5f, 0.5f,1f, 0.5f,0.5f,
                    -0.5f, -0.5f,0.5f, 1f,0.5f,
                    0.5f, -0.5f,0.5f, 0.5f,1f
    
            };
    
    两个坐标位x&y,和三个颜色位rgb
    

    效果如下


    2.2 正方形

    只需要修改 顶点数组和glDrawArrays的count参数即可

    float[] TRIANGLE_COORDS = {
                   0.5f, 0.5f,1f, 0.5f,0.5f,
                   -0.5f, -0.5f,0.5f, 1f,0.5f,
                   0.5f, -0.5f,0.5f, 0.5f,1f,
                   0.5f, 0.5f,1f, 0.5f,0.5f,
                   -0.5f, 0.5f,0.5f, 0.5f,1f,
                   -0.5f, -0.5f,0.5f, 1f,0.5f,
           };
    
    
      public void onDrawFrame(GL10 gl) {
           GLES20.glClear(GL_COLOR_BUFFER_BIT);
           GLES20.glDrawArrays(GLES20.GL_TRIANGLES,0,6);
       }
    

    2.3 直线

         float[] TRIANGLE_COORDS = {
                -0.5f, 0f, 1f, 0f, 0f,
                 0.5f, 0f, 0f, 0f, 1f,
            };
    
        public void onDrawFrame(GL10 gl) {
            GLES20.glClear(GL_COLOR_BUFFER_BIT);
            GLES20.glDrawArrays(GLES20.GL_LINES,0,2);
        }
    

    通过log分析,我们发现GLSurfaceView.Renderer是运行中一个叫GLThread的线程中,它的作用和意义是什么,下一篇我们通过对GLSurfaceView的源码分析来理解EGL和GLThread,了解完整的流程,然后不使用Render,采用自建管理EGL和创建GLThread,通过TextureView实现图形的绘制。做到知其然,也知其所以然。

    三、遇到的问题

    1. A/libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 9940 (GLThread 6703)_
    
    发现是GLES20.glCreateProgram()时出现的上述崩溃
    原因是因为没有glSView.setEGLContextClientVersion(3);
    
    2. GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, status, 0)
    Shader编译后获取状态为0(失败了)
    
    原因是因为glsl语言注释不对用成了 # 而不是 //
    
    3.  无法正常的看到期望的图片
    这个就是开头说的说的那个折腾了一个小时的问题,一个“逗号”引发的问题
    
    具体如下:
    
    float[] TRIANGLE_COORDS = {0.5f, 0.5f,1f, 0.5f,0.5f ///注意这里没有加逗号,就是这个导致的,glsl认为到这里就结束了。。但是代码上写的是3个顶点,找不到就无法正常绘制。多么痛的领悟。
                    -0.5f, -0.5f,0.5f, 1f,0.5f,
                    0.5f, -0.5f,0.5f, 0.5f,1f
    
            };
    

    四、参考

    《OpenGL ES应用开发实践指南》
    《音视频开发进阶指南》
    [搭建OpenGL ES环境的两种方式]
    [Android OpenGL ES(一)-开始描绘一个平面三角形]
    [Android OpenGL ES(三)-平面图形]
    [EGL 环境搭建流程]

    五、收获

    1. 通过代码实践加深了对GLSL语法,OpenGL基本概念和绘制流程的熟悉。
    2. glsl程序编写androidstudio等IDE插件的了解
    3. 理解实现了如何给着色器输入数据,又如何在屏幕上绘制。
    4. 绘制三角形、正方形、直线等平面图形
    5. 遇到的问题分析解决,弥补认知不足。

    下一篇我们来解析GLSurfaceView源码&自己实现EGL管理与GLThread。欢迎关注公众号“音视频开发之旅”,一起学习成长。

    欢迎交流

    相关文章

      网友评论

          本文标题:音视频开发之旅(九) OpenGL ES 绘制平面图形

          本文链接:https://www.haomeiwen.com/subject/luanvktx.html