読者です 読者をやめる 読者になる 読者になる

凹みTips

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

PS Eye × 2個 を Unity 上で Oculus SDK と共に使ってみた

Oculus C# Unity

はじめに

先日、以下のように PS Eye を 2 つ使用して、現実の世界を Oculus Rift 越しに覗くという内容のエントリを書きました。

ここから実際の世界に色々なものを重畳して行きたいと思っているのですが、WPF で 3D 周りをやっていくのはツラミがあります。そこで Unity を使いたいところですが、Unity で複数の PS Eye を使おうとすると、デバイスの名前が被ってしまうことから、WebCamTexture で 1 つのデバイスしか使えない問題に行きあたってしまいます。
そこで、先日用いた CL Eye Platform SDK を利用して、DLL から直接ピクセルデータをもらってテクスチャ化することで、同時に 2 つの PS Eye を使用することに試みてみました。

デモ

AR はまだ出来ていないです。PS Eye には KSW-3 を装着して使用しています。

スマートフォン用外付け魚眼コンバージョンレンズ(ブラック)

スマートフォン用外付け魚眼コンバージョンレンズ(ブラック)

問題点(通常の WebCamTexture のコード)

通常の WebCamTexture では以下の様なコードを記述します。

using UnityEngine;
using System.Collections;

public class WebCam : MonoBehaviour {

	public int cameraNumber = 0;
	private WebCamTexture webcamTexture;

	void Start () {
		WebCamDevice[] devices = WebCamTexture.devices;
		if (devices.Length > cameraNumber) {
			webcamTexture = new WebCamTexture(devices[cameraNumber].name, 320, 240, 6);
			renderer.material.mainTexture = webcamTexture;
			webcamTexture.Play();
		} else {
			Debug.Log("no camera");
		}
	}
}

しかしながら、PSEye を 2 個接続しても devices.Length が 1 になってしまい、テクスチャを取ってくることができません。

CL Eye Platform SDK を利用したコード

そこで、先のブログでも利用した「CL Eye Platform SDK」を利用して、dll から直接ポインタ経由でデータをもらってきます。基本的なコードは CL Eye Platform SDK のサンプルの CLEyeWinFormTest を参考にしています。

using UnityEngine;
using System;
using System.Collections;
using System.Runtime.InteropServices;
using System.Threading;

public class PSEye : MonoBehaviour 
{
	// dll name
	private const string CLEyeMulticam_DllName = "CLEyeMulticam.dll";
    
	#region [ Camera Parameters ]
	// camera color mode
	public enum CLEyeCameraColorMode
	{
	    CLEYE_MONO_PROCESSED,
	    CLEYE_COLOR_PROCESSED,
	    CLEYE_MONO_RAW,
	    CLEYE_COLOR_RAW,
	    CLEYE_BAYER_RAW
	};
	
	// camera resolution
	public enum CLEyeCameraResolution
	{
	    CLEYE_QVGA,
	    CLEYE_VGA
	};
	
	// camera parameters
	public enum CLEyeCameraParameter
	{
	    // camera sensor parameters
	    CLEYE_AUTO_GAIN,				// [false, true]
	    CLEYE_GAIN,					// [0, 79]
	    CLEYE_AUTO_EXPOSURE,			// [false, true]
	    CLEYE_EXPOSURE,				// [0, 511]
	    CLEYE_AUTO_WHITEBALANCE,			// [false, true]
	    CLEYE_WHITEBALANCE_RED,			// [0, 255]
	    CLEYE_WHITEBALANCE_GREEN,			// [0, 255]
	    CLEYE_WHITEBALANCE_BLUE,			// [0, 255]
	    // camera linear transform parameters
	    CLEYE_HFLIP,				// [false, true]
	    CLEYE_VFLIP,				// [false, true]
	    CLEYE_HKEYSTONE,				// [-500, 500]
	    CLEYE_VKEYSTONE,				// [-500, 500]
	    CLEYE_XOFFSET,				// [-500, 500]
	    CLEYE_YOFFSET,				// [-500, 500]
	    CLEYE_ROTATION,				// [-500, 500]
	    CLEYE_ZOOM,					// [-500, 500]
	    // camera non-linear transform parameters
	    CLEYE_LENSCORRECTION1,			// [-500, 500]
	    CLEYE_LENSCORRECTION2,			// [-500, 500]
	    CLEYE_LENSCORRECTION3,			// [-500, 500]
	    CLEYE_LENSBRIGHTNESS			// [-500, 500]
	};
	#endregion
	
	
	#region [ CLEyeMulticam Imports ]
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern int CLEyeGetCameraCount();
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern Guid CLEyeGetCameraUUID(int camId);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern IntPtr CLEyeCreateCamera(Guid camUUID, CLEyeCameraColorMode mode, CLEyeCameraResolution res, float frameRate);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern bool CLEyeDestroyCamera(IntPtr camera);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern bool CLEyeCameraStart(IntPtr camera);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern bool CLEyeCameraStop(IntPtr camera);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern bool CLEyeCameraLED(IntPtr camera, bool on);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern bool CLEyeSetCameraParameter(IntPtr camera, CLEyeCameraParameter param, int value);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern int CLEyeGetCameraParameter(IntPtr camera, CLEyeCameraParameter param);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern bool CLEyeCameraGetFrameDimensions(IntPtr camera, ref int width, ref int height);
	[DllImport(CLEyeMulticam_DllName, CallingConvention = CallingConvention.Cdecl)]
	public static extern bool CLEyeCameraGetFrame(IntPtr camera, IntPtr pData, int waitTimeout);
	#endregion
    
    
	#region [ Static members ]
	private static int CameraCount { get { return CLEyeGetCameraCount(); } }
	private static Guid CameraUUID(int idx) { return CLEyeGetCameraUUID(idx); }	
	#endregion
	
	
	#region [ Private members ]
	private IntPtr camera_ = IntPtr.Zero;
	private int width_  = 0;
	private int height_ = 0;
	
