凹みTips

id:hecomi が興味をもった C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

複数の AR マーカを利用した Oculus Rift アプリが作れる Ovrvision SDK v0.5 を試してみた

はじめに

先日は Ovrvision SDK v0.3 についての記事(Oculus Rift x Ovrvision で拡張現実(AR)を試す方法をまとめてみた - 凹みTips)を書きましたが、1ヶ月も立たないうちに次のバージョンである v0.4 がリリースされ、そして今日 4/21 にバグフィックス版であるv0.5 が続けてリリースされました。

v0.4 および v0.5 のリリースの内容は以下のとおりです。

  1. The Configuration XMLfile reading system is now available(.\ovrvision_config.xml).
    • ovrvision_config.xml でカメラの設定(コントラストや露出、樽型の歪みなど)を調整(Unity Pro 版には未同梱)
  2. Fixed a bug in the camera control of Mac OSX.
  3. The OvrvisionSDK can display multiple models using multiple AR markers in the Unity Pro.
    • Unity Pro 版で複数の AR マーカに対応(v0.5 でバグ修正)

やはり 3 がでかいですね!これによって複数の AR オブジェクトを出したり、組み合わせてロバスト性を上げたり、ポジショントラッキングに応用したりといろいろなことが可能になると思います。

また、リリースノートには載っていないのですが、Unite Japan 2014 でお会いした際にお願いさせていただいた歪みなし左右画の取得(v0.3 までは左目のみ取得)も対応されています!

以降は v0.3 の知識を前提として話を進めますので、初めての方は v0.3 の記事に先に目を通していただくと分かりやすいと思います。

デモと解説

今回後半で解説する、左右カメラ画をそれぞれ解析した際の利点について説明しています。


利用方法

上記サイトから Ovrvision SDK for Unity Pro をダウンロードします。中には前回同様 SDKovrvisionsdk.unitypackage とサンプル unity_ar_example に加え、ArUco のマーカ(64 / 56番)が入った marker_sample が同梱されています。まずは ovrvisionsdk_unity/unity_ar_example/Assets/ovrvision_sence.unity を起動してみましょう。

f:id:hecomi:20140421213447p:plain

v0.3 時同様、Hierarchy ビューを見てみると、OVRCameraController(Oculus SDK に含まれるカメラ)、OvrvisionSDK(Ovrvision SDK 本体)がありますが、今回はマルチマーカのハンドルが可能になったため、OvrvisionView OvrvisionView2 が追加されています。そしてこれらには OvrvisionTracker というスクリプトがアタッチされていて、Marker ID フィールドで対象となる ArUco のマーカ番号が設定できるようになっています。サンプルでは、付属のマーカに対応する 64 と 56 が設定されています。


そこで実行してマーカをカメラに見せてみると…、

f:id:hecomi:20140421215016p:plain

2つ同時に表示されます!

コード解説

認識は Ovrvision.cs の Update が起点になります。

Ovrvision.cs
void Update ()
{
	//camStatus
	if (!camStatus)
		return;

	byte[] undis = new byte[ovGetBufferSize()];
	GCHandle undis_handle = GCHandle.Alloc(undis, GCHandleType.Pinned);

	if (go_CamTexLeft == null || go_CamTexRight == null)
		return;

	//Get the camera image.
	ovGetCamImageForUnityColor32 (go_pixelsPointerLeft, go_pixelsPointerRight, undis_handle.AddrOfPinnedObject(), System.IntPtr.Zero);

	//Apply
	go_CamTexLeft.SetPixels32(go_pixelsColorLeft);
	go_CamTexLeft.Apply();
	go_CamTexRight.SetPixels32(go_pixelsColorRight);
	go_CamTexRight.Apply();

	//Ex renderer
	go_ovrvisionEx.Render (undis_handle.AddrOfPinnedObject());
	undis_handle.Free();

	//Key Input
	CameraViewKeySetting ();
}

