私はいかにしてUnityのシリアルポートで悩むのをやめ、MQTTでLive2D少女を操ることになったか


動画：https://twitter.com/ocaokgbu/status/687457479140093954
github：https://github.com/kgbu/marionette

FaceRigでLive2DってのがGOROmanさんによる爆発的ヒットだったので、FaceRigのかわりに６個のパラメータが一遍にとれるセンサー(MPU6050)をArduinoで受信して、そのままUnityに取り込めるかとおもったら最近のUnity5だとうまくいかない事例があるということでMQTT経由にしました。
そのためRaspberry Pi2とその上で動くMQTTゲートウェイソフトウェアのFujiが一枚かんでいます。

MQTT経由にすることで、Live2Dの「振り」を多数（それこそ数千でも）の端末に同時にリアルタイム配信できるようになったので、ケガの功名かも。

この仕組みで対戦ゲームとか、できるわけです（高頻度でパケット送るのに適した回線じゃないとあれだけど、動画配信よりは楽そう）。

## Thanks to

- すべてのネタの源泉であるGOROmanさんへ
-  Live2D のサンプルコードなど：http://sites.cybernoids.jp/cubism2/lets-do-it/my-first-lapp
-  Live2Dのモデルを駆動するパラメータの素晴らしい解説文書を作成してくださったMaruchuさんへ https://drive.google.com/file/d/0B-99Ww6ro_aodkhET1lwWFVFQTQ/view
- ３軸ジャイロ、加速度センサーのArduinoスケッチをパブリックドメインにしてくれたArduino User JohnChiへ http://playground.arduino.cc/Main/MPU-6050
- Windows10とUnity5の組み合わせではうまくいかなかったけどシリアルポートの扱いを教えてくれたmakoragi on Qiitaさんへ http://qiita.com/makoragi/items/79813b660edbd1246c7c
- Unityのシリアルポートの扱いは大変そうだと教えてくれた7of9さんへ http://qiita.com/7of9/items/cb08473ec5f3cc8d4b61
- UnityでMQTT送受信の記事をかいてくれたxhatenenさんへ http://xhatenen.hatenablog.com/entry/2014/12/23/230657


## 構成

### ハードウェア

- センサー（３軸加速度、３軸ジャイロ）：MPU-6050
- Arduino nano：センサーの値をI2Cで10Hzでサンプリングして115200baudでJSON形式でシリアル通信
- Raspberry Pi 2 ：シリアル通信して得たデータをFujiを使ってMQTTでブローカーサーバーへ送信
- Unityを実行したPC：
    - CPU：Intel Core i7-6600HQ 2.6GHz 4core Hyperthreading
    - RAM：16GB
    - Graphic board NVIDIA GEFORCE GTX970M (3GB memory)
    - 内蔵ビデオカメラ

### ソフトウェア

- Arduinoのスケッチによるセンサーの値の読み取りとシリアル通信
- RaspberryPi2上のMQTTゲートウェイソフトウェア：Fujiによるシリアルポートからの読み込みとMQTTでのpublish
- インターネットのサーバー上のMQTTブローカーソフトウェア
- Windows 10 Home
- Unity 5
- Live2D

# コードはどないなっとんの

githubにあげてます。https://github.com/kgbu/marionette
ほとんどサンプルのままなので、Thanks toの情報を読んだほうが良いと思います。

## Arduinoのスケッチ

