凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Ovrvision SDK v2 x Oculus Rift DK2 で AR を楽しむ方法についてまとめてみた

はじめに

Oculus Rift と組み合わせて立体視可能なスルー画を楽しめるステレオカメラOvrvision の待望の DK2 版マウンタ及び SDK が発売・公開され、先週予約していたものが届きました。

Ovrvision 1 : Stereo camera for Oculus Rift

Ovrvision 1 : Stereo camera for Oculus Rift

Ovrvision 1 : for Oculus Rift DK2 Cover

Ovrvision 1 : for Oculus Rift DK2 Cover

Ovrvision は 60 fps640x480x2 な広角ステレオカメラで、DK1 の頃も SDK を利用しながら AR と組み合わせて色々と楽しむことが出来ました。

PS Eye で自作していた頃と比較すると格段に楽になり、また軽量な AruCo を利用した AR を簡単に扱うことのできるマルチプラットフォームSDK も用意されていることから、安価で使いやすいステレオ視可能な AR 環境を手に入れることが出来ます。ハンズオンから SDK の利用、高速化あたりのノウハウ、今後の情報などについてまとめてみました。

環境

外観・組み立て

Ovrvision1 を購入するとパーツが届きますので、まずは組み立てが必要です。組み立てに必要な情報は以下にあがっています。

Ovrvision は以下の様な円筒の箱に入って届きます。この箱は後でサンプルアプリで利用するので捨てないようにしましょう。

f:id:hecomi:20141122153131j:plain

中には本体、DK2 マウンタの土台およびカバー、組み立て用のドライバおよびネジ(短4本、長4本)、ケーブルが同梱されています。

f:id:hecomi:20141122153222j:plain

ネジ穴が左右対称なので上下間違いやすいですが、Oculus Rift DK2 に取り付けた際に DK2 の USB ポートと同じ側に Ovrvision1 の USB miniB 口が来るような向きで取り付けましょう。基本的には公式の動画を参照すると良いと思います。

f:id:hecomi:20141122184413j:plain

完成するとこんな感じになります。ケーブルも本体で完結するのでとてもシンプルです。

f:id:hecomi:20141122193209j:plain

なお、Oculus Rift DK2 のケーブル結束部に 5V 電源をつないであげないと DK2 の USB ポートにつないだデバイスが認識されないので注意してください。

サンプルについて

Ovrvision SDK for Unity

後述します。

Ovrvision SDK for Windows

ダウンロードして bin フォルダを見てみると、x86x64 向けにそれぞれダイナミックリンク用に *.dll 及び *.lib が付属していますので、以前と同様に C++ でソフトを作成することが可能です。また C# 用の DLL やサンプルも付属しています(ただし exe は画の同期ずれが起きていました...)。詳細は以下のページをご参照下さい。

x64 フォルダにはビルド済みの exe が入っているのでクリックすると以下の様な画面が開きます。Extend モードにして頑張って DK2 側へウィンドウを持っていけば立体視出来ます。

f:id:hecomi:20141125153631p:plain

これを利用して本体のカメラを回してピント調節をすると良いと思います。また、A キーを押下すると AR モードになって、ArUco 用のマーカを見せると番号と座標を dump してくれます。

f:id:hecomi:20141125153752p:plain

Mac についてはまだ見ていませんがだいたい同じものと思われます。

Calibration ツールについて

カメラの歪み補正を行うキャリブレーションツールtools/ovrvision_calibration.exe として付属しています。これを実行すると以下の様なウィンドウが表示され、「Open Ovrvision」 > 「Start Calibration」に従って 25 枚のまんべんなく画面を覆うようなチェックボードの写真を撮ります。

f:id:hecomi:20141125142602p:plain

撮り終わると自動的に計算が走り、キャリブレーションデータが PC に保存されます。ドキュメントが見当たらなかったのでおそらくですが、C:\Users\<USER_NAME>\AppData\Local\Temp\ovrvision_conf2.xmlキャリブレーション結果を保存しているファイルだと思われます。

AR のサンプル

以下のページからクエリちゃんの AR サンプル / Ovrvision ケースのテクスチャを変更するサンプルをダウンロードできます。

Ovrvision が入っていた円筒形のケースの天面をなるべく明るい状態で近づいてじっと見ているとクエリちゃんが出てきて喋りながら動いてくれたり、筒のテクスチャが十字キーで変更できたりします。

f:id:hecomi:20141125154648p:plain

f:id:hecomi:20141125185041p:plain

左目はバッチリなのですが右目のズレが目立ちますね...(処理自体は左の画像をベースに行っているので)。カメラの向きが調整・固定出来たら良いのですがどなたか良い方法確立したら教えて下さい。

