沿路径的 SceneKit 动画节点

SceneKit animate node along path

我有一个盒子节点

_boxNode = [SCNNode node];
_boxNode.geometry = [SCNBox boxWithWidth:1 height:1 length:1 chamferRadius:0];
_boxNode.position = SCNVector3Make(0, 0, -2);
[scene.rootNode addChildNode:_boxNode];

我有路

CGPathRef path = CGPathCreateWithEllipseInRect(CGRectMake(-2, -2, 4, 4), nil);

我想让我的盒子沿着我的路径走一次。

如何在 SceneKit 中执行此操作?

我想制作一个看起来像

的方法
[_boxNode runAction:[SCNAction moveAlongPath:path forDuration:duration]];

我也遇到了这个问题,我写了一个小游乐场。动画效果很好。需要做一件事。必须计算每个点之间的距离,以便可以缩放时间以获得流畅的动画。只需将代码复制并粘贴到操场上即可。代码在Swift 3.

这是我的解决方案(BezierPath 扩展不是我的,在这里找到的):

import UIKit
import SceneKit
import PlaygroundSupport

let animationDuration = 0.1

public extension UIBezierPath {

    var elements: [PathElement] {
        var pathElements = [PathElement]()
        withUnsafeMutablePointer(to: &pathElements) { elementsPointer in
            cgPath.apply(info: elementsPointer) { (userInfo, nextElementPointer) in
                let nextElement = PathElement(element: nextElementPointer.pointee)
                let elementsPointer = userInfo!.assumingMemoryBound(to: [PathElement].self)
                elementsPointer.pointee.append(nextElement)
            }
        }
        return pathElements
    }
}

public enum PathElement {

    case moveToPoint(CGPoint)
    case addLineToPoint(CGPoint)
    case addQuadCurveToPoint(CGPoint, CGPoint)
    case addCurveToPoint(CGPoint, CGPoint, CGPoint)
    case closeSubpath

    init(element: CGPathElement) {
        switch element.type {
        case .moveToPoint: self = .moveToPoint(element.points[0])
        case .addLineToPoint: self = .addLineToPoint(element.points[0])
        case .addQuadCurveToPoint: self = .addQuadCurveToPoint(element.points[0], element.points[1])
        case .addCurveToPoint: self = .addCurveToPoint(element.points[0], element.points[1], element.points[2])
        case .closeSubpath: self = .closeSubpath
        }
    }
}

public extension SCNAction {

    class func moveAlong(path: UIBezierPath) -> SCNAction {

        let points = path.elements
        var actions = [SCNAction]()

        for point in points {

            switch point {
            case .moveToPoint(let a):
                let moveAction = SCNAction.move(to: SCNVector3(a.x, a.y, 0), duration: animationDuration)
                actions.append(moveAction)
                break

            case .addCurveToPoint(let a, let b, let c):
                let moveAction1 = SCNAction.move(to: SCNVector3(a.x, a.y, 0), duration: animationDuration)
                let moveAction2 = SCNAction.move(to: SCNVector3(b.x, b.y, 0), duration: animationDuration)
                let moveAction3 = SCNAction.move(to: SCNVector3(c.x, c.y, 0), duration: animationDuration)
                actions.append(moveAction1)
                actions.append(moveAction2)
                actions.append(moveAction3)
                break

            case .addLineToPoint(let a):
                let moveAction = SCNAction.move(to: SCNVector3(a.x, a.y, 0), duration: animationDuration)
                actions.append(moveAction)
                break

            case .addQuadCurveToPoint(let a, let b):
                let moveAction1 = SCNAction.move(to: SCNVector3(a.x, a.y, 0), duration: animationDuration)
                let moveAction2 = SCNAction.move(to: SCNVector3(b.x, b.y, 0), duration: animationDuration)
                actions.append(moveAction1)
                actions.append(moveAction2)
                break

            default:
                let moveAction = SCNAction.move(to: SCNVector3(0, 0, 0), duration: animationDuration)
                actions.append(moveAction)
                break
            }   
        }
        return SCNAction.sequence(actions)
    }
}



let scnView = SCNView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
scnView.autoenablesDefaultLighting = true

let scene = SCNScene()
scnView.scene = scene

let light = SCNLight()
light.type = .ambient
let lightNode = SCNNode()
lightNode.light = light
scene.rootNode.addChildNode(lightNode)

let camera = SCNCamera()
let cameraNode = SCNNode()
cameraNode.camera = camera
cameraNode.position = SCNVector3(0,0,10)
scene.rootNode.addChildNode(cameraNode)

let box = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0)
let boxNode = SCNNode(geometry: box)
boxNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red

scene.rootNode.addChildNode(boxNode)

let path1 = UIBezierPath(roundedRect: CGRect(x: 1, y: 1, width: 2, height: 2), cornerRadius: 1)

let moveAction = SCNAction.moveAlong(path: path1)
let repeatAction = SCNAction.repeatForever(moveAction)
SCNTransaction.begin()
SCNTransaction.animationDuration = Double(path1.elements.count) * animationDuration
boxNode.runAction(repeatAction)
SCNTransaction.commit()

PlaygroundPage.current.liveView = scnView