ovGetCamImageForUnityColor32() が今回アップデートされ、引数には、歪み有左カメラ画、歪み有右カメラ画、歪み無左カメラ画、歪み無右カメラ画(New!)のポインタを突っ込みます。こうして得られた歪み無カメラ画を、OvrvisionEx.Render() に突っ込むことでマーカの解析を行っています。OvrvisionEx 側のコードを見てみます。

OvrvisionEx.cs
private const int MARKERGET_MAXNUM10 = 100; 
private const int MARKERGET_ARG10 = 10; 

public int Render(System.IntPtr pImgSrc)
{
	float[] markerGet = new float[MARKERGET_MAXNUM10];
	GCHandle marker = GCHandle.Alloc(markerGet, GCHandleType.Pinned);

	ovExSetImage (pImgSrc, 640, 480);
	ovExRender ();

	//Get marker data
	int ri = ovExGetData(marker.AddrOfPinnedObject(), MARKERGET_MAXNUM10);

	OvrvisionTracker[] otobjs = GameObject.FindObjectsOfType(typeof(OvrvisionTracker)) as OvrvisionTracker[];
	foreach (OvrvisionTracker otobj in otobjs) {
		for (int i=0; i < ri; i++) {
			if(otobj.markerID == (int)markerGet[i*MARKERGET_ARG10]) {
				otobj.UpdateTransform(markerGet,i);
				break;
			} else {
				otobj.UpdateTransformNone();
			}
		}
	}

	marker.Free ();

	return ri;
}

おおまかな流れは以前と同じですが、ovExGetData による認識の結果、返ってくる値が以前は 0/1 だったのが、v0.4 から見つかったマーカの個数になりました。また v0.5 より、バッファとして渡している第 1 引数、コードで言うところの marker に、複数個分のマーカの情報が入ってくるようになりました!1 つのマーカあたりの情報は 10 個(MARKERGET_MAXNUM10)なので、10 個おきにバッファの中身を見ていけば複数個のマーカ情報がハンドル出来るわけです。そして OvrvisionTracker をアタッチされたスクリプトの番号を順番に見ていって設定された番号を見つけるとオブジェクトに UpdateTransform、見つからなければ UpdateTransformNone で隠すようにしています。これらのコードを見てみます。

OvrvisionTracker.cs
public void UpdateTransform (float[] markerGet, int elementNo) {
	int i = elementNo * MARKERGET_ARG10;
	this.transform.localPosition = new Vector3 (markerGet[i+1],markerGet[i+2],markerGet[i+3]);
	this.transform.localRotation = new Quaternion (markerGet[i+4],markerGet[i+5],markerGet[i+6],markerGet[i+7]);
}

public void UpdateTransformNone () {
	this.transform.localPosition = new Vector3 (-10000.0f, -10000.0f, -10000.0f);
}

marker[0 + 10*n] はマーカ番号が入っていて、marker[1~3 + 10*n] は位置、marker[4~7 + 10*n] は傾きの情報が入っていることが分かります。ここまで分かれば、後は自分のやりたいことに応じてカスタマイズしていけば色々出来そうですね。

左右それぞれの画を別々に認識

前回のエントリでも書いたのですが、現在の SDK では左目のカメラ画を利用して AR オブジェクトの位置と傾きを決定しています。そして Oculus SDK のカメラでこの物体を視ることで立体視を行っています。しかしながら実際の世界を見る画角とバーチャルの世界を見る画角が合わなかったり、マーカの上にぴったり乗っている表現が苦手だったりします。そこで以前やった方法(Oculus Rift でミクさんに画面の中から出てきていただいた:解説 - 凹みTips)のように、左右の目それぞれを解析することでこの問題を解決してみます。

スレッド切ったりとまじめにやると長くなるので、最小限の手順だけ紹介します。