```text
// MPU-6050 Short Example Sketch
// By Arduino User JohnChi
// August 17, 2014
// Public Domain
//
// modified to print JSON 
// By kgbu@github 
// January 13, 2015
// Public Domain+

#include<Wire.h>
const int MPU_addr=0x68; // I2C address of the MPU-6050
int16_t AcX,AcY,AcZ;
int16_t Tmp;
int16_t GyX,GyY,GyZ;


void setup(){
  Wire.begin();
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x6B); // PWR_MGMT_1 register
  Wire.write(0); // set to zero (wakes up the MPU-6050)
  Wire.endTransmission(true);
  Serial.begin(115200);
}

void loop(){
  Wire.beginTransmission(MPU_addr);
  Wire.write(0x3B); // starting with register 0x38 (ACCEL_XOUT_H)
  Wire.endTransmission(false);
  Wire.requestFrom(MPU_addr,14,true); // request a total of 14 registers
  AcX=Wire.read()<<8|Wire.read(); // 0x3B (ACCEL_XOUT_H) & 0x3C (ACCEL_XOUT_L)
  AcY=Wire.read()<<8|Wire.read(); // 0x3D (ACCEL_YOUT_H) & 0x3E (ACCEL_YOUT_L)
  AcZ=Wire.read()<<8|Wire.read(); // 0x3F (ACCEL_ZOUT_H) & 0x40 (ACCEL_ZOUT_L)
  Tmp=Wire.read()<<8|Wire.read(); // 0x41 (TEMP_OUT_H) & 0x42 (TEMP_OUT_L)
  GyX=Wire.read()<<8|Wire.read(); // 0x43 (GYRO_XOUT_H) & 0x44 (GYRO_XOUT_L)
  GyY=Wire.read()<<8|Wire.read(); // 0x45 (GYRO_YOUT_H) & 0x46 (GYRO_YOUT_L)
  GyZ=Wire.read()<<8|Wire.read(); // 0x47 (GYRO_ZOUT_H) & 0x48 (GYRO_ZOUT_L)
  Serial.print("{\"AcX\": "); Serial.print(AcX);
  Serial.print(", \"AcY\": "); Serial.print(AcY);
  Serial.print(", \"AcZ\": "); Serial.print(AcZ);
  Serial.print(", \"GyX\": "); Serial.print(GyX);
  Serial.print(", \"GyY\": "); Serial.print(GyY);
  Serial.print(", \"GyZ\": "); Serial.print(GyZ);
  Serial.println("}");
  delay(100);
}
```

## MQTTゲートウェイFujiの設定ファイル

Fujiとは？ https://github.com/shiguredo/fuji

```toml
[gateway]
    name = "mpu"
    
[[broker."mqttbroker"]]
    hostname = "MQTTbrokerHostname"
    port = 1883
    username = "USERNAME"
    password = "PASSWORD"

[device."sensor"]
    type = "serial"
    broker = "mqttbroker"
    qos = 0
    serial = "/dev/USB0"
    baud = 115200
```


## Unityのスクリプト

### Live2Dのモデルにアタッチしたもの