サンプルについては今後も随時拡充していくとのことです。

Unity サンプルについて

取り敢えず使ってみる

以下のページからダウンロードします。

ovrvision_unity_includeOVR.unitypackage を開くと現在のプロジェクトへインポートされます。そこに含まれている ovrvision_scene を開くと以下の様なシーンが開きます。

f:id:hecomi:20141125161935p:plain

OvrvisionViewObject にアタッチされている OvrvisionTrackerMarker ID に該当する番号の ArUco マーカを見つけると OvrvisionViewObject に入っているオブジェクトがそのマーカの上に重畳される形になります。

f:id:hecomi:20141125164350p:plain

自分の好きなオブジェクトにしてあげれば色々なものを表示できます。取り敢えずはダウンロードしたフォルダの中にある marker_sample に 56 番と 64 番のマーカが入っていますのでこれを利用すると良いと思います。それ以外の番号のマーカについては自前で AruUco をビルドするか、izm さんのブログから入手できるビルド済みのバイナリを利用するのが良いと思います。

複数の AR オブジェクトを表示

OvrvisionViewObject をコピーして複数のトラッカーを作成してあげれば同時に複数のオブジェクトを表示することも出来ます。

f:id:hecomi:20141125165810p:plain

カメラ画と AR オブジェクトの焦点がずれるのを調整

内部的には 20 cm のマーカを仮定して計算しているようなので、マーカのサイズが小さいと視差のズレが目立ってしまいます。これはそれぞれのカメラ画から得られたマーカの姿勢を使ってそれぞれのカメラ画用に AR オブジェクトを別々にレンダリングすることで、ぴったりあった画を作成できますが*1、異なるオブジェクトを扱うのがコスト高なので、出来ることなら避けたいです。公式ではマーカを物理的に大きくするかカメラを動かすことを推奨しています。

2014/11/25 現在だと古い SDK の情報なので今回直す方法を紹介します。OVRCameraRig オブジェクトにアタッチされている OVRCameraRig.cs を開き、UpdateAnchors() を見てみます。

private void UpdateAnchors()
{
    OVRPose leftEye = OVRManager.display.GetEyePose(OVREye.Left);
    OVRPose rightEye = OVRManager.display.GetEyePose(OVREye.Right);

    //leftEyeAnchor.localRotation = leftEye.orientation;
    //centerEyeAnchor.localRotation = leftEye.orientation; // using left eye for now
    //rightEyeAnchor.localRotation = rightEye.orientation;

    leftEyeAnchor.localPosition = new Vector3(0.0f,0.0f,0.0f);
    centerEyeAnchor.localPosition = new Vector3(0.0f, 0.0f, 0.0f);
    rightEyeAnchor.localPosition = new Vector3(0.05f, 0.0f, 0.0f);
}

ここではオリジナルの OVRCamearRig.cs のコードが編集されて、localPosition が固定値となっています。ここで rightEyeAnchor.localPositionx を変更してあげることで視差を調整することが出来ます。例えば上の一連のスクリーンショットでは 0.05f0.2f にしています。この設定では目が 20 cm 離れている人間になっているので気持ち悪いですが、そこそこ合うので対症療法的には良いと思います。

高速化について

Deep Profile してみるとテクスチャを撮ってくるところと姿勢の計算部が重い事がわかります。

f:id:hecomi:20141125200230p:plain

ここを例のごとくやっつけですが別スレッド化しました。

これで処理に大分余裕ができたと思います。

f:id:hecomi:20141125200327p:plain

OVR のバージョン

Ovrvision SDK v2.0 では v0.4.3 だったものが、Ovrvision SDK v2.1 より v0.4.3.1 になりました。

以前の問題点の改善

以前の Ovrvision SDK for Unity で発生していた描画順に付随する問題(参考:2014-04-07 - 凹みTips)ですが、今回は専用のレンダリングカメラを用意するわけではなく、Ovrvision で撮ってきた画をテクスチャにしたプレーンを OVRCameraRig に突っ込む形になったため、透明なものを描画しても問題なく描画されるようになっています。

f:id:hecomi:20141125174503p:plain

今後のロードマップ

Ovrvision for Metaio SDK

マーカレスな AR を実現できるように Metaio SDK とのバインディングを作成されているようです。おそらく前述のクエリちゃんのデモはこれを利用しているのではないかと思われます。ただし有償のようです。

Ovrvision Pro

