Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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]];
like image 797
Konrad Wright Avatar asked May 05 '15 23:05

Konrad Wright


2 Answers

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
like image 92
Nico S. Avatar answered Nov 11 '22 12:11

Nico S.


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 my gist 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
    """
    }
}
like image 1
Farini Avatar answered Nov 11 '22 14:11

Farini