園児ニアのメモ

ただのエンジニア。何でも屋みたいな扱い受けてます。

UnityのuGUIで縦書きテキスト表示

はじめに

Unityで縦書き表示を使いたい時があると思う。
そういう時のためにメモ。

動作検証環境
MacOS:Sierra10.12.1
Unity:5.5.0

ソース

回転させたくない文字を「NonRotatableCharacters」に入れる。
例:「ー」「。」など...

回転だけだと位置がずれる、回転させなくても位置がおかしいものに関して
Pixelで修正したいものは「ShiftCharacters」に文字を入れ、「ShiftXPixels」「ShiftYPixels」で修正位置を記入する
例:「、」「。」など(Pixelは現在の位置から+-pxで指定)

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections.Generic;
using System.Linq;

[RequireComponent(typeof(Text))]
public class RotateText : UIBehaviour, IMeshModifier
{
	private Text textComponent;
	private string text = null;
	private char[] characters;

	// 回転させない文字群
	[SerializeField] private List<char> NonRotatableCharacters;
	[SerializeField] static int ShiftChar = 0;
	[SerializeField] private char[] ShiftCharacters = new char[ShiftChar];
	[SerializeField] private float[] ShiftXPixels = new float[ShiftChar];
	[SerializeField] private float[] ShiftYPixels = new float[ShiftChar];

	void Update() {
		if (textComponent == null) {
			textComponent = this.GetComponent<Text>();
		} else {
			if (textComponent.text != "") {
				if (textComponent.text != text) {
					text = textComponent.text;
					var graphics = base.GetComponent<Graphic>();
					if (graphics != null) {
						graphics.SetVerticesDirty();
					}
				}
			}
		}
	}

	void OnValidate() {
		textComponent = this.GetComponent<Text>();
		if (textComponent.text != null && textComponent.text != "") {
			if (textComponent.text != text) {
				text = textComponent.text;
				var graphics = base.GetComponent<Graphic>();
				if (graphics != null) {
					graphics.SetVerticesDirty();
				}
			}
		}
	}

	public void ModifyMesh (Mesh mesh) {}
	public void ModifyMesh (VertexHelper verts)
	{
		if (!this.IsActive())
		{
			return;
		}

		List<UIVertex> vertexList = new List<UIVertex>();
		verts.GetUIVertexStream(vertexList);

		ModifyVertices(vertexList);

		verts.Clear();
		verts.AddUIVertexTriangleStream(vertexList);
	}

	void ModifyVertices(List<UIVertex> vertexList) {
		if (textComponent != null) {
			if (textComponent.text != null && textComponent.text != "") {
				characters = textComponent.text.ToCharArray ();
				if (characters.Length == 0) {
					return;
				}

				for (int i = 0, vertexListCount = vertexList.Count; i < vertexListCount; i += 6) {
					int index = i / 6;
					//文字の回転の制御
					if (!IsNonrotatableCharactor (characters [index])) {
						var center = Vector2.Lerp (vertexList [i].position, vertexList [i + 3].position, 0.5f);
						for (int r = 0; r < 6; r++) {
							var element = vertexList [i + r];
							var pos = element.position - (Vector3)center;
							var newPos = new Vector2 (
								            pos.x * Mathf.Cos (90 * Mathf.Deg2Rad) - pos.y * Mathf.Sin (90 * Mathf.Deg2Rad),
								            pos.x * Mathf.Sin (90 * Mathf.Deg2Rad) + pos.y * Mathf.Cos (90 * Mathf.Deg2Rad)
							            );
							element.position = (Vector3)(newPos + center);
							vertexList [i + r] = element;
						}
					}
					//文字の位置の制御
					float[] shiftPixel = GetPixelShiftCharactor (characters [index]);
					if (shiftPixel [0] != 0 || shiftPixel [1] != 0) {
						var center = Vector2.Lerp (vertexList [i].position, vertexList [i + 3].position, 0.5f);
						for (int r = 0; r < 6; r++) {
							var element = vertexList [i + r];
							Debug.Log ("before:" + element.position.x + "," + element.position.y);
							var pos = element.position - (Vector3)center;
							var newPos = new Vector2 (
								            pos.x + shiftPixel [0],
								            pos.y + shiftPixel [1]
							            );
							element.position = (Vector3)(newPos + center);
							Debug.Log ("after:" + element.position.x + "," + element.position.y);
							vertexList [i + r] = element;
						}
					}
				}
			}
		}
	}

	bool IsNonrotatableCharactor(char character) {
		return NonRotatableCharacters.Any(x => x == character);
	}

	float[] GetPixelShiftCharactor(char character) {
		int index = System.Array.IndexOf(ShiftCharacters,character);
		float[] pixel = new float[2];
		if (0 <= index && index < ShiftXPixels.Length && index < ShiftYPixels.Length) {
			pixel[0] = ShiftXPixels[index];
			pixel[1] = ShiftYPixels[index];
		}
		return pixel;
	}
}

UnityAdsを実装してみた

はじめに

動作検証環境
MacOS:Sierra10.12.1
Unity:5.5.0
Xcode:8.1
iPhone6:iOS8.0

広告についてよく知らない、どんな種類があるの?導入する際に気をつけることは?などに関して自分の整理のために以下の記事を書きましたので興味があればご覧下さい。
madgenius.hateblo.jp

では、UnityAdsについて...
UnityAdsはUnityが提供する動画広告の仕組み、またその機能のことを指す。
Unityのゲームをプレイ中に動画視聴でゲーム内通貨プレゼントなどというものを見たことないだろうか?主にそういう実装で使用されているためユーザーへの不快感は少ない。

項目 概要
種類 動画(30秒)
収益 CPI型(Install数×広告主の設定価格)
不快感 少なめ
実装コスト 少なめ
提供 Unity

30秒の動画を視聴させユーザーが興味を持ちストアからダウンロードすればInstall数に応じた広告主の設定価格が収益として手に入る仕組みだ。
Unityが提供しているためUnityでの動作の安定性はもちろん実装コストも最低限までに短縮されている。
実装コストとユーザーへの不快感は少ないため実装することに対してのデメリットはない。
CPI型なのが収益へ繋がるか不安という方もいるだろうがUnity製アプリの広告のみが出るため主に高クオリティで動画なので音声やプレイ画面から興味を持つユーザーも多いので心配はないそうだ。
また、クリックが収益になるバナー広告などとも共存は可能。

