OpenGL ES---绘制二维图形

背景

 绘制 3D 图,总觉得是一件很炫酷的事。虽然在项目中一直没有用到过,但是还是想找个时间,实践一下。
 绘制二维图形,尽管使用 OpenGL 有它的优势,但是还是感觉有点杀鸡用牛刀的意 思。这里主要是借助对 二维图形 的绘制过程,解释相关概念。

什么是 OpenGL ES

 首先,来说明一下 OpenGL ES for Embedded Systems(OpenGL ES)。它是 OpenGL 的子集,用以渲染 2D 、3D 矢量图的跨语言、跨平台的API,这个 API 通常会和 GPU 交互,完成硬件加速渲染。
 Android 平台支持不同版本 OpenGL ES 的 API。其中:

  • OpenGL ES 1.0 and 1.1 - 该 API 被Android 1.0 及更高版本支持.
  • OpenGL ES 2.0 - 该 API 被 Android 2.2 (API level 8) 及更高版本支持.
  • OpenGL ES 3.0 - 该 API 被 Android 4.3 (API level 18) 及更高版本支持.
  • OpenGL ES 3.1 - 该 API 被 Android 5.0 (API level 21) 及更高版本支持.

 为了获取更广泛的设备支持,通常会基于 OpenGL ES 2.0 做开发。本文也是基于该版本展开。

Android 平台提供的基础

 Android 框架层提供了两个对象以使用 OpenGL ES API 操作图像:GLSurfaceView 和 GLSurfaceView.Renderer。

GLSurfaceView

 GLSurfaceView 继承自 SurfaceView,拥有专用的 surface 以展示 OpenGL 渲染。它提供了一下特性:

  • 管理 surface,同时使得 OpenGL 可以在 surface 上渲染;
  • 可以使用用户自定义的 Renderer 对象进行实际的渲染工作;
  • 渲染线程独立于 UI 线程之外 ;
  • 支持在需要时才进行的被动渲染 和 不间断自动进行的主动渲染两种渲染方式;
//设置一下模式,为被动刷新
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
  • 调试渲染调用

简单的使用方式如下所示:

//直接创建一个 GLSurfaceView,当然,也可以通过布局文件创建
glSurfaceView = new GLSurfaceView(this) ;
//使用 OpenGL ES 2.0 context.
glSurfaceView.setEGLContextClientVersion(2);
//设置我们自定义的 renderer
glSurfaceView.setRenderer(new MyRenderer());
setContentView(glSurfaceView);
GLSurfaceView.Renderer

 负责进行帧渲染。提供的回调函数有:

  • onDrawFrame:负责对当前帧的绘制;
  • onSurfaceChanged:当 surface 的大小发生变化时候的回调,比如刚创建 surface 的时候,绘制屏幕发生旋转;
  • onSurfaceCreated:当 surface 创建或者重建的时候被调用。调用发生在渲染线程开始的时候,或者当 EGL context 丢失的时候(该 context 通常会在设备从睡眠中唤醒的时候丢失)。
    注意,当 EGL context 丢失,和 context 关联的所有 OpenGL 自愿将会被自动删除。

管线渲染过程

 要明白这个过程,首先要知道什么是管线。所谓管线,就是在显卡上执行的将数据源转换投射到屏幕像素点上的过程。也就是将我们通过顶点定义的形状,显示到屏幕上。

管线渲染-1

如上图所示,

  1. 定义顶点;
  2. 通过 vertexShader着色器 告知 GPU 顶点的位置等属性;
  3. 通过图元装配,生成要绘制的形状;
  4. 光栅化处理,将所有的点转化为片元(fragment);
  5. 通过 fragmentShader着色器 为片元上色;
  6. 将片元投射到屏幕上的像素上。

更加形象一点的过程如下所示:


管线渲染-2

 注意,其中 vertexShader 和 fragmentShader 是通过 GLSL 语言定义的,并直接运行在 GPU 上。

VertexShader

顶点着色器,主要用于确定顶点位置,由 GLSL 语言定义,对于每个顶点都会执行该程序。通常用法如下:

//通过 GLSL 定义 VertexShader
private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                     "uniform mat4 u_Matrix;"+
                    "void main() {" +
                   // "gl_Position = vPosition;" +
                     "gl_Position = u_Matrix * vPosition;" +
                    "gl_PointSize = 10.0;"+
                    "}";

 @Override
 public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    ...
//取出位置索引
        aPositionLocation = GLES20.glGetAttribLocation(program,"vPosition") ;
 //将位置索引和我们定义的数据源 vertexes 进行绑定,将 vertexes 中的每个顶点拿出来赋值到 vPosition ,并分别执行上面定义的 GLSL 程序
       GLES20.glVertexAttribPointer(aPositionLocation,2,GLES20.GL_FLOAT,false,0,vertexes);
        GLES20.glEnableVertexAttribArray(aPositionLocation);
}
FragmentShader

