ARでクリスマス気分を味わってみた！

こんばんは！ZOZOTOWNでAndroidエンジニアをやっているマサと申します。

この投稿はZOZO Android Adventカレンダーのその2の25日目(最終回)の記事となります。

この記事ではAndroidのアプリ上でAR(拡張現実)を使用してクリスマスツリーを設置する方法を共有したと思います。
 
キッカケは、僕が普段からARに関連するアプリを(Pokemon Go，マップアプリ)使用する機会があり、ふとAndroidでARアプリを作成する方法が気になったからです。また、ちょうど投稿日がクリスマスということもあり、今回はARでクリスマスツリーを設置してみようと思います！

## はじめに
この記事は主に以下のような読者を対象として想定して書いてみました。他にも様々な実装手段やツールがあると思いますので、あくまで参考程度にしていただければと思います。

この記事にはどういう特徴があるの？
- OpenGLについてあまり詳しくないけどARをAndoridで書いてみたいまたは実装方法を知りたい
- Andoridを普段JavaまたはKotlinを使用してAndroidStudioで開発している
- 一般的なGradleDependenciesと同じようにしてARフレームワークを使用したい

## ARをAndroidで実装するには？
ARをAndroid上で実現するためのフレームワークとして、公式のAdnroidDeveloperサイトやネットを捜索しているとARCoreをベースとして開発されたSceneformという以下のレポジトリにたどり着くと思います。

https://github.com/google-ar/sceneform-android-sdk

ですがこのリポジトリと関連ドキュメントを読みすすんでいくと、このSDKはもうアーカイブされており以下のような問題が発生します。
- SDKがArchivedになっており、一度SDKをローカルにダウンロードしてライブラリとしてImportする必要がある。
- アーカイブされた時点で、AndroidSupportのNameSpaceを使用しているDependencyの依存がいくつか存在しているため、AndroidXに移行しないと使用できない。
- 公式のチュートリアルの手順に沿って進めても、AndroidStudioで3Dモデルを作成するのに必要ようなSceneformのプラグインがもうサポートされておらず、以下のようなエラーメッセージが表示される。
![error.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/83bd1f79-9943-0837-f2d2-034b35293639.jpeg)

## 解決法はあるの？
あります！幸いなことに、Sceneformは以下のレポジトリにて熱心な数名のエンジニアの方々によってメンテナンスされており、定期的に更新されてます。なので今回はこちらのSDKを使用してARの具体的な実装方法を説明していきたいと思います。

https://github.com/ThomasGorisse/sceneform-android-sdk


## 進め方
ここからは**3Dモデルの準備 → 事前セットアップ → コードでの実装**と順を追って解説します。すぐに完成品を確認したいという方は以下の私のGitHubリポジトリからもチェックできます！

https://github.com/masah517/ARCamera

また説明の部分に入る前に、以降のセクションでよく使用している単語を先に説明したと思います。開発手順で下記の単語が出えた場合は、下の定義リストを参考にしていただければと思います。

**Scene：**ARモデルを配置するためにレンダリングされる3Dワールドを指します。
**ModelRenderable：**ARモデルを構築するためのビルダークラスです。
**HitResult：**ユーザーがタップした空間の点に関連する情報を持ったオブジェクトです。
**Anchor：**実世界の固定された場所。ローカル座標（ユーザーの表示による）を実際の座標に変換するために使用されます。
**TransformableNode：**ユーザーのジェスチャー(Pinch、Rotate、Dragなど)に反応できるようにカスタマイズされたノードクラスです。


## Gradleスクリプトのセットアップ
**手順1:** 
まずはappモジュール下のbuild.gradleに以下の依存関係を追加してください。

```groovy
     implementation("com.gorisse.thomas.sceneform:sceneform:1.20.3")
```
そしてScenformSDKに合わせるため、MinSDKのVersionを24以上にしてください。　

```groovy
    defaultConfig {
        applicationId "com.masa.arcamera"
        minSdk 24   ← ここを24以上にする
        targetSdk 31
        ...
```

## ARCoreのエントリをAndroidManifestに追加する
**手順2:**
今回はARアプリを作成するために、**カメラの権限**と**ARのMetaDataオプション**を設定する必要があります。以下のように宣言することで、このアプリを実行するのにカメラの権限とAR機能が必須ではなくOptionalであることを宣言しています。

最後のMetaDataのタグはOptionalでない場合は、アプリの実行にはＡＲ機能が必須と判定され、Playストア上でARCoreに対応していない端末からはこのアプリが見えないようフィルタリングすされます。

```xml
<uses-permission android:name="android.permission.CAMERA" />

<application>
    …
    <meta-data android:name="com.google.ar.core" android:value="optional" />
</application>
```

## Sceneformフラグメントの追加
**手順3:**
次に、ARモデルを表示するためにArFragmentをlayoutに配置します。その際にArFragmentのname属性を追加してください。ArFragmentを配置することで初期起動時のインジケーターの表示や、ARモデルの配置や拡大縮小などのジェスチャーをうまいことハンドリングしてくれます。

```xml
 <androidx.fragment.app.FragmentContainerView
            android:id="@+id/arFragment"
            android:name="com.google.ar.sceneform.ux.ArFragment"　← ここ
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
```


