ARKit
ARKit框架通过集成iOS设备摄像头和运动功能,在您的应用程序或游戏中产生增强现实体验。
概述
增强现实(AR)描述了将2D或3D元素添加到设备相机中的实时视图的用户体验,使得这些元素看起来就存在于现实世界中。ARKit框架结合了设备运动追踪、相机场景捕捉、高级场景处理和便利显示,简化了AR体验的任务。
重要
ARkit需要A9或更高版本处理器的iOS设备。
要使应用程序仅在支持ARKit的设备上可用,请在应用程序的info.plist这种添加UIRequiredDeviceCapabilities-arkit键值对。若增强现实知识应用程序中的一个辅助功能,请在项目中使用isSupported属性来确定当前设备是否支持你要使用的会话配置。
<br />
关于增强现实与ARKit
发现构建强大的AR体验的支持概念、功能和最佳实践方式。
概述
任何AR体验的基本要求以及ARKit的定义特征是创建和追踪用户所在的现实世界空间与可视化内容建模的虚拟空间之间的对应关系的能力。当您的应用程序显示实时摄像头图像的内容时,用户体验到增强现实:虚拟内容是真实世界的一部分的错觉。
在所有ARKit体验中,ARKit使用世界和相机坐标系,并遵循右手定则:y轴正向向上,z轴正向指向观察者,x轴正向指向观察者的右侧。
会话配置可以改变相对于现实世界的坐标系的起点和方向(参见worldAlignment)。AR会话中的每个锚定义了自己的本地坐标系,也遵循右手定则,例如:ARFaceAnchor类定义了一个用于定位面部特征的系统。
世界追踪的工作原理
ARKit使用视觉惯性测距的技术来创建真实空间和虚拟空间之间的一个对应关系。该过程将来自iOS设备的运动传感器硬件的信息与设备相机可见的场景的计算机视觉分析相结合。ARKit识别场景图像中的显著特征,追踪视频帧中这些特征位置的差异,并将该信息与运动传感器数据进行比较,之后生成设备的位置和运动的高精度模型。
世界追踪还分析和了解场景的内容。使用碰撞测试方法(参见TestResult类)来查找与摄像机图像中的点相对应的真实世界凸面。如果在会话中启用planeDetection设置,ARKit会检测摄像机图像中的曲面,并报告其位置和大小。可以使用碰撞测试结果或检测到的平面,放置在场景中或与虚拟内容交互。
最佳实践与限制
世界追踪是一个不精确的科学。这个过程经常会产生令人印象深刻的准确性,从而导致现实的AR体验。然而,它依赖于设备的物理环境的细节,这些细节并不总是一致的,或者难以实时测量(带有一定程度的错误)。要建立高品质的AR体验,注意一下注意事项和建议。
设计可预见的照明条件的AR体验。世界追踪涉及图像分析,这需要清晰的图像。当相机无法看到细节,例如当相机指向空白墙或场景太暗时,追踪质量会降低。
使用追踪质量信息提供用户反馈。世界追踪将图像分析与设备运动相关联。如果设备正在移动,即时只是微妙移动,ARKit可以更好地了解场景。过多的动作--太远、太快或剧烈晃动--会导致图像模糊或追踪视频帧之间的特征距离过远,从而降低追踪质量。ARCamera类提供追踪状态原因信息,你可以使用它来开发UI,告诉用户如何解决低质量的追踪情况。
给予平面检测一定的时间以产生清晰的结果,并在获得所需结果时禁用平面检测。平面检测结果随时间而变化--当首次检测到平面时,其位置和范围可能不准确。随着时间的推移,该平面仍在场景中,ARKit对其位置和范围的估计作了细化。当一个较大的平面在场景中时,ARKit可能会在已经放置好平面后继续改变平面的位置、范围和转换。
<br />
创建第一个AR体验
创建运行AR会话的应用程序,并使用平面检测通过SceneKit框架放置3D内容。
概述
此示例app运行一个ARKit世界追踪会话,并显示在SceneKit视图内容。为演示平面检测,应用程序仅放置一个SCNPlane对象来可视化每个检测到的ARPlaneAnchor对象。
配置并运行AR会话
ARSCNView类是一个SceneKit视图,它包含一个ARSession对象,用于管理和创建增强现实体验所需的运动追踪和图像处理。但是,要运行会话,必须提供会话配置。
ARWorldTrackingConfiguration类提供高精度的运动追踪功能,可以帮助你将虚拟内容放置在真实世界的表面。要启动AR会话,使用所需的选项(如平面检测)创建会话配置对象,然后在ARSCNView实例的会话对象上调用run(_:options:)方法:
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
sceneView.session.run(configuration)
只有当显示它的视图在屏幕上时,才能运行会话。
重要提示:如果您的应用程序需要ARKit的核心功能,请使用应用程序的Info.plist文件部分中的arkit键使您的应用程序仅在支持ARKit的设备上可用。 如果AR是应用程序的辅助功能,请使用isSupported属性来确定是否提供基于AR的功能。
在检测到的平面上放置3D内容
设置AR会话后,可使用SceneKit在视图中防止虚拟内容。
当启用平面检测时,ARKit会为每个检测到的平面添加和更新锚点。默认情况下,ARSCNView类为每个锚点的SceneKit场景添加一个SCNNode对象。视图委托可以实现renderer(_:didAdd:for:)方法来向场景添加内容。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// Place content only for anchors found by plane detection.
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
// Create a *SceneKit* plane to visualize the plane anchor using its position and extent.
let plane = SCNPlane(width: CGFloat(planeAnchor.extent.x), height: CGFloat(planeAnchor.extent.z))
let planeNode = SCNNode(geometry: plane)
planeNode.simdPosition = float3(planeAnchor.center.x, 0, planeAnchor.center.z)
/*
`SCNPlane` is vertically oriented in its local coordinate space, so
rotate the plane to match the horizontal orientation of `ARPlaneAnchor`.
*/
planeNode.eulerAngles.x = -.pi / 2
// Make the plane visualization semitransparent to clearly show real-world placement.
planeNode.opacity = 0.25
/*
Add the plane visualization to the ARKit-managed node so that it tracks
changes in the plane anchor as plane estimation continues.
*/
node.addChildNode(planeNode)
}
如果将内容添加为与锚点对应的节点的子节点,则ARSCNView类将自动移动该内容,因为ARKit会优化其对平面位置和范围的估计。为了现实估计平面的完整范围,此示例应用程序还实现了renderer(_:didUpdate:for:)方法更新SCNPlane对象的大小以反映ARKit提供的预估值。
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
// Update content only for plane anchors and nodes matching the setup created in `renderer(_:didAdd:for:)`.
guard let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let plane = planeNode.geometry as? SCNPlane
else { return }
// Plane estimation may shift the center of a plane relative to its anchor's transform.
planeNode.simdPosition = float3(planeAnchor.center.x, 0, planeAnchor.center.z)
/*
Plane estimation may extend the size of the plane, or combine previously detected
planes into a larger one. In the latter case, `ARSCNView` automatically deletes the
corresponding node for one plane, then calls this method to update the size of
the remaining plane.
*/
plane.width = CGFloat(planeAnchor.extent.x)
plane.height = CGFloat(planeAnchor.extent.z)
}
<br />
处理增强现实中的3D交互和UI控件
遵循AR体验中的视觉反馈、手势交互和现实渲染的最佳实践。
概述
增强现实为用户提供了与你的应用程序中的现实和虚拟3D内容进行交互的新方式。然而,人机接口设计的许多基本原理仍然有效。令人信服的AR幻想需要尤其注意3D资源设计和渲染。iOS人机接口指南包括有关AR接口设计原则的建议。该项目展现了应用这些原则的方法,并轻松创建沉浸式、直观的AR体验。
此示例应用程序提供了简单的AR体验,允许用户将一个或多个逼真的虚拟对象放置在现实环境中,然后使用直观的手势来排列这些对象。该应用程序提供了用户界面提示,以帮助用户了解AR体验的状态及其交互选项。
以下部分对应于iOS Human Interface Guidelines > Augmented Reality的部分内容,并提供有关此示例应用程序如何实施这些原则的详细信息。
放置虚拟对象
帮助人们了解何时找到一个表面并放置一个对象。FocusSquare类在AR视图中绘制一个方形轮廓,为用户提供关于ARKit世界追踪状态的提示。
该方块通过改变大小和方向以反映预估的场景深度,并用突出的动画切换开启和关闭状态,以现实ARKit是否检测到适合放置对象的表面。在用户放置虚拟对象后,焦点方块消失,保持隐藏状态,直到用户将相机指向另一个表面。
用户放置对象时作适当响应。当用户选择要防止的虚拟对象时,示例应用程序的setPosition(_:relativeTo:smoothMovement) 方法使用FocusSquare对象的简单启发式方法将对象放置在屏幕中间大致逼真的位置,即使ARKit尚未在该位置检测到一个平面。
guard let cameraTransform = session.currentFrame?.camera.transform,
let focusSquarePosition = focusSquare.lastPosition else {
statusViewController.showMessage("CANNOT PLACE OBJECT\nTry moving left or right.")
return
}
virtualObjectInteraction.selectedObject = virtualObject
virtualObject.setPosition(focusSquarePosition, relativeTo: cameraTransform, smoothMovement: false)
updateQueue.async {
self.sceneView.scene.rootNode.addChildNode(virtualObject)
}
这个位置可能不是用户想要放置虚拟对象的真实表面的准确预估,但是它足够接近以快速获得对象。
随着时间的推移,ARKit会检测到平面并调整其位置的预估,调用renderer(_:didAdd:for:)和renderer(_:didUpdate:for:)代理方法来报告结果。在这些方法中,示例应用程序调用其adjustOntoPlaneAnchor(_:using:)方法来确定先前放置的虚拟对象是否接近检测到的平面。如果是这样,该方法使用微妙的动画将虚拟对象移动到该平面,从而使对象看起来处于用户选择的位置。
// Move onto the plane if it is near it (within 5 centimeters).
let verticalAllowance: Float = 0.05
let epsilon: Float = 0.001 // Do not update if the difference is less than 1 mm.
let distanceToPlane = abs(planePosition.y)
if distanceToPlane > epsilon && distanceToPlane < verticalAllowance {
SCNTransaction.begin()
SCNTransaction.animationDuration = CFTimeInterval(distanceToPlane * 500) // Move 2 mm per second.
SCNTransaction.animationTimingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
position.y = anchor.transform.columns.3.y
SCNTransaction.commit()
}
用户与虚拟对象的交互
允许人们使用标准熟悉的手势直接与虚拟对象进行交互。示例应用程序使用单指点击、单指和双指拖拽以及双指旋转手势识别器来让用户定位和定向虚拟对象。示例代码的VirtualObjectInteraction类管理这些手势。
一般来说,保持交互的简单性。当拖拽虚拟对象时,示例应用程序将对象的移动限制为放置在其上的二维平面,类似地,由于虚拟对象依赖于水平平面,旋转手势仅绕其垂直轴旋转对象,以使对象保留在平面上。
在交互式虚拟物体的合理接近范围内回应手势。示例代码的objectInteracting(with:in:)方法实用手势识别器提供的接触位置来执行碰撞测试。该方式通过虚拟对象的边界框进行碰撞测试,使得用户接触更可能影响物体,即使接触位置不在对象具有可见内容的点上。该方法通过多点触控手势执行多次碰撞测试,使得用户接触更可能应用预估对象:
for index in 0..<gesture.numberOfTouches {
let touchLocation = gesture.location(ofTouch: index, in: view)
// Look for an object directly under the `touchLocation`.
if let object = sceneView.virtualObject(at: touchLocation) {
return object
}
}
// As a last resort look for an object under the center of the touches.
return sceneView.virtualObject(at: gesture.center(in: view))
考虑用户启动的对象缩放是否必要。这个放置逼真的虚拟物体的AR体验可能会自然地出现在用户环境中,因此对象的内在大小有助于实现现实。因此,示例应用程序不会添加手势或其他UI来启用对象缩放。另外,通过不启用缩放手势,可防止用户对于手势是调整对象大小还是改变对象距相机的距离而困惑。(如果选择在应用程序中启用对象缩放,请使用捏合手势识别器。)
警惕潜在的手势冲突。示例代码的ThresholdPanGesture类是一个UIPanGestureRecognizer子类,它提供一种延迟手势识别器效果的方法,直到正在进行的手势通过指定的移动阀值。示例代码的touchesMoved(with:)方法使用此类让用户在拖拽对象之间平滑过渡并在单个双指手势中旋转对象:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
let translationMagnitude = translation(in: view).length
// Adjust the threshold based on the number of touches being used.
let threshold = ThresholdPanGesture.threshold(forTouchCount: touches.count)
if !isThresholdExceeded && translationMagnitude > threshold {
isThresholdExceeded = true
// Set the overall translation to zero as the gesture should now begin.
setTranslation(.zero, in: view)
}
}
确保虚拟对象的顺利移动。示例代码的setPosition(_:relativeTo:smoothMovement)方法在导致拖拽对象触摸手势位置和该对象的最近位置的历史记录之间插值。该方法通过根据距离摄像机的距离平均最近的位置,可以产生平滑的拖拽运动,而不会使拖拽的对象滞后于用户的手势:
if smoothMovement {
let hitTestResultDistance = simd_length(positionOffsetFromCamera)
// Add the latest position and keep up to 10 recent distances to smooth with.
recentVirtualObjectDistances.append(hitTestResultDistance)
recentVirtualObjectDistances = Array(recentVirtualObjectDistances.suffix(10))
let averageDistance = recentVirtualObjectDistances.average!
let averagedDistancePosition = simd_normalize(positionOffsetFromCamera) * averageDistance
simdPosition = cameraWorldPosition + averagedDistancePosition
} else {
simdPosition = cameraWorldPosition + positionOffsetFromCamera
}
探索更有吸引力的交互方式。在AR体验中,拖拽手势--即将手指移动到设备屏幕上--并不是将虚拟内容拖到新位置的唯一自然方式。用户还可以直观地尝试在移动设备的同时将手指保持在屏幕上,在AR场景中有效地拖拽触摸点。
示例应用程序通过在拖拽手势正在进行时持续调用updateObjectToCurrentTrackingPosition()方法来支持这种手势,即时手势的触摸位置没有改变。如果设备在拖拽期间移动,则该方法会计算与触摸位置相对应的新世界位置,并相应地移动虚拟对象。
进入增强现实
指示正在进行的初始化并提示用户。示例应用程序显示有关AR会话状态的文本提示以及使用浮动文本视图与AR体验进行交互的说明。示例代码的StatusViewController类管理此视图,显示允许用户读取它们之后淡出的临时指令或重要的状态信息,直到用户更正为止。
处理问题
当不符合用户期望时,允许重置体验。示例应用程序具有始终在UI右上角可见的重置按钮,无论当前状态如何允许用户重新启动AR体验。参阅示例代码的restartExperience()方法。
仅在兼容的设备上提供AR功能。示例应用程序需要ARKit的核心功能,需在项目的Info.plist文件中设置UIRequiredDeviceCapabilities - arkit键值对。部署内建项目时,此键可防止在不支持ARKit的设备上安装应用程序。
若app仅将AR作为次要功能,请使用isSupported方法来确定是否隐藏需要ARKit的功能。
<br />
创建基于面部的AR体验
使用iPhone X上的TrueDepth摄像头,放置和动画追踪用户脸部的3D内容并匹配面部表情。
概述
此示例应用程序提供了一个简单的界面,可让你在具有TrueDepth前置摄像头的设备上选择一下四个增强现实可视化(参阅iOS设备兼容性参考)。
- 单独的相机视图,没有任何AR内容
- ARKit提供的面部网格,可以自动估计真实的定向照明环境
- 附加到用户真实面孔的虚拟3D内容(并被部分遮蔽)
- 一个可动简单的机器人角色,其表情与用户匹配
使用示例应用程序的“+”按钮在这些模式之间切换,如下所示:
在SceneKit视图中启动面部追踪会话
与ARKit其他用途一样,面部追踪需要配置和运行会话(ARSession对象),并将视频图像与虚拟内容一起渲染。有关会话和视图设置的更详细的说明,请参阅关于增强现实与ARKit和创建第一个AR体验。此示例使用SceneKit显示AR体验,但也可以使用SpriteKit或使用Metal创建自己的渲染器(参阅ARSKView和使用Metal显示AR体验**)。
面部追踪与用于配置会话的类中的ARKit的其他用途不同。要启用面部追踪,请创建一个ARFaceTrackingConfiguration实例,配置其属性,并将其传递给与视图关联的AR会话的run(_:options:)方法,如下所示:
guard ARFaceTrackingConfiguration.isSupported else { return }
let configuration = ARFaceTrackingConfiguration()
configuration.isLightEstimationEnabled = true
session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
在提供需要面部追踪AR会话的用户功能之前,请检查ARFaceTrackingConfiguration.isSupported属性以确定当前设备是否支持ARKit面部追踪。
面部追踪的位置和方向
当面部追踪处于活动状态时,ARKit会自动将ARFaceAnchor对象添加到正在运行的AR会话中,其中包含有关用户面部的信息,包括其位置和方向。
注意
ARKit会检测并提供有关用户面部的信息。如果摄像机图像中存在多个面部,则ARKit会选择最大或最清晰可识别的面部。
在基于SceneKit的AR体验中,可以在renderer(_:didAdd:for:)方法中添加与面部锚点对应的3D内容。ARKit为锚点添加了一个SceneKit节点,并更新了每个frame上节点的位置和方向,因此你添加到该节点的任何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()
}
}
在此示例中,renderer(_:didAdd:for:)方法将SceneKit内容添加到faceNode。例如,如果更改示例代码中的showsCoordinateOrigin变量,则应用程序将x/y/z轴的可视化添加到该节点,指示面部锚点坐标系的起点。
使用面部几何体给用户面部建模
ARKit提供了一个与用户面部的大小、形状、拓扑与当前面部表情相匹配的粗略3D网格几何体。还提供了ARSCNFaceGeometry类,它是一种在SceneKit中可视化此网格的简单方法。
AR体验可以使用此网格放置或绘制看起来附加到面部的内容。例如,通过将半透明纹理应用于此几何体,可以将虚拟纹身或化妆画到用户的皮肤上。
要创建SceneKit面部几何体,请使用SceneKit视图用于渲染的Metal设备初始化一个ARSCNFaceGeometry对象:
// This relies on the earlier check of `ARFaceTrackingConfiguration.isSupported`.
let device = sceneView.device!
let maskGeometry = ARSCNFaceGeometry(device: device)!
示例代码的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)
}
然后,通过将新的面部网格传递到update(from:)方法,更新场景中的ARSCNFaceGeometry对象:
func update(withFaceAnchor anchor: ARFaceAnchor) {
let faceGeometry = geometry as! ARSCNFaceGeometry
faceGeometry.update(from: anchor.geometry)
}
在用户面部放置3D内容
ARKit提供的面部网格的另一个用途是在场景中创建遮挡几何体。遮挡几何体是不会呈现任何可见内容(允许相机图像显示)的3D模型,但会阻碍相机对场景中其他虚拟内容的视图。
这种技术创造出真实面部与虚拟对象交互的错觉,即使面部是2D摄像机图像,虚拟内容是渲染的3D对象。例如,如果将遮挡几何体图形和虚拟眼镜放置在用户的面部上,则面部可能会遮挡眼睛的frame。
要创建面部的遮挡几何体,请先创建一个ARSCNFaceGeometry对象,如上例所示,但是,不要使用可见的外观来配置该对象的SceneKit材质,而是在渲染期间将材质设置为渲染深度而不是颜色:
geometry.firstMaterial!.colorBufferWriteMask = []
occlusionNode = SCNNode(geometry: geometry)
occlusionNode.renderingOrder = -1
由于材质呈现深度,所以SceneKit渲染的其他对象正确地出现在它的前面或后面。但是由于材质不会呈现颜色,相机图像会出现在其位置上。示例应用程序将此技术与位于用户眼睛前方的SceneKit对象相结合,创建一个效果,其中对象被用户的鼻子实际遮挡。
用混合形状动画化一个角色
除了上述示例中所示的面部网格之外,ARKit还提供了一个更为抽象的用户面部表情模型,其形式为blendShapes字典。可以使用该字典中的命名系数值来控制自己的2D或3D资源的动画参数,创建遵循用户真实面部动作和表情的角色(如头像或木偶)。
作为混合形状动画的基本演示,此示例包含使用ScenceKit原始形状创建的机器人角色头部的简单模型。(参阅源代码中的robotHead.scn文件。)
要获取用户当前的面部表情,请从renderer(_:didUpdate:for:)代理回调中读取面部锚点的blendShapes字典:
func update(withFaceAnchor faceAnchor: ARFaceAnchor) {
blendShapes = faceAnchor.blendShapes
}
然后,检查该字典中的键值对以计算模型的动画参数。有52个独特的ARFaceAnchor.BlendShapeLocation系数。app可以使用尽可能少的或很多的必要条件来创建想要的艺术效果。在此示例中,RobotHead类执行此计算,将eyeBlinkLeft和eyeBlinkRight参数映射到机器人眼睛的比例因子的一个轴线,并将jawOpen参数映射到机器人颌骨的位置。
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
}
}
<br />
使用Metal显示AR体验
通过渲染相机图像和使用位置追踪信息显示叠加内容来构建自定义AR视图。
概述
ARKit包括用于使用SceneKit或SpriteKit轻松现实AR体验的视图类。但是,如果建立自己的渲染引擎(或集成第三方引擎),则ARKit还提供必要的支持以现实具有自定义视图的AR体验。
在任何AR体验中,第一步是配置一个ARSession对象来管理摄像头捕获和运动处理。会话定义和维护设备所在的真实世界与你为AR内容建模的虚拟空间之间的对应关系。要在自定义视图中显示AR体验,需要:
- 从会话中检索视频帧和追踪信息。
- 将这些frame图像作为视图的背景渲染。
- 使用追踪信息在相机图像上方定位和绘制AR内容。
注意
下面介绍了Xcode项目模版中的代码。有关完整的示例代码,请使用Augmented Realitytemplate模版创建一个新的iOS应用程序,然后从Content Technology弹出菜单中选择Metal。
从会话中获取视频帧和追踪数据
创建和维护自己的ARSession实例,并使用适合你想要支持的AR体验类型的会话配置运行它。会话从相机捕获视频,在建模的3D空间中追踪设备的位置和方向爱哪个,并提供ARFrame对象。每个这样的对象从捕获帧的时刻都包含单独的视频帧图像和位置追踪信息。
有两种方法来访问由AR会话产生的ARFrame对象,这取决于你的应用是否偏好pull或push设计模式。
如果你偏重控制帧定时(pull设计模式),请使用会话的currentFrame属性来获取当前帧图像和每次重绘视图内容时的追踪信息。ARKit Xcode模版使用这种方法:
// in Renderer class, called from MTKViewDelegate.draw(in:) via Renderer.update()
func updateGameState() {
guard let currentFrame = session.currentFrame else {
return
}
updateSharedUniforms(frame: currentFrame)
updateAnchors(frame: currentFrame)
updateCapturedImageTextures(frame: currentFrame)
if viewportSizeDidChange {
viewportSizeDidChange = false
updateImagePlane(frame: currentFrame)
}
}
或者,如果你的应用程序偏好push模式,则实现session(_:didUpdate:)代理方法,会话将为其捕获的每个视频帧调用一次(默认为60帧/秒)。
获得一个frame后,你需要绘制相机图像,并更新和渲染你的AR体验中包含的任何覆盖内容。
绘制相机图像
每个ARFrame对象的capturedImage属性都包含从设备摄像头捕获到的像素缓冲区。要将此图像绘制为自定义视图的背景,需要从图像内容创建纹理,并提交使用这些纹理的GPU渲染命令。
像素缓冲区的内容被编码为双曲面YCbCr(也称为YUV)数据格式;要渲染图像,需要将此像素数据转换为可绘制的RGB格式。对于使用Metal渲染,可以在GPU着色器代码中最有效地执行此转换。使用CVMetalTextureCache API从像素缓冲区创建两个Metal纹理--每个用于缓冲区的亮度(Y)和色度(CbCr)平面:
func updateCapturedImageTextures(frame: ARFrame) {
// Create two textures (Y and CbCr) from the provided frame's captured image
let pixelBuffer = frame.capturedImage
if (CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
return
}
capturedImageTextureY = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.r8Unorm, planeIndex:0)!
capturedImageTextureCbCr = createTexture(fromPixelBuffer: pixelBuffer, pixelFormat:.rg8Unorm, planeIndex:1)!
}
func createTexture(fromPixelBuffer pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) -> MTLTexture? {
var mtlTexture: MTLTexture? = nil
let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex)
let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex)
var texture: CVMetalTexture? = nil
let status = CVMetalTextureCacheCreateTextureFromImage(nil, capturedImageTextureCache, pixelBuffer, nil, pixelFormat, width, height, planeIndex, &texture)
if status == kCVReturnSuccess {
mtlTexture = CVMetalTextureGetTexture(texture!)
}
return mtlTexture
}
接下来,使用颜色转换矩阵执行YCbCr到RGB转换的部分功能来编码这两个纹理的渲染命令:
fragment float4 capturedImageFragmentShader(ImageColorInOut in [[stage_in]],
texture2d<float, access::sample> capturedImageTextureY [[ texture(kTextureIndexY) ]],
texture2d<float, access::sample> capturedImageTextureCbCr [[ texture(kTextureIndexCbCr) ]]) {
constexpr sampler colorSampler(mip_filter::linear,
mag_filter::linear,
min_filter::linear);
const float4x4 ycbcrToRGBTransform = float4x4(
float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f),
float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f),
float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f),
float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)
);
// Sample Y and CbCr textures to get the YCbCr color at the given texture coordinate
float4 ycbcr = float4(capturedImageTextureY.sample(colorSampler, in.texCoord).r,
capturedImageTextureCbCr.sample(colorSampler, in.texCoord).rg, 1.0);
// Return converted RGB color
return ycbcrToRGBTransform * ycbcr;
}
注意
使用displayTransform(for:viewportSize:)方法确保相机图像是否覆盖整个视图。例如使用这个方法,以及完整的Metal管道设置代码,请参阅完整的Xcode模版。
追踪并渲染覆盖内容
AR体验通常侧重于渲染3D覆盖内容,使内容看起来是从相机图像中看到的真实世界的一部分。为了实现这种幻象,使用ARAnchor类来模拟自己的3D内容相对于现实世界空间的位置和方向。锚点对象提供了可在渲染过程中引用的转换。
例如,Xcode模版在用户点击屏幕时创建位于设备前面大约20厘米处的锚点:
func handleTap(gestureRecognize: UITapGestureRecognizer) {
// Create anchor using the camera's current position
if let currentFrame = session.currentFrame {
// Create a transform with a translation of 0.2 meters in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.2
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
session.add(anchor: anchor)
}
}
在渲染引擎中,使用每个ARAnchor对象的transform属性来放置可视化内容。Xcode模版使用在其handleTap方法添加到会话中的每个锚点来定位简单的立体网格:
func updateAnchors(frame: ARFrame) {
// Update the anchor uniform buffer with transforms of the current frame's anchors
anchorInstanceCount = min(frame.anchors.count, kMaxAnchorInstanceCount)
var anchorOffset: Int = 0
if anchorInstanceCount == kMaxAnchorInstanceCount {
anchorOffset = max(frame.anchors.count - kMaxAnchorInstanceCount, 0)
}
for index in 0..<anchorInstanceCount {
let anchor = frame.anchors[index + anchorOffset]
// Flip Z axis to convert geometry from right handed to left handed
var coordinateSpaceTransform = matrix_identity_float4x4
coordinateSpaceTransform.columns.2.z = -1.0
let modelMatrix = simd_mul(anchor.transform, coordinateSpaceTransform)
let anchorUniforms = anchorUniformBufferAddress.assumingMemoryBound(to: InstanceUniforms.self).advanced(by: index)
anchorUniforms.pointee.modelMatrix = modelMatrix
}
}
注意
在更复杂的AR体验中,可以使用碰撞测试或平面检测来查找真实世界曲面的位置。有关详细信息,请参阅planeDetection属性和hitTest(_:types:)方法。在这两种情况下,ARKit都会提供ARAnchor对象的结果,因此你仍然可以使用锚点转换来放置视觉内容。
现实光照渲染
当你配置用于在场景中绘制3D内容的着色器时,请使用每个ARFrame对象中的预计光照信息来产生更逼真的阴影:
// in Renderer.updateSharedUniforms(frame:):
// Set up lighting for the scene using the ambient intensity if provided
var ambientIntensity: Float = 1.0
if let lightEstimate = frame.lightEstimate {
ambientIntensity = Float(lightEstimate.ambientIntensity) / 1000.0
}
let ambientLightColor: vector_float3 = vector3(0.5, 0.5, 0.5)
uniforms.pointee.ambientLightColor = ambientLightColor * ambientIntensity