simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:オンラインVRMチャットサンプルの作り方1

VRM 選択画面と動的ロードの実装詳細

f:id:simplestar_tech:20190519192536p:plain
最後まで読むとできるようになる絵

Unity 2019.1.3f1 で新規プロジェクトを作成
ゲームを起動すると次の UI が表示されるように UI 要素をセットします。

f:id:simplestar_tech:20190519182834p:plain
ファイル選択UI

VRM を動的にロードして TPS カメラワークで TPS キャラクターとして動かしてみます。

依存するアセットは
assetstore.unity.com
assetstore.unity.com

github.com

依存するパッケージは
Cinemachine
TMPro(日本語使えるように準備したもの)

以下のロジックを組みます。

using Cinemachine;
using Invector.vCharacterController;
using Invector.vCharacterController.vActions;
using SimpleFileBrowser;
using System;
using System.IO;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using VRM;

public class VRMPlayerModelLoader : MonoBehaviour
{
    #region UI Connection
    [SerializeField] Button buttonBrowse;
    [SerializeField] Button buttonLoadVRM;
    [SerializeField] InputField inputFieldVRMPath;
    [SerializeField] TMP_Text textStatus;
    [SerializeField] GameObject panelSelectLoadVRM;
    #endregion

    #region Scene Components
    [SerializeField] Transform playerLookAtTarget;
    [SerializeField] CinemachineFreeLook cinemachineFreeLook;
    #endregion

    #region Assets
    [SerializeField] RuntimeAnimatorController playerAnimController;
    [SerializeField] Shader vrmShader;
    #endregion

    internal UnityAction<GameObject> OnLoadPlayerVRM;

    private void Start()
    {
        this.InitializeUi();
    }

    #region VRM

