ARToolkitをUnityで使ってみる
これはなに?
ARToolkit:1999年から存在するARライブラリ。マーカーベースARの先駆け
ARToolkit5が2015年にオープンソース化し、いろんなforkが生まれた
そんなforkの中からUnityで使えそうなものを調べてみる
ちなみに,ARToolkitの原初の論文(Marker tracking and HMD calibration for a video-based augmented reality conferencing system)の第一著者は現在奈良先端大(執筆当時は広島市立大学)に所属している加藤博一先生らしい.
H. Kato and M. Billinghurst, "Marker tracking and HMD calibration for a video-based augmented reality conferencing system," Proceedings 2nd IEEE and ACM International Workshop on Augmented Reality (IWAR'99), San Francisco, CA, USA, 1999, pp. 85-94, doi: 10.1109/IWAR.1999.803809. keywords: {Calibration;Augmented reality;Collaborative work;Virtual reality;Collaboration;Computer interfaces;Computer displays;Humans;Virtual environment;Computer vision},
ARToolkit一覧
順次追加予定
ARToolkit5
オリジナルARToolkitのアーカイブ
C++で記述されている
当然ながらメンテは終わってるので今だとちょっと古いかも?
ARToolkitX
ARKitチームが作った現代版ARKitらしい
ちゃんとオープンソース
メインrepoはC++と,iOS,Androidの実装が含まれる
ちゃんとメンテされてるっぽいので期待
Unityの公式SDKもあるみたい
NyARToolkit
Ryo Iizukaさんが作ったARToolkitの実装
全部C#で書いてるやつらしい,すごい
ただ2012年実装なのでさすがに今だと厳しいかも?
大本命ARToolkitXを使ってみる
環境
- Windows 11
- Unity 2022.3.22f1
- arunityx 1.2.11
- Pixel 8 (検証用)
インストール
- Package Managerを開く
- add package from git repo...から,
https://github.com/artoolkitx/arunityx.git#upm
を入力してAdd
とりあえず使う
- 適当なマーカーをプリントアウトする.今回は,下のような適当なQRコード(ARtestという文字列を読み込んでいる)を用意した
- Projectを作成し,arunityx(ARToolkitのUnity用SDK)をインストール
- マーカーの画像をartest.pngにして
StreamingAssets
フォルダに入れる - シーン内に,空のオブジェクトを作成して,名前をARXControllerにする
- ARXControllerオブジェクトに,
ARXController
コンポーネントをアタッチする.アタッチするとARXVideoConfig
も自動的にアタッチされる - ARXControllerオブジェクトに,
ARXTrackable
コンポーネントをアタッチする -
ARXTrackable
コンポーネントに対して次の設定を行う-
Trackable tag
をtestに -
image file
をartest.pngに- この時,コンソールにロードできたかのログが流れるので,ロードできてなかったら画像ファイル名が間違っているかStreamingAssetsフォルダに入っていない
-
- 空のオブジェクトを作成して,名前をOriginにする
- Originに対して,
ARX Origin
をアタッチする - Originの下に空のオブジェクトを作成して,名前をTrackedObjectにする.
- TrackedObjectに対して,
ARXTrackedObject
コンポーネントをアタッチする -
ARXTrackedObject
のTrackable Tag
フィールドにtestと入力- 先ほどの
ARXTrackable
のTrackable tag
に対応する.ここも実行時ではなく静的にロードしてくれるので,コンソールを見てエラーになってそうだったら値を見直す
- 先ほどの
- Main CameraをOriginの中に入れる
- Main Cameraに対して,
ARX Camera
,ARXVideoBakground
をアタッチする - Main CameraのCameraコンポーネントに対して次を設定する
- Clear FlagsをDepth onlyに
- Culling MaskをDefaultのみに
- 光の状況によってはオブジェクトが不自然になるので,お好みでHDRをOffに
- Directional LightをOriginの子オブジェクトにする(?)
- Sampleがこうなってたから倣ってみたけど,必要かどうかは定かではない
- TrackedObjectの子にお好みで適当なオブジェクトを入れる
- ビルドしてテストする
最終的にヒエラルキーの構成としては次のようになるはず
最終的にこんな感じに出てくるはず
ハマったところと疑問
-
なんかEditorでテストできないしUnityが落ちる
- Playmodeに入ろうとするとUnityがクラッシュする
- GitHubのIssueに同じ問題が上がっており,それによると仮想カメラを使うとクラッシュしないらしい
- 実際,OBSのVirtual Cameraを使ってみたらクラッシュはしなかったが,テストはできなかった
- というか仮想カメラが映ってくれてなかったので純粋に僕の使い方が悪いのかも…?
- Editorでテストできると開発上すごく楽になるんだけど…要検証
- ARXTrackableのImage widthがなんかよくわからない
- Imageの実寸を入れるのかと思ったら,なんか実寸に近い値を入れるとむしろレンダリングされなくなる
- Cubeの大きさやオフセットもImage widthによって相対的な調節がされてる様子もあんまりない…なんなんだろうこれ…
- マーカーとの奥行部分の距離は実はARToolkitでは取れないのか…?
- ARX Video Backgroundの動作
- ぶっちゃけこれ何やってるんだ…?
- Layerに指定したレイヤをculling maskに入れなくてもパススルーになってるのでよくわからない
- 要調査・検証
- TrackedObjectの動作
- TrackedObjectってマーカがない限り消えてるけど,これはオブジェクト事態がinactiveになっているんだろうか…?
- スクリプトの制御などに影響ありそうなので要検証
とりあえずオープンソースなので該当箇所のソースを読んでいけば仕様がわかると思われるのでちょこちょこ読んでいく予定
OSS万歳!!!!
TrackedObjectの動作
- 結論から言うと操作できるみたい
- Inspectorが小さくて隠れてたけど,
OnTrackedObjectFoundActiveChildren
とOnTrackedObjectLostDeativeChildren
を切ることで,ChildrenのSetactive状態を制御できるらしい
(変数名長すぎる…どうにかならなかったのかこれ…)
ARXTrackableのImage widthがなんかよくわからない
transform.localScale = Vector3.one; // Local scale is always 1 for now
transform.position = ARXUtilityFunctions.PositionFromMatrix(pose);
transform.rotation = ARXUtilityFunctions.QuaternionFromMatrix(pose);
とりあえずObjectのscaleは変わらないもよう
これに関連して,実際にTrackingの動作がどうなっているのかを見てみる
ARXTrackedObjectのトラッキング中の動作を見てみる
まず,多分オブジェクト移動の大本はこのメソッドみたい(ARXTrackedObject.cs 203行目~)
private void VisibleInternal(Matrix4x4 pose)
{
transform.localScale = Vector3.one; // Local scale is always 1 for now
transform.position = ARXUtilityFunctions.PositionFromMatrix(pose);
transform.rotation = ARXUtilityFunctions.QuaternionFromMatrix(pose);
if (!_visible)
{
// Trackable was hidden but now is visible.
_visible = _visibleOrRemain = true;
OnTrackedObjectFound.Invoke(this);
if (eventReceiver != null) eventReceiver.BroadcastMessage("OnTrackableFound", _trackable, SendMessageOptions.DontRequireReceiver);
if (OnTrackedObjectFoundActivateChildren)
{
for (int i = 0; i < this.transform.childCount; i++)
{
this.transform.GetChild(i).gameObject.SetActive(true);
}
}
}
OnTrackedObjectTracked.Invoke(this);
if (eventReceiver != null) eventReceiver.BroadcastMessage("OnTrackableTracked", _trackable, SendMessageOptions.DontRequireReceiver);
}
とりあえず,このメソッドでは単純に渡されたpose
引数の通りに自分を移動させて,childrenの可視性を変化させるだけみたい
で,このposeがどこからきているかというとARXTrackedObject.csのUpdate()
の中175行目付近で,
ARXTrackable baseTrackable = origin.GetBaseTrackable();
if (baseTrackable != null && trackable.Visible)
{
VisibleInternal(trackable == baseTrackable ? origin.transform.localToWorldMatrix : (origin.transform.localToWorldMatrix * baseTrackable.TransformationMatrix.inverse * trackable.TransformationMatrix));
}
else /* (baseTrackable == null || !trackable.Visible) */
{
NotVisibleInternal();
}
なるほど,originっていうやつがよしなにに決めているらしい.
originっていうのはARXOrigin
のインスタンスで,GetOrigin()
メソッドによって取得されている.GetOrigin()
は108行目に記載があって,
public virtual ARXOrigin GetOrigin()
{
if (_origin == null) {
// Locate the origin in parent.
_origin = this.gameObject.GetComponentInParent<ARXOrigin>();
}
return _origin;
}
つまり親オブジェクトのARXOriginをとってくるらしい.
最初に呼ばれているorigin.GetBaseTrackable()
はARXOrigin
の129行目にあって,
public ARXTrackable GetBaseTrackable()
{
if (baseTrackable != null) {
if (baseTrackable.Visible) return baseTrackable;
else baseTrackable = null;
}
foreach (ARXTrackable m in trackablesEligibleForBaseTrackable) {
if (m.Visible) {
baseTrackable = m;
ARXController.Log("Trackable " + m.UID + " became base trackable.");
break;
}
}
return baseTrackable;
}
ここが結構キモで,どうやら最初に検知したTrackableの位置=ARXOriginの位置になるらしい.このベースになっているTrackableがBaseTrackableということっぽい.
それをもとに,新ためてposeの式:
VisibleInternal(trackable == baseTrackable ? origin.transform.localToWorldMatrix : (origin.transform.localToWorldMatrix * baseTrackable.TransformationMatrix.inverse * trackable.TransformationMatrix));
を見ると,まずorigin
はbaseTrackable
にくっついているので,自分が追従しているtrackable
がbaseTrackable
ならorigin
の姿勢を単純に持ってくればいい.(origin.transform.localToWorldMatrix
)
baseTrackable
でない場合,origin
の姿勢はbaseTrackable
の姿勢分本来の原点から離れているので,いったん打ち消してから自分のtrackable
の姿勢を適用すればいい.
回転の打ち消しは逆行列をかければいいので,origin.transform.localToWorldMatrix * baseTrackable.TransformationMatrix.inverse * trackable.TransformationMatrix
となる.
(…あれ?別にoriginはずっと本来の原点にいてやってtrackable.TransformationMatrix
をpose
にしていすればいいだけなのでは…?まぁ何かしら理由があるんだろうか…?)
ここまででbaseTrackable
周りの仕様はわかったが,肝心の姿勢がどこからきているかわかっていない.ここで,とりあえずARXTrackable.TransformationMatrix
に姿勢情報が入っていることがわかったので,これがどこから来るかを見てみる.
とりあえずコードをたどってみたところ,ARXTrackable.cs
のUpdate()
内の567行目にたどり着いた.
float[] matrixRawArray = new float[16];
float[] matrixRawArrayR = new float[16];
bool stereoVideo = ARXController.Instance.VideoIsStereo;
if (!stereoVideo)
{
v = ARXController.Instance.PluginFunctions.arwQueryTrackableVisibilityAndTransformation(UID, matrixRawArray);
}
else
{
v = ARXController.Instance.PluginFunctions.arwQueryTrackableVisibilityAndTransformationStereo(UID, matrixRawArray, matrixRawArrayR);
}
//ARXController.Log(LogTag + "ARXTrackable.Update() UID=" + UID + ", visible=" + v);
if (v)
{
matrixRawArray[12] *= 0.001f; // Scale the position from artoolkitX units (mm) into Unity units (m).
matrixRawArray[13] *= 0.001f;
matrixRawArray[14] *= 0.001f;
Matrix4x4 matrixRaw = ARXUtilityFunctions.MatrixFromFloatArray(matrixRawArray);
//.Log("arwQueryTrackableTransformation(" + UID + ") got matrix: [" + Environment.NewLine + matrixRaw.ToString("F3").Trim() + "]");
// artoolkitX uses right-hand coordinate system where the marker lies in x-y plane with right in direction of +x,
// up in direction of +y, and forward (towards viewer) in direction of +z.
// Need to convert to Unity's left-hand coordinate system where marker lies in x-y plane with right in direction of +x,
// up in direction of +y, and forward (towards viewer) in direction of -z.
transformationMatrix = ARXUtilityFunctions.LHMatrixFromRHMatrix(matrixRaw);
}
ここら辺のarwQueryTrackableVisibilityAndTransformationStereo
などはたどっていくとextern
になっていたので,つまりベースのARToolkitXのCで書かれているライブラリのものになっているらしい.
そっちをforkして読みに行ってもいいが,今回は個人的に取得したQRコードの位置を直接取得したいだけだったのでいったんここで深堀はやめておく.というかここまで来たらコードじゃなくて元論文とか仕組みを解説してるサイトとかを読んだ方が早い気がする.
結論として,ARXTrackable.TransformationMatrix
を見に行けば,そのARXTrackableの姿勢が4x4行列でわかるということが分かった.ついでに,ARXTrackable.Visible
を見に行けば,それが見えているか否かも判定できるっぽい.こいつらはUpdate()
で更新されるので明示的にこっちから読んであげる必要はなさげ(逆に言えば,パフォーマンスに問題が出そうならここら辺の書き換えが必要そう).
ここら辺を応用すれば,arunityx公式が出してるトラッキング実装に縛られず,必要な部分だけ取り出して独自にトラッキングを実装しに行けそう.
ARX Video Backgroundの動作
とりあえず,まずARXVideoBackgroundはライフサイクル的にはOnEnable
とOnDisable
でいろいろやってそう
void OnEnable()
{
arCamera = gameObject.GetComponent<ARXCamera>();
cam = gameObject.GetComponent<Camera>();
arController = ARXController.Instance;
arController.onVideoStarted.AddListener(OnVideoStarted);
arController.onVideoStopped.AddListener(OnVideoStopped);
arController.onVideoFrame.AddListener(OnVideoFrame);
arController.onScreenGeometryChanged.AddListener(OnScreenGeometryChanged);
}
ARXController
の各種イベントは普通に適切なタイミングで発火するだけだと思うので,OnVideoStarted
とOnVideoFrame
に鍵がありそう?
OnVideoStarted
の重要そうなところを切り出すと,
_videoBackgroundCameraGO = new GameObject($"Video background{nameSuffix}");
if (_videoBackgroundCameraGO == null)
{
ARXController.Log(LogTag + "Error: CreateVideoBackgroundCamera cannot create GameObject.");
return;
}
_videoBackgroundCamera = _videoBackgroundCameraGO.AddComponent<Camera>();
if (_videoBackgroundCamera == null)
{
ARXController.Log(LogTag + "Error: CreateVideoBackgroundCamera cannot add Camera to GameObject.");
Destroy(_videoBackgroundCameraGO);
return;
}
// Camera at origin.
_videoBackgroundCamera.transform.position = new Vector3(0.0f, 0.0f, 0.0f);
_videoBackgroundCamera.transform.rotation = new Quaternion(0.0f, 0.0f, 0.0f, 1.0f);
_videoBackgroundCamera.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
// Rendering settings.
_videoBackgroundCamera.cullingMask = 1 << BackgroundLayer; // The background camera displays only the chosen background layer.
_videoBackgroundCamera.depth = cam.depth - 1; // Render before foreground cameras.
_videoBackgroundCamera.clearFlags = CameraClearFlags.SolidColor;
_videoBackgroundCamera.backgroundColor = Color.black;
ようするにbackgroundだけ出すカメラをForegroundの後ろに出してるっぽい
BackgroundLayerの仕組みがよくわかってなかったが,生成されたカメラが見ているだけなのでForegroundはcullingMaskにBackgroundLayerを指定しなくても良いっぽい
また,OnVideoFrame()
を見たとき,カメラ映像の取得は,
updatedTexture = arController.PluginFunctions.arwUpdateTexture32(_videoColor32Array);
でされていたので,arController.PluginFunctions.arwUpdateTexture32
で取得できる…?
名前的にUpdateなので更新して受け取るみたいな話かもしれないので本家のリファレンスを見た方がよさげ
とりあえずBackgroundLayerが何なのか知れたのでいったんOK
あとここも処理内容的に必須ではないっぽいので,Quest3とかと併用するときはARXTrackableだけ流用して本家のパススルーAPIを使うとかしてもいいかも(カメラアクセスとれるか~とかはいろいろ考えないといけなさそうだけど)