まず、左目の映像と右目の映像を分けるためにレイヤを作成します。「OvrArObjLeft」「OvrArObjRight」などと適当に名前をつけて、11 番目 / 12 番目のレイヤとしておきます。次に、OvrvisionView をコピーして同じものを 2 つ作成し、OvrvisionViewLeft / OvrvisionViewRight などとリネームしてそれぞれを OvrArObjLeft / OvrArObjRight なレイヤにしておきます。

f:id:hecomi:20140421234115p:plain

そして OVRCameraController の CameraLeft / CameraRight のカリングマスクに OvrArObjLeft / OvrArObjRight を追加します。

f:id:hecomi:20140422004455p:plain

これでそれぞれのカメラから左右それぞれのオブジェクトしか見えないようになります。

次に、スクリプト側で左目の映像だけでなく右目の映像も解析するようにします。

Ovrvision.cs
void Update ()
{
	//camStatus
	if (!camStatus)
		return;

	byte[] undis_left  = new byte[ovGetBufferSize()];
	byte[] undis_right = new byte[ovGetBufferSize()];
	GCHandle undis_left_handle  = GCHandle.Alloc(undis_left,  GCHandleType.Pinned);
	GCHandle undis_right_handle = GCHandle.Alloc(undis_right, GCHandleType.Pinned);

	if (go_CamTexLeft == null || go_CamTexRight == null)
		return;

	//Get the camera image.
	ovGetCamImageForUnityColor32 (
		go_pixelsPointerLeft, go_pixelsPointerRight, undis_left_handle.AddrOfPinnedObject(), undis_right_handle.AddrOfPinnedObject());

	//Apply
	go_CamTexLeft.SetPixels32(go_pixelsColorLeft);
	go_CamTexLeft.Apply();
	go_CamTexRight.SetPixels32(go_pixelsColorRight);
	go_CamTexRight.Apply();

	//Ex renderer
	go_ovrvisionEx.Render (undis_left_handle.AddrOfPinnedObject(),  11);
	go_ovrvisionEx.Render (undis_right_handle.AddrOfPinnedObject(), 12);
	undis_left_handle.Free();
	undis_right_handle.Free();

	//Key Input
	CameraViewKeySetting ();
}

右カメラ画用のバッファを追加して、ovGetCamImageForUnityColor32() の第 4 引数で渡すようにします。そして、OvrvisionEx.Render() にそれぞれのカメラ画を投げるようにして、それと共にどのレイヤのオブジェクトを処理するかを第 2 引数で伝えられるようにしています。

OvrvisionEx.cs

レイヤを受け取れるように改造した Render を見てみます。

public int Render(System.IntPtr pImgSrc, int layer)
{
	float[] markerGet = new float[MARKERGET_MAXNUM10];
	GCHandle marker = GCHandle.Alloc(markerGet, GCHandleType.Pinned);

	ovExSetImage (pImgSrc, 640, 480);
	ovExRender ();

	//Get marker data
	int ri = ovExGetData(marker.AddrOfPinnedObject(), MARKERGET_MAXNUM10);

	OvrvisionTracker[] otobjs = GameObject.FindObjectsOfType(typeof(OvrvisionTracker)) as OvrvisionTracker[];
	foreach (OvrvisionTracker otobj in otobjs) {
		if (otobj.gameObject.layer == layer) {
			for (int i=0; i < ri; i++) {
				if(otobj.markerID == (int)markerGet[i*MARKERGET_ARG10]) {
						otobj.UpdateTransform(markerGet,i);
					break;
				} else {
					otobj.UpdateTransformNone();
				}
			}
		}
	}

	marker.Free ();

	return ri;
}

レイヤが一致するときだけ処理するようにしただけです。

これで冒頭のデモ動画の後半のような感じになります。上記処理は Transform を更新する以外は全て Unity のアップデートと非同期で行えるので、Thread を使ってかなり高速化出来ると思います(別途記事を書きます)。

おわりに