UnityAdsダッシュボードの設定

UnityAdsのページからUnityのデベロッパーアカウントを使ってログインができる。
また、新規作成もできるので持ってない人でもここで作成可能。
f:id:nanokanato:20170417121402p:plain:w300

アカウント認証後に広告を出すアプリ情報やユーザー情報などを聞かれる場合がありますがあとから変更や削除可能です。またアプリ情報は最新版であればUnityから自動生成ができます。
f:id:nanokanato:20170417121431p:plain:w300

ログインが完了するとAdminのページになる。左は日本版サイト、右は海外用サイトだ。
広告の導入や収益の確認はコインが重なっている方を、広告を配布してユーザー獲得する場合は人型のボタンを押してください。
ここでは広告の出し方に関しては記載しません。
f:id:nanokanato:20170417121448p:plain:w300

広告を掲載しているアプリのダッシュボードです。
画像はログイン時にアプリ情報の入力があったためすでにプロジェクトが追加されています。

  1. 新しいプロジェクトのボタンからいつでも追加できます。

f:id:nanokanato:20170417121523p:plain:w300

プロジェクトの追加画面の入力内容です。プロジェクトをサイトから追加する場合に使用します。
しかし、最新版のUnityでは自動生成されるため不要なので飛ばして結構です。
画面下の13歳以下を対象にしているかのチェックを入れるとおそらく過激な表現(戦争,暴力など?)の動画などは表示されなくなります。もし、過激な表現の動画が表示OKでもアメリカ合衆国13歳以下の児童を対象にしている場合はチェックをつけましょう。
f:id:nanokanato:20170417121545p:plain:w300

以上で完了です。これだけ?って感じですね。
Unity5.1以前の場合はAsset StoreよりUnityAdsのAssetの追加と、作成したプロジェクトごとのIDなどが必要になるようですが....それより新しいバージョンだとサイトでプロジェクトを作成するだけになります。
最新版でAssetを追加するとデフォルトで入っているため競合しエラーが発生します。

UnityAdsサンプル用のプロジェクトの作成

UnityAdsを有効にするためにUnityを起動してのプロジェクトを開きましょう。
プロジェクト名は先ほどサイトで追加したAdsTestです。

ProjectのタブはSceneとScriptのみです。
UnityAdsの実装処理を書くためのUnityAdsScript.csを用意しました。
f:id:nanokanato:20170417121615p:plain:w300

Awakeメソッドの中身は公式の実装方法にはなかったですが追加することで動作するようになりました。
GameIDの部分は置き換えますが次のUnityAdsの有効化で説明します。

using UnityEngine;
using UnityEngine.Advertisements;

public class UnityAdsScript : MonoBehaviour
{
	public UnityEngine.UI.Text CountText;
	private int count = 0;

	void Awake()
	{   
		// まずはAwake()内で、初期化をします。先ほどのゲームIDを入力。
		Advertisement.Initialize ("【GameID】");
	}

	public void ShowRewardedAd()
	{
		if (Advertisement.IsReady("rewardedVideo"))
		{
			var options = new ShowOptions { resultCallback = HandleShowResult };
			Advertisement.Show("rewardedVideo", options);
		}
	}

	private void HandleShowResult(ShowResult result)
	{
		switch (result)
		{
		case ShowResult.Finished:
			Debug.Log ("The ad was successfully shown.");
			//
			// YOUR CODE TO REWARD THE GAMER
			// Give coins etc.
			count++;
			CountText.text = "視聴回数:" + count;
			break;
		case ShowResult.Skipped:
			Debug.Log("The ad was skipped before reaching the end.");
			break;
		case ShowResult.Failed:
			Debug.LogError("The ad failed to be shown.");
			break;
		}
	}
}

HierarchyはCanvasを使用し、Textを2つとButtonを配置。Buttonを押すと動画広告を表示にします。
どこでも大丈夫ですがCanvasにUnityAdsScript.csを追加して、CountTextとHierarchyのCountTextを紐付けます。これによって動画広告が視聴完了して閉じられた時にCountTextに表示している視聴回数をカウントします。
Buttonが押された時UnityAdsScript.csのShowRewardedAd()が呼ばれるように設定します。
f:id:nanokanato:20170417121644p:plain:w300

Sceneが完成しました。
Buttonの文字は「動画広告を表示」に変更。
CountTextは「視聴回数:0」に変更。
DetailTextは「↑これがポイント付与要素になる」に変更しました。
f:id:nanokanato:20170417121711p:plain:w300

UnityAdsの有効化

UnityのWindowからServicesを選択してServicesのタブを表示します。
f:id:nanokanato:20170417121758p:plain:w300

最初にログインを求められることがありますがUnityAdsのサイトのアカウントでログインしてください。
ログインするとサイトにプロジェクトが自動で追加されます。その際設定が必要なIDなども自動で入力してくれています。
サイトで行った13歳以下対象のチェックやプロジェクト名、プロジェクトIDの変更などはSERVICESのところを押すと変更可能です。サイトで作成したプロジェクトを使う場合はプロジェクトIDを変更してください。
今回はAdsを有効にするためAdsを選択しましょう。
Adsの項目があるので
f:id:nanokanato:20170417121814p:plain:w300

デフォルトでは上のスイッチがOFFになってますのでONにしてください。ONで下の項目も表示されます。
GameIDの部分は自動で生成されます。こちらもサイトで作成したプロジェクトを使う場合、置き換えてください。GameIDをUnityAdsScript.csのAwakeメソッドでの初期化に使いますので置き換えてください。
f:id:nanokanato:20170417121829p:plain:w300

動作確認

UnityEditorでもエラーになったりはしないが非対応のような専用画面が表示される。Closeを押すことで動画視聴完了のコールバックが呼ばれ画面を閉じることができる。
f:id:nanokanato:20170417121928p:plain:w300

iOSの実機ではこんな感じ
動画再生中
f:id:nanokanato:20170417121947p:plain:w300
動画再生終了
f:id:nanokanato:20170417122020p:plain:w300