3万円代後半のプロ仕様のモデルが開発される予定とのことです。特徴としては、

  • Oculus Rift DK2(もしくはコンシューマモデル)に取り付け可能なマウンタ
  • 解像度 ⇒ 720p HD(1280x720p) x 2
  • フレームレート ⇒ 60 fps
  • カメラ間同期(ステレオ)⇒ より正確なARに対応
  • ハイダイナミックレンジ対応 ⇒ 暗さに強くなり、白とびも改善
  • カメラ内部へ設定情報を保存する機能(EEPROM)
  • カメラ単独での録画機能
  • 汎用プログラマブルインターフェイス(GPIF)

とのことです。サイト上では 11 月末よりクラウドファンディング開始と書いてありますが、残念ですがこれは 2 月に延期されたようです。

ストレッチゴールも考えられているようなので、是非応援したいです。

その他メモ

  • DK1 の時と比べて上下が反転しているため、DK1 の時に作成したソフトをそのまま動かすと上下逆さな映像になります。
  • Leap Motion の公式マウンタを取り付けている際は干渉しますので外すか別の DK2 を用意する必要があります。
  • DK2 は 75 fps で Ovrvision は 60 fps なので同期が取れていないのですが、ここの処理をどうしているかは未だノーチェックです。

おわりに

この分野はとても面白くて単に AR オブジェクトを表示するだけでなく、人間の行動を支援するみたいな実験も色々と出来ると思いますので、色々とやってみたいと思います。

Unity のオーディオの再生・エフェクト・解析周りについてまとめてみた

はじめに

Unity では単純に音を再生するだけではなくて、エフェクトを掛けたり解析したりする所まで含めて簡単に音を扱える仕組みが整っています。音の解析結果をグラフィカルに出力したり、逆に何らかの入力を音にフィードバックしたりすることもとても簡単に実現できます。

そこで本エントリでは、具体的に何をすれば、またどういうコードを書けばそういったことが実現できるかといった例をまとめてみました。

なお、Unity 5 から導入される Audio Mixer に関しては本エントリの範囲外とします。

オーディオ解析の例

再掲になりますが、本エントリで扱う内容で出来る例は以下になります。

オーディオの仕組みの復習

Unity で音を鳴らす仕組みに関しては、Audio ClipAudio SourceAudio Listener の3つの要素の理解が重要になってきます。

f:id:hecomi:20141111015001p:plain

Audio Clip

Audio Clip は音そのものの情報になります。物理的なものに例えると CD 等のメディアです。ステレオ、モノラルを表す channels や、サンプリング周波数の frequency、そのオーディオファイルの長さの length などがここに含まれています。

Audio Source

Audio Source は Audio Clip を再生する役割をします。物理的なものに例えるとスピーカーです。Audio Clip が 3D サウンド(≠ 2D サウンド)の場合、AudioSource がアタッチされた Game Object の位置から音がなります。音がどのように距離に応じて減衰するかもここで設定できます。音を鳴らす役割のため、保持している Audio Clip の clip、再生中かの isPlaying、ループするかの loop、再生している音量の volume(≠ 局所的な音量)などを変数として持っています。

Audio Listener

Audio Listener は Audio Source を聴く役割をします。物理的なものに例えるとマイクや耳になります。シーンに 1 つだけ存在することが出来るコンポーネントで、基本的にはカメラにくっついています。特別なプロパティはありません。モノラル、ステレオ、サラウンドなのか、といった設定は「Edit > Project Settings > Audio」で Insepctor に表示できる Audio Manager によって設定することが出来ます。

マイク入力を Audio Clip として扱う

音楽ファイルだけではなくてマイク入力も Audio Clip として扱うことが出来ます。これは Microphone を利用します。

適当に録音して再生するサンプルコードは以下になります。

using UnityEngine;
using System.Collections;

// 空の Audio Source を作って置く
[RequireComponent (typeof (AudioSource))]
public class MicrophoneSource : MonoBehaviour 
{
    void Start() 
    {
        // 空の Audio Sourceを取得
        var audio = GetComponent<AudioSource>();
        // Audio Source の Audio Clip をマイク入力に設定
        // 引数は、デバイス名(null ならデフォルト)、ループ、何秒取るか、サンプリング周波数
        audio.clip = Microphone.Start(null, false, 10, 44100);
        // マイクが Ready になるまで待機(一瞬)
        while (Microphone.GetPosition(null) <= 0) {}
        // 再生開始(録った先から再生、スピーカーから出力するとハウリングします)
        audio.Play();
    }
}

これで他の音声ファイルによる Audio Clip と同様に操作・解析できるようになります。

生データの取得

生データはそれぞれのオーディオ要素から取得する API が用意されています。

