我如何以编程方式移动一个锚?

时间:2023-01-30 16:27:46

I'm trying out the new ARKit to replace another similar solution I have. It's pretty great! But I can't seem to figure out how to move an ARAnchor programmatically. I want to slowly move the anchor to the left of the user.

我正在试用新的ARKit来替代我的另一个类似的解决方案。很好了!但我似乎不知道如何以编程的方式移动ARAnchor。我想慢慢地将锚点移动到用户的左边。

Creating the anchor to be 2 meters in front of the user:

将锚定在用户面前2米:

        var translation = matrix_identity_float4x4
        translation.columns.3.z = -2.0
        let transform = simd_mul(currentFrame.camera.transform, translation)

        let anchor = ARAnchor(transform: transform)
        sceneView.session.add(anchor: anchor)

later, moving the object to the left/right of the user (x-axis)...

稍后,将对象移动到用户的左/右(x轴)…

anchor.transform.columns.3.x = anchor.transform.columns.3.x + 0.1

repeated every 50 milliseconds (or whatever).

每50毫秒重复一次。

The above does not work because transform is a get-only property.

上述方法不起作用,因为转换是一个只获取的属性。

I need a way to change the position of an AR object in space relative to the user in a way that keeps the AR experience intact - meaning, if you move your device, the AR object will be moving but also won't be "stuck" to the camera like it's simply painted on, but moves like you would see a person move while you were walking by - they are moving and you are moving and it looks natural.

我需要一种方法来改变一个基于“增大化现实”技术的对象在空间的位置相对于用户体验,使基于“增大化现实”技术的完整——这意味着,如果你移动你的设备,基于“增大化现实”技术的对象将被移动,也不会被“困”相机像简单的画,但是就像你会看到一个人你路过的时候,他们正在和你正在和它看起来自然。

Please note the scope of this question relates only to how to move an object in space in relation to the user (ARAnchor), not in relation to a plane (ARPlaneAnchor) or to another detected surface (ARHitTestResult).

请注意,此问题的范围仅涉及如何在空间中移动与用户(ARAnchor)相关的对象,而与平面(ARPlaneAnchor)或另一个被探测表面(ARHitTestResult)无关。

Thanks!

谢谢!

1 个解决方案

#1


31  

You don't need to move anchors. (hand wave) That's not the API you're looking for...

你不需要移动锚。(手波)那不是你要找的API…

Adding ARAnchor objects to a session is effectively about "labeling" a point in real-world space so that you can refer to it later. The point (1,1,1) (for example) is always the point (1,1,1) — you can't move it someplace else because then it's not the point (1,1,1) anymore.

向会话添加ARAnchor对象实际上是关于在真实空间中“标记”一个点,以便稍后您可以引用它。点(1,1,1)(例如)总是点(1,1,1)你不能把它移到别的地方因为它不再是点(1,1,1)

To make a 2D analogy: anchors are reference points sort of like the bounds of a view. The system (or another piece of your code) tells the view where it's boundaries are, and the view draws its content relative to those boundaries. Anchors in AR give you reference points you can use for drawing content in 3D.

做一个二维的类比:锚是参考点有点像一个视图的边界。系统(或代码的另一部分)告诉视图它的边界在哪里,视图根据这些边界绘制内容。在增强现实中,锚点为你提供了可以在3D中绘制内容的参考点。

What you're asking is really about moving (and animating the movement of) virtual content between two points. And ARKit itself really isn't about displaying or animating virtual content — there are plenty of great graphics engines out there, so ARKit doesn't need to reinvent that wheel. What ARKit does is provide a real-world frame of reference for you to display or animate content using an existing graphics technology like SceneKit or SpriteKit (or Unity or Unreal, or a custom engine built with Metal or GL).

你真正想要的是在两点之间移动(和动画)虚拟内容。而ARKit本身并不是关于显示或动画虚拟内容——有很多很棒的图形引擎,所以ARKit不需要重新发明这个*。ARKit所做的是为您提供一个真实的参考框架,以便您使用现有的图形技术(如SceneKit或SpriteKit)(或Unity或Unreal,或使用金属或GL构建的自定义引擎)来显示或动画内容。


Since you mentioned trying to do this with SpriteKit... beware, it gets messy. SpriteKit is a 2D engine, and while ARSKView provides some ways to shoehorn a third dimension in there, those ways have their limits.

既然你提到要用SpriteKit做这个…注意,它会变得混乱不堪。SpriteKit是一个2D引擎,虽然ARSKView提供了一些方法来将第三维度硬塞进去,但这些方法都有其局限性。

