ARKitを使って空間に線を描く方法（上級編）

昨日の[ARKitを使って空間に線を描く方法（初級編）](https://qiita.com/k-boy/items/f68ddfc3937ca57d3a41)という記事に引き続き、上級編です。

初級編をクリアしたらこちらの記事も読んでみてください。

本記事では、以下のように、**１本の線単位の曲線のジオメトリを持つSCNNodeを作る方法**を解説していきます。

![GIFイメージ-D660F2DDC91F-1.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/7fffb3b9-6443-d83d-e03b-0f0b4ac05f3d.gif)

１ライン１ジオメトリです。

# ballのジオメトリをカスタムジオメトリに変更

今まで([昨日の記事](https://qiita.com/k-boy/items/f68ddfc3937ca57d3a41))は球(SCNSphere)のジオメトリを作っていました。

```swift
let ball = SCNSphere(radius: 0.005)
let node = SCNNode(geometry: ball)
sceneView.scene.rootNode.addChildNode(node)
```

このballのジオメトリの部分を１本の線の形を意味するジオメトリに変更することができれば、先程の3DBrushのような描画が再現できます。

# 三角柱をつないだジオメトリを作る

ということで、下記のような頂点の組み合わせで三角柱をつないだようなジオメトリを作ってみたいと思います。

<img width="588" alt="IMG_45833D2F455D-1.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/111366/98964e93-c928-4c65-2492-102f7a119c57.jpeg">

スクリーンのタップした位置から点（下図の青い点）を決定し、それを重心とした三角形をいくつも作っていき、その頂点を三角ポリゴンで面を作ってジオメトリとします。

<img width="303" alt="IMG_B1B05F2CABF1-1.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/111366/d35a4ee4-7bc9-2529-103d-894bd0e85748.jpeg">

# 各正三角形の座標の定義

青い点を重心とする正三角形をつくっていきます。

- 正三角形で1辺の長さは`l`
- 重心がタップした位置で`(x, y)`とする

この条件において、0, 1, 2の点は以下のように定まります。

<img width="636" alt="IMG_B42C6EAC84D5-1.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/111366/d686bbe4-9f4a-1aae-37d8-12f339defde3.jpeg">

0の座標...`(x, y - 3l/√3)`
1の座標...`(x - l/2, y + √3l/6)`
2の座標...`(x + l/2, y + √3l/6)`

# 頂点をつなぐ順番をきめる

三角形の各頂点の座標が確定したので、あとはこれをつないでポリゴンを作るだけです。

つなぐ順番を決めるために、インデックスの規則を求めます。

<img width="588" alt="IMG_1663D6CB3D80-1.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/111366/77a2ce5f-7a62-3e90-5db9-ec643bfcb686.jpeg">

以下のような3点を持つ三角形を結んでいくと、表面を作れて、狙ったジオメトリが作れると思います。

```
// 底面
(0, 1, 2)

// 側面0
(0, 1, 3)
(0, 2, 3)
(1, 2, 4)
(1, 3, 4)
(1, 2, 5)
(2, 3, 5)

// 側面1
(3, 4, 7)
(3, 5, 8)
(4, 5, 7)
(4, 6, 7)
(4, 5, 8)
(5, 6, 8)

// 側面n
(3n, 3n + 1, 3n + 3)
(3n, 3n + 2, 3n + 3)
(3n + 1, 3n + 2, 3n + 4)
(3n + 1, 3n + 3, 3n + 4)
(3n + 1, 3n + 2, 3n + 5)
(3n + 2, 3n + 3, 3n + 5)

// 底面
(3n + 3, 3n + 4, 3n + 5)
```

# 頂点座標と頂点インデックスで三角ポリゴンを定義しジオメトリを作る方法

それではSwiftのコードに戻ります。

ここまでで、ジオメトリの頂点(各三角形の頂点)の座標、頂点を結んでいくインデックスの配列が定まりました。あとはこれらを使ってジオメトリを作っていきます。

SCNGeometryに`init(sources:elements:)`というSCNGeometrySourceとSCNGeometryElementでジオメトリを作るためのイニシャライザがあるのでそれを使います。

ちなみにSCNGeometrySourceとSCNGeometryElementをそれぞれの意味は、

#### SCNGeometrySourceとは
```
// イメージ..xyzの座標
O(0, 0, 0)
A(1, 1, 1)
B(1, 2, 1)
..
```

>A container for vertex data forming part of the definition for a three-dimensional object, or geometry.
https://developer.apple.com/documentation/scenekit/scngeometrysource

3D物体の形状を決めるための頂点データで、頂点座標の集合です。

#### SCNGeometryElementとは
```
// イメージ
(0, 1, 3) // ..0番目と1番目と3番目をつなぐ
(0, 2, 3)
(1, 2, 4)
...
```

>A container for index data describing how vertices connect to define a three-dimensional object, or geometry.
https://developer.apple.com/documentation/scenekit/scngeometryelement

こちらは、SCNGeometrySourceの頂点座標をどのようにつないでいくかというインデックスのデータ。

#### SCNGeometryをinitする方法

以上、頂点座標の配列と、その頂点をどうつなぐか、２つのデータを使ってジオメトリを作ることができます。

以下のようなイメージです。

`polygonVertices`が頂点座標の配列、indicesはインデックスの配列です。

```swift
let source = SCNGeometrySource(vertices: polygonVertices)
let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
let geometry = SCNGeometry(sources: [source], elements: [element])

let node = SCNNode()
node.geometry = geometry
```

SCNGeometryElementのprimitiveTypeには今回trianglesを採用しました。自分でジオメトリを手書きで考えたのでイメージしやすくするためです。

他にもtriangleStrip, line, pointという[SCNGeometryPrimitiveType](https://developer.apple.com/documentation/scenekit/scngeometryprimitivetype)があって、今回の例はtriangleStripで作ったほうが効率が良いのではないかと思いますが、自分の頭で考えられなかったので、trianglesで行いました。

ちなみに[堤さん](https://twitter.com/shu223)の線のジオメトリをつくるための[サンプルコード](https://github.com/shu223/ARKit-Sampler/blob/master/ARKit-Sampler/Samples/ARDrawing/DynamicGeometry.swift#L58)はtriangleStripになってます。

# SCNGeometrySourceを算出しよう

それではまず、SCNGeometrySourceを作るために、頂点座標の配列を算出しましょう。

```swift
// カメラのnode
guard let camera = sceneView.pointOfView else {
    return
}

// pointTouchingは、スクリーンをタップした位置のx, y座標
// それをワールド座標系に変換後、さらにカメラ座標に変換
// pointCameraは各三角形の重心のカメラ座標です。
let pointWorld = sceneView.unprojectPoint(SCNVector3Make(Float(pointTouching.x), Float(pointTouching.y), 0.997))
let pointCamera = camera.convertPosition(pointWorld, from: nil)

// カメラ座標で、重心の座標から、三角形の3つの頂点座標を求めます。
let x: Float = pointCamera.x
let y: Float = pointCamera.y
let z: Float = -0.2
let lengthOfTriangle: Float = 0.01

// 先程算出した式を適用してます
let vertice0InCamera = SCNVector3Make(
    x,
    y - (sqrt(3) * lengthOfTriangle / 3),
    z
)
let vertice1InCamera = SCNVector3Make(
    x - lengthOfTriangle / 2,
    y + (sqrt(3) * lengthOfTriangle / 6),
    z
)
let vertice2InCamera = SCNVector3Make(
    x + lengthOfTriangle / 2,
    y +  (sqrt(3) * lengthOfTriangle / 6),
    z
)

// ここまではカメラ座標なので最後にワールド座標系に変換
let vertice0 = camera.convertPosition(vertice0InCamera, to: nil)
let vertice1 = camera.convertPosition(vertice1InCamera, to: nil)
let vertice2 = camera.convertPosition(vertice2InCamera, to: nil)

// 配列に追加しとく
polygonVertices += [vertice0, vertice1, vertice2]
```

# SCNGeometryElementを算出しよう

```swift

// 頂点を追加した回数をカウントしときます。
let n: Int32 = centerVerticesCount - 2

// nのときのインデックスの配列は先程計算したやつを適用してます
// ちなみに底面も毎回作っちゃってます。本当はジオメトリの最初と最後だけでいいと思いますが、コードが煩雑になるので一旦毎回作りました。
let m: Int32 = 3 * n
let nextIndices: [Int32] = [
    m    , m + 1, m + 2, // 底面
    m    , m + 1, m + 3, // 以下、側面を作るためのインデックス
    m    , m + 2, m + 3,
    m + 1, m + 2, m + 4,
    m + 1, m + 3, m + 4,
    m + 1, m + 2, m + 5,
    m + 2, m + 3, m + 5, // 以上、側面を作るためのインデックス
    m + 4, m + 3, m + 5, // 底面
]

// 配列に追加
indices += nextIndices
```

# そしてSCNGeometryにverticesとindicesを渡す

```swift
let source = SCNGeometrySource(vertices: polygonVertices)
let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
let geometry = SCNGeometry(sources: [source], elements: [element])

let node = SCNNode()
node.geometry = geometry
```

ジオメトリが作れました！！

あとは、このSCNNodeは線の書き始めだけしか作らず、線を引いているときはジオメトリだけアップデートをかけます。詳しくは[サンプルコード](https://github.com/kboy-silvergym/ARKit-Emperor/tree/master/ARKit-Emperor/View/Doodle2)を御覧ください！

# 完成品

カメラ座標系で、zを一定にして正三角形を作っているので、常に三角形がこっちを向いていますが、無事、狙ったとおり三角形の連続のジオメトリを作ることができました。1ライン1ジオメトリです。

![drawing2.gif](https://qiita-image-store.s3.amazonaws.com/0/111366/6091ef79-b434-6ee8-1b86-3cdb6f75192a.gif)

# まとめ

- 頂点と頂点インデックスで線のポリゴンは作れる
- むずい
- indicesをミスると線のジオメトリが一気に乱れる
- むずすぎ

# サンプルコード

サンプルコードは[ARKit-Emperor](https://github.com/kboy-silvergym/ARKit-Emperor)にあるのでよければ見てください！

https://github.com/kboy-silvergym/ARKit-Emperor/tree/master/ARKit-Emperor/View/Doodle2