阅读 140

NDK OpenGL ES渲染系列 之 绘制三角形

前言

新的知识学习都是循序渐进的,从基础到复杂。前面OpenGL ES概念 已经介绍了OpenGL ES的相关概念,这篇文章开始我们就正式开始OpenGL ES渲染系列第一站---绘制三角形。绘制三角形不涉及复杂的矩阵变换和纹理采样。渲染时OpenGL ES上下文并不是采用原生GLSurfaceView,而是自己参考GLSurfaceView流程在NDK层实现了一套EGL渲染的上下文逻辑,好处是自己更加清楚的了解了OpenGL ES上下文切换逻辑的实现。有兴趣可以参考文章OpenGL ES升级打怪之 GLSurfaceView源码分析的分析和实现。

在用法上和GLSurfaceView.Renderer用法基本一致,只需要关心以下三个方法实现:

  • onSurfaceCreated(ANativeWindow *nativeWindow)
  • onSurfaceChanged(ANativeWindow *nativeWindow, int format, int width, int height)
  • draw() //对应Render的onDrawFrame(gl: GL10?)

OpenGL ES渲染原理

OpenGL渲染原理涉及计算机图形学知识,有兴趣的同学可以研究一下。 图片4

OpenGL ES概念复习

着色器

着色器(Shader)是运行在GPU上的小程序,可以处理顶点和片元相关计算。我们只关心顶点着色器和片元着色器代码,着色器语言是GLSL。 主要用到的是顶点着色器和片元着色器

GLSL简介

OpenGL ES着色语言GLSL是一种高级图形化编程语言,其源自C语言,同时它提供了更加丰富的针对图像处理的原生类型,诸如向量、矩阵之类。

OpenGLES主要包含以下特性:

  • GLSL是一种高级面向过程的语言,(并不是面向对象);
  • GLSL的基本语法与C/C++基本相同;
  • 完美支持向量与矩阵的各种操作。
  • 通过类型限定符操作来管理输入输出类型的
  • GLSL提供了大量的内置函数来提供丰富的拓展功能。
  • 总之、OpenGL ES着色语言是一种易于实现、功能强大、便于使用,并且可以高度并行处理、性能优良的高级图形编程语言。

更加详细GLSL语法可以参考OpenGL之GLSL

顶点数组缓冲区(Vertex Array Object)

简称VAO, VAO是保存了所有顶点属性信息VBO的引用,本身不存储任何顶点信息。

顶点缓冲区(Vertex Buffer Object)

简称VBO, VBO是在GPU显存开辟一个内存空间,用于存放顶点各类属性信息,包括顶点坐标,顶点法向量,顶点颜色数据等。好处是渲染时可以直接从显存读取顶点属性信息,不需要从CPU传入,效率更高。

索引缓冲区(Element Buffer Object)

简称EBO,EBO是在GPU显存开辟一个内存空间,用于存放所有顶点位置索引indices。

绘制方式

OpenGL ES中支持的绘制方式大致分3类,包括点、线段、三角形,每类中包含一种或者多种绘制方式,各种具体绘制方式如下:

  • 点绘制方式:GL_POINTS;
  • 线段绘制方式:GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP;
  • 三角形绘制方式GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_F

图片2 图片3

如何实现OpenGL ES渲染

复习一下如何实现OpenGL ES渲染流程,实现渲染有以下几个步骤:

  • 加载顶点着色器和片元着色器代码
  • 编译着色器代码
  • 创建program编译链接已编译着色器
  • 加载顶点坐标和颜色坐标或者纹理坐标
  • 根据绘制方式画出相应的图形

绘制三角形

根据上述渲染流程,开始我们的渲染旅程吧,我们先创建一个TriangleFilter对象,没错,我把它理解为滤镜,其实和GLSurfaceView.Renderer功能是一样的。我自己实现的Demo里面使用了VBO和EBO分别存放顶点坐标数据和索引坐标数据。VBO和EBO的概念可以参考OpenGL ES概念的高级概念, 本文会介绍VBO和EBO如何使用。

初始化顶点坐标

DEMO使用了VBO存放顶点坐标数据和颜色数据。

GLfloat *vertex_color_coords = new GLfloat[] {
    // Positions        // Colors
    0.0f,   0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,   // Top Right
    -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,   // Bottom Right
    0.5f,  -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,   // Bottom Left
    };
复制代码

初始化索引坐标

DEMO使用了EBO存放绘制索引数据。

 GLshort *vertex_indexs = new GLshort[] {0, 1, 2};
