A-Frame と Unity の 位置や Quaternion 回転を Node-RED HTTP → WebSocket 経由で連動させたメモ

A-Frame と Unity の 位置や Quaternion 回転を Node-RED HTTP → WebSocket 経由で連動させたメモです。この記事は 2021年 ゆるくすすめる ( ワンフットシーバス ) GWアドベントカレンダー の8日目の記事でもあります。

Quaternion が別アプリで連携できるかどうかがテーマ

この記事の Unity の持つ Quaternion が、A-Frame の Quaternion にうまく連携できるかを試しまして、結果としてはうまくいきました。各アプリの位置や回転軸の違いなど細かな検証は、今後頑張ります。

位置情報は法則さえわかればなんとか変換できるとして、問題は回転をつかさどる Quaternion でしたが、

をヒントに A-Frame の中で Three.js の Quaternion まわりを呼び出してオブジェクトに与えることは こちらの Three.jsドキュメント で把握できました。

Unity から HTTP で Node-RED にデータを送る

まず、Unity側のつくりです。HTTP で Node-RED にデータを送ります。/postというURLに送っていて、今回起動した Node-RED は 18801 ポートで起動しています。

image

このように、Cube をアニメーションを動かして、以下の SendMovement で定期的にデータを送るようにしています。特に加工せず、位置情報と回転の Quaternion をそのまま送っています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Text;
using UnityEngine.Networking;

public class SendMovement : MonoBehaviour
{
    // Start is called before the first frame update

    GameObject cube;

    void Start()
    {
        cube = GameObject.Find("Cube");

        InvokeRepeating("SendData", 1.0f, 0.1f);
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    [Serializable]
    private sealed class Data
    {
        public float PositionX = 0;
        public float PositionY = 0;
        public float PositionZ = 0;
        public float RotationX = 0;
        public float RotationY = 0;
        public float RotationZ = 0;
        public float RotationW = 0;
    }

    private void SendData()
    {
        Debug.LogFormat("Position x:{0} y:{1} z:{2}", cube.transform.localPosition.x, cube.transform.localPosition.y, cube.transform.localPosition.z);
        Debug.LogFormat("Quaternion w:{0} x:{1} y:{2} z:{3}", cube.transform.localRotation.w, cube.transform.localRotation.x, cube.transform.localRotation.y, cube.transform.localRotation.z);

        var url = "http://localhost:18801/post";
        
        var data = new Data();
        data.PositionX = cube.transform.localPosition.x;
        data.PositionY = cube.transform.localPosition.y;
        data.PositionZ = cube.transform.localPosition.z;
        data.RotationX = cube.transform.localRotation.x;
        data.RotationY = cube.transform.localRotation.y;
        data.RotationZ = cube.transform.localRotation.z;
        data.RotationW = cube.transform.localRotation.w;

        var json = JsonUtility.ToJson(data);
        var postData = Encoding.UTF8.GetBytes(json);

        var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST)
        {
            uploadHandler = new UploadHandlerRaw(postData),
            downloadHandler = new DownloadHandlerBuffer()
        };

        request.SetRequestHeader("Content-Type", "application/json");

        var operation = request.SendWebRequest();

        operation.completed += _ =>
        {
            Debug.Log(operation.isDone);
            Debug.Log(operation.webRequest.downloadHandler.text);
            Debug.Log(operation.webRequest.isHttpError);
            Debug.Log(operation.webRequest.isNetworkError);
        };
    }
}

Node-RED のフロー

Node-RED のフローはこちらです。

image

ごくごくシンプルに /post で Unity から HTTP でデータを受け付けて、このあとお伝えする A-Frame の表示が WebSocket でデータを待ち受けているので、そのまま何も加工せずに伝えます。 A-Frame 表示 HTML とコメントが書いてあるフローが A-Frame の表示部分です。

Node-RED のフローの JSON データを以下に置いておきます。

