VRChatからVRC_Panoramaを使いESP-WROOM-02経由で外部機器を操作する

# はじめに
この記事は[VRChat Advent Calendar 2018](https://qiita.com/advent-calendar/2018/vrchat)の20日目の記事です
昨日の記事は[ヨツミフレーム(@y23586)](https://twitter.com/y23586)さんによる、[仮想世界のつながり：VRChatにおけるソーシャルネットワークの可視化](https://y23586.net/2018/12/18/630/)です。

こんにちは。あきら([@sh_akira](https://twitter.com/sh_akira), VRC:sh_akira)です。
普段は[バーチャルモーションキャプチャー](https://sh-akira.github.io/VirtualMotionCapture/)というVRMモデルを操作するアプリを作っています。

今回はVRChatのトリガーをVRC_Panorama経由でローカルHTTPサーバーで受ける方法と、その使用例としてさらに外部のESP-WROOM-02にTCP接続して、
VRChat内でトリガーを引くと、外のLEDが付く仕組みを作ってみます。

記事中のソースコードはパブリックドメインです。プラグインやGitHubのコード等については各ライセンスに従ってください。

# 完成品
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">VRChatアドカレ20日目、完成品(埋め込み用) <a href="https://t.co/nzRBOXSxCW">pic.twitter.com/nzRBOXSxCW</a></p>&mdash; 🗾あきら☎︎＠VMC0.21 (@sh_akira) <a href="https://twitter.com/sh_akira/status/1075507384242528256?ref_src=twsrc%5Etfw">2018年12月19日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# VRChatのワールドを作る

## 最低限必要な設定だけしたワールドを作る
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/2fb13ae8-1fac-20fe-c728-81d3888bd8ba.png)
まずはUnity 2017.4.15f1で新規プロジェクトを作ります。Untitledシーンを適当な名前(PanoramaRepeater)で保存します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/6a62a03a-28f6-8f41-eb1e-40379238196f.png)
まずはメニューのAssets->Import->Custom PackageからVRCSDKをインポートします。
執筆時のパッケージはVRCSDK-2018.12.04.10.25_Public.unitypackageです

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/b512c8b0-d5df-50bc-4b17-8e08774735de.png)
床が無いのでとりあえずPlaneを置きます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/62709802-c0bc-8f70-3223-b69c773d4c17.png)
空のゲームオブジェクトを置いて、VRCPanoramaと名前を付けておきます。
ここにInspectorのAdd Componentからスクリプトを追加してください。

- VRC_Panorama (追加すると勝手にVRC_DataStorageも入る)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/bd31c22c-3432-2f38-9ddd-ec206c292961.png)
VRC_PanoramaのPanoramasのSizeを2にしてそれぞれUrlに

- http://localhost:61221/?TRIGGER
- http://localhost:61221/?TRIGGER

と同じものを二つ入れておきます。
VRC_DataStorageのDataを開きSizeを1にして、displayと入れて右側のNoneをIntにします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/c1596128-0468-bdaa-19cd-ac328e70b1b9.png)
シーンにUIのButtonを追加します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/ac731ea7-bcd6-1a4b-6476-e947fb2e4a85.png)
表示はいらないのでButtonのRect TransformのPos X, PosY, Width, Heightをそれぞれ0にしてしまいます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/3159bf0d-ed24-3f61-b8d5-ade3df01a603.png)
Button追加時に自動で追加されたCanvasのRender ModeをWorld Spaceにして、こちらもPos X, PosY, Width, Heightをそれぞれ0にしてしまいます。

