1ビットの孤独

コンピュータに関する技術系ブログ。斎藤 康毅のブログ。

「天才けんけんぱ」:Unityとセンサーの連携(はじめに)

半年ぐらい前にUnityについての技術レポートを書きました。ただ残念なことに、予定していた発表の場を逃してしまいました(本当は書籍になる予定だったのに、残念...)。そこで、せっかく書いたのだからということで、ここに公開しておきたいと思います。

技術レポートのタイトルは『天才けんけんぱ:Unityとセンサーの 連携』というものです。ちなみに、次の動画が「天才けんけんぱ」というアトラクションの紹介になります。技術レポートでは、このアトラクションを例にとって、Unityとセンサーの連携を中心に技術的な解説を行っています。

Hopscotch for Geniuses / 天才ケンケンパ - YouTube

少しページが多いので、下記のように節ごとに分けています。

「天才けんけんぱ」:Unityとセンサーの連携(1/4)
「天才けんけんぱ」:Unityとセンサーの連携(2/4)
「天才けんけんぱ」:Unityとセンサーの連携(3/4)
「天才けんけんぱ」:Unityとセンサーの連携(4/4)

また、pdf版は下記リンクよりダウンロードできます。
Dropbox - Unity_ksaitoh.pdf

参考になれば幸いです。

「天才けんけんぱ」:Unityとセンサーの連携(4/4)

本記事は次に示す4つのページから構成されています。
「天才けんけんぱ」:Unityとセンサーの連携(1/4)
「天才けんけんぱ」:Unityとセンサーの連携(2/4)
「天才けんけんぱ」:Unityとセンサーの連携(3/4)
「天才けんけんぱ」:Unityとセンサーの連携(4/4)

1.4 まとめ

f:id:koki0702:20141120144410p:plain

本章では「天才けんけんぱ」というインタラクティブなシステムを例にとって、Unityとセンサーの連携、またそこで使われている技術的なトピックを解説しました。Unityは主にゲーム開発で使われますが、今回のような現実世界におけるインタラクティブなシステムにおいても、その威力を発揮してくれます。Unityの開発フローの利便性が相まって、開発効率や表現力を格段に向上させてくれるでしょう。

今回のような現実世界とインタラクティブに反応するシステムにおいて、センサーは必ず必要になります。本章では、測域センサーというセンサーを例にとって解説しましたが、webカメラなど他のセンサーを使用した場合でも、本章で述べたことが適用できる部分は多くあると思います。

以上で本章は終わりになります。本章の内容が少しでもお役に立つことができれば幸いです。

コラム1 開発のプロセス

Unityを用いた開発は、素材の差し替えから実行までスピーディーに行えます。そのため、思いついたことをすぐに試すことができ、それにより様々な可能性が広がるでしょう。今回の「天才けんけんぱ」の場合でも、デザインコンセプトを決めるために、実際の環境で素材を変えながら、その場で見た目の確認を行いました。また、開発段階で実際に子供に遊んでもらう過程で、デザインを変更しながら子供の反応を伺うことができました。

f:id:koki0702:20141120144411p:plain

開発段階で開発メンバーや子供の反応を見ながら、様々なデザイン・動きなどを試していく

コラム2 水面の表現

川の水面を表現するためには、Unityのアセットで用意されてある「Water」を使うことができます。UnityのメニューでAssetsからImport Packageで「Water(Basic)」または「Water(Pro Only)」を選択します。下に示す通り、リアルタイムに水面の上下運動や反射・屈折がシミュレーションされます。

f:id:koki0702:20141120144412p:plain

川の水面:波の上下する運動や光の反射・屈折がシミュレーションされる

参考文献

[1] OpenSound Control:http://opensoundcontrol.org/

[2] OpenCV「findHomography」:http://opencv.jp/opencv-2svn/cpp/camera_calibration_and_3d_reconstruction.html

[3] 『詳解 OpenCV ―コンピュータビジョンライブラリを使った画像処理・認識』Gary Bradski、 Adrian Kaehler、 松田 晃一(翻訳)、オライリージャパン

[4] UnityOSC:https://github.com/jorgegarcia/UnityOSC

