ARKitのための3D数学

<img width="733" alt="スクリーンショット 2018-08-29 23.58.57.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/9a23510d-8aaa-cf3e-7a4d-1faba6f003cb.png">

この記事はiOSDC2018で発表した内容のまとめ、そして続きになります。

- [ARKitのための3D算数 iOSDC Japan 2018](https://fortee.jp/iosdc-japan-2018/proposal/f59795b0-ef1e-4e98-9199-8401e7a0acf9)
- [筋肉ネタを含んだスライド](https://speakerdeck.com/fujikawakei/arkitfalsetamefalse3dsuan-shu)

ARKitやSceneKitは用意されたAPIを使えば色々なことが簡単にできてしまいますが、
高度なことをしようとすると、空間ベクトル、座標変換などの算数(数学)の知識が必要になることに気づくでしょう。

例えば以下の例を見てみましょう。

|カメラの前にスタンプを置く|カメラの前に文字を書く|
|:--:|:--:|
|![image](https://user-images.githubusercontent.com/17683316/42438970-8c02222c-839c-11e8-8ccb-5b0d266aa02a.gif)|![line](https://cdn-ak.f.st-hatena.com/images/fotolife/g/graffity/20180731/20180731131141.gif)|

カメラの前に文字を書いたり、スタンプを書いたりする際、一度カメラ座標で考えてからワールド座標に変換すると簡単に表現することができたりします。

ということで、本記事ではARKitを使いこなすために自分が勉強した**3Dプログラミングと基本的な算数(数学)**について分かりやすく説明します。

# ARKitで使う座標系

まず、基本ですが、ARKitでは**右手座標系**(画面の手前がz)を使います。

<img src="https://docs-assets.developer.apple.com/published/ffb3831f78/d937c9e9-05fd-46e3-b72d-eb353b79bfbd.png" width=300>

3Dをやったことないと、直感的にzは画面の奥の方を示すイメージをしてしまうので注意です。(僕がそうでした)

それでは各座標系について説明します。
## まずは座標まとめ

<img width="690" alt="スクリーンショット 2018-08-31 19.20.18.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/ef7e6afc-1f65-e34c-3f5d-4d8973907382.png">

今回説明するのは、ワールド座標系、オブジェクト座標系、カメラ座標系、スクリーン座標系です。１つずつ簡単に解説していきます。

## ワールド座標系
<img width="424" alt="スクリーンショット 2018-08-25 19.42.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/0a15adee-4679-5f92-0bae-267418a0787b.png">

ARKitのSessionをRunして空間を認識した時に決まる座標系。ワールド空間は、**これより大きな外側の座標空間で表現できません。**

実世界で考えると、すべての国が載っている世界地図というイメージ。AR空間に置く物体は、この座標系で絶対位置を求められます。

## オブジェクト座標系

<img width="438" alt="スクリーンショット 2018-08-25 19.34.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/3d657e54-ac6b-c51e-58f7-9a1216528f13.png">

世界地図に対して日本地図とか、東京の地図みたいなイメージ。
上図では、**飛行機自身のローカル座標系です。**

例えばこの座標系に飛行機と鳥がいて、鳥は飛行機の横1mの位置にいるという状況を考えましょう。

飛行機がこの座標系の原点である場合、飛行機のオブジェクト座標系における鳥の座標は`(1, 0, 0)` といった感じで示されます。

## カメラ座標系
<img width="499" alt="スクリーンショット 2018-08-25 19.46.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/0a9f00a5-aa41-bb1a-c40d-9fe355df1eec.png">

カメラ座標系は、オブジェクト座標系の１つです。上図でいう左側の**カメラのローカル座標系**です。

ARKitを起動した後、ユーザーはスマホを空間上で動かしますよね。このときにカメラはワールド座標系の座標を変化してしまうので、**カメラからの相対位置を表現するときに**このカメラ座標系が役に立つわけです。

## スクリーン座標系
<img src="https://cdn-images-1.medium.com/max/1600/1*SJU8WmP-aHgrwlT92oCRAw.jpeg">

UIKitでおなじみの座標系。左上が原点xは右が正、yは下が正。(Macは違うみたい)

カメラから見た景色は、スクリーン上に投影されて表現されるわけです。

### カメラ座標とスクリーン座標の関係
<img width="634" alt="スクリーンショット 2018-08-29 9.31.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/d7187768-6d9c-f9d0-5b00-cd1a6e8691e1.png">

黄色で囲まれた部分がスクリーン上に映る範囲(視錐台のnearからfarの間)です。カメラ座標系の原点はスクリーンの後ろにあります。

<img width="682" alt="スクリーンショット 2018-09-21 21.40.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/550c8cf9-1a61-def9-8745-b8a15c15c8f3.png">

カメラ座標系で、スクリーン上に映る範囲のz座標はARKitでは、**0.001~1000m**です。それに対して、それをスクリーン座標系のzで表現すると**0~1.0**になります。

注意するべきなのは、これらは**比で表現できない**ことです。投影するときの座標変換はもっと複雑で以下のような曲線の式になります。

<img width="764" alt="スクリーンショット 2018-08-29 10.22.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/dc7204bb-6bfa-83b7-cdb9-3001787028ee.png">
http://www.alecjacobson.com/weblog/?p=3835

カメラ座標系のzに関しては**SCNCamera**のドキュメントに、スクリーン座標系のzに関しては**unprojectPoint**メソッドや**projectPoint**のメソッドのドキュメントに書いてあるのでチェックしてみましょう。

- [SCNCamera](https://developer.apple.com/documentation/scenekit/scncamera)
- [unprojectPoint](https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522631-unprojectpoint)
- [projectPoint](https://developer.apple.com/documentation/scenekit/scnscenerenderer/1524089-projectpoint)

# 飛行機をカメラの30cm前に置く時

具体的に座標系をどう使いこなすのかの例をあげます。以下のようにカメラの30cm前に飛行機を置きたいとき、どのようなコードを書くのか紹介します。

![GIFイメージ-0677D152F51B-1.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/cf0fe81f-4a26-cd77-ed96-9a097037bcaf.gif)

まずカメラ座標で30cm前を表現してから、スクリーン座標で指のxyを取得し、世界座標に変換してから飛行機を置きます。

```swift
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
    // カメラ座標系で30cm前
    let infrontOfCamera = SCNVector3(x: 0, y: 0, z: -0.3)
    
    // カメラ座標系 -> ワールド座標系
    guard let cameraNode = sceneView.pointOfView else { return }
    let pointInWorld = cameraNode.convertPosition(infrontOfCamera, to: nil)
    // ワールド座標系 -> スクリーン座標系
    var screenPos = sceneView.projectPoint(pointInWorld)
    
    // スクリーン座標系で
    // x, yだけ指の位置に変更
    // zは変えない
    let finger = recognizer.location(in: nil)
    screenPos.x = Float(finger.x)
    screenPos.y = Float(finger.y)
    
    // ワールド座標に戻す
    let finalPosition = sceneView.unprojectPoint(screenPos)
    
    // nodeを置く
    let airPlaneNode = airPlane
    airPlaneNode.position = finalPosition
    sceneView.scene.rootNode.addChildNode(airPlaneNode)
}
```

# SceneKitのメソッドで座標変換

では、具体的な座標変換メソッドを紹介します。

## ローカルA座標系 <-> ローカルB座標系

```swift
func convertPosition(_ position: SCNVector3, 
                  to node: SCNNode?) -> SCNVector3
```

https://developer.apple.com/documentation/scenekit/scnnode/1407990-convertposition

### 使用例

#### カメラ座標系をワールド座標系に変換

```swift
// to: の引数をnilにするとワールド座標系に変換される
let positionWorld = cameraNode.convertPosition(positionCamera, to: nil)
```

#### カメラ座標系を飛行機オブジェクトのローカル座標系に変換
```swift
let positionPlaneLocal = cameraNode.convertPosition(positionCamera, to: airPlaneNode)
```

## ワールド座標系 -> スクリーン座標系

```swift
func projectPoint(_ point: SCNVector3) -> SCNVector3
```

https://developer.apple.com/documentation/scenekit/scnscenerenderer/1524089-projectpoint

### 使用例

#### ワールド座標系のある位置をスクリーンに投影

```swift
let position = SCNVector3(x: 0, y: 0, z: 1)

// ワールド座標系 -> スクリーン座標系
var screenPos = sceneView.projectPoint(position)
```

## スクリーン座標系 -> ワールド座標系

```swift
func unprojectPoint(_ point: SCNVector3) -> SCNVector3
```

https://developer.apple.com/documentation/scenekit/scnscenerenderer/1522631-unprojectpoint

### 使用例

#### スクリーンのタップした位置を取得してワールド座標に変換

```swift
@objc func tapGesture(_ recognizer: UITapGestureRecognizer) {
    // スクリーン座標系
    let finger = recognizer.location(in: nil)
    let pos = SCNVector3(finger.x, finger.y, 0.996)
    
    // ワールド座標に戻す
    let finalPosition = sceneView.unprojectPoint(pos)
    
    // nodeを置く
    airPlaneNode.position = finalPosition
    sceneView.scene.rootNode.addChildNode(airPlaneNode)
}
```

# SceneKitのメソッドで拡大縮小
<img width="221" alt="スクリーンショット 2018-08-29 23.45.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/180e9be4-f4f3-e4e7-eef2-a77b6c8be16f.png">

scaleが1.0だった場合は以下のメソッドで物体の大きさが２倍になります。

```swift
let ship1 = node.childNode(withName: "ship1", recursively: true)!
ship1.scale = SCNVector3(2.0, 2.0, 2.0)
```

# SceneKitのメソッドで回転
<img width="268" alt="スクリーンショット 2018-08-29 23.40.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/9afb343b-d04d-dcb3-2bcc-9d0929f0aab3.png">

重要なのは、SceneKit, ARKitは右手座標系なので回転は右ねじの法則に従うことです。

## rotationを使う方法

```swift
let ship2 = node.childNode(withName: "ship2", recursively: true)!
ship2.rotation = SCNVector4(x: 1,
                            y: 0,
                            z: 0,
                            w: -.pi / 4)
```

この方法では、軸と回転角で回転を定義します。

<img width="712" alt="スクリーンショット 2018-08-29 23.29.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/52c53132-238e-cddf-eba4-73a1ca7c561b.png">

SCNNodeの[`pivot`](https://developer.apple.com/documentation/scenekit/scnnode/1408044-pivot)と、ここで定義したx,y,zの点を結ぶ線が軸です。

pivotは原点を示します。特別変更しない場合はこのローカル座標系の`(0, 0, 0)`です。

また `.pi`は数学でおなじみのπです。180度を示します。

## Euler angleを使う方法

```swift
let ship2 = node.childNode(withName: "ship2", recursively: true)!
ship2.eulerAngles.x = -.pi / 4
```

上でrotationで行ったことと同じことは、このコードで表現できます。個人的にはこちらの方が直感的でわかりやすいです。

以下にeulerAnglesのx,y,zをそれぞれ変更したらどう変わるかの図を示します。

### x
<img width="268" alt="スクリーンショット 2018-08-29 23.40.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/9afb343b-d04d-dcb3-2bcc-9d0929f0aab3.png">

```swift
ship2.eulerAngles.x = -.pi / 4
```

### y
<img width="348" alt="スクリーンショット 2018-08-29 23.41.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/ae6541dc-924a-4585-f444-72ca3ac1496c.png">

```swift
ship2.eulerAngles.y = -.pi / 4
```

### z
<img width="298" alt="スクリーンショット 2018-08-29 23.41.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/bbceda54-7217-56c2-25dd-eb11d5696f54.png">

```swift
ship2.eulerAngles.z = -.pi / 4
```

# ARKit(SceneKit)を使いこなそう

以上がARKit, SceneKitのメソッドを使った座標空間でした。ここまでを概念的に理解すればARKitの実装で困ることはないでしょう。しかし、数学的な理解も含めて置くと、もっと高度なことができるし汎用的な3Dプログラミングの知識を得ることができます。

というのも、`SCNNode`には`transform`というPropertyがあります。

https://developer.apple.com/documentation/scenekit/scnnode/1407964-transform

ここまでに紹介した方法で、座標変換や拡大縮小、回転などを行うことができますが、以下の3D数学を勉強することで、`transform`を使った変換を理解することができます。というのも`transform`の型は[`SCNMatrix4`](https://developer.apple.com/documentation/scenekit/scnmatrix4)で、matrixは**行列**という意味だからです。

# 座標変換の数学的な表現

それでは行列の話です。

ARにおける座標変換は、回転したり、大きくなったり、位置が変わる幾何学的な表現ですが、**数学的に**計算する時には「**ベクトル * 行列**」　と表現することができます。

## 2Dの座標変換

まずは、2Dでイメージするとわかりやすいです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/111366/e479625f-0989-6901-bde9-dac73345bc99.jpeg">

### 式

```math
\begin{pmatrix}
2 & 1 \\
-1 & 2
\end{pmatrix}

\begin{pmatrix}
1 \\
1
\end{pmatrix}
```

幾何学的に、回転したり、大きくしたりすることが確認できると思います。

## 3Dの座標変換

次は3Dで考えてみましょう。

<img src="https://qiita-image-store.s3.amazonaws.com/0/111366/eee91fd7-c44c-375c-24a7-a006a485d6af.jpeg" width=540>

### 式

```math

\begin{pmatrix}
0.707 & -0.707 & 0 \\
1.250 & 1.250 & 0 \\
0 & 0 & 1
\end{pmatrix}

\begin{pmatrix}
1 \\
1 \\
1
\end{pmatrix}
```

この場合では行列の３行目が`[0, 0, 1]`であるため、+zは変化していません。

結果的にポットは、z軸を中心に45度回転し、縦長にスケーリングされています。

では、どういう行列を掛けると回転して、どういうのを掛けるとスケーリングするのでしょうか？

#### z軸の周りをθ回転させる変換行列

```math
\begin{pmatrix}
cosθ & sinθ & 0 \\
-sinθ & cosθ & 0 \\
0 & 0 & 1
\end{pmatrix}
```

#### x軸方向に2倍, yは３倍, zは4倍にスケーリングする行列

```math
\begin{pmatrix}
2 & 0 & 0 \\
0 & 3 & 0 \\
0 & 0 & 4
\end{pmatrix}
```

#### 組み合わせると

```math
\begin{pmatrix}
2 & 0 & 0 \\
0 & 3 & 0 \\
0 & 0 & 4
\end{pmatrix}

\begin{pmatrix}
cosθ & sinθ & 0 \\
-sinθ & cosθ & 0 \\
0 & 0 & 1
\end{pmatrix}

\begin{pmatrix}
1 \\
1 \\
1
\end{pmatrix}
```

https://ja.wikipedia.org/wiki/%E5%9B%9E%E8%BB%A2%E8%A1%8C%E5%88%97

z軸の周りをθ回転させたあとに、スケーリングすることができます。

## 4Dの座標変換

最後に4Dについて解説します。3次元まではx, y, zで示すのでイメージしやすいですが、空間は３次元でしか表わせないのに。４次元目を使うことがあります。

４次元目はなんでしょうか？ **時空を超えるのでしょうか？**

**違います。計算用に使うだけです。**

4Dベクトルの４つ目の成分は`w`で、同次座標とも呼ばれます。

理解しやすくするためにまずは2Dの物理空間に対する3Dの同次空間とはどういう意味なのか考えてみましょう。

<img src="https://qiita-image-store.s3.amazonaws.com/0/111366/7dfc54a0-277b-67e9-f4c6-1beeba70be97.jpeg" width=540>

図のような `(x, y, w)`という形をしている2Dの同次座標を調べてみます。3D空間上の平面w=1にある物理的な2Dの点`(x, y)`は同次空間で`(x, y, 1)`と表されます。ここで、同次座標`(x, y, w)`は物理的な2Dの点`(x/w, y/w)`に対応づけられます。

つまり、w=5の同次空間の点`(5, 10, 5)`は物理的な2D空間に投影すると、`(1, 2)`になるということです。逆に言うと、`(1, 2)`には`(3, 6, 3)`,(4, 8, 4)` というような同次空間上の点が無限にあるということです。

では、**次元を一個上にあげて、物理的3D空間に対する4Dの同次空間で考えてみましょう。**4Dという空間を幾何学的に理解する必要はありません(理解できない)。3Dの一個上にそういう空間があることだけイメージできれば良いです。

この考え方で、物理的な3Dの点は4D内のw=1の「平面」内に存在していると考えられます。4Dの点は`(x, y, z, w)`という形であり、4Dの点を対応する3Dの点`(x/w, y/w, z/w)`を作る「平面」に投影します。

あえてイメージできない4D空間を使うのには理由があります。

**線形変換で平行移動を表現するため**です。

３次元座標に3*3の正方行列をかけることで、幾何学的に回転やスケーリングと言った線形変換が可能なことを先程述べましたが、このままだと、ポットは原点から離れられないのです。

<img src="https://qiita-image-store.s3.amazonaws.com/0/111366/eee91fd7-c44c-375c-24a7-a006a485d6af.jpeg" width=540>

そのため4Dを使います。

wが常に1であると仮定します。これにより、3Dベクトル`(x, y, z)`は常に4Dで`(x, y, z, 1)`と表されます。

また、3×3の変換行列はすべて4Dでは次のように表すことができます。

```math
\begin{pmatrix}
m11 & m12 & m13 \\
m21 & m22 & m22 \\
m31 & m32 & m33
\end{pmatrix}

=>

\begin{pmatrix}
m11 & m12 & m13 & 0 \\
m21 & m22 & m22 & 0 \\
m31 & m32 & m33 & 0 \\
0 & 0 & 0 & 1 
\end{pmatrix}
```

3×4や4×3の行列式は計算できませんが、4×4なら計算することができます。4行目にΔx, Δy, Δzと示すことによって平行移動を示すことができるのです。

以下は3次元の単位に、4行目、４列目を足して、平行移動を表現した式です。

```math
\begin{pmatrix}
1 & 0 & 0 & Δx \\
0 & 1 & 0 & Δy \\
0 & 0 & 1 & Δz \\
0 & 0 & 0 & 1 
\end{pmatrix}

\begin{pmatrix}
x \\
y \\
z \\
1
\end{pmatrix}


=

\begin{pmatrix}
x + Δx \\
y + Δy \\
z + Δz \\
1
\end{pmatrix}
```

x, y, zだけに着目するとそれぞれΔ分平行移動することができています。このように4Dで考えることで平行移動を表現することができるのです。

## アフィン変換

<img width="151" alt="スクリーンショット 2018-10-03 16.32.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/111366/93238f04-3c33-43d6-cef9-8923fd1bd126.png">

先に述べた４次元の行列を使って座標変換することをアフィン変換と呼びます。

赤い部分が線形変換で、青いところが平行移動です。　
アフィン変換を使うと線形変換と平行移動が１つの行列で表現できるわけです。

## SCNNodeのtransform

拡大縮小、拡大、せん断（投影）、平行移動は４*4の行列で表現できることがわかったと思います。これをSCNNodeの[transform](https://developer.apple.com/documentation/scenekit/scnnode/1407964-transform)に適用することで、ARKit(SceneKit)でもアフィン変換をすることができます。

### あるnodeのtransformをx軸を中心に90度回転させる

transformを使って計算する際も自前でゴリゴリ掛け算足し算をして行列を算出するのはクールではありません。ここは[SCNMatrix4](https://developer.apple.com/documentation/scenekit/scnmatrix4)を使うのがいいと思います。

例えばあるnodeのtransform(右側の1~16で示される行列)を**x軸を中心にθ度回転させる**行列は以下です。

```math
\begin{pmatrix}
1 & 0 & 0 & 0\\
0 & cosθ & -sinθ & 0\\
0 & sinθ & cosθ & 0\\
0 & 0 & 0 & 1
\end{pmatrix}

\begin{pmatrix}
1 & 2 & 3 & 4 \\
5 & 6 & 7 & 8 \\
9 & 10 & 11 & 12 \\
13 & 14 & 15 & 16
\end{pmatrix}
```

https://ja.wikipedia.org/wiki/%E5%9B%9E%E8%BB%A2%E8%A1%8C%E5%88%97

これをSCNMatrix4を使って表現すると以下のようになります。

```swift
// あるnodeのtransform
let transform: SCNMatrix4 = SCNMatrix4(m11: 1, m12: 2, m13: 3, m14: 4,
                                       m21: 5, m22: 6, m23: 7, m24: 8,
                                       m31: 9, m32: 10, m33: 11, m34: 12,
                                       m41: 13, m42: 14, m43: 15, m44: 16)

// x軸を中心に90度の回転
let rotate = SCNMatrix4Rotate(
    transform,
    .pi/2,
    1,
    0,
    0
)

// 行列の積。上の図の行列式と同じ意味。
let mult = SCNMatrix4Mult(rotate, transform)

// 型は場合に応じて変更
let matrixFloat: simd_float4x4 = matrix_float4x4(mult)
```

行列の積はかける順番によってアウトプットが変わるので注意が必要です。

参考： [SceneKitは行ベクトル？列ベクトル？
](https://qiita.com/k-boy/items/40354da198b91619f5c3)

### 例： タップした位置のxyzを取得したい

SCNVector3ではなくtransformでしか値を取得できないケースがあります。HitTestを得る場合などです。

```swift
var sceneView: ARSCNView!
let point: CGPoint = CGPoint(x: 0, y: 0)

if let hitestResult = sceneView.hitTest(point, types: .featurePoint).first {
    let column3 = hitestResult.worldTransform.columns.3

    // ここに注目！！
    let float: float3 = float3(column3.x, column3.y, column3.z)
    let position: SCNVector3 = SCNVector3(float)
}
```

hitTestというメソッドを使ってタップした位置の3D座標xyzを取得したい時があると思いますが、この時取得できるのはhitTestの4次元配列transformです。

この場合に4行目

```math
\begin{pmatrix}
Δx \\
Δy \\
Δz \\
1
\end{pmatrix}
```

のxyzを取り出したのが

```swift
let float: float3 = float3(column3.x, column3.y, column3.z)
```

のコードです。ΔxΔyΔzは平行移動を示す値ですよね。

原点にこの平行移動の座標変換をかけると求める3D座標の**点**を求めることができるのです。

# まとめ

以上、ARKitで使う座標系の紹介、座標変換メソッド、そして数学的な表現を紹介しました。両方理解して　自由自在に物体を操っていきたいですね。

# 参考文献
- [SceneKit | Apple Developer Documentation](https://developer.apple.com/documentation/scenekit/)
- [実例で学ぶゲーム3D数学](https://amzn.to/2MSuMeG) (画像を一部こちらから拝借しています :bow: )

# サンプルコード

https://github.com/kboy-silvergym/3DMath-For-ARKit

# iOSDCでの発表スライド

https://speakerdeck.com/fujikawakei/arkitfalsetamefalse3dsuan-shu

# こちらもチェック

- [ARKit 2.0のサンプルコード集「ARKit-Emperor」を公開しました！](https://qiita.com/k-boy/items/760f8b3add6ea283f41c)
- [ARKit 2.0の何が凄いのか](https://qiita.com/k-boy/items/d935b76fed67e11b9fb2)
- [ARKitの可能性を知るためにオススメの教材2選
](https://qiita.com/k-boy/items/7e47c818557f6876cf45)
- [ARCoreのCloud AnchorsをSwift, iOSで試してみた](https://qiita.com/k-boy/items/69e345fc69d681a9bff3)
- [ARKitで空間上に写真を置く
](https://qiita.com/k-boy/items/e18e74e8b341f5d5675d)