このエントリは【カヤック】面白法人グループ Advent Calendar 2023の21日目の記事です。
はじめに
こんにちは。カヤックアキバスタジオでエンジニアをやっている臼井です。
1年あっという間ですね。
去年送ってもらったtoioが埃を被ったまま放置され過ぎてしまい、このままでは触れる機会がなくなってしまう・・・ということでこのタイミングで引っ張り出してきました。
去年は付属のゲームを遊んでみたり、toio SDK for Unityのサンプルに触れてみましたが、今回はどうしようかなと小一時間ほど悩んだすえ、せっかくの機会だし新しいことにもチャレンジしよう! ということでAR + toioを使って試した事を紹介しようと思います。
去年の記事もご一緒にどうぞ。
環境
- MacBook Pro 14インチ(macOS Sonoma 14.1)
- iPhone 12 mini(iOS 16.6)
- Unity2021.3.33f1
- UniTask(2.5.0)
- toio-sdk-for-unity(1.5.1)
準備
UnityHubからNew Projectを選択しAR Coreを選択してUnityプロジェクトを作成します
Package Manager経由でUniTaskをimportします
toio-sdk-for-unity.unitypackageをダウンロードしてpackageをimportします
Project SettingsのXR Plug-in Managementを選択しiOSタブ内にあるARKitにチェックを入れます
これで準備完了です。 既に用意されているBlankAR.unityを使っていきます。
実装
toioのマットをAR空間上に表示してタップした箇所にコアキューブを移動させるというのをやってみました。
画像認識機能の準備
toioのマットを撮影しUnityプロジェクトにいれます
Projectビュー内で右クリック > Create > XR > Reference Image Library を選択します
2.で作成されたファイルを選択し、Inspector上からAdd Imageボタンを押し1.で追加した画像を設定します
Specify Sizeにチェックを入れて、Physical Size(meters)に0.56を設定
※マットのサイズが56cmくらいなので0.56という数値を設定しておきます。Hierarchy内のAR Session Originオブジェクトを選択しAR Tracked Image Managerコンポーネントをアタッチします
Inspector上からAR Tracked Image MangaerのSerialized Libraryに2.で作成したファイルを設定します
Max Number Of Moving Imagesの値を1に設定し、Tracked Image Prefabに画像を認識した際に生成されるPrefabの参照を設定します。(今回はCubeを生成する形にしました。)

ビルドして実行したものがこちらになります。 マットを認識してCubeが生成されました。 画像によっては認識されずらかったりすることもあったので、用意する画像は認識されやすいものにするか複数登録するといった対応が良さそうでした。(部屋の明るさによっても認識されやすが変わる気がしました)

マット用オブジェクトの準備
Hierarchy上で右クリック > 3D Object > Quad を選択し作成されたオブジェクトをprefab化しておきます
prefabを選択しInspector上からRotation Xを90に、Scaleを全て0.56に設定します
Projectビュー内で右クリック > Create > Material を選択してMaterialを作成しtoio_collection_front.pngを設定します。
※Textureが設定できるshaderを選択3.で作成したMaterialを1.で作成したprefabに設定します