[{"id":"6ac16914.265178","type":"debug","z":"2225f688.ecf4ca","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":510,"y":500,"wires":[]},{"id":"486807c7.c250b8","type":"http in","z":"2225f688.ecf4ca","name":"","url":"/post","method":"post","upload":false,"swaggerDoc":"","x":280,"y":460,"wires":[["5bdb0ef3.8907b","6ac16914.265178","855f8c94.5292d"]]},{"id":"5bdb0ef3.8907b","type":"http response","z":"2225f688.ecf4ca","name":"","statusCode":"","headers":{},"x":490,"y":460,"wires":[]},{"id":"1406e62a.c619ca","type":"http in","z":"2225f688.ecf4ca","name":"","url":"/aframe","method":"get","upload":false,"swaggerDoc":"","x":290,"y":340,"wires":[["dcc4f309.d1358"]]},{"id":"d033e8a9.cdb408","type":"http response","z":"2225f688.ecf4ca","name":"","statusCode":"","headers":{},"x":700,"y":340,"wires":[]},{"id":"dcc4f309.d1358","type":"template","z":"2225f688.ecf4ca","name":"HTML","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n  <head>\n    <script src=\"http://aframe.io/releases/1.2.0/aframe.min.js\"></script>\n  </head>\n  <body>\n    <a-scene>\n      <a-box id=\"myBox\" position=\"-1 0.5 -3\" rotation=\"0 45 0\" color=\"#4CC3D9\"></a-box>\n      <a-sphere position=\"0 1.25 -5\" radius=\"1.25\" color=\"#EF2D5E\"></a-sphere>\n      <a-cylinder position=\"1 0.75 -3\" radius=\"0.5\" height=\"1.5\" color=\"#FFC65D\"></a-cylinder>\n      <a-plane position=\"0 0 -4\" rotation=\"-90 0 0\" width=\"4\" height=\"4\" color=\"#7BC8A4\"></a-plane>\n      <a-sky color=\"#ECECEC\"></a-sky>\n    </a-scene>\n    <script>\n        function ws_connect(){\n          ws = new WebSocket(\"ws://localhost:18801/ws\");\n         \n          ws.addEventListener('open', function() {\n            console.log('-- ws open');\n          });\n          \n          // データを受信したら Cubu に Quaternion で反映\n          ws.addEventListener('message', function(event) {\n            // console.log('-- ws message');\n            // console.log(event.data);\n            let sceneEl = document.querySelector('a-scene');\n            let myBox = sceneEl.querySelector('#myBox');\n            let unityCubeData = JSON.parse(event.data);\n            // console.log(unityCubeData);\n            // https://threejs.org/docs/#api/en/math/Quaternion\n            // https://crieit.net/posts/A-Frame-5d306e3d1b914\n            let unityCubeRotationQuaternion = new THREE.Quaternion(unityCubeData.RotationX,unityCubeData.RotationY,unityCubeData.RotationZ,unityCubeData.RotationW);\n            myBox.object3D.applyQuaternion(unityCubeRotationQuaternion);\n            myBox.object3D.position.x = unityCubeData.PositionX;\n            myBox.object3D.position.y = unityCubeData.PositionY;\n            myBox.object3D.position.z = unityCubeData.PositionZ;\n          });\n           \n          ws.addEventListener('close', function(event) {\n            console.log('-- ws close');\n            // https://developer.mozilla.org/ja/docs/Web/API/CloseEvent\n            // console.log('ws close.event',event);\n            // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/readyState\n            // console.log('ws.readyState',ws.readyState);\n            // 切断時に再接続する\n            ws_connect();\n          });\n        }\n        \n        ws_connect();\n    </script>\n  </body>\n</html>","output":"str","x":490,"y":340,"wires":[["d033e8a9.cdb408"]]},{"id":"ae7e1426.528338","type":"comment","z":"2225f688.ecf4ca","name":"A-Frame 表示 HTML","info":"http://localhost:18801/aframe","x":310,"y":300,"wires":[]},{"id":"855f8c94.5292d","type":"websocket out","z":"2225f688.ecf4ca","name":"","server":"63c75858.5e35a8","client":"","x":500,"y":540,"wires":[]},{"id":"1fc6873d.8494f9","type":"comment","z":"2225f688.ecf4ca","name":"→ Unity からのデータ受け付け","info":"","x":350,"y":420,"wires":[]},{"id":"e31a3a01.89de08","type":"comment","z":"2225f688.ecf4ca","name":"→ A-Frame 表示に Unity のデータを通知","info":"","x":740,"y":540,"wires":[]},{"id":"63c75858.5e35a8","type":"websocket-listener","z":"","path":"/ws","wholemsg":"false"}]