    public void OnButtonBrowse()
    {
        FileBrowser.SetFilters(false, new FileBrowser.Filter("VRM", ".vrm"));
        FileBrowser.SetDefaultFilter(".vrm");
        FileBrowser.SetExcludedExtensions(".lnk", ".tmp", ".zip", ".rar", ".exe");
        FileBrowser.AddQuickLink("Downloads", @"C:\Users\" + Environment.UserName + @"\Downloads");
        FileBrowser.ShowLoadDialog(path => this.inputFieldVRMPath.text = path, null, false, @"C:\Users\" + Environment.UserName + @"\Downloads");
    }

    public void OnButtonLoadVRM()
    {
        if (0 == this.inputFieldVRMPath.text.Length)
        {
            textStatus.text = "Input VRM Path.";
            return;
        }

        var vrmFilePath = this.inputFieldVRMPath.text;
        if (!File.Exists(vrmFilePath))
        {
            textStatus.text = "VRM Path File is Not Found.";
            return;
        }

        var vrmData = File.ReadAllBytes(vrmFilePath);
        var vrmRoot = LoadVRMinMemory(vrmData);

        if (null == vrmRoot)
        {
            textStatus.text = "Invalid VRM File, VRM Instantiation is Failed.";
            return;
        }
        PlayerPrefs.SetString(PlayerVRMPath, vrmFilePath);
        this.panelSelectLoadVRM.SetActive(false);
    }

    private void InitializeUi()
    {
        this.buttonBrowse.onClick.AddListener(OnButtonBrowse);
        this.buttonLoadVRM.onClick.AddListener(OnButtonLoadVRM);
        var playerVRMPath = PlayerPrefs.GetString(PlayerVRMPath);
        if (File.Exists(playerVRMPath))
        {
            this.inputFieldVRMPath.text = playerVRMPath;
        }
    }

    public GameObject LoadVRMinMemory(byte[] vrmData, bool isUserPlayerFlag = true)
    {
        var context = new VRMImporterContext();
        context.ParseGlb(vrmData);
        context.Load();
        if (null != context.Root && isUserPlayerFlag)
        {
            int layerMask = LayerMask.NameToLayer(LayerMask_Player);
            context.Root.SetLayerRecursively(layerMask);
            context.Root.SetTagRecursively(Tag_Player);
        }
        context.ShowMeshes();
        context.EnableUpdateWhenOffscreen();
        context.ShowMeshes();
        if (null != context.Root)
        {
            SetModel(context.Root, isUserPlayerFlag);
        }
        return context.Root;
    }

    void SetModel(GameObject vrmRoot, bool isUserPlayerFlag)
    {
        if (null != this.vrmShader)
        {
            foreach (var renderer in vrmRoot.GetComponentsInChildren<Renderer>())
            {
                foreach (var material in renderer.sharedMaterials)
                {
                    var tex = material.mainTexture;
                    material.shader = this.vrmShader;
                    material.mainTexture = tex;
                }
            }
        }
        var lookAt = vrmRoot.GetComponent<VRMLookAtHead>();
        if (lookAt)
        {
            vrmRoot.AddComponent<Blinker>();
            lookAt.Target = this.playerLookAtTarget;
            lookAt.UpdateType = UpdateType.LateUpdate; // after HumanPoseTransfer's setPose
        }

        if (isUserPlayerFlag)
        {
            var target = vrmRoot.transform.FindHeightRecursively(1.0f);
            cinemachineFreeLook.Follow = vrmRoot.transform;
            cinemachineFreeLook.LookAt = target;

            // Invector が動的コンポーネント追加を考慮していないので、必要と思われる公開パラメータをここで初期化
            var vInput = vrmRoot.AddComponent<vThirdPersonInput>();
            vInput.OnLateUpdate = new UnityEngine.Events.UnityEvent();
            vInput.jumpInput = new GenericInput("Space", "A", "A");
            vInput.rollInput = new GenericInput("Q", "Y", "Y");
            vInput.crouchInput = new GenericInput("C", "X", "X");

            var vController = vrmRoot.AddComponent<vThirdPersonController>();
            vController.strafeSpeed = new vThirdPersonMotor.vMovementSpeed();
            vController.freeSpeed = new vThirdPersonMotor.vMovementSpeed();
            vController.OnCrouch = new UnityEvent();
            vController.OnStandUp = new UnityEvent();
            vController.OnJump = new UnityEvent();
            vController.OnStartSprinting = new UnityEvent();
            vController.OnFinishSprinting = new UnityEvent();
            vController.OnFinishSprintingByStamina = new UnityEvent();
            vController.OnStaminaEnd = new UnityEvent();

            var vAction = vrmRoot.AddComponent<vGenericAction>();
            vAction.OnStartAction = new UnityEvent();
            vAction.OnEndAction = new UnityEvent();
        }

        var animator = vrmRoot.GetComponent<Animator>();
        if (animator && !animator.runtimeAnimatorController)
        {
            animator.runtimeAnimatorController = playerAnimController;
        }
        if (isUserPlayerFlag)
        {
            var rb = vrmRoot.AddComponent<Rigidbody>();
            rb.constraints = RigidbodyConstraints.FreezeRotation;
            rb.collisionDetectionMode = CollisionDetectionMode.Continuous;

            var coll = vrmRoot.AddComponent<CapsuleCollider>();
            coll.center = new Vector3(0, 0.8f, 0);
            coll.radius = 0.25f;
            coll.height = coll.center.y * 2;

            var animSync = vrmRoot.AddComponent<AnimationSync>();           
            OnLoadPlayerVRM?.Invoke(vrmRoot);
        }
    }
    
    #endregion

    const string PlayerName = "PlayerName";
    const string PlayerVRMPath = "PlayerVRMPath";
    const string Tag_Player = "Player";
    const string LayerMask_Player = "Player";
}

シーンに配置したら、次のように UI 要素やアセットと結びつけます。

f:id:simplestar_tech:20190519190230p:plain
VRMモデルローダースクリプトのインスペクタ

Cinemachine の Free Look の Radius の設定は次の通り

f:id:simplestar_tech:20190519190514p:plain
Free Look 半径の設定

追記:おっと拡張メソッド使っていますね。
以下のような拡張メソッドを実装しておきました。

TransformExtensions.cs

using UnityEngine;

public static class TransformExtensions
{
    public static Transform FindRecursively(
        this Transform self,
        string n
    )
    {
        var child = self.Find(n);
        if (null == child)
        {
            foreach (Transform c in self.transform)
            {
                child = FindRecursively(c, n);
                if (null != child)
                {
                    break;
                }
            }
        }
        return child;
    }

    public static Transform FindHeightRecursively(
        this Transform self,
        float height
    )
    {
        if (height < self.localPosition.y)
        {
            return self;
        }
        foreach (Transform c in self.transform)
        {
            var child = FindHeightRecursively(c, height - self.localPosition.y);
            if (null != child)
            {
                return child;
            }
        }
        return null;
    }
}

GameObjectExtensions.cs

using UnityEngine;

public static class GameObjectExtensions
{
    public static void SetLayerRecursively(
        this GameObject self,
        int layer
    )
    {
        self.layer = layer;

        foreach (Transform n in self.transform)
        {
            SetLayerRecursively(n.gameObject, layer);
        }
    }

    public static void SetTagRecursively(
        this GameObject self,
        string tag
    )
    {
        self.tag = tag;

        foreach (Transform n in self.transform)
        {
            SetTagRecursively(n.gameObject, tag);
        }
    }
}

Load VRM ボタンを押せば、次の通りマウスで TPS カメラ操作しつつ、WSAD キーで TPS キャラクター操作しつつ E キーでアクションできるようになっています。
カメラ操作は Free Look のディフォルトの入力を好みで反転させました。

次の記事ではチャット入力画面を作ってみましょう。
simplestar-tech.hatenablog.com