スクリプトの準備
- マット用のスクリプトを作成しマット用オブジェクトにアタッチします
ARMat.cs
using UnityEngine;
#nullable enable
public class ARMat : MonoBehaviour
{
private const float MESH_SIZE = 1f;
private const float MESH_HALF_SIZE = MESH_SIZE / 2f;
[SerializeField] private RectInt _rect = new RectInt(65, 65, 370, 370);
/// <summary>
/// AR座標からマット座標へ変換
/// </summary>
public Vector3 WorldToLocal(Vector3 position)
{
var point = transform.InverseTransformPoint(position);
var rateX = (point.x + MESH_HALF_SIZE) / MESH_SIZE;
var rateY = (point.y + MESH_HALF_SIZE) / MESH_SIZE;
return new Vector3(
Mathf.Lerp(_rect.x, _rect.xMax, rateX),
Mathf.Lerp(_rect.yMax, _rect.y, rateY),
0f);
}
/// <summary>
/// マット座標からAR座標へ変換
/// </summary>
public Vector3 LocalToWorld(Vector2 point)
{
var rateX = (point.x - _rect.x) / _rect.width;
var rateY = (point.y - _rect.y) / _rect.height;
var position = new Vector3(
Mathf.Lerp(-MESH_HALF_SIZE, MESH_HALF_SIZE, rateX),
Mathf.Lerp(MESH_HALF_SIZE, -MESH_HALF_SIZE, rateY),
transform.position.z);
return transform.TransformPoint(position);
}
}
- コアキューブのコネクト、移動制御用のスクリプトを作成しシーン内の任意オブジェクトにアタッチします
SampleScene.cs
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using toio;
#nullable enable
public class SampleScene : MonoBehaviour
{
[SerializeField] private Camera _camera = null!;
[SerializeField] private ARTrackedImageManager _arTracked = null!;
[SerializeField] private ARMat _arMat = null!;
[SerializeField] private Transform _cubeMarker = null!;
[SerializeField] private Transform _targetMarker = null!;
private CubeManager? _cubeManager;
private Cube? _cube;
async void Start()
{
// Cube接続処理
_cubeManager = new CubeManager(ConnectType.Real);
await _cubeManager.SingleConnect();
_cube = _cubeManager!.cubes[0];
}
private void OnEnable()
{
_arTracked.trackedImagesChanged += OnTrackedImagesChanged;
}
private void OnDisable()
{
_arTracked.trackedImagesChanged -= OnTrackedImagesChanged;
}
private void Update()
{
if (_cube == null)
{
return;
}
// キューブの座標をAR空間座標に変換してARマット上にマーカーを表示する
_cubeMarker.position = _arMat.LocalToWorld(new Vector2(_cube.x, _cube.y));
if (!Input.GetMouseButtonDown(0))
{
return;
}
var ray = _camera.ScreenPointToRay(Input.mousePosition);
if (!Physics.Raycast(ray, out var hit, 5f, 1 << _arMat.gameObject.layer))
{
return;
}
// ARマット上のタップした位置からキューブの移動先を取得
var targetPos = _arMat.WorldToLocal(hit.point);
_targetMarker.position = _arMat.LocalToWorld(targetPos);
// 移動方向の角度を取得(0〜360になるように調整)
var targetAngle =
(int)(Mathf.Atan2(targetPos.y - _cube.pos.y, targetPos.x - _cube.pos.x) * Mathf.Rad2Deg + 360) % 360;
// 指定した座標にキューブを移動させる
_cube.TargetMove(
(int)targetPos.x, (int)targetPos.y,
targetAngle,
targetMoveType: Cube.TargetMoveType.RoundForwardMove);
}
// ARTrackedImagesChangedイベントが通知された時の処理
void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs args)
{
Transform? parent = null;
foreach (var target in args.added)
{
// 新規追加されたものがある
parent ??= target.transform;
}
foreach (var target in args.updated)
{
if (target.trackingState == TrackingState.Tracking)
{
// 既存オブジェクトの情報が更新された
parent ??= target.transform;
}
}
if (parent != null)
{
// ARMatの親を設定
_arMat.transform.SetParent(parent, false);
}
}
}

端末で実行したものがこちら
タップした位置にコアキューブを移動させることができました。1点注意点として使うマットによってコアキューブが読み取る座標が違うということです。 最初気づかなくてタップした位置に移動せずハマってしまいました。あと、処理を作成した後に気づいたのですがsdk側にMat.csが用意されていたのでそれを使う形でもよかったかもしれないなと。

コアキューブにはボタンもついてて、押し込んだ際の通知を検知することもできます。 あとはLEDを光らせたり音を鳴らしたり傾き情報を取得することもできました。
コアキューブを使って、見えないオブジェクトを探し出す宝探し的な遊びも楽しそうだなと思い、コアキューブとオブジェクトの距離によって獲れる情報が変化するというのも作ってみました。 距離が近いほど音のボリュームやLEDの色を変化させています。

あとはマット上でコアキューブを押し込んで、その座標をもとにAR空間上にオブジェクトを表示することもできるため、一人がコアキューブを使い、もう一人がARアプリ上で生成されたオブジェクトを撃つといった遊びも楽しそうだなと思いました。(今回は一人で動画をうまく撮ることが出来なさそうなので断念しました)
まとめ
実際にプログラムを組んでみて、殆どプログラムを書かずにコアキューブの制御ができるため敷居は引くいのかなと感じました。 またサンプルも豊富でこれからUnityやプログラムを始める方にはおすすめしても良さそう感じがしました。(コアキューブ、マットが複数ある場合も殆どプログラムが変わらないのもいいですね)
ARの機能は画像認識以外はさわれていないため、他の機能も使ってみつつ次回のネタ探しをしていこうと思います。