安卓和Unity线程共享context
Unity项目作为library集成到安卓内请看安卓集成Unity开发示例
本项目的目的是实现以下的流程
Android和iOS原生代码里面操作摄像头->获取视频流数据->人脸检测或美颜->传输给Unity
一开始我尝试了最直接的方法,通过
@Override public void onPreviewFrame(byte[] data, Camera camera){ // function trans data[] to Unity }
但是涉及到格式转换问题此流程非常低效,而且onPreviewFrame()
方法的回传也涉及到从GPU拷贝到CPU的操作,性能依然不高。
既然我们的最终目的都是传到GPU上让Unity渲染线程渲染,那能不能直接在GPU层传递纹理数据到Unity呢?
完全没有问题,只要我们在Unity线程中拿到EGLContext和EGLConfig,将其作为参数传递给Java线程的eglCreateContext()
创建Java线程的EGLContext,两个线程就可以共享EGLContext了
先在安卓端写好获取eglcontext的方法,供Unity调用
// 创建单线程池,用于处理OpenGL纹理 private final ExecutorService mRenderThread = Executors.newSingleThreadExecutor(); public int getStreamTextureWidth() {return mTextureWidth;} public int getStreamTextureHeight() {return mTextureHeight;} public int getStreamTextureID() {return mTextureID;} private void glLogE(String msg) { Log.e(TAG, msg + ", err=" + GLES20.glGetError()); } // 被unity调用获取EGLContext public void setupOpenGL() { Log.d(TAG, "setupOpenGL called by Unity "); // 获取Unity线程的EGLContext,EGLDisplay mSharedEglContext = EGL14.eglGetCurrentContext(); if (mSharedEglContext == EGL14.EGL_NO_CONTEXT) { glLogE("eglGetCurrentContext failed"); return; }glLogE("eglGetCurrentContext success"); EGLDisplay sharedEglDisplay = EGL14.eglGetCurrentDisplay(); if (sharedEglDisplay == EGL14.EGL_NO_DISPLAY) { glLogE("sharedEglDisplay failed"); return; }glLogE("sharedEglDisplay success"); // 获取Unity绘制线程的EGLConfig int[] numEglConfigs = new int[1]; EGLConfig[] eglConfigs = new EGLConfig[1]; if (!EGL14.eglGetConfigs(sharedEglDisplay, eglConfigs, 0, eglConfigs.length, numEglConfigs, 0)) { glLogE("eglGetConfigs failed"); return; }glLogE("eglGetConfigs success"); mSharedEglConfig = eglConfigs[0]; mRenderThread.execute(new Runnable() { @Override public void run() { // Java线程初始化OpenGL环境 initOpenGL(); // 生成OpenGL纹理ID int textures[] = new int[1]; GLES20.glGenTextures(1, textures, 0); if (textures[0] == 0) { glLogE("glGenTextures failed"); return; }glLogE("glGenTextures success"); mTextureID = textures[0]; mTextureWidth = 670; mTextureHeight = 670; } }); }
Java线程在此之后初始化OpenGL环境
private void initOpenGL() { mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { glLogE("eglGetDisplay failed"); return; }glLogE("eglGetDisplay success"); int[] version = new int[2]; if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { mEGLDisplay = null; glLogE("eglInitialize failed"); return; }glLogE("eglInitialize success"); int[] eglContextAttribList = new int[]{ EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, // 版本需要与Unity使用的一致 EGL14.EGL_NONE }; // 将Unity线程的EGLContext和EGLConfig作为参数,传递给eglCreateContext, // 创建Java线程的EGLContext,从而实现两个线程共享EGLContext mEglContext = EGL14.eglCreateContext( mEGLDisplay, mSharedEglConfig, mSharedEglContext, eglContextAttribList, 0); if (mEglContext == EGL14.EGL_NO_CONTEXT) { glLogE("eglCreateContext failed"); return; }glLogE("eglCreateContext success"); int[] surfaceAttribList = { EGL14.EGL_WIDTH, 64, EGL14.EGL_HEIGHT, 64, EGL14.EGL_NONE }; // Java线程不进行实际绘制,因此创建PbufferSurface而非WindowSurface // 将Unity线程的EGLConfig作为参数传递给eglCreatePbufferSurface // 创建Java线程的EGLSurface mEglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mSharedEglConfig, surfaceAttribList, 0); if (mEglSurface == EGL14.EGL_NO_SURFACE) { glLogE("eglCreatePbufferSurface failed"); return; }glLogE("eglCreatePbufferSurface success"); if (!EGL14.eglMakeCurrent(mEGLDisplay, mEglSurface, mEglSurface, mEglContext)) { glLogE("eglMakeCurrent failed"); return; }glLogE("eglMakeCurrent success"); GLES20.glFlush(); }
共享context后,两个线程就可以共享纹理了。将Java线程生成的纹理id返回给Unity线程即可,C#代码如下:
using System; using System.Collections; using System.Collections.Generic; using UnityEditor; using UnityEngine; public class GLTexture : MonoBehaviour { private AndroidJavaObject mGLTexCtrl; private int mTextureId; private int mWidth; private int mHeight; private void Awake(){ // 实例化com.xxx.nativeandroidapp.GLTexture类的对象 mGLTexCtrl = new AndroidJavaObject("com.xxx.nativeandroidapp.GLTexture"); // 初始化OpenGL mGLTexCtrl.Call("setupOpenGL"); } void Start(){ BindTexture(); } void BindTexture(){ // 获取JavaPlugin生成的纹理ID mTextureId = mGLTexCtrl.Call<int>("getStreamTextureID"); if (mTextureId == 0){ Debug.LogError("getStreamTextureID failed"); return; } Debug.Log("getStreamTextureID success"); mWidth = mGLTexCtrl.Call<int>("getStreamTextureWidth"); mHeight = mGLTexCtrl.Call<int>("getStreamTextureHeight"); // 将纹理ID与当前GameObject绑定 material.mainTexture = Texture2D.CreateExternalTexture(mWidth, mHeight, TextureFormat.ARGB32, false, false, (IntPtr)mTextureId); // 更新纹理数据 mGLTexCtrl.Call("updateTexture"); } }
unity需要调用updateTexture方法更新纹理
public void updateTexture() { //Log.d(TAG,"updateTexture called by unity"); mRenderThread.execute(new Runnable() { @Override public void run() { String imageFilePath = "your own picture path";//图片路径 final Bitmap bitmap = BitmapFactory.decodeFile(imageFilePath); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); bitmap.recycle();//回收内存 } }); }
同时注意必须关闭unity的多线程渲染,否则无法获得Unity渲染线程的EGLContext:
然后就可以将Unity工程打包到安卓项目,如果没意外是可以显示纹理到cube等3Dobject上的
上述的方案假定Java层更新纹理时使用的是RGB或RBGA格式的数据,但是播放视频或者camera预览这种应用场景下,解码器解码出来的数据如果是YUV格式,渲染起来就稍微麻烦点,考虑到性能需要使用GPU进行转换,可以编写Shader来实现。
如前文所述,Unity只需要Java层的纹理ID,当使用Shader进行YUV转RGB时,怎么实现更新该纹理的数据呢?答案是Render to Texture。具体做法是,创建一个FrameBuffer,调用glFramebufferTexture2D将纹理与FrameBuffer关联起来,这样在FrameBuffer上进行的绘制,就会被写入到该纹理中。
我们先新建TextureSurface用于接收摄像头的数据
mCameraInputSurface = new SurfaceTexture(0); mCameraInputSurface.setOnFrameAvailableListener(this); mCameraInputSurface.setDefaultBufferSize(mFrameWidth, mFrameHeight); mOutputSurfaceTexture.setOnFrameAvailableListener(this);
设置摄像机的预览到这个Surface,并开始预览
mCamera.setPreviewTexture(mCameraInputSurface); mCamera.startPreview();
然后这个SurfaceTexture就可以跨线程共享硬件纹理数据了。
和之前一样,在u3d的主线程中获取OpenGL的共享Context,用这个context创建共享线程,然后这个线程对mOutputTex
进行绘制。先用这个mOutputTex
绑定好FBO,然后把上面mCameraInputSurface
的图像渲染到FBO中,最后通知u3d的主线程,可以使用这张纹理进行渲染了。这里的坑之后再填。