Unity的Flutter组件渲染

字节跳动

unity和flutter都是可以通过opengl和vulkan绘制界面,那有没有一种方法可以使得二者界面互相融合,即将flutter的界面渲染到unity的物体中,或者将unity的界面渲染到flutter的widget上。由于这两种渲染方式大体相同,下面我们就着重讲下如何将flutter界面渲染到unity中。

首先我们想到的是将flutter界面截屏成bitmap,然后通过交互将bitmap传递给unity,并在unity中使用该bitmap,并且我们也可以立马发现flutter自带截屏函数。但是我们会立刻发现此方案的弊端,此方案首先需要将flutter界面从gpu中下载到内存,然后再通过unity与java通信将bitmap传递到unity中,最后再在unity中将bitmap上传至gpu中作为纹理使用。如此一番折腾,需要在内存中倒腾很多遍,更何况bitmap通常很大,来回传递严重影响效率。那有没有一种更好的方案来解决这个问题呢,当然有,那就是--

纹理共享

我们知道opengl上下文通常是和线程绑定的,不同上下文之间环境比较独立,无法共享内容,但是为了更好的在多线程环境下工作,opengl提供了纹理共享的方式来弥补上诉问题,提高多线程下工作效率。使用方法也比较简单,即在新建opengl环境的时候传入一个已有opengl上下文和configs,这样就可以在两个opengl环境下共享使用同一份纹理(具体可以搜索opengl纹理共享)。

至于如何在安卓上实现与unity的纹理共享,大家可以参考该文章blog.csdn.net/jnlws/artic…

该文章讲解的比较详细,并且代码比较完善,大家可以仔细阅读并自己实现一遍即可,这边我大概讲解下相机纹理共享的实现流程(因为flutter共享过程与之类似)。

  1. 在unity线程中通过与安卓的通信方式回调java的方法
  2. 在java方法中获取该unity线程的opengl上下文和参数配置,同时新建一个java 的线程用来作java的渲染线程,并在java线程中新建一个opengl上下文环境,传入unity的opengl上下文,以此来实现纹理共享
  3. 将安卓相机数据输出到surfacetexture,同时将该surfacetexture绑定到opengl中新建的一个textureid上,通过fbo离屏渲染将相机数据渲染到新的textureid(因为安卓中surfacetexture输出的纹理是安卓特有的纹理格式GL_TEXTURE_EXTERNAL_OES,无法直接在unity中使用,因此需要通过离屏渲染将其转换成unity可以使用的纹理格式),返回新的textureid给unity
  4. unity收到textureid后将其渲染到gameobject上

关键代码如下

纹理共享是opengl的方法,对于vulkan和metal这样天生就支持多线程的渲染管线,不需要这种方式,不过目前对于vulkan不是很熟悉,所以这里就先不进行讲解。

flutter界面渲染到纹理中

上面我们已经分析了如何将相机数据通过纹理共享到unity中,我们知道可以通过surfacetexture和fbo离屏渲染将纹理共享给unity,因此我们只需要找到如何将flutter界面渲染到surfacetexture中,即可实现flutter和unity界面的融合。接下来我们来分析flutter源码,接下来的源码都是基于flutter1.16来分析的。

首先flutter在新的版本中,为了方便进行混合接入,抽离出了flutter engine,而且我们不需要将flutter界面直接渲染出来,所以我们只需要创建一个没有view的flutter fragment放入unity activity中就可以了。我们将flutter fragment源码拷贝出来并进行改造,与之对应的还需要将FlutterActivityAndFragmentDelegate,接下来我们顺着代码分析flutter界面是如何渲染到安卓上的,首先flutter是通过FlutterView加载到安卓界面的

而FlutterView分为两种模式,我们只需要分析surface模式,我们发现有个FlutterSurfaceView用来实现和flutter关联,我们发现在surfaceview surface创建的时候通过FlutterRender将surface传入flutter中。