コールバックが無事に呼ばれたので視聴回数が1回になった。
f:id:nanokanato:20170417122039p:plain:w300

Unity for MacでVisualStudioを使用する

UnityをWindowsでやっていた人が、Macでやり始めると思うこと?にMonoDevelopってなんだ?ってのがあるかと思い調べてみた。
MacだとVisualStudioではなくMonoDevelopなので使い勝手がいいほうのエディターに変えたいという人もいるだろう。

Visual Studioをインストール

まず、MacにはVisualStudioはない....(完)
だが、VisualStudioCodeというEditorがあるのでそちらを使うことにする。

サイトのトップよりダウンロードができるのでインストールする。
f:id:nanokanato:20170417115610p:plain:w300

単品で使った場合、WindowsのVisualStadioとはレイアウトなどが違うが、使い勝手や機能は大体同じのようだ。

Unityと連携させるために...

Unityで普通にEditorを変更させようとしたがVisualStudioCodeが起動してそこからMonoDevelopが起動してしまう。

調べているとこんなものが…
VisualStadioCodeをUnityに追加するためのScriptらしい。
VSCode - Unity Visual Studio Code Integration

これをAssets以下に追加する
f:id:nanokanato:20170417115716p:plain:w300

Editorを変更

UnityのツールバーからUnity > Preferencesを選択。
f:id:nanokanato:20170417115749p:plain:w300

PreferencesのWindowが開く
VSCode.csを追加したのでVSCodeという欄が増えている
f:id:nanokanato:20170417115818p:plain:w300

External Toolsを選択してEditor AttachingをCodeに変更
もしくはBrowse...からVisualStudioCodeを選択する。
f:id:nanokanato:20170417115836p:plain:w300

これで連携が完了です。EditorがVisualStudioCodeになりました。

インテリセンス(自動補完)を利用する

VisualStudioCodeをEditorに設定し無事Scriptを編集できたのですが、MonoDevelopにはあるインテリセンスがなかったので設定する。

左側のタブから拡張機能( f:id:nanokanato:20170417115947p:plain:w12 )を選択。OmniSharpで検索してインストールしてください。
f:id:nanokanato:20170417120024p:plain:w300

使い心地

Editorとしては最高だと思います。

  • 拡張機能でインストールすることで多言語対応
  • Windows,Mac,LinaxなどのOSで利用でき、OS間の操作感の違いを吸収
  • Microsoftや利用者の更新頻度が高く、機能拡張が増えている。
  • Methodやif,forなどで折りたたむことができる
  • 左右でエディターの分割ができる。(コピペや他プロジェクトとの比較が楽)
  • 上書き保存していないScriptに応じて左上にバッジ表示(自動保存もあり)
  • インテリセンスは機能拡張から追加できるので独自のものも可能?

など...

f:id:nanokanato:20170417120050p:plain:w300

しかし、Unityで使うには以下の点よりまだ早いようです。

  • Projectに常にVSCode.csを追加しないといけない
  • Debug時にBuild構成ファイルやタスクランナーの構成が必要
  • ブレークポイントの設定がし辛い?
  • Unityでの利用者が少ないので何か起こると困るしかない

それでも多くのOSと多言語に対応しているMicrosoftのフリーエディタですのでどこかのすごい人が解決していそうです。
解決策があればぜひ教えていただきたいです。

KudanARのUnity用SDKのサンプルを検証

KudanARとは

日本人起業家がイギリスで創業した株式会社Kudanが作ったARエンジンのこと。
投資家から2億近く集めたとしてニュースにもなったりした。
もともとKudanはARアプリを開発しており、ARのSDK,Vuforiaを作っていたが大手企業の修正に対応するにあたり限界が出たため自社開発された。

KudanARの特徴は今までの空間認識などのARにGPS、コンパス、時刻、天気情報などから太陽の位置、光源を特定し3Dオブジェクトへ環境光を反映させることができるらしい...
また、単眼レンズで読み取った映像から対象物の距離や大きさを推定できると書いてある。このような機能をSLAMというそうだがその機能はVuforiaにはないものだ。

対応開発環境はNativeSDKとしてiOS, Android、Cross-platformとしてUnityが用意されている。

  • ライセンスに関して
    • Development SDK:サンプル動作,検証用
    • Production License:£1000/1アプリ年間(約140,264円-2016/11/24現在)
      • £(ポンド)での支払いのみ。日本円は現在対応中だとのこと。
      • 一応日本に会社がありメールフォームがあるので問い合わせ次第では...
    • Volume License:複数アプリの場合は問い合わせにて割引できるそう?

SDKのダウンロード

今回はサンプルを動かすだけなのでKudanのライセンスの支払いはいりません。
公式ページより、PRICING / DOWNLOADのページへ遷移しましょう。
Free Downloadを押して、Unityを選択。
支払いページに飛びますが、£0.00なのでそのまま名前などを入力しちゃいましょう...

  • Email*:メールアドレス(必須)
  • First name*:名前(必須)
  • Last name*:苗字(必須)
  • Address*:URL?(必須)
  • Country *:国(必須)
  • VAT:EUの付加価値税らしい..とりあえず未記入
  • User profile*:あなたのプロフィール(必須)
    • Student:学生
    • Independent Developer:独立開発者
    • Company:企業
    • Others:その他
  • Number of employees:従業員数、企業の時に選択するものだと思われ
    • 0~10,10~50,50~100,100以上から選ぶ
  • Reason for download*:ダウンロードの理由(必須)
    • Evaluation:評価、うわさとかそういう意味でしょうか?
    • App Development:アプリ開発
    • Research:検証,研究
    • Others:その他
  • App category*:アプリのカテゴリ
  • How did you hear about us?*:どこで知りましたか?
    • 技術者ブログとでも書いておけばok?

上記の入力とともにUsername,Passwordをしましょう。(このUsernameとPasswordはのちほど使うので覚えておこう)
Show Termsが利用規約です。英語だから少しつらい。
Terms and Conditions(利用規約に同意)にチェックするのを忘れずに、入力内容が消えます。
画面下のDownloadよりアカウントの作成とダウンロード画面への遷移のようです。

「Kudan AR Unity Package – Special Note for Windows Users: Please download the Kudan AR Toolkit from our」と出たのをクリックしたらダウンロードが始まります。

