简体   繁体   中英

SceneKit animate node along path

I have a box node

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

I have a path

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

I want to have my box travel along my path once.

How do I do this in SceneKit?

I'd like to make a method that would look like

[_boxNode runAction:[SCNAction moveAlongPath:path forDuration:duration]];

I came across that question as well and I wrote a little playground. The animation works well. One thing needs to be done. The distance between every point has to be calculated so that time can be scaled to get a smooth animation. Just copy & paste the code into a playground. The code is in Swift 3.

Here is my solution (the BezierPath extension is not from me, found it here):

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

Here I made a quick tutorial on how to create a NURBS path in blender, and then have an object to follow it (in this case the ship that comes with the default new project code in Xcode.

You can also find this under mygist here

Things to consider

  1. Move.: You need the points in space [SCNVector]

  2. Point.: One object to move ahead, so the original object (ship) can follow, respecting the orientation of the path.

  3. The boxes are just for illustration of the path. They can be removed

  4. See how to export NURBS in RoutePath

  5. This is an XCode project that comes when you start a new project -> game -> 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 }

For the Path Object to follow:

This path was made in blender, with a Nurbs Path. Then it was exported as .obj file.

OPTIONS - IMPORTANT When exporting, mark the following options

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

Open the .obj file in text editor and copy the vertex positions, as you see in the 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
    """
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM