動画: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
- すべてのネタの源泉である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
- 3軸ジャイロ、加速度センサーの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
構成
ハードウェア
- センサー(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();
}
}