• OpenGL ES 学习(三) -- 绘制平面图形


    OpenGL 学习教程
    Android OpenGL ES 学习(一) – 基本概念
    Android OpenGL ES 学习(二) – 图形渲染管线和GLSL
    Android OpenGL ES 学习(三) – 绘制平面图形
    Android OpenGL ES 学习(四) – 正交投屏
    Android OpenGL ES 学习(五) – 渐变色
    Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序
    Android OpenGL ES 学习(七) – 纹理
    代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git

    上一章中,已经对 OpenGL 的编程语言 GLSL 和渲染模式有了一定的了解,今天,将运用之前的知识,完成一些平面图形的操作。效果如下:
    在这里插入图片描述

    如果你对 OpenGL 的基本概念或者渲染流程不清晰,建议先看 OpenGL ES 学习(一) – 基本概念OpenGL ES 学习(二) – 渲染模式和GLSL
    这两篇文章。

    看下面一张图:
    在这里插入图片描述
    在写程序之前,先有个认知,就是我们的写的程序是在 client ,就是cpu 的部分,那怎么跟 GPU (server) 通信呢?
    从图看,它是通过顶点着色器的数据进行通信的,比如 attributes (in) 或 uniforms 的数据,在变成着色器程序后,就可以操作 gpu ,实现快速绘制的效果。

    一. 完整程序编写

    知道上面通信的机制,所以,第一步,就是顶点着色器的代码编写。

    1.1 着色器代码代码编写

    先新建一个 GLSL 的文件,编写顶点着色器的代码,对 GLSL 不熟悉,可以先 OpenGL ES 学习(二) – 渲染模式和GLSL

    一个着色器的代码,都是一个执行片段,所以,都是 main 函数为入口。
    一个点绘制,需要位置,大小和颜色,而着色的内置参数为:

    • 顶点着色器(Vectex Shader):gl_Position(位置) 和 gl_pointSize (大小)

    所以,点的顶点着色器代码为:
    在这里插入图片描述
    其中,#version 300 es 表明 gl 的版本
    顶点着色器的代码字符串为(后面用):

            //注意 #version 这里,一定要第一行,不然gl识别不到
            private val VERTEX_SHADER = """#version 300 es
               layout(location=0) in vec4 a_Position;
             
                void main(){
                    gl_Position = a_Position;
                    gl_PointSize = 100.0;
                }
                
            """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    片段着色器这里,只需要涂上一个颜色即可,这个颜色值由程序给,

    private val FRAGMENT_SHADER = """#version 300 es
             // 定义所有浮点数据类型的默认精度;有lowp、mediump、highp 三种,但只有部分硬件支持片段着色器使用highp。(顶点着色器默认highp)
             precision mediump float;
             //颜色是4分量,如果没有设置,则默认黑色 RGBA
             out vec4 u_Color;
             void main(){
                 u_Color = vec4(1.0,0.0,0.0,1.0);
             }
     """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    1.2 着色的创建/编译

    先看一张图:
    在这里插入图片描述
    所以,一个着色器的代码生成,可以理解为:

    1. 着色器的代码构建:glCreateShader -> glShaderSource -> glCompileShader 组成
    2. 使用 glCreateProgram 拿到 OpenGL 对象
    3. 使用 glAttachShader 关联着色器代码,
    4. glLinkProgram 将着色器的程序关联到 OpenGL 对象,组成一个 OpenGL 程序
    5. 使用 GLES20.glUseProgram,使用上述的 OpenGL 程序

    1.2.1 编译着色器

        /**
         * 编译着色器代码,获取代码Id
         */
        open fun compileShader(type: Int, shaderCode: String): Int {
            //创建一个shader 对象
            val shaderId = GLES30.glCreateShader(type)
            if (shaderId == 0) {
                Log.d(TAG, " 创建失败")
                return 0
            }
            //将着色器代码上传到着色器对象中
            GLES30.glShaderSource(shaderId, shaderCode)
            //编译对象
            GLES30.glCompileShader(shaderId)
            //获取编译状态,OpenGL 把想要获取的值放入长度为1的数据首位
            val compileStatus = intArrayOf(1)
            GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, compileStatus, 0)
            Log.d(TAG, " compileShader: ${compileStatus[0]}")
    
            if (compileStatus[0] == 0) {
                Log.d(TAG, " 编译失败: ${GLES30.glGetShaderInfoLog(shaderId)}")
                GLES30.glDeleteShader(shaderId)
                return 0
            }
    
            return shaderId
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    可以使用 GLES30.glGetShaderInfoLog(shaderId) 获取执行错误的信息。

    1.2.2 关联着色器代码,组成可执行程序

        /**
         * 关联着色器代码,组成可执行程序
         */
        open fun linkProgram(vertexShaderId: Int, fragmentShaderId: Int): Int {
            //创建一个 OpenGL 程序对象
            val programId = GLES30.glCreateProgram()
            if (programId == 0) {
                Log.d(TAG, " 创建OpenGL程序对象失败")
                return 0
            }
            //关联顶点着色器
            GLES30.glAttachShader(programId, vertexShaderId)
            //关联片段周色漆
            GLES30.glAttachShader(programId, fragmentShaderId)
            //将两个着色器关联到 OpenGL 对象
            GLES30.glLinkProgram(programId)
            //获取链接状态,OpenGL 把想要获取的值放入长度为1的数据首位
            val linkStatus = intArrayOf(1)
            GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0)
            Log.d(TAG, " linkProgram: ${linkStatus[0]}")
    
            if (linkStatus[0] == 0) {
                GLES30.glDeleteProgram(programId)
                Log.d(TAG, " 编译失败")
                return 0
            }
            return programId;
    
        }
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    1.2.3 使用程序

        /**
         * 生成可执行程序,并使用该程序
         */
        protected fun makeProgram(vertexShaderCode: String, fragmentShaderCode: String): Int {
            //需要编译着色器,编译成一段可执行的bin,去与显卡交流
            val vertexShader = compileShader(GLES30.GL_VERTEX_SHADER, vertexShaderCode)
            //步骤2,编译片段着色器
            val fragmentShader = compileShader(GLES30.GL_FRAGMENT_SHADER, fragmentShaderCode)
    
            // 步骤3:将顶点着色器、片段着色器进行链接,组装成一个OpenGL程序
            programId = linkProgram(vertexShader, fragmentShader)
            //链接之后就可以删除着色器对象了,不需要了
            GLES30.glDeleteShader(vertexShader)
            GLES30.glDeleteShader(fragmentShader)
    
            //通过OpenGL 使用该程序
            GLES30.glUseProgram(programId)
            return programId
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    着色器创建、编译和链接,都可以拿到状态值,大于0的时候,则表示是可用的。这几步都是固定,封装好就可以了。

    1.3 获取定编索引和通过 GL 使用索引

    1.3.1 定义顶点

    这里先用一个点来表示,上面说道,顶点有4个分量,但是点是x,y 两个分量,所以设置为0,0 即可,如下

            //定点的数据,只有一个点,就放中心即可
            private val POINT_DATA = floatArrayOf(0f, 0f)
    
            /**
             * Float类型占4Byte
             */
            private val BYTES_PER_FLOAT = 4
            /**
             * 每个顶点数据关联的分量个数:当前案例只有x、y,故为2
             */
            private val POSITION_COMPONENT_COUNT = 2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    加载顶点数据到内存

        //通过nio ByteBuffer把设置的顶点数据加载到内存
        private var vertexData =ByteBuffer
            // 分配顶点坐标分量个数 * Float占的Byte位数
            .allocateDirect(POINT_DATA.size * BYTES_PER_FLOAT)
            // 按照本地字节序排序
            .order(ByteOrder.nativeOrder())
            // Byte类型转Float类型
            .asFloatBuffer()
            .put(POINT_DATA)
            //将缓冲区的指针指到头部,保证数据从头开始
            .position(0)
    
    //后面封装成 vertexData = BufferUtil.createFloatBuffer(POINT_DATA)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    1.3.1 关联顶点数据

    GL 关联索引,使用的是 glVertexAttribPointer 方法,它会把顶点数据和属性关联到 GL 里,然后再通过 glEnableVertexAttribArray,告知 GL 使用指定的顶点属性索引。

    完成代码如下:

        override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
            //白色背景
            GLES30.glClearColor(1f, 1f, 1f, 1f)
            // 编译着色器相关程序
            makeProgram(VERTEX_SHADER, FRAGMENT_SHADER)
    
            // 关联顶点坐标属性和缓存数据,参数说明如下:
            GLES30.glVertexAttribPointer(
                0, //位置索引
                POSITION_COMPONENT_COUNT,//用几个分量描述一个顶点
                GLES30.GL_FLOAT,//分量类型
                false, //固定点数据值是否应该被归一化
                0, //指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0
                vertexData
            ) //顶点数据缓冲区
    
            //通知GL程序使用指定的顶点属性索引
            GLES30.glEnableVertexAttribArray(0)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    1.4 渲染

    OpenGL 的加载容器使用的是 GLSurfaceView ,基于 SurfaceView ,通过 Render 来加载数据。
    因此,我们可以继承 GLSurfaceView.Renderer,重写方法:

    @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
          //着色器的加载、赋值
        }
    
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
            GLES30.glViewport(0, 0, width, height);
        }
    
        @Override
        public void onDrawFrame(GL10 gl) {
            //清屏
            GLES30.glClear(GL_COLOR_BUFFER_BIT);
            
            //绘制
            GLES30.glDrawArrays(GLES20.GL_POINTS,0,1);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    上面在 onSurfaceCreated 中完成了着色器的加载和复制。
    而当有数据来的时候,会回调 onDrawFrame 方法,我们可以在这里,使用 glDrawArrays 去绘制顶点的类型,和个数,该方法的解释为,假如现按顺序有A、B、C、D、E、F一共6个点。
    而mode的具体参数值如下:
    图片来源https://www.jianshu.com/p/eb11a8346cf6

    图片来源:https://juejin.cn/post/7143614036046217230

    二. 画几何图形

    2.1 画点

    完成代码为:

        override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
            //填充整个页面
            GLES30.glViewport(0, 0, width, height)
        }
    
        override fun onDrawFrame(gl: GL10?) {
            Log.d(TAG, "onDrawFrame() call")
            //步骤1:使用glClearColor设置的颜色,刷新Surface
            GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
            // 1.绘制的图形类型;2.从顶点数组读取的起点;3.从顶点数组读取的顶点个数 ,这里只绘制一个点
            GLES30.glDrawArrays(GLES30.GL_POINTS, 0, 1)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    GLSurfaceView 的使用

    glSurfaceView = GLSurfaceView(this@MainActivity).apply {
    				//设置 GL 的版本
                        setEGLContextClientVersion(3)
                        setEGLConfigChooser(false)
                        //你继承的  GLSurfaceView.Renderer
                        setRenderer(render)
                        //等待点击才会刷帧
                        renderMode = GLSurfaceView.RENDERMODE_WHEN_DIRTY
    
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    效果:
    在这里插入图片描述

    2.2 画多边形

    修改顶点数据

            private val POINT_DATA = floatArrayOf(
                //x,y 一个点,这里相当于一个棱形,自己画个坐标
                0f, 0f,
                0f, 0.5f,
                -0.5f, 0f,
                0f, -0.5f,
                0.5f, -0.5f,
                0.5f, 0f,
                0.5f, 0.5f,
            )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    坐标时[-1,1] 之间,可以想象一下。
    其他不变,在 onDrawFrame 修改绘制的顶点个数,当点击时,刷新个数:

        override fun onDrawFrame(gl: GL10?) {
            //步骤1:使用glClearColor设置的颜色,刷新Surface
            GLES30.glClear(GLES20.GL_COLOR_BUFFER_BIT)
            drawIndex++
            // glDrawArrays 可以理解成绘制一个图层,多个图层可以叠加,然后通过onDrawFrame绘制到这一帧上
            drawTriangle()
            drawLine()
            drawPoint();
    
            if (drawIndex >= POINT_DATA.size / 2) {
                drawIndex = 0
            }
        }
    
        private fun drawLine() {
            // GL_LINES:每2个点构成一条线段
            // GL_LINE_LOOP:按顺序将所有的点连接起来,包括首位相连
            // GL_LINE_STRIP:按顺序将所有的点连接起来,不包括首位相连
            GLES30.glUniform4f(uniformColor, 1f, 0f, 0f, 1f)
            GLES30.glDrawArrays(GLES30.GL_LINE_LOOP, 0, drawIndex)
        }
    
        private fun drawPoint() {
            GLES30.glUniform4f(uniformColor, 0f, 0f, 1f, 1f)
            GLES30.glDrawArrays(GLES30.GL_POINTS, 0, drawIndex)
        }
    
        private fun drawTriangle() {
            // GL_TRIANGLES:每3个点构成一个三角形
            // GL_TRIANGLE_STRIP:相邻3个点构成一个三角形,不包括首位两个点
            // GL_TRIANGLE_FAN:第一个点和之后所有相邻的2个点构成一个三角形
            GLES30.glUniform4f(uniformColor, 1f, 1f, 0f, 1f)
            GLES30.glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, drawIndex)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 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

    效果:
    在这里插入图片描述
    这样就学习完几何图形的绘制了。更多代码,参考工程:https://github.com/LillteZheng/OpenGLDemo

    参考:
    https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/
    https://www.jianshu.com/p/eb11a8346cf6
    https://mp.weixin.qq.com/s?__biz=MzU5NjkxMjE5Mg==&mid=2247483783&idx=1&sn=6c8fa673eff0aaffe0872227432c3214&chksm=fe5a30a8c92db9bea01b92d35c37efa16a7acb08237bdf6ad0db510549e3b8a14d692fbac638&scene=21#wechat_redirect

  • 相关阅读:
    从1开始的Matlab(快速入门)
    分布式定时任务调度
    Linux:Socket套接字编程 | TCP
    网上预约挂号系统的设计与实现
    真趣科技:多业务形态的企业需要灵活可配置的CRM系统
    Android查看签名信息系列 · 使用逆向分析工具JadxGUI获取签名
    重要功能更新:妙手正式接入SHEIN供货模式(OBM)店铺,赋能卖家把握出海新机遇!
    编程-设计模式 3:单例模式
    Z41H-64C高压闸阀型号解析
    day20 - 绘制物体的运动轨迹
  • 原文地址:https://blog.csdn.net/u011418943/article/details/128049711