[SwiftUI, ARKit]RealityComposerでARのスクロールテロップを作る

最近どこかでスクロール型のテロップをみて、そういえばARでもそれっぽいものを作れそうだなと思い、試しに作ってみました。

#完成するもの
こんなものができます。
![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/f086a5b6-59ef-535b-41e1-3579bcd3fa28.gif)

#環境

- Swift5.3.2
- XCode12.4
- iOS14.4
- iPadPro 11inch(2020)

デバイスによっては動かない可能性があります。
LiDARを搭載している2020以降のiPadProかiPhone12Proなら確実です。

#コード

先にコードから。全文は以下のとおりです。
今回はコードによる複雑な操作が存在しないため、``ContentView``と``UIViewRepresentable``の2つを実装するだけです。

```swift:ContentView.swift
import SwiftUI
import RealityKit

struct ContentView : View {
    var body: some View {
        let container = ARViewContainer()
        
        return ZStack() {
            container.edgesIgnoringSafeArea(.all)
            VStack() {
                Spacer()
                HStack() {
                    Spacer()
                    Button(action: {
                        container.addTelop()
                    }, label: {
                        Image(systemName: "plus.circle.fill")
                            .resizable()
                            .frame(width: 50, height: 50)
                            .foregroundColor(.orange)
                            .padding(20)
                    })
                }
            }
        }
    }
}

struct ARViewContainer: UIViewRepresentable {
    
    let arView = ARView(frame: .zero)
    
    func makeUIView(context: Context) -> ARView {
        arView.environment.sceneUnderstanding.options = .occlusion
        arView.debugOptions = .showSceneUnderstanding
        
        return arView
    }
    
    func updateUIView(_ uiView: ARView, context: Context) {}
    
    func addTelop() {
        guard let wt = arView.raycast(from: arView.center,
                                      allowing: .estimatedPlane,
                                      alignment: .any).first?.worldTransform else {
            return
        }
        
        let telop = try! Experience.loadTelop()
        telop.children[0].anchor?.anchoring = AnchoringComponent(.plane(.any, classification: .any, minimumBounds: [0, 0]))
        telop.transform = Transform(matrix: wt)
        
        arView.scene.anchors.append(telop)
    }
}
```

###ContentViewについて
``ARViewContainer``を定数としているのは、テロップを追加する際に``upDateUIView(_:content:)``ではなく``addTelop()``というオリジナルの関数を使うためです。

AR画面の上にボタンを置くためには``ZStack()``を使います。``ZStack()``内に存在するViewは、一番上から重なっていきます。
ボタンの位置は、``HStack()``と``VStack()``に``Spacer()``を加えることで、大きさに応じて自動的に調整してくれます。

###ARViewContainerについて
定数である``ARView(frame:)``は``addTelop()``で使うために定数としています。
これは必ずしも定数である必要はなく、``upDateUIView(_:content:)``に代用することもできます。

また、今回のサンプルは``debugOptions.showSceneUnderstanding``が必須となります。

``worldTransform``の取得部分ですが、画面の中央からray(ビームのようなもの)を照射し、ヒットした場合のみその後の処理を行うようにしています。
こちらは``raycast(from:allowing:alignment:)``の引数によって、精度を変えたり垂直・平面のみなどの設定を行ったりできます。

``worldTransform``が取得できたら、その位置に後述するテロップの3Dモデルを置きます。
その前にテロップの``children[0].anchor?.anchoring``に``AnchoringComponent``を設定していますが、これは``RealityComposer``で作成した3Dモデルを垂直・水平どちらの平面にもおくためです。

いくつか子要素を経由しているのは、垂直・水平の設定を行なっているのがその子要素だからです。
テロップ自体にも``anchoring``は存在しますが、そちらに``AnchorComponent``を設定しても効果がありませんのでご注意ください。

#テロップの作成
今回のサンプルの中心部分です。

普段よく見かけるテロップは、右から1文字ずつ出てきて、同じように左に1文字ずつ消えていくものだと思います。

この表現をARで実装するために、今回は(今回も)``RealityComposer``を使います。

