前置知识:安卓集成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() 一步步检查报错,按上面的流程应该是没有问题的,完整 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

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 做完效果之后的。

END

感谢阅读

Unity安卓共享纹理的更多相关文章

  1. [Unity][安卓]Unity和Android Studio 3.0 交互通讯(1)Android Studio 3.0 设置

    [安卓]Android Studio 3.0 JDK安卓环境配置(2017.10) http://blog.csdn.net/bulademian/article/details/78387052 [ ...

  2. OpenGL 多线程共享纹理

    1:opengl 多线程共享纹理纹理: //解码时候使用opengl进行绘制,需要构建队列和两个线程,分别用于解码数据并且填充纹理和渲染. 主线程常见两个共享上下文: main() { ⋯⋯⋯⋯ gH ...

  3. unity开发android游戏(一)搭建Unity安卓开发环境

    unity开发android游戏(一)搭建Unity安卓开发环境 分类: Unity2014-03-23 16:14 5626人阅读 评论(2) 收藏 举报 unity开发androidunity安卓 ...

  4. Unity安卓apk打包过程

    前言:对于Unity开发小白来说,Android打包无疑是个头痛的问题,所以我总结了 Unity安卓APK的打包过程 第一步:下载对应版本的Android Platform 第二步:安装JDK并配置J ...

  5. unity安卓和IOS读写目录

    StreamingAssets文件夹下的只读不可写路径: 安卓读:filePath = Application.streamingAssetsPath + "/文件名.格式名"; ...

  6. 【转】unity开发android游戏(一)搭建Unity安卓开发环境

    http://blog.csdn.net/chenggong2dm/article/details/20654075 1,下载安装Java的JDK: http://www.oracle.com/tec ...

  7. Unity安卓上播放视频的问题,暂时无解记录一下

    设备联想A7600m,好像是联发科的cpu 先用网上流传很广的这个Unity自带接口试验一下: Handheld.PlayFullScreenMovie(Path.Combine(Applicatio ...

  8. Unity 安卓Jar包的小错误

    好久没写博客了,也就意味着好久没有学习了,近几天在搞Unity接入有米的SDk遇到了一点小错误,今天早上解决了,和大家分享下! 1,我们的目的是在在U3D中调用Android产生的Jar包,首先在Ec ...

  9. 搭建Unity安卓开发环境

    原文见 https://blog.csdn.net/chenggong2dm/article/details/20654075 tiny教程 https://docs.unity3d.com/Pack ...

随机推荐

  1. 这一次搞懂Spring的Bean实例化原理

    文章目录 前言 正文 环境准备 两个重要的Processor 注册BeanPostProcessor对象 Bean对象的创建 createBeanInstance addSingletonFactor ...

  2. v-if和v-show的使用和特点

    v-if的特点是每次都会重新删除或创建操作 v-show的特点是每次不会进行DOM的删除和创建操作,只是切换了元素的display:none样式 <div id="app"& ...

  3. 手写spring事务框架-蚂蚁课堂

    1.视频参加C:\Users\Administrator\Desktop\蚂蚁3期\[www.zxit8.com] 0017-(每特教育&每特学院&蚂蚁课堂)-3期-源码分析-手写Sp ...

  4. 入门大数据---Flume 简介及基本使用

    一.Flume简介 Apache Flume 是一个分布式,高可用的数据收集系统.它可以从不同的数据源收集数据,经过聚合后发送到存储系统中,通常用于日志数据的收集.Flume 分为 NG 和 OG ( ...

  5. 入门大数据---基于Zookeeper搭建Kafka高可用集群

    一.Zookeeper集群搭建 为保证集群高可用,Zookeeper 集群的节点数最好是奇数,最少有三个节点,所以这里搭建一个三个节点的集群. 1.1 下载 & 解压 下载对应版本 Zooke ...

  6. ceph luminous版本的安装部署

    1. 前期准备   本次安装环境为:   ceph1(集群命令分发管控,提供磁盘服务集群) CentOs7.5 10.160.20.28   ceph2(提供磁盘服务集群) CentOs7.5 10. ...

  7. Python实用笔记 (15)函数式编程——装饰器

    什么函数可以被称为闭包函数呢?主要是满足两点:函数内部定义的函数:引用了外部变量但非全局变量. python装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰 ...

  8. 内存疯狂换页!CPU怒批操作系统

    内存访问瓶颈 我是CPU一号车间的阿Q,前一阵子我们厂里发生了一件大喜事,老板拉到了一笔投资,准备扩大生产规模. 不过老板挺抠门的,拉到了投资也不给我们涨点工资,就知道让我们拼命干活,压榨我们的劳动力 ...

  9. HotSpot二分模型(1)

    HotSpot采用了OOP-Klass模型来描述Java类和对象.OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象的具体类型. 那么为何要设计这样一 ...

  10. c语言学习笔记第二章———入门

    B站有视频演示 2.1软件安装 推荐软件 1.dev-c++ 下载链接:(腾讯软件管家的下载地址) https://sm.myapp.com/original/Development/Dev-Cpp_ ...