ARMeshAnchorからタップした箇所のARMeshClassificationを取得する

ARKit 3.5とLiDAR搭載の新型iPad Proが出ましたね。これらを試せるAppleの公式サンプル「[Visualizing and Interacting with a Reconstructed Scene](https://developer.apple.com/documentation/arkit/world_tracking/visualizing_and_interacting_with_a_reconstructed_scene)」が出ています。

どんな感じのサンプルかはこちらのツイートの動画がよくわかります。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">First look at the iPad Pro LiDAR Scanner <a href="https://t.co/kwkl1YBy2n">pic.twitter.com/kwkl1YBy2n</a></p>&mdash; Tim Field (@nobbis) <a href="https://twitter.com/nobbis/status/1242926445913157634?ref_src=twsrc%5Etfw">March 25, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このサンプルでは、**タップした箇所のClassification結果が3Dテキストで可視化**されるようになっています。

Classificationというのはそのメッシュが壁なのか床なのかテーブルなのか、という分類を示す情報で、ARMeshClassificationというenumで定義されています。

```swift
public enum ARMeshClassification : Int {
   case none = 0
   case wall = 1
   case floor = 2
   case ceiling = 3
   case table = 4
   case seat = 5
   case window = 6
   case door = 7
}
```

https://developer.apple.com/documentation/arkit/armeshclassification

公式サンプルでは、画面（`ARView`）タップでraycastによる（`.estimatedPlane`との）当たり判定を行い、classificationの判定結果をテキスト表示しているのですが、

```swift
if let result = arView.raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .any).first {
   let resultAnchor = AnchorEntity(world: result.worldTransform)
   ...
}
```

ここらへんのコードが実はおもしろくて、Scene Reconstructionの有効化やメッシュの可視化はめちゃくちゃ簡単にできるようになっているのでClassificationの結果も`ARMeshAnchor`のプロパティから取り出して終わりでしょ、と思いきや、そんなプロパティはなく、**意外とめんどくさい処理**をやっています。

https://developer.apple.com/documentation/arkit/armeshanchor

### 

具体的には`ARMeshAnchor`の`geometry`プロパティに入っている`ARMeshGeometry`オブジェクトの`classification`プロパティから取り出しているのですが、これがまだ`ARMeshClassification`型ではなくて、`ARGeometrySource`型なのです。

```swift
var vertices: ARGeometrySource
```

https://developer.apple.com/documentation/arkit/armeshgeometry

で、`ARGeometrySource`というのが割とめんどくさいクラスで、そのデータを`MTLBuffer`というMetalのバッファに保持しています。

```swift
open class ARGeometrySource : NSObject, NSSecureCoding {

   
   /**
    A Metal buffer containing per-vector data for the source.
    */
   open var buffer: MTLBuffer { get }

   
   ...
}
```

https://developer.apple.com/documentation/arkit/argeometrysource

サンプルではこのバッファを（GPUではなく）CPUでほじくり、とりだした生の数値から`ARMeshClassification`を初期化しています。

```swift
extension ARMeshGeometry {
    ...
    
    /// To get the mesh's classification, the sample app parses the classification's raw data and instantiates an
    /// `ARMeshClassification` object. For efficiency, ARKit stores classifications in a Metal buffer in `ARMeshGeometry`.
    func classificationOf(faceWithIndex index: Int) -> ARMeshClassification {
        guard let classification = classification else { return .none }
        assert(classification.format == MTLVertexFormat.uchar, "Expected one unsigned char (one byte) per classification")
        let classificationPointer = classification.buffer.contents().advanced(by: classification.offset + (classification.stride * index))
        let classificationValue = Int(classificationPointer.assumingMemoryBound(to: CUnsignedChar.self).pointee)
        return ARMeshClassification(rawValue: classificationValue) ?? .none
    }
```

### タップ位置に近いメッシュのfaceを抽出

前項の処理で出てきた`classificationOf(faceWithIndex:)`メソッドではfaceのindexを引数に渡し、そのfaceについてのclassifiation結果を取り出しています。

メッシュの中にたくさんある中で、タップした位置に近いfaceを取り出してそのclassification結果を可視化しているわけです。

その「タップした位置に近い（5cm以内）場所にあるfaceを取り出す」実装はこうなっています。

```swift
for anchor in meshAnchors {
    for index in 0..<anchor.geometry.faces.count {
        // Get the center of the face so that we can compare it to the given location.
        let geometricCenterOfFace = anchor.geometry.centerOf(faceWithIndex: index)
        
        // Convert the face's center to world coordinates.
        var centerLocalTransform = matrix_identity_float4x4
        centerLocalTransform.columns.3 = SIMD4<Float>(geometricCenterOfFace.0, geometricCenterOfFace.1, geometricCenterOfFace.2, 1)
        let centerWorldPosition = (anchor.transform * centerLocalTransform).position
         
        // We're interested in a classification that is sufficiently close to the given location––within 5 cm.
        let distanceToFace = distance(centerWorldPosition, location)
        if distanceToFace <= 0.05 {
            ...
        }
    }
}
```

この計算をするためには、`ARMeshGeometry`の`vertices`プロパティと`faces`プロパティを使用する必要があり、これまた`ARGeometrySource`型。Metalバッファから数値を読み出すために、`ARMeshGeometry`のextensionとして次のようなメソッドが実装されています。

```swift
extension ARMeshGeometry {
    func vertex(at index: UInt32) -> (Float, Float, Float) {
        assert(vertices.format == MTLVertexFormat.float3, "Expected three floats (twelve bytes) per vertex.")
        let vertexPointer = vertices.buffer.contents().advanced(by: vertices.offset + (vertices.stride * Int(index)))
        let vertex = vertexPointer.assumingMemoryBound(to: (Float, Float, Float).self).pointee
        return vertex
    }

    ...
    
    func vertexIndicesOf(faceWithIndex faceIndex: Int) -> [UInt32] {
        assert(faces.bytesPerIndex == MemoryLayout<UInt32>.size, "Expected one UInt32 (four bytes) per vertex index")
        let vertexCountPerFace = faces.indexCountPerPrimitive
        let vertexIndicesPointer = faces.buffer.contents()
        var vertexIndices = [UInt32]()
        vertexIndices.reserveCapacity(vertexCountPerFace)
        for vertexOffset in 0..<vertexCountPerFace {
            let vertexIndexPointer = vertexIndicesPointer.advanced(by: (faceIndex * vertexCountPerFace + vertexOffset) * MemoryLayout<UInt32>.size)
            vertexIndices.append(vertexIndexPointer.assumingMemoryBound(to: UInt32.self).pointee)
        }
        return vertexIndices
    }
    
    func verticesOf(faceWithIndex index: Int) -> [(Float, Float, Float)] {
        let vertexIndices = vertexIndicesOf(faceWithIndex: index)
        let vertices = vertexIndices.map { vertex(at: $0) }
        return vertices
    }
    
    func centerOf(faceWithIndex index: Int) -> (Float, Float, Float) {
        let vertices = verticesOf(faceWithIndex: index)
        let sum = vertices.reduce((0, 0, 0)) { ($0.0 + $1.0, $0.1 + $1.1, $0.2 + $1.2) }
        let geometricCenter = (sum.0 / 3, sum.1 / 3, sum.2 / 3)
        return geometricCenter
    }
}
```

`ARMeshGeometry`の`MTLBuffer`オブジェクトに格納されたデータをCPUから取り出す実装はMetalや3Dデータの扱いに不慣れなiOSデベロッパは多いと思われ、このサンプルは非常に貴重です。

## 関連

- [ARKitのサンプルコード集「ARKit-Sampler」 - Qiita](https://qiita.com/shu223/items/e55e1426c9e036254761)

- [実践ARKit - BOOTH](https://shu223.booth.pm/items/1038241)
- [Metal入門 - BOOTH](https://shu223.booth.pm/items/826318)