SDK同包のパッケージ、KudanARUnity.unitypackageをダウンロードできました。
しかし、パッケージの中やこれと同包の説明PDFなどは見つからないのであった...(公式サイトのDEMOの方に動画のサンプルや解説はあります。)
f:id:nanokanato:20170417111155p:plain:w50

パッケージの追加

パッケージを追加するために新規でプロジェクトを作成しましょう。
プロジェクトを作成したら先ほどダウンロードしたKudanARUnity.unitypackageをダブルクリックします。
Import用のウィンドウが出てくるのでとりあえず全て選択してImportします。
f:id:nanokanato:20170417111220p:plain:w300

サンプルの再生

サンプル読み込み

Assets/Samples/にサンプルのsceneがいくつかありますので再生してみたいと思います。
Samplesの中にAngelScene.unityというのがあるのでダブルクリック。
HierarchyにGameObjectが読み込まれて、
f:id:nanokanato:20170417111250p:plain:w300
なんかボタンと天使らしきものが出てきます。
f:id:nanokanato:20170417111308p:plain:w300

サンプルの再生時のエラー

ボタンを押すとどうなるのか、天使はなんなのか期待しながら再生!
「!!」エラーです!
Editor Play Mode Keyが有効ではありません。
バンドルのライセンスキーは無効です。
f:id:nanokanato:20170417111339p:plain:w300

怪しいのはあきらかにこれだ...
KudanCameraのKudanTracker.csにEditor API Keyがある。
f:id:nanokanato:20170417111359p:plain:w300

Get Editor API Keyとあるので押してみると外部サイトに飛ばされました。
英語のKudanのページにてログインを求められるのでログインする。(SDKダウンロード時に入力したUsernameとPassword)
ログイン後、Request Unity Editor Keyを押すとKeyを受け取ることができました。
受け取ったKeyはInspectorのEditor API Keyに入力します。
これで「[KudanAR] Editor Play Mode Key is NOT Valid」は解決します。

次はInspectorのSet App/Bundle IDを押下します。
※UnityのツールバーのFile>Build SettingsからPlayer Settingsでも同じ
f:id:nanokanato:20170417111507p:plain:w300
ここの青字のBundle Identifierを変えればいいと思うのだが、何をいれればいいかわからない。(なんでもいいなら今のでエラーが出ないはず)

再度Get Editor API Keyのボタンを押してみるとページに「For development license keys please click here」というリンクがあったので押すとDevelopment License Keysというページへ遷移する。
f:id:nanokanato:20170417111528p:plain:w300
Bundle IDとLicense Keyのリストがあるのでこれを使用する。
※念のためモザイクをかけております

このBundle IDでいける!と入力するとまた「[KudanAR] License Key is INVALID for Bundle」のエラーが出ました。
試行錯誤すること10分ほど...
上のページで取得したBundle IDはBundle Identifierに入力したまま。
その対にページで書かれているLicense KeyをKudanTracker.csのAPI Keyに入力することで解決です!
これで「[KudanAR] License Key is INVALID for Bundle: com.Company.ProductName」も解決しました。

サンプルを再生する

ではやっとAngelSceneを再生です!
f:id:nanokanato:20170417111633p:plain:w300
WebカメラがPC本体のものじゃなくてUSBのWebカメラなど別のものに変えたい方はKudanTracker.csのPlay Mode Web Cam IDを変更してみてください。0がデフォルトのカメラです。

カメラが映ったのと、エラーが出ないので再生はしているがボタンを押したりしても大きな変化はない。ARの画像を写していないからだ。
「Assets/KudanAR/Samples/Texture/」にlego.jpgとBatmanLegoMovie.pngがある。

lego.jpgをiPhoneで表示してそれを撮影してみたらこんな感じ。
iPhoneに写しているためたまに画面反射で認識しなくなるが端末を回転させると天使の像も回転したり、遠近で大きさも変わるようになっている。
f:id:nanokanato:20170417111705p:plain:w300

他のサンプルも基本的に同様に再生が可能なはずです。
Bundle IDはプロジェクト単位なので保存されますが、API KeyはSceneごとなので設定して保存しましょう。一回設定すればその後は不要です。(公式にて変更がない限り...)

サンプル説明

AngelScene.unity

今回の解説で使った天使の像。
lego.jpgをカメラで撮ると天使の像が現れる。
f:id:nanokanato:20170417111736p:plain:w300

KudanSample.unity

lego.jpgをカメラで撮ると球体が現れる。
f:id:nanokanato:20170417111751p:plain:w300

全体的にARに言えることは感動を伝えたくてプレイ中のスクショを見せても相手には伝わらず楽しくはない。
次からはUnityのプレイ画面を動画撮影してそれのGIFなどにしてみたいと思う...

SmartARのUnity用SDKのサンプルを検証

検証環境
Windows:OS.10
Mac:Sierra(10.12.1)
Unity:5.3.2 Free版
Xcode:8.1
iOS:8.0
SmartAR:1.1.0

SmartARとは

SmartARについてとSDKをダウンロードするまではこちらをどうぞ
madgenius.hateblo.jp

パッケージを追加

とりあえず、パッケージを追加するために新規でプロジェクトを作成。
サンプルがあるsmartar_moving_target.unitypackageをダブルクリックでいつもAssetのImportのWindowが出てきます。
Importの速度は速いので待ち時間はそんなにないです。

Importされました。
PluginとSmartARのフォルダ、StreamingAssetsには辞書ファイルが追加されました。
f:id:nanokanato:20170417102232p:plain:w300

サンプル再生するまでの失敗

いくつかサンプルを再生するまでに失敗がありましたので途中で詰まっている方などのために経緯を書いておきます。(2016/11/28現在時点)
Sceneは「Assets/SmartAR/Sample(You can remove from project.)/Scenes/」に「sample_api_full.unity」があるのでこちらを使用しています。

MacのUnityEditorでは再生できない

ビルド時エラー(Mac UnityEditor)

(ん?辞書作成ツールがMac非対応、PluginsにMacがないという走馬灯...)
公式のPDFによるとMacでのUnity Editorでの再生はできないとのこと。
つまり開発はWindowsでやってiOSに入れる時だけMacXcode向けにビルドしろってことみたい。
いずれは公式にMac対応していただきたい...

