ARKitで顔のGeometryを表示する

はじめに

いまさらですが最近ARKitに熱が入っているのでFaceGeometryの表示を試してみました。

下の画像のような感じのものです

解説

ARKitでFaceTrackingを行う場合はARFaceTrackingConfigurationを追加います。

let configuration = ARFaceTrackingConfiguration()
sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])

Anchorの追加/更新

アンカーの取得は2種類のメソッドで行います。

renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor)はアンカーの追加、更新、削除などが行われた場合に呼び出されるメソッドです。顔にGeometryを貼り付ける場合は一度追加するだけなので一度しか呼ばれません。

renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor)はARKitのフレーム更新のたびに呼ばれるメソッドです、この中でfaceGeometryに対して更新をかけることで表情を変えた時に追従させます。

extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let faceAnchor = anchor as? ARFaceAnchor else { return }
        
        faceGeometry.update(from: faceAnchor.geometry)
        node.addChildNode(faceNode)
    }

    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let faceAnchor = anchor as? ARFaceAnchor else { return }
        faceGeometry.update(from: faceAnchor.geometry)
    }
}

顔にくっつけるGeometry

GeometryとNodeをプロパティとして定義して以下のメソッドのようにマテリアルを定義しておきます。

ここのマテリアルに画像を入れたりしたらいろいろ遊べそうな気がするのでやってみてもいいかもしれません。

    var faceGeometry: ARSCNFaceGeometry!
    var faceNode: SCNNode = SCNNode()
    
    func updateFaceGeometry() {
        guard let device = sceneView.device else { return }
        faceGeometry = ARSCNFaceGeometry(device: device)
        if let material = faceGeometry.firstMaterial {
            material.diffuse.contents = UIColor.white
            material.lightingModel = .physicallyBased
        }
        faceNode.geometry = faceGeometry
    }

コード

import ARKit
import SceneKit
import UIKit

class ViewController: UIViewController, ARSessionDelegate {
    @IBOutlet var sceneView: ARSCNView! {
        didSet {
            sceneView.delegate = self
            sceneView.automaticallyUpdatesLighting = true
            sceneView.scene = SCNScene()
            let configuration = ARFaceTrackingConfiguration()
            configuration.isLightEstimationEnabled = true
            sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
        }
    }
    
    var faceGeometry: ARSCNFaceGeometry!
    var faceNode: SCNNode = SCNNode()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //フェイストラッキングのサポート確認
        guard ARFaceTrackingConfiguration.isSupported else { fatalError("Not supported") }
        updateFaceGeometry()
    }
    
    func updateFaceGeometry() {
        guard let device = sceneView.device else { return }
        faceGeometry = ARSCNFaceGeometry(device: device)
        if let material = faceGeometry.firstMaterial {
            material.diffuse.contents = UIColor.white
            material.lightingModel = .physicallyBased
        }
        faceNode.geometry = faceGeometry
    }
}

extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let faceAnchor = anchor as? ARFaceAnchor else { return }
        
        faceGeometry.update(from: faceAnchor.geometry)
        node.addChildNode(faceNode)
    }

    func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
        guard let faceAnchor = anchor as? ARFaceAnchor else { return }
        faceGeometry.update(from: faceAnchor.geometry)
    }
}

動いてるところ

目と口を塞いだ全面でのメッシュ

目と口のメッシュで覆いたい場合はARSCNFaceGeometryに以下のようなconvenience initializerが用意されているのでtrueで渡すとこうなります。

convenience init?(device: MTLDevice, fillMesh: Bool)

ワイヤーメッシュ

ARSCNViewのdebugOptionsというプロパティにrenderAsWireframeを渡すとワイヤーメッシュで表示されます

sceneView.debugOptions = [.renderAsWireframe]