```csharp
using UnityEngine;
using System;
using System.IO.Ports;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
using live2d;
using live2d.framework;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;



[ExecuteInEditMode]
public class SimpleModel : MonoBehaviour
{
   
    public TextAsset mocFile;
    public TextAsset physicsFile;
    public Texture2D[] textureFiles;

    private MqttClient4Unity client;

    public string brokerHostname;
    public int brokerPort = 1883;
    public string userName = "";
    public string password = "";
    public string topic = "";
    public float AccRatio = 2000.0F;
    public float GyroRatio = 7000.0F;


    private float gyX, gyY, gyZ;
    private const Int64 mean_size = 20;
    private float[] _lastGyX = new float[20];
    private float[] _lastGyY = new float[20];
    private float[] _lastGyZ = new float[20];
    private float acX, acY, acZ;

    private Int64 meanindex = 0;

    private Live2DModelUnity live2DModel;
    private EyeBlinkMotion eyeBlink = new EyeBlinkMotion();
    private L2DPhysics physics;
    private Matrix4x4 live2DCanvasPos;



    void Start()
    {
        Live2D.init();

        load();

        if (brokerHostname != null && userName != null && password != null)
        {
            Connect();
            client.Subscribe(topic);
        }

        var j = (IDictionary) MiniJSON.Json.Deserialize("{\"AcX\": -15200, \"AcY\": -1416, \"AcZ\": 4292, \"Tmp\": -4528, \"GyX\": -203, \"GyY\": 72, \"GyZ\": -48}");
        Debug.Log(j.GetType());
        
    }

    void Connect()
    {
        // SSL使用時はtrue、CAを指定
        client = new MqttClient4Unity(brokerHostname, brokerPort, false,
                                      null);
        // clientidを生成
        string clientId = Guid.NewGuid().ToString();
        client.Connect(clientId, userName, password);
    }


    void load()
    {
        live2DModel = Live2DModelUnity.loadModel(mocFile.bytes);

        for (int i = 0; i < textureFiles.Length; i++)
        {
            live2DModel.setTexture(i, textureFiles[i]);
        }

        float modelWidth = live2DModel.getCanvasWidth();
        live2DCanvasPos = Matrix4x4.Ortho(0, modelWidth, modelWidth, 0, -50.0f, 50.0f);

        if (physicsFile != null) physics = L2DPhysics.load(physicsFile.bytes);
    }


    void Update()
    {
        if (live2DModel == null) load();
        live2DModel.setMatrix(transform.localToWorldMatrix * live2DCanvasPos);
        if (!Application.isPlaying)
        {
            live2DModel.update();
            return;
        }
        while (client.Count() > 0)
        {
            meanindex = (meanindex + 1) % mean_size;
            string jsonLine = client.Receive().Split('\n')[0];
            var json = MiniJSON.Json.Deserialize(jsonLine) as Dictionary<string, object>;
            Debug.Log(jsonLine);

            if (json == null) { continue;  }
            Debug.Log(json["AcX"].GetType() + "type of json");
            Debug.Log(json["AcX"]);
            acX = (Int64)json["AcX"] / AccRatio;
            acY = (Int64)json["AcY"] / AccRatio;
            acZ = (Int64)json["AcZ"] / AccRatio;
            _lastGyX[meanindex] = ((Int64)json["GyX"]);
            _lastGyY[meanindex] = ((Int64)json["GyY"]);
            _lastGyZ[meanindex] = ((Int64)json["GyZ"]);
            gyX = gyY = gyZ = 0.0f;
            for (var i = 0; i < mean_size; i++)
            {
                gyX += _lastGyX[i];
                gyY += _lastGyY[i];
                gyY += _lastGyY[i];
            }
            gyX = gyX / GyroRatio;
            gyY = gyY / GyroRatio;
            gyZ = gyZ / GyroRatio;

            Debug.LogFormat("{0} {1} {2} {3} {4} {5}",acX, acY, acZ, gyX, gyY, gyZ);
            break;
        }
        

        live2DModel.setParamFloat("PARAM_ANGLE_X", acX); // head panning : value range -30.0 to 30.0 (degree)
        live2DModel.setParamFloat("PARAM_ANGLE_Y", acY); // head banking back and forth : -30 to 30 

        live2DModel.setParamFloat("PARAM_BODY_ANGLE_X", acZ); // body angle sideway : -30.0 to 30.0

        live2DModel.setParamFloat("PARAM_EYE_L_OPEN", gyX + 0.5f); // 0 to 1
        live2DModel.setParamFloat("PARAM_EYE_R_OPEN", gyX + 0.5f); // 

        live2DModel.setParamFloat("PARAM_BROW_L_Y", gyX); // -1.0 to 1.0
        live2DModel.setParamFloat("PARAM_BROW_R_Y", gyX); // 

        live2DModel.setParamFloat("PARAM_MOUTH_OPEN_Y", gyY + 0.5f); // 0 to 1.0
        live2DModel.setParamFloat("PARAM_MOUTH_FORM", gyZ); //  -1.0 to 1.0


        live2DModel.setParamFloat("PARAM_BREATH", 1);

        eyeBlink.setParam(live2DModel);

        if (physics != null) physics.updateParam(live2DModel);

        live2DModel.update();
    }



    void OnRenderObject()
    {
        if (live2DModel == null) load();
        if (live2DModel.getRenderMode() == Live2D.L2D_RENDER_DRAW_MESH_NOW) live2DModel.draw();
    }


}
```

### 操作する人を映したカメラ画像をはりつけたPlaneにアタッチしたもの

```csharp
using UnityEngine;
using System.Collections;

public class videocaptureTexture : MonoBehaviour {


        public int Width = 1920;
        public int Height = 1080;
        public int FPS = 30;

        void Start()
        {
            WebCamDevice[] devices = WebCamTexture.devices;
            // display all cameras
            for (var i = 0; i < devices.Length; i++)
            {
                Debug.Log(devices[i].name);
            }

            WebCamTexture webcamTexture = new WebCamTexture(devices[0].name, Width, Height, FPS);
            GetComponent<Renderer>().material.mainTexture = webcamTexture;
            webcamTexture.Play();
        }

}
```