OpenGL ES 绘制形状(Shape)

本篇文章属于 使用 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();
    }
}

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


OpenGL ES 绘制形状(Shape)_第1张图片
实际运行效果图

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

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

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

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

你可能感兴趣的:(OpenGL ES 绘制形状(Shape))