見出し画像

visionOS向けアプリ「トリらっくす」の開発話

目次


はじめに

今回は、弊社からvisionOS向けにリリースされているトリらっくす(以下 本アプリ)の開発で得た知見についてのまとめ記事になります。

本記事はvisionOS、PolySpatialについてはある程度知識がある前提となります。
visionOS、PolySpatialの基礎に関しては別記事があるので、まずそちらからご覧ください。

開発環境

  • Unity 2022.3.25f1

  • Xcode 15.3

  • PolySpatial 1.2.3

  • visionOS 1.2

アプリモードの切り替え

visionOSのMRアプリには Volume と Immersive という2つのアプリモードがあります。
本アプリでは、この2つのモードを双方向に行き来できるようにしました。

アプリモードはシーン上の Volume Camera コンポーネントの Volume Window Configuration に設定された Volume Camera Window Configuration の設定値によって決定されます。

アプリモードを動的に切り替えるには、以下の2つの方法が考えられます。

A. Volume のシーンと Immersive のシーンをそれぞれ用意してシーンを切り替える。
B. Volume Camera コンポーネントに設定する Volume Camera Window Configuration を切り替える。

今回は1つのシーンで切り替えれる方が都合が良かったので、B.の方法で実装しました。

実装方法

1. Volume とImmersive の2つの設定ファイルを作成する

Mode を Bounded と Unbouded に設定した Volume Camera Window Configuration をそれぞれ作成します。
※ PolySpatial では Bounded = Volume 、 Unbouded = Immersive です。

2. 切り替え処理を実装する

1.で作成した Volume Camera Window Configuration を Volume Camera に設定する処理を実装します。
実装方法はとてもシンプルで、 VolumeCamera.WIndowConfiguration プロパティに任意の VolumeCameraWindowConfiguration を設定するだけです。

_volumeCamera.WindowConfiguration = _volumeCamera.WindowMode is PolySpatialVolumeCameraMode.Bounded ? _unbounded : _bounded;

本アプリではトグルボタンを押したら双方向に切り替わるようにしたかったので、現在の WindowMode を参照して、反対のモードに切り替えるようにしました。

モードの切り替えを検知する

モード切り替えの実装自体は上記のコードで完成です。
本アプリでは、モードが切り替わったタイミングで行いたい処理があったので、モード切り替えを検知する処理を実装しました。

/// <summary>
/// Volumeのモードが切り替わった時
/// </summary>
public Action<PolySpatialVolumeCameraMode> OnModeChanged;

/// <summary>
/// Awake
/// </summary>
private void Awake()
{
    _volumeCamera.OnWindowEvent.AddListener(onWindowEvent);
}

/// <summary>
/// OnDestroy
/// </summary>
private void OnDestroy()
{
    _volumeCamera.OnWindowEvent.RemoveListener(onWindowEvent);
}

/// <summary>
/// WindowEvent時
/// </summary>
private void onWindowEvent(VolumeCamera.WindowState state)
{
    if (state.WindowEvent is VolumeCamera.WindowEvent.Resized)
    {
        OnModeChanged?.Invoke(state.Mode);
    }
}

上記のコードでモード切り替えのタイミングを検知できます。

※ ただし、このタイミングでは AR Camera の初期化が完了していない場合があります。
本アプリでは、このタイミングで AR Camera の位置を取得してコンテンツの表示位置を調整したかったのですが、 Vector3(0, 0, 0) が返ってきてしまいました。
AR Camera の初期化タイミングを取得する方法が見つからなかったので、本アプリではモード切り替え後、2秒のディレイを入れることで解決しました。

★クリックで動画を再生★

手乗り機能

本アプリには、鳥を手に乗せることができる機能(以下 手乗り機能)があります。
手乗り機能は、手の位置と向きが必要だったので、ハンドトラッキングで手の情報を取得することにしました。
VisionProのMRアプリは Immersive モードでのみハンドトラッキングを使用することができるので、手乗り機能は Immersive モード限定の機能として実装しました。

ハンドトラッキングについては↓の記事で以前書いているので、そちらと合わせてご覧ください。

↓の図は XR Hands の Hand data model です。
この図での呼称を元に書いていきます。

手乗り位置

鳥が手に乗る時の位置についてです。
先ほどの図を見ると Palm (手のひら) がちょうど良さそうだと思い、最初に Palm で試してみました。
しかし、 Vision Pro では Palm の Pose を取得することができませんでした。
なので、本アプリでは Middle Proximal (中指の付け根) を使用することにしました。

指先に近くなりすぎてしまうかとも考えましたが、それ程気にならなかったので、 Palm の代用として Middle Proximal を使用するのは全然アリだと思います。

★クリックで動画を再生★

GUIの作成

PolySpatialには現在ユーザーが視線を合わせているGUIを光らせる(以下 ホバーエフェクト)ためのコンポーネント VisionOSHoverEffect があります。

問題点

VisionOSHoverEffect コンポーネントには問題点があり、メッシュにつけた場合は問題ないのですが、SpriteやuGUIにつけた場合に、それらの画像の形とは関係なく、矩形で光るようになってしまいます。

★クリックで動画を再生★

本アプリでも↑の動画のようなUIをデザイナーさんに作ってもらいました。(記事用に大きく表示してます。)
全体的に丸いデザインになっていて、これをスプライトやUIで実装するとホバーエフェクトだけが四角くなってしまい、微妙な見た目になってしまいます。

解決方法