簡単に視野角全体を使った AR の立体視が出来るようになりました。触った感じですが、Ovrvision の解像度が高い点と ArUco が優秀なおかげか、以前やっていた PS Eye x ARToolKit とくらべて綺麗で追従性の高い立体視が出来るように感じます。マーカも複数個ハンドル出来るため、マーカ毎に色々なコンテンツを出したりロバスト性を上げたりと色々なことが出来そうです!

Unity から Node.js を裏でこっそり立ち上げてアレコレ出来るアセットをつくってみた

はじめに

先日書いたエントリ(Unity から Node.js を起動時に裏で実行・通信して諸々の処理を肩代わりしてもらう方法考えてみた - 凹みTips)のネタをだれでも使えるようにアセットにしました。起動時に Node.js を裏で実行して面倒なネットワーク周りの処理などを行い、Unity とは OSCWebSocket を通じてやりとりすることで、C# で書くのが面倒な処理を JavaScript(≠ Unity の JS)で肩代わりできるものです。別途サーバを立ち上げる方式よりもアプリを立ち上げれば自動で Node のサーバも立ち上がるので楽、というのが狙いです。

動作環境

両対応しました。

デモ

実行した結果のログなどは簡易的ですが Editor 上で見れるようになっています。

動画では WebSocket サーバの立ち上げと、それを利用した HTML ファイルをホストする処理を行っていて、以下の様なスクリプトを起動時に裏で走らせています。

Assets/StreamingAssets/.node/websocket.js
var http = require('http');
var fs   = require('fs');

var server = http.createServer(function(req, res) {
    fs.createReadStream('./websocket.html').pipe(res);
}).listen(8080);

var io = require('socket.io').listen(server);

io.sockets.on('connection', function (socket) {
    socket.emit('server/hello', { from: 'server' });
    socket.on('browser/move', function (data) {
        io.sockets.emit('browser/move', data);
    });
});

デモでは WebSocket でマウス座標送っているだけですが、Twitter のストリーム取ってきて整形したデータを渡すとかすれば楽に色々出来ると思います。

ダウンロード

unitypackage を開いて下さい。原因は不明ですが Mac でフリーズする時がある気がします(結構します)。。強制終了した際は、前回の Node.js のプロセスが生きているかもしれないので ps コマンドで調べて kill して下さい。。Win 版は安定していると思います。

使い方

NodeJs スクリプトを空のオブジェクトにアタッチすれば、Script Path で指定した js(StreamingAssets/.node/ 以下のパス)を裏で実行します。サンプルを実行するためには npm からモジュールをインストールしてビルドする必要があるので、Assets/StreamingAssets/.node 以下でコマンドを実行して下さい。

$ cd Assets/StreamingAssets/.node
$ npm install

ただ、ビルドには Node.js の環境とビルド環境(Win なら Visual StudioMac なら Xcode で command line tools)が必要です。

仕組み

前も書きましたが図に起こしてみるとこんな感じです。
f:id:hecomi:20140420161520p:plain
なので改変すれば Node.js じゃなくて何でも動かしておけると思います。

隠しフォルダとビルドについて

.hogehoge という "." プレフィックスがつくディレクトリは Win でも Mac でも Unity によってハンドルされません。そこで、「.node」というディレクトリを Streaming Assets 以下に作り、そこに js や Node.js のモジュールを入れます。これによって Unity に js がコンパイルされ、パースエラーで怒られる、といったことがなくなります。

しかしながら、ビルド時にこれが問題になってきます。Windows では問題なく Streaming Assets の中身がビルド後のディレクトリへすべてコピーされるのですが、Mac だとコピーされないようです。そこでビルドプレイヤーパイプラインを利用して、ビルド後のポストプロセスとして .node ディレクトリをビルド後の Streaming Assets ディレクトリへコピーするようにしています。

Editor/PostprocessBuildPlayer

#!/usr/bin/perl

my $installPath = $ARGV[0];
my $node_dir    = "Assets/StreamingAssets/.node";
my $target_dir  = "$installPath/Contents/Data/StreamingAssets/.";

