系列文章目录
体系化学习系列博文,请看音视频系统学习总目录
实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目 欢迎各位来star~
相关专栏:
通过阅读本文,你将获得以下收获:
1.如何在安卓平台上搭建基于ndk的OpenGL项目
2.EGL的概念以及如何配置EGL
上篇回顾
前面2篇博文,轻松入门OpenGL ES——图形渲染管线的那些事
轻松入门OpenGL ES——再谈OpenGL工作机制基本已经描述了OpenGL整个工作流程机制,因为都是理论的东西,可能读者会觉得显示比较枯燥也比较空,没办法,路要一步一步走,饭要一口一口吃。幸运的是,今天就可以开始实战啦,经过今天的实战,你们才能真切感受到前2篇博文的意义之大。
今天的目标是完成一个三角形的绘制:
一个看似简单的三角形,却曾经是无数初学者的梦魇,也让我当年初学折腾了许久。不过不记紧张,之所以这是很多初学者的梦魇是因为很多初学者并没有理解OpenGL的工作流程就开始写代码实战,所以由于你们有幸经过前面2节课的熏陶,加上你们的聪明,相信这并不难。
因为笔者是从事Android平台开发的,所以今天就在基于Android平台来绘制这个三角形。
OpenGL es
有没有注意到,我的标题是“轻松入门OpenGL ES——XX”,但是前面2篇博文讲的都是OpenGL,那么二者之间有什么关系呢?
只要是概念,那第一时间照搬官方已经成为条件反射:
官方叙述如下:
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的世界(二)
基本代码框架搭建
使用Android studio创建一个ndk项目:
于是项目的基本结如下图所示,添加2个Java文件如下:
CMakeList文件内容如下:
# 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++层做了什么。
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画一个三角形的代码就这么少,大家自行理解,本文结束,下次见。
咳咳,开玩笑的……这才是本文的重点。那么接下来,就要用抽丝剥茧的方式,用“行”级的方式讲解这一段代码,结合之前讲的工作机制,让你们彻底明白这个三角形是这么画出来的(很多人可能是糊里糊涂画出来的,比如当年的我)。
讲解代码之前,首先要理解一个东西,叫做EGL。
EGL
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绘制的内容能够在呈现当前设备上。它主要由系统制造商实现。
简单的图表示它们的关系如下:
OpenGL ES只负责处理数据,但是并不知道如何渲染到本地窗口,即屏幕,所以必须把数据交给对本地窗口熟念于心的EGL,因为EGL是系统制造商实现的,而系统又是和硬件关系最紧密的,所以系统制造商才知道怎么去实现对图像数据的渲染。通过EGL,也成全了OpenGL ES跨平台能力。当然除了OpenGL ES有EGL,桌面版本的OpenGL也需要类似EGL的东西,比如WINDOWS系统就需要WGL,基本作用也是差不多。
详细来说,EGL的作用如下:
a:与设备的原生窗口系统通信。
b:查询绘图表面的可用类型和配置。
c:创建绘图表面。
d:在OpenGL ES 和其他图形渲染API之间同步渲染。
e:管理纹理贴图等渲染资源。
接下来就是代码解析环节了。
首先有几个关键概念要明白: 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的配置工作究竟干了啥呢?我们来回顾一下:
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篇文章说过的图形渲染管线了:
第一个步骤,就是组织好要渲染的数据,然后交给顶点着色器,介于篇幅关系,还有为了防止各位看久了打瞌睡,本文就先到这,下一篇博文将继续这个三角形的绘制,从组织好要渲染的数据和顶点着色器讲起~
总结
本文为使用OpenGL es在Andoird平台上绘制三角形的第一篇,主要是介绍了项目的基本结构和相关的Java以及CmakeList文件并重点介绍了EGL以及如何配置EGL,当这一切准备就绪之后,下一篇博文一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)才是我们真正绘制三角形的开始。
代码地址
(项目代码将不断更新)
github.com/yishuinanfe…
参考
EGL Reference Pages
EGL 作用及其使用
Let’s talk about eglMakeCurrent, eglSwapBuffers, glFlush, glFinish
原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。