欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

详解Unity安卓共享纹理

程序员文章站 2022-06-23 09:10:27
概述本文的目的是实现以下的流程:android/ios native app 操作摄像头 -> 获取视频流数据 -> 人脸检测或美颜 -> 传输给 unity 渲染 -> un...

概述

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

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

相关标签: Unity 共享纹理