Audio Clip から取得

AudioClip.GetData() を利用します。第1引数に配列、第2引数にオフセットをセットして第1引数の配列の大きさで指定しただけ生データを取ってきます。

using UnityEngine;
using System.Collections;

public class GetDataSample : MonoBehaviour
{
    public AudioClip clip;
    public int lengthYouWant;

    void Start()
    {
        var data = new float[lengthYouWant];
        clip.GetData(data, 0);
    }
}

現在流れている音でない、音そのものの情報を使って何かするとき(全体の音情報を使って何か事前処理するなど)に使えると思います。

Audio Source から取得

AudioSource.GetOutputData() を利用します。第1引数に配列、第2引数にチャンネル数をセットし、その Audio Source から現在出力されている波形データを取得できます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class WaveOutputter : MonoBehaviour 
{
    private AudioSource audio;
    float[] waveData_ = new float[1024];

    void Start() 
    {
        audio = GetComponent<AudioSource>();
    }

    void Update()
    {
        audio.GetOutputData(waveData_, 1);
    }
}

これで毎フレーム wave_ に現在の波形データが 1024 点分格納されます。試しに波形を出力(描画には Unity の Editor 拡張でインスペクタにグラフを描画する方法を色々調べてみた - 凹みTips を利用)してみるとこんな感じです。

f:id:hecomi:20141109163155p:plain

Audio Listener から取得

上記 Audio Source と同様、Audio Listener でも GetOutputData() を利用することが出来ます。Audio Listener は前述のようにシーンに 1 つしか存在しないことになっているため、static な形で取得することが出来ます。

using UnityEngine;
using System.Collections;

public class WaveOutputter : MonoBehaviour 
{
    float[] waveData_ = new float[1024];

    void Update()
    {
        AudioListener.GetOutputData(waveData_, 1);
    }
}

Audio Listener だと直感的には GetInputData() な感じがしますが、プログラム的には Output なのでこの名前になってるのかな、と思います。

音量の計算

上記例を利用して音量を計算してみましょう。

using UnityEngine;
using System.Linq;

public class WaveOutputter : MonoBehaviour 
{
    private float[] waveData_ = new float[1024];

    void Update()
    {
        AudioListener.GetOutputData(waveData_, 1);
        var volume = waveData_.Select(x => x*x).Sum() / waveData_.Length;
        transform.localScale = Vector3.one * volume;
    }
}

これで音量に応じて Game Object のスケールが変化します。

フーリエ変換による周波数成分の取得

フーリエ変換によって周波数成分を取得できるメソッドとして、AudioSource.GetSpectrumData()AudioListener.GetSpectrumData() が用意されています。後者は GetOutputData() と同じく static なメソッドです。

フーリエ変換は馴染みのない人もいらっしゃると思いますが、音の波を周波数成分に分解してくれる役割をします。今鳴ってる音は高い音が多いのか、低い音が多いのか、ということを調べることが出来ます。

公式のグラフを描画するサンプルを実行してみます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class SpectrumAnalyzer : MonoBehaviour 
{
    private AudioSource audio_;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
    }

    void Update() {
        var spectrum = audio_.GetSpectrumData(1024, 0, FFTWindow.Hamming);
        for (int i = 1; i < spectrum.Length - 1; ++i) {
            Debug.DrawLine(
                    new Vector3(i - 1, spectrum[i] + 10, 0), 
                    new Vector3(i, spectrum[i + 1] + 10, 0), 
                    Color.red);
            Debug.DrawLine(
                    new Vector3(i - 1, Mathf.Log(spectrum[i - 1]) + 10, 2), 
                    new Vector3(i, Mathf.Log(spectrum[i]) + 10, 2), 
                    Color.cyan);
            Debug.DrawLine(
                    new Vector3(Mathf.Log(i - 1), spectrum[i - 1] - 10, 1), 
                    new Vector3(Mathf.Log(i), spectrum[i] - 10, 1), 
                    Color.green);
            Debug.DrawLine(
                    new Vector3(Mathf.Log(i - 1), Mathf.Log(spectrum[i - 1]), 3), 
                    new Vector3(Mathf.Log(i), Mathf.Log(spectrum[i]), 3), 
                    Color.yellow);
        }
    }
}

f:id:hecomi:20141109173523p:plain

順番に、出力されたデータの通常のグラフ、片対数グラフ(x, y)、両対数グラフになっています。FFTWindow はいわゆる GUI の Window ではなく、FFTフーリエ変換)の計算に用いる窓関数を表しています。