system "cp -r $node_dir $target_dir";

上記では Perl でコピーしてますが、Unity からは単に実行するだけなので Shebang で指定さえすればシェルスクリプトだろうがなんだろうが大丈夫なようです。

サンプルで使用しているライブラリについて

おわりに

面白い体験を如何に早く作るかが問われる時代だと思うので、要所要所に使いやすいツールを使用するのは重要だと思います。ただ別のサーバをいちいち立ち上げるのは面倒...、という時にこういう解決法も有りかな、と思いました。

Unity でシェーダを使って 20,000 人が音楽に合わせてサイリウム振ってる様子を作ってみた

はじめに

以前、Unityシェーダのエントリ(Unity のシェーダの基礎を勉強してみたのでやる気出してまとめてみた - 凹みTips)を書いた際に @yuujii さんからこんなご提案を頂きました!


面白そうなのでやってみました。

デモ

低音域・中音域・高音域を青・緑・赤にマッピングして、ボリュームに合わせて振り幅が変わり、音が大きくなると横振りから縦振りに変わります。サイリウムはシェーダで動いているので 20,000 人出してもかなり軽いです。音からテンポを検出するのは難しそうなのでやっていません。。


音楽は H/MIX GALLERY さまから「砂塵の城塞」をお借りしました。壮大です。

f:id:hecomi:20140419220924j:plain

ダウンロード

デモと全く同じものが入っています。

解説

簡単にまとめると、以下の様な感じです。

  1. 外部から与えられたパラメタを元に頂点シェーダでイイ感じに振ってるように見せてくれるシェーダを書く
  2. 観客席を Blender で作成してインポート
  3. Particle System の Shape でインポートしたメッシュを適用
  4. Renderer でマテリアルに先ほどのシェーダを適用したマテリアルを設定
  5. 音楽を AudioSource.GetSpectrumData で解析して音量・周波数を取得
  6. シェーダのパラメタに解析した値を入力
シェーダについて

シェーダのコードを貼るとこんな感じです。

Shader "Custom/Cyalume" {
    Properties {
        _BaseColor ("Base Color", Color) = (0.0, 1.0, 0.0)
        _WaveFactorX("Wave Factor X", Range(0.0, 2.0)) = 0.0
        _WaveFactorZ("Wave Factor Z", Range(0.0, 2.0)) = 0.0
        _WaveCorrection("Wave Correction", float) = 0.3
        _Pitch("Wave Pitch", float) = 1.0
        _Delay("Delay by Distance", float) = 0.02
        _Bend("Bend", float) = 0.3
        _MainTex ("Base (RGB)", 2D) = "white" {}
    }
    SubShader {
        Tags {
            "Queue"           = "Transparent"
            "RenderType"      = "Transparent"
        }
        Blend SrcAlpha One
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #define PI 3.14159

            #include "UnityCG.cginc"

            uniform float _WaveFactorX;
            uniform float _WaveFactorZ;
            uniform float _WaveCorrection;
            uniform float _Pitch;
            uniform float _Delay;
            uniform float _Bend;
            uniform float _WaveFactorY;
            uniform float4 _BaseColor;
            uniform sampler2D _MainTex;

            struct v2f {
                float4 position : SV_POSITION;
                fixed4 color    : COLOR;
                float2 uv       : TEXCOORD0;
            };

            v2f vert(appdata_full v) {
                float wave   = 2 * PI * _Time.x * 1000 / 60 / _Pitch;
                float delay  = _Delay * v.vertex.z;
                float bendX  = _Bend * v.texcoord.x;
                float angleX = wave + delay + bendX;
                float bendY  = _Bend * v.texcoord.y;
                float angleY = wave + delay + bendY;
                float bendZ  = _Bend * v.texcoord.z;
                float angleZ = wave + delay + bendZ;
                float lean   = sin((v.texcoord.y + 0.5) * PI) - 1.0;

                v.vertex.x += _WaveFactorX * sin(angleX) * lean;
                v.vertex.y += (_WaveFactorX + _WaveFactorZ) * _WaveCorrection * (1.0 - pow(cos(angleX), 2.0)) * lean;
                v.vertex.z += _WaveFactorZ * sin(angleZ) * lean;

                // Parameters given to fragment shader
                v2f o;
                o.position = mul(UNITY_MATRIX_MVP, v.vertex);
                o.uv       = v.texcoord;
                o.color    = v.color;
                return o;
            }

            fixed4 frag(v2f i) : COLOR {
                fixed4 tex = tex2D(_MainTex, i.uv);
                tex.rgb *= _BaseColor * i.color.rgb;
                tex.a   *= i.color.a;
                return tex;
            }
            ENDCG
        }
    }
    Fallback "VertexLit"
}

