VRM 選択画面と動的ロードの実装詳細
Unity 2019.1.3f1 で新規プロジェクトを作成
ゲームを起動すると次の UI が表示されるように UI 要素をセットします。
VRM を動的にロードして TPS カメラワークで TPS キャラクターとして動かしてみます。
依存するアセットは
assetstore.unity.com
assetstore.unity.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 要素やアセットと結びつけます。
Cinemachine の Free Look の Radius の設定は次の通り
追記:おっと拡張メソッド使っていますね。
以下のような拡張メソッドを実装しておきました。
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