Unity安卓共享纹理

 

 

Unity安卓共享纹理

前置知识:安卓集成Unity开发示例

本文的目的是实现以下的流程:

Android/iOS native app 操作摄像头 -> 获取视频流数据 -> 人脸检测或美颜 -> 传输给 Unity 渲染 -> Unity做出更多的效果(滤镜/粒子)

简单通信#

在之前的博客里已经说到,Unity 和安卓通信最简单的方法是用 UnitySendMessage 等 API 实现。

  • Android调用Unity:

     

    Copy

    //向unity发消息 UnityPlayer.UnitySendMessage("Main Camera", //gameobject的名字 "ChangeColor", //调用方法的名字 ""); //参数智能传字符串,没有参数则传空字符串
  • Unity调用Android:

     

    Copy

    //通过该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 这个回调里执行就能跑通。

 

Copy

@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 的代码已省略)

 

Copy

// 创建单线程池,用于处理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 环境

 

Copy

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#代码如下:

 

Copy

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方法更新纹理

 

Copy

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() 一步步检查报错,按上面的流程应该是没有问题的,完整 java 代码在这里

视频流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

 

Copy

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 。

 

Copy

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代码

 

Copy

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() 加入这几行

 

Copy

// 创建读出的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 做完效果之后的。

END

感谢阅读

作者: zhxmdefj

出处:https://www.cnblogs.com/zhxmdefj/p/13295243.html

版权:本文采用「署名-非商业性使用-相同方式共享 4.0 国际」知识共享许可协议进行许可。

文章链接:

https://www.cnblogs.com/zhxmdefj/p/13295243.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unity是一款广泛应用于游戏开发的跨平台游戏引擎,而DX11则是较新一代的DirectX图形API。共享纹理Unity和DX11之间的一种交互方式,用于在Unity中使用DX11的功能。下面将就Unity中DX11共享纹理进行简要阐述。 首先,DX11共享纹理允许在Unity和DX11之间实现纹理资源的共享,这意味着可以在Unity中创建纹理,然后将其传递给DX11进行进一步的图形渲染处理。这种共享可以通过创建和管理ID3D11Texture2D对象来实现。 其次,为了在Unity中使用DX11共享纹理,需要建立一个安装了DX11支持的插件。这个插件将提供一些接口和函数,使得Unity能够与DX11进行交互。在使用DX11共享纹理时,需要在Unity中导入插件并设置相关的渲染纹理。 另外,由于Unity使用的是OpenGL图形API,而不是DX11,因此在使用DX11共享纹理时需要对纹理进行格式转换以适应OpenGL。这可以通过将DX11纹理复制到辅助纹理中,并将其绑定到OpenGL纹理进行实现。 最后,Unity中的脚本可以使用DX11共享纹理进行更高级的图形处理,例如实现自定义的图形特效、着色器等。通过将Unity和DX11结合起来,可以获得更高质量和更灵活的图像渲染效果,从而提升游戏的视觉体验。 总的来说,Unity中的DX11共享纹理能够实现Unity和DX11之间的纹理资源共享,需要使用支持DX11的插件并进行纹理格式转换。这种搭配可以提供更多的图形渲染功能和更好的图像效果,让游戏开发者能够更自由地实现各种图形特效和着色器。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值