swiftPunk
swiftPunk

Reputation: 1

SceneKit – How can I use materials to access node's transform?

I have a free 3d skull like in this photo and code in below:

enter image description here

import UIKit
import SceneKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = SCNScene(named: "skull.scn")!
        
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        scene.rootNode.addChildNode(cameraNode)
        
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
        
        let lightNode = SCNNode()
        lightNode.light = SCNLight()
        lightNode.light!.type = .omni
        lightNode.position = SCNVector3(x: 0, y: 10, z: 10)
        scene.rootNode.addChildNode(lightNode)
        
        let ambientLightNode = SCNNode()
        ambientLightNode.light = SCNLight()
        ambientLightNode.light!.type = .ambient
        ambientLightNode.light!.color = UIColor.darkGray
        scene.rootNode.addChildNode(ambientLightNode)
        

        let scnView = self.view as! SCNView
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.backgroundColor = UIColor.black
        
        
        let button = UIButton(frame: CGRect(x: 100, y: 600, width: 200, height: 60))
        button.setTitle("Open Jaw", for: .normal)
        button.setTitleColor(.systemBlue, for: .normal)
        
        button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
        
        self.view.addSubview(button)

    }
    
    @objc func buttonAction() {
        print("Button pressed")
        // How can I open jaw here?
    }

}

As you can see in code, I want open the jaw via button, since I have all materials in file, how could I use Dflt2 to do this, I want to know how can I get access to Dflt2 for rotation as opening Jaw with a smooth animation or even changing the color, is it possible?

enter image description here

Upvotes: 1

Views: 642

Answers (1)

Andy Jazz
Andy Jazz

Reputation: 58053

Node-centric behaviour

Unlike pro apps, like Autodesk Maya, where through the material you can select the geometry which that material is assigned to, SceneKit doesn't allow you to do that. However, SceneKit, like Maya, has a nodal hierarchical scene structure...

SCNView –> SCNScene -> RootNode 
                           | -> Node_01
                           |       | -> Subnode_A -> Geometry -> Materials
                           |       | -> Subnode_B -> Geometry -> Materials
                           |
                           | -> Node_02 -> Geometry -> Materials
                           |
                           | -> Node_03 -> Geometry -> Materials
                           |
                           | -> Node_04
                           |       | -> Subnode_A
                           |                | -> Subnode_B -> Camera
                           |
                           | -> Node_05 -> SpotLight
                           |
                           | -> Node_06 -> ParticleSystem
                           |
                           | -> Node_07 -> AudioPlayer

...where you can get to desired material only by selecting a specific geometry within a particular node. Such an approach is called node-centric, there's no a material-centric approach, by default. The above abstract diagram shows us the connection of nodes in the scene, where each node has its own transform parameters (translate, rotate and scale).

Thus, you can do what you want only when retrieving a node:

import SceneKit

class GameViewController: UIViewController {
    
    @IBOutlet var sceneView: SCNView!
    let scene = SCNScene(named: "art.scnassets/Skull.usdz")!
    var lowerJaw: SCNNode? = nil
    var counter: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        sceneView.scene = self.scene
        sceneView.allowsCameraControl = true
        sceneView.autoenablesDefaultLighting = true
        sceneView.backgroundColor = .black
        
        guard let skullNode = self.scene.rootNode.childNode(
                                  withName: "Geom", recursively: true)
        else { return }
        
        self.lowerJaw = skullNode.childNodes[0].childNode(
                            withName: "lower_part", recursively: true)
        
        lowerJaw?.childNodes[0].geometry?.firstMaterial?.diffuse.contents = 
                                                         UIColor.systemBlue
    }
    
    @IBAction func pressed(_ sender: UIButton) {
        
        self.counter += 1
        
        let opened = SCNAction.rotateTo(x: .pi/20, y: 0, z: 0, duration: 0.3)
        let closed = SCNAction.rotateTo(x: 0, y: 0, z: 0, duration: 0.3)
        
        if (counter % 2 == 0) {
            self.lowerJaw?.runAction(closed)
        } else {
            self.lowerJaw?.runAction(opened)
        }
    }
}

As you can see, in the Xcode Scene graph, the "Geom" node contains all the parts of the skull, and the "lower_part" subnode, that we're rotating, contains the skull's jaw and lower teeth. So, the most important thing about 3D objects – you need to have separate 3D geometry parts (nodes) in a scene in order to animate them (not a mono-model). The "lower_part" node is rotated around its pivot point. Each SceneKit's node has its own personal pivot. If you need to move the pivot point to another location use this code.

In SceneKit you can use .usdz, .scn, .dae or .obj scenes. Xcode's Scene graph allows you to create, delete, reposition, nest and rename nodes. Nevertheless, the most robust place to build an hierarchy of nodes is 3D authoring app (like Maya or Blender).

enter image description here


Material-centric approach

However, nothing prevents you from creating a collection of dictionaries like [SCNMaterial: SCNNode].

Abstract example (2 spheres, 2 materials):

import SceneKit

let sceneView = SCNView(frame: .zero)
sceneView.scene = SCNScene()

let redMaterial = SCNMaterial()
redMaterial.diffuse.contents = UIColor.red

let greenMaterial = SCNMaterial()
redMaterial.diffuse.contents = UIColor.green

let nodeA = SCNNode(geometry: SCNSphere(radius: 0.1))
nodeA.position.z = -2.5
nodeA.geometry?.materials[0] = redMaterial

let nodeB = SCNNode(geometry: SCNSphere(radius: 0.1))
nodeB.position.y = 1.73
nodeB.geometry?.materials[0] = greenMaterial

sceneView.scene?.rootNode.addChildNode(nodeA)
sceneView.scene?.rootNode.addChildNode(nodeB)

// Dictionary
var getNode = [redMaterial: nodeA, greenMaterial: nodeB]

getNode[redMaterial]?.position.z             // -2.5
getNode[greenMaterial]?.position.y           // 1.73

Upvotes: 2

Related Questions