iOS 创建基于人脸的AR应用

本文翻译自Creating Face-Based AR Experiences
结合iPhone X的TrueDepth摄像头,进行人脸追踪和表情移植。

概述

这个Demo 实现了基于iPhone X的TrueDepth摄像头的四种AR方式

  • 纯相机,没有任何AR内容。
  • ARKit 传来的面部网格,可以自动估计真实的定向照明环境。
  • 虚拟3D
  • 一个简单的跟随用户表情的机器人

image

在SceneKit View 中创建一个 Face Tracking Session

和其他ARKit用法一样,人脸跟踪需要配置开启一个ARSession类型的会话,并将虚拟内容和相机图像一起渲染到view中。关于session和view创建的更多信息可以参考About Augmented Reality and ARKitBuilding Your First AR Experience 两篇文章。这个例子使用SceneKit显示AR信息,但你也可以使用SpriteKit或者用Metal创建一个渲染器(参考ARSKViewDisplaying an AR Experience with Metal)。

面部跟踪和其他ARKit 中配置session的方法不同。创建一个ARFaceTrackingConfiguration对象,设置一些属性,并传给绑定view的AR session的run(_:options:)方法。具体如下所示。


guard ARFaceTrackingConfiguration.isSupported else { return }
let configuration = ARFaceTrackingConfiguration()
configuration.isLightEstimationEnabled = true
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])

在请求面部跟踪 session之前,需要用ARFaceTrackingConfiguration.isSupported方法判断当前设备是否支持该特性。

跟踪表情的位置和方向

当面部跟踪激活时,ARKit自动添加ARFaceAnchor对象到AR session中,其中包含用户面部的位置和方向信息。

ARKit 只能检测一个面部信息。如果图像中出现多个面部信息,ARKit选择最大的或者最清晰可辨的面孔。

如果使用SceneKit实现的AR效果,你可以在renderer(_:didAdd:for:)方法中添加与anchor相对应的3D内容(来自ARSCNViewDelegate。ARKit 为每个anchor增加一个SceneKit节点,并在每一帧更新这个节点的位置和方向,因此你添加到该节点的任何SceneKit内容都会自动跟随用户脸部的位置和方向。


func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Hold onto the `faceNode` so that the session does not need to be restarted when switching masks.
faceNode = node
serialQueue.async {
self.setupFaceNodeContent()
}
}

在这个Demo中,renderer(_:didAdd:for:)方法调用setupFaceNodeContent方法向faceNode中添加SceneKit 内容。比如,如果你改变 showsCoordinateOrigin 变量值,Demo将各个点的 x/y/z 轴可视化,指示面部锚点坐标系的起点。

使用面几何来建模用户脸部

ARKit 提供一个和用户脸部 的大小,形状,拓扑和当前面部表情相匹配的粗略3D网格几何。ARKit 还提供ARSCNFaceGeometry类方便在SceneKit中创建可视化网格。

你可以使用这个网格向脸部绘制一些内容,比如绘制半透明纹理来模拟纹身或者妆效。


// This relies on the earlier check of `ARFaceTrackingConfiguration.isSupported`.
let device = sceneView.device!
let maskGeometry = ARSCNFaceGeometry(device: device)!

Demo 里的 setupFaceNodeContent 方法在场景中添加了一个包含面几何的节点。通过使该节点成为由脸部锚点提供的节点的子节点,脸部模型自动跟踪用户脸部的位置和方向。

为了能在用户眨眼,说话或者做其他面部动作时也让屏幕脸部和真实用户保持一致,你还需要在renderer(_:didUpdate:for:)回调中更新面部网格。

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

virtualFaceNode?.update(withFaceAnchor: faceAnchor)
}

然后更新ARSCNFaceGeometry 对象。

func update(withFaceAnchor anchor: ARFaceAnchor) {
let faceGeometry = geometry as! ARSCNFaceGeometry
faceGeometry.update(from: anchor.geometry)
}

在脸上渲染3D内容

ARKit 提供的另一种面网格的用法是在场景中创建封闭几何。一个封闭集合是一个不渲染任何可见物体的3D模型,但会阻碍相机对场景中其他虚拟内容的视图。

这种技术创造出真实面对与虚拟对象交互的错觉,即使脸部是2D摄像机图像,虚拟内容是渲染的3D对象。 例如,如果您将遮挡几何图形和虚拟眼镜放置在用户的脸部上,则脸部可能会遮挡眼镜框架。

要创建面部的遮挡几何,请先如上例所示创建一个ARSCNFaceGeometry对象。 但是,不要使用可见的外观来配置该对象的SceneKit材质,而是在渲染期间将材质设置为渲染深度而不是颜色:

geometry.firstMaterial!.colorBufferWriteMask = []
occlusionNode = SCNNode(geometry: geometry)
occlusionNode.renderingOrder = -1

因为材料呈现深度,所以SceneKit渲染的其他对象正确地出现在它的前面或后面。 但是由于材质不会呈现颜色,相机图像会出现在其位置。 Demo将此技术与位于用户眼睛前方的SceneKit对象相结合,创建一个物体被用户的鼻子遮蔽的效果。

用Blend Shapes动画化一个人物

除了上述示例中所示的面部网格之外,ARKit还提供了一个更为抽象的用户面部表情模型,其形式为blendShapes字典。 您可以使用该字典中的命名系数值来控制自己的2D或3D资产的动画参数,创建遵循用户真实面部动作和表达的角色(如头像或木偶)。

作为混合形状动画的基本演示,此示例包含使用SceneKit原始形状创建的机器人角色头部的简单模型。 (请参阅源代码中的robotHead.scn文件。)

renderer:didUpdateNode:forAnchor:回调中读取face Anchor的blendShapes字典内容以获取用户当前真实表情。

func update(withFaceAnchor faceAnchor: ARFaceAnchor) {
blendShapes = faceAnchor.blendShapes
}

然后,检查该字典中的键值对以计算模型的动画参数。 有52个独特的ARBlendShapeLocation系数。 您的应用程序可以使用尽可能少的或很多的必要条件来创建您想要的艺术效果。 在此示例中,RobotHead类执行此计算,将ARBlendShapeLocationEyeBlinkLeftARBlendShapeLocationEyeBlinkRight参数映射到机器人眼睛的scale的一个轴以及ARBlendShapeLocationJawOpen参数,以抵消机器人下颚的位置。

var blendShapes: [ARFaceAnchor.BlendShapeLocation: Any] = [:] {
didSet {
guard let eyeBlinkLeft = blendShapes[.eyeBlinkLeft] as? Float,
let eyeBlinkRight = blendShapes[.eyeBlinkRight] as? Float,
let jawOpen = blendShapes[.jawOpen] as? Float
else { return }
eyeLeftNode.scale.z = 1 - eyeBlinkLeft
eyeRightNode.scale.z = 1 - eyeBlinkRight
jawNode.position.y = originalJawY - jawHeight * jawOpen
}
}