UnityのWebGLビルドでgif撮影/保存する


PartyParrotぽいVRMサイト作ってみるならgif保存がしたい

ホントにそれだけでgif保存に手を出してみました。意外とやっている話が見つからずjsのライブラリを使ってやることにしました。

jslibとの連携はUnityのjslib経由で別ファイルjsを呼び出すで書いてある手法を使用しています。

実装したのがこちら、VRMを移動させてPartyParrot風のgifをダウンロードします

↓ ダウンロードしたgif

かなり雑ですがキャプチャできています。今のままではファイルサイズが大きいため、容量削減のために解像度を低くしたりなどの一工夫が必要にはなる気がしています。

使ったjsのライブラリはccapture.jsというものです。

そこのissueにgif capture result contains complete black framesというUnityのWebGLを表示しているcanvasをキャプチャしても真っ暗な画面になるというものがありました。実際自分も同じ現象にあって調べるうちに辿り着きました。そしてissueの最後にWaitForEndOfFrame()で最後のフレームまで待ってからjsでcaptureを実行することで成功しました。感謝🙏

実装

PartyParrotVRMはuGUIボタンからイベントが始まるのでその処理を基準に書きます。

jslibとNativeExecuter.csはUnityのjslib経由で別ファイルjsを呼び出すを使っているものとして省きます。

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;

namespace Sample
{
    public class DownloadGif : MonoBehaviour
    {

        [SerializeField]
        public class CaptureParameter
        {
            public int index;
            public int max;
        }

        private const int CAPTURE_COUNT = 10;

        [SerializeField] private Button downloadGifButton;
        private NativeExecuter executer = new NativeExecuter();

        private int captureIndex = 0;

        public string CallbackMethodName
        {
            get
            {
                Action callback = StartNotify;
                return callback.Method.Name;
            }
        }

        void Start()
        {
            downloadGifButton.onClick.AddListener(() =>
            {
                var callbackParameter = new CallbackParameter
                {
                    callbackGameObjectName = gameObject.name,
                    callbackFunctionName = CallbackMethodName
                };
                var parameterJson = JsonUtility.ToJson(callbackParameter);
                executer.Execute("downloadStartGif", parameterJson);
            });
        }

        public void StartNotify()
        {
            StartCoroutine(CaptureScreen());
        }

        IEnumerator CaptureScreen()
        {
            while (captureIndex < CAPTURE_COUNT)
            {
                yield return new WaitForEndOfFrame();
                captureIndex += 1;
                var captureParameter = new CaptureParameter
                {
                    index = captureIndex,
                    max = CAPTURE_COUNT
                };
                var parameterJson = JsonUtility.ToJson(captureParameter);
                executer.Execute("captureFrame", parameterJson);
            }
            captureIndex = 0;
        }
    }
}

WebGLTempleteにあるindex.htmlに以下の二行を追加しておきます。

index.html
  <script src="https://cdn.jsdelivr.net/npm/ccapture.js/build/CCapture.all.min.js"></script>
  <script id="execute" src="execute.js"></script>

ccapture.jsに必要な処理のようなのですがgif.worker.jsというものをダウンロードしてindex.htmlの階層配下に置いておく必要があるそうです。ないとエラーが出ました。

executer.js
let capturer = {}
let captureIndex = 0

function downloadStartGif(parameter) {
  unityCanvas = document.getElementById('#canvas')
  // workersPathはgif.worker.jsの置き場です。この場合は`js/gif.worker.js`という置き方をしています。
  capturer = new CCapture( { format: 'gif', workersPath: 'js/' } )
  capturer.start()
  unityInstance.SendMessage(parameter.callbackGameObjectName, parameter.callbackFunctionName)
}

function captureFrame(parameter) {
  capturer.capture(unityCanvas)
  if (parameter.index === parameter.max) {
    capturer.stop()
    capturer.save()
  }
}

function recieveMessage(event) {
  var data = JSON.parse(event.detail)
  var methodName = data.methodName
  var parameter = data.parameter
  try {
    parameter = JSON.parse(parameter)
  } catch (e) {
    parameter = null
  }
  eval(`${methodName}(parameter)`)
}

window.addEventListener('unityMessage', recieveMessage, false)

まとめ

Unityでgif作成処理と保存処理を作成するよりもネイティブのライブラリを使った方がいいかなと思い今回はネイティブ側で実装しました。issueにたどり着くまでに時間が少しかかりましたがわかってしまえば結構簡単でした。
実はキャプチャのライブラリをいくつか試しているうちにUnityのWebGL canvasのContextはwebgl2で取得できるという知見を得ました。WebGL2RenderingContextで詳しい描画のデータが取れるようです。ネイティブ書くときに必要になるかもな…というメモ…