詳解Unity安卓共享紋理
概述
本文的目的是實現以下的流程:
Android/iOS native app 操作攝像頭 -> 獲取視頻流數據 -> 人臉檢測或美顏 -> 傳輸給 Unity 渲染 -> Unity做出更多的效果(濾鏡/粒子)
簡單通信
在之前的博客裡已經說到,Unity 和安卓通信最簡單的方法是用 UnitySendMessage 等 API 實現。
Android調用Unity:
//向unity發消息 UnityPlayer.UnitySendMessage("Main Camera", //gameobject的名字 "ChangeColor", //調用方法的名字 ""); //參數智能傳字符串,沒有參數則傳空字符串
Unity調用Android:
//通過該API來實例化java代碼中對應的類 AndroidJavaObject jc = new AndroidJavaObject("com.xxx.xxx.UnityPlayer"); jo.Call("Test");//調用void Test()方法 jo.Call("Text1", msg);//調用string Test1(string str)方法 jo.Call("Text2", 1, 2);//調用int Test1(int x, int y)方法
所以按理來說我們可以通過 UnitySendMessage 將每一幀的數據傳給 Unity,隻要在 onPreviewFrame 這個回調裡執行就能跑通。
@Override public void onPreviewFrame(byte[] data, Camera camera){ // function trans data[] to Unity }
但是,且不說 UnitySendMessage 隻能傳遞字符串數據(必然帶來的格式轉換的開銷), onPreviewFrame()
回調方法也涉及到從GPU拷貝到CPU的操作,總的流程相當於下圖所示,用屁股想都知道性能太低瞭。既然我們的最終目的都是傳到GPU上讓Unity渲染線程渲染,那何不直接在GPU層傳遞紋理數據到Unity。
獲取和創建Context
於是我們開始嘗試從 Unity 線程中拿到 EGLContext 和 EGLConfig ,將其作為參數傳遞給 Java線程 的 eglCreateContext() 方法創建 Java 線程的 EGLContext ,兩個線程就相當於共享 EGLContext 瞭
先在安卓端寫好獲取上下文的方法 setupOpenGL() ,供 Unity 調用(代碼太長,if 裡的 check 的代碼已省略)
// 創建單線程池,用於處理OpenGL紋理 private final ExecutorService mRenderThread = Executors.newSingleThreadExecutor(); private volatile EGLContext mSharedEglContext; private volatile EGLConfig mSharedEglConfig; // 被unity調用獲取EGLContext,在Unity線程執行 public void setupOpenGL { Log.d(TAG, "setupOpenGL called by Unity "); // 獲取Unity線程的EGLContext,EGLDisplay mSharedEglContext = EGL14.eglGetCurrentContext(); if (mSharedEglContext == EGL14.EGL_NO_CONTEXT) {...} EGLDisplay sharedEglDisplay = EGL14.eglGetCurrentDisplay(); if (sharedEglDisplay == EGL14.EGL_NO_DISPLAY) {...} // 獲取Unity繪制線程的EGLConfig int[] numEglConfigs = new int[1]; EGLConfig[] eglConfigs = new EGLConfig[1]; if (!EGL14.eglGetConfigs(sharedEglDisplay, eglConfigs, 0, eglConfigs.length,numEglConfigs, 0)) {...} mSharedEglConfig = eglConfigs[0]; mRenderThread.execute(new Runnable() { // Java線程內 @Override public void run() { // Java線程初始化OpenGL環境 initOpenGL(); // 生成OpenGL紋理ID int textures[] = new int[1]; GLES20.glGenTextures(1, textures, 0); if (textures[0] == 0) {...} 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) {...} int[] version = new int[2]; if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {...} 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) {...} 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) {...} if (!EGL14.eglMakeCurrent( mEGLDisplay, mEglSurface, mEglSurface, mEglContext)) {...} GLES20.glFlush(); }
共享紋理
共享context完成後,兩個線程就可以共享紋理瞭。隻要讓 Unity 線程拿到將 Java 線程生成的紋理 id ,再用 CreateExternalTexture() 創建紋理渲染出即可,C#代碼如下:
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(){ // 獲取 Java 線程生成的紋理ID mTextureId = mGLTexCtrl.Call<int>("getStreamTextureID"); if (mTextureId == 0) {...} mWidth = mGLTexCtrl.Call<int>("getStreamTextureWidth"); mHeight = mGLTexCtrl.Call<int>("getStreamTextureHeight"); // 創建紋理並綁定到當前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() { //java線程內 @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(應該有辦法,小弟還沒摸索出來),還要選擇對應的圖形 API,我們之前寫的是 GLES3,如果我們寫的是 GLES2,就要換成 2 。
然後就可以將 Unity 工程打包到安卓項目,如果沒意外是可以顯示紋理出來的。
如果沒有成功可以用 glGetError() 一步步檢查報錯,按上面的流程應該是沒有問題的
視頻流RTT
那麼如果把圖片換成 camera 視頻流的話呢?上述的方案假定 Java 層更新紋理時使用的是 RGB 或 RBGA 格式的數據,但是播放視頻或者 camera 預覽這種應用場景下,解碼器解碼出來的數據是 YUV 格式,Unity 讀不懂這個格式的數據,但是問題不大,我們可以編寫 Unity Shader 來解釋這個數據流(也就是用 GPU 進行格式轉換瞭)
另一個更簡單的做法是通過一個 FBO 進行轉換:先讓 camera 視頻流渲染到 SurfaceTexture 裡(SurfaceTexture 使用的是 GL_TEXTURE_EXTERNAL_OES ,Unity不支持),再創建一份 Unity 支持的 GL_Texture2D 。待 SurfaceTexture 有新的幀後,創建 FBO,調用 glFramebufferTexture2D 將 GL_Texture2D 紋理與 FBO 關聯起來,這樣在 FBO 上進行的繪制,就會被寫入到該紋理中。之後和上面一樣,再把 Texutrid 返回給 unity ,就可以使用這個紋理瞭。這就是 RTT Render To Texture。
private SurfaceTexture mSurfaceTexture; //camera preview private GLTextureOES mTextureOES; //GL_TEXTURE_EXTERNAL_OES private GLTexture2D mUnityTexture; //GL_TEXTURE_2D 用於在Unity裡顯示的貼圖 private FBO mFBO; //具體代碼在github倉庫 public void openCamera() { ...... // 利用OpenGL生成OES紋理並綁定到mSurfaceTexture // 再把camera的預覽數據設置顯示到mSurfaceTexture,OpenGL就能拿到攝像頭數據。 mTextureOES = new GLTextureOES(UnityPlayer.currentActivity, 0,0); mSurfaceTexture = new SurfaceTexture(mTextureOES.getTextureID()); mSurfaceTexture.setOnFrameAvailableListener(this); try { mCamera.setPreviewTexture(mSurfaceTexture); } catch (IOException e) { e.printStackTrace(); } mCamera.startPreview(); }
SurfaceTexture 更新後(可以在 onFrameAvailable 回調內設置 bool mFrameUpdated = true; )讓 Unity 調用這個 updateTexture() 獲取紋理 id 。
public int updateTexture() { synchronized (this) { if (mFrameUpdated) { mFrameUpdated = false; } mSurfaceTexture.updateTexImage(); int width = mCamera.getParameters().getPreviewSize().width; int height = mCamera.getParameters().getPreviewSize().height; // 根據寬高創建Unity使用的GL_TEXTURE_2D紋理 if (mUnityTexture == null) { Log.d(TAG, "width = " + width + ", height = " + height); mUnityTexture = new GLTexture2D(UnityPlayer.currentActivity, width, height); mFBO = new FBO(mUnityTexture); } Matrix.setIdentityM(mMVPMatrix, 0); mFBO.FBOBegin(); GLES20.glViewport(0, 0, width, height); mTextureOES.draw(mMVPMatrix); mFBO.FBOEnd(); Point size = new Point(); if (Build.VERSION.SDK_INT >= 17) { UnityPlayer.currentActivity.getWindowManager().getDefaultDisplay().getRealSize(size); } else { UnityPlayer.currentActivity.getWindowManager().getDefaultDisplay().getSize(size); } GLES20.glViewport(0, 0, size.x, size.y); return mUnityTexture.getTextureID(); } }
詳細的代碼可以看這個 demo,簡單封裝瞭下。
跑通流程之後就很好辦瞭,Unity 場景可以直接顯示camera預覽
這時候你想做什麼效果都很簡單瞭,比如用 Unity Shader 寫一個賽博朋克風格的濾鏡:
shader代碼
Shader "Unlit/CyberpunkShader" { Properties { _MainTex("Base (RGB)", 2D) = "white" {} _Power("Power", Range(0,1)) = 1 } SubShader { Tags { "RenderType" = "Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct a2v { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; half2 texcoord : TEXCOORD0; }; sampler2D _MainTex; float4 _MainTex_ST; float _Power; v2f vert(a2v v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 baseTex = tex2D(_MainTex, i.texcoord); float3 xyz = baseTex.rgb; float oldx = xyz.x; float oldy = xyz.y; float add = abs(oldx - oldy)*0.5; float stepxy = step(xyz.y, xyz.x); float stepyx = 1 - stepxy; xyz.x = stepxy * (oldx + add) + stepyx * (oldx - add); xyz.y = stepyx * (oldy + add) + stepxy * (oldy - add); xyz.z = sqrt(xyz.z); baseTex.rgb = lerp(baseTex.rgb, xyz, _Power); return baseTex; } ENDCG } } Fallback off }
還有其他粒子效果也可以加入,比如Unity音量可視化——粒子隨聲浪跳動
紋理取回
在安卓端取回紋理也是可行的,我沒有寫太多,這裡做瞭一個示例,在 updateTexture() 加入這幾行
// 創建讀出的GL_TEXTURE_2D紋理 if (mUnityTextureCopy == null) { Log.d(TAG, "width = " + width + ", height = " + height); mUnityTextureCopy = new GLTexture2D(UnityPlayer.currentActivity, size.x, size.y); mFBOCopy = new FBO(mUnityTextureCopy); } GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mUnityTextureCopy.mTextureID); GLES20.glCopyTexSubImage2D(GLES20.GL_TEXTURE_2D, 0,0,0,0,0,size.x, size.y); mFBOCopy.FBOBegin(); // //test是否是當前FBO // GLES20.glClearColor(1,0,0,1); // GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // GLES20.glFinish(); int mImageWidth = size.x; int mImageHeight = size.y; Bitmap dest = Bitmap.createBitmap(mImageWidth, mImageHeight, Bitmap.Config.ARGB_8888); final ByteBuffer buffer = ByteBuffer.allocateDirect(mImageWidth * mImageHeight * 4); GLES20.glReadPixels(0, 0, mImageWidth, mImageHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); dest.copyPixelsFromBuffer(buffer); dest = null;//斷點 mFBOCopy.FBOEnd();
在 dest = null; 打個斷點,就能在 android studio 查看當前捕捉下來的 Bitmap,是 Unity 做完效果之後的。
以上就是詳解Unity安卓共享紋理的詳細內容,更多關於Unity安卓共享紋理的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Android中的OpenGL使用配置詳解
- unity置灰處理的實現
- Unity2021發佈WebGL與網頁交互問題的解決
- Unity Shader實現動態過場切換圖片效果
- Android OpenGL仿自如APP裸眼3D效果詳解