ARSKView automatically updates the xScale, yScale, and zRotation of each sprite associated with an ARAnchor, providing the illusion of 3D perspective. But that applies only to nodes attached to anchors, and as noted above, anchors are static.

ARSKView自动更新与ARAnchor有关的每个精灵的xScale、yScale和zrotate,提供3D透视图的错觉。但这只适用于附着在锚上的节点,如上所述,锚是静止的。

You can, however, add other nodes to your scene, and use those same properties to make those nodes match the ARSKView-managed nodes. Here's some code you can add/replace in the ARKit/SpriteKit Xcode template project to do that. We'll start with some basic logic to run a bouncing animation on the third tap (after using the first two taps to place anchors).

但是,您可以向场景添加其他节点,并使用相同的属性使这些节点与arskview管理的节点匹配。以下是一些可以在ARKit/SpriteKit Xcode模板项目中添加/替换的代码。我们将从一些基本的逻辑开始,在第三个tap上运行一个跳动的动画(使用前两个tap放置锚点之后)。

var anchors: [ARAnchor] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    // Start bouncing on touch after placing 2 anchors (don't allow more)
    if anchors.count > 1 {
        startBouncing(time: 1)
        return
    }
    // Create anchor using the camera's current position
    guard let sceneView = self.view as? ARSKView else { return }
    if let currentFrame = sceneView.session.currentFrame {

        // Create a transform with a translation of 30 cm in front of the camera
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.3
        let transform = simd_mul(currentFrame.camera.transform, translation)

        // Add a new anchor to the session
        let anchor = ARAnchor(transform: transform)
        sceneView.session.add(anchor: anchor)
        anchors.append(anchor)
    }
}

Then, some SpriteKit fun for making that animation happen:

然后,一些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)
}

Here's a video so you can see this code in action. You'll notice that the illusion only works as long as you don't move the camera — not so great for AR. When using SKAction, we can't adjust the start/end states of the animation while animating, so the ball keeps bouncing back and forth between its original (screen-space) positions/rotations/scales.

这是一个视频,你可以看到这段代码的作用。你会注意到,只有当你不移动相机时,这种错觉才会起作用——对AR来说不是很好。当使用SKAction时,我们不能调整动画的开始/结束状态,所以球在原来的(屏幕空间)位置/旋转/缩放之间来回反弹。

You could do better by animating the ball directly, but it's a lot of work. You'd need to, on every frame (or every view(_:didUpdate:for:) delegate callback):

你可以做得更好,通过直接动画球,但这是很多的工作。您需要在每个帧(或每个视图(_:didUpdate:for:)上委托回调):

  1. Save off the updated position, rotation, and scale values for the anchor-based nodes at each end of the animation. You'll need to do this twice per didUpdate callback, because you'll get one callback for each node.

    保存动画每个末端基于锚点的节点的更新位置、旋转和缩放值。每个didUpdate回调需要做两次,因为每个节点都有一个回调。

  2. Work out position, rotation, and scale values for the node being animated, by interpolating between the two endpoint values based on the current time.

    通过基于当前时间在两个端点值之间插入来计算被动画的节点的位置、旋转和缩放值。

  3. Set the new attributes on the node. (Or maybe animate it to those attributes over a very short duration, so it doesn't jump too much in one frame?)

    在节点上设置新属性。(或者可能在很短的时间内将它动画到这些属性,这样它在一帧中不会跳得太多?)

That's kind of a lot of work to shoehorn a fake 3D illusion into a 2D graphics toolkit — hence my comments about SpriteKit not being a great first step into ARKit.

要把一个假的3D错觉硬塞进一个2D图形工具包需要做很多工作——因此我对SpriteKit的评论并不是进入ARKit的第一步。


If you want 3D positioning and animation for your AR overlays, it's a lot easier to use a 3D graphics toolkit. Here's a repeat of the previous example, but using SceneKit instead. Start with the ARKit/SceneKit Xcode template, take the spaceship out, and paste the same touchesBegan function from above into the ViewController. (Change the as ARSKView casts to as ARSCNView, too.)

如果您想要3D定位和动画您的AR覆盖,使用3D图形工具包要容易得多。这里重复前面的示例,但是使用SceneKit。从ARKit/SceneKit Xcode模板开始,取出太空船,并将同样的touchesBegan函数从上面粘贴到ViewController中。(也将as ARSKView转换为ARSCNView。)

Then, some quick code for placing 2D billboarded sprites, matching via SceneKit the behavior of the ARKit/SpriteKit template:

然后,通过SceneKit匹配ARKit/SpriteKit模板的行为,编写一些用于放置2D billboardsprite的快速代码:

// in global scope
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
}