片元着色器,目的就是告诉 GPU 每个片段的最终颜色应该是什么。对于基于图元的每个片段,片段着色器都会被调用一次。因此,如果一个三角形被映射到 10000 个片段,片段着色器就会被调用 10000次。

 private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";
   @Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//取出颜色索引
      aColorLocation = GLES20.glGetUniformLocation(program,"vColor") ;
}

@Override
public void onDrawFrame(GL10 gl) {
//给颜色赋值
        GLES20.glUniform4f(aColorLocation,  0f,1,1, 1.0f);
//绘制
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
}

完整代码如下:

public class MyRenderer implements GLSurfaceView.Renderer {

    private FloatBuffer vertexes ;

    private final String vertexShaderCode =
            "attribute vec4 vPosition;" +
                     "uniform mat4 u_Matrix;"+
                    "void main() {" +
                     "gl_Position = u_Matrix * vPosition;" +
                    "gl_PointSize = 10.0;"+
                    "}";

    private final String fragmentShaderCode =
            "precision mediump float;" +
                    "uniform vec4 vColor;" +
                    "void main() {" +
                    "  gl_FragColor = vColor;" +
                    "}";

    private int aPositionLocation ;
    private int aColorLocation ;
    private int uMatrixLocation;
    private final float[] projectionMatrix = new float[16];

    private void createVertexes(){
        float [] vertexesArray = new float[]{
                0,1,
                -1,-1,
                1,-1
        } ;
        vertexes.clear();
        vertexes.put(vertexesArray) ;
    }

    private void init(){
//创建本地内存,以便将我们定义的顶点放进去,供设备访问
//堆内存上的数据,GPU是无法直接访问的
        vertexes =  ByteBuffer.allocateDirect(6*4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer() ;
        //创建 顶点 坐标
        createVertexes();
    }

    public MyRenderer(){
        init();
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        GLES20.glClearColor(1f,1f,0f,0f);

        int vertexShader = ShaderHelper.compileVertexShader(vertexShaderCode) ;
        int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderCode) ;
        int program =  ShaderHelper.linkProgram(vertexShader, fragmentShader);

        if (LoggerConfig.ON) {
            ShaderHelper.validateProgram(program);
        }

        GLES20.glUseProgram(program);
        aPositionLocation = GLES20.glGetAttribLocation(program,"vPosition") ;
        aColorLocation = GLES20.glGetUniformLocation(program,"vColor") ;
        uMatrixLocation = GLES20.glGetUniformLocation(program, "u_Matrix");
        vertexes.position(0) ;
        GLES20.glVertexAttribPointer(aPositionLocation,2,GLES20.GL_FLOAT,false,0,vertexes);
        GLES20.glEnableVertexAttribArray(aPositionLocation);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
        // 根据屏幕方向设置投影矩阵
        float ratio= width > height ? (float)width / height : (float)height / width;
        if (width > height) {
            // 横屏
            Matrix.orthoM(projectionMatrix, 0, -ratio, ratio, -1, 1, 0, 5);
        } else {
            Matrix.orthoM(projectionMatrix, 0, -1, 1, -ratio, ratio, 0, 5);
        }
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        // Clear the rendering surface.
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
        // Assign the matrix
        GLES20.glUniformMatrix4fv(uMatrixLocation, 1, false, projectionMatrix, 0);
        GLES20.glUniform4f(aColorLocation,  0f,1,1, 1.0f);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
    }
}

下面对以上代码块做几点说明:

  • 在使用 OpenGL ES 的时候,我们一般会做以下处理:1. 编译顶点着色器;2. 编译出片元着色器;3. 创建程序;4.通过程序链接所有着色器;5. 使用程序,并通过程序,取出位置、颜色等索引,以便后期绘制。
  • 上面代码块中,回调方法 onDrawFrame 的调用时机需要注意。默认情况下,会按照屏幕刷新的周期来调用该方法,即每秒执行 60 次。当然,我们可以通过在代码中设置以改变这种默认行为。
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
  • 注意本地内存和虚拟机中堆内存的使用。后者是无法直接被设备访问的,后者可以,同时后者不受 GC 的影响。
  • GLES20 中的所有方法,实际上都是本地方法,即通过 JNI 调用实现的,底层是由 C 语言实现。

小结

 本节主要借用二维图形的绘制,讲解了 OpenGL ES 在 Android 应用中使用的相关概念。下面想讲述一下坐标变换。

参考链接:
https://developer.android.com/training/graphics/opengl/environment.html
http://www.cs.ucr.edu/~shinar/courses/cs130-spring-2012/schedule.html
https://www.zhihu.com/question/29163054
https://en.wikibooks.org/wiki/GLSL_Programming/OpenGL_ES_2.0_Pipeline

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