这下分析下来就比较明了了,我们只需要创建一个surface,通过FlutterRender将其传给flutter,同时通过将其数据输出到surfacetexture,并通过fbo离屏渲染将其输出到unity可以使用的纹理id中即可。代码如下

  public void attachToFlutterEngine(FlutterEngine flutterEngine) {
        this.flutterEngine = flutterEngine;
        FlutterRenderer flutterRenderer = flutterEngine.getRenderer();

        flutterRenderer.startRenderingToSurface(GLTexture.instance.surface);
        flutterRenderer.surfaceChanged(GLTexture.instance.getStreamTextureWidth(), GLTexture.instance.getStreamTextureHeight());
        FlutterRenderer.ViewportMetrics viewportMetrics = new FlutterRenderer.ViewportMetrics();
        viewportMetrics.width = GLTexture.instance.getStreamTextureWidth();
        viewportMetrics.height = GLTexture.instance.getStreamTextureHeight();
        viewportMetrics.devicePixelRatio = GLTexture.instance.context.getResources().getDisplayMetrics().density;
        flutterRenderer.setViewportMetrics(viewportMetrics);
        flutterRenderer.addIsDisplayingFlutterUiListener(new FlutterUiDisplayListener() {
            @Override
            public void onFlutterUiDisplayed() {
                GLTexture.instance.setNeedUpdate(true);
                GLTexture.instance.updateTexture();
            }

            @Override
            public void onFlutterUiNoLongerDisplayed() {

            }
        });
        GLTexture.instance.attachFlutterSurface(this);
    }
复制代码

这里需要注意的是需要给flutter传递宽和高,不然flutter界面可能显示不出来

接下来我们只需要和相机一样,在unity中接收纹理并渲染到gameobject就可以了。

最终实现效果如下,我们可以看到flutter界面完美的渲染到unity中了

整体代码流程如下,其中重绘时候数据都在gpu内部,不涉及来回拷贝,这就是纹理共享的好处,可以实现更高的刷新率。

点击事件

我们接下来还需要处理flutter点击事件,我们需要在unity中获取点击事件,并将其传递给安卓,然后传递给flutter即可。

unity点击处理代码如下

void Update()
{
    #if UNITY_ANDROID
    if(mGLTexCtrl.Call<bool>("isNeedUpdate"))
        mGLTexCtrl.Call("updateTexture");
    if (Input.touches.Length > 0){
        if(haveStartFlutter == 1){
           //坐标转换并传递给java,代码省略,具体可以参考unity事件处理
        }else{
            mFlutterApp.Call("startFlutter");
            haveStartFlutter = 1;
        }
    }
    #endif 

    if(Input.GetMouseButtonDown(1)){
        Debug.Log(Input.mousePosition);
    }
}
复制代码

安卓传递给flutter代码如下。这里我们可以在flutter源码中发现如何传递点击事件,这里也借助于flutter源码来实现

public void onTouchEvent(int type, double x, double y) {
        ByteBuffer packet =
                ByteBuffer.allocateDirect(1 * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
        packet.order(ByteOrder.LITTLE_ENDIAN);
        double x1, y1;
        x1 = GLTexture.instance.getStreamTextureWidth() * x;
        y1 = GLTexture.instance.getStreamTextureHeight() * y;
        Log.i("myyf", "x:" + x1 + "&y:" + y1 + "&type:" + type);
        addPointerForIndex(x1, y1, type + 4, 0, packet);
        if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) {
            throw new AssertionError("Packet position is not on field boundary");
        }

        flutterEngine.getRenderer().dispatchPointerDataPacket(packet,packet.position());

    }

//传递点击信息 ,具体代码参考flutter
// TODO(mattcarroll): consider creating a PointerPacket class instead of using a procedure that
// mutates inputs.
private void addPointerForIndex(
        double x, double y, int pointerChange, int pointerData, ByteBuffer packet) {
    if (pointerChange == -1) {
        return;
    }
    //……此处省略
   

}
复制代码

这样我们就可以实现在unity中点击flutter界面了最终实现效果如下

总结

上面我们分析了如和将flutter界面渲染到unity中,通过opengl的纹理共享和安卓的surface即可实现,同理如何将unity界面渲染到flutter也是一样,我们只需要自定义UnityPlayer将其输出到纹理中,并在flutter中使用即可,可以更广泛的推广,我们可以通过这种方法将安卓界面渲染到flutter和unity中。

文章分类
Android