[5] Python - Simple OSC:http://opensoundcontrol.org/implementation/python-simple-osc

[6] 『Flocks, Herds, and Schools:A Distributed Behavioral Model』Craig Reynolds、SIGGRAPH '87

[7] 『Unity Shaders and Effects Cookbook』Kenneth Lammers、Packt Publishing

[8] 『The Cg tutorial―プログラム可能なリアルタイムグラフィックス完全ガイド』Randima Fernando、 杉山 明、木下 裕義、Mark J.Kilgard(編集)、中本 浩(編集)、ボーンデジタル

「天才けんけんぱ」:Unityとセンサーの連携(3/4)

本記事は次に示す4つのページから構成されています。
「天才けんけんぱ」:Unityとセンサーの連携(1/4)
「天才けんけんぱ」:Unityとセンサーの連携(2/4)
「天才けんけんぱ」:Unityとセンサーの連携(3/4)
「天才けんけんぱ」:Unityとセンサーの連携(4/4)

1.3 演出のポイント

ここでは、「天才けんけんぱ」というアトラクションにおいて、応用できそうな点をピックアップして解説していきます。

本アトラクションでは、川辺のステージを作り、その川に住む生き物や植物も楽しさを演出するために一役買っています(図7参照)。ここでは、その演出の中で「群シミュレーション」と「Shaderを用いた“揺れる”動き」の二点を解説したいと思います。

f:id:koki0702:20141120144357p:plain

図7.川辺には、オタマジャクシやカエル、トンボなどが生息して、足に反応して動きを変える

一つ目の「群シミュレーション」は、魚や鳥などの群れの動きをシミュレーションするためのアルゴリズムです。本アトラクションでは、オタマジャクシや魚を群れとなって行動させています。これにより、本当の川に住む生き物のようなリアルな動きを演出することができます。また、人の足が近づくと、一斉に逃げていくような動作を追加することで、“けんけんぱ”とは別の楽しさを子供に与えることができます。

二つ目の「Shaderを用いた“揺れる”動き」は、川の左右に配置した植物の風に揺れる動きと、人が踏んだときに草葉が揺れる動きについてです。この草葉の動きは、植物の3Dモデルを左右に周期的に波打たせることで、その動きを表現しています。このような動きを行わせるにはShaderを用いて行うことができます。

1.3.1 群シミュレーション

魚や鳥などの群(Flock)をなして行動する動きをシミュレーションするための手法に、Boidsアルゴリズム[6]があります。このアルゴリズムを用いることで、図8のように、オタマジャクシの群れの動きをシミュレーションすることができます。

f:id:koki0702:20141120144359p:plain

図8.おたまじゃくしの群シミュレーション

上の図のような動きを生み出すためには、とても複雑なことをやるのではないかと思うかもしれません。しかし、Boidsアルゴリズムはとてもシンプルです。実際、オタマジャクシに「ある3つのルール」を適用するだけで、上のような複雑な群れの動きを作り出すことができます。ここにBoidsアルゴリズムの面白さと奥深さがあります。

Boidsアルゴリズムを説明する際に、「エージェント」という言葉が出てきます。エージェントとは、群れを構成している個体のことで、上の例では、オタマジャクシを指します。

Boidsアルゴリズムは、エージェントに対して動きのルールを適用することで、群れの動きをシミュレーションします。先ほど述べた通り、エージェントに適用するルールは3つで、次のようになります。

1.分離:エージェントは互いに一定の距離をとる
2.整列:エージェントは群れ全体の方向へ整列する
3.結合:エージェントは群れ全体の重心へ向かう

「集結」のルールで、エージェントが集まり、「整列」のルールで、群全体の進む方向を合わせます。「整列」と「集結」だけではエージェントが密集してしまうので、「分離」の制約を設け、エージェント同士の間に適度な距離を作ります。

Boidsアルゴリズムの説明は以上です。それでは、実際にUnityで実装していきます。ここで説明に用いる実装は、出来るだけ単純化しているため、オリジナルのものとは少し違っています。詳細については、原著論文[6]や他の解説記事などを参考にして下さい。

