【安卓音视频开发升级打怪之路】 开发入门(一):初识OpenGLES

1,853 阅读9分钟

本文正在参加「金石计划」

大家好,我是小余,本篇文章是OpenGL ES系列的第一篇文章。主要通过一个基础的三角形绘制来带大家了解OpenGL ES的基础知识,作为一个入门文章。

OpenGLES是什么?

OpenGLES是OpenGL的一个子集,也就是OpenGL的一个精简指令集,主要用于嵌入式设备,如手机,平板等,本质上是一个跨编程语言,跨平台的编程接口规范。注意是规范,或者你简单可以理解为就是一个接口API。

image.png

OpenGLES如何工作的?

讲到如何工作,一定要知道OpenGl的图形渲染管线,指将一些基础数据,如顶点数据,颜色,纹理等数据作为输入,经过多个变化处理,最终输出到屏幕上一个过程。这个过程大致包括:顶点着色器,图元装备,几何着色器,光栅化,片段着色器,测试与混合这六个阶段

img

对于我们GLES来说,大部分情况我们只需要和顶点着色器以及片段着色器打交道,其他的我们暂时不需要去了解。

顶点着色器

顶点着色器的主要作用是将3D坐标转换为另外一个坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。顶点着色器是几个可编程着色器之一。

输入:3D坐标数据

输出:变化后的坐标数据

编程语言:GLSL

片段着色器

片段着色器是一个给你的片段进行上色的一个步骤。假设需要对不同片段进行不同的处理,那么就需要获取当前片段的位置坐标,如何获取呢?还记得刚才说的顶点着色器么,他可以输出每个片段的位置属性信息,然后在片段着色器中作为输入属性就可以了。 输入:各种属性数据,不只限于位置属性,对于一些需要光照场景,还会添加一个物体材质,法向量,光照属性等

输出:当前片段的颜色值

编程语言:GLSL

GLSL是什么? GLSL(OpenGL Shading Language),着色器编程语言,也就是说用来编写我们着色器的。他的写法和C语言的写法很相似。

下面是一个最简单的顶点着色器的编写:

#version 300 es
layout (location = 0) in vec3 position;
void main()
{
   gl_Position = vec4(position, 1.0f);
}
  • 第1行:用来指定当前使用的GLES的版本

  • 第2行:in代表这个是一个输入属性,position是当前属性的name,这个自己随意指定,vec3表示当前的position属性是一个三位坐标系,layout (location = 0):表示当前属性的location值为0,在外部指定属性编号的时候会用到,后面讲demo的时候会说到。

  • 第5行:main里面的gl_Position代表当前顶点位置属性的输出,这个名称不能更改,后面处理会默认使用这个名称。此处的gl_Position代表一个4分量的向量,注意最后一个值并非位置分量,而是用来处理透视划分使用到的,暂时不用去深入,默认为1.0.

既然是编程语言那就需要去编译。

编译着色器

首先编译着色器之前我们需要去创建一个着色器对象,gl api中已经给我们提供了:

GLuint vertexShader; 
vertexShader = glCreateShader(GL_VERTEX_SHADER);

这里的vertexShader就是一个着色器对象的索引ID。GL_VERTEX_SHADER:表示当前创建的是一个顶点着色器。

其次我们需要将着色器源码附加到这个着色器对象中:

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);

第三个参数vertexShaderSource表示GLSL源码地址,最后编译:

glCompileShader(vertexShader);

对于片段着色器也是同样的编译方法。

那编译完成之后就可以使用了么?

着色器程序

着色器程序对象是多个着色器合并之后,链接的版本,前面创建的着色器对象需要使用,一定需要将他们链接到一个着色器对象上之后才能使用。如何链接?

首先我们需要创建一个着色器程序:

GLuint shaderProgram;
shaderProgram = glCreateProgram();

然后用glLinkProgram链接着色器对象:

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

最后在绘制之前调用:

glUseProgram(shaderProgram);

之后所有的操作都是针对前面编写的着色器对象了。

链接顶点属性

顶点着色器其实不止会传入顶点数据作为输入,比如对颜色以及法向量等也可以使用顶点数组的形式传入。

所以就需要区分当前输入的数据对应的到底是哪一个属性。

一般我们的顶点数据会被解析成下面这种形式:

img

  • 位置数据被储存为32-bit(4字节)浮点值
  • 每个位置包含3个这样的值。每个位置代表当前x,y,z分量
  • 这几个值在数组中紧密排列,没有缝隙。
  • 数据中的第一个值在缓冲开始的位置。