##準備〜文字の作成
XCodeでプロジェクトを作成し、SwiftUIと``Augmented Reality App``を選択します。
するとプロジェクトツリーに先程の``ContentView.swift``を含めてファイル一式が出来上がるので、その中から``Experience.rcproject``を開き、右上の``Open in Reality Composer``をクリックします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/29ad94a8-d505-89a0-79d3-078ce8fbfe3a.png"" width=500>

``Reality Composer``が立ち上がります。
そのままだと初期設定の豆腐(白い正方形の箱)があるので、そいつを消して、ウィンドウ右上の``テキスト``をクリックします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/bcbb92ec-b06a-e7a9-c4da-0c9553d8d739.png" width=500>

白いAaが表示されました。ついでに右上のシーン名もBoxからTelopに変更しておきましょう。
このシーン名ですが、``Experience.load〇〇()``の〇〇部分に相当する名前になります。
そのため、プロジェクトを最初に作成した際には、デフォルトで``Experience.loadBox()``と表示されているはずです。
今回のサンプルで``Experience.loadTelop()``としてロードしているのは、このシーン名をTelopにしているからです。

さて、表示されたAaのテキストですが、このままでは味気ないので、いくつか修正を加えていきます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/7959f3d5-0cda-10d8-c069-fb6657e8f138.png" width=500>
修正内容は以下のとおりです。

| 項目 | 修正内容 |
|:-:|:-:|
|  テキスト |  こんにちは |
|  フォントサイズ |  4cm |
|  フォント | ヒラギノ丸ゴProN  |

テキストの部分はどんなものでも問題ありません。あとでご紹介しますが、ものによっては結構インパクトが出るものもあります。

次にテキストの位置を調節します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/8879fbbd-11e9-c77f-c3f4-717cf710d4ce.png" width=500>
こちらも修正内容は以下のとおりです。

| 項目 | 修正内容 |
|:-:|:-:|
| 位置X  | 30cm  |
|  位置Y | -1cm  |
| 回転X  | -90度  |
|  回転Z | -5度  |

上の画像だと位置が見にくいですが、図に表すとこのようになっています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/2aa248c2-285b-5bed-04bc-f73c03a7679c.png" width=500>
ちょうど「こんにちは」という文字をパタンと倒して、ちょっとだけ右に傾けています。
次のアニメーションの部分でも詳しく説明しますが、初期位置と角度をこのような設定にしているのは、最初にAR空間の平面より下に隠しておいて、１文字ずつ出てくるようにするためです。

地面に埋まっているつくしが、傾いた状態で少しずつ地上に伸びてくる感じですかね、たぶん。

##アニメーションの設定
次にアニメーションを設定します。
前項で文字の初期状態を原点から見て右下の方に配置しましたので、アニメーションによって地上に出てきて、その後でまた地上に潜っていくような形にしていきます。

メニューに``ビヘイビア``という項目があるのでクリックします。するとウィンドウ下にビヘイビアの設定が出てきます。
![スクリーンショット 2021-03-16 11.10.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/458120d9-02ed-9ccd-03e4-9975d6830311.png)
ビヘイビアは初期状態だと何も設定されていないので、＋ボタンをクリックし、一番下の``カスタム``を選択します。
![スクリーンショット 2021-03-16 11.09.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/abeadb60-c9ce-fc89-2705-f4ae0471a5f7.png)
するとトリガとアクションシーケンスという枠が出てくるので、トリガの枠をクリックし、``シーン開始``を選択します。
![スクリーンショット 2021-03-16 11.10.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/363ba2a4-8fa8-eb7d-472a-e75ce19f2408.png)
``シーン開始``は、テロップが画面に表示された時から実行されるものです。

最後にアクションシーケンスを追加します。
アクションシーケンスとは、3Dモデルの大きさを変えたり、移動したり、音を鳴らしたりといったアクションを1つ1つ設定するものです。

今回追加するアクションシーケンスは以下の3つです。
①左上への移動
②左下への移動
③初期状態へのリターン

具体的にどのような移動になるのか説明するために、先ほど文字の作成時に使った図を拡張します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/2fc5b7dc-0396-157e-3859-19d2687df0ae.png" width=500>
オレンジの矢印がアクションシーケンスの追加内容です。

