Animate rotation node in Scenekit using CAKeyframeAnimation - swift

in order to study ARKit and SceneKit I'm writing a simple app which should do 2 things:
place an airplane in a 3d world
animate this plane in order to make this plane fly in a circular way (I'll be happy if I can make fly this airplane in a square path).
Now, I'm stuck on the second point, animate the plane.
I tried different approaches but not successful.
First try: Animate using "sequanceAction", but as you can you can see from the following code the plane first move to X that rotate(Yaxes) and than move in Z (the animation is very ugly).
func animataAirplane(nodeToAnimate: SCNNode){
let moveinx = SCNAction.move(to: SCNVector3(nodeToAnimate.position.x + 1,nodeToAnimate.position.y,nodeToAnimate.position.z), duration: 2)
let rotateRight = SCNAction.rotate(by: deg2rad(-90), around: SCNVector3(0, 1, 0), duration: 1)
let moveinz = SCNAction.move(to: SCNVector3(nodeToAnimate.position.x + 1,nodeToAnimate.position.y,nodeToAnimate.position.z + 1), duration: 2)
let sequanceAction = SCNAction.sequence([moveinx, rotateRight, moveinz])
nodeToAnimate.runAction(sequenceAction)
}
what I want is overly the animations, almost at the end of the moveX action start the rotation action and moveinZ, in order to give the illusion of a real plane turn.
Is that possible? I'm looking for someone point me in the right direction what I should look .
My second approach was try "CAKeyframeAnimation":
But I can't find exactly how to combine this animation:
func animatePlaneKey(nodeToAnimate: SCNNode){
let pos = nodeToAnimate.position
let animation = CAKeyframeAnimation(keyPath: "position")
let pos1 = SCNVector3(pos.x, pos.y, pos.z)
let pos2 = SCNVector3(pos.x + 1 , pos.y, pos.z)
let pos3 = SCNVector3(pos.x + 1 , pos.y, pos.z + 1)
animation.values = [pos1,pos2, pos3]
animation.keyTimes = [0,0.5,1]
animation.calculationMode = .linear
animation.duration = 10
animation.repeatCount = 1
animation.isAdditive = true
let animation2 = CAKeyframeAnimation(keyPath: "rotation")
let pos1rot = SCNVector3(nodeToAnimate.eulerAngles.x, nodeToAnimate.eulerAngles.y, nodeToAnimate.eulerAngles.z)
let pos2rot = SCNVector3(nodeToAnimate.eulerAngles.x + Float(deg2rad(90)), nodeToAnimate.eulerAngles.y + Float(deg2rad(90)), nodeToAnimate.eulerAngles.z + Float(deg2rad(90)))
animation2.values = [pos1rot,pos2rot]
animation2.keyTimes = [0,1]
animation2.duration = 2
animation2.repeatCount = 1
animation2.isAdditive = true
nodeToAnimate.addAnimation(animation, forKey: "position")
nodeToAnimate.addAnimation(animation2, forKey: "rotation")
}
Will be happy if someone can point me in the right direction in order to simulate this fly animation.
Thanks a lot.

Here's a working version of your code:
func animatePlaneKey(nodeToAnimate: SCNNode) {
let pos = nodeToAnimate.position
let animation = CAKeyframeAnimation(keyPath: "position")
let pos1 = SCNVector3(pos.x, pos.y, pos.z)
let pos2 = SCNVector3(pos.x + 1 , pos.y, pos.z)
let pos3 = SCNVector3(pos.x + 1 , pos.y, pos.z + 1)
animation.values = [pos1,pos2, pos3]
animation.keyTimes = [0,0.5,1]
animation.calculationMode = .linear
animation.duration = 10
animation.repeatCount = 1
animation.isAdditive = true
let animation2 = CAKeyframeAnimation(keyPath: "rotation")
let pos1rot = SCNVector4(0, 0, 0, 0)
let pos2rot = SCNVector4(0, 1, 0, CGFloat(Float.pi/2))
animation2.values = [pos1rot, pos2rot]
animation2.keyTimes = [0, 1]
animation2.duration = 20
animation2.repeatCount = 1
animation2.isAdditive = true
nodeToAnimate.addAnimation(animation, forKey: "position")
nodeToAnimate.addAnimation(animation2, forKey: "spin around")
}

Related

Why does my 3D extruded UIBezierPath have duplicates?

I have a series of UIBezierPath's that I've turned into 3D objects in SceneKit. They form a long, jagged line with occasional gaps (a gap separates one object from another).
Here's what that looks like:
The problem: As I move the camera around, the material on the side of the objects flickers strangely and changes colors.
I believe what's happening is that my path-drawing code is wrong, and is somehow creating duplicate objects positioned inside the existing object(s). So, I think the flickering color is really the other, duplicate object showing through.
Here's what the flickering color showing through looks like:
To see the problem in action, the following code can be pasted directly into a new Game template Xcode project using SceneKit. You may paste it at the end of GameViewController's viewDidLoad.
var previousBezierPathPoint: CGPoint = CGPoint.zero
let numOfPointsPerLine: Int = 8
var hugePath = UIBezierPath()
var wasGap: Bool = false
let gapWidth: CGFloat = 10.0
var currentZdepth: CGFloat = CGFloat.random(in: 5.0...30.0)
for i in 0..<14 {
let pp = wasGap ? CGPoint(x: CGFloat(previousBezierPathPoint.x) + gapWidth, y: CGFloat(previousBezierPathPoint.y)) : CGPoint(x: CGFloat(previousBezierPathPoint.x), y: CGFloat(previousBezierPathPoint.y))
let isGap: Bool = i > 1 && Float.random(in: 0...100) > 60.0
if !isGap {
if wasGap || i == 0 {
hugePath = UIBezierPath()
hugePath.move(to: pp)
currentZdepth = CGFloat.random(in: 5.0...30.0)
}
for j in 1..<numOfPointsPerLine {
let point = CGPoint(x: pp.x + (CGFloat(j)*25.0), y: pp.y + CGFloat.random(in: -8.0...8.0))
hugePath.addLine(to: point)
previousBezierPathPoint = point
}
let pathRef = hugePath.cgPath.copy(strokingWithWidth: 10.0, lineCap: CGLineCap.butt, lineJoin: CGLineJoin.miter, miterLimit: 1.0)
let newPath = UIBezierPath(cgPath: pathRef.normalized())
let hugeShape = SCNShape(path: newPath, extrusionDepth: currentZdepth)
let colors = [
UIColor.green,
UIColor.yellow,
UIColor.purple,
UIColor.gray,
UIColor.darkGray
]
let tempMat = SCNMaterial()
tempMat.diffuse.contents = colors[Int.random(in: 0..<colors.count)]
let frontMat = SCNMaterial()
frontMat.diffuse.contents = UIColor.red
hugeShape.materials = [frontMat, tempMat, tempMat, tempMat, tempMat, tempMat]
let hugeNode = SCNNode()
hugeNode.geometry = hugeShape
hugeNode.position.x = 0.0
hugeNode.position.z = Float(-50.0 + (currentZdepth*0.5))
hugeNode.position.y = 0.0
scnView.scene?.rootNode.addChildNode(hugeNode)
} else {
if hugePath.isEmpty == false {
hugePath.close()
}
}
wasGap = isGap
}
Please note that this question is related to this one.
Question: How can I change my code so that there are no duplicate objects?
It looks like you are continually building your path each time through the loop...
Not entirely sure if this will give you your desired results, but...
Instead of this:
if wasGap || i == 0 {
hugePath = UIBezierPath()
hugePath.move(to: pp)
currentZdepth = CGFloat.random(in: 5.0...30.0)
}
try this:
hugePath = UIBezierPath()
hugePath.move(to: pp)
if wasGap || i == 0 {
currentZdepth = CGFloat.random(in: 5.0...30.0)
}

SCNPhysicsShape don't allowed my car to move when apply engine force

I finally manage to create a driving car in SceneKit, using SCNPhysicsVehicle.
The following code is working fine... my car is dropped in the scene, I can apply engine force and I can steer the wheels and drive the car..
but once I activate body.physicsShape = SCNPhysicsShape(node: chassie), the car don't move anymore.
func setupVeicles(){
let chassie = loadAssetWithName(nameFile: "rc_car", nameNode: "rccarBody", type: "scn", scale: SCNVector3(1, 1, 1))
chassie.position = SCNVector3(0, 1, 0)
let body = SCNPhysicsBody.dynamic()
// WHY THIS LINE OF CODE BLOCK MY CAR TO MOVE
// body.physicsShape = SCNPhysicsShape(node: chassie)
body.categoryBitMask = BodyType.car.rawValue
body.collisionBitMask = BodyType.floor.rawValue
body.contactTestBitMask = BodyType.floor.rawValue
body.allowsResting = false
body.mass = 1
body.restitution = 0.1
body.friction = 0.5
body.rollingFriction = 0
chassie.physicsBody = body
// Load the wheel
let wheelFL = chassie.childNode(withName: "wheelLocator_FL", recursively: true)!
let wheelFR = chassie.childNode(withName: "wheelLocator_FR", recursively: true)!
let wheelBL = chassie.childNode(withName: "wheelLocator_RL", recursively: true)!
let wheelBR = chassie.childNode(withName: "wheelLocator_RR", recursively: true)!
let phywheelFL = createPhysicsVehicleWheel(wheelNode: wheelFL, position: SCNVector3(3, 1.31, 2.753))
let phywheelFR = createPhysicsVehicleWheel(wheelNode: wheelFR, position: SCNVector3(-3, 1.271, 2.753))
let phywheelBL = createPhysicsVehicleWheel(wheelNode: wheelBL, position: SCNVector3(4, 1.31, -3.566))
let phywheelBR = createPhysicsVehicleWheel(wheelNode: wheelBR, position: SCNVector3(-4, 1.31, -3.566))
physicsVehicle = SCNPhysicsVehicle(chassisBody: chassie.physicsBody!, wheels: [phywheelFL,phywheelFR,phywheelBL,phywheelBR])
self.scene.physicsWorld.addBehavior(physicsVehicle)
self.scene.rootNode.addChildNode(chassie)
}
What I don't understand is why if I activate SCNPhysicsShape the car doesn't move anymore..
This in my mind will allow me to simulate better Physics.
My floor is set like this:
func makeFloor()->SCNNode{
let geo = SCNFloor()
let matFloor = SCNMaterial()
matFloor.blendMode = .multiply
matFloor.diffuse.wrapS = .repeat
matFloor.diffuse.wrapT = .repeat
matFloor.diffuse.contents = UIImage(named: "concrete.png")
geo.materials = [matFloor]
let floor = SCNNode(geometry: geo)
let body = SCNPhysicsBody.static()
body.categoryBitMask = BodyType.floor.rawValue
body.allowsResting = false
body.restitution = 0.1
body.friction = 0.5
body.rollingFriction = 0
floor.physicsBody = body
floor.name = "floor"
return floor
}
If I deactivate SCNPhysicsShape the wheel are into the ground .. looks unrealistic..
With SCNPhysicsShape(node: chassie) applied:
without SCNPhysicsShape(node: chassie):

SCNNode rotation around a generic axis with animation

I need to rotate the gray bar around the red dot (see picture below).
I managed to do that translating the center point of the bar, rotating the bar and then translating the bar back to the original position.
Down here my code:
struct TestView2: View {
var scene: SCNScene
var arm: SCNNode
var length = 16.0
var with = 1.0
init() {
scene = SCNScene()
scene.setAttribute(400000, forKey: SCNScene.Attribute.endTime.rawValue)
let rootNode = scene.rootNode
let armShape = SCNBox(width: with, height: 0.5, length: length, chamferRadius: 0)
armShape.firstMaterial?.diffuse.contents = NSColor.gray
arm = SCNNode()
arm.geometry = armShape
arm.position = SCNVector3(0, 0, 0)
rootNode.addChildNode(arm)
let axisShape = SCNCylinder(radius: with/2, height: 0.51)
axisShape.firstMaterial?.diffuse.contents = NSColor.red
let axis = SCNNode()
axis.geometry = axisShape
axis.position = SCNVector3(0, 0, length/2-with/2)
rootNode.addChildNode(axis)
// Camera
let camera = SCNNode()
camera.camera = SCNCamera()
camera.camera?.usesOrthographicProjection = true
camera.camera?.orthographicScale = 15
camera.camera?.zNear = 0
camera.camera?.zFar = 100
camera.position = SCNVector3(0, 0, 50)
let cameraOrbit = SCNNode()
cameraOrbit.addChildNode(camera)
rootNode.addChildNode(cameraOrbit)
var eulerAngles = cameraOrbit.eulerAngles
eulerAngles.x = CGFloat(Float.pi / 2.0)
eulerAngles.y = 0
eulerAngles.z = 0
cameraOrbit.eulerAngles = eulerAngles
}
var body: some View {
VStack {
SceneView(scene: scene, options: [.allowsCameraControl, .autoenablesDefaultLighting])
Button("Rotate") {
let currentTransform = arm.transform
let translationTransform = SCNMatrix4MakeTranslation(0, 0, length/2-with/2)
let rotationTrasform = SCNMatrix4MakeRotation(90 * .pi / 180.0, 0, 1, 0)
let backTranslationTransform = SCNMatrix4MakeTranslation(0, 0, -(length/2-with/2))
let newTransform = SCNMatrix4Mult(backTranslationTransform,
SCNMatrix4Mult(rotationTrasform,
SCNMatrix4Mult(translationTransform, currentTransform)))
arm.transform = newTransform
}
}
}
}
The bar rotate as expected. I then added an animation:
...
let animation = CABasicAnimation(keyPath: "transform")
let currentTransform = arm.transform
let translationTransform = SCNMatrix4MakeTranslation(0, 0, length/2-with/2)
let rotationTrasform = SCNMatrix4MakeRotation(90 * .pi / 180.0, 0, 1, 0)
let backTranslationTransform = SCNMatrix4MakeTranslation(0, 0, -(length/2-with/2))
let newTransform = SCNMatrix4Mult(backTranslationTransform,
SCNMatrix4Mult(rotationTrasform,
SCNMatrix4Mult(translationTransform, currentTransform)))
animation.fromValue = currentTransform
animation.toValue = newTransform
animation.duration = 1.0
arm.addAnimation(animation, forKey: nil)
arm.transform = newTransform
...
The result is very clumsy though because the translations before and after the rotation are visible:
Since I am new to SceneKit, I think my approach to rotate the bar is completely wrong.
How can I improve the code to have both the rotation and animation at the same time?

Why I can't animate the SCNnode on eulerAngle using CAKeyframeAnimation

I'm try to animate an SCNnode using CAKeyframeAnimation, I been able to change the position of my object without problem but I can't animate the change of EulerAngle property.
The following code change the position of the node.
func animatePlaneKey(nodeToAnimate: SCNNode){
let pos = nodeToAnimate.position
let animation = CAKeyframeAnimation(keyPath: "position")
let pos1 = SCNVector3(pos.x, pos.y, pos.z)
let pos2 = SCNVector3(pos.x + 1 , pos.y, pos.z)
let pos3 = SCNVector3(pos.x + 1 , pos.y, pos.z + 1)
animation.values = [pos1,pos2, pos3]
animation.keyTimes = [0,0.5,1]
animation.calculationMode = .linear
animation.duration = 10
animation.repeatCount = 1
animation.isAdditive = true
nodeToAnimate.addAnimation(animation, forKey: "position")
}
and the following one should change the eulerAngles property but does't work.
any idea why I can't animate the rotation?
func animatePlaneKey(nodeToAnimate: SCNNode){
let animation2 = CAKeyframeAnimation(keyPath: "rotation")
let angles = nodeToAnimate.eulerAngles
let rot0 = SCNVector3(angles.x, angles.y, angles.z)
let rot1 = SCNVector3(angles.x, angles.y + Float(deg2rad(45)) , angles.z)
animation2.values = [rot0, rot1]
animation2.keyTimes = [0, 1]
animation2.duration = 2
animation2.repeatCount = .infinity
animation2.isAdditive = true
nodeToAnimate.addAnimation(animation2, forKey: "rotation")
}
Thanks for the help.
You have to use SCNVector4 type (a.k.a. Quaternion) instead of SCNVector3 type (a.k.a. Euler).
And a key "spin around" instead of "rotation".
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
let animation = CABasicAnimation(keyPath: "rotation")
let start = SCNVector4(0, 0, 0, 0)
let end = SCNVector4(0, 1, 0, CGFloat(Float.pi * 6))
animation.fromValue = start
animation.toValue = end
animation.duration = 10
animation.repeatCount = .infinity
ship.addAnimation(animation, forKey: "spin around")

How to achieve this animation with multiple CAShapeLayer

I'm having a lot of trouble exporting a video with animated layers.
The final goal is to export to a video (that's why I need this done with CALayers) a moving CAShapeLayer around the screen and a line following its trace.
First of all, I have an array of CGPoint with all the coordinate points which a CAShapeLayer should use to animate itself. To create the animation I'm using a CAKeyframeAnimation.
let values = [CGPoint(x: 50, y: 100),
CGPoint(x: 100, y: 150)] //Example values
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.values = values
animation.duration = videoLength
With this, I'm able to show the CALayer moving around the screen. It's 1 value per frame of the video, it's a 1:1 value.
The real problem comes with adding a line that shows the path of the CAShapeLayer.
What I think I might do, is to create lines with UIBezierPath, animate them and add each line
lines.forEach { (line) in
let path = UIBezierPath()
let shapeLayer = CAShapeLayer()
for (index, point) in line.points.enumerated() {
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = line.color.cgColor
shapeLayer.lineWidth = 4
shapeLayer.position = animatedLayer.position
shapeLayer.path = path.cgPath
shapeLayer.opacity = 0
shapeLayers.append(shapeLayer)
}
And to animate the lines...
var timeOffset: Double = 0
let eachLineTime = totalTime / Double(shapeLayers.count)
for shapeLayer in shapeLayers {
CATransaction.begin()
mainLayer.addSublayer(shapeLayer)
let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0
strokeAnimation.toValue = 1
strokeAnimation.duration = eachLineTime
strokeAnimation.beginTime = CACurrentMediaTime() + timeOffset
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0
opacityAnimation.toValue = 1
opacityAnimation.duration = eachLineTime
opacityAnimation.beginTime = CACurrentMediaTime() + timeOffset
CATransaction.setCompletionBlock {
shapeLayer.strokeColor = UIColor.orange.cgColor
shapeLayer.opacity = 1
}
shapeLayer.add(strokeAnimation, forKey: nil)
shapeLayer.add(opacityAnimation, forKey: nil)
timeOffset += eachLineTime
CATransaction.commit()
}
I need them to be different lines because I want to change the color of each line when it finishes each animation.
I'm getting the next output in Swift Playgrounds
But I'm not getting anything when I export the video in the app.
How can I simply add a trail of the first CAShapeLayer to see the (square in this case) moving path?
How can I move the CABasicAnimation with the same timing of the CAKeyframeAnimation?