详解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安卓共享纹理的资料请关注其它相关文章!
推荐阅读
-
android选择文件(安卓系统文件夹详解)
-
猩猩安卓模拟器共享文件夹在哪里?猩猩模拟器共享文件夹位置介绍
-
雷电安卓模拟器共享文件怎么用?电脑文件与模拟器互通图文教程
-
微信JS-SDK updateAppMessageShareData安卓不能自定义分享详解
-
小程序scroll-view安卓机隐藏横向滚动条的实现详解
-
新版wifi共享精灵xp安卓手机搜不到热点解决方法介绍
-
详解一次Vue低版本安卓白屏问题的解决过程
-
安卓平板电脑如何恢复出厂设置?安卓平板恢复出厂设置方法图文详解
-
安卓手机QQ 8.2.0发布:备份聊天记录、共享实时位置
-
Ubuntu 14.04下创建Genymotion安卓虚拟机的步骤详解