f:id:hecomi:20140419214021p:plain

ちょっと長いですがミソは3行です。

v.vertex.x += _WaveFactorX * sin(angleX) * lean;
v.vertex.y += (_WaveFactorX + _WaveFactorZ) * _WaveCorrection * (1.0 - pow(cos(angleX), 2.0)) * lean;
v.vertex.z += _WaveFactorZ * sin(angleZ) * lean;

本当は回転させるべきだと思うのですが、手抜きで「」な形状を「」になるように、X 方向、Z 方向それぞれに対して変形しています。一番下の頂点は固定して上の頂点に行くほど線形にずらせば正方形のポリゴンが平行四辺形に変形されます。この辺りの計算については以下の記事が詳しいです:

Particle System によるパーティクルのアニメーション

これを Particle System を使って大量生産します。アニメーションするシェーダを Particle System をと組み合わせて使うイケてるアイディアは以下を参考にしています(ありがとうございます)。

Particle System ではパーティクルの発生源の形状を Shape から Sphere や Cone などで指定できますが、Mesh を指定して好きな形から発生させることができます。

f:id:hecomi:20140419214817p:plain

そこで、観客席っぽいポリゴンを Blender で作成してこれを Mesh に指定しました。

f:id:hecomi:20140419214914p:plain

そして、Renderer の Material で先ほどのシェーダを設定したマテリアルを指定します。Render Mode では処理が軽いので Billboard(常にカメラの方を向く板)を指定しています。

f:id:hecomi:20140420124501p:plain

音の解析

低音や高音に応じて効果を加えたかったので音を周波数に分解(FFT)する必要があります。これは AudioSource.GetSpectrumData() を利用すると簡単に実現できます。

上記ドキュメントのサンプルの使い方は obesolete で警告が出るので、以下のようにバッファは予め確保した使い方をしましょう。

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour {
    void Update() {
    	var spectrum = new float[1024];
        audio.GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris);
        ...
    }
}

手抜きですが音量もここから取ってしまいます。正確には AudioSource.GetOutputData() で取った波形から計算するのが良いと思います。Unity Answers の以下のスレッドが参考になります。

シェーダにパラメタを渡す

そしてこれらをシェーダの入力値として渡します。Material の GetFloat() / SetFloat() や GetColor() / SetColor() などの Getter / Setter を通じてやりとりが出来ます。

public class CyalumeController : MonoBehaviour
{
	public Color baseColor {
		get { return renderer.material.GetColor("_BaseColor"); }
		set { renderer.material.SetColor("_BaseColor", value); }
	}

	public float waveX {
		get { return renderer.material.GetFloat("_WaveFactorX"); }
		set { renderer.material.SetFloat("_WaveFactorX", value); }
	}
	...
}

これで冒頭のデモができます。

おわりに

パーティクルと組み合わせたシェーダによるアニメーションを書いてるとちょっと数式間違えた時もキレイな感じのエフェクトになったりして楽しいですし、もっと色々な表現ができると思います。今回のように、何かに特化した表現を作りこんであげたりすれば、軽い処理で結構面白いことが色々出来るのではないでしょうか。