17
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

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

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

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

Thanks to

構成

ハードウェア

  • センサー(3軸加速度、3軸ジャイロ):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のスケッチ

// 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

[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のモデルにアタッチしたもの

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にアタッチしたもの

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();
        }

}
17
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?