こんにちは!エンジニアのヒビキ(@hibiki_cube)です。
最近技術調査をする機会があり、その一環でiPhone / iPad向けにARアプリを開発してみました。
せっかく色々知識をつけることができたので、誰かのお役に立てればいいなと思いつつ開発の一連の流れをまとめておこうと思います。
できるだけ最近の技術や書き方を使って開発するようにしたので、新しく開発するときでも参考にしやすいのではないかと思います。
今回の記事ではプロジェクトを作った後、画面をタッチして自分で用意した3Dモデルを表示できるアプリを作ってみます。
目次
環境を準備する
まずは開発のための環境から用意していきます。
例外もありますが、基本的にiPhoneアプリの開発はMacで行います。
それ以外にも専用のアプリのインストールなど必要な準備があるので、まずは環境から準備していきます。
iPhone / iPad向けにアプリを開発するには最低限以下の環境が必要です。
- Mac
- Xcode(IDE)
快適に開発をするにはmacOSとXcodeは最新のものが使えると良いです。
また、AR機能のデバッグはシミュレーターではできないのでAR機能に対応する実機もあるとさらに便利です。
その場合の環境は以下の通りです。
- 最新のmacOSに対応しているMac
- 最新のXcode
- A9以降のチップを搭載したiPhoneまたはiPad
- 2015年以降に発売されたiPhone
- 2017年以降に発売されたiPad miniまたは2015年以降に発売されたiPadシリーズ
Appleが提供しているAR開発用のフレームワークであるARKitはA9以降のチップが動作要件なので、このような条件をつけています。
周辺環境の高度な認識など、よりリッチな体験を実現したい場合はLiDARセンサーを搭載したデバイスの利用をお勧めします。
なお、この記事では以下の環境を前提としています。
- MacBook Pro (16インチ, 2021), Apple M1 Maxチップ
- macOS Sonoma 14.5
- Xcode 15.4
- iPhone SE (第 3 世代)
- iOS 17.1.2
プロジェクトを作る
まずはアプリを開発する場所となるプロジェクトを用意します。
Xcodeを起動して「Create New Project」を選んで新しくプロジェクトを作ります。 プロジェクトのテンプレートを選ぶ画面が出てくるので、プラットフォームは「iOS」のままで「Augmented Reality App」を選んで「Next」をクリックします。
これによってXcodeがARアプリ用の基本的なコードを準備してくれます。 次にChoose options for your new projectというモーダルが出てくるので、「Product Name」,「Team」,「Organization Identifier」をそれぞれ入力し、「Next」をクリックします。
そのあとはプロジェクトを保存する場所を選択するよう促されるので、任意の場所を選びます。 このような画面が出たらプロジェクトの作成は完了です。 このままアプリをビルドして実行すると、以下のように一番近くの平面上にグレーの立方体が表示されるようになります。
デフォルトのままでも、立方体の表面の写り込みに実際の景色が反映されていたり、立方体の影が実際の机にも反映されていたりします。
自分の3Dモデルを表示する
プロジェクトのデフォルトのサンプルコードでは立方体が表示されるようになっていますが、これを任意の3Dモデルに置き換えてみます。
まずは使うモデルを用意する必要があります。
Blenderのような3Dモデリングのソフトや、Appleが無料で提供しているReality Composer Proなどを使ってUSDZ形式のモデルを用意してこのプロジェクトに追加しておきます。
今回は社内にあった公式キャラクターの「ウエパ」のぬいぐるみを3Dスキャンしてモデルを用意しました。
まずはこのモデルを読み込む処理から書いていきます。
ARViewContainer
の中で定義されているmakeUIView
メソッドの中にある以下の処理を
// Create a cube model let mesh = MeshResource.generateBox(size: 0.1, cornerRadius: 0.005) let material = SimpleMaterial(color: .gray, roughness: 0.15, isMetallic: true) let model = ModelEntity(mesh: mesh, materials: [material]) model.transform.translation.y = 0.05
次のように書き換えます。
// Load my model let model = try! ModelEntity.loadModel(named: "wepa")
なお、このコードで"wepa"
としている部分は自分が使うモデルのファイル名に合わせて適宜変更してください。
この時点でのコードはこんな感じになります。 このModelEntity.loadModel()
メソッドは指定した3Dモデルを読み込む関数で、もしモデルが見つからないとエラーになってしまいます。
ただ、今回は動的にモデルを取得したりするわけではなく常に自分が用意したモデルを読み込むので、冒頭にtry!
とつけてエラーになったときの処理を省略しています。
この状態でアプリをビルドして実行すると、このように自分で用意したモデルが表示されるようになります。
タップした場所にモデルを表示する
このままのAR体験も悪くはありませんが、まだまだ改善の余地があります。
例えば、今のままだと3Dモデルは認識された平面のどこかに自動的に表示されるようになっています。
自分の思った場所に配置できるわけではないので少し不便です。
この問題を解決すべく、画面をタップしたらその位置に3Dモデルが表示されるようにしてみます。
なお、今回の例では特にコードの分割などは行わず1つのファイルにギュギュッとまとめて書いています。
通常であれば機能ごとに分割したり関数にまとめたりするのですが、今回はしていませんのでご注意ください。
画面のタップで処理を実行する
画面をタップしたらその位置に3Dモデルが表示される
の動作を実現するには、まずは画面タップに応じて処理を実行できる必要があります。
Swift UIでタップ操作を扱うには、Viewの.onTapGesture()
メソッドを使います。
このメソッドのクロージャの中でさまざまな処理を行うことで、ユーザーはタップで画面を操作できるようになります。
ここでいう”View”は今回の例ではARViewContainer
が該当するので、コードは以下のようになります。
var body: some View { ARViewContainer(arView: arView) .edgesIgnoringSafeArea(.all) .onTapGesture(coordinateSpace: .global) { location in // ここに色々処理を書く } }
このあとの処理で画面上のタップした位置の座標が必要なので、そのための書き方をしています。
.onTapGesture(coordinateSpace: .global) { location in
とすることで、画面の原点からの座標をlocation
という名前の引数として取得しています。
ちなみにこのときの”原点”は画面の左上です。
AR空間の座標を取得する
先ほどの処理で取得した「タップした位置の座標」は2次元の座標なので、そこにそのまま3Dモデルを配置することはできません。
なので次はこの2D座標の情報をもとに、AR空間の3D座標を取得します。
このような処理を実現するには、ARView
のmakeRaycastQuery()
メソッドを使います。
このメソッドを使うと、画面上の指定した点からAR空間の中に擬似的にレーザー光線を飛ばします。
その光線が床やテーブルなどの平面に当たると、その位置の3D座標を取得することができます。
これを実際にコードに落とし込んでみると、以下のようになります。
ARViewContainer(arView: arView) .edgesIgnoringSafeArea(.all) .onTapGesture(coordinateSpace: .global) { location in guard let query = arView.makeRaycastQuery( from: location, allowing: .estimatedPlane, alignment: .horizontal ) else { return } guard let result = arView.session.raycast(query).first else { return } }
まず19~23行目で擬似的なレーザー光線の飛ばし方を指定しています。
この場合だと、発射地点を先ほど取得した「タップした位置の座標」にし、さらに水平な面だけを検出するように絞り込みをしています。
次の25行目では実際に平面との当たり判定を行なって、結果の3D座標を取得しています。
モデルを表示する
最後に、3Dモデルを配置する処理です。
この部分はこれ以前に使っていたコードが流用できそうです。
ARViewContainer(arView: arView) .edgesIgnoringSafeArea(.all) .onTapGesture(coordinateSpace: .global) { location in guard let query = arView.makeRaycastQuery( from: location, allowing: .estimatedPlane, alignment: .horizontal ) else { return } guard let result = arView.session.raycast(query).first else { return } let model = try! ModelEntity.loadModel(named: "wepa") let anchor = AnchorEntity(world: result.worldTransform) anchor.addChild(model) arView.scene.addAnchor(anchor) }
中盤で登場したModelEntity.loadModel()
メソッドで3Dモデルを読み込んだあと、先ほどの処理で得られた3D座標の位置にモデルを配置しています。
ここまでのコードに加えて、不要な部分の整理やエラーの解消なども行うと、最終的なコードはこのようになります。
コード全文はこちらをクリック
// // ContentView.swift // Demo // // Created by ヒビキ on 2024/06/17. // import SwiftUI import RealityKit struct ContentView : View { let arView = ARView(frame: .zero) var body: some View { ARViewContainer(arView: arView) .edgesIgnoringSafeArea(.all) .onTapGesture(coordinateSpace: .global) { location in guard let query = arView.makeRaycastQuery( from: location, allowing: .estimatedPlane, alignment: .horizontal ) else { return } guard let result = arView.session.raycast(query).first else { return } let model = try! ModelEntity.loadModel(named: "wepa") let anchor = AnchorEntity(world: result.worldTransform) anchor.addChild(model) arView.scene.addAnchor(anchor) } } } struct ARViewContainer: UIViewRepresentable { private(set) var arView: ARView func makeUIView(context: Context) -> ARView { return arView } func updateUIView(_ uiView: ARView, context: Context) {} } #Preview { ContentView() }
なお、AppDelegate.swiftはテンプレートで生成されたものをそのまま使っています。
この状態でアプリをビルド&実行すると、このようにタップした位置に3Dモデルが表示されるようになりました。
まとめ
テンプレートのコードをベースに、ステップバイステップでSwift UIとRealityKitでARアプリを作ってみました。
タップした位置を判定する処理などもシンプルに実装したので、うまく伝わっているといいなと思います。
今回の技術検証によって、ネイティブアプリの開発の一連の流れからARアプリ特有のノウハウまで、さまざまな知見を得ることができました。
空間コンピューティングは今後ますます盛り上がっていく分野だと思うので、今後もキャッチアップを続けていきたいと思います。
お読みくださったみなさんも、ぜひARアプリの開発に挑戦してみてください!