一看就懂的OpenGL ES教程——项目搭建与EGL配置

4,631 阅读12分钟

系列文章目录

体系化学习系列博文,请看音视频系统学习总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目 欢迎各位来star~

相关专栏:

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

一看就懂的OpenGL es教程

通过阅读本文,你将获得以下收获:

1.如何在安卓平台上搭建基于ndk的OpenGL项目
2.EGL的概念以及如何配置EGL

上篇回顾

前面2篇博文,轻松入门OpenGL ES——图形渲染管线的那些事
轻松入门OpenGL ES——再谈OpenGL工作机制基本已经描述了OpenGL整个工作流程机制,因为都是理论的东西,可能读者会觉得显示比较枯燥也比较空,没办法,路要一步一步走,饭要一口一口吃。幸运的是,今天就可以开始实战啦,经过今天的实战,你们才能真切感受到前2篇博文的意义之大

今天的目标是完成一个三角形的绘制:

image.png

一个看似简单的三角形,却曾经是无数初学者的梦魇,也让我当年初学折腾了许久。不过不记紧张,之所以这是很多初学者的梦魇是因为很多初学者并没有理解OpenGL的工作流程就开始写代码实战,所以由于你们有幸经过前面2节课的熏陶,加上你们的聪明,相信这并不难。

1658569020665.png

因为笔者是从事Android平台开发的,所以今天就在基于Android平台来绘制这个三角形

OpenGL es

有没有注意到,我的标题是“轻松入门OpenGL ES——XX”,但是前面2篇博文讲的都是OpenGL,那么二者之间有什么关系呢?

只要是概念,那第一时间照搬官方已经成为条件反射:

image.png

官方叙述如下:

OpenGL® ES is a royalty-free, cross-platform API for rendering advanced 2D and 3D graphics on embedded and mobile systems - including consoles, phones, appliances and vehicles. It consists of a well-defined subset of desktop OpenGL suitable for low-power devices, and provides a flexible and powerful interface between software and graphics acceleration hardware.

翻译过来就是:

OpenGL ES是一种免版税的跨平台API,用于在嵌入式和移动系统(包括控制台、电话、家电和车辆)上渲染高级2D和3D图形。 它是桌面OpenGL的子集,用于适合低功耗设备,并在软件和图形加速硬件之间提供灵活而强大的接口。

和OpenGL很相似,重点是用于嵌入式和移动系统,是OpenGL的子集,即是OpenGL的裁剪版本,裁减掉了不适合低功耗的嵌入式和移动系统的Api,那作为移动端巨头级别的Android系统,自然用的就是OpenGL ES,那么今天的三角形,自然用的就是OpenGL ES。

Android平台上绘制三角形

那么我们马不停蹄,马上开始编码吧。之前已经写过ndk系列文章初探ndk的世界(一) 初探ndk的世界(二),在这里不如趁热打铁,直接写一个ndk项目来学习OpenGL。如果对ndk开发不熟悉的话,建议先看下这两篇文章初探ndk的世界(一) 初探ndk的世界(二)

4c1d65488154871eb0fb43b356c44d6e.gif

基本代码框架搭建

使用Android studio创建一个ndk项目:

1658633038973.png

于是项目的基本结如下图所示,添加2个Java文件如下:

1658633152722.png

CMakeList文件内容如下:

1658633204132.png

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib
                       # OpenGL es依赖库
                       GLESv3 
                       # EGL依赖库
                       EGL 
                       android
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

主要是引用了2个库,分别是GLESv3和EGL前者是为了使用OpenGL ES 3.0,后者等会会有详细的解释。

MainActivity代码:

class MainActivity : AppCompatActivity() {
    // Used to load the 'native-lib' library on application startup.

