- 投稿日:2019-03-30T23:41:20+09:00
オレオレInputModuleを作ってみた!for XR
汎用的なXR向けInputModule
プロジェクトはこちら
https://github.com/r-benjamin-cotton/XRController.git
解説
ターゲットとなるVRシステムに依存しない汎用的なInputModuleが欲しかったので作ってみました。
できた後でUnityのVRサンプルにもあることに気が付いたけれど、
きっと自分の求めてるのとは違うはず!(あっちのブドウはすっぱいよ!)私は標準の機能で作られたWorldSpaceのUIそのままで使えることを目標にしています。
EventDataやRaycasterには手を加えずInputModuleのみで実現できました。コントローラー位置は任意のGameObjectで指定し、
このオブジェクトのfoward方向に指定長さのレイを飛ばしUIを探索します。探索には全RaycasterをFindで列挙。
各Raycasterのオブジェクトの座標系のxy平面(z=0)にCanvasがあるものとしてレイと平面の衝突位置を計算。
衝突位置をRaycasterに付いているCameraでWorldScreen変換を行いRaycastを行います。
この時1つのRaycasterからは一つのリザルトのみ採用し最上位のUI要素を取り出します。すべてのRaycasterでHITテストを行い、結果のリストからコントローラーに最も近い物を検索。
このUI要素に対して各種イベント処理を発行していきます。最初EventSystemのRaycastAllも使ってみたのですが、
これだとプライオリティやヒエラルキー上の順位でのみ判定されるので、後ろのキャンバスにヒットが食われたりと使えませんでした。使う人が拡張しやすいよういわゆるレーザーポインターとしての処理は分離しました。
XRInputModuleにOnUpdatePositionイベントを作ったのでこれをフックすることで任意の処理を記述できます。またRaycasterをFindで拾ってくるのが重いのでこれも置き換え可能なようにしてあります。
prefabなどにして気軽に付け外しができるよう
EventSystem.currentを操作し強制的に置き変えるようにもしてみました。とはいえ実際に使う場合はそれぞれの目的に応じた形に書き換えてしまうほうが楽かも。
コード
XRInputModule.cs#define ENABLE_MOUSE_DEBUG #if !UNITY_EDITOR # undef ENABLE_MOUSE_DEBUG #endif using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.EventSystems; namespace MySpace { /// <summary> /// オレオレInputModule for XR /// </summary> public class XRInputModule : BaseInputModule { [SerializeField, Tooltip("ポインターとして使うオブジェクト")] private Transform controller = null; [SerializeField, Tooltip("レイの長さ")] private float rayLength = 10.0f; [SerializeField, Tooltip("クリックボタン(Input)")] private string clickButton = "Submit"; [SerializeField, Tooltip("近いもの優先")] private bool nearest = true; [SerializeField, Tooltip("強制的にcurrentのEventSystemを置き換える")] private bool forceSetEventSystem = true; private PointerEventData pointerEventData = null; private PointerEventData pointerEventDataWork = null; private GameObject holdedObject = null; private GameObject clickableObject = null; /// <summary> /// ポインターの更新ハンドラー /// </summary> /// <param name="hit"></param> /// <param name="controllerPosition"></param> /// <param name="targetPosition"></param> public delegate void OnUpdatePositionHandler(bool hit, Vector3 controllerPosition, Vector3 targetPosition); /// <summary> /// ポインター更新時に呼ばれるイベント /// </summary> public event OnUpdatePositionHandler OnUpdatePosition; public override bool IsModuleSupported() { #if ENABLE_MOUSE_DEBUG return true; #else return UnityEngine.XR.XRDevice.isPresent; #endif } public override bool ShouldActivateModule() { if (!base.ShouldActivateModule()) { return false; } return true; } public override void ActivateModule() { base.ActivateModule(); pointerEventData = new PointerEventData(eventSystem); } public override void DeactivateModule() { base.DeactivateModule(); Select(null); pointerEventData = null; } #if false public override void UpdateModule() { } #endif public override void Process() { if (eventSystem.currentSelectedGameObject != null) { var data = GetBaseEventData(); ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler); } var pointerEventData = GetPointerEventData(); var targetObject = pointerEventData.pointerCurrentRaycast.gameObject; if (pointerEventData.pointerEnter != targetObject) { base.HandlePointerExitAndEnter(pointerEventData, null); base.HandlePointerExitAndEnter(pointerEventData, targetObject); } var buttonDown = Input.GetButtonDown(clickButton); #if ENABLE_MOUSE_DEBUG if (Input.GetMouseButtonDown(0)) { buttonDown = true; } #endif if (buttonDown) { pointerEventData.eligibleForClick = true; pointerEventData.pressPosition = pointerEventData.position; pointerEventData.pointerPressRaycast = pointerEventData.pointerCurrentRaycast; pointerEventData.pointerPress = null; pointerEventData.pointerDrag = null; pointerEventData.dragging = false; holdedObject = null; clickableObject = null; if (targetObject != null) { holdedObject = ExecuteEvents.ExecuteHierarchy(targetObject, pointerEventData, ExecuteEvents.pointerDownHandler); clickableObject = ExecuteEvents.GetEventHandler<IPointerClickHandler>(targetObject); if (holdedObject == null) { holdedObject = clickableObject; } pointerEventData.pointerPress = holdedObject; pointerEventData.rawPointerPress = targetObject; pointerEventData.pointerDrag = ExecuteEvents.ExecuteHierarchy(targetObject, pointerEventData, ExecuteEvents.initializePotentialDrag); if (pointerEventData.pointerDrag != null) { ExecuteEvents.Execute(pointerEventData.pointerDrag, pointerEventData, ExecuteEvents.beginDragHandler); } } Select(targetObject); } if (pointerEventData.pointerDrag != null) { ExecuteEvents.Execute(pointerEventData.pointerDrag, pointerEventData, ExecuteEvents.dragHandler); } var buttonUp = Input.GetButtonUp(clickButton); #if ENABLE_MOUSE_DEBUG if (Input.GetMouseButtonUp(0)) { buttonUp = true; } #endif if (buttonUp) { if (holdedObject != null) { ExecuteEvents.ExecuteHierarchy(targetObject, pointerEventData, ExecuteEvents.pointerUpHandler); } if (targetObject != null) { var clickHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(targetObject); if ((clickHandler == clickableObject) && pointerEventData.eligibleForClick) { ExecuteEvents.Execute(clickHandler, pointerEventData, ExecuteEvents.pointerClickHandler); } else if (pointerEventData.pointerDrag != null) { ExecuteEvents.ExecuteHierarchy(targetObject, pointerEventData, ExecuteEvents.dropHandler); } } if (pointerEventData.pointerDrag != null) { ExecuteEvents.Execute(pointerEventData.pointerDrag, pointerEventData, ExecuteEvents.endDragHandler); } pointerEventData.eligibleForClick = false; pointerEventData.rawPointerPress = null; pointerEventData.pointerPress = null; pointerEventData.pointerDrag = null; pointerEventData.dragging = false; holdedObject = null; } } private void Select(GameObject gameObject) { if (gameObject != null) { var selectableObject = ExecuteEvents.GetEventHandler<ISelectHandler>(gameObject); base.eventSystem.SetSelectedGameObject(selectableObject); } else { base.eventSystem.SetSelectedGameObject(null); } } /// <summary> /// 現在の入力からPointerEventDataを生成 /// </summary> /// <returns></returns> private PointerEventData GetPointerEventData() { #if ENABLE_MOUSE_DEBUG // デバッグ用にコントローラーの向きをマウスで指示したオブジェクトへ if (!UnityEngine.XR.XRDevice.isPresent) { pointerEventDataWork.Reset(); pointerEventDataWork.position = Input.mousePosition; EventSystem.current.RaycastAll(pointerEventDataWork, m_RaycastResultCache); var raycast = FindFirstRaycast(m_RaycastResultCache); if (raycast.gameObject != null) { var rx = raycast.gameObject.GetComponentInParent<BaseRaycaster>(); if ((rx != null) && (rx.eventCamera != null)) { var sp = (Vector3)raycast.screenPosition; sp.z = raycast.distance; var px = rx.eventCamera.ScreenToWorldPoint(sp); var buttonDown = Input.GetMouseButtonDown(0); if (buttonDown) { //Debug.Log(raycast.gameObject.name + ":" + sp.ToString() + ":" + px.ToString()); } controller.LookAt(px, Vector3.up); } } m_RaycastResultCache.Clear(); } #endif // コントローラの位置と向きからレイを飛ばす var p0 = controller.position; var p1 = p0 + controller.TransformDirection(Vector3.forward * rayLength); RaycastAll(p0, p1, m_RaycastResultCache); // ヒットしたもののうち最も近い結果を取得 RaycastResult rr; Vector3 worldPos; Vector2 screenPosition; bool hit; if (m_RaycastResultCache.Count != 0) { rr = m_RaycastResultCache.OrderBy((a) => (a.worldPosition - p0).sqrMagnitude).First(); worldPos = rr.worldPosition; // 一時保存していたワールドポジション screenPosition = rr.screenPosition; hit = true; } else { rr = new RaycastResult(); worldPos = p1; screenPosition = Camera.main.WorldToScreenPoint(p0); hit = false; } m_RaycastResultCache.Clear(); // ポインター更新ベント発行 OnUpdatePosition?.Invoke(hit, p0, worldPos); // PointeEventDataを更新 { pointerEventData.Reset(); pointerEventData.delta = pointerEventData.position - screenPosition; pointerEventData.position = screenPosition; pointerEventData.scrollDelta = Vector3.zero; pointerEventData.pointerCurrentRaycast = rr; } // ヒットしたスクリーンポジションを使ってEventSystemのRaycastを呼びなおす。 // ※FindFirstRaycastでは最初に見つけたオブジェクトを返す=キャンバスのプライオリティとヒエラルキ上の順序により決まる // 三次元の位置を無視するので使いにくい~ if (!nearest) { eventSystem.RaycastAll(pointerEventData, m_RaycastResultCache); rr = FindFirstRaycast(m_RaycastResultCache); pointerEventData.pointerCurrentRaycast = rr; m_RaycastResultCache.Clear(); } #if false if (rr.isValid) { Debug.Log(GetPath(rr.gameObject)); } #endif return pointerEventData; } /// <summary> /// レイを飛ばす対象の配列を返す /// </summary> /// <returns></returns> protected BaseRaycaster[] GetRaycasters() { // 毎回FindObjectするのは重い!!!けどオブジェクトの増減をフックできない?ので仕方なし。。 // canvasの生成をユーザーが完全に管理しているのならここをoverrideし管理している配列を返すと効率的。 return FindObjectsOfType<BaseRaycaster>(); } private void RaycastAll(Vector3 p0, Vector3 p1, List<RaycastResult> results) { var raycasters = GetRaycasters(); foreach (var raycaster in raycasters) { var cam = raycaster.eventCamera; if (cam == null) { continue; } // レイをraycaster=canvas空間へ投影 var lp0 = raycaster.transform.InverseTransformPoint(p0); if (lp0.z > 0.0f) { continue; } var lp1 = raycaster.transform.InverseTransformPoint(p1); if (lp1.z < 0.0f) { continue; } // xy平面(z=0)にUIがあるものとして面までの距離を計算 var dist = lp1.z - lp0.z; if (dist < 1.192e-7) { continue; } // レイと平面までの距離から、交差点の座標を計算 var hp = Vector3.Lerp(lp0, lp1, -lp0.z / dist); // ワールド空間へ再投影 var wp = raycaster.transform.TransformPoint(hp); // 関連付けられているカメラのスクリーンへ投影 var sp = cam.WorldToScreenPoint(wp); // スクリーンポジションからraycast pointerEventDataWork.Reset(); pointerEventDataWork.position = new Vector2(sp.x, sp.y); var ix = results.Count; raycaster.Raycast(pointerEventDataWork, results); if (ix != results.Count) { // 1つのRaycasterにつき最初の一つだけ採用 var num = results.Count - ix; if (num > 1) { results.RemoveRange(ix + 1, num - 1); } var r = results[ix]; r.screenPosition = sp; r.worldPosition = wp; // GraphicRaycasterでは使われていないが一時領域として利用 results[ix] = r; } } } protected override void OnEnable() { base.OnEnable(); #if !ENABLE_MOUSE_DEBUG if (!UnityEngine.XR.XRDevice.isPresent) { Destroy(gameObject); return; } #endif // 強制的にイベントシステムを置き換える if (forceSetEventSystem) { if (EventSystem.current != eventSystem) { EventSystem.current = eventSystem; } } pointerEventDataWork = new PointerEventData(eventSystem); } protected override void OnDisable() { base.OnDisable(); pointerEventDataWork = null; holdedObject = null; clickableObject = null; } /// <summary> /// デバッグ用、transformの階層を取得 /// </summary> /// <param name="go"></param> /// <returns></returns> private static string GetPath(GameObject go) { string path = go.name; var parent = go.transform.parent; while (parent != null) { path = parent.name + "/" + path; parent = parent.parent; } return go.scene.name + ":" + path; } } }サンプルプロジェクトの説明。
サンプルではコントローラーのオブジェクトにXRInputTrackerというスクリプトをつけています。
このスクリプトでXRNodeから向きを取得しコントローラーに適用しています。
簡単な腕モデルで位置を計算しそれっぽく動かしてみました。
おまけというかテストのために色々入ってますがそれらもよければ参考にしてみてください。地面が寂しかったので動かしてみたけれど、これは酔うかも。。ごめんなさい。
テスト用にXRDeviceが提供されていない場合に
マウスカーソルとUIオブジェクトとの衝突点へコントローラーオブジェクトが向くようにしてあります。
衝突点がない場合はポインターは動かないです。おまけ。Daydreamのコントローラー情報を取得してみた。
そもそもはDaydreamSDKに依存しないで似たような処理を作りたかったのですが、
コントローラーをどうやって取得するのかなーと、inputの設定をいろいろいじって探ってみました。
結果以下の通り無事取れました!TouchpadHorizontal : 4th axis
TouchpadVertical : 5th axis
TouchpadPress : joystick button 9
TouchpadTouch : joystick button 17
(-)Button : joystick button 0Daydream向けにアプリとしてリリースするならDaydreamSDKを使うべきですが、
実験やプロトタイプで他のターゲットと切り替えたりしたい場合はSDKは邪魔なので
今回のようなSDKに依存しないコンポーネントがあると便利なのではないでしょうか。
- 投稿日:2019-03-30T19:47:54+09:00
VRMのモデルをthree.jsで表示してみた
概要
WebブラウザだけでVRM形式のキャラクターを表示してみたい!
成果物
こんなものを作ってみました。
three.js + Cannon.js でめり込ませない!
preview: https://takenokotech.github.io/ts-vrm
code: https://github.com/TakenokoTech/ts-vrmthree.js + Leap Motion で動かす!
Unity + Leap Motion + VRM → WebSocket → three.js
— たけのこ? (@TakenokoT42) 2019年3月3日
WebVRを触っていきたかったのでVrmAvaterのアニメータをjsonで投げまくってモーションデータ共有できるようにした。 pic.twitter.com/kCodKoQ8jtそもそもVRMとは?
VRアプリケーション向けの人型3Dアバター(3Dモデル)データを扱うためのファイルフォーマット
— https://dwango.github.io/vrm/ファイル自体は、JSONで書かれた3Dモデルやシーンを表現するフォーマット「glTF2.0」のデータにVRMの拡張情報を追加したデータです。
three.jsでVRMを表示
three.jsにはGLTFLoaderというgltfファイルを読み込めるローダーがあるので何も考えずに表示してみます。
モデルにはルービンちゃんをお借りしてます。sample.tsimport * as THREE from "three"; import GLTFLoader from "three-gltf-loader"; import OrbitControls from "three-orbitcontrols"; // 幅、高さ取得 const width = window.innerWidth; const height = window.innerHeight; // レンダラの作成、DOMに追加 const renderer = new THREE.WebGLRenderer(); renderer.setSize(width, height); renderer.setClearColor(0xf6f6f6, 1.0); document.body.appendChild(renderer.domElement); // シーンの作成、ライトの作成と追加 const scene = new THREE.Scene(); scene.add(new THREE.AmbientLight(0xffffff, 1)); // カメラの作成と追加、OrbitControlsの追加 const camera = new THREE.PerspectiveCamera(45, width / height, 1, 100); camera.position.set(0, 1, -3); const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 1, 0); controls.update(); // VRMのファイルをロード new GLTFLoader().load("vrm/Victoria_Rubin.vrm", (data) => { scene.add(data.scene); }); // レンダリング const render = () => { renderer.render(scene, camera); requestAnimationFrame(render); }; render();残念ながら、VRM拡張のマテリアルが当たらなくて髪が真っ黒になってしまいました
VRMが表示できるローダーを作ってみる
https://github.com/Keshigom/WebVRM/blob/master/docs/src/WebVRM.js
こちらを参考にさせていただきました。VRMLoader.tsimport * as THREE from "three"; export default class VrmLoader { load(url: string, callback: (vrm: THREE.GLTF) => void) { new GLTFLoader(THREE.DefaultLoadingManager).load(url, (vrm: THREE.GLTF) => { vrm.scene.name = "VRM"; vrm.scene.castShadow = true; vrm.scene.traverse(this.attachMaterial); callback(vrm); }); } private attachMaterial(object3D: THREE.Object3D) { const createMaterial = (material: any): THREE.MeshLambertMaterial => { let newMaterial: any = new THREE.MeshLambertMaterial(); newMaterial.name = material.name; newMaterial.color.copy(material.color); newMaterial.map = material.map; newMaterial.alphaTest = material.alphaTest; newMaterial.morphTargets = material.morphTargets; newMaterial.morphNormals = material.morphNormals; newMaterial.skinning = material.skinning; newMaterial.transparent = material.transparent; // newMaterial.wireframe = true; return newMaterial; }; let mesh = object3D as THREE.Mesh; if (!mesh || !mesh.material) return; mesh.castShadow = true; if (Array.isArray(mesh.material)) { const list: THREE.Material[] = mesh.material; list.forEach((m: THREE.Material, index: number) => (list[index] = createMaterial(m))); } else { mesh.material = createMaterial(mesh.material); } } }sample.ts- new GLTFLoader().load("vrm/Victoria_Rubin.vrm", (data) => { + new VrmLoader().load("vrm/Victoria_Rubin.vrm", (data) => {VRM拡張: /extensions/VRM/materialProperties あたりのUnity依存のマテリアル情報を無視してthree.jsのマテリアルを当てなおしました。
あと、テカりの出ないMeshBasicMaterialを使っていたりするので、MeshLambertMaterialで作り直しています。
イイ感じthree.jsで表示したVRMモデルを動かす
今回はUnityでVRMモデルの動きをアニメーションデータとして出力したものをthree.jsで読み込んで動かしてみます。
VRMでは「VRM拡張: モデルのボーンマッピング(json.extensions.VRM.humanoid)」という項目でnodeとHumanoidが紐づいたボーン構造が取得できます。
schema/humanoid.jsonはHumanBodyBonesをjsonファイルに変換したものです。
Appendixにファイルを置いておきますね。sample.tsimport humanoidBone from "schema/humanoid.json" // VRMのファイルをロード new VrmLoader().load("vrm/Victoria_Rubin.vrm", (data) => { scene.add(data.scene); + attachHumanoidBone(data) }); //=== 追加 === function attachHumanoidBone(vrm: Vrm): { [n: number]: THREE.Bone } { const humanoidMap: { [n: number]: THREE.Bone } = {}; for (const [i, humanBone] of vrm.userData.gltfExtensions.VRM.humanoid.humanBones.entries()) { for (const [j, node] of vrm.parser.json.nodes.entries()) { if (humanBone.node == j) { vrm.scene.traverse((object: any) => { if (object.name == node.name) humanoidMap[humanoidBone[humanBone.bone]] = object; }); } } } return humanoidMap; }これで、UnityのHumanoidと同じボーン構造として扱えるようになります。
three.js Unity あとは、マッスルを当てるだけ!
Unity側でアニメーションデータを作る
それでは、Unityでthree.jsで扱えるアニメーションデータを作ってみましょう。
通信にはwebsocket-sharpを使ってみます。送信元
WebSocketSender.csusing System; using System.Collections.Generic; using UnityEngine; using WebSocketSharp; using static VrmAnimationJson; public class WebSocketSender : MonoBehaviour { [SerializeField] private string characterName; [SerializeField] private Animator animator; [SerializeField] private Transform rootBone; private WebSocket webSocket; private HumanPose humanPose = new HumanPose (); private readonly VrmAnimationJson anime = new VrmAnimationJson (); void Start () { this.LoadBone (); this.StartWebsocket (); } void Update () { if (!webSocket.IsAlive) { webSocket.Connect (); return; } this.UpdateBone (); this.UpdateWebsocket (); } private void OnDestroy () { if (webSocket != null) webSocket.Close (); } private void StartWebsocket () { Debug.Log ("WebsocketAccessor Start"); webSocket = new WebSocket ("ws://localhost:5001/"); webSocket.OnOpen += (sender, e) => { Debug.Log ("Opended"); }; webSocket.OnMessage += (sender, e) => { }; webSocket.Connect (); } private void UpdateWebsocket () { Debug.Log ("WebsocketAccessor Update"); try { webSocket.Send (JsonUtility.ToJson (this.anime)); } catch (Exception e) { Debug.Log (e); } } private void LoadBone () { for (int i = 0; i <= 54; i++) { Transform bone = GetComponent<Animator> ().GetBoneTransform ((HumanBodyBones) i); this.anime.vrmAnimation.Add (new VrmAnimation ()); this.anime.vrmAnimation[i].keys.Add (new Key ()); } } private void UpdateBone () { Animator animator = GetComponent<Animator> (); HumanPoseHandler humanPoseHandler = new HumanPoseHandler (animator.avatar, this.rootBone); humanPoseHandler.GetHumanPose (ref humanPose); for (int i = 0; i <= 54; i++) { Transform bone = GetComponent<Animator> ().GetBoneTransform ((HumanBodyBones) i); if (bone == null) continue; float[] pos = new float[3] { bone.localPosition.x, bone.localPosition.y, bone.localPosition.z }; float[] rot = new float[4] { bone.localRotation.x, bone.localRotation.y, bone.localRotation.z, bone.localRotation.w }; float[] scl = new float[3] { bone.localScale.x, bone.localScale.y, bone.localScale.z }; this.anime.vrmAnimation[i].name = "" + i; this.anime.vrmAnimation[i].bone = bone.name; this.anime.vrmAnimation[i].keys[0] = new Key (pos, rot, scl, 0); } } }
- StartでWebsocketサーバーの接続とVrmAnimatorからボーン構造の取得
- Updateでボーン状態の取得とWebsocketサーバーへの送信
送信するアニメーションのモデル
VrmAnimationJson.cs[Serializable] public class VrmAnimationJson { public List<VrmAnimation> vrmAnimation = new List<VrmAnimation> (); [Serializable] public class VrmAnimation { public string name = ""; public string bone = ""; public List<Key> keys = new List<Key> (); } [Serializable] public class Key { public float[] pos; public float[] rot; public float[] scl; public long time; public Key (float[] pos, float[] rot, float[] scl, long time) { this.pos = pos; this.rot = rot; this.scl = scl; this.time = time; } public Key () { } } }作った
Web Socket Senderを適当なオブジェクトに当てて、AnimatorとTransformにVRMを割り当てます。
ボーン構造がちがうVRMでも動きが確認できるようにアリシアちゃんをお借りしてます。
WebSocket Server
unityからwebにアニメーションを送るため、nodeでWebSocketのサーバーを立てます。
server.tsimport { Server } from "ws"; const server = new Server({ port: 5001 }); server.on("connection", websocket => { let time = new Date(); websocket.on("message", message => { console.log("Received: " + (new Date().getMilliseconds() - time.getMilliseconds()) + "ms" /* + message*/); time = new Date(); server.clients.forEach(client => { client.send(message); }); }); websocket.on("close", () => { console.log("I lost a client"); }); }); console.log("start websocket server. port=5001");three.jsのボーンにあてる
WebSocket から受け取ったアニメーションデータをthree.jsで扱えるように変換してボーンを動かします。
sample.ts//=== 追加 === let boneMap: { [n: number]: THREE.Bone }; // attachHumanoidBoneで取得したボーン状態を入れておく let messageData: any; try { const socket = new WebSocket("ws://127.0.0.1:5001"); socket.onopen = (event: Event) => { console.log("websocket open"); socket.onmessage = (message: any) => { messageData = message.data; }; socket.onclose = () => { console.log("websocket close"); }; }; } catch (e) { console.warn(e); } // renderで各フレームごとに呼び出す function updateFrame() { if (!messageData) return; const animation: VrmAnimation[] = JSON.parse(messageData).vrmAnimation; for (let ani of animation) { const name = -(-ani.name); const key = ani.keys[ani.keys.length - 1]; if (!boneMap || !boneMap[name] || key.rot.length != 4) continue; boneMap[name].quaternion.set(-key.rot[0], -key.rot[1], key.rot[2], key.rot[3]); } } export interface VrmAnimation { name: string; bone: string; keys: Key[]; } export interface Key { pos: number[]; rot: number[]; scl: number[]; time: number; }unityとWebsocketサーバーを起動してみたら無事にアニメーションが当たりました
まとめ
Unityで良い気がしてきた・・・
Appendix.
VRMとgltfのスキーマを、型定義ファイル(.d.ts)に変換するpowershellスクリプト
https://github.com/TakenokoTech/ts-vrm/blob/develop/schema/pull.ps1humanoidのJSON
https://github.com/TakenokoTech/ts-vrm/blob/develop/schema/bone.json
- 投稿日:2019-03-30T19:47:54+09:00
VRMのモデルをthree.jsで動かしてみた
概要
Webブラウザ(+α)でVRM形式のキャラクターを動かしてみた!
成果物
こんなものを作ってみました。
three.js + Cannon.js でめり込ませない!
preview: https://takenokotech.github.io/ts-vrm
code: https://github.com/TakenokoTech/ts-vrmthree.js + Leap Motion で動かす!
Unity + Leap Motion + VRM → WebSocket → three.js
— たけのこ? (@TakenokoT42) 2019年3月3日
WebVRを触っていきたかったのでVrmAvaterのアニメータをjsonで投げまくってモーションデータ共有できるようにした。 pic.twitter.com/kCodKoQ8jtそもそもVRMとは?
VRアプリケーション向けの人型3Dアバター(3Dモデル)データを扱うためのファイルフォーマット
— https://dwango.github.io/vrm/ファイル自体は、JSONで書かれた3Dモデルやシーンを表現するフォーマット「glTF2.0」のデータにVRMの拡張情報を追加したデータです。
three.jsでVRMを表示
three.jsにはGLTFLoaderというgltfファイルを読み込めるローダーがあるので何も考えずに表示してみます。
モデルにはルービンちゃんをお借りしてます。sample.tsimport * as THREE from "three"; import GLTFLoader from "three-gltf-loader"; import OrbitControls from "three-orbitcontrols"; // 幅、高さ取得 const width = window.innerWidth; const height = window.innerHeight; // レンダラの作成、DOMに追加 const renderer = new THREE.WebGLRenderer(); renderer.setSize(width, height); renderer.setClearColor(0xf6f6f6, 1.0); document.body.appendChild(renderer.domElement); // シーンの作成、ライトの作成と追加 const scene = new THREE.Scene(); scene.add(new THREE.AmbientLight(0xffffff, 1)); // カメラの作成と追加、OrbitControlsの追加 const camera = new THREE.PerspectiveCamera(45, width / height, 1, 100); camera.position.set(0, 1, -3); const controls = new OrbitControls(camera, renderer.domElement); controls.target.set(0, 1, 0); controls.update(); // VRMのファイルをロード new GLTFLoader().load("vrm/Victoria_Rubin.vrm", (data) => { scene.add(data.scene); }); // レンダリング const render = () => { renderer.render(scene, camera); requestAnimationFrame(render); }; render();残念ながら、VRM拡張のマテリアルが当たらなくて髪が真っ黒になってしまいました
VRMが表示できるローダーを作ってみる
https://github.com/Keshigom/WebVRM/blob/master/docs/src/WebVRM.js
こちらを参考にさせていただきました。VRMLoader.tsimport * as THREE from "three"; export default class VrmLoader { load(url: string, callback: (vrm: THREE.GLTF) => void) { new GLTFLoader(THREE.DefaultLoadingManager).load(url, (vrm: THREE.GLTF) => { vrm.scene.name = "VRM"; vrm.scene.castShadow = true; vrm.scene.traverse(this.attachMaterial); callback(vrm); }); } private attachMaterial(object3D: THREE.Object3D) { const createMaterial = (material: any): THREE.MeshLambertMaterial => { let newMaterial: any = new THREE.MeshLambertMaterial(); newMaterial.name = material.name; newMaterial.color.copy(material.color); newMaterial.map = material.map; newMaterial.alphaTest = material.alphaTest; newMaterial.morphTargets = material.morphTargets; newMaterial.morphNormals = material.morphNormals; newMaterial.skinning = material.skinning; newMaterial.transparent = material.transparent; // newMaterial.wireframe = true; return newMaterial; }; let mesh = object3D as THREE.Mesh; if (!mesh || !mesh.material) return; mesh.castShadow = true; if (Array.isArray(mesh.material)) { const list: THREE.Material[] = mesh.material; list.forEach((m: THREE.Material, index: number) => (list[index] = createMaterial(m))); } else { mesh.material = createMaterial(mesh.material); } } }sample.ts- new GLTFLoader().load("vrm/Victoria_Rubin.vrm", (data) => { + new VrmLoader().load("vrm/Victoria_Rubin.vrm", (data) => {VRM拡張: /extensions/VRM/materialProperties あたりのUnity依存のマテリアル情報を無視してthree.jsのマテリアルを当てなおしました。
あと、テカりの出ないMeshBasicMaterialを使っていたりするので、MeshLambertMaterialで作り直しています。
イイ感じthree.jsで表示したVRMモデルを動かす
今回はUnityでVRMモデルの動きをアニメーションデータとして出力したものをthree.jsで読み込んで動かしてみます。
VRMでは「VRM拡張: モデルのボーンマッピング(json.extensions.VRM.humanoid)」という項目でnodeとHumanoidが紐づいたボーン構造が取得できます。
schema/humanoid.jsonはHumanBodyBonesをjsonファイルに変換したものです。
Appendixにファイルを置いておきますね。sample.tsimport humanoidBone from "schema/humanoid.json" // VRMのファイルをロード new VrmLoader().load("vrm/Victoria_Rubin.vrm", (data) => { scene.add(data.scene); + attachHumanoidBone(data) }); //=== 追加 === function attachHumanoidBone(vrm: Vrm): { [n: number]: THREE.Bone } { const humanoidMap: { [n: number]: THREE.Bone } = {}; for (const [i, humanBone] of vrm.userData.gltfExtensions.VRM.humanoid.humanBones.entries()) { for (const [j, node] of vrm.parser.json.nodes.entries()) { if (humanBone.node == j) { vrm.scene.traverse((object: any) => { if (object.name == node.name) humanoidMap[humanoidBone[humanBone.bone]] = object; }); } } } return humanoidMap; }これで、UnityのHumanoidと同じボーン構造として扱えるようになります。
three.js Unity あとは、マッスルを当てるだけ!
Unity側でアニメーションデータを作る
それでは、Unityでthree.jsで扱えるアニメーションデータを作ってみましょう。
通信にはwebsocket-sharpを使ってみます。送信元
WebSocketSender.csusing System; using System.Collections.Generic; using UnityEngine; using WebSocketSharp; using static VrmAnimationJson; public class WebSocketSender : MonoBehaviour { [SerializeField] private string characterName; [SerializeField] private Animator animator; [SerializeField] private Transform rootBone; private WebSocket webSocket; private HumanPose humanPose = new HumanPose (); private readonly VrmAnimationJson anime = new VrmAnimationJson (); void Start () { this.LoadBone (); this.StartWebsocket (); } void Update () { if (!webSocket.IsAlive) { webSocket.Connect (); return; } this.UpdateBone (); this.UpdateWebsocket (); } private void OnDestroy () { if (webSocket != null) webSocket.Close (); } private void StartWebsocket () { Debug.Log ("WebsocketAccessor Start"); webSocket = new WebSocket ("ws://localhost:5001/"); webSocket.OnOpen += (sender, e) => { Debug.Log ("Opended"); }; webSocket.OnMessage += (sender, e) => { }; webSocket.Connect (); } private void UpdateWebsocket () { Debug.Log ("WebsocketAccessor Update"); try { webSocket.Send (JsonUtility.ToJson (this.anime)); } catch (Exception e) { Debug.Log (e); } } private void LoadBone () { for (int i = 0; i <= 54; i++) { Transform bone = GetComponent<Animator> ().GetBoneTransform ((HumanBodyBones) i); this.anime.vrmAnimation.Add (new VrmAnimation ()); this.anime.vrmAnimation[i].keys.Add (new Key ()); } } private void UpdateBone () { Animator animator = GetComponent<Animator> (); HumanPoseHandler humanPoseHandler = new HumanPoseHandler (animator.avatar, this.rootBone); humanPoseHandler.GetHumanPose (ref humanPose); for (int i = 0; i <= 54; i++) { Transform bone = GetComponent<Animator> ().GetBoneTransform ((HumanBodyBones) i); if (bone == null) continue; float[] pos = new float[3] { bone.localPosition.x, bone.localPosition.y, bone.localPosition.z }; float[] rot = new float[4] { bone.localRotation.x, bone.localRotation.y, bone.localRotation.z, bone.localRotation.w }; float[] scl = new float[3] { bone.localScale.x, bone.localScale.y, bone.localScale.z }; this.anime.vrmAnimation[i].name = "" + i; this.anime.vrmAnimation[i].bone = bone.name; this.anime.vrmAnimation[i].keys[0] = new Key (pos, rot, scl, 0); } } }
- StartでWebsocketサーバーの接続とVrmAnimatorからボーン構造の取得
- Updateでボーン状態の取得とWebsocketサーバーへの送信
送信するアニメーションのモデル
VrmAnimationJson.cs[Serializable] public class VrmAnimationJson { public List<VrmAnimation> vrmAnimation = new List<VrmAnimation> (); [Serializable] public class VrmAnimation { public string name = ""; public string bone = ""; public List<Key> keys = new List<Key> (); } [Serializable] public class Key { public float[] pos; public float[] rot; public float[] scl; public long time; public Key (float[] pos, float[] rot, float[] scl, long time) { this.pos = pos; this.rot = rot; this.scl = scl; this.time = time; } public Key () { } } }作った
Web Socket Senderを適当なオブジェクトに当てて、AnimatorとTransformにVRMを割り当てます。
ボーン構造がちがうVRMでも動きが確認できるようにアリシアちゃんをお借りしてます。
WebSocket Server
unityからwebにアニメーションを送るため、nodeでWebSocketのサーバーを立てます。
server.tsimport { Server } from "ws"; const server = new Server({ port: 5001 }); server.on("connection", websocket => { let time = new Date(); websocket.on("message", message => { console.log("Received: " + (new Date().getMilliseconds() - time.getMilliseconds()) + "ms" /* + message*/); time = new Date(); server.clients.forEach(client => { client.send(message); }); }); websocket.on("close", () => { console.log("I lost a client"); }); }); console.log("start websocket server. port=5001");three.jsのボーンにあてる
WebSocket から受け取ったアニメーションデータをthree.jsで扱えるように変換してボーンを動かします。
sample.ts//=== 追加 === let boneMap: { [n: number]: THREE.Bone }; // attachHumanoidBoneで取得したボーン状態を入れておく let messageData: any; try { const socket = new WebSocket("ws://127.0.0.1:5001"); socket.onopen = (event: Event) => { console.log("websocket open"); socket.onmessage = (message: any) => { messageData = message.data; }; socket.onclose = () => { console.log("websocket close"); }; }; } catch (e) { console.warn(e); } // renderで各フレームごとに呼び出す function updateFrame() { if (!messageData) return; const animation: VrmAnimation[] = JSON.parse(messageData).vrmAnimation; for (let ani of animation) { const name = -(-ani.name); const key = ani.keys[ani.keys.length - 1]; if (!boneMap || !boneMap[name] || key.rot.length != 4) continue; boneMap[name].quaternion.set(-key.rot[0], -key.rot[1], key.rot[2], key.rot[3]); } } export interface VrmAnimation { name: string; bone: string; keys: Key[]; } export interface Key { pos: number[]; rot: number[]; scl: number[]; time: number; }unityとWebsocketサーバーを起動してみたら無事にアニメーションが当たりました
まとめ
今回はWebSocketで通信してモーションデータを送りました。
ちなみに
VrmAnimation.Keyのtimeを使ってフレームごとに記録するようにすると、
静的なjsonファイルを読み込むだけでキャラクターを動かすことができます。AnimationClipを使う必要がないので楽チンですね!
Appendix.
VRMとgltfのスキーマを、型定義ファイル(.d.ts)に変換するpowershellスクリプト
https://github.com/TakenokoTech/ts-vrm/blob/develop/schema/pull.ps1humanoidのJSON
https://github.com/TakenokoTech/ts-vrm/blob/develop/schema/bone.json
- 投稿日:2019-03-30T19:16:15+09:00
Kerasで作ったモデルをUnityに持っていくときのハマりどころ
はじめに
Unityでは、ゲーム内で強化学習させるならml-agentsとかKelpNetなどを使えますが、一方でゲーム中に得たデータを保存し、別の環境で機械学習させた後に学習結果をUnityにもっていく、という方法もあります。
そういう方法をDeep Neural Networkで行う場合はKerasが便利です。ネットワークを設計するのも簡単ですし、Google Colaboratoryにはデフォルトで入ってるので環境構築に悩むことなくすぐに作業できます。
ただ、Kerasで作ったモデルをUnity、特にOculus GoなどのAndroidデバイスに持っていくときにハマりどころがいくつかあります。しかも、ネットで見られる情報が不完全で試しても上手くいかないことが多いです。この記事では、そのハマりどころの紹介と回避方法を紹介します。ただし、使っている関数のいくつかがdeprecatedなので、そのうち修整が必要になるでしょう。問題が発生しましたら記事へのコメントでお知らせください。なお、記事で使用しているUnityのバージョンは2018.3.8f1です。
Kerasモデルの準備
こちらのチュートリアル の内容を実装します。開発環境はGoogle Colaboratoryです。
まず、こちらのデータをダウンロードして、pima-indians-diabetes.data.csvというファイル名でGoogle Colaboratoryのドライブにアップロードしてください。
次に、Keras Functional APIでモデルと学習プロセスを定義します。ここでinputは"input_x",outputは"output_y"と定義していることに注意してください。
from keras.models import Sequential from keras.layers import Dense import numpy numpy.random.seed(7) dataset = numpy.loadtxt("pima-indians-diabetes.data.csv", delimiter=",") X = dataset[:,0:8] Y = dataset[:,8] from keras.layers import Input from keras.models import Model inputs = Input(shape=(8,),name='input_x') x = Dense(12, activation='relu')(inputs) x = Dense(8, activation='relu')(x) predictions = Dense(1, activation='sigmoid',name='output_y')(x) model = Model(input=inputs, output=predictions) model.compile(loss='binary_crossentropy', optimizer='adam', metrics=['accuracy']) model.fit(X, Y, epochs=150, batch_size=10) scores = model.evaluate(X, Y) print("\n%s: %.2f%%" % (model.metrics_names[1], scores[1]*100))学習が終了したら、
print(model.input) print(model.output)でinputとoutputの情報を見てください。
Tensor("input_x:0", shape=(?, 8), dtype=float32) Tensor("output_y/Sigmoid:0", shape=(?, 1), dtype=float32)上記のように、inputのほうは"input_x"と元の名前の通りですが、outputの方は"/Sigmoid"が追加されてます。この追加されてる方の名前をUnityで使うので注意してください。
KerasのモデルをTensorFlowグラフとして出力する
Kerasのモデルは直接Unityで読めないので、一旦TensorFlowの形式に変換します。ネットでググると変換の方法がいくつも出てきますが、ほとんどの方法はUnityに持っていったときに上手くいかなかったです。ここでは、こちらの記事の方法を紹介します。以下のコードはほぼ元記事の通りですが、そのままだとエラーが出るので一部修正しています。
import tensorflow as tf from tensorflow.python.framework import graph_util from tensorflow.python.framework import graph_io from keras import backend as K ksess = K.get_session() K.set_learning_phase(0) graph = ksess.graph num_output = 1 prefix = "output" pred = [None]*num_output outputName = [None]*num_output for i in range(num_output): outputName[i] = prefix + str(i) pred[i] = tf.identity(model.get_output_at(i), name=outputName[i]) constant_graph = graph_util.convert_variables_to_constants(ksess, ksess.graph.as_graph_def(), outputName)コードの内容を見るとわかりますが、Kerasのセッションをget_session()でTensorFlowのセッションとして呼び出し、学習したモデル内の変数をgraph_util.convert_variables_to_constantsで定数に変換しています。
output_dir = "./" output_graph_name = "keras2tf.pb" graph_io.write_graph(constant_graph, output_dir, output_graph_name, as_text=False)これで、keras2tf.pbというファイルにTensorflowグラフが保存されました。このファイルをローカルPCにダウンロードして、拡張子を.bytesに変更してください。
Unity内での作業
ここからはUnity内での作業になります。以下の作業はOculus Go向けのものですが、他のAndroidデバイスでも同様の流れで大丈夫かと思います。
Unityで新しいプロジェクトを作ったら、Androidをビルドターゲットにし、Player SettingでAPI Lebelを25に、Scripting Define SymbolをENABLE_TENSORFLOWにします。
つぎに、TensorFlowSharp Unityプラグインをインポートしてください。こちらからダウンロードできます。
ImportするとPlugins/Androidというフォルダが作られますが、その中の"System."と最初に名前のつくファイルは全て削除してください。そうしないとビルド時にエラーが出ます。どうやら、Unity2018.3で出るエラーのようです。
あと、上で作ったkeras2tf.bytesをAssetsフォルダの下にドラッグアンドドロップしてください。
次にModelImportExample.csというファイルを作り、以下のスクリプトを入力してください。スクリプト作成にはこちらの記事を参考にしました。
ModelImportExample.csusing UnityEngine; using TensorFlow; public class ModelImportExample : MonoBehaviour { public TextAsset model; private float[,] inputTensor = new float[1, 8]; private float[] testData = new float[] { 6f, 148f, 72f, 35f, 0f, 33.6f, 0.627f, 50f }; void Start() { #if UNITY_ANDROID && !UNITY_EDITOR TensorFlowSharp.Android.NativeBinding.Init(); #endif TFGraph graph = new TFGraph(); graph.Import(model.bytes); TFSession sess = new TFSession(graph); for (int i = 0; i < 8; i++) { inputTensor[0, i] = testData[i]; } TFTensor input = inputTensor; var runner = sess.GetRunner(); var test = runner.AddInput(graph["input_x"][0], input); test.Fetch(graph["output_y/Sigmoid"][0]); var output = runner.Run(); var result = output[0].GetValue() as float[,]; Debug.Log(result[0,0]); } }重要なポイントは、まず、Android向けにビルドするときは
#if UNITY_ANDROID && !UNITY_EDITOR TensorFlowSharp.Android.NativeBinding.Init(); #endifを追加してください。無いとビルドできません。
つぎに、インポートしたkeras2tf.bytesはTextAssetとしてエディタ上でmodelに割り当ててください。
あと、グラフへの入力と出力は
var test = runner.AddInput(graph["input_x"][0], input); test.Fetch(graph["output_y/Sigmoid"][0]);で定義しています。それぞれの名前は、Keras側で確認した通り"input_x", "output_y/Sigmoid"になっていることに注意してください。
これでエディタ上でスクリプトを実行すると、コンソールに"0.9049003"とアウトプットが出てくるはずです。確認したら、ビルドしてください。正常に終了するはずです。Oculus Goでも動作確認済みです。
- 投稿日:2019-03-30T17:08:42+09:00
OpenXRとはなんぞや。
OpenXRとは
OpenXRは、バーチャルリアリティおよび拡張現実プラットフォームおよびデバイスへのアクセスに関するオープンでロイヤリティフリーの規格です。クロノス・グループ コンソーシアムによって管理されているワーキンググループによって開発されています。
リリース
OpenXRは2017年2月27日のGame Developers Conference 2017にてクロノス・グループによって発表されました。
規格の暫定版は2019年3月18日にリリースされ、開発者と実装者がそれに関するフィードバックを送ることができます。OpenXRの構想
2つの層があります。
Application Interface層
今回リリースされたVersion 0.9でこのAPI層が提供されました。
Device Plugin Interface層
各デバイスへの対応は、Version 1.0以降で、2019年後半ごろにリリースされる予定のようです。
何が変わるの?!
Unityを例にとって説明します。
現状、UnityではBuildをする際に、下の図のようにプラットフォームを選択するようになっています。そして、PlatformごとにBuildをする必要があります。OpenXRが実装されると、PlatformごとにBuildをする必要がなくなり、Buildしたものはどのプラットフォームでも実行することができるようになります。(たぶん!)
OpenXRがなんのために議論されているか、少しイメージがついたでしょうか。
公式サイトで用意されているもの
1.Web XR 概要のスライド(英語版)
https://www.khronos.org/assets/uploads/apis/OpenXR-Update-GDC_v20190316c_Mar19.pdf2.Manual
https://www.khronos.org/registry/OpenXR/specs/0.90/html/xrspec.html#introduction3.サンプルコード
https://github.com/KhronosGroup/OpenXR-SDKおまけ(Tシャツ売ってるみたいです。)













