一、前言
OpenGL定义了一个跨编程语言、跨平台的专业图形程序接口。可用于二维或三维图像的处理与渲染,它是一个功能强大、调用方便的底层图形库。对于嵌入式设备,其提供了OpenGL ES(OpenGL for Embedded Systems)版本。
由于OpenGL是跨编程语言、跨平台的设计,所以在每个平台上都要有它的具体实现,负责提供OpenGL的上下文环境以及窗口的管理。在Android平台使用EGL提供本地平台对OpenGL ES的实现。
二、搭建OpenGL上下文环境
我们的目标是实现一个小Demo:在Android手机上利用OpenGL绘制一个三角形出来。
当然我们可以直接使用Android提供的GLSurfaceView,通过这种方式使用OpenGL比较简单,不需要我们再去搭建OpenGL上下文环境。本文我们不会去使用GLSurfaceView,而是直接借助EGL的API,在C++环境搭建出OpenGL上下文环境。
2.1 添加EGL库
首先,我们用Android Studio创建一个Native C++工程出来,编写CMakeList.txt文件,加入EGL库,文件内容如下:
cmake_minimum_required(VERSION 3.10.2)
project("eglrender")
add_library( # Sets the name of the library.
egl-render
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
EglJni.cpp
common/looper.cpp
common/MyLooper.cpp
render/GLRender.cpp
render/Triangle.cpp
gles/EglCore.cpp
gles/GLUtils.cpp )
find_library(log-lib log)
find_library(EGL-lib EGL)
find_library(GLESv2-lib GLESv2)
target_link_libraries(
# Specifies the target library.
egl-render
android
${log-lib}
${EGL-lib}
${GLESv2-lib} )
关注target_link_libraries选项,在链接时,我们添加了android、EGL-lib、GLESv2-lib三个库。在搭建OpenGL上下文环境,以及执行OpenGL程序需要用到这三个库中的方法。
2.2 创建EGL上下文
首先,需要获取显示设备,通过eglGetDisplay方法获取
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == EGL_NO_DISPLAY) {
return false;
}
然后,调用eglInitialize()来初始化这个显示设备:
// 不关心版本号,直接传0即可
if (!eglInitialize(mEGLDisplay, 0, 0)) {
return false;
}
EGL有了Display之后,我们需要准备配置选项,比如:色彩格式、像素格式、SurfaceType等。最终通过eglChooseConfig得到配置选项信息。
const EGLint attribList[] = {EGL_BUFFER_SIZE, 32,
EGL_ALPHA_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_NONE };
// 获取config
EGLConfig config;
int numConfigs;
if (!eglChooseConfig(mEGLDisplay, attribList, &config, 1, &numConfigs)) {
return false;
}
有了EGLDisplay和EGLConfig之后,接下来就可以创建上下文环境:EGLContext了,通过eglCreateContext完成。
int attrib2_list[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
EGLContext context = eglCreateContext(mEGLDisplay, config, sharedContext, attrib2_list);
2.2 创建显示EGLSurface
有了上下文EGLContext之后,接下来需要创建EGLSurface,这是用来指定OpenGL渲染输出的目的地。就Android平台来说,它代表SurfaceView的surface,surface对象将从java传到native层。
override fun surfaceCreated(holder: SurfaceHolder) {
onSurfaceCreated(holder.surface)
}
对应的JNI代码如下:
extern "C" JNIEXPORT void JNICALL
Java_com_baidu_eglrender_MainActivity_onSurfaceCreated(
JNIEnv *env, jobject instance, jobject surface) {
if (mWindow) {
ANativeWindow_release(mWindow);
mWindow = NULL;
}
mWindow = ANativeWindow_fromSurface(env, surface);
if (mLooper) {
LOGD("post MsgSurfaceCreated")
mLooper->post(MsgSurfaceCreated, mWindow);
}
}
我们通过ANativeWindow_fromSurface方法从surface中拿到ANativeWindow对象指针。然后再根据ANativeWindow指针创建出EGLSurface对象
EGLSurface EglCore::createWindowSurface(ANativeWindow *window) {
if (window == NULL) {
return NULL;
}
int surfaceAttribs[] = {
EGL_NONE
};
mEglSurface = eglCreateWindowSurface(mEGLDisplay, mEGLConfig, window, surfaceAttribs);
if (mEglSurface == NULL) {
return NULL;
}
return mEglSurface;
}
需要明确一点,OpenGL的渲染操作需要在一个新的线程中执行,而且还必须为该线程绑定显示设备(surface)和上下文环境(Context),这样才可以执行OpenGL指令。
if (!eglMakeCurrent(mEGLDisplay, mEglSurface, mEglSurface, mEGLContext)) {
LOGD("eglMakeCurrent error")
}
我们还需要创建一个线程,用来执行OpenGL指令。我们计划在Native层实现一个Looper 线程,功能类似Android的异步消息机制。这个Looper线程的设计可参考官方native-codec项目,本文就不展开了。
三、执行OpenGL绘制
接下来我们可以执行OpenGL指令,绘制一个三角形出来。我们编写一个Triangle.cpp实现具体功能。
#include "Triangle.h"
const GLint COORDS_PER_VERTEX = 3;
const GLint vertexStride = COORDS_PER_VERTEX * 4;
Triangle::Triangle() {}
Triangle::~Triangle() {}
int Triangle::init() {
char vertexShader[] =
"#version 300 es\n"
"layout(location = 0) in vec4 a_position;\n"
"layout(location = 1) in vec4 a_color;\n"
"out vec4 v_color;"
"void main()\n"
"{\n"
" gl_Position = a_position;\n"
" v_color = a_color;\n"
"}\n";
char fragmentShader[] =
"#version 300 es\n"
"precision mediump float;\n"
"in vec4 v_color;\n"
"out vec4 fragColor;\n"
"void main()\n"
"{\n"
" fragColor = v_color;\n"
"}\n";
// 根据着色器,创建GL程序
programHandle = createProgram(vertexShader, fragmentShader);
if (programHandle <= 0) {
LOGD("create program error, programHandle=%d\n", programHandle)
return -1;
}
// 清屏
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
return 0;
}
void Triangle::onDraw(int width, int height) {
// 顶点坐标
GLfloat vertices[] = {
0.0f, 0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f
};
// 三角形颜色
GLfloat color[] = {
1.0f, 0.0f, 0.0f, 1.0f
};
GLint vertexCount = sizeof(vertices) / (sizeof(vertices[0]) * COORDS_PER_VERTEX);
glViewport(0, 0, width, height);
glClear(GL_COLOR_BUFFER_BIT);
// 使用Program
glUseProgram(programHandle);
// 获取GL程序顶点着色器的a_position变量
GLint positionHandle = glGetAttribLocation(programHandle, "a_position");
// 设置顶点位置信息
glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX, GL_FLOAT, GL_FALSE, vertexStride,
vertices);
glEnableVertexAttribArray(positionHandle);
// 设置片远着色器的color变量
glVertexAttrib4fv(1, color);
// 执行绘制
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
glDisableVertexAttribArray(positionHandle);
}
void Triangle::destroy() {
if (programHandle > 0) {
glDeleteProgram(programHandle);
}
programHandle = -1;
}
看下init方法,首先创建顶点着色器vertexShader和片元着色器代码fragmentShader,然后根据这两个着色器调用createProgram创建出GL程序。该方法会返回一个programe句柄。接着在onDraw()方法中就是具体的绘制逻辑,最终通过glDrawArrays()方法完成OpenGL绘制。
离屏幕展示渲染结果还差最后一步:调用eglSwapBuffers()方法,完成前后台FrameBuffer交换。代码如下:
void GLRender::surfaceChanged(int width, int height) {
// 执行OpenGL指令,绘制三角形
mTriangle->onDraw(width, height);
// 交换前后台FrameBuffer
mEglCore->swapBuffers();
}
// 调用eglSwapBuffers交换前后台FrameBuffer
bool EglCore::swapBuffers(EGLSurface eglSurface) {
return eglSwapBuffers(mEGLDisplay, eglSurface);
}
这里说明下EGL的双缓冲模式:EGL内部有两个FrameBuffer(帧缓冲区),当EGL将一个FrameBuffer显示到屏幕上的时候,另一个FrameBuffer就在后台等待OpenGL ES进行渲染输出了。直到调用eglSwapBuffers指令,把前台FrameBuffer和后台FrameBuffer进行交换,这样用户就可以在屏幕上看到刚才OpenGL ES渲染输出的结果了。
编译运行程序,就可以看到Demo成功绘制了一个红色的三角形,如下图所示:
源码地址:EGLRender