复制代码

初始化着色器代码

DEMO中顶点着色器代码:

const char *vShaderStr =
            "#version 300 es                          \n"
            "layout(location = 0) in vec3 aPosition;  \n"
            "layout(location = 1) in vec4 aColor;     \n"
            "out vec4 vColor;                         \n"
            "void main()                              \n"
            "{                                        \n"
            "   gl_Position = vec4(aPosition, 1.0);   \n"
            "   vColor = aColor;                      \n"
            "}                                        \n";
复制代码

DEMO中片元着色器代码:

const char *fShaderStr =
            "#version 300 es                              \n"
            "precision mediump float;                     \n"
            "in vec4 vColor;                              \n"
            "out vec4 fragColor;                          \n"
            "void main()                                  \n"
            "{                                            \n"
            "   fragColor = vColor;                       \n"
            "}                                            \n";  
复制代码

上述代码中version 300 es是由于在OpenGL ES 3.0对应GLSL版本是3.0

编译和链接着色器

由于着色器代码是运行在GPU上的代码,所以我们不能直接使用的,那如何才能使用呢?我们需要编译着色器代码并链接到program实例上,可以理解这个program是一个特色程序指针,当program链接到Shader程序后,就可以拿program进行操作。

编译着色器

根据不同着色器类型进行编译,并返回着色器的特殊指针,这个特色指针只有大于0时才认为是可用的。

GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
复制代码

顶点着色器使用GL_VERTEX_SHADER类型,片元着色器使用GL_FRAGMENT_SHADER。 具体编译着色器代码如下:

GLuint GLShaderUtil::compileShader(GLenum type, const char *shaderSource) {
    DLOGD(GLShaderUtil_TAG, "~~~compileShader~~~\n");
    GLuint shader = glCreateShader(type);
    checkGlError("glCreateShader");
    if (shader == 0) {
        DLOGE(GLShaderUtil_TAG, "Could not create new shader.\n");
    }
    glShaderSource(shader, 1, &shaderSource, NULL);
    checkGlError("glShaderSource");
    glCompileShader(shader);
    checkGlError("glCompileShader");
    GLint compiled = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
    if (!compiled) {
        GLint infoLen = 0;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen) {
            char *buf = (char *) malloc(sizeof(char) * infoLen);
            if (buf) {
                glGetShaderInfoLog(shader, infoLen, NULL, buf);
                DFLOGE(GLShaderUtil_TAG, "Could not compile shader %d:\n%s\n", shader, buf);
            }
            free(buf);
        }
        glDeleteShader(shader);
        shader = 0;
    } else {
        DFLOGD(GLShaderUtil_TAG, "Create type = 0x%x, shader = %d Success! \n", type, shader);
    }
    return shader;
}
复制代码

这里需要和大家同步一点,就是OpenGL只是一个标准API, 实现是各个产商实现的,所以在我们定位问题时发现不能debug进入gpu进行调试,当然有些工具可以,比如RenderDoc(我并没使用这个工具,只是知道它的存在,有兴趣同学可以研究一下,研究后可以告知一下我)。可能有人会问如果在没有工具debug的时候我们怎么定位问题呢?这就是我要告诉大家的内容,我是使用glGetErrorglGetShaderInfoLog获取错误码和Shader错误日志。

下面这代码就是在执行完一个OpenGL API后检测错误的方法,返回的错误码都可以在<GLES3/gl3.h>头文件中,根据错误码你们就可以查阅相关资料定位问题,我的经验告诉我,错误码很重要,只要按照流程写代码,这些错误码是不会出现的,如果出现了要好好review一下你们自己的实现。(可能大家觉得是废话,当你们对比正常的代码review了好几遍自然就明白了)

static void checkGlError(const char* op) {
    GLint error;
    for (error = glGetError(); error; error = glGetError()) {
        if (DebugEnable && GL_ERROR_DEBUG) {
            DFLOGE(GL_ERROR_TAG, "error::after %s() glError (0x%x)\n", op, error);
        }
    }
}
复制代码

链接着色器

接下来就是创建program和链接着色器程序,program只有大于0才有意义。

GLuint program = linkProgram(vertexShader, fragmentShader);
复制代码

具体链接着色器代码如下:

