安卓和Unity线程共享context

炒沙作縻终不饱,缕冰文章费工巧。这篇文章主要讲述安卓和Unity线程共享context相关的知识,希望能为你提供帮助。
Unity项目作为library集成到安卓内请看安卓集成Unity开发示例
本项目的目的是实现以下的流程

android和ios原生代码里面操作摄像头-> 获取视频流数据-> 人脸检测或美颜-> 传输给Unity
一开始我尝试了最直接的方法,通过
@Override public void onPreviewFrame(byte[] data, Camera camera){ // function trans data[] to Unity }

但是涉及到格式转换问题此流程非常低效,而且onPreviewFrame()方法的回传也涉及到从GPU拷贝到CPU的操作,性能依然不高。
安卓和Unity线程共享context

文章图片

既然我们的最终目的都是传到GPU上让Unity渲染线程渲染,那能不能直接在GPU层传递纹理数据到Unity呢?
安卓和Unity线程共享context

文章图片

完全没有问题,只要我们在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线程共享context

文章图片

然后就可以将Unity工程打包到安卓项目,如果没意外是可以显示纹理到cube等3Dobject上的
安卓和Unity线程共享context

文章图片

上述的方案假定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就可以跨线程共享硬件纹理数据了。
【安卓和Unity线程共享context】和之前一样,在u3d的主线程中获取OpenGL的共享Context,用这个context创建共享线程,然后这个线程对mOutputTex进行绘制。先用这个mOutputTex绑定好FBO,然后把上面mCameraInputSurface的图像渲染到FBO中,最后通知u3d的主线程,可以使用这张纹理进行渲染了。这里的坑之后再填。

    推荐阅读