有了上面的顶点数组信息以后,我们就可以使用OpenGl给我们提供的属性api来指定属性了。

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer参数定义:

  • ** 第一个参数0**:你可能还记得我们前面分析顶点着色器的时候说的那个location值,那个location值就是在这里使用的,当指定为0,着色器对象将会将当前属性分配给着色器中对应location为0的变量。
  • 第二个参数3:指定了当前属性占用的长度,对于位置属性,我们一般使用vec3类型,也就是3分量x,y,z来表示,所以这里我们指定长度为3,一些其他情况,如纹理坐标,这里就可能为2了,因为只需要用到纹理坐标只需要使用到x和y分量。
  • 第3个参数GL_FLOAT:指定了数据的类型,GLSL中vec*都是由浮点数值组成的。
  • 第4个参数GL_FALSE:是否希望数据被标准化,标准化后的数据都会在0和1之间或者-1和1之间。
  • 第5个参数3 * sizeof(GLfloat):表示到下一个同类型的属性的内存间隙,这里我们只有一种属性,且属性的长度为3,所以间隔为3,这里说的是内存之间的距离,所以使用了3 * sizeof(GLfloat)。
  • ** 第6个参数(GLvoid*)0**:表示离起始位置的偏移量,这里只有一种属性,所以偏移量为0,如果有多种属性,那么就不会只为0了。

glEnableVertexAttribArray(0):表示启用当前location为0的顶点属性,默认关闭。

VAO与VBO

VBO:顶点缓冲对象,在顶点着色器处理阶段,我们使用VBO,在GPU上提前创建一块内存,用于缓存顶点数据。这样可以避免在每次绘制的时候都需要重新发送数据给GPU,毕竟这是一个比较耗时的操作。

VAO:顶点数组对象,VAO的主要作用是用来管理VBO或者EBO, ,减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,提高顶点数组切换步骤。

VAO 与 VBO 之间的关系

VAO 与 VBO 之间的关系

有了以下基础之后,我们再来绘制一个三角形

绘制一个三角形

应用层我们使用GLSurfaceView作为媒介,因为其内部封装了EGL环境的搭建,所以我们只需要把它引入到我们的项目中即可。

public class MyGLSurfaceView extends GLSurfaceView {
​
    Renderer mRenderer;
    public MyGLSurfaceView(Context context) {
        this(context,null);
    }
​
    public MyGLSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setEGLContextClientVersion(2);
        setEGLConfigChooser(8,8,8,8,16,8);
    }
​
    public void setmRenderer(MyGLSurfaceRenderer mRenderer) {
        this.mRenderer = mRenderer;
        setRenderer(mRenderer);
        setRenderMode(mRenderer.rendererMode);
    }
​
}

我们还自定义了个Renderer

public class MyGLSurfaceRenderer implements GLSurfaceView.Renderer {
    int rendererMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY;
    MyNativeRenderer nativeRenderer;
    MyGLSurfaceRenderer(){
        nativeRenderer = new MyNativeRenderer();
    }
    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
        nativeRenderer.native_onSurfaceCreated();
    }
​
    @Override
    public void onSurfaceChanged(GL10 gl10, int width, int height) {
        nativeRenderer.native_onSurfaceChanged(width,height);
    }
​
    @Override
    public void onDrawFrame(GL10 gl10) {
        nativeRenderer.native_onDrawFrame();
    }
​
    public void destroy(){
        nativeRenderer.native_destroy();
    }
}

并搭建一个MyNativeRenderer用来和native层进行操作,直接使用的是native层的opengles接口进行开发。

其实也可以使用java层的GLES接口进行处理,但是这种方式一方面移植性不好,opengl还是有很多优秀的开源c/c++库,使用java来开发就无法移植了,所以小余选择使用了native层进行开发,另外一方面也是为了巩固自己对jni层的理解。

public class MyNativeRenderer {
    static {
        System.loadLibrary("opengl-es-lib");
    }
    public MyNativeRenderer(){
​
    }
    public native void native_onSurfaceCreated();
    public native void native_onSurfaceChanged(int width,int height);
    public native void native_onDrawFrame();
    public native void native_destroy();
​
}

在jni层,我们引入GLES类库,代码如下:

MyGLRenderContext* MyGLRenderContext::context = nullptr;
//单例类
MyGLRenderContext * MyGLRenderContext::getInstance() {
    if(context == nullptr){
        context = new MyGLRenderContext();
    }
    return context;
}
void MyGLRenderContext::OnSurfaceCreated() {
    glClearColor(0.1f,0.2f,0.3f,1.0f);
​
}
​
void MyGLRenderContext::OnSurfaceChanged(int width, int height) {
    glViewport(0,0,width,height);
}
/**
 * 绘制前操作
 * 0.初始化顶点数据
 * 1.创建着色器程序对象
 * 2.生成VAO,VBO对象
 * */
