安卓和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就可以跨线程共享硬件纹理数据了。

和之前一样,在u3d的主线程中获取OpenGL的共享Context,用这个context创建共享线程,然后这个线程对mOutputTex进行绘制。先用这个mOutputTex绑定好FBO,然后把上面mCameraInputSurface的图像渲染到FBO中,最后通知u3d的主线程,可以使用这张纹理进行渲染了。这里的坑之后再填。