Open9

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++で記述されている
当然ながらメンテは終わってるので今だとちょっと古いかも?
https://github.com/artoolkit/ARToolKit5

ARToolkitX

ARKitチームが作った現代版ARKitらしい
ちゃんとオープンソース
メインrepoはC++と,iOS,Androidの実装が含まれる
ちゃんとメンテされてるっぽいので期待
https://www.artoolkitx.org/
https://github.com/artoolkitx/artoolkitx

Unityの公式SDKもあるみたい
https://github.com/artoolkitx/arunityx

NyARToolkit

Ryo Iizukaさんが作ったARToolkitの実装
全部C#で書いてるやつらしい,すごい
ただ2012年実装なのでさすがに今だと厳しいかも?
https://nyatla.jp/nyartoolkit/wp/
https://github.com/nyatla/NyARToolkitUnity/tree/master

だんごだんご

とりあえず使う

  1. 適当なマーカーをプリントアウトする.今回は,下のような適当なQRコード(ARtestという文字列を読み込んでいる)を用意した
  2. Projectを作成し,arunityx(ARToolkitのUnity用SDK)をインストール
  3. マーカーの画像をartest.pngにしてStreamingAssetsフォルダに入れる
  4. シーン内に,空のオブジェクトを作成して,名前をARXControllerにする
  5. ARXControllerオブジェクトに,ARXControllerコンポーネントをアタッチする.アタッチするとARXVideoConfigも自動的にアタッチされる
  6. ARXControllerオブジェクトに,ARXTrackableコンポーネントをアタッチする
  7. ARXTrackableコンポーネントに対して次の設定を行う
    • Trackable tagをtestに
    • image fileをartest.pngに
      • この時,コンソールにロードできたかのログが流れるので,ロードできてなかったら画像ファイル名が間違っているかStreamingAssetsフォルダに入っていない
  8. 空のオブジェクトを作成して,名前をOriginにする
  9. Originに対して,ARX Originをアタッチする
  10. Originの下に空のオブジェクトを作成して,名前をTrackedObjectにする.
  11. TrackedObjectに対して,ARXTrackedObjectコンポーネントをアタッチする
  12. ARXTrackedObjectTrackable Tagフィールドにtestと入力
    • 先ほどのARXTrackableTrackable tagに対応する.ここも実行時ではなく静的にロードしてくれるので,コンソールを見てエラーになってそうだったら値を見直す
  13. Main CameraをOriginの中に入れる
  14. Main Cameraに対して,ARX CameraARXVideoBakgroundをアタッチする
  15. Main CameraのCameraコンポーネントに対して次を設定する
    • Clear FlagsをDepth onlyに
    • Culling MaskをDefaultのみに
    • 光の状況によってはオブジェクトが不自然になるので,お好みでHDRをOffに
  16. Directional LightをOriginの子オブジェクトにする(?)
    • Sampleがこうなってたから倣ってみたけど,必要かどうかは定かではない
  17. TrackedObjectの子にお好みで適当なオブジェクトを入れる
  18. ビルドしてテストする

最終的にヒエラルキーの構成としては次のようになるはず

最終的にこんな感じに出てくるはず

だんごだんご

ハマったところと疑問

  • なんか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が小さくて隠れてたけど,OnTrackedObjectFoundActiveChildrenOnTrackedObjectLostDeativeChildrenを切ることで,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));

を見ると,まずoriginbaseTrackableにくっついているので,自分が追従しているtrackablebaseTrackableならoriginの姿勢を単純に持ってくればいい.(origin.transform.localToWorldMatrix
baseTrackableでない場合,originの姿勢はbaseTrackableの姿勢分本来の原点から離れているので,いったん打ち消してから自分のtrackableの姿勢を適用すればいい.
回転の打ち消しは逆行列をかければいいので,origin.transform.localToWorldMatrix * baseTrackable.TransformationMatrix.inverse * trackable.TransformationMatrixとなる.

(…あれ?別にoriginはずっと本来の原点にいてやってtrackable.TransformationMatrixposeにしていすればいいだけなのでは…?まぁ何かしら理由があるんだろうか…?)

ここまででbaseTrackable周りの仕様はわかったが,肝心の姿勢がどこからきているかわかっていない.ここで,とりあえずARXTrackable.TransformationMatrixに姿勢情報が入っていることがわかったので,これがどこから来るかを見てみる.
とりあえずコードをたどってみたところ,ARXTrackable.csUpdate()内の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はライフサイクル的にはOnEnableOnDisableでいろいろやってそう

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の各種イベントは普通に適切なタイミングで発火するだけだと思うので,OnVideoStartedOnVideoFrameに鍵がありそう?
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を使うとかしてもいいかも(カメラアクセスとれるか~とかはいろいろ考えないといけなさそうだけど)