詳解Unity安卓共享紋理
概述
本文的目的是實現(xiàn)以下的流程:
Android/iOS native app 操作攝像頭 -> 獲取視頻流數(shù)據(jù) -> 人臉檢測或美顏 -> 傳輸給 Unity 渲染 -> Unity做出更多的效果(濾鏡/粒子)
簡單通信
在之前的博客里已經(jīng)說到,Unity 和安卓通信最簡單的方法是用 UnitySendMessage 等 API 實現(xiàn)。
Android調(diào)用Unity:
//向unity發(fā)消息 UnityPlayer.UnitySendMessage("Main Camera", //gameobject的名字 "ChangeColor", //調(diào)用方法的名字 ""); //參數(shù)智能傳字符串,沒有參數(shù)則傳空字符串
Unity調(diào)用Android:
//通過該API來實例化java代碼中對應(yīng)的類 AndroidJavaObject jc = new AndroidJavaObject("com.xxx.xxx.UnityPlayer"); jo.Call("Test");//調(diào)用void Test()方法 jo.Call("Text1", msg);//調(diào)用string Test1(string str)方法 jo.Call("Text2", 1, 2);//調(diào)用int Test1(int x, int y)方法
所以按理來說我們可以通過 UnitySendMessage 將每一幀的數(shù)據(jù)傳給 Unity,只要在 onPreviewFrame 這個回調(diào)里執(zhí)行就能跑通。
@Override public void onPreviewFrame(byte[] data, Camera camera){ // function trans data[] to Unity }
但是,且不說 UnitySendMessage 只能傳遞字符串?dāng)?shù)據(jù)(必然帶來的格式轉(zhuǎn)換的開銷), onPreviewFrame()
回調(diào)方法也涉及到從GPU拷貝到CPU的操作,總的流程相當(dāng)于下圖所示,用屁股想都知道性能太低了。既然我們的最終目的都是傳到GPU上讓Unity渲染線程渲染,那何不直接在GPU層傳遞紋理數(shù)據(jù)到Unity。
獲取和創(chuàng)建Context
于是我們開始嘗試從 Unity 線程中拿到 EGLContext 和 EGLConfig ,將其作為參數(shù)傳遞給 Java線程 的 eglCreateContext() 方法創(chuàng)建 Java 線程的 EGLContext ,兩個線程就相當(dāng)于共享 EGLContext 了
先在安卓端寫好獲取上下文的方法 setupOpenGL() ,供 Unity 調(diào)用(代碼太長,if 里的 check 的代碼已省略)
// 創(chuàng)建單線程池,用于處理OpenGL紋理 private final ExecutorService mRenderThread = Executors.newSingleThreadExecutor(); private volatile EGLContext mSharedEglContext; private volatile EGLConfig mSharedEglConfig; // 被unity調(diào)用獲取EGLContext,在Unity線程執(zhí)行 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線程內(nèi) @Override public void run() { // Java線程初始化OpenGL環(huán)境 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 線程內(nèi)初始化 OpenGL 環(huán)境
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作為參數(shù),傳遞給eglCreateContext, // 創(chuàng)建Java線程的EGLContext,從而實現(xiàn)兩個線程共享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線程不進行實際繪制,因此創(chuàng)建PbufferSurface而非WindowSurface // 將Unity線程的EGLConfig作為參數(shù)傳遞給eglCreatePbufferSurface // 創(chuàng)建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() 創(chuàng)建紋理渲染出即可,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"); // 創(chuàng)建紋理并綁定到當(dāng)前GameObject上 material.mainTexture = Texture2D.CreateExternalTexture( mWidth, mHeight, TextureFormat.ARGB32, false, false, (IntPtr)mTextureId); // 更新紋理數(shù)據(jù) mGLTexCtrl.Call("updateTexture"); } }
unity需要調(diào)用updateTexture方法更新紋理
public void updateTexture() { //Log.d(TAG,"updateTexture called by unity"); mRenderThread.execute(new Runnable() { //java線程內(nèi) @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();//回收內(nèi)存 } }); }
同時注意必須關(guān)閉unity的多線程渲染,否則無法獲得Unity渲染線程的EGLContext(應(yīng)該有辦法,小弟還沒摸索出來),還要選擇對應(yīng)的圖形 API,我們之前寫的是 GLES3,如果我們寫的是 GLES2,就要換成 2 。
然后就可以將 Unity 工程打包到安卓項目,如果沒意外是可以顯示紋理出來的。
如果沒有成功可以用 glGetError() 一步步檢查報錯,按上面的流程應(yīng)該是沒有問題的
視頻流RTT
那么如果把圖片換成 camera 視頻流的話呢?上述的方案假定 Java 層更新紋理時使用的是 RGB 或 RBGA 格式的數(shù)據(jù),但是播放視頻或者 camera 預(yù)覽這種應(yīng)用場景下,解碼器解碼出來的數(shù)據(jù)是 YUV 格式,Unity 讀不懂這個格式的數(shù)據(jù),但是問題不大,我們可以編寫 Unity Shader 來解釋這個數(shù)據(jù)流(也就是用 GPU 進行格式轉(zhuǎn)換了)
另一個更簡單的做法是通過一個 FBO 進行轉(zhuǎn)換:先讓 camera 視頻流渲染到 SurfaceTexture 里(SurfaceTexture 使用的是 GL_TEXTURE_EXTERNAL_OES ,Unity不支持),再創(chuàng)建一份 Unity 支持的 GL_Texture2D 。待 SurfaceTexture 有新的幀后,創(chuàng)建 FBO,調(diào)用 glFramebufferTexture2D 將 GL_Texture2D 紋理與 FBO 關(guān)聯(lián)起來,這樣在 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的預(yù)覽數(shù)據(jù)設(shè)置顯示到mSurfaceTexture,OpenGL就能拿到攝像頭數(shù)據(jù)。 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 回調(diào)內(nèi)設(shè)置 bool mFrameUpdated = true; )讓 Unity 調(diào)用這個 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; // 根據(jù)寬高創(chuàng)建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預(yù)覽
這時候你想做什么效果都很簡單了,比如用 Unity Shader 寫一個賽博朋克風(fēng)格的濾鏡:
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() 加入這幾行
// 創(chuàng)建讀出的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是否是當(dāng)前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 查看當(dāng)前捕捉下來的 Bitmap,是 Unity 做完效果之后的。
以上就是詳解Unity安卓共享紋理的詳細內(nèi)容,更多關(guān)于Unity安卓共享紋理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#事件標(biāo)準(zhǔn)命名規(guī)則及說明(包括用作事件類型的委托命名)
這篇文章主要介紹了C#事件標(biāo)準(zhǔn)命名規(guī)則及說明(包括用作事件類型的委托命名),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02C#開發(fā)微信門戶及應(yīng)用(5) 用戶分組信息管理
這篇文章主要為大家詳細介紹了C#開發(fā)微信門戶及應(yīng)用第五篇,用戶分組信息管理,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06winfrom 在業(yè)務(wù)層實現(xiàn)事務(wù)控制的小例子
winfrom 在業(yè)務(wù)層實現(xiàn)事務(wù)控制的小例子,需要的朋友可以參考一下2013-03-03