Android OpenGL ES 2.基本框架-Hello World

一.视图组件 GLSurfaceView

Android上用于显示OpenGL视图,一般是使用GLSurfaceView,一个继承自SurfaceView的组件。
它的渲染绘制在一个单独的线程中,而非主线程。
GLSurfaceView一般是结合一个GLSurfaceView的内部接口类Renderer来使用。Renderer类负责渲染图形图像,而GLSurfaceView负责触摸事件等逻辑的处理。

Renderer接口

  • onSurfaceCreated(GL10 gl, EGLConfig config):GLSurfaceView内的Surface被创建时会被调用到
  • onSurfaceChanged(GL10 gl, int width, int height):Surface尺寸改变时调用到
  • onDrawFrame(GL10 gl):渲染绘制每一帧时调用到

所以一般情况下,首次创建GLSurfaceView时,会按顺序调用onSurfaceCreated、onSurfaceChanged、onDrawFrame这3个方法,然后每绘制一帧,都会不停地回调onDrawFrame方法。

GLSurfaceView常用方法

  • setEGLContextClientVersion:设置OpenGL ES版本,2.0则设置2
  • onPause:暂停渲染,最好是在Activity、Fragment的onPause方法内调用,减少不必要的性能开销,避免不必要的崩溃
  • onResume:恢复渲染,用法类比onPause
  • setRenderer:设置渲染器
  • setRenderMode:设置渲染模式
  • requestRender: 请求渲染,由于是请求异步线程进行渲染,所以不是同步方法,调用后不会立刻就进行渲染。渲染会回调到Renderer接口的onDrawFrame方法。
  • queueEvent:插入一个Runnable任务到后台渲染线程上执行。相应的,渲染线程中可以通过Activity的runOnUIThread的方法来传递事件给主线程去执行

GLSurfaceView渲染模式

  • RENDERMODE_CONTINUOUSLY:不停地渲染
  • RENDERMODE_WHEN_DIRTY:只有调用了requestRender之后才会触发渲染回调onDrawFrame方法

二.编程流程

  • 编写GLSL:重点学习
  • 编译GLSL,获取OpenGL程序对象:基本固定,不需要死记,理解即可。后期会进行封装,便于使用。
  • 获取GLSL中变量的引用:理解调用方式
  • 通过内存Buffer,将数据传递给变量引用,从而控制绘制图形、颜色:重点学习

1. 简单的GLSL

/**
 * 顶点着色器
 */
private static final String VERTEX_SHADER = "" +
        // vec4:4个分量的向量:x、y、z、w
        "attribute vec4 a_Position;\n" +
        "void main()\n" +
        "{\n" +
        // gl_Position:GL中默认定义的输出变量,决定了当前顶点的最终位置
        "    gl_Position = a_Position;\n" +
        // gl_PointSize:GL中默认定义的输出变量,决定了当前顶点的大小
        "    gl_PointSize = 40.0;\n" +
        "}";

/**
 * 片段着色器
 */
private static final String FRAGMENT_SHADER = "" +
        // 定义所有浮点数据类型的默认精度;有lowp、mediump、highp 三种,但只有部分硬件支持片段着色器使用highp。(顶点着色器默认highp)
        "precision mediump float;\n" +
        "uniform mediump vec4 u_Color;\n" +
        "void main()\n" +
        "{\n" +
        // gl_FragColor:GL中默认定义的输出变量,决定了当前片段的最终颜色
        "    gl_FragColor = u_Color;\n" +
        "}";

注意

在声明vec向量的时候,一定要标识其精度类型,否则会导致部分机型花屏,如红米note2

2.1 编译着色器

使用compileVertexShader、compileFragmentShader两个方法分别调用上面定义的顶点着色器、片段着色器。

/**
 * 编译顶点着色器
 *
 * @param shaderCode 编译代码
 * @return 着色器对象ID
 */
public static int compileVertexShader(String shaderCode) {
    return compileShader(GLES20.GL_VERTEX_SHADER, shaderCode);
}

/**
 * 编译片段着色器
 *
 * @param shaderCode 编译代码
 * @return 着色器对象ID
 */
public static int compileFragmentShader(String shaderCode) {
    return compileShader(GLES20.GL_FRAGMENT_SHADER, shaderCode);
}