1.3.1.1 UnityでのBoidsアルゴリズムの実装

まずはじめに、エージェントをPrefabとして作成します。ここでは、図9に示すような直方体を用いることにします。適宜、魚や鳥などの3Dモデルに変更することも、もちろん可能です。図9に示す通り、Rigidbodyを追加して、物理的な動きを制御できるようにします。ここでは、エージェントの動きを平面上に制約するため、y軸方向には動けないように、Freeze Positonのyにチェックを入れています。また、回転軸もy軸上だけで回転するように、Freeze Rotationのyにチェックを入れます

f:id:koki0702:20141120144400p:plain

図9.エージェント(群れで動く個体)をPrefabとして用意する

また、上のPrefabにはAgent.csというスクリプトを追加しています。Agent.csが行うことは、エージェントの動く方向に回転するだけです。ファイルの中身はUpdate関数に次の一行を追加しただけになります。

void Update () {
        transform.rotation = Quaternion.LookRotation(rigidbody.velocity);
    }       

続いて、郡れ全体を管理するためのオブジェクトを作成します。図10に示すとおり、FlockManagerという名前のオブジェクトに、後ほど作成するFlockManager.csというスクリプトを追加します。このFlockManager.csがエージェントを配置し、3つのルールに従って、各エージェントを動かす指示を与えます。

f:id:koki0702:20141120144402p:plain

図10.FlockManagerが群れ全体の動きの指示を出す

FlockManager.csは、次に示す3つのメンバー変数を持ちます。numAgentsはエージェントの数を、agentPrefabはエージェントのPrefabをそれぞれ指定します。_agentListは各エージェントをリストで保持します。

public int numAgents = 20;
public GameObject agentPrefab;
private List<GameObject> _agentList = new List<GameObject>();

FlockManager.csは、初期化処理として、エージェントを生成し、ランダムに配置します。そのためのコードは次に示すようになります。

void Start () {       
        for( int i = 0; i < numAgents; i++ ) {
            _agentList.Add( GameObject.Instantiate(agentPrefab) as GameObject );
            _agentList[i].transform.position = new Vector3(
                Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f)
            );
        }
    }

FlockManager.csの毎フレームの処理として、先ほど述べた3つのルールを適用してエージェントを動かします。そのため、Update関数は次のようになります。ruleAlignmentは「整列」、ruleCohesionは「結合」、_ruleSeparationは「分離」のためのルールを記述した関数です。

void Update () {
        _ruleAlignment();
        _ruleCohesion();        
        _ruleSeparation();
    }

それでは、3つのルールを一つずつ解説していきます。はじめに_ruleAlignment「整列」についてです。ここで行うことは、群れ全体の平均的な移動速度を求め(コード中では、averageVeclocity)、各エージェントにその平均的速度を設定します。

private void _rule_Alignment() { 
        Vector3 averageVelocity = Vector3.zero;     
        foreach (GameObject agent in _agentList) 
            averageVelocity += agent.rigidbody.velocity;
        averageVelocity /= _agentList.Count;        
        foreach (GameObject agent in _agentList) 
            _setVelocity( agent, averageVelocity );
    }

ここでは、_setVelocityという関数を用いています。これは、現在のエージェントの速度を、希望する速度(targetV)にいきなり設定するのではなく、現在のエージェントの速度から徐々に希望する速度に近づけるために使用します。

private void _setVelocity( GameObject agent, Vector3 targetV) {
        float RATIO = .001f;
        agent.rigidbody.velocity = agent.rigidbody.velocity * (1f - RATIO) + targetV * RATIO;
    }

続いて、_ruleCohesion、「結合」についてです。「結合」のルールで行うことは、群れの中心位置(コード中ではflockCenter)を求めて、その中心に向かう速度を設定することです。コードは次のようになります。

private void _rule_Cohesion() {
        Vector3 flockCenter = Vector3.zero;
        foreach(GameObject agent in _agentList ) {
            flockCenter += agent.transform.position;
        }
        flockCenter /= _agentList.Count;            
        foreach(GameObject agent in _agentList ) {
            Vector3 dir = (flockCenter - agent.transform.position).normalized;                      
            _setVelocity( agent, dir );
        }
    }