適当な周波数帯を区切って平均を取ればレベルメーターみたいなものが作れます。例えば以下のエントリで作っているサイリウム振ってるエフェクトは、流れてる音楽の周波数に応じて色が変わるようにしています。

曲の低中高領域の成分で localScale を適当に変化させるサンプルを書くと、以下のようになります。

using UnityEngine;

[RequireComponent(typeof(AudioSource))]
public class SpectrumAnalyzer : MonoBehaviour 
{
    public int resolution = 1024;
    public Transform lowMeter, midMeter, highMeter;
    public float lowFreqThreshold = 14700, midFreqThreshold = 29400, highFreqThreshold = 44100;
    public float lowEnhance = 1f, midEnhance = 10f, highEnhance = 100f;

    private AudioSource audio_;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
        audio_.Play();
    }

    void Update() {
        var spectrum = audio_.GetSpectrumData(resolution, 0, FFTWindow.BlackmanHarris);
        
        var deltaFreq = AudioSettings.outputSampleRate / resolution;
        float low = 0f, mid = 0f, high = 0f;
        
        for (var i = 0; i < resolution; ++i) {
            var freq = deltaFreq * i;
            if      (freq <= lowFreqThreshold)  low  += spectrum[i];
            else if (freq <= midFreqThreshold)  mid  += spectrum[i];
            else if (freq <= highFreqThreshold) high += spectrum[i];
        }

        low  *= lowEnhance;
        mid  *= midEnhance;
        high *= highEnhance;

        lowMeter.localScale  = new Vector3(lowMeter.localScale.x,  low,  lowMeter.localScale.z);
        midMeter.localScale  = new Vector3(midMeter.localScale.x,  mid,  midMeter.localScale.z);
        highMeter.localScale = new Vector3(highMeter.localScale.x, high, highMeter.localScale.z);
    }
}

f:id:hecomi:20141110194636p:plain

計算は面倒なので適当ですが、レベルメーターっぽい何かは上記のようにして簡単に出来ます。AudioSettings.outputSampleRate は出力のサンプリングレートが入っており、デフォルトでは 44100 Hz が入っています(PC環境)。(情報が見つからず恐らくですが)上記のように計算すれば各周波数帯にどれだけの成分が含まれているかが分かると思います。

組み込みのフーリエ変換では満足できない場合は独自に解析する必要があります。例として LPC 分析をしてフォルマントを得る例を載せておきます。

フィルタをかける(プロ版のみ)

プロ版のみですが、Audio Filter という出力される音にエフェクトをかける仕組みが用意されています。

用意されているフィルタの他に、自分でフィルタを作成する仕組みも用意されています。

用意されているフィルタ

用意されているフィルタは以下になります。

f:id:hecomi:20141110203545p:plain

フィルタは複数アタッチすることができるので、例えばバンドパスフィルタを作りたければローパスフィルタとハイパスフィルタを適用します。

自作フィルタ(プロ版のみ)

自作フィルタを作るには、OnAudioFilterRead()MonoBehaviour を継承したスクリプトで定義してエフェクトを掛けます。例えば時間に応じてシュワンシュワン音量が変化するフィルタを作るとすると以下のようになります。

using UnityEngine;

public class MyAudioFilter : MonoBehaviour 
{
    public float pitch = 1f;
    private float t_ = 0;

    void FixedUpdate()
    {
        t_ += Time.fixedDeltaTime;
    }

    void OnAudioFilterRead(float[] data, int channels)
    {
        var angle = 2 * Mathf.PI * t_ / pitch * 0.5f;
        var intensity = Mathf.Pow(Mathf.Sin(angle), 2f);
        for (int i = 0; i < data.Length; ++i) {
            data[i] *= intensity;
        }
    }
}

f:id:hecomi:20141110205700p:plain

OnAudioFilterRead() を使うとインスペクタに音量ゲージが現れるようになります。

検索すればいくつかサンプルが見つかります。例としてローパスフィルタのコードが以下に上がっているので参考になると思います。

残響のあるエリアを設定できる Reverb Zone

エリアに入った Audio Source に対してリバーブ(残響効果)をかける Reverb Zone という仕組みが用意されています。

部屋に入ったり洞窟に入ったりした時に適切なリバーブをかけて反響をかけることでリアルな演出をすることが出来ます。使い方は簡単で、Add Component から Audio Reverb Zoneを選択するだけです。後は適切な Reverb Preset(プリセットの RoomHall やユーザ定義の User 等)を選択し、Min DistanceMax Distance を設定します。この 2 つの値はドキュメントに記載されていますが、Max DistanceMin Distance の間で連続的にリバーブ具合が変化し、Min Distance 内ではフルにリバーブがかかる、という形です。