	private Texture2D texture_;
	private Color32[] pixels_;
	private GCHandle  pixels_handle_;
	#endregion
	
	
	#region [ Public members ]
	public int cameraNumber = 0;
	#endregion
	
	
	#region [ Member functions ]
	void Awake() {
		camera_ = CLEyeCreateCamera(CameraUUID(cameraNumber), CLEyeCameraColorMode.CLEYE_COLOR_RAW, CLEyeCameraResolution.CLEYE_VGA, 30);	
		CLEyeCameraGetFrameDimensions(camera_, ref width_, ref height_);
		
		texture_ = new Texture2D(width_, height_, TextureFormat.ARGB32, false);
		pixels_  = texture_.GetPixels32();
		pixels_handle_ = GCHandle.Alloc(pixels_, GCHandleType.Pinned);
		renderer.material.mainTexture = texture_;
		
		Vector3 scale = transform.localScale;
		transform.localScale = new Vector3(scale.x, scale.y, -scale.z);
		
		CLEyeCameraStart(camera_);
	}
	
	void Update() {
		CLEyeCameraGetFrame(camera_, pixels_handle_.AddrOfPinnedObject(), 500);
		texture_.SetPixels32(pixels_);
		texture_.Apply();
	}
	
	void OnApplicationQuit() {
		pixels_handle_.Free();
		CLEyeCameraStop(camera_);
		CLEyeDestroyCamera(camera_);
	}
	#endregion
}

テクスチャを生成(new Texture)し、そこから Color32[] 型の領域を Texture2D.GetPixels32() で確保します。ここから IntPtr を取り出せるように GCHandle を作成し、これ経由で Update 時に CLEyeCameraGetFrame からピクセルデータをもらってきて、SetPixels32 でプロット、Apply して反映させています。本当は直接テクスチャの領域にアクセスして SetPixels しなくても良いようにしたかったのですがやり方が分からず…。なお、終了時に CLEyeCameraStop / CLEyeDestroyCamera を呼んでおかないと、Unity を再起動しない限り次回のプレビュー時からテクスチャが反映されなくなります。この辺りでなかなかテクスチャが表示されず、マーシャリングしたりと色々試行錯誤して苦労しました...。
これで以下のように映るようになります。

なお、画像は反転してしまっているので、Z-Scale を負の値にして反転させています。

メインスレッドからの追い出し

PS Eye から画素データをもらってくる CLEyeCameraGetFrame はそこそこ重い処理になっています。なので私のマシンでは、上述のコードのように Update 時に走らせると 30 fps 程度まで実行速度が落ち込んでしまいました。そこでこの処理を別スレッドに追いやることにします。スレッドについては以下のエントリを参考にしました。

// (略)
private Color32[] pixelsABGR_;

void Awake() {
	// (略)
	pixelsABGR_ = texture_.GetPixels32();
	
	mainLoop_ = new Mutex(true);
	thread_ = new Thread(ThreadWorker);
	thread_.Start();
}

void Update() {
	mainLoop_.ReleaseMutex();
	mainLoop_.WaitOne();
	texture_.SetPixels32(pixelsABGR_);
	texture_.Apply();
}

void ThreadWorker() {
	try {
		_ThreadWorker();
	} catch (Exception e) {
		if (!(e is ThreadAbortException)) {
			Debug.LogError("Unexpected Death: " + e.ToString());
		}
	}
}

void _ThreadWorker() {
	for (;;) {
		Thread.Sleep(0);
		
		CLEyeCameraGetFrame(camera_, pixels_handle_.AddrOfPinnedObject(), 500);

		mainLoop_.WaitOne();
		for (int i = 0; i < width_ * height_; ++i) {
			pixelsABGR_[i].a = pixels_[i].a; 
			pixelsABGR_[i].r = pixels_[i].b;
			pixelsABGR_[i].g = pixels_[i].g;
			pixelsABGR_[i].b = pixels_[i].r; 
		}
		mainLoop_.ReleaseMutex();
	}
}

void OnApplicationQuit() {
	thread_.Abort();
	// (略)
}