/**
 * 编译片段着色器
 *
 * @param type       着色器类型
 * @param shaderCode 编译代码
 * @return 着色器对象ID
 */
private static int compileShader(int type, String shaderCode) {
    // 1.创建一个新的着色器对象
    final int shaderObjectId = GLES20.glCreateShader(type);

    // 2.获取创建状态
    if (shaderObjectId == 0) {
        // 在OpenGL中,都是通过整型值去作为OpenGL对象的引用。之后进行操作的时候都是将这个整型值传回给OpenGL进行操作。
        // 返回值0代表着创建对象失败。
        if (LoggerConfig.ON) {
            Log.w(TAG, "Could not create new shader.");
        }
        return 0;
    }

    // 3.将着色器代码上传到着色器对象中
    GLES20.glShaderSource(shaderObjectId, shaderCode);

    // 4.编译着色器对象
    GLES20.glCompileShader(shaderObjectId);

    // 5.获取编译状态:OpenGL将想要获取的值放入长度为1的数组的首位
    final int[] compileStatus = new int[1];
    GLES20.glGetShaderiv(shaderObjectId, GLES20.GL_COMPILE_STATUS, compileStatus, 0);

    if (LoggerConfig.ON) {
        // 打印编译的着色器信息
        Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
                + GLES20.glGetShaderInfoLog(shaderObjectId));
    }

    // 6.验证编译状态
    if (compileStatus[0] == 0) {
        // 如果编译失败,则删除创建的着色器对象
        GLES20.glDeleteShader(shaderObjectId);

        if (LoggerConfig.ON) {
            Log.w(TAG, "Compilation of shader failed.");
        }

        // 7.返回着色器对象:失败,为0
        return 0;
    }

    // 7.返回着色器对象:成功,非0
    return shaderObjectId;
}

2.2 创建OpenGL程序对象,链接顶点着色器、片段着色器

/**
 * 创建OpenGL程序对象
 *
 * @param vertexShader   顶点着色器代码
 * @param fragmentShader 片段着色器代码
 */
protected void makeProgram(String vertexShader, String fragmentShader) {
    // 步骤1:编译顶点着色器
    int vertexShaderId = ShaderHelper.compileVertexShader(vertexShader);
    // 步骤2:编译片段着色器
    int fragmentShaderId = ShaderHelper.compileFragmentShader(fragmentShader);
    // 步骤3:将顶点着色器、片段着色器进行链接,组装成一个OpenGL程序
    mProgram = ShaderHelper.linkProgram(vertexShaderId, fragmentShaderId);

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

    // 步骤4:通知OpenGL开始使用该程序
    GLES20.glUseProgram(mProgram);
}

/**
 * 创建OpenGL程序:通过链接顶点着色器、片段着色器
 *
 * @param vertexShaderId   顶点着色器ID
 * @param fragmentShaderId 片段着色器ID
 * @return OpenGL程序ID
 */
public static int linkProgram(int vertexShaderId, int fragmentShaderId) {

    // 1.创建一个OpenGL程序对象
    final int programObjectId = GLES20.glCreateProgram();

    // 2.获取创建状态
    if (programObjectId == 0) {
        if (LoggerConfig.ON) {
            Log.w(TAG, "Could not create new program");
        }
        return 0;
    }

    // 3.将顶点着色器依附到OpenGL程序对象
    GLES20.glAttachShader(programObjectId, vertexShaderId);
    // 3.将片段着色器依附到OpenGL程序对象
    GLES20.glAttachShader(programObjectId, fragmentShaderId);

    // 4.将两个着色器链接到OpenGL程序对象
    GLES20.glLinkProgram(programObjectId);

    // 5.获取链接状态:OpenGL将想要获取的值放入长度为1的数组的首位
    final int[] linkStatus = new int[1];
    GLES20.glGetProgramiv(programObjectId, GLES20.GL_LINK_STATUS, linkStatus, 0);

    if (LoggerConfig.ON) {
        // 打印链接信息
        Log.v(TAG, "Results of linking program:\n"
                + GLES20.glGetProgramInfoLog(programObjectId));
    }

    // 6.验证链接状态
    if (linkStatus[0] == 0) {
        // 链接失败则删除程序对象
        GLES20.glDeleteProgram(programObjectId);
        if (LoggerConfig.ON) {
            Log.w(TAG, "Linking of program failed.");
        }
        // 7.返回程序对象:失败,为0
        return 0;
    }

    // 7.返回程序对象:成功,非0
    return programObjectId;
}