Unity5.3.2より新しいバージョンでは動作できない

MacがダメらしいのでWindowsに切り替えてUnity5.4.1を起動してサンプルを読み込み動作させる

ビルドするとUnityがクラッシュ!
これは心折れるやつ。

公式PDFに5.3 ~ 5.4.0に対応しているとのこと。
しかし、5.4.0に変えてもダメ...と少しずつダウングレードしたところ5.3.2でクラッシュはしなくなりました。

これに関しては公式がそのうち対応してくれるでしょう。

GraphicsAPIをOpenGLに変えないといけない

以下引用は解決するまでに試したこととして省略可なので流し読み程度でどうぞ。

調べるとUnityをOpenGLモードで起動するのが必須とのこと。

先ほどのショートカットをこのまま起動するのではなく、右クリックでプロパティを開きます。
ショートカットのリンク先に"~~Unity.exe"とあるので後ろに` -force-opengl`と追記して適用してOKしましょう。名前もわかりやすいように変更しました。
f:id:nanokanato:20170417102659p:plain:w300 f:id:nanokanato:20170417102716p:plain:w50

作ったショートカットからUnityを起動して作成したプロジェクトを開く。
上のバーにOpenGLと書いてあるとOpenGLモードになっているようです。
f:id:nanokanato:20170417102743p:plain:w300

ビルド!動作しない!(いつもの)

しかもウィンドウをリサイズしたら変になったのでUnity再起動。
f:id:nanokanato:20170417102759p:plain:w300

Documentに入っていたPDF、SmartARSDK-Unity_j.pdfのp20に以下の文が...

「SmartARTM Unity3DPluginではGraphicsAPIにOpenGLを使用するように設定する必要があります。」

とりあえず、UnityのツールバーのFile > Build Settings...からBuild Settingsを開く。
Player Settings...を押すとInspectorのウィンドウにPlayerSettingsが表示される。
Other SettingsのGraphics APIのチェックを外し、OpenGLCoreのみを選択。
Windowsの場合。Android,iOSは別の設定がありました。PDFをご覧ください。
f:id:nanokanato:20170417102956p:plain:w300

そしてPDFによるとこの設定を行った場合OpenGLモードは必要ないそうです!
なのでOpenGLモードのためにやったこと(上記,引用文の作業)は戻しました...

Editorでの再生にはUnity Proライセンス必須

そしてPDFには続きでこんなことが...
「Unity Editor 上で実行するには Unity Pro ライセンスが必須です。」

Proライセンスを持っていないのでビルドするとエラーがたくさん出てきます。

Xcodeにいきなり吐き出してビルド

Editorで動作できないのでiOS向けに書き出して動作確認したいと思います。
iOS,Androidでの動作にはUnity Proライセンスが必須と書いていないためです。
Xcodeに書き出すだけなのでWindowsからまたMacへ戻ってきました...

sceneをビルドしてXcodeで実行したらカメラの許可などを求められてそのままエラーが出ます。
f:id:nanokanato:20170417103127p:plain:w300

エラーの箇所のソースを見るとライセンスキーを入れろという内容っぽい。ですよね。
SmartARSDK-Overview_j.pdfの7Pにはライセンスのことが書いてあるがUnity向けの説明はない??

SmartARSDK-Overview_j.pdf(7P)ライセンス認証について
SmartARTM SDKはライセンス認証を行います。
ライセンスファイルを、Androidの場合はassetsフォルダ以下に配置、iOSの場合はプロジェクトに追加して、アプリケーションのリソースとして含まれるようにし、SarSmartの初期化時にファイルパスを指定することで認証を行うことができます。
ライセンスが認証されていない状態でもSmartARTMを使用することが出来ますが、以下の機能が制限されます。
SarCameraDeviceから取得した以外の画像を認識処理に渡すことができません
SarCameraDeviceから取得したカメラ画像を描画するためにはSarCameraImageDrawerを使用する
必要があります
カメラ画像描画にはWatermarkが付与されます
ライブラリの画像キャプチャ機能を使用してカメラ画像を取得することができません
具体的なAPIの使用制限については「SmartARSDK-Reference_j.pdf」を参照して下さい。
サンプルプログラムではライセンスファイルとしてlicense.sigというファイルをリソースとして持ち、ライセンス認証を行っています。(ダミーファイルのため、実際には認証処理に失敗します。)
SarSmartの初期化処理は SDK/Sample/mobile_common/sample_simple/sample_simple.ccの初期化部分を参考にすることが出来ます。

実際UnityのパッケージでImportした内容にはlicense.sigなんてものはない。

そこで、iOSSDKのサンプルを解凍して見てみるとlicense.sigというファイルがありました。
license.sigを「Assets/StreamingAssets/」に追加し、SmartAR CameraのGameObjectに追加されているSmartARController.csのLicense File Nameに「license.sig」と追加してみました。
f:id:nanokanato:20170417103343p:plain:w300

これでエラーが解決しました。
ただし、PDFにもあるようにライセンスの認証に失敗しているのでビルド時にSmartARのロゴは表示されます。

サンプルの再生

上のいろいろなエラーなどを乗り越えXcodeから実機にビルドできましたので動作の紹介。

起動時カメラ認証を求められてカメラの画面が表示されます。
f:id:nanokanato:20170417103459p:plain:w300

左下のResetでカメラのリセットです。ARマーカーや特徴点を再度読み込みなおします。
右下のMenuで画面中央にMenuが出てきます。
f:id:nanokanato:20170417103524p:plain:w300

iOSSDKに入っていたARマーカー「Sample/ios/target_picture/smartar02.png」を撮影すると綺麗に重なるようにオブジェクトが配置されました。
f:id:nanokanato:20170417103602p:plain:w300

ある程度マーカーが画面外になったりしても一度認識すれば補完してくれるみたいで、追従性がありました。
iOSのサンプルにはARマーカー以外にも特徴点のみから表示する3D空間認識ARのサンプルがあったのですがUnity版のサンプルにはないみたいです。(引き続き調査中)

HTC-VIVEコントローラーの認識が入れ替わるのを制御

はじめに

