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();
    }
}

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


实际运行效果图

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

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

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

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

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

推荐阅读更多精彩内容