Skip to content

Instantly share code, notes, and snippets.

@Farini
Created August 21, 2021 17:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Farini/ede665cab4e736480d4f399fbb4ca4f3 to your computer and use it in GitHub Desktop.
Save Farini/ede665cab4e736480d4f399fbb4ca4f3 to your computer and use it in GitHub Desktop.
SceneKit node follow NURBS path.
// Pathfinder
// Created by Carlos Farini on 8/21/21.
/**
This is a tutorial that tries to approach the following question:
How to make a `SCNNode` follow a 3D Path (with orientation) created with Blender NURBS curve.
A: Simple. Move and Point to the next object.
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
*/
import SceneKit
class GameViewController: NSViewController {
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
}
}
/**
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
"""
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment