昨日のARKitを使って空間に線を描く方法(初級編)という記事に引き続き、上級編です。
初級編をクリアしたらこちらの記事も読んでみてください。
本記事では、以下のように、1本の線単位の曲線のジオメトリを持つSCNNodeを作る方法を解説していきます。
1ライン1ジオメトリです。
ballのジオメトリをカスタムジオメトリに変更
今まで(昨日の記事)は球(SCNSphere)のジオメトリを作っていました。
let ball = SCNSphere(radius: 0.005)
let node = SCNNode(geometry: ball)
sceneView.scene.rootNode.addChildNode(node)
このballのジオメトリの部分を1本の線の形を意味するジオメトリに変更することができれば、先程の3DBrushのような描画が再現できます。
三角柱をつないだジオメトリを作る
ということで、下記のような頂点の組み合わせで三角柱をつないだようなジオメトリを作ってみたいと思います。
スクリーンのタップした位置から点(下図の青い点)を決定し、それを重心とした三角形をいくつも作っていき、その頂点を三角ポリゴンで面を作ってジオメトリとします。
各正三角形の座標の定義
青い点を重心とする正三角形をつくっていきます。
- 正三角形で1辺の長さは
l
- 重心がタップした位置で
(x, y)
とする
この条件において、0, 1, 2の点は以下のように定まります。
0の座標...(x, y - 3l/√3)
1の座標...(x - l/2, y + √3l/6)
2の座標...(x + l/2, y + √3l/6)
頂点をつなぐ順番をきめる
三角形の各頂点の座標が確定したので、あとはこれをつないでポリゴンを作るだけです。
つなぐ順番を決めるために、インデックスの規則を求めます。
以下のような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する方法
以上、頂点座標の配列と、その頂点をどうつなぐか、2つのデータを使ってジオメトリを作ることができます。
以下のようなイメージです。
polygonVertices
が頂点座標の配列、indicesはインデックスの配列です。
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があって、今回の例はtriangleStripで作ったほうが効率が良いのではないかと思いますが、自分の頭で考えられなかったので、trianglesで行いました。
ちなみに堤さんの線のジオメトリをつくるためのサンプルコードはtriangleStripになってます。
SCNGeometrySourceを算出しよう
それではまず、SCNGeometrySourceを作るために、頂点座標の配列を算出しましょう。
// カメラの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を算出しよう
// 頂点を追加した回数をカウントしときます。
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を渡す
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は線の書き始めだけしか作らず、線を引いているときはジオメトリだけアップデートをかけます。詳しくはサンプルコードを御覧ください!
完成品
カメラ座標系で、zを一定にして正三角形を作っているので、常に三角形がこっちを向いていますが、無事、狙ったとおり三角形の連続のジオメトリを作ることができました。1ライン1ジオメトリです。
まとめ
- 頂点と頂点インデックスで線のポリゴンは作れる
- むずい
- indicesをミスると線のジオメトリが一気に乱れる
- むずすぎ
サンプルコード
サンプルコードはARKit-Emperorにあるのでよければ見てください!