このチュートリアルでは、Nuitrack SDK を使って、ユーザーセグメントを視覚化する方法を紹介します。結果として、ユーザーは、単色の 2D シルエットとして表示されます。カメラの前に複数のユーザーが立っている場合、それぞれ異なる色のシルエットで表示されます。ユーザー セグメントは、アプリやゲームの作成等様々な目的のために使用できます。
このプロジェクトを作成するために必要なものは以下の通りです。
- Nuitrack Runtime と Nuitrack SDK
- サポートされているセンサー (サポートしているセンサーの一覧は、Nuitrack Webサイトを参照)
- Unity 2019.2.11f 以上
完成したプロジェクトの Nuitrack SDK 保存先は、[Unity 3D] > [NuitrackSDK.unitypackage] > [チュートリアル] > [SegmentExample]です。
左がaeroTAP 3D USB Camera、右がaeroTAP 3D USB Camera GS
ユーザーセグメントを視覚化
このチュートリアルでは、セグメントの視覚化のプロセスについて説明します。ユーザー セグメントを作成する場合必要なのは、Nuitrack SDK とそれに互換性のあるセンサー (TVico 等) だけです。
ユーザーの存在を確認
-
ユーザー セグメントの視覚化の前に、ユーザーがカメラによって認識されているかどうかを確認する必要があります。まず、Nuitrack プレハブ を Nuitrack SDK からご自分の Unity プロジェクトにインポートします。プロジェクトに必要な Nuitrack モジュールを選択します (Depth Module、User Tracker Module、Skeleton Tracker Module)。これらのモジュールが必須なのは、サンプルでセンサーの深度データに加え、センサー前に立つユーザーのデータを使用するからです。
このプロジェクトに必要な Nuitrack モジュール
-
スクリプトを作成し、SegmentPaint.cs と名前を付けます。このスクリプトには、ユーザー セグメントに関するすべての情報が含まれることになります。Start メソッドで、ユーザーのいるフレームの更新を登録します。
public class SegmentPaint : MonoBehaviour
{
void Start()
{
NuitrackManager.onUserTrackerUpdate += ColorizeUser;
}
}
-
シーンまたはゲームが終了すると生じる onDestroy メソッドを作成します。ユーザー フレーム更新イベントの登録を解除することで、別のシーンに移動した際に空レファレンスが生成されないようにします。イベントの除外命令機能についての詳細は、こちらをご覧ください。
NuitrackManager.onUserTrackerUpdate += ColorizeUser;
}
void OnDestroy()
{
NuitrackManager.onUserTrackerUpdate -= ColorizeUser;
}
}
-
受け取ったフレームを処理し、センサーの前のユーザーを認識していることを確認します。まず、msg 変数を使用して、カメラの前に一人以上たっている場合に「ユーザー検出」、そうでない場合に「ユーザー検出されず」のメッセージを表示するよう設定します。ColorizeUser メソッドで処理されます。「ユーザー検出 / ユーザー検出されず」のメッセージの特性 (色とサイズ) をOnGUI メソッドで設定することもを忘れずに行ってください。
NuitrackManager.onUserTrackerUpdate -= ColorizeUser;
}
string msg = "";
void ColorizeUser(nuitrack.UserFrame frame)
{
if (frame.Users.Length > 0)
msg = "User found";
else
msg = "User not found";
}
private void OnGUI()
{
GUI.color = Color.red;
GUI.skin.label.fontSize = 50;
GUILayout.Label(msg);
}
}
- [メイン カメラ]に、SegmentPaint.cs スクリプトをドラッグ アンド ドロップします。
- プロジェクトを実行し、ユーザーの検出を確認します。問題がない場合、カメラの前に立つと、「ユーザー検出」のメッセージが画面に表示されます。問題がないことが確認できたら、次のステップに進みます。
「ユーザー検出」のメッセージ表示
ユーザーセグメントの作成とレンダリング (表示)
- シーンに、ユーザー セグメントを表示するキャンバスを作成します(GameObject > UI > Canvas を選択)。
-
[メイン カメラ]の設定は、デフォルトのままです。
- 注意
- Orthographic または Perspective のどちらのカメラ投影を選択した場合でも、キャンバス サイズは常に自動で調整されます。
-
ゲーム オブジェクトを追加することで、キャンバスにユーザー セグメントを表示させます。追加するには、Game Object > UI > Image を選択し、Segment と名前を付けます。このオブジェクトのサイズとキャンバスのサイズは同じである必要があります。オブジェクトの幅を引き伸ばすことで、キャンバスの大きさと一致させることができます。[Rect Transform]設定が、以下の写真と同じであることを確認してください。
セグメントの設定
-
SegmentPaint.cs スクリプトで、ユーザーの色付けに関わるColor32 配列、画像内のスプライトをフレーム化するために使用する長方形に関する Rect フィールド、キャンバスに表示される画像に関する Image フィールド、セグメント表示に使用するテクスチャに関する Texture2D、スプライトに関する Sprite 、センサー入力データの処理に関する byte 配列、セグメントのマトリクス表示に関するcols と rows を作成します。
public class SegmentPaint :MonoBehaviour
{
[SerializeField]
Color32[] colorsList;
Rect imageRect;
[SerializeField]
Image segmentOut;
Texture2D segmentTexture;
Sprite segmentSprite;
byte[] outSegment;
int cols = 0;
int rows = 0;
}
-
SetMirror メソッドを使用して取得した画像のミラーリングを Start メソッドで行います。
void Start()
{
NuitrackManager.onUserTrackerUpdate += ColorizeUser;
NuitrackManager.DepthSensor.SetMirror(true);
}
-
深度センサーの出力画像パラメーターをリクエストします。
NuitrackManager.DepthSensor.SetMirror(true);
nuitrack.OutputMode mode = NuitrackManager.DepthSensor.GetOutputMode();
cols = mode.XRes;
rows = mode.YRes;
}
-
テクスチャの境界を定義するために Rect を作成します。
rows = mode.YRes;
imageRect = new Rect(0, 0, cols, rows);
}
-
セグメントのテクスチャを作成し、幅と高さを設定します。テクスチャに ARGB32 形式を設定します。この形式は、アルファ チャンネル、全部で4チャンネルある各チャンネル 1 バイト (8 ビット) に対応しています。アルファ チャンネルが必要なのは、ユーザー透過のない領域を作成するためです。ARGB32 についての詳細は、こちらをご覧ください。
imageRect = new Rect(0, 0, cols, rows);
segmentTexture = new Texture2D(cols, rows, TextureFormat.ARGB32, false);
}
-
出力セグメントを作成し、サイズをバイト単位で指定します。画像サイズに 4 をかけるのは、各ピクセルに 4 つのチャンネルがあるからです (ARGB32)。
segmentTexture = new Texture2D(cols, rows, TextureFormat.ARGB32, false);
outSegment = new byte[cols * rows * 4];
}
-
Image type を Simple に設定することで、画像が標準モードで引き伸ばされることなく表示します。それに加え、preserveAspect = true flag に設定することで、画像の縦横比を維持できます。
outSegment = new byte[cols * rows * 4];
segmentOut.type = Image.Type.Simple;
segmentOut.preserveAspect = true;
}
-
In the ColorizeUser method, process the input data in the for (int i = 0; i < (cols * rows); i++) loop.i-th user、his/her id (0, 1, 2, 3...) を受け取り、ユーザーID に対応するピクセルを色でペイントします。その結果取得する色を含んだ配列は、バイトという形で表されるユーザー (1 から 6) に対応します。
void ColorizeUser(nuitrack.UserFrame frame)
...
msg = "User not found";
for (int i = 0; i < (cols * rows); i++)
{
Color32 currentColor = colorsList[frame[i]];
int ptr = i * 4;
outSegment[ptr] = currentColor.a;
outSegment[ptr + 1] = currentColor.r;
outSegment[ptr + 2] = currentColor.g;
outSegment[ptr + 3] = currentColor.b;
}
}
-
テクスチャ塗りつぶしのために配列を渡し、適用します。
outSegment[ptr + 3] = currentColor.b;
}
segmentTexture.LoadRawTextureData(outSegment);
segmentTexture.Apply();
}
-
スプライトにテクスチャを適用します。引数として、テクスチャ、長方形、オフセット (画像を中心を設定するために、Vector3 に 0.5 を掛ける)、テクスチャのディテール、 押し出し (スプライト メッシュが外側にどれほど押し出されるかの値)、メッシュ タイプを指定します。FullRect メッシュ タイプを使用すると、スプライトのサイズは大きくなりますが、処理時間は大幅に減ります。Sprite.Create パラメーターについての詳細は、こちらをご覧ください。
segmentTexture.Apply();
segmentSprite = Sprite.Create(segmentTexture, imageRect, Vector3.one * 0.5f, 100f, 0, SpriteMeshType.FullRect);
}
-
Sprite を画像に適用します。各フレームに新しいスプライトが作成されますが、パフォーマンスに影響はありません。そのため、テクスチャをスプライトまたは素材のどちらに使用しても問題ありません。
segmentSprite = Sprite.Create(segmentTexture, imageRect, Vector3.one * 0.5f, 100f, 0, SpriteMeshType.FullRect);
segmentOut.sprite = segmentSprite;
}
-
Unity で Segment Paint (スクリプト) を調整します。セグメントの色を設定します。最初の色は、ユーザーが検出されない場合に使用されますので、透明 (アルファ = 0) に設定します。他の 6 色については、自由に色を選択できます。全部で 7 色選択します。[Segment Out ]設定では、[キャンバス]から[セグメント画像]へのレファレンスを作成します。
色の選択
-
プロジェクトを実行します。この段階では、色つきユーザー セグメントが画面に表示されています。
ユーザー セグメント
Nuitrack SDK を使ったユーザー セグメントの作成が完了しました!作成したユーザー セグメントを使用して、様々なアプリやゲームを作成できます。このセグメントを使用して、 Unity でゲームを作成する方法は、このチュートリアルの後半をご覧ください。
ユーザーセグメントを使用したゲームを作成
ここからは、簡単なゲームの作成方法を紹介します。簡単なゲームとは、ユーザーがセグメントとして表示されている状態で、落下するオブジェクトをできるだけ多くタッチして破壊するというものです。破壊 (タッチ) した落下物ごとにポイントを取得できます。タッチできずに、一番下の線まで行ってしまった場合は、ポイントがひかれます。このゲームは、Unity を使い慣れていなくても作成できます。
ゲーム オブジェクトとのインタラクションのためにセグメントを調整
-
[キャンバス]設定を変更します。キャンバスが画面の上ではなく、カメラの前に来るよう位置を変更します (Main Camera > Camera > Screen Space を使用)。これで、キャンバスはカメラの動きに合わせて移動します。カメラの範囲内にキャンバスがくるよう、距離を設定します。
キャンバスの設定
- 他のゲーム オブジェクトと相互に作用するコライダーをセグメントに添付します。コライダーの動作を新しいスクリプトGameColliders.cs で定義します。
-
GameColliders クラスで、次のように必要なフィールドを作成します。
public class GameColliders :MonoBehaviour
{
[SerializeField]
Transform parentObject;
[SerializeField]
GameObject userPixelPrefab;
[SerializeField]
GameObject bottomLinePrefab;
GameObject[,] colliderObjects;
int cols = 0;
int rows = 0;
[Range (0.1f, 1)]
[SerializeField]
float colliderDetails = 1f;
}
-
センサーから入力データ (カラムと行の数) を受け取る CreateColliders public メソッドを作成します。このメソッドでは、colliderDetails で設定したコライダーの詳細レベルに従ってコライダーの新しいサイズを計算する際、詳細レベルにカラムや行の数を掛けます。
float colliderDetails = 1f; // set the detail of colliders
public void CreateColliders(int imageCols, int imageRows)
{
cols = (int)(colliderDetails * imageCols);
rows = (int)(colliderDetails * imageRows);
}
}
-
オブジェクトの配列を作成し、サイズを設定します。
cols = (int)(colliderDetails * imageCols);
colliderObjects = new GameObject[cols, rows];
}
-
imageScale 変数を使用して、コライダーのマトリクスと画像のサイズを調整します。画像は、センサーからの画像に応じて、幅もしくは高さで並べられます。Screen クラスのプロパティについての詳細は、こちらをご覧ください。
colliderObjects = new GameObject[cols, rows];
float imageScale = Mathf.Min((float)Screen.width / cols, (float)Screen.height / rows);
}
-
配列に、オブジェクトのループを入れます。
float imageScale = Mathf.Min((float)Screen.width / cols, (float)Screen.height / rows);
for (int c = 0; c < cols; c++)
{
for (int r = 0; r < rows; r++)
{
GameObject currentCollider = Instantiate(userPixelPrefab);
currentCollider.transform.SetParent(parentObject, false);
currentCollider.transform.localPosition = new Vector3((cols / 2 - c) * imageScale, (rows / 2 - r) * imageScale, 0);
currentCollider.transform.localScale = Vector3.one * imageScale;
colliderObjects[c, r] = currentCollider;
}
}
}
-
一番下のラインを作成し、その特性を UserPixel と同様、親に設定し、位置や拡大率を定義します。
colliderObjects[c, r] = currentCollider; // put a collider into the matrix of colliders
}
}
GameObject bottomLine = Instantiate(bottomLinePrefab);
bottomLine.transform.SetParent(parentObject, false);
bottomLine.transform.localPosition = new Vector3(0, -(rows / 2) * imageScale, 0);
bottomLine.transform.localScale = new Vector3(imageScale * cols, imageScale, imageScale);
}
-
SegmentPaint スクリプトで画像の幅と高さを渡す gameColliders フィールドを追加します。
public class SegmentPaint : MonoBehaviour
{
...
int rows = 0;
[SerializeField]
GameColliders gameColliders;
Rect imageRect;
-
このスクリプトで、Start メソッドの gameColliders メソッド (カラムと行を渡す) を呼び出して、コライダーを作成します。
segmentOut.preserveAspect = true;
gameColliders.CreateColliders(cols, rows);
}
- Unity で、一番下のラインを表示する BottomLine と、ユーザーのシルエットを作成する UserPixel の2種類のプレハブを作成します。立方体の形状として作成します。分かりやすいように、それぞれ別の色に設定します。ここでは、bottom line は赤、Userpixel は黄色に設定します。Rigidbody 要素を user pixel オブジェクトに追加し、[Is Kinematic]チェックボックスをオンにすることで、他のオブジェクトとの衝突時に物理特性が影響しないようにします。
-
GameColliders スクリプトをカメラにラッグ アンド ドロップします。Unity で、このスクリプトの userPixelPrefab と bottomLinePrefab を指定します。キャンバスを parentObject にドラッグ アンド ドロップします。colliderDetails で詳細レベルを指定するには、0 から 1 の範囲で数字を選択します。数字が低いほどパフォーマンスは高くなります。
Game Colliders (Script) 設定
-
SegmentPaint で、GameColliders へのレファレンスを作成します。
GameColliders へのレファレンス
- プロジェクトを実行し、ゲーム オブジェクトが正しく作成されていることを確認します。この段階では、キャンバスがコライダーによって完全に隠れているので、セグメントは見えません。一番下のラインが表示されます。
作成したコライダーに覆われているキャンバス
ゲーム オブジェクトを含むセグメントを作成
-
GameColliders.cs スクリプトで、UpdateFrame メソッドを作成します。フレーム内にユーザーがいる場合にだけ、シルエットを表示するゲーム オブジェクトが有効になり、そのほかの場合は隠されます。
public void UpdateFrame(nuitrack.UserFrame frame)
{
for (int c = 0; c < cols; c++)
{
for (int r = 0; r < rows; r++)
{
ushort userId = frame[(int)(r / colliderDetails), (int)(c / colliderDetails)];
if (userId == 0)
colliderObjects[c, r].SetActive(false);
else
colliderObjects[c, r].SetActive(true);
}
}
}
-
このメソッドを、SegmentPaint スクリプトの ColorizeUser メソッドから呼び出します。
void ColorizeUser(nuitrack.UserFrame frame)
{
...
segmentOut.sprite = segmentSprite;
gameColliders.UpdateFrame(frame);
}
-
この段階でプロジェクトを実行すると、ユーザーのシルエットは、テクスチャとして生じされます。テクスチャと重なり合ったゲーム オブジェクトが見えます。
ゲーム オブジェクトが重なっているユーザー セグメント
-
Unity で、UserPixel プレハブの [Mesh Renderer]チェックボックスをオフにすることで、立方体のメッシュのレンダリングが行われないようにします (立方体は透明になります)。
[Mesh Renderer]要素のチェックボックスがオフ
- プロジェクトを実行し、セグメントがコライダーなしで、テクスチャとして表示されることを確認します。
ユーザー セグメント (コライダーなし)
落ちてくるオブジェクトを作成
-
新しいスクリプト、ObjectSpawner.cs を作成します。このスクリプトでは、GameObject[] fallingObjects を含む配列を作成します。落ちてくるオブジェクトの間隔の最小 (1秒) と最大 (2秒) を設定します。halfWidth 変数は、画像の中心から一方の端までの幅を距離として定義します。
public class ObjectSpawner :MonoBehaviour
{
[SerializeField]
GameObject[] fallingObjects;
[Range(0.5f, 2f)]
[SerializeField]
float minTimeInterval = 1;
[Range(2f, 4f)]
[SerializeField]
float maxTimeInterval = 2;
float halfWidth;
}
-
StartSpawnメソッドを作成します。元の画像の幅を取得し、コルーチンを開始します。
float halfWidth;
public void StartSpawn(float widthImage)
{
halfWidth = widthImage / 2;
StartCoroutine(SpawnObject(0f));
}
}
-
コルーチン コンテンツを定義します。
StartCoroutine(SpawnObject(0f));
}
IEnumerator SpawnObject(float waitingTime)
{
yield return new WaitForSeconds(waitingTime);
float randX = Random.Range(-halfWidth, halfWidth);
Vector3 localSpawnPosition = new Vector3(randX, 0, 0);
GameObject currentObject = Instantiate(fallingObjects[Random.Range(0, fallingObjects.Length)]);
currentObject.transform.SetParent(gameObject.transform, true);
currentObject.transform.localPosition = localSpawnPosition;
StartCoroutine(SpawnObject(Random.Range(minTimeInterval, maxTimeInterval)));
}
}
オブジェクトは、時間間隔の最小と最大で設定した秒数の幅の中で、ランダムに上部から落ちてきます。Random クラスについての詳細は、こちらをご覧ください。
-
Unity で空のオブジェクトを作成し、[Canvas]にドラッグ アンド ドロップし、Rect Transform 要素を追加すると、オブジェクトが常に Canvas の上部に配置されます。上部中央揃えを実行します。その後、このオブジェクトに ObjectSpawner をドラッグ アンド ドロップします。このオブジェクトは、オブジェクトの落下開始位置となる位置を定義します。
ObjectSpawner の設定
-
Unity で、上部から落下するオブジェクトの表示に使用される Capsule と Cube の 2つのプレハブを作成します。ユーザーは、これらのオブジェクトを「破壊」することになります。これらのプレハブに、RigidBody コンポーネントを追加します。オブジェクトを、メインカメラのObjectSpawner セクションにドラッグ アンド ドロップします。作成したプレハブを fallingObjects 配列に入力します。
Capsule と Cube の指定
- 注意
- 落下の速度は、プレハブの空気抵抗の調節によって制御されます (RigidBody > Drag)。値が低いほど、空気抵抗が少なくなります (0 の場合は、空気抵抗なし)。
-
キャンバスの ObjectSpawner にプレハブをドラッグ アンド ドロップします。
Object Spawner で落ちるオブジェクトを指定
-
SegmentPaint スクリプトに、パラメーターを渡して実行するための ObjectSpawner フィールドを追加します。
public class SegmentPaint : MonoBehaviour
{
...
GameColliders gameColliders;
[SerializeField]
ObjectSpawner objectSpawner;
void Start()
-
Start メソッドで、GameObjectSpawner にパラメーターを渡します。
void Start()
{
...
gameColliders.CreateColliders(cols, rows);
objectSpawner.StartSpawn(cols);
}
-
FallingObjects.cs というスクリプトを作成します。落下するオブジェクトと他のオブジェクトの衝突による破壊の状況を定義します。OnCollisionEnter メソッドを作成し、Destroy メソッドを呼び出します。このメソッドを使用するのは、落下するオブジェクトが、何であれオブジェクトにぶつかった場合に破壊されるゲームだからです。
public class FallingObjects : MonoBehaviour
{
private void OnCollisionEnter(Collision collision)
{
Destroy(gameObject);
}
}
- Unity の設定で、このスクリプトを落下するオブジェクト (Capsule, Cube) にドラッグ アンド ドロップします。
-
ObjectSpawner と SegmentPaint.の MainCamera へのレファレンスを作成します。
Segment Paint (Script) の設定
- プロジェクトを実行します。オブジェクトが上から落下し、ユーザー セグメントか一番下のラインにぶつかったオブジェクトは破壊されるようになります。
ユーザー セグメントと落下するオブジェクト
得点の追加
- プロジェクトにゲームの要素を加えましたが、まだゲームとは言えないでしょう。簡単なゲームをもう少し面白みのあるものにするため、オブジェクトをキャッチした時としなかった時の得点を導入します。そのために、新しいスクリプト、GameProgress.cs を作成します。このスクリプトには、このゲームの得点に関するすべての設定が含まれます。
-
このスクリプトで、singleton (自分へのレファレンスを作成) を定義するフィールドを作成します。それにより、落下するオブジェクトは、直接的なレファレンスがなくてもこのクラスのメソッドを呼び出すことができ、出力テキストのフィールド、オブジェクトにぶつかったときに追加する/差し引くポイントの数についても同様です。Singleton についての詳細は、こちらをご覧ください。
public class GameProgress :MonoBehaviour
{
public static GameProgress instance = null;
[SerializeField]
Text scoreText;
int currentScore = 0;
void Awake()
{
if (instance == null)
instance = this;
else if (instance != this)
Destroy(gameObject);
}
}
-
テキストの更新に使用するUpdateScoreText メソッドを作成します。
Destroy(gameObject);
}
void UpdateScoreText()
{
scoreText.text = "Your score:" + currentScore;
}
}
-
AddScore と RemoveScore static メソッドを追加し、加点と減点をそれぞれ定義します。
scoreText.text = "Your score:" + currentScore;
}
public void AddScore(int val)
{
currentScore += val;
UpdateScoreText();
}
public void RemoveScore(int val)
{
currentScore -= val;
UpdateScoreText();
}
}
-
FallingObject.cs スクリプトで、加点/減点する点数を定義するための ScoreValue フィールドを追加します。
public class FallingObjects : MonoBehaviour
{
[SerializeField]
int scoreValue = 5;
}
-
OnCollisionEnter メソッドで、ユーザーが落下するオブジェクトをとらえたときにポイントを加え、オブジェクトをとらえられずに一番下のラインまで落ちてしまった場合にポイントを減らすよう、タグ チェックを追加します。それに加え、active flag を設定することで、ユーザーのシルエットが落下するオブジェクトに触ったときに複数回認識されるのを防ぎます。Destroy メソッドについての詳細は、こちらをご覧ください。
int scoreValue = 5;
bool active = true;
private void OnCollisionEnter(Collision collision)
{
if (!active)
return;
active = false;
Destroy(gameObject);
if (collision.transform.tag == "UserPixel")
GameProgress.instance.AddScore(scoreValue);
else if (collision.transform.tag == "BottomLine")
GameProgress.instance.RemoveScore(scoreValue);
}
}
-
Unity, で、UserPixel と BottomLine プレハブに関連のあるタグを設定します (Add Tag > UserPixel / BottomLine)。
プレハブに必要なタグ
-
キャンバスにテキスト フィールドを作成するには、Game Object > UI > Text を使用し、好きなところにテキスト フィールドを作成します。
新しいテキスト フィールド
-
[メイン カメラ]に、GameProgress (Script) をドラッグ アンド ドロップします。今作成したテキストを[ScoreText]にドラッグ アンド ドロップすると、画面にテキストが表示されます。
テキスト フィールドの指定
- プロジェクトを実行します。落ちてくるオブジェクトをタッチして破壊すれば、加点されるようになりました。オブジェクトをタッチできず、一番下まで落ちた場合は、減点されます。
ユーザー セグメントと得点が表示されているゲームの完成