アクションシーケンスですが、ビヘイビアと同様、＋ボタンをクリックして上から4つ目の``移動/回転/拡大（絶対）``を選択します。
![スクリーンショット 2021-03-16 15.55.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/f7a1edbe-236d-660e-3f1f-15ec48fae453.png)
選択後に影響を受けるオブジェクトを指定するように言われるので、文字をクリックします。

今回必要なアクションシーケンスは3つなので、同じ作業を3回繰り返します。
その後、継続時間を1つ目と2つ目のアクションシーケンスで4秒、3つ目で0秒に設定し、全てのアクションシーケンスのイージングタイプを``なし``にします。

ここまで終わると、画面は以下のようになっていると思います。
![スクリーンショット 2021-03-16 16.10.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/83a38040-567f-12be-553e-795997eb5969.png)

継続時間はそのままの意味です。例えば3Dモデルを左に30cm移動させるアクションシーケンスの継続時間を4秒にすると、4秒間かけて左に移動します。

イージングタイプは3Dモデルの加減速に関する設定です。
例えばイージングタイプをイーズインに設定した場合、3Dモデルは移動や回転などの動きを、ゆっくりと加速しながら行います。逆にイーズアウトは停止する際の減速です。
車の発進・停止をイメージするとわかりやすいと思います。

次に、上の図のような形で動くように、各アクションシーケンスの位置と回転を設定していきます。
こちらは実際に設定した画像をご覧下さい。
![スクリーンショット 2021-03-16 16.22.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/f657e0f8-2906-e73e-5ac6-0d44197e72bd.png)

左から順番に図の①、②、③に対応しています。

まず左の①に対応するアクションシーケンスについて。
位置をX:0cm, Y:0.5cm, Z:0cmに設定し、文字が平面を通って左上に浮き出てくるようにします。
また、この時点で回転のZは0度になっていますが、これは初期状態で-5度傾いていたものを、平面に対して平行にする処理です。

次に真ん中の②に対応するアクションシーケンスについて。
これは初期状態をY軸に対称化したのものです。位置のXを-30cmに、回転のZを5度に設定し、その他は初期状態に合わせます。

最後に右の③に対応するアクションシーケンスについて。
これは初期状態へのリターンなので、値を初期状態と同じにしています。
このアクションシーケンスのみ継続時間を0秒としたのは、平面の下で行われるためです。

最後にアクションシーケンスのタイトル右にある循環マークをクリックし、ループを設定します。
これでシーンが開始すればこの一連のアクションシーケンスを繰り返し続けます。
![スクリーンショット 2021-03-16 16.38.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/619565df-9b61-3449-2e01-4608f3c8f23b.png)

これでアクションシーケンスの設定は終了です。
メニューの``再生``をクリックして確認してみましょう。以下のように動けば大丈夫です。
見にくいですが、図と同じアングルから①〜③のアクションが通して実行されます。
![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/4aa149f3-2186-7771-eb49-0405d0c87b4a.gif)

ここまでお疲れ様でした。編集を終了し、プロジェクトをデバイスにビルド＆インストールして確認しています。

今回は右下のボタンをタップすることでテロップがAR空間に追加されるようにしています。
AR空間を構築したら、壁や床などで試してみます。

上手くテロップみたいにできました。
![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/670074/f086a5b6-59ef-535b-41e1-3579bcd3fa28.gif)
※最初にお見せしたものと同じ動画です

表示する位置がちょっとずれていたり、``debugOptions.showSceneUnderstanding``が必須だったりと改善の余地は多いですが、ひとまず今回はこのぐらいにします。
ここまでお読みいただき、ありがとうございました。

#おまけ
試しに文字を変えてみました。そのうちARグラスが出たら、大胆な告白方法として使えるかもしれません、たぶん。

<iframe width="560" height="315"src="https://www.youtube.com/embed/yCL4sAbjsG0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

#プロジェクト全文
GitHubにリポジトリを作成しましたので、よろしければお使いください。
こちらはテロップの設定まで全て完了しており、実機にビルド＆インストールすればすぐに使えるかと思います。

https://github.com/y-taka-avant/TelopAR