最後に、_ruleSeparation、「分離」についてです。ここで行うことは、群れの中で最も近いエージェントを探し出して、そのエージェントとの距離がある一定以内であれば(コード中ではSEPARATIOIN_DIST)、そのエージェントから遠ざかる方向へ移動させます。コードは次のようになります。

private void _rule_Separation() {
        float SEPARATIOIN_DIST = 1.0f;

        foreach (GameObject a1 in _agentList) {
            float minDis = float.MaxValue;
            Vector3 minDiff = Vector3.zero;

            foreach (GameObject a2 in _agentList) {
                if (a1 == a2) continue;
                
                Vector3 diff = a1.transform.position - a2.transform.position;
                
                if( minDis > diff.sqrMagnitude ) {
                    minDis = diff.sqrMagnitude;
                    minDiff = diff;
                }
            }

            if( minDis < SEPARATIOIN_DIST ) {
                float ratio = minDis / SEPARATIOIN_DIST;
                _setVelocity( a1, a1.rigidbody.velocity.magnitude * minDiff.normalized );           
            }
        }
    }

以上で実装についての説明は終わりです。実際に実行すると、図11のように、エージェントが群れをつくって動き出します。ここで説明したBoidsアルゴリズムは出来るだけ単純な実装を心掛けています。実際には、他にもパラメータを設定して、動きの調整が必要になるでしょう。

f:id:koki0702:20141120144403p:plain

図11.UnityでのBoidsアルゴリズム:群れをなして動きだす

1.3.2 Shaderを用いた“揺れる”動き

ここでは、草花の風に揺れる動きをUnityでどのように表現するか、ということについて解説します。具体的には図12に示すような、風で周期的に揺れ動く葉っぱの表現を対象とします。「天才けんけんぱ」では、通常の“揺れ”に加えて、人が踏んだときだけ揺れの度合いを強めることで、踏まれたときのアクションも追加して実装しています。

f:id:koki0702:20141120144405p:plain

図12.風に揺れ動く葉っぱの動きをShaderを用いて表現する

上の図のような周期的な動きをUnityで実装する方法はいくつかありますが、ここではUnityの「Surface Shader」を用いる方法を見ていきます。Shaderとは、GPU上で計算されるプロセスをカスタマイズできる機能です。Shaderを用いることでレンダリング(描画)方法を細かくカスタマイズすることができ、多彩な表現を可能とします。

Shaderを使ってできることは、簡単に言うと、「色の塗り方」と「3Dモデルの頂点の場所」を自分好みに変更できることです。今回の場合、「3Dモデルの頂点の場所」を周期的に動かすことで、風に揺れ動く草葉を表現したいと思います。具体的には、草の3Dモデルは図12に示すように、板状の面にテクスチャが貼られた形になります。この3Dモデルの各頂点(バーテックス)を時間とともに、場所に応じて位置を変更します。

f:id:koki0702:20141120144407p:plain

図12.茂った草の3Dモデル:右図は使用したテクスチャ

それでは、GrassWave.shaderという名前のSahderを作成します。GrassWave.shaderの中身を後ほど示すコードの通り記述すれば、マテリアルのシェーダを、図13のようにドロップダウンメニューから選ぶことができます。

f:id:koki0702:20141120144408p:plain

図13.GrassWaveシェーダを適用する

Shader "GrassWave" {
Properties {
    _Color ("Main Color r:ampl g:speed b:time", Color) = (1,1,1,1)
    _MainTex ("Base (RGB) Trans (A)", 2D) = "white" {}
    _Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
    
    _WaveCycle ("Wave Cycle", Range(0.0,5.0) ) = 1.0
    _WaveAmount ("Wave Amount", Range(0,0.1) ) = 0.02
}

SubShader { 
    Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout" }
    LOD 200
    Lighting Off
    Cull Off
        
        
CGPROGRAM
#pragma surface surf Lambert alphatest:_Cutoff vertex:vert

sampler2D _MainTex;
float4 _Color;
float _WaveCycle;
float _WaveAmount;

struct Input {
    float2 uv_MainTex;
    fixed4 color : COLOR;
};

void surf (Input IN, inout SurfaceOutput o) {
    half4 c = _Color * tex2D(_MainTex, float2(IN.uv_MainTex));
    o.Albedo = c.rgb;
    o.Alpha = c.a;
}

void vert (inout appdata_full v) {
    float4 p = v.vertex;
    
    float sy = p.y;
    p.x += sin((_Time.y * _WaveCycle)) *  _WaveAmount * sy;
    p.z += cos((_Time.y * _WaveCycle)) *  _WaveAmount * sy;

    v.vertex = p;   
}   

ENDCG
}
}