## ArFragmentフィールドの追加
**手順4:**
続いてARシーンの実装においてベースとなるARFragmentをクラスメンバーで以下のように宣言します。

```kotlin
private lateinit var arFragment: ArFragment
```

続いてARFragmentの初期化の方法ですが、下記のようにFragmentManager経由でfindFragmentByIdメソッドにARFragmentのLayoutIdを引数として渡す感じで初期化を行ってください。
`※Activityからの呼び出しはSupportFragmentManager、FragmentからはchildFragmentManagerをそれぞれ使用してください。`


```kotlin
 arFragment = (childFragmentManager.findFragmentById(R.id.arFragment) as ArFragment)
```

ここまでの時点で実装に問題がなかった場合、アプリを起動した際に以下のような端末を動かすように促すインジケーターが表示されると思います。
![Indicator.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/b27dd0a5-e769-b46e-62c5-f43a9bbe9bba.jpeg)

## ARモデルの非同期ローディングによる初期化
**手順5:**
次にARモデルのロードの方法を説明します。下図のように、モジュールの下にAssetフォルダを作成して事前に用意していたglbファイルを作成したAsset下のフォルダ内にimportしてください。
![ModelImport.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/c728967f-5473-7434-6171-409e84e59b88.jpeg)

**手順6:**
手順5が完了したら、ModelRenderableビルダーを使用して以下のように用意したARモデルの読み込みを実行します。
モデルのローディングは非同期で実行して、ロード完了時に結果が返されるためawait()を追加して結果を待つ必要があります。

```kotlin
ModelRenderable.builder()
            .setSource(context, Uri.parse("models/halloween.glb"))
            .setIsFilamentGltf(true)
            .await()
```

`ネットワークが未接続の時に、リモートでのロードをする場合はExceptionHnadlingをする必要があります。`



## タッチイベント処理
**手順7:**
タップ判定を行うための実装をしていきます。
AR空間が無事立ち上がった時に、下図のようにARモデルを配置できる場所が白い点で形成された平面が表示されます。
<img width="250" alt="Plane.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/8ea00be0-be7c-da4c-2ca6-54d79c4f0d63.jpeg">


この平面をタッチした点を判別するためには、下記のようにARFragmentのsetOnTapArPlaneListenerを実装する必要があります。

```kotlin
 arFragment = (childFragmentManager.findFragmentById(R.id.arFragment) as ArFragment).apply {
            setOnSessionConfigurationListener { session, config ->
            }

            setOnViewCreatedListener { arSceneView ->
                arSceneView.setFrameRateFactor(SceneView.FrameRate.FULL)
            }
            
            setOnTapArPlaneListener(::onPlaneTapped)
        }
```

では実際に引数として渡しているonPlaneTappedメソッドがどんな実装になっているのか見ていきましょう。

最初に**ARモデル**(今回はクリスマスツリーモデル)のロードが完了しているかを判定しています。タップした時点でモデルがロード未完了の場合はシンプルにToastでロード中の文言を表示し、再度トライするように促します。

ロードが完了してた場合は、initSceneのメソッドを呼び出しモデルを配置するためのSceneの初期化を行います。

```kotlin
 private fun onPlaneTapped(hitResult: HitResult, plane: Plane, motionEvent: MotionEvent) {
        if (model == null) {
            Toast.makeText(context, "Loading AR Model...", Toast.LENGTH_SHORT).show()
            return
        }
        initScene(hitResult)
    }
```

## addNodeToSceneメソッドの追加

addChild()ではArSceneViewのシーンオブジェクトにAttachするための２つのNodeオブジェクトを生成して渡す必要があります。

一つ目がAnchoNodeです。AnchorNodeはARCore Anchorのポーズを元に配置されます。

２つ目はTransformableNodeです。TransformableNodeを使用することでユーザーの操作によるARモデルの回転や拡大/縮小、それから移動などといったジェスチャーに対応する機能を持たせることができます。
Nodeが生成され、SceneがNodeを認識すると自動でARモデルが空間上に配置されます。また、animate(true)にしておくことでアニメーション形式のglbファイルも表示できます。

```kotlin
 private fun initScene(hitResult: HitResult){
     arScene.addChild(AnchorNode(hitResult.createAnchor()).apply {
         addChild(TransformableNode(arFragment.transformationSystem).apply {
             renderable = model
             renderableInstance.animate(true).start()
          })
     })
}
```

## 実際に実行してみよう！
ではこの記事の冒頭でも書いたように、実際にクリスマスツリーを配置してみましょう！
アプリを立ち上げると無事立派なツリーが反映されました🎄🎉🎉🎉
![Tree.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/646120/7ad8d521-ad70-1dba-3f3e-9e7ec30f9ce2.jpeg)

## 最後に

ARをAndroidで実装するのが初めてだったので、調査などの下準備に結構時間がかかったのですが無事に表示できてよかったと思います。サンプルアプリの方ではARモデルごと写真を撮れる機能を実装しましたので、良かったら使ってみてください。

それでは最後に皆さん良きクリスマスと年末年始を！
メリークリスマス！