今回はHTC-VIVE(SteamVR)のコントローラーの認識について書きます。

前提 :コントローラーの左手を1P、右手を2Pとして2人プレイのゲームを作成。
    HMD(ヘッドマウントディスプレイ)は使用しない。
問題点:起動時とSceneの再読み込み時に1Pで持っていたのが2Pと認識される。
    プレイ中に認識が外れるとたまに1Pと2Pが入れ替わる。
f:id:nanokanato:20170414140332j:plain:w300

今回は問題点を解決するためにやったことを記載します。

解決策

  • 解決案1:1Pと2Pが入れ替わったらプレイヤーに持ち替えてもらう
    • なんか気持ち悪いので却下
  • 解決案2:1Pと2Pの立ち位置をある程度決めておき、位置を判別して1P,2Pを判別する
    • これを採用

なぜ左右を誤認識するのか

HTC-VIVEのコントローラーの認識と処理までの流れについて

  • SteamVR_ControllerManager:Deviceの認識
    ↓ SetDeviceIndexでDeviceIDを指定
  • SteamVR_TrackedObject:Deviceの位置をGameObjectに反映
    ↓ index(public変数)でDeviceIDを共有
  • ControllerManager(left):DeviceIDからDevice情報の取得

調査した結果、今回の問題はSteamVR_ControllerManagerから送られてくるDeviceIDが入れ替わっているのが問題だとわかった。

SteamVR_ControllerManager.csではRefresh()のメソッドでデバイスの認識が行われている。(認識ができなくなったり再認識時も呼ばれる)
ここの処理でDeviceIDを取得する時にETrackedControllerRole.LeftHand(左手=1P)を指定してもまれに右手(2P)のコントローラーのIDが返ってきていることがわかった。(逆にRightHandの時は左手が返る)

uint leftIndex = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand)

対策:DeviceIDのチェックを追加

左右が間違って認識されて返ってきた時、コントローラーの位置をチェックしてより左にいた方が左手(1P)というふうに修正する。
DeviceIDから位置を取得する方法はSteamVR_TrackedObject.csで行なっていた以下の処理を利用した。

SteamVR_Events.Action newPosesAction; //コントローラーの位置が変わったら通知
TrackedDevicePose_t[] trackedDevicePoses; //コントローラーの位置

void Awake () {
    newPosesAction = SteamVR_Events.NewPosesAction(OnNewPoses); //通知を受け取る設定
}

private void OnNewPoses(TrackedDevicePose_t[] poses) {
    trackedDevicePoses = new TrackedDevicePose_t[poses.Length];
    poses.CopyTo (trackedDevicePoses, 0); //コントローラーの位置を保持
}

Refreshの中でDeviceIDを受け取ったらコントローラーの位置を比較してより左のほうをleftIndexにする。
ソース内で1秒後に再度チェックするのは起動時やSceneの読み込み時はコントローラーの位置が取れるより先にRefresh()が呼ばれるため。

public void Refresh() {
    int objectIndex = 0;

    //【DeviceIDの取得開始】
    var system = OpenVR.System;
    if (system != null) {
        uint left_index = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand);
        uint right_index = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.RightHand);

        if (left_index != OpenVR.k_unTrackedDeviceIndexInvalid && right_index != OpenVR.k_unTrackedDeviceIndexInvalid) {
            //SteamVRの認識では左右どちらも認識できている
            // →コントローラーの位置をチェックして左右が正しいか確認
            if (trackedDevicePoses != null && left_index < trackedDevicePoses.Length && right_index < trackedDevicePoses.Length) {
                //コントローラーの位置が取得できた
                // →左にある方が左手(1P)、右にある方が右手(2P)に修正
                if (leftRigidTransform.pos.x < rightRigidTransform.pos.x) {
                    leftIndex = left_index;
                    rightIndex = right_index;
                } else {
                    leftIndex = right_index;
                    rightIndex = left_index;
                }
            } else {
                //コントローラーの位置が取得できなかった
                // →SteamVRを信じて一旦DeviceIDを送り、1秒後に再度チェックする
                leftIndex = left_index;
                rightIndex = right_index;
                Invoke("Refresh",1.0f);
            }
        } else {
            //SteamVRの認識では右または左、もしくはその両方の認識ができていない
            // →どちらも認識できなかったことにする
            leftIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
            rightIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
        }
    }
    //【DeviceIDの取得終了】
    
    //【TrackedObejctに送る処理は省略】
}

また、DefaultのソースだとRefresh()でDeviceIDが取得できなかったらとりあえず取得できたDeviceを右手として認識してTrackedObejctに送るという処理が入っているため外す。

public void Refresh() {
    int objectIndex = 0;

    //【DeviceIDの取得処理】

    //【TrackedObejctに送る処理開始】
    if (leftIndex != OpenVR.k_unTrackedDeviceIndexInvalid && rightIndex != OpenVR.k_unTrackedDeviceIndexInvalid) {
        //左右どちらのDeviceIDも認識できている
        //右用のDeviceIDを割り当てる
        uint rightTrackedDeviceIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
        if (rightIndex < connected.Length && connected[rightIndex]) {
            rightTrackedDeviceIndex = rightIndex;
        }
        SetTrackedDeviceIndex(objectIndex, rightTrackedDeviceIndex);
        objectIndex++;
        
        //左用のDeviceIDを割り当てる
        uint leftTrackedDeviceIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
        if (leftIndex < connected.Length && connected[leftIndex]) {
            leftTrackedDeviceIndex = leftIndex;
        }
        SetTrackedDeviceIndex(objectIndex, leftTrackedDeviceIndex);
        objectIndex++;
    }

    //残りのコントローラーはリセットします。
    while (objectIndex < objects.Length) {
        //割り振られていないコントローラーはリセット
        SetTrackedDeviceIndex(objectIndex++, OpenVR.k_unTrackedDeviceIndexInvalid);
    }
  //【TrackedObejctに送る処理終了】
}

 
これらの修正で1P,2Pの認識が入れ替わる問題は解決した。
以下が修正したソースです。

//======= Copyright (c) Valve Corporation, All rights reserved. ===============
//
// 目的:接続と割り当てられた役割に基づいてオブジェクトを有効/無効にします。
//
//=============================================================================

