UnityのWebGLで作ったサイトにドラッグ&ドロップでVRMを出現させる


VRMの切り替えをドラッグ&ドロップでやりたい

VRMをPartyParrot風に表示できるPartyParrotVRMを作ってみたという記事にまとめている「PartyParrotVRM」を作っている際に、VRMの切り替えをドラッグ&ドロップでやりたいと思いました。

実装したものがこちらです

途中で固まってしまうのがネックなのですが別スレッドで実行するなどの処理がうまくいかず現時点ではこのようになっています…一応切り替えれているのでいいかなと…

drag&dropを検知してUnityにデータを渡す

VRMのSDKを入れたプロジェクトを作成しておきます。
そして、以下のようなC#を定義してGameObjectに追加します。

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using VRM;

namespace Provider
{
    public class VrmProvider : MonoBehaviour
    {

        private int length;
        private int index;
        private List<byte> byteContent = new List<byte>();

        public void ByteLength(int length)
        {
            this.length = length;
            index = 0;
            byteContent.Clear();
        }

        public void AddRange(string byteString)
        {
            var decode = Convert.FromBase64String(byteString);
            byteContent.AddRange(decode);
            index += 1;
            if (length != index) return;
            Spawn();
        }

        public void Spawn()
        {
            var vrmBytes = byteContent.ToArray();
            var context = new VRMImporterContext();
            context.ParseGlb(vrmBytes);
            context.Load();
            context.ShowMeshes();
            context.EnableUpdateWhenOffscreen();
        }
    }
}

上記のcsのメソッドに対してjsからデータを送り込んでいきます。

UnityのWebGLTempleteを編集して以下のjsを追加して、index.htmlにもscriptタグで追加しました。

dragAndDrop.js

document.addEventListener('DOMContentLoaded', function () {
    var dropArea = document.getElementById('screen')

    function confirmVrmFile(files) {
        let length = files.length, file

        for (let i = 0; i < length; i++) {
            file = files[i]

            let isVrmFile = file.name.endsWith('.vrm')

            if (isVrmFile) {
                // .vrmのbyteをstringに変換する
                let reader = new FileReader()
                reader.onload = function () {
                    let source = this.result
                    let bytes = new Uint8Array(source)
                    let len = source.byteLength
                    let byteString = ""
                    for (var i = 0; i < len; i++) {
                        byteString += String.fromCharCode(bytes[i])
                    }
                    var base64String = window.btoa(byteString)
                    sendUnity(base64String)
                };
                reader.readAsArrayBuffer(file)
                break
            }
        }
    }

    function sendUnity(base64String) {
        // stringの容量が大きいままSendMessageに渡そうとするとエラーが起きるので文字列を分割する
        let splitLength = 1000
        let len = parseInt(base64String.length / splitLength)
        unityInstance.SendMessage("VrmProvider", "ByteLength", len + 1)
        for (let i = 0; i < len; i++) {
            let next = base64String.substr(i * splitLength, splitLength)
            unityInstance.SendMessage("VrmProvider", "AddRange", next)
        }
        let last = base64String.substr(
            splitLength * len,
            base64String.length % splitLength
        )
        unityInstance.SendMessage("VrmProvider", "AddRange", last)
    }

    dropArea.addEventListener('dragover', function (event) {
        event.preventDefault()
        event.dataTransfer.dropEffect = 'copy'
        dropArea.classList.add('dragover')
    })

    dropArea.addEventListener('dragleave', function () {
        dropArea.classList.remove('dragover')
    })

    dropArea.addEventListener('drop', function (event) {
        event.preventDefault()
        dropArea.classList.remove('dragover')
        confirmVrmFile(event.dataTransfer.files)
    })
})

実際にPartyParrotVRMというので使っているものから少し変えていますがこれでドラッグ&ドロップで動くと思います。

まとめ

画面が固まらないようにするのができれば切り替えたいとは思っています。最低限の切り替え処理にはなりますがそんなにコードを書かなくてもできたので便利だなと思っています。VRMも簡単に扱えてとても便利な印象を持ちました。