美文网首页Android开发OpenGL ES
OpenGL ES 绘制形状(Shape)

OpenGL ES 绘制形状(Shape)

作者: 石先 | 来源:发表于2017-08-18 18:39 被阅读76次

    本篇文章属于 使用 OpenGL ES 进行图形绘制 这个系列的第三篇文章,主要内容是介绍在如何在 Android 应用中利用 OpenGL 绘制图形的形状。文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。

    在上篇文章:OpenGL ES 定义形状 中我们定义了 OpenGL 绘制的形状之后,下面就一起看看如何使用 OpenGL ES 2.0 接口绘制出在 OpenGL ES 定义形状 文章中定义的形状。

    使用 OpenGL ES 2.0 绘制图形可能会腻比想象当中要复杂一些,因为 Android 中保留提供了大量对于图形渲染流程控制的 API ,就像我们在绘制自定义 View 时一样,绘制控制的方法和参数都会很丰富。

    其实在前面文章:配置 OpenGL ES 的环境 里面有提到 一个核心的类 GLSurfaceView.Renderer,它是控制 view 绘制过程的渲染器,之前文章中展示了如何使用 GLSurfaceView.Renderer 进行绘制黑色背景的简单试验,所以接下来的关于形状的绘制必然少不了它的参与。

    初始化形状

    在开始绘制之前,需要对绘制的图形进行初始化并加载。如果这些形状结构(原始坐标)在执行过程不会发生变化,那么应该在 Renderer 的 onSurfaceCreated() 方法中进行初始化和加载,这样可以更省内存以及提升执行效率。

    public class MyGLRenderer2 implements GLSurfaceView.Renderer {
    
        ...
        private Triangle mTriangle;
        private Square mSquare;
    
        @Override
        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            // initialize a triangle
            mTriangle = new Triangle();
            // initialize a square
            mSquare = new Square();
        }
        ...
    }
    

    绘制形状

    使用 OpenGL ES 2.0 绘制一个定义好的形状需要较多代码,因为你需要提供很多图形渲染流程的细节,比如:

    • 顶点着色器(Vertex Shader):用来渲染形状(shape)顶点的 OpenGL ES 代码。

    OpenGL ES 2.0 渲染管线中顶点着色器(Vertex Shader)取代了 OpenGL ES 1.x 渲染管线中的“变换和光照”

    • 片元着色器(Fragment Shader):使用颜色或纹理(texture)渲染形状表面(face)的 OpenGL ES 代码。

    片元着色器取代了 OpenGL ES 1.x 渲染管线中的“纹理环境和颜色求和”、“雾”以及“Alpha测试”

    • 程式(Program):一个 OpenGL ES 对象,包含了你希望用来绘制一个或更多图形(shape)所要用到的着色器(shader)。

    以上三个,你需要至少一个顶点着色器(Vertex Shader)来绘制一个形状,以及一个片元着色器(Fragment Shader)为该形状上色。这些着色器必须被编译然后再添加到一个OpenGL ES Program当中,并利用这个 progrem 来绘制形状。通过编写顶点及片元着色器程序,来完成一些顶点变换和纹理颜色计算工作,实现更加灵活、精细化的计算与渲染。

    下面的代码在 Triangle 类中定义了基本的着色器,我们可以利用它们绘制出一个图形:

    public class Triangle {
    
       /**
         * 顶点着色器代码
         * attribute变量(属性变量)只能用于顶点着色器中,不能用于片元着色器。一般用该变量来表示一些顶点数据,如:顶点坐标、纹理坐标、颜色等
         * uniforms变量(一致变量)用来将数据值从应用程其序传递到顶点着色器或者片元着色器。 该变量有点类似C语言中的常量(const),即该变量的值不能被shader程序修改。一般用该变量表示变换矩阵、光照参数、纹理采样器等。
         * varying变量(易变变量)是从顶点着色器传递到片元着色器的数据变量。顶点着色器可以使用易变变量来传递需要插值的颜色、法向量、纹理坐标等任意值。 在顶点与片元shader程序间传递数据是很容易的,一般在顶点shader中修改varying变量值,然后片元shader中使用该值,当然,该变量在顶点及片元这两段shader程序中声明必须是一致的。
         * gl_Position 为内建变量,表示变换后点的空间位置。 顶点着色器从应用程序中获得原始的顶点位置数据,这些原始的顶点数据在顶点着色器中经过平移、旋转、缩放等数学变换后,生成新的顶点位置。新的顶点位置通过在顶点着色器中写入gl_Position传递到渲染管线的后继阶段继续处理。
         */
        private final String vertexShaderCode =
                "attribute vec4 vPosition;" +  // 应用程序传入顶点着色器的顶点位置
                        "void main() {" +
                        "  gl_Position = vPosition;" + // 设置此次绘制此顶点位置
                        "}";
    
        /**
         * 片元着色器代码
         */
        private final String fragmentShaderCode =
                "precision mediump float;" +  // 设置工作精度
                        "uniform vec4 vColor;" +  // 应用程序传入着色器的颜色变量
                        "void main() {" +
                        "  gl_FragColor = vColor;" + // 颜色值传给 gl_FragColor内建变量,完成片元的着色
                        "}";
       ...
    }
    

    关于着色器和 GLSL 语言推荐几篇文章
    OpenGL ES 入门(一)着色器简介
    OpenGL Shading language学习总结

    着色器(Shader)包含了OpenGL Shading Language(GLSL)代码,它必须先被编译然后才能在 OpenGL 环境中使用。要编译 GLSL 代码需要在渲染器类中创建一个辅助方法:

    public class MyGLRenderer2 implements GLSurfaceView.Renderer 
        ...
    
        /**
         * 加载并编译着色器代码
         * @param type 渲染器类型 {GLES20.GL_VERTEX_SHADER, GLES20.GL_FRAGMENT_SHADER}
         * @param shaderCode 渲染器代码 GLSL
         * @return
         */
        public static int loadShader(int type, String shaderCode){
    
            // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
            // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
            int shader = GLES20.glCreateShader(type);
    
            // add the source code to the shader and compile it
            GLES20.glShaderSource(shader, shaderCode);
            GLES20.glCompileShader(shader);
    
            return shader;
        }
    }
    

    要绘制图形前,必须先编译着色器代码并将它们添加至一个 OpenGL ES Program 对象中,然后执行链接方法。

    Note:编译 OpenGL ES 着色器及链接操作对于 CPU 周期和处理时间而言消耗巨大,所以应该避免重复执行这些事情。这个操作建议在形状类的构造方法中调用,这样只会执行一次。如果在执行期间不知道着色器的内容,可以考虑使用一次后缓存以备后续使用。

    public class Triangle() {
        ...
    
        private final int mProgram;
    
        public Triangle() {
            ...
            
            // 加载编译顶点渲染器
            int vertexShader = MyGLRenderer2.loadShader(GLES20.GL_VERTEX_SHADER,
                    vertexShaderCode);
    
            // 加载编译片元渲染器
            int fragmentShader = MyGLRenderer2.loadShader(GLES20.GL_FRAGMENT_SHADER,
                    fragmentShaderCode);
    
            // create empty OpenGL ES Program
            mProgram = GLES20.glCreateProgram();
    
            // add the vertex shader to program
            GLES20.glAttachShader(mProgram, vertexShader);
    
            // add the fragment shader to program
            GLES20.glAttachShader(mProgram, fragmentShader);
    
            // creates OpenGL ES program executables
            GLES20.glLinkProgram(mProgram);
        }
    

    至此,你已经完全准备好添加实际的调用语句来绘制你的图形了。使用 OpenGL ES 需要一些参数来告诉渲染流程(redering pipeline )你要绘制的内容以及如何绘制,由于每个 shape 的 drawing option 都不一样,因此将每个 shape 的绘制逻辑放到自己的类里面是一个比较好的方法。

    创建一个 draw() 方法来绘制图形。下面的代码为形状的顶点着色器和形状着色器设置了位置和颜色值,然后执行绘制函数:

    public class Triangle {
    
        // 绘制形状的顶点数量
        private static final int COORDS_PER_VERTEX = 3;
    
        ...
    
        private int mPositionHandle;
        private int mColorHandle;
    
        private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
        private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
    
            public void draw() {
            // Add program to OpenGL ES environment
            GLES20.glUseProgram(mProgram);
    
            // get handle to vertex shader's vPosition member
            mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
    
            // Enable a handle to the triangle vertices
            GLES20.glEnableVertexAttribArray(mPositionHandle);
    
            // Prepare the triangle coordinate data
            GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
                    GLES20.GL_FLOAT, false,
                    vertexStride, vertexBuffer);
    
            // get handle to fragment shader's vColor member
            mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
    
            // Set color for drawing the triangle
            GLES20.glUniform4fv(mColorHandle, 1, color, 0);
    
            // Draw the triangle
            GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
    
            // Disable vertex array
            GLES20.glDisableVertexAttribArray(mPositionHandle);
        }
    }
    

    一旦完成了上述所有代码,仅需要在渲染器的 onDrawFrame() 方法中调用 draw() 方法就可以画出我们想要画的对象了:

    public class MyGLRenderer2 implements GLSurfaceView.Renderer {
    
        @Override
        public void onDrawFrame(GL10 gl) {
            ...
            mTriangle.draw();
        }
    }
    

    运行这个应用时,它看上去会像是这样:


    实际运行效果图

    实际操作过程中你发现,这个三角形看上去有一些扁,另外当你改变屏幕方向时,它的形状也会随之改变。发生形变的原因是因为对象的顶点没有根据显示 GLSurfaceView 的屏幕区域的长宽比进行修正。你可以使用投影(Projection)或者相机视角(Camera View)来解决这个问题。

    文章中所有的代码示例都已放在 Github 上,可以去项目 OpenGL-ES-Learning 中查看 。

    最后,这个三角形是静止的,这看上去有些无聊。在后续文章会让这个形状发生旋转,并使用一些 OpenGL ES 图形处理流程中更加新奇的用法。

    >>>>Next>>>> : OpenGL ES 运用投影与相机视角

    相关文章

      网友评论

        本文标题:OpenGL ES 绘制形状(Shape)

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