// inside ViewController
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    // emoji to image based on https://*.com/a/41021662/957768
    let billboard = makeBillboardNode(image: "⛹️".image())
    node.addChildNode(billboard)
}

Finally, adding the animation for the bouncing ball:

最后,为跳跃的球添加动画:

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)
}

This time the animation code is a lot shorter than the SpriteKit version. Here's how it looks in action.

这一次动画代码比SpriteKit版本短得多。这是它的实际效果。

Because we're working in 3D to start with, we're actually animating between two 3D positions — unlike in the SpriteKit version, the animation stays where it's supposed to. (And without the extra work for directly interpolating and animating attributes.)

因为我们从3D开始,我们实际上是在两个3D位置之间进行动画制作——不像SpriteKit版本,动画会保持在它应该的位置。(不需要直接插入和动画属性的额外工作。)

#1


31  

You don't need to move anchors. (hand wave) That's not the API you're looking for...

你不需要移动锚。(手波)那不是你要找的API…

Adding ARAnchor objects to a session is effectively about "labeling" a point in real-world space so that you can refer to it later. The point (1,1,1) (for example) is always the point (1,1,1) — you can't move it someplace else because then it's not the point (1,1,1) anymore.

向会话添加ARAnchor对象实际上是关于在真实空间中“标记”一个点,以便稍后您可以引用它。点(1,1,1)(例如)总是点(1,1,1)你不能把它移到别的地方因为它不再是点(1,1,1)

To make a 2D analogy: anchors are reference points sort of like the bounds of a view. The system (or another piece of your code) tells the view where it's boundaries are, and the view draws its content relative to those boundaries. Anchors in AR give you reference points you can use for drawing content in 3D.

做一个二维的类比:锚是参考点有点像一个视图的边界。系统(或代码的另一部分)告诉视图它的边界在哪里,视图根据这些边界绘制内容。在增强现实中,锚点为你提供了可以在3D中绘制内容的参考点。

What you're asking is really about moving (and animating the movement of) virtual content between two points. And ARKit itself really isn't about displaying or animating virtual content — there are plenty of great graphics engines out there, so ARKit doesn't need to reinvent that wheel. What ARKit does is provide a real-world frame of reference for you to display or animate content using an existing graphics technology like SceneKit or SpriteKit (or Unity or Unreal, or a custom engine built with Metal or GL).

你真正想要的是在两点之间移动(和动画)虚拟内容。而ARKit本身并不是关于显示或动画虚拟内容——有很多很棒的图形引擎,所以ARKit不需要重新发明这个*。ARKit所做的是为您提供一个真实的参考框架,以便您使用现有的图形技术(如SceneKit或SpriteKit)(或Unity或Unreal,或使用金属或GL构建的自定义引擎)来显示或动画内容。


Since you mentioned trying to do this with SpriteKit... beware, it gets messy. SpriteKit is a 2D engine, and while ARSKView provides some ways to shoehorn a third dimension in there, those ways have their limits.

既然你提到要用SpriteKit做这个…注意,它会变得混乱不堪。SpriteKit是一个2D引擎,虽然ARSKView提供了一些方法来将第三维度硬塞进去,但这些方法都有其局限性。

ARSKView automatically updates the xScale, yScale, and zRotation of each sprite associated with an ARAnchor, providing the illusion of 3D perspective. But that applies only to nodes attached to anchors, and as noted above, anchors are static.

ARSKView自动更新与ARAnchor有关的每个精灵的xScale、yScale和zrotate,提供3D透视图的错觉。但这只适用于附着在锚上的节点,如上所述,锚是静止的。

You can, however, add other nodes to your scene, and use those same properties to make those nodes match the ARSKView-managed nodes. Here's some code you can add/replace in the ARKit/SpriteKit Xcode template project to do that. We'll start with some basic logic to run a bouncing animation on the third tap (after using the first two taps to place anchors).

但是,您可以向场景添加其他节点,并使用相同的属性使这些节点与arskview管理的节点匹配。以下是一些可以在ARKit/SpriteKit Xcode模板项目中添加/替换的代码。我们将从一些基本的逻辑开始,在第三个tap上运行一个跳动的动画(使用前两个tap放置锚点之后)。