**そのままボタンとして置きたい場合は、CanvasにVRC_Ui Shapeをアタッチして、ボタンを自由にレイアウトしてください**

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/d7e03e2e-8a5b-9dd2-9c13-847a315e4f36.png)
次にButtonをVRC_Triggerから押すためのAnimationを作ります。
AnimatorControllerとAnimationを新規作成します、それぞれButtonPressAnimatorControllerとButtonPressAnimationと名付けました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/bba98d91-1007-79a3-46c0-b10773f3c17d.png)
WindowメニューからAnimationを選んでAnimationウィンドウを開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/645bcf4a-ddeb-6e12-6689-83e18db2a7a9.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/0af74338-03d5-04a9-e1e4-d7e4e80723eb.png)
Add Eventボタンを押して、InspectorのAnimation EventのFunctionにPressと入れます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/fa214429-abd7-3f87-d03a-f1bb1c7d25c2.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/49eca1da-dc14-0605-85e0-b4ac6e4d3a6f.png)
フレーム番号に1を入れて隣のフレームに移動して、またAddEventを押し、今度は空のEventを作成します。何も入力する必要はありません。
これによってアニメーションがすぐに終了し、連打することが可能になります。
0フレーム目のPressだけにすると、アニメーションに1秒かかり、1秒ごとにしか押せなくなります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/06f91eee-3c5d-c6f2-5197-8ea73c15d252.png)
次にメニューのWindowからAnimatorを選び、ProjectからButtonPressAnimatorControllerを選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/3cb38ca7-00a7-20c0-6d20-bfcf9aa7f91d.png)
適当なところを右クリックして、Create StateからEmptyを選びます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/6efc18ca-808d-a6b0-409a-3feac3724698.png)
ButtonPressAnimationをドラッグしてAnimatorに入れます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/185e3052-4be8-0eec-defc-b1f0124c9a52.png)
Parameters内の＋ボタンからTriggerを選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/726a9604-6a49-8164-abdc-a09ee4b12e92.png)
名前を`OneShot`とします
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/1648aae3-cece-7bca-ddee-6e9e6dcc5704.png)
New Stateを右クリックしてMake Transitionします。出てきた矢印はButtonPressAnimationをクリックしてくっつけてください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/ba2a612b-dbf2-5e8b-dfac-572eb48d93bf.png)
繋いだ矢印を選択して、InspectorのConditionの＋を押してOneShotを選択し、
Has Exit Timeのチェックを外して、Settings内のTransition DurationとOffsetを0にします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/31c29983-5544-c7af-5ae9-64cc00967422.png)
同じように今度は逆方向にButtonPressAnimationからNew Stateに矢印を繋いで、
Settingsの値を全て0にします。こちらはConditionsは空のままで良いです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/d095f4ee-367a-2ee5-6e5a-8215f798523c.png)
ButtonPressAnimatorControllerをButtonにドラッグして、ButtonにAnimatorを付けます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/0aed7277-61ad-8d65-5a12-8f9ab0c279c1.png)
VRC_TriggerをつけてボタンにするためのCubeを置きます。とりあえず位置はPos (0, 0.6, 0), Scale (0.3, 0.3, 0.3)にしました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/86dcbb3e-69ac-9e72-801c-913db86bae68.png)
InspectorのAdd Componentからスクリプトを追加してください。

- VRC_Trigger (追加すると勝手にVRC_Event Handlerも入る)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/a8966e06-d313-9db1-78a1-54985b5e2254.png)
VRC_TriggerのAdvancedModeにチェックを入れ、`OnInteract`を選択してAddを押します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/58f29904-6e37-10a6-2876-e4f021a16f27.png)
AlwaysBufferOneをAlwaysにします。
分かりやすいようにInteraction TextはGPIO SWにします。
Actionsの＋を押して、AnimationTriggerを選択します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/5c7b0bbf-466e-5e7a-99e5-3ddcfe69c8b2.png)
ButtonをドラッグしてAnimatorが登録されるようにします。
Triggerに先ほどAnimatorで設定したのと同じ名前のOneShotを入れます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/9e9f0f97-2b47-fa0b-ea6a-45db02fb860b.png)
Buttonを選択し、On Click()の＋ボタンを押して、VRCPanoramaをドラッグで登録したら、VRC_Panorama->NextPanoを選択します。

これでVRC_TriggerからVRC_PanoramaのNextPano関数を呼び出す仕組みが出来ました。
VRC_Panoramaの関数、ShowPanoAtやNextPano、PrevPanoはRPCからは呼べず、uGUIからなら呼ぶことが出来ます。
今回はuGUIのButtonのClickイベントでVRCPanoramaのNextPanoを呼び出しています。
そしてこれをTriggerで制御するために、ボタンをPressするだけのアニメーションを作成し、
Trigger時にはこのAnimationを呼んでボタンを押したことにすることで、Trigger->uGUI Button->VRC_Panoramaを呼べるようにしました。
VRC_PanoramaのShowPanoAtは同じIndexでは一度しか呼べない為、わざと同じURLを2個登録して、NextPanoで行ったり来たりしています。
ワールドに入った際に1度一つ目のURLが呼び出されてしまいますが、中継サーバー側で無視します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/b661e26a-1c85-c3e3-63dc-22e7378222a9.png)
Assets/VRCSDK/Prefabs/WorldのVRCWorldを適当な場所に設置します。設置位置がリスポーン地点です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/13c9b529-1271-afba-928a-46c32fbac21e.png)
メニューのVRChat SDKからShow Build Control Panelを押します。
まずはSetup Layers for VRChatを押します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/f7c1d3dc-e6b0-25cb-683a-433fa1844830.png)
Do it!
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/b6f2c7e7-ac53-ee76-429b-dd0b5402f000.png)
続いてSetup Collision Layer Matrix for VRChatを押します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/f3278dcc-cb62-e7e8-ef55-d14c30ad1ff6.png)
Do it !!!!
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/6ee139f7-1d0d-428d-b211-fd46f8b82af7.png)
ついでにEnable 3D spatialization on all 3D AudioSources in scene nowも押します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/2c999e2e-942c-83d2-ffb0-365f4776f7f4.png)
Do it!!!!
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/17df59c4-cd3d-a7a7-6fe6-531d7514fec0.png)
Canvasを選択し、LayerをDefaultに変えます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/1680f4c1-720c-ead1-9687-9a254ca11528.png)
Yes, change children

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/ccf99ef2-55a9-9cc2-2079-cc11930f427f.png)
以上でワールド作成完了です。
TestのNew Buildを押して、VRChat起動させてきちんとワールド読み込まれるか確認してください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/ec8742b5-dae9-5461-fc5a-9dfa4184aa84.png)

