您不需要移动锚点。(手挥)那不是您要找的API……
向会话中添加ARAnchor
对象实际上是为了“标记”现实世界中的一个点,以便您之后可以引用它。例如,点(1,1,1)
始终是点(1,1,1)
- 您不能将其移动到其他地方,因为那么它就不再是点(1,1,1)
了。
为了进行二维类比:锚点是参考点,有点像视图的边界。系统(或您代码的另一部分)告诉视图其边界在哪里,视图相对于这些边界绘制其内容。在增强现实中,锚点为您提供了可以用于绘制三维内容的参考点。
你所询问的实际上是关于在两个点之间移动(和动画化移动)虚拟内容。ARKit 本身并不涉及显示或动画化虚拟内容 - 有很多出色的图形引擎,因此 ARKit 不需要重新发明轮子。ARKit 所做的是为您提供一个现实世界的参考框架,以使用现有的图形技术(如 SceneKit 或 SpriteKit(或 Unity 或 Unreal,或使用 Metal 或 GL 构建的自定义引擎)来显示或动画化内容。
既然你提到要使用SpriteKit来实现这个功能...请注意,这会变得很混乱。SpriteKit是一个2D引擎,虽然ARSKView
提供了一些方法来将第三个维度塞进去,但这些方法有其限制。
ARSKView
会自动更新与ARAnchor
相关联的每个精灵的xScale
、yScale
和zRotation
,从而提供3D透视的幻觉。但这仅适用于附加到锚点的节点,正如上面所述,锚点是静态的。
不过,你可以向场景中添加其他节点,并使用相同的属性使这些节点与ARSKView
管理的节点匹配。下面是一些代码,你可以将其添加/替换到ARKit/SpriteKit Xcode模板项目中以实现这一点。我们将从一些基本逻辑开始,在第三次点击后(在使用前两次点击放置锚点之后)运行一个弹跳动画。
var anchors: [ARAnchor] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if anchors.count > 1 {
startBouncing(time: 1)
return
}
guard let sceneView = self.view as? ARSKView else { return }
if let currentFrame = sceneView.session.currentFrame {
var translation = matrix_identity_float4x4
translation.columns.3.z = -0.3
let transform = simd_mul(currentFrame.camera.transform, translation)
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
anchors.append(anchor)
}
}
接下来,一些SpriteKit的乐趣,让动画变得更有趣:
var ballNode: SKLabelNode = {
let labelNode = SKLabelNode(text: "")
labelNode.horizontalAlignmentMode = .center
labelNode.verticalAlignmentMode = .center
return labelNode
}()
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSKView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
addChild(ballNode)
}
ballNode.setScale(start.xScale)
ballNode.zRotation = start.zRotation
ballNode.position = start.position
let scale = SKAction.scale(to: end.xScale, duration: time)
let rotate = SKAction.rotate(toAngle: end.zRotation, duration: time)
let move = SKAction.move(to: end.position, duration: time)
let scaleBack = SKAction.scale(to: start.xScale, duration: time)
let rotateBack = SKAction.rotate(toAngle: start.zRotation, duration: time)
let moveBack = SKAction.move(to: start.position, duration: time)
let action = SKAction.repeatForever(.sequence([
.group([scale, rotate, move]),
.group([scaleBack, rotateBack, moveBack])
]))
ballNode.removeAllActions()
ballNode.run(action)
}
这里有一个视频,你可以看到这段代码的实际效果。你会注意到,只要你不移动相机,这个幻觉才能起作用——对于AR来说并不是很好。当使用SKAction
时,我们无法在动画过程中调整动画的起始/结束状态,因此球只会在其原始(屏幕空间)位置/旋转/缩放之间来回弹跳。
你可以通过直接对球进行动画来做得更好,但这需要很多工作。你需要在每一帧(或每个view(_:didUpdate:for:)
委托回调)上:
在动画的每个端点处,保存基于锚点的节点的更新后的位置、旋转和缩放值。每个 didUpdate
回调需要执行两次此操作,因为每个节点都会得到一个回调。
通过根据当前时间在两个端点值之间插值计算,确定正在进行动画的节点的位置、旋转和缩放值。
设置节点的新属性(或者将其动画到这些属性,在非常短的时间内,这样它不会在一帧中跳跃太多)。
为了将一个假的3D视觉效果塞到2D图形工具包中,需要完成很多工作;这也是我关于SpriteKit不是进入ARKit的好第一步的评论所在。
如果你想要在 AR 叠加图层中实现 3D 定位和动画,使用 3D 图形工具包会更加容易。以下是前面例子的重复版,但使用 SceneKit 替代。首先使用 ARKit/SceneKit Xcode 模板,将飞船移除,并将上方相同的 touchesBegan
函数粘贴到 ViewController
中(还需要将 as ARSKView
转换为 as ARSCNView
)。
然后,通过一些快速的代码以匹配 ARKit/SpriteKit 模板的行为,在 SceneKit 中放置 2D billboarded 精灵:
func makeBillboardNode(image: UIImage) -> SCNNode {
let plane = SCNPlane(width: 0.1, height: 0.1)
plane.firstMaterial!.diffuse.contents = image
let node = SCNNode(geometry: plane)
node.constraints = [SCNBillboardConstraint()]
return node
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
let billboard = makeBillboardNode(image: "⛹️".image())
node.addChildNode(billboard)
}
最后,为弹跳球添加动画:
let ballNode = makeBillboardNode(image: "".image())
func startBouncing(time: TimeInterval) {
guard
let sceneView = self.view as? ARSCNView,
let first = anchors.first, let start = sceneView.node(for: first),
let last = anchors.last, let end = sceneView.node(for: last)
else { return }
if ballNode.parent == nil {
sceneView.scene.rootNode.addChildNode(ballNode)
}
let animation = CABasicAnimation(keyPath: #keyPath(SCNNode.transform))
animation.fromValue = start.transform
animation.toValue = end.transform
animation.duration = time
animation.autoreverses = true
animation.repeatCount = .infinity
ballNode.removeAllAnimations()
ballNode.addAnimation(animation, forKey: nil)
}
这次的动画代码比SpriteKit版本要短得多。
这是它的实际效果。
因为我们一开始就在3D中工作,所以我们实际上是在两个3D位置之间进行动画 —— 不像SpriteKit版本,动画会停留在它应该停留的地方。(而且不需要直接插值和动画属性的额外工作。)