これで 60 fps が確保できるようになりました。なお、画像は Red と Blue が入れ替わった状態になってしまっているので、同期用の別領域(pixelABGR_)を用意して、生成したスレッド内の同期部分で手動で交換しています。

各パラメータの設定を行う

CL Eye Platform SDK によって提供されている各パラメータを Inspector から変更できるようにしました。

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(PSEyeTexture))]
public sealed class PSEyeTextureEditor : Editor 
{
	public override void OnInspectorGUI()
	{
		// Draw default public members
		DrawDefaultInspector();
		
		// Get target instance
		var psEye = target as PSEyeTexture;

		if (!psEye.isGaming()) return;

		// Label
		EditorGUILayout.Separator();
		EditorGUILayout.BeginHorizontal(); {
			GUILayout.FlexibleSpace();
			EditorGUILayout.LabelField("Camera Sensor");
			GUILayout.FlexibleSpace();
		} EditorGUILayout.EndHorizontal();
		
		// Auto Gain
		bool autoGain = EditorGUILayout.Toggle("Auto Gain", psEye.AutoGain);
		if (autoGain != psEye.AutoGain) psEye.AutoGain = autoGain;
		
		// Gain 
		int gain = EditorGUILayout.IntSlider("Gain", psEye.Gain, 0, 79); 
		if (gain != psEye.Gain) psEye.Gain = gain;
		
		// 以下略
	}
}


各パラメタの Getter / Setter は Sample の CLEyeMulticamWPFTest から頂きました。Getter / Setter 付きの変数でも拡張エディタから編集できるの良いですね。
ただ、Camera Sensor の値は変わるのですがその他のパラメタ(Camera Linear Transform / Camera Non-linear Transform)は変更されませんでした…。追って調査します。

OVRCameraController と連動させる

Oculus SDK の OVRCameraController で Oculus Rift 用に用意される 2 つのカメラ、CameraLeft と CameraRight の遠方にそれぞれビルボードを用意し、Culling Mask を利用して一方しか見えないようにすることで立体視出来るようにします。

using UnityEngine;
using System.Collections;

public class PSEyeCameraBackground : MonoBehaviour {
	
	public Camera     targetCamera = null;
	public GameObject prefab       = null;
	public int        psEyeNumber  = 0;
	public int        layer        = 0;
	public int        hiddenLayer  = 0;
	public Vector3    rotation;
	
	private GameObject billboard_  = null;
	
	// child plane name
	private const string CHILD_NAME = "PSEyeBackgroundImage";
	
	// adjustment parameter
	private const float EXPAND_WIDTH_RATE  = 1.8f;
	private const float EXPAND_HEIGHT_RATE = 2.7f;
	private const float FARNESS_RATE       = 0.9f;

	void Awake() {
		if (targetCamera == null) {
			Debug.LogError("No target camera!");	
			return;
		}
		if (prefab == null) {
			Debug.LogError("No prefab!");	
			return;
		}
		
		// set the PS Eye number before instantiation
		GameObject child = prefab.transform.FindChild(CHILD_NAME).gameObject;
		child.GetComponent<PSEyeTexture>().cameraNumber = psEyeNumber;
		
		// create a billboard
		billboard_ = Instantiate(prefab) as GameObject;
		
		// set the billboard's size and child's rotation
		float width  = targetCamera.far * EXPAND_WIDTH_RATE;
		float height = targetCamera.far * EXPAND_HEIGHT_RATE;
		billboard_.transform.localScale = new Vector3(width, height, 0.1f);
		child.transform.localRotation = Quaternion.Euler(rotation);
		
		// set the billboard's layer and the targetCamera's culling mask
		billboard_.layer = child.layer = layer;
		targetCamera.cullingMask &= ~(1 << hiddenLayer); 
		
		// update the billboard's position and rotation
		UpdateObject();
	}
	
	void LateUpdate() {
		if (targetCamera != null && billboard_ != null) {
			UpdateObject();
		}
	}
	
	void UpdateObject() {
		billboard_.transform.position = targetCamera.transform.position + targetCamera.transform.forward * targetCamera.far * FARNESS_RATE;
		billboard_.transform.rotation = targetCamera.transform.rotation;
	}
}

ビルボードは、Plane で作成するとカメラの方を向かせる計算がややこしくなるので Cube で薄い板を作成した方が楽ですが、無駄な描画が気になります。そこで空 GameObject で Plane をラップして角度調整をしておきました。詳細は別記事を書きます。
これで、冒頭のムービーのような動画が作成出来ます。

コード

現状のものをアップロードしておきます。

使用には前回の記事で書いた CL Eye が必要になります。なぜか DLL が読み込まれないことが多いので、DllNotFoundException が吐かれた時は Unity を再起動してみて下さい。

おわりに

PS Eye の視差と OVRCameraController で作られる視差(輻輳角)が異なり、現状だと立方体が二重に見えてしまいます。PS Eye の位置の調整か、OVRCameraController のパラメタ調整で解決したいです。
そして次はいよいよ AR に挑戦したいですが、この方式で String や QCAR を果たして使えるのかまだ分かっていないので併せて調査したいと思います。