AtharvSalokhe
AtharvSalokhe

Reputation: 533

How to rotate a SCNNode by multiple angles?

I am having a cylinder as a SCNCylinder in a SCNView. I want to rotate the cylinder by a number of given angles. I am using SwiftUI to give the required input (angle of rotation). Suppose I give it a input as 90° . Currently it can easily rotate by 90°, but if I give it a second input of 180°, it goes back to its original position and then it rotates by 180°. I want it to rotate by 180° from the position it got after its 90° rotation.

Here is my code :

struct ContentView: View {
 @State var rotationAngle: Float = 0
 var body: some View {

    VStack{

        Text("180°").onTapGesture {
            self.rotationAngle = 180.0
        }

        Divider()

        Text("90°").onTapGesture {
            self.rotationAngle = 90.0
        }

        SceneKitView(angle: $rotationAngle)
            .position(x: 225.0, y: 175)
            .frame(width: 300, height: 300, alignment: .center)

    }
  }    
}

struct SceneKitView: UIViewRepresentable {
  @Binding var angle: Float


func degreesToRadians(_ degrees: Float) -> CGFloat {
    return CGFloat(degrees * .pi / 180)
}

func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {

    let sceneView = SCNView()
    sceneView.scene = SCNScene()
    sceneView.allowsCameraControl = true
    sceneView.autoenablesDefaultLighting = true
    sceneView.backgroundColor = UIColor.white
    sceneView.frame = CGRect(x: 0, y: 10, width: 0, height: 1)

    return sceneView
}

func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {

        sceneView.scene?.rootNode.enumerateChildNodes { (node, stop) in
            node.removeFromParentNode() }


        let cylinder = SCNCylinder(radius: 0.02, height: 2.0)
        let cylindernode = SCNNode(geometry: cylinder)
        cylindernode.position = SCNVector3(x: 0, y: 0, z: 0)
        cylinder.firstMaterial?.diffuse.contents = UIColor.green


        cylindernode.pivot = SCNMatrix4MakeTranslation(0, -1, 0)




        let rotation = SCNAction.rotate(by: self.degreesToRadians(self.angle),
                                        around: SCNVector3(1, 0, 0), duration: 5)



         cylindernode.runAction(rotation)


        sceneView.scene?.rootNode.addChildNode(cylindernode)

   }      

I wish it to rotate properly no matter how many angles I give it.

Upvotes: 0

Views: 678

Answers (1)

Cenk Bilgen
Cenk Bilgen

Reputation: 1435

To be clear, you want the cylinder to continue rotating from it's current position every time the button is pressed not have the cylinder go to a fixed either 90 or 180 position.

Because your current code seems to be setup to do the second thing (except for the rotate:by). The rotationAngle is held as a fixed state. Really you should be sending rotate commands to the node without keeping the amount it rotated around in a state variable.

But, working with the code you have now:

Inside the updateUIView stick to only doing updates to your nodes. Right now, you are tearing down and recreating the entire node hierarchy to it's original position every time rotationAngle changes. That's probably why thing are redrawing. Instead do that setup inside makeUIView or inside an initializer for SceneKitView and in updateUIView just apply the new rotation.

Second problem is that when you set rotationAngle by pressing the button to say 180, if you press the same button again, the value is set again to 180. The value hasn't changed so as far as SwiftUI is concerned, there is no need to call updateUIView. There's a couple ways to make it update, but a quick and easy way is probably just rotate by 0 then immediately after by the angle you want. But again you really should not hold that angle in a state variable. Better to just make a rotateCylinder(angle: Angle) function in SceneKitView and call it from the button.

Also, why not use the new Angle type? Here's your code with some of those changes. I think this what you are trying to do, but I could be wrong.

import SwiftUI
import UIKit
import SceneKit

struct ContentView: View {

  @State var rotationAngle: Angle = .zero

  var body: some View {

    VStack{

      HStack {

        Spacer()
        Text("180°").onTapGesture {
          self.rotationAngle = .zero
          self.rotationAngle = .degrees(180)
        }
        Spacer()

        Divider()

        Spacer()
        Text("90°").onTapGesture {
          self.rotationAngle = .zero
          self.rotationAngle = .degrees(90)
        }
        Spacer()

        }.frame(height: 100).padding()

      SceneKitView(radius: 0.2, height: 2, angle: $rotationAngle)

    }
  }
}

struct SceneKitView: UIViewRepresentable {

  @Binding var angle: Angle

  let node: SCNNode

  init(radius: CGFloat, height: CGFloat, angle: Binding<Angle>) {

    let cylinder = SCNCylinder(radius: radius, height: height)
    cylinder.firstMaterial?.diffuse.contents = UIColor.green
    self.node = SCNNode(geometry: cylinder)

    self._angle = angle
  }

  func makeUIView(context: UIViewRepresentableContext<SceneKitView>) -> SCNView {

    let sceneView = SCNView()
    sceneView.scene = SCNScene()
    sceneView.autoenablesDefaultLighting = true
    sceneView.scene?.rootNode.addChildNode(node)

    return sceneView
  }

  func updateUIView(_ sceneView: SCNView, context: UIViewRepresentableContext<SceneKitView>) {
    print("Updating view \(angle.degrees)")

    // To continue rotating
    let rotation = SCNAction.rotate(by: CGFloat(angle.radians), around: SCNVector3(1, 0, 0), duration: 3)

    // To go to a fixed angle state
    //let rotation = SCNAction.rotate(toAxisAngle: SCNVector4(1, 0, 0, angle.radians), duration: 3)

    node.runAction(rotation)

  }

}

Upvotes: 1

Related Questions