Unity的Flutter元件渲染

位元組遊戲中臺客戶端團隊發表於2021-08-27

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中。

相關文章