Extending Scnnode in Swift

Extending SCNNode in Swift

You can simply do:

class MyNode : SCNNode {
/* Xcode required this */
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Which will inherit the designated initialiser from the base class.

If you want to specify your own initialiser that takes a geometry, you must call a designated initialiser of the base class, so I'd say this would be what you'd want:

class MyNode : SCNNode {
init(geometry: SCNGeometry) {
super.init()
self.geometry = geometry
}
/* Xcode required this */
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

The initialiser you're seeing in the SCNNode.h code is an initialiser translated (automatically, I believe) from the Objective C method nodeWithGeometry. I believe this is a convenience initialiser, even though it's not specifically marked as such, which I'd personally say is a bug/limitation of the automatic header file translation. The documentation specifically says:

For consistency and simplicity, Objective-C factory methods get mapped as convenience initializers in Swift.

Don't be confused by the word abstract in the comments; this is the HeaderDoc tag for a short description of a method, i.e. "abstract" is used in the sense of an abridgement or summary.

Scenekit: Why SCNNode should not be subclassed?

I personally don't see anything wrong with subclassing SCNNode, depending of course on why you need to do so.

A key consideration here is the following:

If you are adding general purpose functionalities that should be
available to every SCNNode, then make an extension.

All SCNNode instances can then call these new methods.

On the other hand:

If you are adding functionality that should be restricted to special
instances of SCNNode, and you need to identify these specifically: then
make a subclass, since only instances of these can use your new
methods.

If you chose to use an extension of SCNNode, this would mean that any functions that your create could be applied to any SCNNode.

Let's say for example therefore, that you wanted allow any SCNNode to grow and shrink then an extension would be your best bet e.g:

extension SCNNode{

/// Doubles The Size Of The SCNNode & Then Returns It To Its Original Size
func growAndShrink(){

//1. Create An SCNAction Which Will Double The Size Of Our Node
let growAction = SCNAction.scale(by: 2, duration: 5)

//2. Create Another SCNAction Wjich Will Revert Our Node Back To It's Original Size
let shrinkAction = SCNAction.scale(by: 0.5, duration: 5)

//3. Create An Animation Sequence Which Will Store Our Actions
let animationSequence = SCNAction.sequence([growAction, shrinkAction])

//4. Run The Sequence
self.runAction(animationSequence)

}

}

However, if you wanted for example to create an SCNNode which had functions which would only be available to that instance, then creating a subclass may be the way forward.

Let's say then that we needed to create an SCNNode with an SCNPlaneGeometry that provided us specific information about that Node, then we might create a subclass like so:

class PlaneNode: SCNNode {

let DEFAULT_IMAGE: String = "defaultGrid"
let NAME: String = "PlaneNode"
var planeGeometry: SCNPlane
var planeAnchor: ARPlaneAnchor

var widthInfo: String!
var heightInfo: String!
var alignmentInfo: String!

//---------------
//MARK: LifeCycle
//---------------

/// Inititialization
///
/// - Parameters:
/// - anchor: ARPlaneAnchor
/// - node: SCNNode
/// - node: Bool
init(anchor: ARPlaneAnchor, node: SCNNode, image: Bool, identifier: Int, opacity: CGFloat = 0.25){

//1. Create The SCNPlaneGeometry
self.planeAnchor = anchor
self.planeGeometry = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z))
let planeNode = SCNNode(geometry: planeGeometry)

super.init()

//2. If The Image Bool Is True We Use The Default Image From The Assets Bundle
let planeMaterial = SCNMaterial()

if image{

planeMaterial.diffuse.contents = UIImage(named: DEFAULT_IMAGE)

}else{

planeMaterial.diffuse.contents = UIColor.cyan
}

//3. Set The Geometries Contents
self.planeGeometry.materials = [planeMaterial]

//4. Set The Position Of The PlaneNode
planeNode.simdPosition = float3(self.planeAnchor.center.x, 0, self.planeAnchor.center.z)

//5. Rotate It On It's XAxis
planeNode.eulerAngles.x = -.pi / 2

//6. Set The Opacity Of The Node
planeNode.opacity = opacity

//7. Add The PlaneNode
node.addChildNode(planeNode)

//8. Set The Nodes ID
node.name = "\(NAME) \(identifier)"

}

required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }

/// Updates The Size Of The Plane As & When The ARPlaneAnchor Has Been Updated
///
/// - Parameter anchor: ARPlaneAnchor
func update(_ anchor: ARPlaneAnchor) {

self.planeAnchor = anchor

self.planeGeometry.width = CGFloat(anchor.extent.x)
self.planeGeometry.height = CGFloat(anchor.extent.z)

self.position = SCNVector3Make(anchor.center.x, 0.01, anchor.center.z)

returnPlaneInfo()
}

//-----------------------
//MARK: Plane Information
//-----------------------

/// Returns The Size Of The ARPlaneAnchor & Its Alignment
func returnPlaneInfo(){

let widthOfPlane = self.planeAnchor.extent.x
let heightOfPlane = self.planeAnchor.extent.z

var planeAlignment: String!

switch planeAnchor.alignment {

case .horizontal:
planeAlignment = "Horizontal"
case .vertical:
planeAlignment = "Vertical"
}

#if DEBUG
print("""
Width Of Plane = \(String(format: "%.2fm", widthOfPlane))
Height Of Plane = \(String(format: "%.2fm", heightOfPlane))
Plane Alignment = \(planeAlignment)
""")
#endif

self.widthInfo = String(format: "%.2fm", widthOfPlane)
self.heightInfo = String(format: "%.2fm", heightOfPlane)
self.alignmentInfo = planeAlignment
}

}