    companion object {
        init {
            //加载生成的so文件
            System.loadLibrary("native-lib")
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    external fun stringFromJNI(): String?
}

啥也不干,大道至简,就只干一件事,就是加载生成的so文件。

YuvPlayer代码:

public class YuvPlayer extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {

    public YuvPlayer(Context context, AttributeSet attrs) {
        super(context, attrs);
        setRenderer(this);

  
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
     Log.d("YuvPlayer","surfaceCreated");
        //在Surface创建的时候启动绘任务
        new Thread(this).start();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {

    }

    @Override
    public void run() {
      //绘制三角形
     drawTriangle(getHolder().getSurface());
     
    }

    
   
    /**
     * 绘制三角形的native方法
     * @param surface
     */

    public native void drawTriangle(Object surface);

    

  
}

Android平台上,已经提供了GLSurfaceView控件专门用于渲染OpenGL es,GLSurfaceView需要通过Render类的回调来获取它的生命周期,一般是如下是三个生命周期方法:

@Override
public void surfaceCreated(SurfaceHolder holder) {
   //surface创建时回调
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    //surface销毁时回调
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    //surface属性发送变化时回调,比如尺寸和颜色格式发生变化的时候
}

在这里,只要在surface创建时调用drawTriangle去绘制三角形即可。所以重点就是进入瞧瞧drawTriangle究竟在C++层做了什么

8c91a9fdabdfe71abd543276c6d8750f.jpeg
extern "C"
JNIEXPORT void JNICALL
Java_com_example_openglstudydemo_YuvPlayer_drawTriangle(JNIEnv *env, jobject thiz,
                                                        jobject surface) {

    /**        此处开始EGL的配置              **/
    ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);
   
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (display == EGL_NO_DISPLAY) {
        LOGD("egl display failed");
        return;
    }
  
    if (EGL_TRUE != eglInitialize(display, 0, 0)) {
        LOGD("eglInitialize failed");
        return;
    }

   
    EGLConfig eglConfig;
    EGLint configNum;
    EGLint configSpec[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_NONE
    };

    if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) {
        LOGD("eglChooseConfig failed");
        return;
    }

   
    EGLSurface winSurface = eglCreateWindowSurface(display, eglConfig, nwin, 0);
    if (winSurface == EGL_NO_SURFACE) {
        LOGD("eglCreateWindowSurface failed");
        return;
    }

    
    const EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
    };
   
    EGLContext context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);
    if (context == EGL_NO_CONTEXT) {
        LOGD("eglCreateContext failed");
        return;
    }
   
    if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
        LOGD("eglMakeCurrent failed");
        return;
    }
    
    /**        此处结束EGL的配置              **/

    GLint vsh = initShader(vertexSimpleShape, GL_VERTEX_SHADER);
    GLint fsh = initShader(fragSimpleShape, GL_FRAGMENT_SHADER);

   
    GLint program = glCreateProgram();
    if (program == 0) {
        LOGD("glCreateProgram failed");
        return;
    }

   
    glAttachShader(program, vsh);
    glAttachShader(program, fsh);

   
    glLinkProgram(program);
    GLint status = 0;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    if (status == 0) {
        LOGD("glLinkProgram failed");
        return;
    }
    LOGD("glLinkProgram success");
    
    glUseProgram(program);

    static float triangleVer[] = {
            0.8f, -0.8f, 0.0f,
            -0.8f, -0.8f, 0.0f,
            0.0f, 0.8f, 0.0f,
    };

    GLuint apos = static_cast<GLuint>(glGetAttribLocation(program, "aPosition"));
    glEnableVertexAttribArray(apos);
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, triangleVer);

    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
   
    eglSwapBuffers(display, winSurface);
}

好了,OpenGL画一个三角形的代码就这么少,大家自行理解,本文结束,下次见。

1658644711730.png

咳咳,开玩笑的……这才是本文的重点。那么接下来,就要用抽丝剥茧的方式,用“行”级的方式讲解这一段代码,结合之前讲的工作机制,让你们彻底明白这个三角形是这么画出来的(很多人可能是糊里糊涂画出来的,比如当年的我)。

image.png

讲解代码之前,首先要理解一个东西,叫做EGL

EGL

