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 my gist here
Things to consider
Move.: You need the points in space [SCNVector]
Point.: One object to move ahead, so the original object (ship) can follow, respecting the orientation of the path.
The boxes are just for illustration of the path. They can be removed
See how to export NURBS in RoutePath
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
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
"""
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With