It seems, in your case, that since you plan to have very specific instances e.g. trucks, airplanes etc, each with their own specific functions, then using an SCNNode subclass could be the way forward.

Hope it helps...

Update: As per you request e.g. How would this work in the case of using an .scn file?

Some pseudo code might look like so:

/// Creates & Manages The Car Model
class Car: SCNNode {

let MODEL_SCALE = SCNVector3(0.5, 0.5, 0.5)
let MODEL_POSITION = SCNVector3(1, 0, -2.5)
let MODEL_ROTATION: CGFloat = 30.45
let TURN_DURATION: Double = 1

var leftFrontWheel: SCNNode!
var rightFrontWheel: SCNNode!
var leftBackWheel: SCNNode!
var rightBackWheel: SCNNode!

//--------------------
//MARK: Initialization
//--------------------

override init() {

super.init()

//1. Get The Car Model From The Assetts Bundle
guard let carModel = SCNScene(named: "StackOverflow.scnassets/Models/Car.scn"),
let modelNode = carModel.rootNode.childNode(withName: "Root", recursively: false),
let frontLeftWheel = modelNode.childNode(withName: "leftFront", recursively: false),
let frontRightWheel = modelNode.childNode(withName: "rightFront", recursively: false),
let rearLeftWheel = modelNode.childNode(withName: "leftRear", recursively: false),
let rearRightWheel = modelNode.childNode(withName: "rightRear", recursively: false) else { return }

//2. Scale, Rotate & Position The Car
self.scale = MODEL_SCALE
self.simdRotation = simd_float4 (0, 1, 0, Float(MODEL_ROTATION.degreesToRadians))
self.position = MODEL_POSITION

//2. Create A Reference To Each Wheel
self.leftFrontWheel = frontLeftWheel
self.rightFrontWheel = frontRightWheel
self.leftBackWheel = rearLeftWheel
self.rightBackWheel = rearRightWheel

//3. Add The Car To The Root Node
self.addChildNode(modelNode)

print("""
Loaded Car Model
Scale = \(MODEL_SCALE)
Rotation = \(MODEL_ROTATION)
Position = \(MODEL_POSITION)
""")

}

required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }

//---------------
//MARK: Animation
//---------------

/// Runs The Wheel Animation
func animateWheels(){

let wheelTurnAnimationOut = SCNAction.rotate(toAxisAngle: SCNVector4(0 , 0 , 1, CGFloat(45).degreesToRadians), duration: TURN_DURATION)
let wheelTurnAnimationIn = SCNAction.rotate(toAxisAngle: SCNVector4(0 , 0 , 1, CGFloat(0).degreesToRadians), duration: TURN_DURATION)
let turningSequence = SCNAction.sequence([wheelTurnAnimationOut, wheelTurnAnimationIn])
let turningAction = SCNAction.repeatForever(turningSequence)
leftFrontWheel.runAction(turningAction)
rightFrontWheel.runAction(turningAction)
leftBackWheel.runAction(turningAction)
rightBackWheel.runAction(turningAction)

}

}

Which you could then initialise and manage the functions like so:

 let car = Car()
self.augmentedRealityView.scene.rootNode.addChildNode(car)
car.animateWheels()

Hope it helps...

How to move a rotated SCNNode in SceneKit, on its own axis?

So my understanding is that you want to move the Box Node along its own X axis (not it's parents X axis). And because the Box Node is rotated, its X axis is not aligned with its parent's one, so you have the problem to convert the translation between the two coordinate systems.

The node hierarchy is

parentNode
|
|----boxNode // rotated around Y (vertical) axis

Using Transformation Matrices

To move boxNode along its own X axis

// First let's get the current boxNode transformation matrix
SCNMatrix4 boxTransform = boxNode.transform;

// Let's make a new matrix for translation +2 along X axis
SCNMatrix4 xTranslation = SCNMatrix4MakeTranslation(2, 0, 0);

// Combine the two matrices, THE ORDER MATTERS !
// if you swap the parameters you will move it in parent's coord system
SCNMatrix4 newTransform = SCNMatrix4Mult(xTranslation, boxTransform);

// Allply the newly generated transform
boxNode.transform = newTransform;

Please Note: The order matters when multiplying matrices

Another option:

Using SCNNode coordinate conversion functions, looks more straight forward to me

// Get the boxNode current position in parent's coord system
SCNVector3 positionInParent = boxNode.position;

// Convert that coordinate to boxNode's own coord system
SCNVector3 positionInSelf = [boxNode convertPosition:positionInParent fromNode:parentNode];

// Translate along own X axis by +2 points
positionInSelf = SCNVector3Make(positionInSelf.x + 2,
positionInSelf.y,
positionInSelf.z);

// Convert that back to parent's coord system
positionInParent = [parentNode convertPosition: positionInSelf fromNode:boxNode];

// Apply the new position
boxNode.position = positionInParent;


Related Topics



Leave a reply



Submit