ここで大切なポイントはvertいう関数です(このvertという名前は変更してかまいません。その場合、「#pragma … vertex:vert」の部分を適宜変更します。)。このvertという関数内で頂点の位置を調整することができます。上のコードでは、v.vertexに3Dモデルの各頂点の座標値が格納されています。そして、このv.vertexの値を変更することで、各頂点の位置を変更することができます。

上のコードでは、各頂点のx座標とz座標の位置をsin・cos関数で円運動するように動かせています。ここでTime.yはビルトイン変数であり、シェーダ内でアニメーションをするために使用することができます。WaveCycleとWaveAmountは、自分で定義したプロパティです。WaveCycleは動きの周期スピード、_WaveAmountは動きの量を調整するための変数です。

これをUnityで実行すると、草のモデルは風で揺れるような周期的な動きをします。また、周期スピードと動きの量を調整することで、人に踏まれた場合の動きを追加することができます。

以上で「Shaderを用いた“揺れる”動き」の説明は終わります。ここではShaderの詳細な説明は省略しましたが、是非時間をとっってShaderについて勉強することをおすすめします。Shaderが使えるようになると、多彩な表現が可能になります。Shaderについて体系的に学びたい場合は、『Unity Shaders and Effects Cookbook[7]』や『The Cg tutorial[8]』などの書籍を参考にして下さい。

「天才けんけんぱ」:Unityとセンサーの連携(1/4)

本記事は次に示す4つのページから構成されています。
「天才けんけんぱ」:Unityとセンサーの連携(1/4)
「天才けんけんぱ」:Unityとセンサーの連携(2/4)
「天才けんけんぱ」:Unityとセンサーの連携(3/4)
「天才けんけんぱ」:Unityとセンサーの連携(4/4)

「天才けんけんぱ」:Unityとセンサーの 連携

f:id:koki0702:20141120144344p:plain

1.1 概要

本章では、「天才けんけんぱ」という子供向けのアトラクションを例にとって、Unityに関連した技術の解説を行います。特に、人の動きにインタラクティブに反応する部分である「Unityとセンサーの連携」を主題としています。また、ワクワクする体験を演出するためUnity上で工夫した点などをピックアップして解説していきます。

1.1.1 天才けんけんぱ

それでは「天才けんけんぱ」の説明から始めたいと思います。このアトラクションは人の足に反応するインタラクティブなシステムです。床面にセンサーを設置し、プロジェクターで床面に映像を投影しています。投影される映像は、図1のように小川の上にボードが浮いているような世界で、ボードの上を“けんけんぱ”して遊ぶことを想定しています。

図1.小川の上にボードが浮いていて、その上を“けんけんぱ”して遊ぶ。

本アトラクションのコンセプトは「未来のけんけんぱ」です。「けんけんぱという昔からある遊びを、現代風にアレンジするとどうなるだろうか?」---このプロジェクトはそういうところからスタートしました。そして、この「天才けんけんぱ」がその問に対する一つの試みだと言えます。

本アトラクションの特徴は「バーチャル(Vritual)」であることです。小川の上で“けんけんぱ”するという体験はバーチャルならではのものでしょう。また、ボードを踏んだときの動作に音やエフェクトで演出を付け足すことや、コースの難易度(ボードの位置)を動的に変更することなど、バーチャルならではの利点があります。

1.1.2 全体構成

全体の構成は図2のようになります。図に示した通り、1台のPCにプロジェクターとセンサーが接続されています。センサーには「測域センサー」を使用しています(「測域センサー」の詳しい説明は後ほど行います)。測域センサーを適切な場所に設置し、人の足の位置をリアルタイムに取得します。

f:id:koki0702:20141120144347p:plain

図2.全体の構成図:PCにプロジェクターとセンサーを接続

図2で示した全体の構成図は、プロジェクターを1台だけ使用した場合のものです。物理的に距離の長いコースを作りたいのであれば、プロジェクターの台数を増やすことで、投影面積を広くすることも可能です。この場合、必要に応じてセンサーの台数も増やす必要があります。また、1台のPCで行うためには、PCの映像出力ポートがプロジェクターの台数分必要になります。そうでなければ、PCを増やす必要があり、PC間の連携を考慮して開発を行わなければならないため、システムが複雑になるでしょう。

「天才けんけんぱ」:Unityとセンサーの連携(2/4)

本記事は次に示す4つのページから構成されています。
「天才けんけんぱ」:Unityとセンサーの連携(1/4)
「天才けんけんぱ」:Unityとセンサーの連携(2/4)
「天才けんけんぱ」:Unityとセンサーの連携(3/4)
「天才けんけんぱ」:Unityとセンサーの連携(4/4)

1.2 Unityとセンサーの連携

現実世界のモノの動きをコンピュータに認識させるためにはセンサーが必要です。センサーが人の動きなどを計測し、そのデータをUnity側のプログラムに渡すことで、インタラクティブに反応するシステムを構築することができます。

ここではソフトウェアの構成として、図3に示すように、Unityで開発されたプログラムとセンサー用のプログラムの二つが実行されている環境を想定します(センサー用プログラムはC++などの言語で開発することが多いでしょう)。また、その二つのプロセス間で通信を行うためにOSC(OpenSound Control)[1]と呼ばれる通信プロトコルを使用します(OSCについては後ほど説明します)。

f:id:koki0702:20141120144348p:plain

図3.センサー用プログラムはOSCという通信プロトコルを介して、Unity側に情報を送信する

もちろん、センサーを取り付けただけでは何も解決されません。センサーから取得できるのは生のデータです。生データに手を加えて、必要な情報を抽出する作業は、各自で開発しなければなりません。本章ではセンサー側の実装については省略しますが、インタラクティブなシステムにおいては必須な作業になります。

1.2.1 センサーの選択

センサーには様々なモノがあります。Webカメラ、距離センサー、温度センサー、圧力センサー、また、最近だとKinectやXtionなどのような深度センサーも一般的になってきています。どのセンサーを使うかにあたって、いくつか基準があると思います。要求を達成できることはもちろんのこと、性能やコスト、業務用かどうかなど、いくつかの指標を天秤にかざして最適と思われるセンサーを選択する必要があります。

ここでの目標は、「人の足の位置をリアルタイムに取得する」ということです。そのために、本プロジェクトでは測域センサーを採用しました。次節では測域センサーについて説明を行います。

1.2.2 測域センサー

測域センサーは「レーザ・レンジ・スキャナー( Laser Range Scanner)」とも呼ばれ、自立型ロボットのセンサーとしてよく用いられます。測域センサーを用いることで、周囲の物理的な形状を計測することができます。今回使用したセンサーは一軸走査型の測域センサーです。図4に示すように、平面上に走査線を発し(図の赤いライン)、その光の飛行時間から、遮蔽物までの距離を計測することができます。

f:id:koki0702:20141120144350p:plain

図4.測域センサーのイメージ図:光の反射時間から距離を計測する

今回の例で言えば、床面に測域センサーを設置することで、センサーから人の足までの距離を計測することができます。走査線は角度が180度超えて放射され、検出エリアも広くカバーすることができます。業務用のため、コストはかかりますが、今回の用途には適していると言えるでしょう。

測域センサーを使用することで、極座標系で周囲の距離を取得できます(「45度の方向に130cm進んだ場所に遮蔽物がある」といった情報が取得できます)。この取得データに対して閾値処理を行い、極座標系データを直行座標に変換することで、xy座標系での遮蔽物の位置を求めることができます(極座標系を直行座標系に変換するには、単純な計算で行えます。このような機能は、測域センサーを提供しているベンダーがアプリケーションとして提供しているかもしれません)。しかし、このセンサーが計算したxy座標系の値をそのまま使うことはできません。センサーの座標系とプロジェクターが出力するUnity上での座標系との対応関係を考慮して変換する必要があります。

1.2.3 キャリブレーション

センサーの座標系をUnity上での座標系に対応させる必要があります。このような対応関係を求めることを「キャリブレーションを行う」と言います。通常「キャリブレーション」とは、機器固有のパラメータを求めることを言いますが、今回のように複数の座標系の対応関係を求めるときにも「キャリブレーション」という言葉を用います。

ここではキャリブレーションについて簡単な説明を行います。やりたいことは、センサーの座標系とUnityのピクセルでの座標系との対応関係を求めることです。たとえば、センサーから見てユーザの足がxy座標で(103cm, 53cm)の位置は、Unityのピクセル座標では(354px, 524px)に対応する、といったような対応関係を求めることが目標になります。

今回の問題では、センサーが計測する平面上の点とプロジェクターが投影する映像の平面上の点との対応関係を求めることになります。この平面上の点の変換は「ホモグラフィ変換」と言います。このホモグラフィ変換行列を求めれば、センサー座標系をUnity座標系に変換することができます。

ホモグラフィ行列を求めるためには、センサー座標とUnity座標の対応する点が4点以上必要です。この対応点としての4点は、たとえば、プロジェクターの映像が出力する四隅を使うのが簡単でしょう。今、ディスプレイの出力サイズが(横1920px、縦1200px)であったとします。図5に示すように、プロジェクターが出力する映像の四隅にモノを置いて、センサーの座標値を記録すれば、この4点の対応関係からホモグラフィ行列を求めることができます(ホモグラフィ行列を求める計算は、OpenCVなどのライブラリを使うのが簡単です)。

f:id:koki0702:20141120144351p:plain

図5.プロジェクターで投影される映像の四隅において、センサーの座標を計測し、Unity座標とセンサー座標の対応関係を求める。その4箇所の対応関係から、ホモグラフィ変換行列Hを求める。

ホモグラフィ変換行列は3×3の行列になります。この行列が求まったら、センサーの取得した位置データに対してホモグラフィ行列を掛けることで、Unity上での座標系に変換することができます。図6には、具体的な計算フローを示しています。ここでは、センサーの座標系は(x, y)の2次元データなので、3番目の次元に1を追加した(x,y,1)となる行列を用い、ホモグラフィ行列を掛けます。また、最終的な座標は、行列の1,2次元目の値を3次元目の値で割り算した値になります。

f:id:koki0702:20141120144353p:plain

図6.センサー座標系を先ほど求めたホモグラフィ行列を掛け算して、Unity座標系へ変換

ここで4つの対応関係からホモグラフィ行列を求めるためには、たとえば、OpenCVというコンピュータビジョンライブラリのfindHomographyという関数を用いることで求めることができます[2]。ホモグラフィ変換の詳しい解説については『詳解 OpenCV [3]』を参考にして下さい。

以上でUnity座標系におけるユーザ位置を求めることができました。センサー側のプログラムは、変換後の座標値をUnityにOSCで送信します。それでは続いて、OSCによる送受信について解説します。

1.2.4 OSC

二つのプロセス間で情報をやりとるする必要がある場合、特定の通信プロトコルにしたがって情報を送受信する必要があります。インタラクティブなシステムにおいて、リアルタイム性が重視される場合、OSCを用いることが多くあります。OSCはネットワークの通信プロトコル上(UDPTCPなど)で動作し、異なるプロセス間で通信することができます。一台のPC内でのプロセス間通信はもちろんのこと、インターネットを介して通信することも可能です。

それでは、Unity上でOSCを用いて通信するプログラムについて説明していきます。現在、UnityのOSC用ライブラリはいくつか用意されています。ここでは、UnityOSC[4]と呼ばれるライブラリを用います。UnityOSCはgithubからダウンロードできます。ファイルを展開すると、「UnityOSC/src」というフォルダが現れるので、そのフォルダを今回使用するUnityのAssets以下に配置します。

f:id:koki0702:20141120144354p:plain

図5.UnityOSCをAssetsフォルダに配置する

続いて、OSCHandler.csファイルの初期化部分に以下を追記します。

public void Init()
{
    //OSCクライアントの初期化
    //CreateClient("KenKenClient”, IPAddress.Parse("127.0.0.1"), 5555);
        
    //OSCサーバの初期化
    CreateServer("KenKenServer”, 6666)
}

OSCを送信する場合はCreateClientという関数を、受信する場合はCreateServerを用います。CreateClientでは引数に「クライアントの名前、送信先のIPアドレス、送信先のポート番号」を取ります。CreateServerでは「サーバの名前、ポート番号」を引数にとります。ここでは、センサーからOSCで情報を受信するため、OSCサーバだけ生成することにします。ちなみに、上の例では6666番のポートで通信を受信するように設定しています。

続いてMyOSCManager.csという名前のファイルを作成し、OSCを受信するコードを書いていきます。初めにStart()関数内に、OSCHandlerを初期化するためのコードを記述します。そして、OSCを受信するコードはUpdate関数内に記述します。OSCHandler.Instance.UpdateLogs()を用いた後、_lastOscTimeStampで記録したOSCを受信したタイムスタンプより新しいデータがある場合、それをコンソールに出力します。

public class MyOscManager : MonoBehaviour {
    private long _lastOscTimeStamp = -1;
    
    void Start () {
        OSCHandler.Instance.Init();
    }
    
    void Update () {
        OSCHandler.Instance.UpdateLogs();
        
        foreach( KeyValuePair<string, ServerLog> item in OSCHandler.Instance.Servers ) {
            for( int i=0; i < item.Value.packets.Count; i++ ) {              if( _lastOscTimeStamp < item.Value.packets[i].TimeStamp ) {
                    _lastOscTimeStamp = item.Value.packets[i].TimeStamp;

                    string address = item.Value.packets[i].Address;
                    int userX = (int)item.Value.packets[i].Data[0];
                    int userY = (int)item.Value.packets[i].Data[1];

                    Debug.Log( address + ":(" + userX + ", " + userY + ")" );
                }                   }                   }       
    }
}

OSCデータの中身は、OSCアドレスとそれに続いてint型やstring型のデータが続きます。ここで想定するデータは、ユーザの足の位置を示すx,y座標がint型で送信されることを想定します。そのため、上記コードではデータ配列の一番目の要素をユーザのx座標値、二番目をy座標値として解釈しています。最後にMyOSCManager.csを空のGameObjectに追加すれば完了です。

それではOSC通信のテストを行うために、テスト用にOSCを送信するプログラムを作成します。ここでは、PythonのsimpleOSCライブラリ[5]を利用して、簡単なテストプログラムを作成します。simpleOSCを使用することで、OSCを送信することができます。

import simpleOSC

simpleOSC.initOSCClient(ip='127.0.0.1', port=6666)
simpleOSC.sendOSCMsg(“/user”, [354, 524])

上に示すPythonのコードでは、初めにIPアドレスとポート番号を指定します。今回はローカルホストを示す127.0.0.1と、先ほどUnity側のプログラムで設定したポート番号6666を指定します。その後で、「/user」というOSCアドレスにデータが[354、524]というint型の数字を送ります。上のテスト用コードを実行すれば、Unity側で受信できているか確認します。通信が成功すれば、図6のように受信した結果が表示されます。

f:id:koki0702:20141120144356p:plain

図6.OSCデータを受信した結果を出力する

以上でOSCの通信部分が完成しました。この受信データを使用して、Unity側で処理を書いていくことができます。たとえば、OSCで取得した位置にエフェクトを発生させたりすることができるでしょう。本アトラクションでは、ユーザ位置とボードの当たり判定を行い、それに応じたエフェクトを発生させています。また、川に波紋を発生させる処理をシェーダで記述したり、その近くにいる生き物が逃げていくようなアクションを起こすような処理も追加しています。