A-Frame の表示

A-Frame の表示 HTML は以下の通りです。 new THREE.Quaternion で Quaternion の処理をはじめて、Unity から受け取った Quaternion の成分 x y z w を myBox.object3D.applyQuaternion でオブジェクトにそのまま渡しています。

<html>
  <head>
    <script src="http://aframe.io/releases/1.2.0/aframe.min.js"></script>
  </head>
  <body>
    <a-scene>
      <a-box id="myBox" position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
      <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
      <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
      <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
      <a-sky color="#ECECEC"></a-sky>
    </a-scene>
    <script>
        function ws_connect(){
          ws = new WebSocket("ws://localhost:18801/ws");
         
          ws.addEventListener('open', function() {
            console.log('-- ws open');
          });
          
          // データを受信したら Cubu に Quaternion で反映
          ws.addEventListener('message', function(event) {
            // console.log('-- ws message');
            // console.log(event.data);
            let sceneEl = document.querySelector('a-scene');
            let myBox = sceneEl.querySelector('#myBox');
            let unityCubeData = JSON.parse(event.data);
            // console.log(unityCubeData);
            // https://threejs.org/docs/#api/en/math/Quaternion
            // https://crieit.net/posts/A-Frame-5d306e3d1b914
            let unityCubeRotationQuaternion = new THREE.Quaternion(unityCubeData.RotationX,unityCubeData.RotationY,unityCubeData.RotationZ,unityCubeData.RotationW);
            myBox.object3D.applyQuaternion(unityCubeRotationQuaternion);
            myBox.object3D.position.x = unityCubeData.PositionX;
            myBox.object3D.position.y = unityCubeData.PositionY;
            myBox.object3D.position.z = unityCubeData.PositionZ;
          });
           
          ws.addEventListener('close', function(event) {
            console.log('-- ws close');
            // https://developer.mozilla.org/ja/docs/Web/API/CloseEvent
            // console.log('ws close.event',event);
            // https://developer.mozilla.org/ja/docs/Web/API/WebSocket/readyState
            // console.log('ws.readyState',ws.readyState);
            // 切断時に再接続する
            ws_connect();
          });
        }
        
        ws_connect();
    </script>
  </body>
</html>

実際に表示してみるとこのように動きました。

image

A-Frame 側でカメラの奥行きが Unity と違って Cube の大きさが分かりにくいところはありますが、位置はちゃんと反映差されて回転もちゃんと伝わっているように見えます!ひとまず、うまくいきましたね!

このナレッジの弱点は HTTP → WebSocket で遅延していること

お気づきかと思うのですが、ちょっとカクカクしています。

というのも、現状は Unity → HTTP → Node-RED → WebSocket → A-Frame でやっているため HTTP 部分が、いくら送る間隔を 1 秒より縮まらないため、いくら A-Frame が WebSocket で受け取ってもこれ以上はスムーズにならないと思います。

Unity で WebSocket 実装をするには sta/websocket-sharp を使って、実装に一段手間がかかるので今回は HTTP でサッと送ってみた次第です。

このあたりは、Unity → WebSocket → Node-RED → WebSocket → A-Frame で 全部 WebSocket で、どれくらいスムーズに動くようになるか今度試してみます。これはこれで、試す余地がまだまだあり、たのしみです!