//=========== Measures against problems that replace left and right ===========
//
// SteamVR_TrackedObjectへコントローラーの役割(DeviceID)を割り当てます。
// 割り当てられたDeviceIDがOpenVR.k_unTrackedDeviceIndexInvalidの時は接続されていません。
// 接続されたコントローラーのDeviceIDはHMD(ヘッドマウントディスプレイ)の他にDevice1~15に割り振られる
// 
// ControllerManagerなどでDeviceIDからDevice情報を取得する場合は接続されているかの確認が必要です。
// 
//=============================================================================

using UnityEngine;
using System.Collections.Generic;
using Valve.VR;

public class SteamVR_ControllerManager : MonoBehaviour 
{
	public GameObject left, right;
	public GameObject[] objects; //追加のコントローラに割り当てるオブジェクトを設定する

	public bool assignAllBeforeIdentified; //そのオブジェクトの役割(左から右)が識別される前にオブジェクトに任意に割り当てられるようにする場合はtrueに設定します

	uint[] indices; //役割(DeviceID)の配列
	bool[] connected = new bool[OpenVR.k_unMaxTrackedDeviceCount]; //コントローラーのみ

	//キャッシュされた左右コントローラーの役割 - 接続されている場合とされていない場合
	uint leftIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
	uint rightIndex = OpenVR.k_unTrackedDeviceIndexInvalid;

	SteamVR_Events.Action inputFocusAction, deviceConnectedAction, trackedDeviceRoleChangedAction;

	static string[] labels = { "left", "right" };

	SteamVR_Events.Action newPosesAction;

	//GameObjectが初期化されEnable(表示)になったときに呼ばれる
	void Awake() {
		newPosesAction = SteamVR_Events.NewPosesAction(OnNewPoses);
		UpdateTargets();
		inputFocusAction = SteamVR_Events.InputFocusAction(OnInputFocus);
		deviceConnectedAction = SteamVR_Events.DeviceConnectedAction(OnDeviceConnected);
		trackedDeviceRoleChangedAction = SteamVR_Events.SystemAction("TrackedDeviceRoleChanged", OnTrackedDeviceRoleChanged);
	}

	//GameObjectがEnable(表示)になったときに呼ばれる
	void OnEnable() {
		for (int i = 0; i < objects.Length; i++) {
			var obj = objects[i];
            if (obj != null) {
                obj.SetActive(true);
            }
		}

		Refresh ();

		for (int i = 0; i < SteamVR.connected.Length; i++) {
			if (SteamVR.connected [i]) {
				OnDeviceConnected (i, true);
			}
		}

		newPosesAction.enabled = true;
		inputFocusAction.enabled = true;
		deviceConnectedAction.enabled = true;
		trackedDeviceRoleChangedAction.enabled = true;
	}

	//GameObjectがDisable(非表示)になったときに呼ばれる
	void OnDisable() {
		newPosesAction.enabled = false;
		inputFocusAction.enabled = false;
		deviceConnectedAction.enabled = false;
		trackedDeviceRoleChangedAction.enabled = false;
	}

	//Awake()から呼ばれます。
	//実行時に左、右、またはオブジェクトを更新する場合(たとえば、動的に生成された場合など)、これを呼び出す必要があります。
	public void UpdateTargets() {
		//左と右のエントリをリストの先頭に追加すると、リスト自体を操作するだけです。
		var additional = 0;
		if (this.objects != null) {
			additional = this.objects.Length;
		}
		var objects = new GameObject[2 + additional];
		indices = new uint[2 + additional];
		objects[0] = right;
		indices[0] = OpenVR.k_unTrackedDeviceIndexInvalid;
		objects[1] = left;
		indices[1] = OpenVR.k_unTrackedDeviceIndexInvalid;
		for (int i = 0; i < additional; i++) {
			objects[2 + i] = this.objects[i];
			indices[2 + i] = OpenVR.k_unTrackedDeviceIndexInvalid;
		}
		this.objects = objects;
	}

	//ダッシュボード(HMDのシステムボタンで表示できる設定画面)が起動しているときにコントローラを非表示にします。
	private void OnInputFocus(bool hasFocus) {
		if (hasFocus) {
			for (int i = 0; i < objects.Length; i++) {
				var obj = objects[i];
				if (obj != null) {
					var label = (i < 2) ? labels[i] : (i - 1).ToString();
					ShowObject(obj.transform, "hidden (" + label + ")");
				}
			}
		} else {
			for (int i = 0; i < objects.Length; i++) {
				var obj = objects[i];
				if (obj != null) {
					var label = (i < 2) ? labels[i] : (i - 1).ToString();
					HideObject(obj.transform, "hidden (" + label + ")");
				}
			}
		}
	}

	// 新しいオブジェクトを管理しなおし、そのオブジェクトを非アクティブにします。
	// OnDeviceConnectedでSetActiveを単独で呼び出すことができます。
	private void HideObject(Transform t, string name) {
		var hidden = new GameObject(name).transform;
		hidden.parent = t.parent;
		t.parent = hidden;
		hidden.gameObject.SetActive(false);
	}
	private void ShowObject(Transform t, string name) {
		var hidden = t.parent;
		if (hidden.gameObject.name != name)
			return;
		t.parent = hidden.parent;
		Destroy(hidden.gameObject);
	}

	//役割(DeviceID)をSteamVR_TrackedObjectに付与します。
	private void SetTrackedDeviceIndex(int objectIndex, uint trackedDeviceIndex) {
		//最初にこのインデックスを誰も使用していないことを確認してください。
		if (trackedDeviceIndex != OpenVR.k_unTrackedDeviceIndexInvalid) {
			for (int i = 0; i < objects.Length; i++) {
				if (i != objectIndex && indices[i] == trackedDeviceIndex) {
					var obj = objects[i];
					if (obj != null) {
						obj.SetActive(false);
					}

					indices[i] = OpenVR.k_unTrackedDeviceIndexInvalid;
				}
			}
		}

		//変更時のみ設定します。
		if (trackedDeviceIndex != indices[objectIndex]) {
			indices[objectIndex] = trackedDeviceIndex;

			var obj = objects[objectIndex];
			if (obj != null) {
				if (trackedDeviceIndex == OpenVR.k_unTrackedDeviceIndexInvalid) {
					obj.SetActive(false);
				} else {
					obj.SetActive(true);
					obj.BroadcastMessage("SetDeviceIndex", (int)trackedDeviceIndex, SendMessageOptions.DontRequireReceiver);
				}
			}
		}
	}

