Unity+ARCore+C4Dで連携させたい

# これは何？
Google社の提供するAR(拡張現実)プラットフォーム[ARCore](https://developers.google.com/ar/discover/)と、MAXON Computer社の提供するDCCツール[Cinema4D](https://www.maxon.net/jp/)(以下C4D)を連携させたいと思ったのでやってみた記事です。
これができれば、C4D上でAR空間にインタラクトするなど色々と遊べるのでは？と思った次第です。Unityでの開発やPythonのC4Dモジュールなど個人的にあまり触ってこなかった技術を盛り込んでいるため備忘録の意味も兼ねて投稿しています。

# 使用するもの
## Unity + ARCore
実際には

- Unity
- ARCore SDK for Unity
- [ARCore対応端末](https://developers.google.com/ar/discover/supported-devices)

が必要です。

ARCoreを使っての開発自体はAndroid(Java)、iOS、Unity、Unreal等々幅広くSDKが用意されているので何を使っても大丈夫ですが、ここではUnityを使って開発しました。

## Cinema4D
今回使ったのは[Cinema4D学生無償版](https://www.maxon.net/jp/%E3%83%88%E3%83%AC%E3%83%BC%E3%83%8B%E3%83%B3%E3%82%B0/%E6%95%99%E8%82%B2%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%82%BD%E3%83%AA%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/%E6%95%99%E8%82%B2%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E3%82%BD%E3%83%AA%E3%83%A5%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/%E5%AD%A6%E7%94%9F/license-information-free/)です。本来ウン十万するC4Dですが高校生以上の学生は申請することで最上位グレードであるStudioを__無償__で使うことができます（！）。
他のDCCツールと比較して、

- インターフェースがわかりやすい
- アニメーションに強い(Mograph)

といった特徴がありますが、今回はスクリプト言語としてPythonを採用していることに着目しました。C4Dのスクリプト環境は紆余曲折あったようで、専用言語のCOFFEEなんてのもありますが、将来的にはPythonで統一する流れになっています。

## OSC
唐突に名前が出てきましたが、OSCとはOpen Sound Controlの略で、MIDIの後継に当たるデータ通信プロトコルのことです。詳しくは[Wikipedia](https://ja.wikipedia.org/wiki/OpenSound_Control)を参照してください。
音楽データの共有としてはまだMIDIのほうが使われるみたいですが、今回のような複数ソフトの連携にOSCはよく使われます。また、様々な言語でOSCを実装したライブラリが存在するということもあり、採用に至りました。

# 準備
## 開発環境
まずUnityとARCoreのSDKをダウンロードし、インストールしておきます。

- [Unity](https://unity3d.com/jp)
- [ARCore SDK for Unity](https://github.com/google-ar/arcore-unity-sdk)

UnityはAndroid Build Support付きでインストールしてください。また、環境によってはAndroid SDKをインストールするために[Android Studio](https://developer.android.com/studio/?hl=ja)を先に入れたり、[JDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)が必要だったりします。

C4Dに関しては、高価なツールであるため既に所有している前提で話を進めます。学生の方はぜひ学生無償版を使ってみてください(露骨な宣伝)

### 注意点
Unityをダウンロードする際、最新版(執筆時点で2018.1)だとARCoreが動かない可能性があります(自分の環境では動きませんでした)。SDKの現在最新版がv1.3.0なので、これを試される方はバージョン2017.4など過去バージョンを導入してください。参考までに自分の環境：

- Unity 2017.4.1f1
- ARCore SDK v1.3.0
- Samsung Galaxy S9 (docomo版)

Unityをインストールしたら、SDKを使って新規プロジェクトを作りサンプルを動かしてみましょう。今回はHelloARのサンプルに組み込まれてるスクリプトを改造していきます。
### 導入方法
[公式ドキュメント](https://developers.google.com/ar/develop/unity/quickstart-android)

導入方法からサンプルの動かし方までわかりやすくまとめられている方がいらしたのでそちらも貼っておきます。
http://kato-robotics.hatenablog.com/entry/2018/04/18/220224

## 使用ライブラリ
今回の環境として、UnityではC#、C4DではPython2.7.10を使うため、それぞれに対応したOSCライブラリを用意しておきます。

- [UnityOSC v1.2](https://github.com/jorgegarcia/UnityOSC)
- [pyOSC 0.3.5b-5294](https://pypi.org/project/pyOSC/)

### Unity
今回はUnity側でOSCメッセージを受信する(Server)ためのセットアップをしていきます。

- ARCoreのサンプルを動かしたプロジェクトを開いておく
- UnityOSCのgitリポジトリをcloneまたはダウンロード＆解凍し、srcディレクトリをAssetsにドラッグ&ドロップなどで置く
![fig1.png](https://qiita-image-store.s3.amazonaws.com/0/154601/fe5fec6e-9d90-ad98-0b86-ae0a41f9a448.png)

- src/OSCHandler.csを開く
- OSCHandlerクラスのInit()メソッドを探す

```cs
public void Init(){
//Initialize OSC clients (transmitters)
//Example:
//CreateClient("SuperCollider", IPAddress.Parse("127.0.0.1"), 5555);

//Initialize OSC servers (listeners)
//Example:
//CreateServer("AndroidPhone", 6666);
}
```
- CreateServerのところのコメントアウトを外し、ポート番号を適当なものに変える(そのままでもOK)

```cs
//Example:
CreateServer("AndroidPhone", 8001);
```

### C4D
C4D側からOSCメッセージを送るための準備です。

- (入れていなければ)Python2.7.9 (orそれ以降)とpipをインストールしておく
- 以下のコマンドでpyOSCをC4DのPythonライブラリを格納するディレクトリにインストールする(Winの場合)

```bash
pip install pyOSC -t "C:\Users\<ユーザー名>\AppData\Roaming\MAXON\Cinema 4D Rxx\library\python\packages\win64"
```
macの場合は…また分かり次第追記します。

- C4Dを起動し、メニューで「スクリプト」→「コンソール」でコンソールを立ち上げ、`import OSC`と打ち込み[OK]と言われれば準備完了です。
![2bc017d07e2b30cdca502250c12c8de8.png](https://qiita-image-store.s3.amazonaws.com/0/154601/4163a5ce-cd51-8ff1-76c6-2f1fa81fef8b.png)

# 実装
今回実装したものは、"ARCoreで召喚したAndy(Androidのマスコットキャラクター)の大きさがOSCメッセージの引数に応じて変わる"というものです。

## Unity側
- HelloARをインポートしたプロジェクトを開き、プロジェクトウィンドウからAssets/GoogleARCore/Examples/HelloAR/HelloARController.csを開きます。
<img width="609" alt="スクリーンショット 2018-07-03 14.11.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/154601/cf888c9f-ac05-ad01-1da6-da25727c64ce.png">
- `Update()`メソッドの前でメンバ変数を宣言しているので、そこに召喚したAndyのインスタンスを確保するリスト`andys`とその一番最後のインデックスを示す`indexOfAndy`を追加します。

```cs:HelloARController.cs
/// <summary>
/// True if the app is in the process of quitting due to an ARCore connection error, otherwise false.
/// </summary>
private bool m_IsQuitting = false;

/// <summary>
/// Added andy instance list.
/// </summary>
public List<GameObject> andys = new List<GameObject>();

/// <summary>
/// Current index of andys.
/// </summary>
public int indexOfAndy = 0;

/// <summary>
/// The Unity Update() method.
/// </summary>
public void Update()
{
```
- `Update()`メソッド内で、タップしたときの当たり判定をしている部分があるので、以下のように変更します。

```cs:HelloARController.cs
if (Frame.Raycast(touch.position.x, touch.position.y, raycastFilter, out hit))
{
    // Use hit pose and camera pose to check if hittest is from the
    // back of the plane, if it is, no need to create the anchor.
    if ((hit.Trackable is DetectedPlane) &&
         Vector3.Dot(FirstPersonCamera.transform.position - hit.Pose.position,
         hit.Pose.rotation * Vector3.up) < 0)
    {
        Debug.Log("Hit at back of the current DetectedPlane");
    }
    else
    {
        // Instantiate Andy model at the hit pose.
        //var andyObject = Instantiate(AndyAndroidPrefab, hit.Pose.position, hit.Pose.rotation);
        andys.Add(Instantiate(AndyAndroidPrefab, hit.Pose.position, hit.Pose.rotation));

        // Compensate for the hitPose rotation facing away from the raycast (i.e. camera).
        andys[indexOfAndy].transform.Rotate(0, k_ModelRotation, 0, Space.Self);

        // Create an anchor to allow ARCore to track the hitpoint as understanding of the physical
        // world evolves.
        var anchor = hit.Trackable.CreateAnchor(hit.Pose);

        // Make Andy model a child of the anchor.
        andys[indexOfAndy].transform.parent = anchor.transform;

        indexOfAndy++;
    }
}
```
元のソースではAndyのインスタンスを生成するだけでしたが、これをリストにとっておくことで既に配置したAndyを参照してパラメータを変更できるようにするといった感じですね。
次にOSC受信用のスクリプトを作ります。

- 適当なゲームオブジェクト(ここではFirst Person Camera)にAdd Component→New Scriptで新しくスクリプトを作ります。ここでは名前を`OSCcontroller.cs`としておきます。
![スクリーンショット 2018-07-03 14.39.46.png](https://qiita-image-store.s3.amazonaws.com/0/154601/68aa4d60-566b-dc2f-b555-900cb6ab7de8.png)
- Assets/直下に`OSCcontroller.cs`があるはずなので、それを開きます。
- `OSCcontroller.cs`の中身を以下のように記述します。

```cs:OSCcontroller.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityOSC;
using GoogleARCore.Examples.HelloAR;

public class OSCcontroller : MonoBehaviour {

	private long lastTimeStamp;
	public GameObject ARC;
	private HelloARController arc;

	// Use this for initialization
	void Start () {
		OSCHandler.Instance.Init();
        lastTimeStamp = -1;
		ARC = GameObject.Find("Example Controller");
		arc = ARC.GetComponent<HelloARController>();
	}
	
	// Update is called once per frame
	void Update () {
		//  受信データの更新
        OSCHandler.Instance.UpdateLogs();
        //  受信データの解析
		string address;
        foreach (KeyValuePair<string, ServerLog> item in OSCHandler.Instance.Servers) {
            for (int i=0; i < item.Value.packets.Count; i++) {
                if (lastTimeStamp < item.Value.packets[i].TimeStamp) {
                    lastTimeStamp = item.Value.packets[i].TimeStamp;
                    //  アドレスパターン（文字列）
                    address = item.Value.packets[i].Address;
					if(address == "/scale"){
						//  引数
						var argx = (float)item.Value.packets[i].Data[0];
						var argy = (float)item.Value.packets[i].Data[1];
						var argz = (float)item.Value.packets[i].Data[2];
                    	//  処理
						var list = arc.andys;
						for(int j=0; j<arc.indexOfAndy; j++){
							list[j].transform.localScale = new Vector3(argx,argy,argz);
						}
					}
                    
                }
            }
        }
	}
}

```
受信周りの処理はhttps://www.ei.tohoku.ac.jp/xkozima/lab/uniTutorial2.html を参考にしました。
今回、C4Dから送られてくるデータをスケールのデータとして処理するのでアドレスを`/scale`として、3つの引数を受け取り、それぞれを`(x,y,z)`のベクトル値として配置されてるAndyのスケールに代入しています。

## C4D側
C4D側は非常にシンプルです。

- 新規プロジェクトを作ります。
- ヌルオブジェクトを呼び出し、名前を"Controller"としておきます(任意)。
- オブジェクトマネージャで呼び出したヌルオブジェクトを右クリックして、「スクリプトタグ」→「Python」でPythonタグをつけます。
![9c6f564abfdb6ffebe317e6dcac1bbd3.png](https://qiita-image-store.s3.amazonaws.com/0/154601/62e0e8d0-1547-97b3-ed7e-6f8aa81f1b30.png)
- Pythonタグをクリックし、スクリプトの中身を以下のようにします。

```python
import c4d
#Welcome to the world of Python
import OSC

ip = 'xxx.xxx.xxx.xxx' #OSCサーバー(Android端末)のIPアドレス
port = 8001 #OSC通信用ポート番号

def genParameter(obj):
    if obj is None: return False
    #objにスライダーのユーザーデータを追加する
    baseCon = c4d.GetCustomDatatypeDefault(c4d.DTYPE_REAL) #浮動少数ユーザーデータの取得
    baseCon[c4d.DESC_NAME] = 'スケール' #ユーザデータの名前を変更
    baseCon[c4d.DESC_SHORT_NAME] = 'スケール'
    baseCon[c4d.DESC_ANIMATE] = c4d.DESC_ANIMATE_ON #アニメーション可能
    baseCon[c4d.DESC_UNIT] = c4d.DESC_UNIT_REAL #単位は実数値
    baseCon[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_REALSLIDER #スライダーとする
    baseCon[c4d.DESC_MIN] = 0.01
    baseCon[c4d.DESC_MINSLIDER] = 0.01 #最小値(0.01倍)
    baseCon[c4d.DESC_MAX] = 2
    baseCon[c4d.DESC_MAXSLIDER] = 2 #最大値(2倍)
    baseCon[c4d.DESC_DEFAULT] = 1 #デフォルト値(1倍)
    baseCon[c4d.DESC_STEP] = 0.01 #スライダーの移動間隔
    u_data = obj.AddUserData(baseCon) #objにユーザデータを追加
    obj[u_data] = 1
    c4d.EventAdd()
    return u_data

def sendMsg(address, val):
    #valがリストのとき、中身を文字列にしてすべて送信
    if isinstance(val, list):
        #OSC通信の準備
        client = OSC.OSCClient()
        msg = OSC.OSCMessage()
        msg.setAddress(address)
        for i in range(len(val)):
            msg.append(val[i])
            
        #送信
        client.sendto(msg, (ip, port))
        print "sent to  %s ,[%s]" % (ip, msg)
        return

def main():
    null_obj = op.GetObject()
    uData = null_obj.GetUserDataContainer()
    #ヌル オブジェクトにユーザーデータがまだない場合のみ追加する
    if not uData:
        uData = genParameter(null_obj) #ヌル オブジェクトにスライダーを追加
        #null_obj.SetUserDataContainer(1, uData)
    
    val = null_obj[c4d.ID_USERDATA,1]
    sendMsg("/scale",[val,val,val])
        
```
スクリプトを入力して他の場所をクリックするなどすると、実行されてヌルオブジェクトにユーザーデータが出現します。
![a3a873996995d152e4c8e57d44d6c434.png](https://qiita-image-store.s3.amazonaws.com/0/154601/cc784fa7-6c46-6aa9-9439-4e10548a871f.png)
スクリプトが実行されると、まずタグが付いているオブジェクトにユーザーデータがあるかどうかを確認します。
なければ新しくスライダーのユーザーデータを追加します。
ユーザーデータがあるとき、スライダーの値を読み取って指定されたIPアドレスに`/scale`アドレスを付けてメッセージを送信するという流れです。
色々調べたところ、Pythonタグのスクリプトが実行されるタイミングとしては

- タイムラインでフレームが進んだとき
- オブジェクトが選択/選択解除されたとき
- オブジェクトのパラメータをいじったときetc

という感じのようなので、これを利用してスライダーの値が動いたら実行することを想定してOSCメッセージを飛ばすようにしています。

# 実行した様子

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">やりたいことができた！ってことでQiita投稿用に動画です<a href="https://twitter.com/hashtag/ARCore?src=hash&amp;ref_src=twsrc%5Etfw">#ARCore</a> × <a href="https://twitter.com/hashtag/Cinema4D?src=hash&amp;ref_src=twsrc%5Etfw">#Cinema4D</a> <a href="https://t.co/ccWxM6Oiu3">pic.twitter.com/ccWxM6Oiu3</a></p>&mdash; Kurogoma.c4d (@Krgm4D) <a href="https://twitter.com/Krgm4D/status/1014161844083150848?ref_src=twsrc%5Etfw">2018年7月3日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

いい感じに動いてます。遅延もそれほどありません。
主にC4D Python関係でハマりどころはちょくちょくありましたがOSCでの連携が容易にできることがわかったのでまだ色々遊べそうですね。

### プロジェクトファイル
(2018/8/3追記)gitlabに今回使ったプロジェクトファイルを置きました。
https://gitlab.com/Kurogoma4D/ARCoreTest