f:id:hecomi:20141110201130p:plain

動画で見たい場合は以下に上がってました。

音声波形をリアルタイムに生成して再生

また音声をリアルタイムに生成することも出来ます。

有料(プロ版)編(OnAudioFilterRead() を使う)

上記で紹介した OnAudioFilterRead() の引数で与えられる data を無視して自分好みの音を詰めれば音を自前で生成することが出来ます。

上記サイトで基本波形の生成が参照できるので、ここでは別の例として趣向を変えて録音した音を逆再生する、みたいなことをやってみます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class ReverseMicInput : MonoBehaviour 
{
    // 録音する長さ
    public int recordDuration = 3;

    // アタッチされた空の Audio Source(マイク録音データ格納 / 再生用)
    private AudioSource audio_;

    // 録音した音声波形の生データ
    private float[] recordedData_;
    // 現在までに再生した位置
    private int offset_ = 0;
    // 再生中か
    private bool isPlaying_ = false;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
    }

    [ContextMenu("Record")]
    void Record() 
    {
        // マイクをオープンして 3 秒間録音
        audio_.clip = Microphone.Start(null, false, recordDuration, 44100);
        while (Microphone.GetPosition(null) <= 0) {}
        StartCoroutine(Play());
    }

    IEnumerator Play()
    {
        // 3 秒間待つ
        yield return new WaitForSeconds(recordDuration);

        // 音声波形の生データを取得
        recordedData_ = new float[audio_.clip.samples];
        audio_.clip.GetData(recordedData_, 0);

        // 再生位置を頭に巻き戻して再生
        offset_ = 0;
        isPlaying_ = true;
        audio_.Play();
    }

    void OnAudioFilterRead(float[] data, int channels)
    {
        // ずっと呼ばれるので再生中かフラグで制御
        if (!isPlaying_) {
            return; 
        }

        // 再生が終わったかどうかを見る
        if (offset_ + data.Length > recordedData_.Length) {
            isPlaying_ = false;
            return;
        }

        // 現在再生する音声波形のチャンク分データを詰める
        // ステレオ出力だと channels = 2 になる
        for (int i = 0; i < data.Length / channels; ++i) {
            // 逆から詰めることで逆再生する
            var x = recordedData_[recordedData_.Length - 1 - offset_ - i]; 
            if (channels == 2) {
                data[2*i] = data[2*i + 1] = x;
            } else {
                data[i] = x;
            }
        }

        // 現在までに再生し長さを記録しておく
        offset_ += data.Length / channels;
    }
}

これで、コンテキストメニューから「Record」を選択して「あいうえお」としゃべると3秒後に「おえういあ」と再生されます。なお、余談になりますが逆再生したいだけであれば、AudioClip.SetData() を使ってあげたほうが楽です。

using UnityEngine;
using System.Collections;
using System.Linq;

[RequireComponent(typeof(AudioSource))]
public class ReverseMicInput : MonoBehaviour 
{
    public recordDuration = 3f;
    private AudioSource audio_;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
    }

    [ContextMenu("Record")]
    void Record() 
    {
        audio_.clip = Microphone.Start(null, false, recordDuration, 44100);
        while (Microphone.GetPosition(null) <= 0) {}
        StartCoroutine(Play());
    }

    IEnumerator Play()
    {
        yield return new WaitForSeconds(recordDuration);

        var data = new float[audio_.clip.samples];
        audio_.clip.GetData(data, 0);
        audio_.clip.SetData(data.Reverse().ToArray(), 0);
        audio_.Play();
    }
}

無料編(OnAudioRead() コールバックを使う)

OnAudioFilterRead() は有料版しか使えませんでしたが、無料でも AudioClip.Create を利用して同じようなことが出来ます。