void MyGLRenderContext::beforeDraw() {
    if(programObj!= 0){
        return;
    }
    //0.初始化顶点数据
    GLfloat vertices[] = {
            0.0f, 0.5f, 0.0f,1.0f,0.0f,0.0f,
            -0.5f,-0.5f, 0.0f,0.0f,1.0f,0.0f,
            0.5f, -0.5f, 0.0f,0.0f,0.0f,1.0f
    };
    //1.创建着色器程序,此处将着色器程序创建封装到一个工具类中
    char vShaderStr[] =
            "#version 300 es                          \n"
            "layout(location = 0) in vec4 vPosition;  \n"
            "layout(location = 1) in vec3 vColor;  \n"
            "out vec3 color;  \n"
            "void main()                              \n"
            "{                                        \n"
            "   gl_Position = vPosition;              \n"
            "   color = vColor;              \n"
            "}                                        \n";
​
    char fShaderStr[] =
            "#version 300 es                              \n"
            "precision mediump float;                     \n"
            "in vec3 color;                          \n"
            "out vec4 fragColor;                          \n"
            "void main()                                  \n"
            "{                                            \n"
            "   fragColor = vec4 (color, 1.0 );  \n"
            "}                                            \n";
​
    programObj = GLUtils::CreateProgram(vShaderStr,fShaderStr);
​
    //2.生成VAO,VBO对象,并绑定顶点属性
    GLuint VBO;
    glGenVertexArrays(1,&VAO);
    glGenBuffers(1,&VBO);
​
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER,VBO);
    glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
​
    //顶点坐标属性
    glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(GLfloat),(GLvoid*)0);
    glEnableVertexAttribArray(0);
    //顶点颜色属性
    glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6*sizeof(GLfloat),(GLvoid*)(3*sizeof(GLfloat)));
    glEnableVertexAttribArray(1);
​
    glBindVertexArray(GL_NONE);
}
/**
 * 1.清除buffer
 * 2.使用程序着色器对象
 * 3.开始绘制
 * 4.解绑
 * */
void MyGLRenderContext::OnDrawFrame() {
    beforeDraw();
    if(programObj == 0){
        return;
    }
    //清除buffer
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(0.3f,0.5f,0.4f,1.0f);
    //使用程序着色器对象
    glUseProgram(programObj);
    //绑定VAO
    glBindVertexArray(VAO);
    //开始绘制
    glDrawArrays(GL_TRIANGLES,0,3);
    //解绑VAO
    glBindVertexArray(GL_NONE);
    //解绑程序着色器对象
    glUseProgram(GL_NONE);
}
void MyGLRenderContext::destroy() {
    if(programObj){
        programObj = GL_NONE;
    }
    glDeleteVertexArrays(1,&VAO);
}

我们使用一个单列类来处理三角形的初始化以及绘制操作,由于着色器的创建以及以及链接操作都是公用的,我们将其封装在一个GLUtils的类中:

GLuint GLUtils::CreateProgram(const char *pVertexShaderSource, const char *pFragShaderSource, GLuint &vertexShaderHandle, GLuint &fragShaderHandle)
{
    GLuint program = 0;
    FUN_BEGIN_TIME("GLUtils::CreateProgram")
        vertexShaderHandle = LoadShader(GL_VERTEX_SHADER, pVertexShaderSource);
        if (!vertexShaderHandle) return program;
        fragShaderHandle = LoadShader(GL_FRAGMENT_SHADER, pFragShaderSource);
        if (!fragShaderHandle) return program;

        program = glCreateProgram();
        if (program)
        {
            glAttachShader(program, vertexShaderHandle);
            CheckGLError("glAttachShader");
            glAttachShader(program, fragShaderHandle);
            CheckGLError("glAttachShader");
            glLinkProgram(program);
            GLint linkStatus = GL_FALSE;
            glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);

            glDetachShader(program, vertexShaderHandle);
            glDeleteShader(vertexShaderHandle);
            vertexShaderHandle = 0;
            glDetachShader(program, fragShaderHandle);
            glDeleteShader(fragShaderHandle);
            fragShaderHandle = 0;
            if (linkStatus != GL_TRUE)
            {
                GLint bufLength = 0;
                glGetProgramiv(program, GL_INFO_LOG_LENGTH, &bufLength);
                if (bufLength)
                {
                    char* buf = (char*) malloc((size_t)bufLength);
                    if (buf)
                    {
                        glGetProgramInfoLog(program, bufLength, NULL, buf);
                        LOGCATE("GLUtils::CreateProgram Could not link program:\n%s\n", buf);
                        free(buf);
                    }
                }
                glDeleteProgram(program);
                program = 0;
            }
        }
    FUN_END_TIME("GLUtils::CreateProgram")
    LOGCATE("GLUtils::CreateProgram program = %d", program);
   return program;
}

结合前面给的一些基础理论知识,相信你是能看懂的。 效果

image.png

代码已经上传到github:需要的自行下载。

好了,本篇文章只是OpenGLES的入门文章,后续还是推出其他相关文章。

另外本人整理了一些关于Android开发进阶以及面试的一些指导: 关注小余,回复“资料”免费获取。