ホバーエフェクトの対象がメッシュでないことが問題の原因なので、ホバーエフェクトの対象となる画像(↑の画像だと緑色の部分)をメッシュ化することで解決しました。

画像をメッシュ化するために、画像をSVGで用意してもらい、SVG画像をメッシュ化するという手法を取りました。
この記事ではBlender4.1を使ってSVG画像をメッシュ化していきます。

※ 以下の理由から本アプリでは一部のみメッシュ化しました。

  • 複雑な画像をメッシュ化すると歪んでしまう(例 : 手乗りボタン)

  • 9 Sliceなどの画像でないとできない (例 : スライダー)

1. BlenderにSVG画像をインポート

ファイル→インポート→Scalable Vector Graphic (.svg) からSVG画像をインポートします。

2. SVGをメッシュに変換

1.でインポートしたSVG画像をメッシュに変換します。
インポートしたSVG画像を選択した状態で、 オブジェクト→変換→メッシュ を選択するとSVGからメッシュに変換できます。

3. 原点を移動

元のSVG画像次第かもしれませんが、そのままだとオブジェクトの原点がメッシュの左下になっていました。この原点はUnityに持っていった時の原点にもなるので、移動させておいた方が後々楽になると思います。

原点を移動させたいオブジェクトを選択した状態で、 オブジェクト→原点を設定→原点を重心に移動(サーフェス) で原点をメッシュの中心に移動させることができます。

4. メッシュをエクスポート

メッシュを ファイル→エクスポート→Wavefront(.obj) または FBX(.fbx) から出力します。

メッシュとスプライトを組み合わせる

前述した通り、本アプリでは緑色の部分のみメッシュ化して、白色の部分はスプライトのまま使用しました。
しかし、PolySpatialにはメッシュとスプライト(もしくはuGUI)を組み合わせると重なった部分のメッシュが透過されてしまうバグがあります。

★クリックで動画を再生★

この問題を解決する方法としてPolySpatialには VisionOSSortingGroup という描画順を決めるvisonOS専用のコンポーネントが用意されています。

↑の画像は本アプリの音量調整スライダーです。
メッシュ化した Back Panel のみ Order を0に設定し、その他のスプライトの Order を1に設定しました。
これで、重なった部分が透過されることなく、狙った順番で描画されるようになりました。

★クリックで動画を再生★

音量アイコンの方は問題ないのですが、左の手乗りボタンが中のアイコンの周りが白くなってしまっています。
この2つのスプライトの違いについては不明でです。
アイコンもメッシュ化すれば解決するかもしれませんが、SVGが複数のパーツで構成されていて、上記の方法だと上手くいきませんでした。(Blenderでちゃんとメッシュを結合して調整すればなんとかなりそう)

負荷対策

Immersiveモード時のみ表示される背景の森を実装しました。
この背景の森は書き割りの1枚絵ではなく、3Dの木や草を生やしています。

森っぽくなるように木や草を大量に配置し、合計で60万ポリゴンほどの森ができました。
この森を実機で見ると、目に見えてフレームレートが低下してしまいました。

フレームレート低下の対策として本アプリでは主に以下の対応を行いました。

  • ポリゴン数の削減

  • サブメッシュの統合

ポリゴン数の削減

森に配置している3Dモデルそれぞれのポリゴン数を見た目に影響が出ない範囲で削減しました。
↓のWWDCの動画でImmersiveは50万(できれば10万)ポリゴン以下と言われていたので、60万ポリゴンの森は明らかにポリゴン数がオーバーしていました。

https://developer.apple.com/jp/videos/play/wwdc2024/10186

メッシュ削減には↓のアセットを使用しました。

https://assetstore.unity.com/packages/tools/modeling/mesh-simplify-43658

木のモデルはポリゴン数を減らすと枝が消えてしまう場合があったので、控えめに約50%削減しました。
草のモデルはポリゴン数を大きく減らしてもそこまで見た目に影響がなかったので、90%近く削減することができました。
これにより森全体で20万ポリゴン程度まで減らすことができました。

サブメッシュの統合

今回使用した木のモデルは幹がメインのメッシュ、枝葉がサブメッシュになっていました。
サブメッシュがあるとその分無駄に描画処理が走ってしまうので、1つのメッシュにまとめることにしました。

メッシュの統合には↓のアセットを使用しました。

https://assetstore.unity.com/packages/tools/modeling/mesh-baker-5017

フレームレート

上記2つの負荷対策を行なった上でフレームレートを計測してみました。

★クリックで動画を再生★

Volumeモード

Volumeモードでは90~100FPS出ていました。
Volumeモードでは100FPSが上限のようです。

Immersiveモード

Immersiveモードでは基本45~50FPS出ていました。
一見、Immersiveモードでは50FPSが上限のようなのですが、コンテンツを表示している方向と逆の方向を向くと100FPSになることがありました。
ただ、その時はコンテンツが見えなくなってしまうので、コンテンツが見えている時は50FPSを目標にするのが良さそうです。

Immersiveモード(森あり)

問題の森ありの状態です。
森を表示すると、最低30FPS程度まで低下しました。
50FPSの時と比べると若干カクついている感もありますが、このくらいなら許容範囲の見え方だったので、本アプリではここまでの作業で負荷対策は完了としました。

おわりに

いかがだったでしょうか?
トリらっくすはXR総合展などのイベントでも好評をいただけているようなので、Vision Proをお持ちの方は是非インストールしてみてください。
弊社では他にもvisionOS向けのアプリをリリースしているので、そちらも是非ご覧ください。


この記事が気に入ったらサポートをしてみませんか?