这里我做了一个关于如何在搅拌机中创建 NURBS 路径的快速教程,然后有一个对象跟随它(在本例中是 Xcode 中默认新项目代码附带的船。

你也可以在我的gist here

下找到这个

需要考虑的事项

  1. 移动。: 你需要space [SCNVector]

    中的点
  2. 点。:一个物体向前移动,因此原始物体(船)可以跟随,尊重路径的方向。

  3. 方框只是为了说明路径。它们可以被移除

  4. RoutePath

    中查看如何导出 NURBS
  5. 这是一个XCode项目,当你开始一个新项目时出现 -> 游戏 -> Swift -> SceneKit

    override func viewDidLoad() {
    
        super.viewDidLoad()
    
        // create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
    
        // create and add a camera to the scene
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
    
        // place the camera
        cameraNode.position = SCNVector3(x: 0, y: 0, z: -10)
    
        // create and add a light to the scene
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)
    
        // create and add an ambient light to the scene
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = NSColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)
    
        // MARK: - Path (Orientation)
    
        // Orientation node: Ahead of the ship, the orientation node is used to
        // maintain the ship's orientation (rotating the ship according to path's next point)
        let orientationNode = SCNNode()
        scene.rootNode.addChildNode(orientationNode)
    
        // MARK: - Path (Ship)
    
        // retrieve the ship node
        let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
        ship.scale = SCNVector3(0.15, 0.15, 0.15)
    
        // Get the path you want to follow
        var pathToFollow:[SCNVector3] = RoutePath.decodePath()
    
        // Set the ship to start at the path's first point
        ship.position = pathToFollow.first!
    
        // Constraint ship to look at orientationNode
        let shipLook = SCNLookAtConstraint(target: orientationNode)
        shipLook.localFront = SCNVector3(0, 0, 1)
        shipLook.worldUp = SCNVector3(0, 1, 0)
        shipLook.isGimbalLockEnabled = true
        ship.constraints = [shipLook]
    
        // Camera Constraints (Following ship)
        let look = SCNLookAtConstraint(target: ship)
        let follow = SCNDistanceConstraint(target: ship)
        follow.minimumDistance = 3
        follow.maximumDistance = 6
        cameraNode.constraints = [look, follow]
    
        // MARK: - Actions
    
        // Ship's actions
        var shipActions:[SCNAction] = []
    
        // Actions for the orientation node
        var orientationActions:[SCNAction] = []
    
        // Populate Path Animations
        while !pathToFollow.isEmpty {
    
            pathToFollow.remove(at: 0)
            if let next = pathToFollow.first {
    
                let act = SCNAction.move(to: next, duration: 0.8)
    
                if pathToFollow.count > 1 {
                    let dest = pathToFollow[1]
                    let oriact = SCNAction.move(to: dest, duration: 0.8)
                    orientationActions.append(oriact)
                }
    
                shipActions.append(act)
    
                // add box
                let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
                let boxNode = SCNNode(geometry: box)
                boxNode.geometry?.materials.first?.diffuse.contents = NSColor.blue
                boxNode.position = SCNVector3(Double(next.x), Double(next.y + 0.4), Double(next.z))
                scene.rootNode.addChildNode(boxNode)
            }
        }
    
        // Animate Orientation node
        let oriSequence = SCNAction.sequence(orientationActions)
        orientationNode.runAction(oriSequence)
    
        // Animate Ship node
        let sequence = SCNAction.sequence(shipActions)
        ship.runAction(sequence) {
            print("Ship finished sequence")
        }
    
        // MARK: - View Setup
    
        // retrieve the SCNView
        let scnView = self.view as! SCNView
    
        // set the scene to the view
        scnView.scene = scene
    
        // show statistics such as fps and timing information
        scnView.showsStatistics = true
    
        // configure the view
        scnView.backgroundColor = NSColor.black
    
    }
    

要遵循的路径对象:

这条路径是用搅拌机制作的,带有 Nurbs 路径。然后导出为 .obj 文件。

选项 - 重要 导出时,勾选如下选项

  1. 'curves as NURBS'
  2. 'keep vertex order'

在文本编辑器中打开 .obj 文件并复制顶点位置,如您在 rawPath String

中所见
struct RoutePath {

/// Transforms the `rawPath` into an array of `SCNVector3`
static func decodePath() -> [SCNVector3] {
    
    let whole = rawPath.components(separatedBy: "\n")
    print("\nWhole:\n\(whole.count)")
    
    var vectors:[SCNVector3] = []
    
    for line in whole {
        
        let vectorParts = line.components(separatedBy: " ")
        if let x = Double(vectorParts[1]),
           let y = Double(vectorParts[2]),
           let z = Double(vectorParts[3]) {
            
            let vector = SCNVector3(x, y, z)
            print("Vector: \(vector)")
            
            vectors.append(vector)
        }
    }
    
    return vectors
}

static var rawPath:String {
    """
    v 26.893915 -4.884228 49.957905
    v 26.893915 -4.884228 48.957905
    v 26.893915 -4.884228 47.957905
    v 26.901930 -4.884228 46.617016
    v 26.901930 -4.884228 45.617016
    v 26.901930 -4.884228 44.617016
    v 26.901930 -4.884228 43.617016
    v 26.901930 -4.884228 42.617016
    v 26.901930 -4.884228 41.617016
    v 26.901930 -4.884228 40.617016
    v 26.901930 -4.884228 39.617016
    v 26.391232 -4.884228 38.617016
    v 25.574114 -4.884228 37.617016
    v 25.046391 -4.884228 36.617016
    v 24.552715 -4.884228 35.617016
    v 24.365459 -4.884228 34.617016
    v 24.365459 -4.884228 33.617016
    v 24.314390 -4.884228 32.617016
    v 24.212250 -4.884228 31.617016
    v 24.110109 -4.884228 30.617016
    v 23.995176 -4.884228 29.617016
    v 23.913080 -4.884228 28.617016
    v 23.814566 -4.884228 27.617016
    v 24.356396 -4.884228 26.978235
    v 25.356396 -4.884228 26.978235
    v 26.356396 -4.884228 26.978235
    v 27.356396 -4.736906 26.978235
    v 28.356396 -4.549107 26.978235
    v 29.356396 -4.549107 26.978235
    """
    }
}