/**
 * 验证OpenGL程序对象状态
 *
 * @param programObjectId OpenGL程序ID
 * @return 是否可用
 */
public static boolean validateProgram(int programObjectId) {
    GLES20.glValidateProgram(programObjectId);

    final int[] validateStatus = new int[1];
    GLES20.glGetProgramiv(programObjectId, GLES20.GL_VALIDATE_STATUS, validateStatus, 0);
    Log.v(TAG, "Results of validating program: " + validateStatus[0]
            + "\nLog:" + GLES20.glGetProgramInfoLog(programObjectId));

    return validateStatus[0] != 0;
}

3. 获取GLSL中的索引

根据索引的类型,调用不同的方法去获取索引,索引的值类型都是int

// 获取顶点坐标属性在OpenGL程序中的索引
aPositionLocation = GLES20.glGetAttribLocation(mProgram, A_POSITION);

// 获取颜色Uniform在OpenGL程序中的索引
uColorLocation = GLES20.glGetUniformLocation(mProgram, U_COLOR);

4.1 将数据传递到Native层内存缓冲中

/**
 * Float类型占4Byte
 */
private static final int BYTES_PER_FLOAT = 4;

/**
 * 创建一个FloatBuffer
 */
public static FloatBuffer createFloatBuffer(float[] array) {
    FloatBuffer buffer = ByteBuffer
            // 分配顶点坐标分量个数 * Float占的Byte位数
            .allocateDirect(array.length * BYTES_PER_FLOAT)
            // 按照本地字节序排序
            .order(ByteOrder.nativeOrder())
            // Byte类型转Float类型
            .asFloatBuffer();

    // 将Java Dalvik的内存数据复制到Native内存中
    buffer.put(array);
    return buffer;
}

4.2 将内存堆中的值传递给GLSL引用

接下来,我们把顶点信息传递给GLSL中的顶点位置引用

// 将缓冲区的指针移动到头部,保证数据是从最开始处读取
mVertexData.position(0);
// 关联顶点坐标属性和缓存数据
// 1. 位置索引;
// 2. 每个顶点属性需要关联的分量个数(必须为1、2、3或者4。初始值为4。);
// 3. 数据类型;
// 4. 指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)(只有使用整数数据时)
// 5. 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。
// 6. 数据缓冲区
GLES20.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES20.GL_FLOAT,
        false, 0, mVertexData);

// 通知GL程序使用指定的顶点属性索引
GLES20.glEnableVertexAttribArray(aPositionLocation);

然后,我们给图形上色

// 更新u_Color的值,即更新画笔颜色
GLES20.glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);

最后,再根据需求绘制不同的图形。当前案例中,我就只绘制一个点。

// 使用数组绘制图形:1.绘制的图形类型;2.从顶点数组读取的起点;3.从顶点数组读取的数据长度
GLES20.glDrawArrays(GLES20.GL_POINTS, 0, 1);

注意:这里一定要先上色,再绘制图形,否则会导致颜色在当前这一帧使用失败,要下一帧才能生效。

刷屏颜色

// 设置刷新屏幕时候使用的颜色值,顺序是RGBA,值的范围从0~1。这里不会立刻刷新,只有在GLES20.glClear调用时使用该颜色值才刷新。
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
// 使用glClearColor设置的颜色,刷新Surface
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

注意

Buffer数据在传递给GLSL之前,一定要调用position方法将指针移到正确的位置,当前是0,之后会有课程讲解到非0的情况。

// 将缓冲区的指针移动到头部,保证数据是从最开始处读取
mVertexData.position(0);

将数组数据put进buffer之后,指针并不是在首位,所以一定要position到0,至关重要!否则会有很多奇妙的错误!如:

java.lang.ArrayIndexOutOfBoundsException: remaining() < count < needed

效果

基础框架效果图

参考

Android OpenGL ES学习资料所列举的博客、资料。

GitHub代码工程

本系列课程所有相关代码请参考我的GitHub项目GLStudio

课程目录

本系列课程目录详见 简书 - Android OpenGL ES教程规划

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