Iganinのブログ

日頃の開発で学んだ知見を中心に記事を書いています。

【iOS】ARKitのFace Trackingの結果をobjファイルとして出力する

TL;DR;

  • ARSessionでARFaceTrackingConfigurationを走らせる
  • ARFaceGeometryをMDLAssetに変換する
  • MDLAssetのexport(to:)メソッドを使う

objファイルについて

いわゆるobj形式のファイル。下記の記事が参考になった。 ブログ記事からの引用ではあるが、objファイルは頂点座標,頂点法線,頂点テクスチャ座標から構成されるポリゴンを記述できる. 頂点座標のみや座標と法線のみなどでの記述も可能である.とのこと。 OBJ形式 - PukiWiki for PBCG Lab

objファイルをXcodeで閲覧すると下記のようになる。 こちらは自身の顔をARKitのFace Trackingしたデータをobjファイルに変換して出力したもの、本稿ではARKitでの検知結果を下記のようなobjファイルとして出力することを目指す。

f:id:Iganin:20211213044526p:plain

ARKit Face Tracking Configuration

ARKitのFace Trackingに関して簡単に記す。 ARSessionを生成し、runする際にARFaceTrackingConfigurationを指定することで、機能を使用できる。 ( Tracking and Visualizing Faces) arSession.run(ARFaceTrackingConfiguration())

ARSCNViewを使用することで、ARSCNViewDelegateのメソッドの renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor)を経由してFace Trackingの情報を取得できる。

if let faceAnchor = anchor as? ARFaceAnchor {
  // do something with ARFaceAnchor
}

ARFaceAnchorからは目の位置、顔が向いている方向などさまざまな情報を取得できる(ARFaceAnchor)が、本稿では geometry: ARFaceGeometryを使用する。 ARFaceGeometryは顔の形状を三角形のメッシュで近似的に検出した際の各頂点やテクスチャの座標系を提供してくれる。(ARFaceGeometry) ARFaceGeometryを使用すれば顔の形状を表示することができる、簡単なところだとARSCNFaceGeometryを使用してARSCNViewに描画することができる。

    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        guard let device = renderer.device else { return nil }
        let node = SCNNode(geometry: ARSCNFaceGeometry(device: device))
        return node
    }

以下ではこのデータをobjファイルに変換する方法を考える。

ARKit の Face Geometryデータをobjファイルに変換する

Face Geometryをobjファイルに変換するにあたって、Face GeometryをMDLAssetに変換する。(MDLAsset) MDLAssetの説明を読むと、export(to:)メソッドを使用することで、MDLAssetの内容をファイルに出力することができる。(export(to:)) 以下、Face GeometryをMDLAssetに変換する方法を記載する。

Face GeometryをMDLAssetに変換するにあたって、大まかに下記の手順を取る。

  • MDLMeshBufferDataAllocatorの作成
  • MDLSubmeshの作成
  • MDLMeshの作成
  • MDLAssetの作成
  • MDLMeshをMDLAssetに含める
  • MDLAssetからobjファイルを出力する

MDLMeshBufferDataAllocatorの作成

MDLAssetの初期化にはよくURLを指定してリソースを読み込む方法が使われるが、今回はbufferAllocatorを使用する。(init(bufferAllocator:)) この方法はドキュメントにもある通り、プログラムでMDLAssetを作成するのに使用される。 MDLMeshBufferAllocatorの生成部分の実装は下記のようになる。

let allocator = MDLMeshBufferDataAllocator()

以下ではindexやvertex、coordinateの情報をallocatorを使用しbufferに変換して使用していく。

MDLSubmeshの作成

次にMDLSubmeshを作成する。MDLSubmeshの作成のためにはindexBufferが必要になる。今回の顔のMeshに関してはARFaceGeometryがtriangleIndicesを持っているためそれを使う。 初期化の実際の実装は下記のようになる。

let triangleIndicesBuffer = allocator.newBuffer(
    with: Data(bytes: triangleIndices, count: triangleIndices.count * MemoryLayout<Int16>.stride),
    type: .index
)

let subMesh = MDLSubmesh(
    indexBuffer: triangleIndicesBuffer,
    indexCount: triangleIndices.count,
    indexType: .uInt16,
    geometryType: .triangles,
    material: nil
)

MDLMeshの作成

MDLMeshを作成する。Mesh作成にあたって必要なindicesは作成しsubmeshに渡したので、ここでは各頂点と各頂点に対応したcoordinateを渡す。 各頂点のverticesBufferとcoordinateBufferの実装は下記のようになる。Data生成時に使用するデータ容量を計算する際にMemoryLayout.strideとし各データ型のメモリ容量を考慮することに注意する。 coordinateは直感的には2 vector必要じゃないかという気もするが、法線が決まれば平面の座標系も決まるはずなので、多分法線ベクトルを意味していると思われる(関連ドキュメントはまだしっかりと読めていない...) vertexはSIMD3なのでとても素直に理解しやすいと思う。

let verticesBuffer = allocator.newBuffer(
    with: Data(bytes: vertices, count: vertices.count * MemoryLayout<SIMD3<Float>>.stride),
    type: .vertex
)

let coordinatesBuffer = allocator.newBuffer(
    with: Data(bytes: textureCoordinates, count: textureCoordinates.count * MemoryLayout<SIMD2<Float>>.stride),
    type: .vertex
)

MDLMeshを作成するには、descriptorを指定する必要がある。(init(vertexBuffers:vertexCount:descriptor:submeshes:)) MDLVertexDesciptorは下記のように作成する。今回はデータとしてvertexとcooridinateを使用しているため、それらの設定を行なっている。 なぜこの設定が必要かについては以下のStackOverflowの回答が詳しい。(Save ARFaceGeometry to OBJ file)

let vertexDescriptor = MDLVertexDescriptor()
vertexDescriptor.attributes[0] = MDLVertexAttribute(
    name: MDLVertexAttributePosition,
    format: .float3,
    offset: 0,
    bufferIndex: 0
)
vertexDescriptor.attributes[1] = MDLVertexAttribute(
    name: MDLVertexAttributeTextureCoordinate,
    format: .float2,
    offset: 0,
    bufferIndex: 1
)
vertexDescriptor.layouts[0] = MDLVertexBufferLayout(
    stride: MemoryLayout<SIMD3<Float>>.stride
)
vertexDescriptor.layouts[1] = MDLVertexBufferLayout(
    stride: MemoryLayout<SIMD2<Float>>.stride
)

上記を使用してMDLMeshを生成する。

let mdlMesh = MDLMesh(
    vertexBuffers: [verticesBuffer, textureCoordinatesBuffer],
    vertexCount: vertices.count,
    descriptor: vertexDescriptor(),
    submeshes: [subMesh]
)
mdlMesh.addNormals(withAttributeNamed: MDLVertexAttributeNormal, creaseThreshold: 0.5)

MDLMeshのaddNormals()を呼び出すことで、実際のデータの生成を行う。

MDLAssetの作成, MDLMeshをMDLAssetに含める, MDLAssetからobjファイルを出力する

MDLAssetを作成し、MDLMeshをMDLAssetに含める。

let asset = MDLAsset(bufferAllocator: allocator)
asset.add(mdlMesh)

最後に作成したMDLAssetをexportする。この際に指定する引数のURLはMDLAssetのドキュメントに記載されている通りファイルURLでなければならないことに注意する。

let saveFile = FileManager.default.createDocFile(ext: "obj")
try asset.export(to: saveFile)

参考