	//割り当てられた役割を監視する
	private void OnTrackedDeviceRoleChanged(VREvent_t vrEvent) {
		Refresh();
	}

	//接続されたコントローラのインデックスを記録します。
	private void OnDeviceConnected(int index, bool connected) {
		bool changed = this.connected[index];
		this.connected[index] = false;

		if (connected) {
			var system = OpenVR.System;
			if (system != null) {
				var deviceClass = system.GetTrackedDeviceClass((uint)index);
				if (deviceClass == ETrackedDeviceClass.Controller || deviceClass == ETrackedDeviceClass.GenericTracker) {
					this.connected[index] = true;
					changed = !changed; //同じインデックスをクリアして設定すると、何も変わりません
				}
			}
		}

		if (changed) {
			Refresh();
		}
	}

	//コントローラーのポーズをハンドリングして保存
	private void OnNewPoses(TrackedDevicePose_t[] poses) {
        trackedDevicePoses = new TrackedDevicePose_t[poses.Length];
        poses.CopyTo (trackedDevicePoses, 0);
	}

	//コントローラーの役割情報(DeviceID)を割り当てます。
	TrackedDevicePose_t[] trackedDevicePoses;
	public void Refresh() {
		int objectIndex = 0;

		var system = OpenVR.System;
		if (system != null) {
			uint left_index = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand);
			uint right_index = system.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.RightHand);

			if (left_index != OpenVR.k_unTrackedDeviceIndexInvalid && right_index != OpenVR.k_unTrackedDeviceIndexInvalid) {
				//SteamVRの認識では左右どちらも認識できている
				// →コントローラーの位置をチェックして左右が正しいか確認
				if (trackedDevicePoses != null && left_index < trackedDevicePoses.Length && right_index < trackedDevicePoses.Length) {
					//コントローラーの位置が取得できた
					// →左にある方が左手(1P)、右にある方が右手(2P)に修正
					if (leftRigidTransform.pos.x < rightRigidTransform.pos.x) {
						leftIndex = left_index;
						rightIndex = right_index;
					} else {
						leftIndex = right_index;
						rightIndex = left_index;
					}
				} else {
					//コントローラーの位置が取得できなかった
					// →SteamVRを信じて一旦DeviceIDを送り、1秒後に再度チェックする
					leftIndex = left_index;
					rightIndex = right_index;
					Invoke("Refresh",1.0f);
				}
			} else {
				//SteamVRの認識では右または左、もしくはその両方の認識ができていない
				// →どちらも認識できなかったことにする
				leftIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
				rightIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
			}
		}

		if (leftIndex != OpenVR.k_unTrackedDeviceIndexInvalid && rightIndex != OpenVR.k_unTrackedDeviceIndexInvalid) {
			//左右どちらのDeviceIDも認識できている
			//右用のDeviceIDを割り当てる
			uint rightTrackedDeviceIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
			if (rightIndex < connected.Length && connected[rightIndex]) {
				rightTrackedDeviceIndex = rightIndex;
			}
			SetTrackedDeviceIndex(objectIndex, rightTrackedDeviceIndex);
			objectIndex++;

			//左用のDeviceIDを割り当てる
			uint leftTrackedDeviceIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
			if (leftIndex < connected.Length && connected[leftIndex]) {
				leftTrackedDeviceIndex = leftIndex;
			}
			SetTrackedDeviceIndex(objectIndex, leftTrackedDeviceIndex);
			objectIndex++;
		}

		//残りのコントローラーはリセットします。
		while (objectIndex < objects.Length) {
			//割り振られていないコントローラーはリセット
			SetTrackedDeviceIndex(objectIndex++, OpenVR.k_unTrackedDeviceIndexInvalid);
		}
	}
}

まとめ

今回は1Pが左に立ち2Pが右に立つということで位置を利用して誤認識を正しましたが、残念ながら1人プレイの時に左右が入れ替わるなどの問題には対応できていません。
また、2人プレイでも場所を左右に動きまわるゲームだと左右で判別できないです。

用途によってこのクラスを修正するのもありですが、Deviceの製造番号など一意のものが取得できればそれで判別するのがベストだと思います。

OpenCV for Unityの動作をデバッグする方法

このページではOpenCVをUnityで扱うためのAsset「OpenCV for Unity」を利用した際のデバッグ方法をメモしています。
OpenCVForUnityを利用する際の手順に関しては以下をご覧ください。
madgenius.hateblo.jp
こちらはUnity5.4.1で作業しました。

Debug.Log()

自分の実装した処理を確認する時

Unityの基本のデバッグ方法ですがDebug.Log()でどこまで処理が走っているかや、データが正しいかなどを確認できます。

if (texture != null && colors != null) {
    Debug.Log("textureとcolorsが初期化されているので処理続行");
    Utils.matToTexture2D (mat, texture, colors);
} else {
    Debug.LogError("textureとcolorsが初期化されていないので処理できません!");
}

try catch

OpenCVForUnityの内部処理を確認する時

OpenCVForUnityのライブラリの中にはthrowをしている部分が多数ありますので受け取ることができます。
例えばmatToTexture2DでMatのサイズとTexture2Dのサイズが違う時などにthrowされるのでそれをcatchしたらエラーで処理を止めずにTexture2Dを作り直すなどができると思います。

try {
    Utils.matToTexture2D (mat, texture, colors);
} catch (System.ArgumentException e) {
    Debug.Log ("ArgumentExceptionが起きた!内容:" + e.Message);
} catch (System.Exception e) {
    Debug.Log ("Exceptionが起きた!内容:" + e.Message);
}

Utils.setDebugMode()

OpenCVの内部処理を確認する時

OpenCVForUnityではなく、OpenCVの内部で起きたエラーをエラーログとして表示します。引数にtrueを送るとエラーログの表示がONに、falseでOFFになります。

Utils.setDebugMode(true);
Utils.matToTexture2D (mat, texture, colors);
Utils.setDebugMode(false);