服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C# - 详解Unity安卓共享纹理

详解Unity安卓共享纹理

2022-11-21 14:49zhxmdefj C#

本文主要介绍了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安卓共享纹理

然后就可以将 Unity 工程打包到安卓项目,如果没意外是可以显示纹理出来的。

详解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安卓共享纹理

这时候你想做什么效果都很简单了,比如用 Unity Shader 写一个赛博朋克风格的滤镜:

详解Unity安卓共享纹理

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安卓共享纹理的详细内容,更多关于Unity安卓共享纹理的资料请关注服务器之家其它相关文章!

原文链接:https://www.cnblogs.com/zhxmdefj/p/13295243.html

延伸 · 阅读

精彩推荐
  • C#C#使用NPOI导入Excel的方法详解

    C#使用NPOI导入Excel的方法详解

    这篇文章主要介绍了C#使用NPOI导入Excel的方法,简单介绍了NPOI的功能及操作Excel的导入、读取等相关技巧,需要的朋友可以参考下...

    布瑞泽的童话7562021-12-31
  • C#C# 服务器发送邮件失败实例分析

    C# 服务器发送邮件失败实例分析

    在本篇文章里小编给大家带来一篇关于C# 服务器发送邮件失败实例内容,需要的朋友们可以学习下。...

    一无是处谢4602022-08-28
  • C#Directory文件类的实例讲解

    Directory文件类的实例讲解

    下面小编就为大家分享一篇Directory文件类的实例讲解,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    happy多乐4142022-02-12
  • C#C#中判断一个集合是否是另一个集合的子集的简单方法

    C#中判断一个集合是否是另一个集合的子集的简单方法

    本文介绍利用C#中内置的系统函数判断一个集合是否是一个集合的子集的方法,此方法代码量极少,分享给大家。...

    Darren Ji9282021-11-18
  • C#C#利用DesignSurface如何实现简单的窗体设计器

    C#利用DesignSurface如何实现简单的窗体设计器

    这篇文章主要介绍了C#利用DesignSurface如何实现简单窗体设计器的相关资料,文中通过图文及示例代码介绍的很详细,对大家具有一定的参考价值,需要的朋...

    JackWang-CUMT4102021-12-27
  • C#C# SqlHelper应用开发学习

    C# SqlHelper应用开发学习

    这篇文章主要和大家一起学习C# SqlHelper应用开发,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    蒙小米9992021-12-18
  • C#C#简单获取全屏中鼠标焦点位置坐标的方法示例

    C#简单获取全屏中鼠标焦点位置坐标的方法示例

    这篇文章主要介绍了C#简单获取全屏中鼠标焦点位置坐标的方法,涉及C#针对鼠标位置Position属性的简单操作技巧,需要的朋友可以参考下...

    a7719485248962022-01-12
  • C#c# 绘制中国象棋棋盘与棋子

    c# 绘制中国象棋棋盘与棋子

    这篇文章主要介绍了c# 绘制中国象棋棋盘与棋子,文中实例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下...

    Alan.hsiang11902022-09-23