【Swift Playgrounds】ARを作るPART.1「スタート地点」を読み解く

# この記事は何か？
iPadアプリケーションSwift Playgroundsは、Swiftプログラミングのスキルを習得できます。
「ブック」と呼ばれるコンテンツを実践することで、Swiftプログラミングやコンピュータの基礎から応用までを学びます。
そして、習得したスキルからオリジナルのコンテンツを開発できるテンプレートのブックがあり、「ARを作る」では拡張現実コンテンツを簡単に構築できます。
この記事では、「ARを作る」の最初のページである「スタート地点」を解説します。
「ARを作る」テンプレートから、独自の拡張現実空間を構築してiPadで遊ぶことができます。

<img width="302" alt="IMG_80046717937D-1.jpeg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70476/021dbfef-9410-b029-4a12-4748a38c29cb.jpeg">

## 実行環境
- iPad Pro 11.5インチ（2018モデル）
- iOS 13.2.3
- Swift Playgrounds（バージョン3.1）
- ARを作る（Swift 5.1）

# ブックの構成
「ARを作る」ブックは、次の4つのページで構成されています。
1. スタート地点（この記事で解説）
2. 簡単なシーン（解説は[こちら](https://qiita.com/chino_tweet/items/db8182ad89d20625a56e)）
3. APIの概要（解説は[こちら](https://qiita.com/chino_tweet/items/221c268de2137b90eef0)）
4. 蝶を捕まえる（解説は[こちら](https://qiita.com/chino_tweet/items/5cd1f4800380ab5ef3b5)）

また、それぞれのページには共通の`SharedCode`というファイルがあります。メインファイルから`SharedCode`に定義された機能を利用できます。

## メインファイル
最初のページなので、メインファイルのコードは極めてシンプルです。
以下の作業を行なっています。

- レモンを作って、シーンに追加する

````Swift:メインファイル
let lemon = Model.lemon

scene.add(lemon)
````

ページを実行すると、iPadのカメラが平面（サーフェス）の検出を開始します。
平面を検出すると、任意の箇所をタップしてレモンを配置できます。
さらに、「シーンを開始する」ボタンをタップすることで、コードで指定した挙動が動き出します。

このファイルにコードを自由に追記することで、独自のAR空間を構築できます。

## SharedCodeファイル
このファイルは、すべてのステージに含まれています。
コメントにもあるように、このファイルに加えた編集内容は「ARを作る」ブックに含まれる全てのページに反映されます。
以下、ソースコードを少しずつ、分解してみていきます。

````Swift:SharedCodeファイル
// 共有コード
// このファイルに書いたコードは、このプレイグラウンドブックのすべてのページで利用できます。
import SceneKit

public var scene = Scene()

public var allModels: [ImaginaryNode] = [Model.alarmClock, Model.arrow, Model.cactus, Model.ear, Model.eye, Model.flyingBug, Model.foot, Model.hand, Model.lemon, Model.lightbulb, Model.lightning, Model.moon, Model.nose, Model.mouth, Model.raincloud, Model.snail, Model.star, Model.sun]
public var origin = Point(x: 0, y: 0, z: 0)
var xOffset = -48.cm
var modelsPlacedCount = 0

// allModels配列のすべてのモデルをシーンの原点の周囲に配置します。
public func placeAllModels() {
    let model = allModels[modelsPlacedCount]
    scene.place(model, at: Point(x: origin.x - xOffset, y: origin.y, z: origin.z - 30.cm))
    model.animate()
    xOffset += 8.cm
    if modelsPlacedCount + 1 < allModels.count {
        modelsPlacedCount += 1
        model.play(.ring, loops: false, completion: placeAllModels)
    }
}
````

### 準備
まず、3D空間を扱うためのフレームワーク`SceneKit`を導入しています。
そのあと、シーンのオブジェクトを生成して、変数`scene`に割り当てています。

````swift:
import SceneKit

public var scene = Scene()
````

### 主要な変数の定義
ここで定義している変数オブジェクトは、以下の4つです。

- allModels（ARコンテンツとして配置できる3Dモデルのコレクション）
- origin（3D空間の原点）
- xOffset（原点からX軸方向への移動距離）
- modelsPlacedCount（配置した3Dモデルの総数）

````swift:
public var allModels: [ImaginaryNode] = [Model.alarmClock, 
                                         Model.arrow, 
                                         Model.cactus, 
                                         Model.ear, 
                                         Model.eye, 
                                         Model.flyingBug, 
                                         Model.foot, 
                                         Model.hand, 
                                         Model.lemon, 
                                         Model.lightbulb, 
                                         Model.lightning, 
                                         Model.moon, 
                                         Model.nose, 
                                         Model.mouth, 
                                         Model.raincloud, 
                                         Model.snail, 
                                         Model.star, 
                                         Model.sun]

public var origin = Point(x: 0, y: 0, z: 0)
var xOffset = -48.cm
var modelsPlacedCount = 0
````

3Dモデルには、生き物から身体の一部、天体に置物などいろいろ用意されています。種類ごとに整理しておくと...
- arrow（矢印）
- alarmClock（目覚し時計）
- lightbulb（電球）
- star（星）
- sun（太陽）
- moon（月）
- lightning（稲妻）
- raincloud（雨雲）
- ear（耳）
- eye（目）
- foot（足）
- hand（手のひら）
- nose（鼻）
- mouth（口）
- cactus（サボテン）
- lemon（レモン）
- snail（カタツムリ）
- flyingBug（空飛ぶ虫）

### placeAllModels()関数の定義
この関数を実行すると、`allModels`配列のすべての3Dモデルをシーンの原点の周囲に配置します。

````swift:placeAllModels()関数
public func placeAllModels() {
    let model = allModels[modelsPlacedCount]
    scene.place(model, at: Point(x: origin.x - xOffset, 
                                 y: origin.y, 
                                 z: origin.z - 30.cm))
    model.animate()
    xOffset += 8.cm
    if modelsPlacedCount + 1 < allModels.count {
        modelsPlacedCount += 1
        model.play(.ring, loops: false, completion: placeAllModels)
    }
}
````

もっと詳しくコードを見てみます。
最初に、モデルのコレクションから指定したインデックスのモデルをひとつだけ取り出して、定数`model`に割り当てています。

````swift:
let model = allModels[modelsPlacedCount]
````

ここでインデックスとして使用されている`modelPlacedCount`は、シーンに配置済みの3Dモデルの総数です。既定値は`0`でした。
つまり、初めて`placeAllModels()`関数が呼び出された時点では、定数`model`に`Model.alarmClock`（レモン）が割り当てられるはずです。

次に、`place(_:at:)`メソッドを使って、この3Dモデルをシーンに追加します。
さらに、配置した3Dモデルの`animate()`メソッドを呼び出して、それぞれ特有の動作をさせています。

````swift:
scene.place(model, at: Point(x: origin.x - xOffset, 
                             y: origin.y, 
                             z: origin.z - 30.cm))
model.animate()
````

なお、3Dモデルは3次元座標が指定されています。
実行したiPadから見て、X軸方向に`48`cm、Z軸方向に`30`cm移動した地点です。

ここからは、モデルを配置した後のコードです。
X軸方向のオフセットである変数`xOffset`を`8`cmだけ増分しています。

````swift:
xOffset += 8.cm
````

既定値が`-48`cmだったので、この時点では`-40`cmに変化します。
もう一度、placeAllModels()が呼び出されると、3Dモデルは`8`cmだけ隣に配置されるはずです。

最後に、モデルのコレクションの全てをシーンに追加しています。

````swift:
if modelsPlacedCount + 1 < allModels.count {
    modelsPlacedCount += 1
    model.play(.ring, loops: false, completion: placeAllModels)
}
````

ここでは、__配置した3Dモデルの数__と__コレクションにあるモデルの総数__を比較しています。
`allModels.count`に達していない場合、`modelsPlacedCount`を`+1`します。

````swift:
modelsPlacedCount += 1
model.play(.ring, loops: false, completion: placeAllModels)
````

`play(_:loops:completion:)`メソッドにある最後のパラメータ`completion:`に注目します。
このパラメータには、`placeAllModels`メソッドが渡されています。
つまり、コレクションにあるすべてのモデルがシーンに追加されるまで、`placeAllModels()`関数が呼び出されることになります。
したがって、メインファイルで`placeAllModels()`メソッドを一度実行するだけで、シーンには用意されたすべてのモデルが配置できます。

## placeAllModels()関数を使ってみる
既存コードをコメントアウトして、`placeAllModels()`関数を呼び出します。

````swift:メインファイル
// let lemon = Model.lemon
//scene.add(lemon)

placeAllModels()
````

ページを実行すると、iPadから見て右から左に向かって、3Dモデルが配置されていきます。
サーフェスを検出したり、モデルの配置場所をタップすることはありません。
3Dモデルが追加される高さは、ちょうどiPadと同じくらいです。
「リン〜、リン〜、...」というリズムに合わせて、空間にひとつずつ3dモデルが出現します。
これは`play(_:loops:completion:)`メソッドの挙動です。
`completion:`部分が完了ハンドラになっていることがわかります。

# コードを読んでわかったこと
「ARを作る」ブックのガイダンスにような内容になっていることが感じ取れました。
また、シーンについては次のようなベクトルがあることがわかりました。

- X軸: 画面左へ + <---> - 画面右へ
- Y軸: 画面上へ + <---> - 画面下へ
- Z軸: 画面前へ + <---> - 画面奥へ

このブックは、少なくとも以下の機能が利用できることがわかりました。

### Scene型オブジェクト
- `add(_:)`メソッド（検出した平面上の任意の場所にモデルを配置する）
- `place(_:at:)`メソッド（コードで指定した場所にモデルを配置する）

### Model型オブジェクト
- `animate()`メソッド
- `play(_:loops:completion:)`メソッド

# さらに
続きのページ「簡単なシーン」の解説は[こちら](https://qiita.com/chino_tweet/items/db8182ad89d20625a56e)