AudioClip.Create() ではデータが読み込まれる時のコールバック(PCMReaderCallback)を第7引数で与えることが出来ます。これは、AudioClip が読み込まれる際に呼ばれるコールバックで、この中に再生したい音データのチャンクを詰めます。再生位置の変更は第8引数のコールバック(PCMSetPositionCallback)で取得できますので、これらをよしなにセットしてあげれば音を再生することが出来ます。下記は正弦波(プーという音)を生成するサンプルです。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class OnAudioReadSample : MonoBehaviour 
{
    public float frequency  = 440;

    private AudioSource audio_;
    private int position_   = 0;
    private int sampleRate_ = 0;

    void Start() 
    {
        // オーディオ出力設定、44100 Hz がデフォルト
        sampleRate_ = AudioSettings.outputSampleRate;

        // Audio Source を取得
        audio_ = GetComponent<AudioSource>();

        // 空の Audio Clip を作成する。引数は、
        //    string name, int lengthSamples, int channels, int frequency, bool _3D, bool stream, 
        //    PCMReaderCallback pcmreadercallback, PCMSetPositionCallback pcmsetpositioncallback
        audio_.clip = AudioClip.Create("Sin Wave", 44100, 1, 44100, false, true, OnAudioRead, OnAudioSetPosition);

        // ループ再生
        audio_.loop = true;
        audio_.Play();
    }

    // AudioClip がデータを読み出す度に呼ばれるコールバック
    void OnAudioRead(float[] data) 
    {
        // 正弦波のデータを詰める
        // チャンク同士を滑らかに接続するように前回位置(position_)を覚えておく
        // NOTE: data.Length は 4096 がデフォルトだが場合によっては 628, 1256 だったりした
        for (int i = 0; i < data.Length; ++i, ++position_) {
            data[i] = Mathf.Sin(2 * Mathf.PI * frequency * position_ / sampleRate_);
        }
    }

    // AudioClip の再生位置が変わる度に呼ばれるコールバック
    // 本サンプルではループ時に newPosition == 0 として呼ばれる
    void OnAudioSetPosition(int newPosition) 
    {
        position_ = newPosition;
    }
}

これを利用してあげれば無料版でもエフェクトを再現したり、リアルタイムなデータの解析を行うことが出来ます。

using UnityEngine;
using System.Collections;

[RequireComponent(typeof(AudioSource))]
public class AudioEffect : MonoBehaviour 
{
    public AudioClip clip;
    public float pitch = 1f;

    private AudioSource audio_;
    private float[] waveRawData_;
    private int position_ = 0;

    void Start()
    {
        audio_ = GetComponent<AudioSource>();
        Play();
    }

    void Play() 
    {
        // 再生前にデータ全部とっておく(∵ OnAudioRead は別スレッドなのでアクセス出来ない)
        // この時、Audio Clip の Load Type(Insepctor から設定可能)は
        // DecompressOnLoad である必要がある
        waveRawData_ = new float[clip.samples];
        clip.GetData(waveRawData_, 0);

        audio_.clip = AudioClip.Create("Sin Wave", clip.samples, clip.channels, clip.frequency, false, true, OnAudioRead, OnAudioSetPosition);
        audio_.Play();
    }

    void OnAudioRead(float[] data) 
    {
        // waveRawData[position_ ~ position_ + data.Length] を解析したり
        // var volume = GetVolume(position_, position_ + data.Length);

        for (int i = 0; i < data.Length; ++i, ++position_) {
            // 何かエフェクトをかけたり
            var effect = Mathf.Sin(position_ * 0.00005f);
            data[i] = waveRawData_[position_] * effect;
        }
    }

    void OnAudioSetPosition(int newPosition) 
    {
        position_ = newPosition;
    }
}

なお、コールバックをセットする処理は瞬間的に重い処理になるのでプリフリーズが発生する恐れがあります。リップシンクプラグインでは、この方法で再生・解析を行っているのですが、この開始時のプチフリーズが問題になっているので Pro 版であれば OnAudioFilterRead() を使う、などの場合分けを入れようと思います。

f:id:hecomi:20141111004056p:plain

AudioClip.GetData() は特に重くないようです、コピーが走っていると思うのですがメモリに展開されているものをコピーするだけなので軽い処理なのだと思われます。

他にも、Unity で対応していないオーディオの形式を専用のライブラリ経由でロードしてデータに詰め込む、みたいなことをやっているのも見かけました。色々な使い方ができると思います。

(おまけ)Unity 5 のオーディオとの関係

Unity5 からは Audio Source の Output 欄に Audio Mixer というものをセットできるようになり、よりグラフィカルに様々なエフェクトをダイナミックに適用できるようになります。以下に動画があがっているので、見てみると雰囲気が掴めると思います。

f:id:hecomi:20141111020132p:plain

このあたりは、どなたかがきっと Unity Advent Calendar 2014 に書いてくれるかな...、と期待しています(誰も書かなかったらそのうちまとめます)。

おわりに

音と 3D による演出は色々と表現力のある分野だと思います。是非みなさんも色々なアイディアを形にしてみてください。

Oculus Rift と Leap Motion で空中お絵描きアプリを作ってみた

f:id:hecomi:20141031012737p:plain

はじめに

Leap Motion VR の登場で簡単に VR 空間に干渉出来る自分の手を持ち込むことが可能になりました。