GLuint GLShaderUtil::linkProgram(GLuint vertexShader, GLuint fragmentShader) {
    DLOGD(GLShaderUtil_TAG, "~~~linkProgram~~~\n");
    GLuint program = glCreateProgram();
    if (program) {
        glAttachShader(program, vertexShader);
        checkGlError("glAttachShader");
        glAttachShader(program, fragmentShader);
        checkGlError("glAttachShader");
        glLinkProgram(program);
        checkGlError("glLinkProgram");
        glDetachShader(program, vertexShader);
        glDetachShader(program, fragmentShader);
        glDeleteShader(vertexShader);
        glDeleteShader(fragmentShader);
        GLint linkStatus = 0;
        glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
        if (!linkStatus) {
            GLint infoLen = 0;
            glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);
            if (infoLen) {
                char *buf = (char *) malloc(sizeof(char) * infoLen);
                if (buf) {
                    glGetProgramInfoLog(program, infoLen, NULL, buf);
                    DFLOGE(GLShaderUtil_TAG, "Could not link program %d:\n%s\n", program, buf);
                }
                free(buf);
            }
            glDeleteProgram(program);
            program = 0;
        }
    } else {
        DLOGE(GLShaderUtil_TAG, "Could not create program.\n");
    }
    return program;
}
复制代码

加载顶点坐标和颜色数据

本Demo使用VAO, VBO存储顶点坐标信息和颜色数据。 VAO和VBO初始化:

    //generate vao vbo
    glGenVertexArrays(1, &vao); //生成VAO对象
    checkGlError("glGenVertexArrays");
    glGenVertexArrays(1, &vbo); //生成VBO对象
    checkGlError("glGenVertexArrays")
复制代码

加载顶点数据和颜色数据到VBO:

    // Bind VAO
    glBindVertexArray(vao); //绑定VAO对象
    checkGlError("glBindVertexArray");
    glBindBuffer(GL_ARRAY_BUFFER, vbo);//绑定VBO对象
    checkGlError("glBindBuffer vbo");
    glBufferData(GL_ARRAY_BUFFER, VERTEX_COLOR_COORDS_LENGTH * BYTES_PER_FLOAT, vertex_color_coords, GL_STATIC_DRAW);//填充缓冲对象管理的内存,分配了一块显存空间,然后把vertex_color_coords存入其中
    checkGlError("glBufferData vbo");
    // Position attribute
    glEnableVertexAttribArray(0);//使能Shader中location = 0的对象
    checkGlError("glEnableVertexAttribArray position");
    glVertexAttribPointer(0, COORDS_PER_VERTEX, GL_FLOAT, false, STRIDE_PRE_COORD * BYTES_PER_FLOAT,(const void *) nullptr); //指定顶点属性数组的数据格式和位置
    checkGlError("glVertexAttribPointer position");
    // Color attribute
    glEnableVertexAttribArray(1);//使能Shader中location = 1的对象
    checkGlError("glEnableVertexAttribArray color");
    glVertexAttribPointer(1, COORDS_PER_COLOR, GL_FLOAT, false, STRIDE_PRE_COORD * BYTES_PER_FLOAT,(const void *) (COORDS_PER_VERTEX * BYTES_PER_FLOAT));//指定颜色数组的数据格式和位置
    checkGlError("glVertexAttribPointer color");
复制代码

加载索引数据

本DEMO使用EBO存放索引数据 EBO初始化:

    //generate ebo
    glGenVertexArrays(1, &ebo); //生成EBO对象
    checkGlError("glGenVertexArrays");
复制代码

加载索引数据到EBO:

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    checkGlError("glBindBuffer ebo");
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, VERTEX_INDICES_LENGTH * BYTES_PER_SHORT, vertex_indexs, GL_STATIC_DRAW);
    checkGlError("glBufferData ebo");
复制代码

绘制

OpenGL ES提供两种API进行绘制,分别是glDrawArraysglDrawElements,两者的区别是glDrawElements可以根据指定的顶点索引进行渲染。这里就渲染了glDrawElements

    // 使用EBO
    glDrawElements(GL_TRIANGLES, VERTEX_INDICES_LENGTH, GL_UNSIGNED_SHORT, (const void *) nullptr);
    // 不使用EBO
    glDrawElements(GL_TRIANGLES, VERTEX_INDICES_LENGTH, GL_UNSIGNED_SHORT, vertex_indexs);
复制代码

可以看到使用EBO和不使用EBO的传入数据不同,使用EBO则传入指针偏移值,不使用EBO则传入指针。

效果图

WechatIMG18-w540

源码传送门

参考文章 OpenGL之GLSL

文章分类
Android
文章标签