iosswiftscenekit

SceneKit flattenedClone changes the node unexpectively


I have a 3D model that contains 3 geometries, as highlighted below

enter image description here

This is my code that create such SCNNode from a USDZ file:

  private func readGeo(fn: String) -> SCNNode {
    let bundle = Bundle.main
    let url = bundle.url(forResource: fn, withExtension: nil)!
    let asset = MDLAsset(url: url)
    asset.loadTextures()
    let object = asset.object(at: 0)
    let node = SCNNode(mdlObject: object)
    return node
  }

Then I need to flatten the node, since I want to make it just a single geometry, so that it's easier to attach a physics body. So I changed the return to:

return node.flattenedClone()

The position of geometries seems to be incorrect, as you can see below: (see the cover is sticking out)

enter image description here

You can download the sample project here: https://drive.google.com/file/d/1ewudz3A2NKeW-72bXRPJJtphyRMae-1e/view?usp=sharing

Edit:

I realize that the position of the sub nodes are changed after flatten. They are originally at non-zero coordinate, but after flatten, they are positioned as if they are at zero coordinate. I don't know why


Solution

  • I believe this is a bug in SceneKit that the transform is ignored if the node has no geometry. I was able to fix its by passing down the transforms to child nodes with a DFS:

    
    fileprivate extension SCNNode {
      
      // pre-process it before taking the flattenedClone()
      var preProcessed: SCNNode {
        let clone = self.clone()
        Self.dfs_passDownTransform(node: clone)
        return clone
      }
      
      static func dfs_passDownTransform(node: SCNNode) {
        // If it's an empty node with just transform, we want to pass the transform to its immediate children
        if node.geometry == nil && !SCNMatrix4IsIdentity(node.transform) {
          for child in node.childNodes {
            child.transform = SCNMatrix4Mult(child.transform, node.transform)
          }
          node.transform = SCNMatrix4Identity
        }
        
        for child in node.childNodes {
          dfs_passDownTransform(node: child)
        }
      }
    }