# 中継HTTPサーバーを作成する
Visual Studio 2017と.NET Framework 4.7.1が必要です。

まずは中継サーバーのプログラムをダウンロードします。
[VRC_Panorama_TCP_Repeater](https://github.com/sh-akira/VRC_Panorama_TCP_Repeater)を開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/b7ebe38a-4e37-32d2-7811-2a68b78a92ed.png)
緑色のClone or downloadからDownload ZIPを押してダウンロードされるzipを解凍してください。
解凍したら中に入っているVRC_Panorama_TCP_Repeater.slnを開きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/e72dd0cb-7d1e-3ea3-c971-1996a1d23abd.png)
そのまま▶開始ボタンを押せば実行できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/b0be536f-d621-eff7-7b00-ee290a8cd082.png)
画面が表示されるので開始ボタンを押してください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/6ab14132-fa07-ad97-a967-0967c2b35a14.png)
初回はファイアウォール警告が出ますのでチェックを入れてアクセスを許可するを押してください。

これだけで準備は完了です。
次回からはVisualStudioを立ち上げなくても、VRC_Panorama_TCP_Repeater-master\VRC_Panorama_TCP_Repeater\bin\DebugフォルダのVRC_Panorama_TCP_Repeater.exeを直接実行出来ます。

少しコードの解説をします。

```csharp:MainWindow.xaml.cs
if (path.StartsWith("?"))// ?で始まる時はコマンドとして処理する
{
    var command = path.Substring(1);
    if (command == "TRIGGER")
    {
        if (isFirstTrigger) //初回のTriggerはワールドに入ったときに自動的に発動する
        {
            isFirstTrigger = false;
            Log("ワールドに入りました");
        }
        else
        {
            Log("TRIGGERコマンド受信");
            var t = SendCommandToESP8266Async("GPIOSW"); //処理に時間がかかる場合があるのでawaitせずに実行
        }
    }
    //コマンド受信時はわざとエラーを返すことで、VRC_Panoramaが再度接続してくるようにする。
    Log($"{client.Client.RemoteEndPoint},404,{path}");
    writer.Write(enc.GetBytes("HTTP/1.0 404 Not Found\r\n"));
    writer.Write(enc.GetBytes("Content-Type: text/plain\r\n"));
    writer.Write(enc.GetBytes("\r\n"));
    writer.Write(enc.GetBytes("File Not Found\r\n"));
    writer.Flush();
    return;
}
```
この部分で、先ほど設定したVRC_Panoramaの通信を処理しています。
VRC_Panoramaでは`http://localhost:61221/?TRIGGER`というアドレスを設定しましたが、
最初の行のpathには`?TRIGGER`が入るようになっていますので、先頭文字が?だった場合コマンドとして処理します。
そしてコマンドがTRIGGERだった場合に
初回はワールドに入ったときなので無視
2回目以降はESP8266に向かってGPIOSWコマンドを送るようになっています。
これでVRC_Panorama->このアプリ->ESP8266の中継を行います。
そして処理した後、404エラーを返しています。このようにすることでVRC_Panoramaにはテクスチャが設定されず、
次回以降も再度問い合わせてくれます(一度テクスチャを読み込めると二度と読み込まない)
ここは404である必要はありません。200でも、テクスチャになる画像を返さなければ良いです。
例えばVRC_Panoramaだけでなく別のアプリからも同じようにトリガーしたいのであれば、404ではなく200にしてOK等の
文字列を返す方が親切かと思います。

この部分を改造して好きなコマンドを実装すれば、VRC_Panoramaから好きなように通信を受けて何かを実行できます。

また、?以外が来たとき、例えば`http://localhost:61221/image.png`等の通常のURLアクセスを行った場合は
exeと同じフォルダにあるwwwフォルダの中身を返すようになっていますので、www/image.png等を置けば
VRC_Panoramaに普通にテクスチャ画像を返すことも可能です。

# ESP8266(ESP-WROOM-02)のプログラムを用意する

今回は自作のESP-WROOM-02ボードを使用していますが、どんなボードでも構いません。
IO13ピンに付けたLEDを光らせるプログラムを作成します。
ESP-WROOM-02自体の設定や書き込み方法はボードによって異なる場合があるので、各自調べてください。
Arduino IDEで書き込みます。