khronos官方文档中描述

EGL™ is an interface between Khronos rendering APIs such as OpenGL ES or OpenVG and the underlying native platform window system. It handles graphics context management, surface/buffer binding, and rendering synchronization and enables high-performance, accelerated, mixed-mode 2D and 3D rendering using other Khronos APIs. EGL also provides interop capability between Khronos to enable efficient transfer of data between APIs – for example between a video subsystem running OpenMAX AL and a GPU running OpenGL ES.

简单来说,EGL 是 类似OpenGL ES这样的 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,EGL作为OpenGL ES与显示设备的桥梁,让OpenGL ES绘制的内容能够在呈现当前设备上。它主要由系统制造商实现。

简单的图表示它们的关系如下:

1658658364667.png

OpenGL ES只负责处理数据,但是并不知道如何渲染到本地窗口,即屏幕,所以必须把数据交给对本地窗口熟念于心的EGL,因为EGL是系统制造商实现的,而系统又是和硬件关系最紧密的,所以系统制造商才知道怎么去实现对图像数据的渲染。通过EGL,也成全了OpenGL ES跨平台能力。当然除了OpenGL ES有EGL,桌面版本的OpenGL也需要类似EGL的东西,比如WINDOWS系统就需要WGL,基本作用也是差不多。

详细来说,EGL的作用如下:
a:与设备的原生窗口系统通信。
b:查询绘图表面的可用类型和配置。
c:创建绘图表面。
d:在OpenGL ES 和其他图形渲染API之间同步渲染。
e:管理纹理贴图等渲染资源。

a8d56f6c8e85f3435018ee4dd2461f53.jpeg

接下来就是代码解析环节了。

首先有几个关键概念要明白: 1.Display(EGLDisplay) 是对实际显示设备的抽象,即不管是手机屏幕还是电脑屏幕或者其他各种形形式式的显示屏幕,在代码里面都化身为EGLDisplay。这也是EGL帮我们隔离了具体的显示设备

2.Surface(EGLSurface)是对用来存储图像的内存区域帧缓存(FrameBuffer )的抽象是设计来存储渲染相关的输出数据额外。通俗来说就是一个存放辅助缓冲的图像数据(颜色缓冲、模板缓冲、深度缓冲)的容器。

3.Context (EGLContext) 存储 OpenGL ES绘图的一些状态信息。记得在上一篇文章轻松入门OpenGL ES——再谈OpenGL工作机制里,说过OpenGL是一个状态机么,那么它的各种状态其实就是存储在这个EGLContext中的,记录了OpenGL渲染需要的所有信息和状态

1.内部状态信息(View port, depth range, clear color, textures, VBO, FBO, ...)
2.调用缓存,保存了在这个Context下发起的GL调用指令。(OpenGL 调用是异步的)

EGLContext是线程相关的,一个线程只有绑定了一个EGLContext之后,才可以使用OpenGL es进行绘制。当然不同的EGLContext就维护了不同组的状态机。另外同一个EGLContext也可以被不同线程共享,但是不能同时被不同线程绑定。

