概述

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

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安卓共享纹理的资料请关注www.887551.com其它相关文章!