Android OpenGL ES从白痴到入门(四):离屏渲染(
2017-06-02 本文已影响3753人
云华兄
注:涉及太专业的知识请自行保留怀疑态度!
一本正经的胡说八道
上一节我们只是把情丝斩断了,还是没偷偷摸摸的干点见不得人的事,这节我们就来吧!
首先,我们来看EGL创建EGLSurface有三个方法:eglCreateWindowSurface()、eglCreatePbufferSurface()和eglCreatePixmapSurface()。这三者有什么不同呢?
- WindowSurface
顾名思义WindowSurface是和窗口相关的,也就是在屏幕上的一块显示区的封装,渲染后即显示在界面上。 - PbufferSurface
在显存中开辟一个空间,将渲染后的数据(帧)存放在这里。 - PixmapSurface
以位图的形式存放在内存中,据说各平台的支持不是很好。
做离屏渲染我们可以选择PbufferSurface或者PixmapSurface(其实WindowSurface也是可以的),什么?你说FBO(Frame Buffer Object),这种高深的学问身为小菜鸡的我根本就不懂好吗(帧缓存对象FBO是对帧缓存的封装,性能要优于Pbuffer但并非可以完全替代Pbuffer)。
OpenGL操作的最终目标实际上是帧缓存(Frame Buffer)后面的各种表现形式则是EGL对Frame Buffer的封装,这么理解会不会好点?
代码整理
这节的知识相对简单,我们先把之前的代码整理一下:
- 新建GLSurface类,对EGLSurface进行封装
- 将GLRenderer类改为抽象类,继承于Thread
- GLRenderer添加一个阻塞队列(消息队列),用于交互和解耦
- GLRenderer添加一个Event内部类
- GLRenderer中添加生命周期,将渲染之类的具体工作放到实现类中
- 添加ShaderUtil类,将着色器创建的代码封装到这个类中
GLSurface.java
public class GLSurface {
public static final int TYPE_WINDOW_SURFACE = 0;
public static final int TYPE_PBUFFER_SURFACE = 1;
public static final int TYPE_PIXMAP_SURFACE = 2;
protected final int type;
protected Object surface; // 显示控件(支持SurfaceView、SurfaceHolder、Surface和SurfaceTexture)
protected EGLSurface eglSurface = EGL14.EGL_NO_SURFACE;
protected Viewport viewport = new Viewport();
public GLSurface(int width, int height) {
setViewport(0, 0, width, height);
surface = null;
type = TYPE_PBUFFER_SURFACE;
}
public GLSurface(Surface surface, int width, int height) {
this(surface,0,0,width,height);
}
public GLSurface(Surface surface, int x, int y, int width, int height) {
setViewport(x, y, width, height);
this.surface = surface;
type = TYPE_WINDOW_SURFACE;
}
public void setViewport(int x, int y, int width, int height){
viewport.x = x;
viewport.y = y;
viewport.width = width;
viewport.height = height;
}
public void setViewport(Viewport viewport){
this.viewport = viewport;
}
public Viewport getViewport(){
return viewport;
}
public static class Viewport{
public int x;
public int y;
public int width;
public int height;
}
}
GLRenderer.java
public abstract class GLRenderer extends Thread {
private static final String TAG = "GLThread";
private EGLConfig eglConfig = null;
private EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY;
private EGLContext eglContext = EGL14.EGL_NO_CONTEXT;
private ArrayBlockingQueue<Event> eventQueue;
private final List<GLSurface> outputSurfaces;
private boolean rendering;
private boolean isRelease;
public GLRenderer() {
setName("GLRenderer-" + getId());
outputSurfaces = new ArrayList<>();
rendering = false;
isRelease = false;
eventQueue = new ArrayBlockingQueue<>(100);
}
private boolean makeOutputSurface(GLSurface surface) {
// 创建Surface缓存
try {
switch (surface.type) {
case GLSurface.TYPE_WINDOW_SURFACE: {
final int[] attributes = {EGL14.EGL_NONE};
// 创建失败时返回EGL14.EGL_NO_SURFACE
surface.eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, surface.surface, attributes, 0);
break;
}
case GLSurface.TYPE_PBUFFER_SURFACE: {
final int[] attributes = {
EGL14.EGL_WIDTH, surface.viewport.width,
EGL14.EGL_HEIGHT, surface.viewport.height,
EGL14.EGL_NONE};
// 创建失败时返回EGL14.EGL_NO_SURFACE
surface.eglSurface = EGL14.eglCreatePbufferSurface(eglDisplay, eglConfig, attributes, 0);
break;
}
case GLSurface.TYPE_PIXMAP_SURFACE: {
Log.w(TAG, "nonsupport pixmap surface");
return false;
}
default:
Log.w(TAG, "surface type error " + surface.type);
return false;
}
} catch (Exception e) {
Log.w(TAG, "can't create eglSurface");
surface.eglSurface = EGL_NO_SURFACE;
return false;
}
return true;
}
public void addSurface(@NonNull final GLSurface surface){
Event event = new Event(Event.ADD_SURFACE);
event.param = surface;
if(!eventQueue.offer(event))
Log.e(TAG,"queue full");
}
public void removeSurface(@NonNull final GLSurface surface){
Event event = new Event(Event.REMOVE_SURFACE);
event.param = surface;
if(!eventQueue.offer(event))
Log.e(TAG,"queue full");
}
/**
* 开始渲染
* 启动线程并等待初始化完毕
*/
public void startRender(){
if(!eventQueue.offer(new Event(Event.START_RENDER)))
Log.e(TAG,"queue full");
if(getState()==State.NEW) {
super.start(); // 启动渲染线程
}
}
public void stopRender(){
if(!eventQueue.offer(new Event(Event.STOP_RENDER)))
Log.e(TAG,"queue full");
}
public boolean postRunnable(@NonNull Runnable runnable){
Event event = new Event(Event.RUNNABLE);
event.param = runnable;
if(!eventQueue.offer(event)) {
Log.e(TAG, "queue full");
return false;
}
return true;
}
@Override
public void start() {
Log.w(TAG,"Don't call this function");
}
public void requestRender(){
eventQueue.offer(new Event(Event.REQ_RENDER));
}
/**
* 创建OpenGL环境
*/
private void createGL() {
// 获取显示设备(默认的显示设备)
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
// 初始化
int[] version = new int[2];
if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
throw new RuntimeException("EGL error " + EGL14.eglGetError());
}
// 获取FrameBuffer格式和能力
int[] configAttribs = {
EGL14.EGL_BUFFER_SIZE, 32,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
EGL14.EGL_NONE
};
int[] numConfigs = new int[1];
EGLConfig[] configs = new EGLConfig[1];
if (!EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, configs.length, numConfigs, 0)) {
throw new RuntimeException("EGL error " + EGL14.eglGetError());
}
eglConfig = configs[0];
// 创建OpenGL上下文(可以先不设置EGLSurface,但EGLContext必须创建,
// 因为后面调用GLES方法基本都要依赖于EGLContext)
int[] contextAttribs = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
};
eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
if (eglContext == EGL14.EGL_NO_CONTEXT) {
throw new RuntimeException("EGL error " + EGL14.eglGetError());
}
// 设置默认的上下文环境和输出缓冲区(小米4上如果不设置有效的eglSurface后面创建着色器会失败,可以先创建一个默认的eglSurface)
//EGL14.eglMakeCurrent(eglDisplay, surface.eglSurface, surface.eglSurface, eglContext);
EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, eglContext);
}
/**
* 销毁OpenGL环境
*/
private void destroyGL() {
EGL14.eglDestroyContext(eglDisplay, eglContext);
eglContext = EGL14.EGL_NO_CONTEXT;
eglDisplay = EGL14.EGL_NO_DISPLAY;
}
/**
* 渲染到各个eglSurface
*/
private void render(){
// 渲染(绘制)
for(GLSurface output:outputSurfaces){
if(output.eglSurface== EGL_NO_SURFACE) {
if(!makeOutputSurface(output))
continue;
}
// 设置当前的上下文环境和输出缓冲区
EGL14.eglMakeCurrent(eglDisplay, output.eglSurface, output.eglSurface, eglContext);
// 设置视窗大小及位置
GLES20.glViewport(output.viewport.x, output.viewport.y, output.viewport.width, output.viewport.height);
// 绘制
onDrawFrame(output);
// 交换显存(将surface显存和显示器的显存交换)
EGL14.eglSwapBuffers(eglDisplay, output.eglSurface);
}
}
@Override
public void run() {
Event event;
Log.d(TAG,getName()+": render create");
createGL();
onCreated();
// 渲染
while(!isRelease){
try {
event = eventQueue.take();
switch(event.event){
case Event.ADD_SURFACE: {
// 创建eglSurface
GLSurface surface = (GLSurface)event.param;
Log.d(TAG,"add:"+surface);
makeOutputSurface(surface);
outputSurfaces.add(surface);
break;
}
case Event.REMOVE_SURFACE: {
GLSurface surface = (GLSurface)event.param;
Log.d(TAG,"remove:"+surface);
EGL14.eglDestroySurface(eglDisplay, surface.eglSurface);
outputSurfaces.remove(surface);
break;
}
case Event.START_RENDER:
rendering = true;
break;
case Event.REQ_RENDER: // 渲染
if(rendering) {
onUpdate();
render(); // 如果surface缓存没有释放(被消费)那么这里将卡住
}
break;
case Event.STOP_RENDER:
rendering = false;
break;
case Event.RUNNABLE:
((Runnable)event.param).run();
break;
case Event.RELEASE:
isRelease = true;
break;
default:
Log.e(TAG,"event error: "+event);
break;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 回调
onDestroy();
// 销毁eglSurface
for(GLSurface outputSurface:outputSurfaces){
EGL14.eglDestroySurface(eglDisplay, outputSurface.eglSurface);
outputSurface.eglSurface = EGL_NO_SURFACE;
}
destroyGL();
eventQueue.clear();
Log.d(TAG,getName()+": render release");
}
/**
* 退出OpenGL渲染并释放资源
* 这里先将渲染器释放(renderer)再退出looper,因为renderer里面可能持有这个looper的handler,
* 先退出looper再释放renderer可能会报一些警告信息(sending message to a Handler on a dead thread)
*/
public void release(){
if(eventQueue.offer(new Event(Event.RELEASE))){
// 等待线程结束,如果不等待,在快速开关的时候可能会导致资源竞争(如竞争摄像头)
// 但这样操作可能会引起界面卡顿,择优取舍
while (isAlive()){
try {
this.join(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 当创建完基本的OpenGL环境后调用此方法,可以在这里初始化纹理之类的东西
*/
public abstract void onCreated();
/**
* 在渲染之前调用,用于更新纹理数据。渲染一帧调用一次
*/
public abstract void onUpdate();
/**
* 绘制渲染,每次绘制都会调用,一帧数据可能调用多次(不同是输出缓存)
* @param outputSurface 输出缓存位置surface
*/
public abstract void onDrawFrame(GLSurface outputSurface);
/**
* 当渲染器销毁前调用,用户回收释放资源
*/
public abstract void onDestroy();
private static String getEGLErrorString() {
return GLUtils.getEGLErrorString(EGL14.eglGetError());
}
private static class Event {
static final int ADD_SURFACE = 1; // 添加输出的surface
static final int REMOVE_SURFACE = 2; // 移除输出的surface
static final int START_RENDER = 3; // 开始渲染
static final int REQ_RENDER = 4; // 请求渲染
static final int STOP_RENDER = 5; // 结束渲染
static final int RUNNABLE = 6; //
static final int RELEASE = 7; // 释放渲染器
final int event;
Object param;
Event(int event) {
this.event = event;
}
}
}
ShaderUtil.java
public class ShaderUtil {
/**
* 加载制定shader的方法
* @param shaderType shader的类型 GLES20.GL_VERTEX_SHADER GLES20.GL_FRAGMENT_SHADER
* @param source shader的脚本字符串
* @return 着色器id
*/
private static int loadShader(int shaderType,String source) {
// 创建一个新shader
int shader = GLES20.glCreateShader(shaderType);
// 若创建成功则加载shader
if (shader != 0) {
//加载shader的源代码
GLES20.glShaderSource(shader, source);
//编译shader
GLES20.glCompileShader(shader);
//存放编译成功shader数量的数组
int[] compiled = new int[1];
//获取Shader的编译情况
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {//若编译失败则显示错误日志并删除此shader
Log.e("ES20_ERROR", "Could not compile shader " + shaderType + ":");
Log.e("ES20_ERROR", GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
}
return shader;
}
/**
* 创建shader程序的方法
*/
public static int createProgram(String vertexSource, String fragmentSource) {
//加载顶点着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
if (vertexShader == 0) {
return 0;
}
//加载片元着色器
int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
if (pixelShader == 0) {
return 0;
}
//创建程序
int program = GLES20.glCreateProgram();
//若程序创建成功则向程序中加入顶点着色器与片元着色器
if (program != 0) {
//向程序中加入顶点着色器
GLES20.glAttachShader(program, vertexShader);
checkGlError("glAttachShader");
//向程序中加入片元着色器
GLES20.glAttachShader(program, pixelShader);
checkGlError("glAttachShader");
//链接程序
GLES20.glLinkProgram(program);
//存放链接成功program数量的数组
int[] linkStatus = new int[1];
//获取program的链接情况
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
//若链接失败则报错并删除程序
if (linkStatus[0] != GLES20.GL_TRUE) {
Log.e("ES20_ERROR", "Could not link program: ");
Log.e("ES20_ERROR", GLES20.glGetProgramInfoLog(program));
GLES20.glDeleteProgram(program);
program = 0;
}
}
return program;
}
//检查每一步操作是否有错误的方法
public static void checkGlError(String op) {
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e("ES20_ERROR", op + ": glError " + error);
throw new RuntimeException(op + ": glError " + error);
}
}
//从sh脚本中加载shader内容的方法
public static String loadFromAssetsFile(String fname, Resources r) {
String result = null;
try {
InputStream in = r.getAssets().open(fname);
int ch = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((ch = in.read()) != -1) {
baos.write(ch);
}
byte[] buff = baos.toByteArray();
baos.close();
in.close();
result = new String(buff, "UTF-8");
result = result.replaceAll("\\r\\n", "\n");
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
离屏渲染关键部分就是在makeOutputSurface()方法中的GLSurface.TYPE_PBUFFER_SURFACE这个case里面,仅9行代码,简单吧(指定大小,使用EGL14.eglCreatePbufferSurface()创建)
测试
工程整理完毕,添加一些测试代码
- 新建TestRenderer继承于GLRenderer,在onCreated()中创建着色器和顶点,在onDrawFrame()中进行绘制
TestRenderer.java
public class TestRenderer extends GLRenderer {
private static final String TAG = "TestRenderer";
private int program;
private int vPosition;
private int uColor;
private FloatBuffer vertices;
/**
* 获取图形的顶点
* 特别提示:由于不同平台字节顺序不同数据单元不是字节的一定要经过ByteBuffer
* 转换,关键是要通过ByteOrder设置nativeOrder(),否则有可能会出问题
*
* @return 顶点Buffer
*/
private FloatBuffer getVertices() {
float vertices[] = {
0.0f, 0.5f,
-0.5f, -0.5f,
0.5f, -0.5f,
};
// 创建顶点坐标数据缓冲
// vertices.length*4是因为一个float占四个字节
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
vbb.order(ByteOrder.nativeOrder()); //设置字节顺序
FloatBuffer vertexBuf = vbb.asFloatBuffer(); //转换为Float型缓冲
vertexBuf.put(vertices); //向缓冲区中放入顶点坐标数据
vertexBuf.position(0); //设置缓冲区起始位置
return vertexBuf;
}
@Override
public void onCreated() {
//基于顶点着色器与片元着色器创建程序
program = createProgram(verticesShader, fragmentShader);
// 获取着色器中的属性引用id(传入的字符串就是我们着色器脚本中的属性名)
vPosition = GLES20.glGetAttribLocation(program, "vPosition");
uColor = GLES20.glGetUniformLocation(program, "uColor");
vertices = getVertices();
}
@Override
public void onUpdate() {
}
@Override
public void onDrawFrame(GLSurface surface) {
// 设置clear color颜色RGBA(这里仅仅是设置清屏时GLES20.glClear()用的颜色值而不是执行清屏)
GLES20.glClearColor(1.0f, 0, 0, 1.0f);
// 清除深度缓冲与颜色缓冲(清屏,否则会出现绘制之外的区域花屏)
GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
// 使用某套shader程序
GLES20.glUseProgram(program);
// 为画笔指定顶点位置数据(vPosition)
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertices);
// 允许顶点位置数据数组
GLES20.glEnableVertexAttribArray(vPosition);
// 设置属性uColor(颜色 索引,R,G,B,A)
GLES20.glUniform4f(uColor, 0.0f, 1.0f, 0.0f, 1.0f);
// 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 3);
}
@Override
public void onDestroy() {
}
// 顶点着色器的脚本
private static final String verticesShader
= "attribute vec2 vPosition; \n" // 顶点位置属性vPosition
+ "void main(){ \n"
+ " gl_Position = vec4(vPosition,0,1);\n" // 确定顶点位置
+ "}";
// 片元着色器的脚本
private static final String fragmentShader
= "precision mediump float; \n" // 声明float类型的精度为中等(精度越高越耗资源)
+ "uniform vec4 uColor; \n" // uniform的属性uColor
+ "void main(){ \n"
+ " gl_FragColor = uColor; \n" // 给此片元的填充色
+ "}";
}
- 在activity_main.xml添加一个ImageView用于显示渲染结果
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_main_image"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center"
android:contentDescription="@string/app_name" />
<SurfaceView
android:id="@+id/sv_main_demo"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
- 修改MainActivity
在onCreate()中创建一个PbufferSurface,加入渲染器,启动渲染,取回像素数据(要在OpenGL线程)转换成Bitmap设置给ImageView
MainActivity.java
public class MainActivity extends Activity {
private TestRenderer glRenderer;
private ImageView imageIv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SurfaceView sv = (SurfaceView)findViewById(R.id.sv_main_demo);
imageIv = (ImageView)findViewById(R.id.iv_main_image);
glRenderer = new TestRenderer();
GLSurface glPbufferSurface = new GLSurface(512,512);
glRenderer.addSurface(glPbufferSurface);
glRenderer.startRender();
glRenderer.requestRender();
glRenderer.postRunnable(new Runnable() {
@Override
public void run() {
IntBuffer ib = IntBuffer.allocate(512 * 512);
GLES20.glReadPixels(0, 0, 512, 512, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, ib);
final Bitmap bitmap = frameToBitmap(512, 512, ib);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
imageIv.setImageBitmap(bitmap);
}
});
}
});
sv.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
GLSurface glWindowSurface = new GLSurface(surfaceHolder.getSurface(),width,height);
glRenderer.addSurface(glWindowSurface);
glRenderer.requestRender();
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
}
});
}
@Override
protected void onDestroy() {
glRenderer.release();
glRenderer = null;
super.onDestroy();
}
/**
* 将数据转换成bitmap(OpenGL和Android的Bitmap色彩空间不一致,这里需要做转换)
*
* @param width 图像宽度
* @param height 图像高度
* @param ib 图像数据
* @return bitmap
*/
private static Bitmap frameToBitmap(int width, int height, IntBuffer ib) {
int pixs[] = ib.array();
// 扫描转置(OpenGl:左上->右下 Bitmap:左下->右上)
for (int y = 0; y < height / 2; y++) {
for (int x = 0; x < width; x++) {
int pos1 = y * width + x;
int pos2 = (height - 1 - y) * width + x;
int tmp = pixs[pos1];
pixs[pos1] = (pixs[pos2] & 0xFF00FF00) | ((pixs[pos2] >> 16) & 0xff) | ((pixs[pos2] << 16) & 0x00ff0000); // ABGR->ARGB
pixs[pos2] = (tmp & 0xFF00FF00) | ((tmp >> 16) & 0xff) | ((tmp << 16) & 0x00ff0000);
}
}
if (height % 2 == 1) { // 中间一行
for (int x = 0; x < width; x++) {
int pos = (height / 2 + 1) * width + x;
pixs[pos] = (pixs[pos] & 0xFF00FF00) | ((pixs[pos] >> 16) & 0xff) | ((pixs[pos] << 16) & 0x00ff0000);
}
}
return Bitmap.createBitmap(pixs, width, height, Bitmap.Config.ARGB_8888);
}
}
运行一下,这里我们就能看到两个三角形了,顶部的小三角形就是在后台渲染的图像了