在本教程中,您将学习如何:
- 使用AR脸部追踪功能通过TrueDepth相机追踪您的脸部。
- 将表情符号覆盖在跟踪的脸上。
- 根据您制作的面部表情操作表情符号。
需要的前置准备
首先要安装好Xcode,然后还需要一台带有前置TrueDepth摄像头的iPhone。也就意味着最少要使用iPhone X以上的设备,才能完成这个demo的功能
开始创建项目
启动Xcode,然后使用快捷键command + shift + n
这里我们选择 Augmented Reality App (增强现实应用),然后点击next
输入项目名字,然后选择 Swift 语言,Content Technology 选择 SceneKit,然后点击next,此时将在您选择的置顶位置创建好项目
项目中的文件的用处
创建好项目之后,默认会有一个art.scnassets的文件夹,在这个文件夹下面有两个文件,正如图片中的,一个是ship.scn, 一个是texture.png,scn文件是3D模型的一种,如果没有texture.png这个图片,那么将显示这样的效果
texture.png被附着到ship.scn这个3d模型上之后,就成了下面的这个样子,变的更好看了,蒙了一层皮,我们称之为纹理,实际上不仅仅能把图片当成ship.scn的”皮“,UIColor,CALayer,甚至是AVPlayer,都可以,暂时不做过多探究
说了这么多,感觉有点枯燥,还是来点代码吧
代码展示
现在到了最重要的环节,代码展示环节
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
@IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// Create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
// Set the scene to the view
sceneView.scene = scene
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
// MARK: - ARSCNViewDelegate
/*
// Override to create and configure nodes for anchors added to the view's session.
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
let node = SCNNode()
return node
}
*/
func session(_ session: ARSession, didFailWithError error: Error) {
// Present an error message to the user
}
func sessionWasInterrupted(_ session: ARSession) {
// Inform the user that the session has been interrupted, for example, by presenting an overlay
}
func sessionInterruptionEnded(_ session: ARSession) {
// Reset tracking and/or remove existing anchors if consistent tracking is required
}
}
仅仅以上几十行代码,我们将能体验到一个非常牛叉的AR飞船的效果,虽然不知道是什么机型,但是不明觉厉,
是的,就这样,第一个AR应用就完成了,但是仍然不知道是怎么回事啊,那么来分析分析是怎么实现的呢
首先,需要一个来显示AR渲染效果的视图ARSCNView,
class ARSCNView : SCNView
SCNView是用来干嘛的呢。用于显示3D SceneKit内容的视图
在macOS中,SCNView是NSView的子类。 在iOS和tvOS中,SCNView是UIView的子类。 作为这两种操作系统的视图层次结构的一部分,SCNView对象在应用程序的用户界面中为SceneKit内容提供了一个位置。 您可以使用它的init(frame:options :)方法或将其添加到nib文件或情节提要中来创建一个SceneKit视图。 要为SceneKit视图提供内容,请将SCNScene对象分配给它的scene属性。
有关使用SceneKit视图的其他重要方法和属性,请参见SCNSceneRenderer协议。 (您还可以使用SCNRenderer类将SceneKit内容渲染到任意Metal命令队列或OpenGL上下文中,或者使用SCNLayer类渲染到macOS上的Core Animation层中。SCNSceneRenderer协议定义了这三个SceneKit渲染类共有的功能。)
sceneView.delegate = self
设置ViewController为sceneView.的代理,那么就可以在ARSCNViewDelegate的代理回调中处理业务上的需求,
sceneView.showsStatistics = true
设置是否显示分析数据,方便分析和调试
// Create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
这个的作用就是加载我们项目中的3d模型资源,从而将3d模型在sceneView中展示出来
SCNScene 是节点层次结构和全局属性的容器,它们共同构成了可显示的3D场景。
// Set the scene to the view
sceneView.scene = scene
将我们刚刚创建好的scene 赋值给sceneView的scene
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Run the view's session
sceneView.session.run(configuration)
}
在视图将要展示的时候,创建一个ARWorldTrackingConfiguration对象
ARWorldTrackingConfiguration的作用是
一种配置,用于监视iOS设备的位置和方向,同时使您能够扩展用户面前的环境。然后 sceneView.session.run(configuration),这样就能将飞船模型展示在相机拍到的现实世界中了。
人脸检测能力
通过上面的简单分析,我们并不需要做很多的操作,写很多的代码,就能实现一个简单的AR应用,暂且将上面的分析作为对AR的一个认识,下面将实现检测到人脸,并且在人脸上贴上一些有趣的emoji表情
我们要改变的代码并不是很多
guard ARFaceTrackingConfiguration.isSupported else {
fatalError("Face tracking is not supported on this device")
}
首先在viewDidLoad方法中加入这段代码,这段代码是来检测您当前的设备是否支持人脸识别,不支持的设备将无法完成本教程的余下功能。
您已经看过ARFaceTrackingConfiguration,它用于配置设备以使用TrueDepth摄像头跟踪您的脸部。酷啊。
但是您还需要了解有关面部跟踪的哪些信息?
您即将使用的三个非常重要的类是ARFaceAnchor,ARFaceGeometry和ARSCNFaceGeometry。
ARFaceAnchor继承自ARAnchor。如果您以前使用ARKit做过任何事情,您就会知道ARAnchors是使它如此强大和简单的原因。它们是ARKit跟踪的现实世界中的位置,移动手机时这些位置不会移动。 ARFaceAnchors还包括有关面部的信息,例如拓扑和表情。
ARFaceGeometry听起来很像。这是一张包含顶点和TextureCoordinates的3D描述。
ARSCNFaceGeometry使用来自ARFaceGeometry的数据来创建SCNGeometry,该SCNGeometry可用于创建SceneKit节点-基本上就是您在屏幕上看到的内容。
好,够了是时候使用这些类了。回到编码!
添加 Mesh Mask
从表面上看,您似乎只打开了前置摄像头。 但是,您看不到您的iPhone已经在跟踪您的脸。 令人毛骨悚然的iPhone。
看到iPhone跟踪的内容不是很好吗? 真是巧合,因为这正是您接下来要做的!
在ViewController类定义的大括号后添加以下代码:
// 1
extension ViewController: ARSCNViewDelegate {
// 2
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
// 3
guard let device = sceneView.device else {
return nil
}
// 4
let faceGeometry = ARSCNFaceGeometry(device: device)
// 5
let node = SCNNode(geometry: faceGeometry)
// 6
node.geometry?.firstMaterial?.fillMode = .lines
// 7
return node
}
}
上面的代码做了这几件事
- 声明ViewController实现ARSCNViewDelegate协议。
- 从协议定义renderer(_:nodeFor :)方法。
- 确保用于渲染的金属设备不为零。
- 创建要由“金属”设备渲染的面几何。
- 根据面部几何形状创建一个SceneKit节点。
- 将节点材质的填充模式设置为仅线。
- 返回节点。
在运行代码之前,确保在viewDidLoad中设置了代理
sceneView.delegate = self
此时运行App你将能够在自己的手机里自己的脸上有层遮罩网格。
更新 Mesh Mask
您是否注意到网格遮罩有点……是静态的? 当然,当您左右移动头部时,它会跟踪您的面部位置并随之移动,但是眨眼或张开嘴会发生什么呢? 没有。
真令人失望。
幸运的是,这很容易解决。 您只需要添加另一个ARSCNViewDelegate方法!
在您的ARSCNViewDelegate扩展的末尾,添加以下方法:
// 1
func renderer(
_ renderer: SCNSceneRenderer,
didUpdate node: SCNNode,
for anchor: ARAnchor) {
// 2
guard let faceAnchor = anchor as? ARFaceAnchor,
let faceGeometry = node.geometry as? ARSCNFaceGeometry else {
return
}
// 3
faceGeometry.update(from: faceAnchor.geometry)
}
上面的方法做了这几件事情
- 定义renderer(_:didUpdate:for :)协议方法的didUpdate版本。
- 确保要更新的锚是ARFaceAnchor,并且节点的几何是ARSCNFaceGeometry。
- 使用ARFaceAnchor的ARFaceGeometry更新ARSCNFaceGeometry
现在,在构建和运行时,您应该看到网格蒙版表格并进行更改以匹配您的面部表情。
再次运行程序,你会发现网格遮罩跟着脸部在运动了,很神奇。
添加Emoji表情
下面我们定义了一些有趣的表情
let noseOptions = ["👃", "🐽", "💧", " "]
let eyeOptions = ["👁", "🌕", "🌟", "🔥", "⚽️", "🔎", " "]
let mouthOptions = ["👄", "👅", "❤️", " "]
let hatOptions = ["🎓", "🎩", "🧢", "⛑", "👒", " "]
let features = ["nose", "leftEye", "rightEye", "mouth", "hat"]
let featureIndices = [[9], [1064], [42], [24, 25], [20]]
以上的表情分别是
- 鼻子上的
- 眼睛上的
- 嘴上的
- 头上的
下面我们定义一个类,用来将String转成UIImage
extension String {
func image() -> UIImage? {
let size = CGSize(width: 20, height: 22)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
UIColor.clear.set()
let rect = CGRect(origin: .zero, size: size)
UIRectFill(CGRect(origin: .zero, size: size))
(self as AnyObject).draw(in: rect, withAttributes: [.font: UIFont.systemFont(ofSize: 15)])
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
}
然后定义一个EmojiNode 类,这个类是SCNNode的子类,
import SceneKit
class EmojiNode: SCNNode {
var options: [String]
var index = 0
init(with options: [String], width: CGFloat = 0.06, height: CGFloat = 0.06) {
self.options = options
super.init()
let plane = SCNPlane(width: width, height: height)
plane.firstMaterial?.diffuse.contents = (options.first ?? " ").image()
plane.firstMaterial?.isDoubleSided = true
geometry = plane
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Custom functions
extension EmojiNode {
func updatePosition(for vectors: [vector_float3]) {
let newPos = vectors.reduce(vector_float3(), +) / Float(vectors.count)
position = SCNVector3(newPos)
}
func next() {
index = (index + 1) % options.count
if let plane = geometry as? SCNPlane {
plane.firstMaterial?.diffuse.contents = options[index].image()
plane.firstMaterial?.isDoubleSided = true
}
}
}
这个类的作用是根据指定的字符串,创建一个图片节点,并且能够更新位置
在ViewController中添加如下方法
func updateFeatures(for node: SCNNode, using anchor: ARFaceAnchor) {
for (feature, indices) in zip(features, featureIndices) {
let child = node.childNode(withName: feature, recursively: false) as? EmojiNode
let vertices = indices.map { anchor.geometry.vertices[$0] }
child?.updatePosition(for: vertices)
switch feature {
case "leftEye":
let scaleX = child?.scale.x ?? 1.0
let eyeBlinkValue = anchor.blendShapes[.eyeBlinkLeft]?.floatValue ?? 0.0
child?.scale = SCNVector3(scaleX, 1.0 - eyeBlinkValue, 1.0)
case "rightEye":
let scaleX = child?.scale.x ?? 1.0
let eyeBlinkValue = anchor.blendShapes[.eyeBlinkRight]?.floatValue ?? 0.0
child?.scale = SCNVector3(scaleX, 1.0 - eyeBlinkValue, 1.0)
case "mouth":
let jawOpenValue = anchor.blendShapes[.jawOpen]?.floatValue ?? 0.2
child?.scale = SCNVector3(1.0, 0.8 + jawOpenValue, 1.0)
default:
break
}
}
}
此方法的作用是更新面部表情
在代理方法中添加嘴,眼睛,鼻子,帽子节点,并在锚点的更新方法中更新Emoji表情节点
extension ViewController: ARSCNViewDelegate {
func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
guard let faceAnchor = anchor as? ARFaceAnchor,
let device = sceneView.device else { return nil }
let faceGeometry = ARSCNFaceGeometry(device: device)
let node = SCNNode(geometry: faceGeometry)
node.geometry?.firstMaterial?.fillMode = .lines
node.geometry?.firstMaterial?.transparency = 0.0
let noseNode = EmojiNode(with: noseOptions)
noseNode.name = "nose"
node.addChildNode(noseNode)
let leftEyeNode = EmojiNode(with: eyeOptions)
leftEyeNode.name = "leftEye"
leftEyeNode.rotation = SCNVector4(0, 1, 0, GLKMathDegreesToRadians(180.0))
node.addChildNode(leftEyeNode)
let rightEyeNode = EmojiNode(with: eyeOptions)
rightEyeNode.name = "rightEye"
node.addChildNode(rightEyeNode)
let mouthNode = EmojiNode(with: mouthOptions)
mouthNode.name = "mouth"
node.addChildNode(mouthNode)
let hatNode = EmojiNode(with: hatOptions)
hatNode.name = "hat"
node.addChildNode(hatNode)
updateFeatures(for: node, using: faceAnchor)
return node
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let faceAnchor = anchor as? ARFaceAnchor, let faceGeometry = node.geometry as? ARSCNFaceGeometry else { return }
faceGeometry.update(from: faceAnchor.geometry)
updateFeatures(for: node, using: faceAnchor)
}
}
看看最终的效果
到目前位置,所有主要的代码都在上面部分,最后运行项目,看看最终的效果
先讲到这里了,感兴趣的同学们,可以下载代码来调试一下,本教程参考资料来自于 《ARKit by Tutorials》这本书