SceneKit – Rotate and animate a SCNNode
Solution 1
Trying to both manually set and animate the same property can cause issues. Using a byValue
animation makes the problem worse -- that concatenates to the current transform, so it's harder to keep track of whether the current transform is what the animation expects to start with.
Instead, separate the fixed orientation of the pyramid (its apex is in the -z direction) from the animation (it spins around the axis it points in). There's two good ways to do this:
Make
pyramidNode
the child of another node that gets the one-time rotation (π/2 around x-axis), and apply the spin animation directly topyramidNode
. (In this case, the apex of the pyramid will still point in the +y direction of its local space, so you'll want to spin around that axis instead of the z-axis.)Use the
pivot
property to transform the local space ofpyramidNode
's contents, and animatepyramidNode
relative to its containing space.
Here's some code to show the second approach:
let pyramid = SCNPyramid(width: 1.0, height: 1.0, length: 1.0)
let pyramidNode = SCNNode(geometry: pyramid)
pyramidNode.position = SCNVector3(x: 0, y: 0, z: 0)
// Point the pyramid in the -z direction
pyramidNode.pivot = SCNMatrix4MakeRotation(CGFloat(M_PI_2), 1, 0, 0)
scene.rootNode.addChildNode(pyramidNode)
let spin = CABasicAnimation(keyPath: "rotation")
// Use from-to to explicitly make a full rotation around z
spin.fromValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: 0))
spin.toValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: CGFloat(2 * M_PI)))
spin.duration = 3
spin.repeatCount = .infinity
pyramidNode.addAnimation(spin, forKey: "spin around")
Some unrelated changes to improve code quality:
- Use
CGFloat
when explicit conversion is required to initialize anSCNVector
component; usingFloat
orDouble
specifically will break on 32 or 64 bit architecture. - Use
.infinity
instead of the legacy BSD math constantHUGE
. This type-infers to whatever the type ofspin.repeatCount
is, and uses a constant value that's defined for all floating-point types. - Use
M_PI_2
for π/2 to be pedantic about precision. - Use
let
instead ofvar
for the animation, since we never assign a different value tospin
.
More on the CGFloat
error business: In Swift, numeric literals have no type until the expression they're in needs one. That's why you can do things like spin.duration = 3
-- even though duration
is a floating-point value, Swift lets you pass an "integer literal". But if you do let d = 3; spin.duration = d
you get an error. Why? Because variables/constants have explicit types, and Swift doesn't do implicit type conversion. The 3
is typeless, but when it gets assigned to d
, type inference defaults to choosing Int
because you haven't specified anything else.
If you're seeing type conversion errors, you probably have code that mixes literals, constants, and/or values returned from functions. You can probably just make the errors go away by converting everything in the expression to CGFloat
(or whatever the type you're passing that expression to is). Of course, that'll make your code unreadable and ugly, so once you get it working you might start removing conversions one at a time until you find the one that does the job.
Solution 2
SceneKit includes animation helpers which are much simpler & shorter to use than CAAnimations. This is ObjC but gets across the point:
[pyramidNode runAction:
[SCNAction repeatActionForever:
[SCNAction rotateByX:0 y:0 z:2*M_PI duration:3]]];
Newalp
Updated on July 09, 2022Comments
-
Newalp almost 2 years
I'm trying to display a pyramid that points following the z axis and then rotates on itself around z too. As my camera is on the z axis, I'm expecting to see the pyramid from above. I managed to rotate the pyramid to see it this way but when I add the animation it seems to rotate on multiple axis.
Here is my code:
// The following create the pyramid and place it how I want let pyramid = SCNPyramid(width: 1.0, height: 1.0, length: 1.0) let pyramidNode = SCNNode(geometry: pyramid) pyramidNode.position = SCNVector3(x: 0, y: 0, z: 0) pyramidNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: Float(M_PI / 2)) scene.rootNode.addChildNode(pyramidNode) // But the animation seems to rotate aroun 2 axis and not just z var spin = CABasicAnimation(keyPath: "rotation") spin.byValue = NSValue(SCNVector4: SCNVector4(x: 0, y: 0, z: 1, w: 2*Float(M_PI))) spin.duration = 3 spin.repeatCount = HUGE pyramidNode.addAnimation(spin, forKey: "spin around")