【问题标题】:SceneKit applyTorqueSceneKit 应用扭矩
【发布时间】:2015-01-12 21:08:02
【问题描述】:

我正在尝试将扭矩应用到我场景中的一个节点。文档指出:

扭矩矢量的每个分量都与绕轴的旋转有关 SCNNode 局部坐标系中的对应轴 包含物理体的对象。例如,施加扭矩 {0.0, 0.0, 1.0} 使节点围绕其逆时针旋转 z 轴。

但是在我的测试中,物理动画似乎不会影响对象的实际位置。因此,轴保持静止(即使实际节点明显移动)。这导致扭矩总是从同一方向施加(无论场景启动时 z 轴在哪里)。

我希望能够施加扭矩,以便它相对于对象始终是恒定的(例如,使节点围绕节点的presentationNode的z轴逆时针旋转,而不是位置节点有(有?)当场景已启动)

【问题讨论】:

    标签: ios scenekit


    【解决方案1】:

    SceneKit 使用每个节点的两个版本:model 节点定义静态行为,presentation 节点实际参与动态行为并在屏幕上使用。这种划分反映了 Core Animation 中使用的功能,并启用了隐式动画等功能(您可以在其中执行诸如设置 node.position 之类的操作并将其设置为新值,而无需处理查询 node.position 的代码的其他部分动画期间的中间值)。

    Physics 在表示节点上运行,但在某些情况下(例如本例)在场景空间中进行输入。

    但是,表示节点和场景之间的唯一区别在于坐标空间,因此您需要做的就是将向量从表示空间转换为场景空间。 (场景的根节点不应被物理、动作或飞行动画转换,因此模型场景空间和演示场景空间之间没有实际区别。)为此,请使用坐标转换方法之一SceneKit 提供,如convertPosition:fromNode:

    这里有一个 Swift 游乐场,可以说明您的困境:

    import Cocoa
    import SceneKit
    import XCPlayground
    
    // Set up a scene for our tests
    let scene = SCNScene()
    let view = SCNView(frame: NSRect(x: 0, y: 0, width: 500, height: 500))
    view.autoenablesDefaultLighting = true
    view.scene = scene
    let cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
    scene.rootNode.addChildNode(cameraNode)
    XCPShowView("view", view)
    
    // Make a pyramid to test on
    let node = SCNNode(geometry: SCNPyramid(width: 1, height: 1, length: 1))
    scene.rootNode.addChildNode(node)
    node.physicsBody = SCNPhysicsBody.dynamicBody()
    scene.physicsWorld.gravity = SCNVector3Zero // Don't fall off screen
    
    // Rotate around the axis that looks into the screen
    node.physicsBody?.applyTorque(SCNVector4(x: 0, y: 0, z: 1, w: 0.1), impulse: true)
    
    // Wait a bit, then try to rotate around the y-axis
    node.runAction(SCNAction.waitForDuration(10), completionHandler: {
        var axis = SCNVector3(x: 0, y: 1, z: 0)
        node.physicsBody?.applyTorque(SCNVector4(x: axis.x, y: axis.y, z: axis.z, w: 1), impulse: true)
    })
    

    第二次旋转有效地使金字塔围绕屏幕的 y 轴旋转,而不是金字塔的 y 轴——穿过金字塔顶点的那个。正如您所指出的,在第一次旋转之前,它正在围绕金字塔的 y 轴旋转;即场景的 y 轴(不受物理影响),而不是表示节点的 y 轴(通过物理旋转)。

    要修复它,请插入以下行(在以 var axis 开头的行之后):

    axis = scene.rootNode.convertPosition(axis, fromNode: node.presentationNode())
    

    convertPosition:fromNode: 的调用说“给我一个场景坐标空间中的向量,它与表示节点空间中的向量等效”。当您围绕转换后的轴施加扭矩时,它会有效地转换回表示节点的空间以模拟物理场,因此您会看到它围绕您想要的轴旋转。


    更新:有一些坐标空间错误,但最终结果几乎相同。

    【讨论】:

    • 非常感谢您的回答。不幸的是对我不起作用。我怀疑这是因为我的对象不在场景的中心,并且没有与世界对齐。我有一个可行的解决方案(已发布),但我认为它不漂亮、准确或优化。
    • 这对我来说就像一个魅力,谢谢! (虽然必须将一些代码更新到 Swift 2,但没什么大不了的。)
    【解决方案2】:

    不幸的是,rickster 提供的解决方案对我不起作用:(

    为了解决这个难题,我创建了(我认为是)一个非常不合标准的解决方案(更多的是概念证明)。它涉及在我试图找到的轴上创建(空)对象,然后我使用它们的位置来找到与轴对齐的向量。

    由于我有一个相当复杂的场景,我从 COLLADA 文件加载它。在该文件中,我模拟了一个简单的坐标三脚架:三个正交圆柱体,顶部有圆锥体(更容易可视化正在发生的事情)。

    然后我将这个三脚架对象约束到我试图对其施加扭矩的对象上。这样我就有了一些对象,这些对象允许我在我试图施加扭矩的对象的presentationNode的轴上检索两个点。然后我可以使用这两个点来确定施加扭矩的矢量。

    // calculate orientation vector in the most unimaginative way possible
    
    // retrieve axis tripod objects. We will be using these as guide objects.
    // The tripod is constructed as a cylinder called "Xaxis" with a cone at the top.
    // All loaded from an external COLLADA file.
    
    SCNNode *XaxisRoot = [scene.rootNode childNodeWithName:@"XAxis" recursively:YES];
    SCNNode *XaxisTip = [XaxisRoot childNodeWithName:@"Cone" recursively:NO];
    
    
    // To devise the vector we will need two points. One is the root of our tripod,
    // the other is at the tip. First, we get their positions. As they are constrained
    // to the _rotatingNode, presentationNode.position is always the same .position
    // because presentationNode returns position in relation to the parent node.
    
    SCNVector3 XaxisRootPos = XaxisRoot.position;
    SCNVector3 XaxisTipPos = XaxisTip.position;
    
    
    // We then convert these two points into _rotatingNode coordinate space. This is
    // the coordinate space applyTorque seems to be using.
    
    XaxisRootPos = [_rotatingNode convertPosition:XaxisRootPos fromNode:_rotatingNode.presentationNode];
    XaxisTipPos = [_rotatingNode convertPosition:XaxisTipPos fromNode:_rotatingNode.presentationNode];
    
    // Now, we have two *points* in _rotatingNode coordinate space. One is at the center
    // of our _rotatingNode, the other is somewhere along it's Xaxis. Subtracting them
    // will give us the *vector* aligned to the x axis of our _rotatingNode
    
    GLKVector3 rawXRotationAxes = GLKVector3Subtract(SCNVector3ToGLKVector3(XaxisRootPos), SCNVector3ToGLKVector3(XaxisTipPos));
    
    // we now normalise this vector
    GLKVector3 normalisedXRotationAxes = GLKVector3Normalize(rawXRotationAxes);
    
    //finally we are able to apply toque reliably
    [_rotatingNode.physicsBody applyTorque:SCNVector4Make(normalisedXRotationAxis.x,normalisedXRotationAxis.y,normalisedXRotationAxis.z, 500) impulse:YES];
    

    您可能会看到,我在 SceneKit 方面经验不足,但即使我可以看到更简单/优化的解决方案确实存在,但我找不到它:(

    【讨论】:

      【解决方案3】:

      我最近遇到了同样的问题,如何将扭矩从对象的局部空间转换为applyTorque 方法所需的世界空间。使用节点的convertPosition:toNodefromNodes 方法的问题在于它们还将节点的平移应用于扭矩,因此这仅在节点位于0,0,0 时有效。这些方法所做的是将 SCNVector3 视为 w 分量为 1.0 的 vec4。我们只想应用旋转,换句话说,我们希望 vec4 的 w 分量为 0。与 SceneKit 不同,GLKit 为我们提供了 2 个选项来说明我们希望 vec3 的倍增方式:

      GLKMatrix4MultiplyVector3 在哪里

      输入向量被视为 w 分量为 0.0 的 4 分量向量。

      GLKMatrix4MultiplyVector3WithTranslation 在哪里

      输入向量被视为 w 分量为 1.0 的 4 分量向量。

      这里我们想要的是前者,只是旋转,而不是平移。

      所以,我们可以往返 GLKit。例如,要将局部 x 轴 (1,0,0)(例如俯仰旋转)转换为施加扭矩所需的全局轴,如下所示:

          let local = GLKMatrix4MultiplyVector3(SCNMatrix4ToGLKMatrix4(node.presentationNode.worldTransform), GLKVector3(v: (1,0,0)))
          node.physicsBody?.applyTorque(SCNVector4(local.x, local.y, local.z, 10), impulse: false)
      

      然而,更 Swiftian 的方法是为 mat4 * vec3 添加一个 * 运算符,它将 vec3 视为具有 0.0 w 分量的 vec4。像这样:

      func * (left: SCNMatrix4, right: SCNVector3) -> SCNVector3 { //multiply mat4 by vec3 as if w is 0.0
          return SCNVector3(
              left.m11 * right.x + left.m21 * right.y + left.m31 * right.z,
              left.m12 * right.x + left.m22 * right.y + left.m32 * right.z,
              left.m13 * right.x + left.m23 * right.y + left.m33 * right.z
          )
      }
      

      虽然这个操作符假设我们希望我们的 vec3s 如何相乘,但我的推理是,由于 convertPosition 方法已经将 w 视为 1,所以有一个 * 操作符也是多余的这个。

      您还可以添加一个 mat4 * SCNVector4 运算符,让用户明确选择他们希望 w 为 0 还是 1。

      因此,我们不必从 SceneKit 到 GLKit 往返,只需编写:

          let local = node.presentationNode.worldTransform * SCNVector3(1,0,0)
          node.physicsBody?.applyTorque(SCNVector4(local.x, local.y, local.z, 10), impulse: false)
      

      您可以使用此方法通过一个applyTorque 调用在多个轴上应用旋转。所以说,如果你有摇杆输入,你希望摇杆上的 x 是偏航(本地 yUp 轴)和摇杆上的 y 是俯仰(本地 x 轴),但具有飞行模拟风格“向下拉回/ up",然后你可以设置为SCNVector3(input.y, -input.x, 0)

      【讨论】:

      • 这正是我所需要的,非常感谢!
      • 虽然请注意,从 iOS 11 开始,SceneKit API 中的这一空白已被填补。现在有一个 SCNNode.convertVector 方法,它将 w 视为 0,而 SCNNode.convertPosition 将 w 视为 1:developer.apple.com/documentation/scenekit/scnnode/…
      • 确实更方便,你是明星:),谢谢!
      • 我没有测试过这个,但它应该类似于let axisInWorldSpace = node.presentationNode.convertVector(SCNVector3(1, 0, 0), to: nil)
      • 再次感谢,它不起作用,直到看到您的评论才知道原因。我使用的是node.convertVector 而不是node.presentationNode.convertVector 现在这是我沿Y 轴旋转对象的方法。 let local = node.presentation.convertVector(SCNVector3(0,1,0), to: nil)node.physicsBody?.applyTorque(SCNVector4(local.x, local.y, local.z, 0.2), asImpulse: false)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-18
      • 1970-01-01
      相关资源
      最近更新 更多