var anchors: [ARAnchor] = []
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    // Start bouncing on touch after placing 2 anchors (don't allow more)
    if anchors.count > 1 {
        startBouncing(time: 1)
        return
    }
    // Create anchor using the camera's current position
    guard let sceneView = self.view as? ARSKView else { return }
    if let currentFrame = sceneView.session.currentFrame {

        // Create a transform with a translation of 30 cm in front of the camera
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.3
        let transform = simd_mul(currentFrame.camera.transform, translation)

        // Add a new anchor to the session
        let anchor = ARAnchor(transform: transform)
        sceneView.session.add(anchor: anchor)
        anchors.append(anchor)
    }
}

Then, some SpriteKit fun for making that animation happen:

然后,一些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)
}

Here's a video so you can see this code in action. You'll notice that the illusion only works as long as you don't move the camera — not so great for AR. When using SKAction, we can't adjust the start/end states of the animation while animating, so the ball keeps bouncing back and forth between its original (screen-space) positions/rotations/scales.

这是一个视频,你可以看到这段代码的作用。你会注意到,只有当你不移动相机时,这种错觉才会起作用——对AR来说不是很好。当使用SKAction时,我们不能调整动画的开始/结束状态,所以球在原来的(屏幕空间)位置/旋转/缩放之间来回反弹。

You could do better by animating the ball directly, but it's a lot of work. You'd need to, on every frame (or every view(_:didUpdate:for:) delegate callback):

你可以做得更好,通过直接动画球,但这是很多的工作。您需要在每个帧(或每个视图(_:didUpdate:for:)上委托回调):

  1. Save off the updated position, rotation, and scale values for the anchor-based nodes at each end of the animation. You'll need to do this twice per didUpdate callback, because you'll get one callback for each node.

    保存动画每个末端基于锚点的节点的更新位置、旋转和缩放值。每个didUpdate回调需要做两次,因为每个节点都有一个回调。

  2. Work out position, rotation, and scale values for the node being animated, by interpolating between the two endpoint values based on the current time.

    通过基于当前时间在两个端点值之间插入来计算被动画的节点的位置、旋转和缩放值。

  3. Set the new attributes on the node. (Or maybe animate it to those attributes over a very short duration, so it doesn't jump too much in one frame?)

    在节点上设置新属性。(或者可能在很短的时间内将它动画到这些属性,这样它在一帧中不会跳得太多?)

That's kind of a lot of work to shoehorn a fake 3D illusion into a 2D graphics toolkit — hence my comments about SpriteKit not being a great first step into ARKit.

要把一个假的3D错觉硬塞进一个2D图形工具包需要做很多工作——因此我对SpriteKit的评论并不是进入ARKit的第一步。


If you want 3D positioning and animation for your AR overlays, it's a lot easier to use a 3D graphics toolkit. Here's a repeat of the previous example, but using SceneKit instead. Start with the ARKit/SceneKit Xcode template, take the spaceship out, and paste the same touchesBegan function from above into the ViewController. (Change the as ARSKView casts to as ARSCNView, too.)

如果您想要3D定位和动画您的AR覆盖,使用3D图形工具包要容易得多。这里重复前面的示例,但是使用SceneKit。从ARKit/SceneKit Xcode模板开始,取出太空船,并将同样的touchesBegan函数从上面粘贴到ViewController中。(也将as ARSKView转换为ARSCNView。)

Then, some quick code for placing 2D billboarded sprites, matching via SceneKit the behavior of the ARKit/SpriteKit template:

然后,通过SceneKit匹配ARKit/SpriteKit模板的行为,编写一些用于放置2D billboardsprite的快速代码:

// in global scope
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
}

// inside ViewController
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    // emoji to image based on https://*.com/a/41021662/957768
    let billboard = makeBillboardNode(image: "⛹️".image())
    node.addChildNode(billboard)
}

Finally, adding the animation for the bouncing ball:

最后,为跳跃的球添加动画:

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)
}

This time the animation code is a lot shorter than the SpriteKit version. Here's how it looks in action.

这一次动画代码比SpriteKit版本短得多。这是它的实际效果。

Because we're working in 3D to start with, we're actually animating between two 3D positions — unlike in the SpriteKit version, the animation stays where it's supposed to. (And without the extra work for directly interpolating and animating attributes.)

因为我们从3D开始,我们实际上是在两个3D位置之间进行动画制作——不像SpriteKit版本,动画会保持在它应该的位置。(不需要直接插入和动画属性的额外工作。)