文章目录
在 Android 平台有两种方法可以构建 OpenGL 环境:通过系统自带的 GLSurfaceView 组件和通过 EGL 。GLSurfaceView 内部自带了 OpenGL 的渲染线程,通过 EGL 需要自己创建渲染线程。
Android 平台通过 OpenGL 渲染图像的本质是通过一系列的 GPU 指令将绘制的图形或者获得的图像帧(本地/网络视频解码或摄像头采集)渲染到设备屏幕上的过程,这个屏幕在系统中被抽象为 Surface ,而图形和图像则被抽象为 Texture (纹理),纹理通过顶点和片段着色器映射到 Surface 上。顶点着色器可以简单理解为确定每个点在屏幕上的坐标,而片段着色器可以简单理解为确定每个点的颜色,即一个计算位置,一个计算颜色。OpenGL 的编程模式为线性编程,即上一段程序的输出是下一段程序的输入。
以绘制三角形为例:
GLSurfaceView
在 Android 的 OpenGL 环境中,用纹理 ID 来标识纹理 ,它实际上指向的是 GPU 中分配的一块内存区域,用于存储纹理数据, SurfaceTexture
将视频帧数据存储在这个内存区域中。
初始设置
// 创建一个OpenGL ES 2.0的上下文
setEGLContextClientVersion(2);
// 仅在绘制数据发生变化时才进行绘制
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
// 设置渲染器
setRenderer(...);
创建 Renderer
public class MyGLRenderer implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 设置清除屏幕所用的颜色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
}
@Override
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// 设置视口
GLES20.glViewport(0, 0, width, height);
}
}
编写着色器
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
"}";
private final String fragmentShaderCode =
"precision mediump float;" +
"uniform vec4 vColor;" +
"void main() {" +
" gl_FragColor = vColor;" +
"}";
编译着色器
public static int loadShader(int type, String shaderCode){
// 创建一个着色器类型(GLES20.GL_VERTEX_SHADER 或 GLES20.GL_FRAGMENT_SHADER)
int shader = GLES20.glCreateShader(type);
// 将源码添加到着色器并编译它
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}
链接着色器
int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderCode);
int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
int mProgram = GLES20.glCreateProgram(); // 创建一个空的OpenGL程序
GLES20.glAttachShader(mProgram, vertexShader); // 将顶点着色器添加到程序中
GLES20.glAttachShader(mProgram, fragmentShader); // 将片段着色器添加到程序中
GLES20.glLinkProgram(mProgram); // 创建可执行的OpenGL程序
定义图形数据
static float triangleCoords[] = { // 按逆时针方向顺序
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
private final FloatBuffer vertexBuffer;
// 设置颜色
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// 初始化顶点字节缓冲区
ByteBuffer bb = ByteBuffer.allocateDirect(
// (每个浮点数占用4个字节)
triangleCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(triangleCoords);
vertexBuffer.position(0);
}
绘制图形
public void draw() {
// 将程序添加到OpenGL环境
GLES20.glUseProgram(mProgram);
// 获取顶点着色器的位置
int positionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// 启用顶点着色器的句柄
GLES20.glEnableVertexAttribArray(positionHandle);
// 准备三角形的坐标数据
GLES20.glVertexAttribPointer(positionHandle, COORDS_PER_VERTEX,
GLES20.GL_FLOAT, false,
vertexStride, vertexBuffer);
// 获取片段着色器的颜色句柄
int colorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
// 设置绘制三角形的颜色
GLES20.glUniform4fv(colorHandle, 1, color, 0);
// 绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(positionHandle);
}
EGL
EGL (Embedded-System Graphics Library) 是一个接口层,连接了 OpenGL ES 等图形库和显示设备(Surface),GLSurfaceView 就是对 EGL 的封装。它有几个关键概念:
EGLDisplay:表示与本地窗口系统的连接。
EGLConfig:描述绘图表面的格式和类型的配置,包括颜色深度、缓冲区类型等。
EGLContext:表示OpenGL ES等图形库的渲染上下文。
EGLSurface:表示一个可以渲染的表面,例如窗口表面、Pixmap表面或Pbuffer表面。
它们的关系可以简单理解为 EGLSurface 和 EGLContext 通过 EGLDisplay 连接。
创建 EGL 显示和初始化
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
int[] version = new int[2];
EGL14.eglInitialize(eglDisplay, version, 0, version, 1);
配置 EGL
int[] configAttribs = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, configs.length, numConfigs, 0);
EGLConfig eglConfig = configs[0];
创建 EGL 上下文
int[] contextAttribs = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
EGLContext eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
创建 EGL Surface
Surface surface = ...; // 获取或创建一个 Android Surface
int[] surfaceAttribs = {
EGL14.EGL_NONE
};
// 创建一个 EGLSurface,关联到一个 Android Surface。
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surface, surfaceAttribs, 0);
编写着色器
编译和链接着色器
定义图形数据
均同上
连接 EGL 上下文和显示设备
// 绑定 EGL 上下文和 Surface
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
// 获取属性位置
int positionHandle = GLES20.glGetAttribLocation(program, "vPosition");
int colorHandle = GLES20.glGetUniformLocation(program, "vColor");
// 启用顶点数组
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 12, vertexBuffer);
// 设置颜色
GLES20.glUniform4fv(colorHandle, 1, color, 0);
// 清屏并绘制三角形
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
// 交换显示缓冲区。GLSurfaceView 自动处理了缓冲区的交换。在渲染循环的每一帧结束时,它会调用 EGL 函数来交换前后缓冲区。这些操作是通过 GLSurfaceView
// 的内部类 GLThread 来完成的。
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
// 禁用顶点数组
GLES20.glDisableVertexAttribArray(positionHandle);
释放 EGL 和 OpenGL 资源
GLES20.glDeleteProgram(program);
GLES20.glDeleteShader(vertexShader);
GLES20.glDeleteShader(fragmentShader);
EGL14.eglDestroySurface(eglDisplay, eglSurface);
EGL14.eglDestroyContext(eglDisplay, eglContext);
EGL14.eglTerminate(eglDisplay);
使用 GLSurfaceView 和 EGL 渲染本地视频帧的 Demo 地址:
https://github.com/cmder/Demos/tree/main/Mp4Player