そこでふと思いついてやってみようと思ったのが、自分の指で空中にお絵描きできるアプリです。本エントリでは作ってみたアプリの配布と、この作り方(なんとコード0行!)についての紹介をしたいと思います。

デモ

ダウンロード

下で解説を行っているものになります(上のデモの線をカラフルにしたものです)。↑キーと↓キーで明るさの調節ができます。

簡単な仕組み解説

Leap Motion VR の世界はおおよそ実空間に則したスケールで作られています(Unity 上のスケール 1 = 実空間の 1m)。これについては以下のエントリで詳しく解説されています。

また、Oculus のトラッキングも同じく実空間に則したスケールで作られているため、なんと特に何もしなくてもおおよその位置が合う形になります(微妙にはずれています)。

環境

作り方

基本的には、指先に Trail Renderer をつけるだけです。

まず、Leap Motion VR のページから Unity 用の Asset を落としてきてインポートします。

LeapMotion+OVR > LeapOculusPassthrough シーンを開いて実行してみると手で崩せるブロックがたくさんある画面になると思います。今回はブロックは必要ないので Walls オブジェクトを削除するか、Inspector からチェックを外して inactive にして非表示にしておきましょう。

次に、指から線が出るように手のモデルである FadingHandbone3(人差し指の先)に Trail Renderer をアタッチします(FadingHandLeap Motion で認識した手に乗っかるコピー元のオブジェクトとして扱われるため、普段は見えないように inactive な状態になってますが、ここでは分かりやすいように一度 active な状態にしてスクリーンショットを撮っています)。

f:id:hecomi:20141031002839p:plain

デフォルトだと 1 の太さ(= 幅 1 メートルの線)になっているので、適当に 1 cm の太さ、線の頂点の間隔を 5 mm などとしてみます。

f:id:hecomi:20141031004056p:plain

最後に、Durovis Dive 用のアセットがビルドの妨げになるので、DiveLeapMotion+Dive は Project から消去した後にビルドします。

f:id:hecomi:20141031004250p:plain

これで完成です!色をつけたい場合は FadingHand にアタッチされている HmdFading スクリプト(手の信頼度に応じて手の透過率を変更するスクリプト)のチェックを外して disabled にしておき、Trail Renderer のパラメタを適当にいじります。

f:id:hecomi:20141031005046p:plain

f:id:hecomi:20141031010057p:plain

f:id:hecomi:20141031010104p:plain

また、LeapPassthroughOVRPlayerController > OVRCameraController > CameraLeft > HandController にアタッチされている HandController.cs の Tool に適当なオブジェクトを指定すると棒状のものもトラッキングできるようになるので、同様にトレイルを引いてくれる棒オブジェクトを作ってあげると空中ペンができます。

f:id:hecomi:20141031020022p:plain

あとはパーティクルをモリモリしたりすればきれいな感じになると思います。また標準の Trail Renderer はカクカクしがちなのでスプラインにしてくれるアセットを代わりに適用しても良いと思います(次回エントリで紹介する Magic VR ではこちらを利用しています)。

Leap Motion VR の無駄な処理削減

さて、Leap Motion VR で画を取ってくる処理ですが、結構重いです。そこで Deep Profile で見てみると、EncodeDistortion() が結構な時間を食っています。

f:id:hecomi:20141031010352p:plain

これは LeapImageRetriever.cs の中で行われている Leap Motion の歪み補正を行うための処理(前回の記事参照)なのですが、テクスチャのサイズが変更されない限り初回のみで良いはずです。そこでこれを最初のみ行われるようにちょっとだけ書き換えたものが以下になります。

これを代わりに使うことで軽量化することが出来ます。

f:id:hecomi:20141031014252p:plain

先行事例紹介

AR の分野では MIT Media Lab の Second Surface が有名かと思います。

Second SurfaceiPad を使ったものですが、自作の透明パッドによる Gravity というプロジェクトもあり、こちらでは Oculus Rift と組み合わたデモも実現されています。

昨今の VR 関連の技術進歩によって、これらのコンセプトを実現することが容易になったのは本当に素晴らしいと思います。

おわりに

ちょっとした工夫でもっと色々なことが出来ると思います。例えば犬を描くとそれを認識して動き出したり、うごメモのように 3D 漫画が作れたり、他人の描いた絵を 3D 空間上でそのまま再現して見れる、などなどたくさん思いつきます。Leap Motion の次期バージョン Dragonfly が出るとスルー画もカラーに出来てもっと楽しくなりそうですね。

次回は、このコンセプトを発展させて現在開発中の、図形を描いて魔法を発射できる Magic VR の紹介と解説エントリを書きたいと思います。