安卓OpenGLES环境搭建(十)
前言
前面学习了opengl es的基础知识,包括GLSL语言,常用函数等等,由于opengl es是基于夸平台的api设计,它本身不提供上下文管理,窗口管理,这些交给具体的设备厂商。在安卓平台这些是由EGL库实现的,接下来我们就学习安卓平台如何搭建opengl es的环境;安卓平台的EGL库分为java层,在com.media.opengl_es包下;native层的EGL库则需要引入头文件
#include <EGL/egl.h>
#include <EGL/eglext.h>
opengl es系列文章
opengl es之-基础概念(一)
opengl es之-GLSL语言(二)
opengl es之-GLSL语言(三)
opengl es之-常用函数介绍(四)
opengl es之-安卓平台在图片上画对角线和截屏(十一)
目标
1、GLSurfaceView搭建opengl es环境
2、SurfaceView搭建opengl es环境
3、TextureView搭建opengl es环境
4、利用上面搭建的环境渲染一张图片到屏幕上
GLSurfaceView、SurfaceView、TextureView区别
1、SurfaceView,它继承自类View,因此它本质上是一个View。但与普通View不同的是,它有自己的Surface(所有的普通View是共享一个Surface的,该Surface由他们的共同父类DecorView提供),利用这个Surface,我们可以进行单独的渲染,并将最终的渲染结果与DecorView合并最终输出到屏幕上。流程如下:
1561205389814.jpg
这里的Surface其实就是前面章节所讲的frame buffer和绑定了frame buffer的 render buffer的封装体,所以在安卓平台利用opengl es并不需要我们再去通过
glGenframebuffers()和glGenRenderbuffers()函数创建FBO和RBO了,因为SurfaceView已经为我们封装好了。
2、GLSurfaceView集成于SurfaceView,它具有SurfaceView的全部特性,内部实现了EGL管理和一个独立的渲染线程,而SurfaceView却需要我们自己实现EGL和渲染线程
3、TextureView继承于View,它内部没有Surface,但是有一个SurfaceTexture,该SurfaceTexture可以作为EGL的参数来创建Surface,SurfaceTexture就相当于frame buffer。使用流程和SurfaceView一致。
GLSurfaceView搭建Opengl es环境
通过前面的学习,我们可以知道,使用GLSurfaceView搭建opengl es环境是最简单的,因为不需要我们自己实现EGL,和对渲染线程的管理了,下面来看看如何使用GLSurfaceView
1、像创建普通View一样创建GLSurfaceView
int h = PixelUtil.dp2px(this,200);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,h);
lp.addRule(RelativeLayout.BELOW, R.id.id_btn3);
lp.topMargin = PixelUtil.dp2px(this,20);
lp.leftMargin = PixelUtil.dp2px(this,20);
lp.rightMargin = PixelUtil.dp2px(this,20);
glSurfaceView = new MyGLSurfaceView(this);
contentLayout.addView(glSurfaceView);
glSurfaceView.setLayoutParams(lp);
MyGLSurfaceView是GLSurfaceView的子类
public class MyGLSurfaceView extends GLSurfaceView {
// 先保存要显示的纹理
private Bitmap mBitmap;
private int mWidth;
private int mHeight;
// 顶点坐标
private ByteBuffer vbuffer;
// 纹理坐标
private ByteBuffer fbuffer;
// 1、初始化GLSurfaceView,包括调用setRenderer()设置GLSurfaceView.Renderer对象
// setRenderer()将会创建一个渲染线程
public MyGLSurfaceView(Context context) {
super(context);
initGLESContext();
}
public MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
initGLESContext();
}
/** 遇到问题:Opengl es函数无法工作,由于没有指定opengl es的版本
* 解决方案:设置正确的opengl es版本
**/
private void initGLESContext() {
// 设置版本,必须要
setEGLContextClientVersion(2);
GLRGBRender render = new GLRGBRender();
setRenderer(render);
setRenderMode(RENDERMODE_WHEN_DIRTY); // 默认是连续渲染模式
}
......
}
注意initGLESContext()函数
要调用setEGLContextClientVersion(2);设置opengl es的版本
调用setRenderer(render);指定GLSurfaceView.Renderer接口的实现者,点进去可以看到此函数调用后GLSurfaceView内部的渲染线程GLThread将自动启用
public void setRenderer(Renderer renderer) {
checkRenderThreadState();
if (mEGLConfigChooser == null) {
mEGLConfigChooser = new SimpleEGLConfigChooser(true);
}
if (mEGLContextFactory == null) {
mEGLContextFactory = new DefaultContextFactory();
}
if (mEGLWindowSurfaceFactory == null) {
mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
}
mRenderer = renderer;
mGLThread = new GLThread(mThisWeakRef);
mGLThread.start();
}
那么有人可能会想,还没开始绘制,渲染线程就空跑起来了?那岂不是很浪费啊,对的,但是我们可以通过如下函数指定该线程的行为
setRenderMode(RENDERMODE_WHEN_DIRTY); // 默认是连续渲染模式
RENDERMODE_CONTINUOUSLY:
默认情况下,渲染线程每隔16ms自动发出一次渲染请求。
RENDERMODE_WHEN_DIRTY:
则表示渲染请求由用户调用requestRender();函数后触发;备注:GLSurfaceView创建成果后也会触发几次渲染请求
2、实现GLSurfaceView.Renderer接口
前面提到了setRenderer(render)函数,这里的render就是实现了GLSurfaceView.Renderer接口的类,该接口告诉了我们EGL环境、Surface的准备情况,我们的opengl es绘制指令要在该接口中去实现,先看下该接口的介绍
public interface Renderer {
....
void onSurfaceCreated(GL10 gl, EGLConfig config);
void onSurfaceChanged(GL10 gl, int width, int height);
void onDrawFrame(GL10 gl);
....
}
void onSurfaceCreated(GL10 gl, EGLConfig config):
代表GLSurfaceView内部的Surface已经创建好,EGL环境也准备就绪,同时我们可以在该回调中重新配置EGLConfig,回调结束后将使用新的EGLConfig配置的EGL环境,当然也可以不做处理使用默认配置。
void onSurfaceChanged(GL10 gl, int width, int height);
当GLSurfaceView的大小改变时往往会伴随着内部Surface大小的改变,此时该回调会被调用,GLSurfaceView首次初始化时该函数也会被执行
void onDrawFrame(GL10 gl);
如果前面是默认的RENDERMODE_WHEN_DIRTY渲染模式,那么此函数每隔16ms执行一次;如果是RENDERMODE_WHEN_DIRTY渲染模式,那么此函数
在GLSurfaceView的requestRender();函数调用后被调用
opengl es函数指令应该在该函数中去执行
@Override
public void onDrawFrame(GL10 gl) {
MLog.log("onDrawFrame thread " + Thread.currentThread());
........
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
}
这里只是讲解opengl es的环境搭建,就不贴出具体的opengl es的代码了,具体可以参考Demo中的MyGLSurfaceView类
备注:它是在渲染线程中被调用
3、GLSurfaceView释放
使用完毕后还要对GLSurfaceView进行释放,那么释放就跟随Activity的生命周期了
至此,GLSurfaceView环境的搭建就讲解完毕了
SurfaceView搭建opengl es环境
SurfaceView与GLSurfaceView一样,它内部会自动创建一个Surface作为opengl es的渲染缓冲区。区别就是不提供EGL的实现和渲染线程的管理,所以要使用SurfaceView搭建Opengl es环境我们得自己实现EGL和渲染线程。
1、实现EGL环境
整个EGL环境的实现有如下几个很重要的类:
EGLDisplay:可以理解为要绘制的地方的一个抽象
EGLConfig:它是EGL上下文的配置参数,比如RGBA的位宽等等
EGLContext:代表EGL上下文
EGLSurface:opengl es渲染结果的缓冲区;opengl es通过它呈现到屏幕上或者实现离屏渲染
有人可能会问SurfaceView中的Surface是不是就是这里的EGLSurface,答案是。不过通过EGLSurface来操作渲染结果更加灵活,所以下面都是用EGLSurface来操作渲染结果;
EGL环境的搭建步骤:
-创建 EGLDisplay
private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
private void initEGLDisplay() {
// 用于绘制的地方的一个抽闲
mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
MLog.log("eglGetDisplay fail");
}
// 初始化EGLDisplay,还可以在这个函数里面获取版本号
boolean ret = EGL14.eglInitialize(mEGLDisplay,null,0,null,0);
if (!ret) {
MLog.log("eglInitialize fail");
}
}
eglGetDisplay()函数选择EGLDisplay类型,选择好了之后,在调用eglInitialize()进行初始化
-创建EGLConfig配置
// 定义 EGLConfig 属性配置,这里定义了红、绿、蓝、透明度、深度、模板缓冲的位数,属性配置数组要以EGL14.EGL_NONE结尾
private static final int[] EGL_CONFIG = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 16,
EGL14.EGL_STENCIL_SIZE, 0,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_NONE,
};
private EGLConfig mEGLConfig;
private void initEGLConfig() {
// 所有符合配置的 EGLConfig 个数
int[] numConfigs = new int[1];
// 所有符合配置的 EGLConfig
EGLConfig[] configs = new EGLConfig[1];
// 会获取所有满足 EGL_CONFIG 的 config,然后取第一个
EGL14.eglChooseConfig(mEGLDisplay, EGL_CONFIG, 0, configs, 0, configs.length, numConfigs, 0);
mEGLConfig = configs[0];
}
eglChooseConfig()函数将选择一个满足配置的EGLconfig
-创建上下文EGLContext
private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
private static final int[] EGLCONTEXT_ATTRIBUTE = {
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE,
};
private void initEGLContext() {
// 创建上下文
mEGLContext = EGL14.eglCreateContext(mEGLDisplay, mEGLConfig, EGL14.EGL_NO_CONTEXT, EGLCONTEXT_ATTRIBUTE, 0);
if (mEGLContext == EGL14.EGL_NO_CONTEXT) {
MLog.log("eglCreateContext fail");
}
}
-创建EGLSurface
EGLSurface分两种,一种是将渲染结果呈现到屏幕上;另外一种是将渲染结果作为离线缓存不呈现到屏幕上,所以创建方法也不一样;
第一种:
public void createWindowSurface(Object surface) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglContext.createWindowSurface(surface);
}
这里的surface变量为SurfaceTexture类型和Surface类型都可以。所以这里可以明白我们其实可以再次创建一个EGLSurface类操作SurfaceView中的Surface的路径了吧,没错,就是通过该方法
第二种:
public void createOffscreenSurface(int width, int height) {
if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
throw new IllegalStateException("surface already created");
}
mEGLSurface = mEglContext.createOffscreenSurface(width, height);
mWidth = width;
mHeight = height;
}
创建离线渲染的EGLSurface,与前面不同的是,这里并不需要surface,只需要指定宽高即可。
至此EGL环境搭建流程全部完毕,具体可以参考
参考 google的示例代码 grafika 地址:https://github.com/google/grafika/
2、实现渲染线程
这里用java的Thread类实现,通过继承Thread类
private class RenderThread extends Thread implements SurfaceHolder.Callback {
......
}
这里先讲一下SurfaceHolder.Callback这个接口,它代表了SurfaceView内部的那个Surface从创建到变化到销毁的整个生命周期,如下:
public interface Callback {
public void surfaceCreated(SurfaceHolder holder);
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height);
public void surfaceDestroyed(SurfaceHolder holder);
}
这几个回调函数意义我就不一一讲解了,看名字也可以知道。这里我只是讲一下为什么自己实现的渲染线程要实现这个接口呢?其实不一定要渲染线程实现,也可以在MySurfaceView类实现,只要实现该接口即可。实现的目的在于:
-我们可通过该Surface对整个EGL的生命周期进行管理
-拿到该Surface后,我们可以把它作为EGLSurface的参数来创建一个EGL对象,这样他们是共享同一个渲染缓冲区的,有利于节约内存。
接下来回到渲染线程的实现。其实渲染线程主要的工作就是调用opengl es的指令进行渲染,然后将渲染结果根据需求是呈现到屏幕上还是离线放到渲染缓冲区
我们这里实现的非常简单,渲染线程开启后调用通过ondraw()函数调用一次opengl es指令然后就关闭该线程(具体实现可以参考GLSurfaceView)
首先看一下渲染线程实现的SurfaceHolder.Callback接口
@Override
public void surfaceCreated(SurfaceHolder holder) {
MLog.log("surfaceCreated 创建了");
synchronized (mLock) {
mSurfaceTexture = holder.getSurface();
mLock.notify();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
MLog.log("surfaceChanged 创建了");
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
MLog.log("surfaceDestroyed 创建了");
}
这些接口都是在UI线程调用的,因为我们的渲染线程执行顺序可能在这个函数之前,为了保证渲染线程中EGLSurface 拿到的Surface不为空,这里用锁和条件变量方式实现,如上
然后是渲染线程
@Override
public void run() {
Surface st = null;
mRun = true;
while (true) {
// 等待Surface的Surface创建成功
synchronized (mLock) {
while (mRun && (st = mSurfaceTexture) == null) {
try {
// 阻塞当前线程,直到在其它线程调用mLock.notifyAll()/mLock.notify()函数
MLog.log("texutre 还未创建,等待");
mLock.wait();
} catch (InterruptedException ie) {
ie.printStackTrace();
}
if (!mRun) {
break;
}
}
}
MLog.log("开始渲染 ");
// 获取到了SurfaceTexture,那么开始做渲染工作
mGLcontext = new GLContext();
mSurface = new GLSurface(mGLcontext);
/** 遇到问题:
* 奔溃:
* eglCreateWindowSurface: native_window_api_connect (win=0x75d7e8d010) failed (0xffffffed) (already connected to another API?)
* 解决方案:
* 因为SurfaceTexture还未与前面的EGLContext解绑就又被绑定到其它EGLContext,导致奔溃。原因就是下面渲染结束后没有跳出循环;在while语句最后添加
* break;语句
* */
mSurface.createWindowSurface(st);
mSurface.makeCurrentReadFrom(mSurface);
onSurfaceCreated();
onDraw();
/** 遇到问题:渲染结果没有成功显示到屏幕上
* 解决方案:因为下面提前释放了SurfaceTexture导致的问题。不应该在这里释放
* */
// 渲染结束 进行相关释放工作
// st.release();
MLog.log("渲染结束");
finishRender = true;
break;
}
}
可以看到,渲染线程在收到SurfaceView的Surface被初始化后的锁通知前一直休眠,直到通知到达才开始EGL环境的初始化
接下来初始化EGL,
接下来ondraw()函数调用
那么opengl es的指令就可以像GLSurfaceView那样写在ondraw函数中了。
private void onDraw() {
....
// 必须要有,否则渲染结果不会呈现到屏幕上
mSurface.swapBuffers();
}
注意:前面我们使用GLSurfaceView时是没有调用swapBuffers()这个函数的,没错,因为GLSurfaceView内部自动调用了,但是这里我们要自己调用,最终的渲染结果才会呈现到屏幕上。
至此整个SurfaceView搭建opengl es环境流程就完了。
TextureView搭建opengl es环境
TextureView和SurfaceView不同的是,它内部没有Surface,但是有SurfaceTexture,前面我们知道以SurfaceTexture作为参数可以创建EGLSurface,同样需要我们自己实现EGL管理和渲染线程的管理。
其实EGL的实现和渲染线程的实现和SurfaceView差不多,这里只是讲一下不同的地方
渲染线程实现SurfaceTextureListener接口,该接口如下:
public static interface SurfaceTextureListener {
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height);
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height);
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface);
public void onSurfaceTextureUpdated(SurfaceTexture surface);
}
咋一看是不是与SurfaceView的SurfaceHolder.Callback接口神似,没错,该接口就是TextureView内部的SurfaceTexture的生命周期。那后面的渲染线程我就不在讲解了,依然是又渲染线程实现SurfaceTextureListener接口,
GLSurfaceView、SurfaceView、TextureView区别总结:
1、我们可以知道GLSurfaceView实现opengl es是最简单的了,我们只需要调用opengl es 函数指令即可,其它都交给GLSurfaceView来实现
2、SurfaceView和TextureView实现opengl es环境流程其实大体一样,至于他们效率上的区别这里就不做讨论了,因为笔者也没有认真去研究。后面研究透了在单独来写
3、GLSurfaceView内部实现的是渲染到屏幕上的EGLSurface,所以这也是它的缺点
渲染一张图片到屏幕上
前面opengl es的环境搭建好了,那么接下来我们做点正事了。如何渲染一张图片到屏幕上,这里就涉及到opengl es的使用流程了,前面的opengl es基础文字中我们知道,opengl es的渲染管线分为七个步骤,但实际上提供给app的只有其中几个步骤,下面一一道来
1、搭建opengl es环境
参考前面,这里用前面TextureView搭建的环境为例来实现
2、初始化顶点坐标和着色器程序
顶点坐标的初始化可以在onSurfaceCreated()回调中进行,当然你也可以在onDraw()回调中,都可以。
// 顶点坐标
private static final float verdata[] = {
-1.0f,-1.0f, // 左下角
1.0f,-1.0f, // 右下角
-1.0f,1.0f, // 左上角
1.0f,1.0f // 右上角
};
// 纹理坐标
private static final float texdata[] = {
0.0f,1.0f, // 左下角
1.0f,1.0f, // 右上角
0.0f,0.0f, // 左上角
1.0f,0.0f, // 右上角
};
前面我们的opengl es基础文章中,我们知道,渲染管线中有一步骤是要确定顶点坐标,纹理坐标,顶点坐标规则和纹理坐标
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
// 初始化着色器程序
mprogram = new GLProgram(vString,fString);
mprogram.useprogram()
// 初始化顶点坐标和纹理坐标v
vbuffer = ByteBuffer.allocateDirect(verdata.length * 4);
vbuffer.order(ByteOrder.nativeOrder())
.asFloatBuffer().put(verdata)
.position(0);
fbuffer = ByteBuffer.allocateDirect(texdata.length * 4);
fbuffer.order(ByteOrder.nativeOrder())
.asFloatBuffer().put(texdata)
.position(0);
}
这里要说明的就是,java中是大端序,而opengl es中数据是小端序,所以这里
order(ByteOrder.nativeOrder())将大端序转换成小端序。
GLProgram是我封装的专门用于处理着色器程序的类,这里将顶点着色器和片段着色器代码作为参数创建一个着色器程序
其实着色器程序的源码加载,编译,连接,加载程序有一个固定的流程,所以就可以封装了,这里不在多讲,具体实现可以参考项目代码。
3、将顶点坐标,纹理坐标传递给opengl es
GLES20.glViewport(0,0,width,height);
GLES20.glClearColor(1.0f,0,0,1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 为着色器程序赋值
mprogram.useprogram();
int position = mprogram.attributeLocationForname("position");
int texcoord = mprogram.attributeLocationForname("texcoord");
int texture = mprogram.uniformaLocationForname("texture");
GLES20.glVertexAttribPointer(position,2,GLES20.GL_FLOAT,false,0,vbuffer);
GLES20.glEnableVertexAttribArray(position);
GLES20.glVertexAttribPointer(texcoord,2,GLES20.GL_FLOAT,false,0,fbuffer);
GLES20.glEnableVertexAttribArray(texcoord);
MLog.log("position " + position + " texcoord " + texcoord + " texture " + texture);
注意:使用着色器程序之前,一定要先调用着色器程序
mprogram.useprogram();
4、上传纹理
// 开始上传纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
// 设置纹理参数
IntBuffer texIntbuffer = IntBuffer.allocate(1);
GLES20.glGenTextures(1,texIntbuffer);
texture = texIntbuffer.get(0);
if (texture == 0) {
MLog.log("glGenTextures fail 0");
}
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture);
// 设置纹理参数
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,texture);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
// 第二个参数和前面用glActiveTexture()函数激活的纹理单元编号要一致,这样opengl es才知道用哪个纹理单元对象 去处理纹理
GLES20.glUniform1i(texture,0);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,mBitmap,0);
注意的地方就是:
-上传纹理之前一定要先调用glActiveTexture()激活当前单元;调用glBindTexture()绑定当前texture id
-调用GLES20.glUniform1i(texture,0);将当前纹理单元id传递给片元着色器中纹理变量
-GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,mBitmap,0);是google提供的java层的上传bitmap到opengl es的工具,opengl es api中没有该函数,但是最终是调用glTexImage2D()函数
这里讲一下该函数。它使用GL_RGBA作为glTexImage2d()的internalformat和format参数,使用GL_UNSIGNED_BYTE作为type参数。所以如果传递的bitmap不是RGBA格式,这里会出错,那么就可以用
public static void texImage2D(int target, int level, int internalformat,
Bitmap bitmap, int type, int border);这个函数了
指定bitmap对应的像素格式和type了
5、渲染并将渲染结果呈现到屏幕上
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
// 必须要有,否则渲染结果不会呈现到屏幕上
mSurface.swapBuffers();
以上就是如何基于TextureView用opengl es渲染一张图片到屏幕上
项目地址
https://github.com/nldzsz/opengles-android
1、GLSurfaceView搭建opengl es环境请参考MyGLSurfaceView类
2、SurfaceView搭建opengl es环境请参考MySurfaceView类
3、TextureView搭建opengl es环境请参考MyTextureView类
4、opengl es渲染一张图片到屏幕上请参考MySurfaceView和MyTextureView