本文へスキップ
Hironobu Iga

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

ARFaceTrackingConfiguration で取得した ARFaceGeometry を MDLAsset に変換し、export(to:) で obj 形式に書き出す手順です。

公開日

この記事は別サイトにも掲載しています。 https://iganin.hatenablog.com/entry/2021/12/18/214028

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 ファイルとして出力することを目指す。

Xcode で obj ファイルを開き、顔のメッシュが表示されている様子

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:)) MDLVertexDescriptor は下記のように作成する。今回はデータとして vertex と coordinate を使用しているため、それらの設定を行なっている。 なぜこの設定が必要かについては以下の Stack Overflow の回答が詳しい。(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)

参考