早速プログラムを紹介します。

```cpp:main.ino
#include <ESP8266WiFi.h>

const char ssid[] = "ssid"; //接続先Wi-FiのSSIDを入れる
const char password[] = "pass"; //接続先Wi-Fiのパスワードを入れる
const int SW = 13; //出力するIOピン

//固定IPアドレス(環境ごとに変えてください)
IPAddress ip(192,168,1,200);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);

WiFiServer server(9999);  //ポート番号

void setup() {
  Serial.begin(115200);
  pinMode(SW, OUTPUT);
  Serial.println("Booting...");
  WiFi.config(ip, gateway, subnet);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  delay(500);

  int retrycnt = 0;
  while(WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.print(".");
    delay(100);
    if(retrycnt++ > 100) {
      ESP.restart();
    }
  }
  Serial.println("Connect Success");
  
  //Portのlistenを開始
  server.begin();
  Serial.println("Start Server");
}

void loop() {
  String cmd = ReceiveCommand();
  if (cmd == "GPIOSW") {
    digitalWrite(SW, HIGH);
    delay(100); //100ms wait
    digitalWrite(SW, LOW);
  }
}

String ReceiveCommand() {
  WiFiClient client = server.available();
  String ret;
  if (client.connected()) {
    //コマンド受信（来なければタイムアウト
    ret = client.readStringUntil('\r');
    Serial.print("Receive:");
    Serial.println(ret);
    //返答する
    client.print("OK\r");
    //接続をクローズ
    client.stop();
  }
  return ret;
}
```
各自自分のWi-FiルーターのSSIDとパスワード、固定するためのIPアドレスを書き換えてください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/3b9ff3b4-0f89-1289-9e41-fe1972e86d54.png)
参考にテスト時の書き込み設定です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/b5953759-e1a5-866a-d914-340a8cf70ade.png)
最低限動作に必要な参考回路図を置いておきます。

# 実際に動かしてみる

1. ESP-WROOM-02の電源を入れます  
2. VRC_Panorama_TCP_Repeater.exeを実行して、開始ボタンを押します。  
3. クライアント設定(ESP8266側)のサーバーIPにESP8266に設定したIPアドレスを入れてください。  
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/e7691d20-03b2-c94b-9362-95e613a30f36.png)
4. 作成したVRChatのワールドに入ります(New Buildを押します)  
5. VRC_Panorama_TCP_Repeaterにワールドに入りましたとログが出ることを確認します。  
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/96945327-40db-f10f-c4f8-a79480088e6b.png)
6. ワールド内のCubeでトリガーを引きます。  
![image.png](https://qiita-image-store.s3.amazonaws.com/0/105690/df04153e-4b58-2710-e7e9-5ca8548a785f.png)
7. トリガーを引くたびに、画面のログにTRIGGERコマンド受信と、OKが返ってきて、LEDが光ることを確認します。  
※OKではなくエラーが返ってくる場合はESP8266との通信に失敗しています。Arduinoターミナルでログを確認してください。

無事LEDが押すたびに点灯すれば完成です。

# 謝辞

AnimatorControllerやAnimationの設定方法について、[がとーしょこら](https://twitter.com/gatosyocora)さんに教えていただきました。
ありがとうございました。

# おわり

VRChatの何かしらのトリガーをローカルで受け取ることが出来るようになりました。これで外部のLEDだけでなく、VRChat外で何かをするアプリケーションや、
例えばワールド内の扇風機のスイッチを押したらリアルの扇風機を動かす装置なんかを作ることも出来ると思います。
使い方色々、面白い使い方待ってます。

今回のVRChatから外部の装置のスイッチを入れる仕組みを使った放送が明日行われます。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">【史上初！失禁実況系Vtuber登場！】<br>まず、フィオが<a href="https://twitter.com/faruco10032?ref_src=twsrc%5Etfw">@faruco10032</a> さんの「失禁体験装置」を装着します。視聴者さんがLiveコメントで「尿意」と打ちます。すると「尿意オブジェクト」が降り注ぎます。徐々に尿意が高まり、限界を超えると装置が起動。<br>そういう配信をするなのだ！クリスマス前だしね。 <a href="https://t.co/ZRLAPLDgtz">pic.twitter.com/ZRLAPLDgtz</a></p>&mdash; 動く城のフィオ⚙仮想空間興行師⚙ (@phio_alchemist) <a href="https://twitter.com/phio_alchemist/status/1069175241660805120?ref_src=twsrc%5Etfw">2018年12月2日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
是非ご覧ください。

それでは。

明日は、[suna(@sunasaji)](https://twitter.com/sunasaji)さんによる記事です。

[VRChat Advent Calendar 2018](https://qiita.com/advent-calendar/2018/vrchat)