接下来逐行解释EGL的配置代码:

    //1.获取原始窗口,ANativeWindow就是Surface在Native的对应物。这里的surface参数即从Java层穿过来的Surface对象
    ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);
    //获取OpenGL es渲染目标Display,EGL_DEFAULT_DISPLAY表示默认的Display
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (display == EGL_NO_DISPLAY) {
        LOGD("egl display failed");
        return;
    }
    //2.初始化egl Diaplay的连接,后两个参数是指针,是分别用来返回EGL主次版本号
    if (EGL_TRUE != eglInitialize(display, 0, 0)) {
        LOGD("eglInitialize failed");
        return;
    }

    //返回的EGL帧缓存配置
    EGLConfig eglConfig;
    //配置数量
    EGLint configNum;
    //期望的EGL帧缓存配置列表,配置为一个key一个value的形式,以下的EGL_RED_SIZE、EGL_GREEN_SIZE、EGL_BLUE_SIZE分别表示EGL帧缓冲中的颜色缓冲一个颜色通道用多少位表示。
    //指定EGL surface类型
    EGLint configSpec[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_NONE
    }; 

    //返回一个和期望的EGL帧缓存配置列表configSpec匹配的EGL帧缓存配置列表,存储在eglConfig中
    if (EGL_TRUE != eglChooseConfig(display, configSpec, &eglConfig, 1, &configNum)) {
        LOGD("eglChooseConfig failed");
        return;
    }

    //通过egl和NativeWindow以及EGL帧缓冲配置创建EGLSurface。最后一个参数为属性信息,0表示不需要属性)
    EGLSurface winSurface = eglCreateWindowSurface(display, eglConfig, nwin, 0);
    if (winSurface == EGL_NO_SURFACE) {
        LOGD("eglCreateWindowSurface failed");
        return;
    }

    //渲染上下文EGLContext关联的帧缓冲配置列表,EGL_CONTEXT_CLIENT_VERSION表示这里是配置EGLContext的版本,
    const EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
    };
    //通过Display和上面获取到的的EGL帧缓存配置列表创建一个EGLContext, EGL_NO_CONTEXT表示不需要多个设备共享上下文
    EGLContext context = eglCreateContext(display, eglConfig, EGL_NO_CONTEXT, ctxAttr);
    if (context == EGL_NO_CONTEXT) {
        //EGL_NO_CONTEXT表示创建上下文失败
        LOGD("eglCreateContext failed");
        return;
    }
    //将EGLContext和当前线程以及draw和read的EGLSurface关联,关联之后,当前线程就成为了OpenGL es的渲染线程
    
    if (EGL_TRUE != eglMakeCurrent(display, winSurface, winSurface, context)) {
        LOGD("eglMakeCurrent failed");
        return;
    }

EGL的配置工作究竟干了啥呢?我们来回顾一下:

Untitled (2).png

1.首先通过Java层传入的Surface对象转化为Native层的ANativeWindow对象。
2.通过eglGetDisplay方法得到设备的抽象对象EGLDisplay。
3.通过eglChooseConfig方法以及传入的期望EGL帧缓冲列表得到最匹配的EGL帧缓存配置列表。
4.通过1得到的ANativeWindow对象、2得到的EGLDisplay对象、3得到的EGL帧缓存配置列表得到EGLSurface对象。
5.通过EGLDisplay对象和3得到的EGL帧缓存配置列表得到EGLContext上下文对象。
6.将EGLContext和当前线程以及draw和read的EGLSurface关联,关联之后,当前线程就成为了OpenGL es的渲染线程。所谓draw和read就是分别是写和读的缓冲区。

关于EGL配置,详细可参见EGL Reference Pages,个人觉得博文也没必要去搬太多官方文档里面的东西,所以大家有需要的话可以看下。

配置好EGL环境之后,就相当给OpenGL es和显示设备之间打通了一条管道,于是便可以开始使用OpenGL es进行绘制了,也就要开始前面2篇文章说过的图形渲染管线了:

image.png

第一个步骤,就是组织好要渲染的数据,然后交给顶点着色器,介于篇幅关系,还有为了防止各位看久了打瞌睡,本文就先到这,下一篇博文将继续这个三角形的绘制,从组织好要渲染的数据和顶点着色器讲起~

fe0014dbdf6acf1ff11c7d72c523a46a.jpeg

总结

本文为使用OpenGL es在Andoird平台上绘制三角形的第一篇,主要是介绍了项目的基本结构相关的Java以及CmakeList文件并重点介绍了EGL以及如何配置EGL,当这一切准备就绪之后,下一篇博文一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)才是我们真正绘制三角形的开始。

代码地址

(项目代码将不断更新)
github.com/yishuinanfe…

参考

EGL Reference Pages
EGL 作用及其使用
Let’s talk about eglMakeCurrent, eglSwapBuffers, glFlush, glFinish

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