Created
August 21, 2021 17:32
-
-
Save Farini/ede665cab4e736480d4f399fbb4ca4f3 to your computer and use it in GitHub Desktop.
SceneKit node follow NURBS path.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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