iosswiftscenekit

How to subclass SCNNode and use the init that takes an MDLObject?


I need to subclass a SCNNode and use the init that takes a MDLObject (which is not a designated init).


public class MyModelNode: SCNNode {
  public var geo: SCNGeometry!

  private func dfs(node: SCNNode) {
    if let geometry = node.geometry {
      geo = geometry
      return
    }
    for child in node.childNodes {
      dfs(node: child)
    }
  }
  
  public init(model: MDLObject) {
    super.init(mdlObject: model)
    dfs(node: self)
  }
  
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

I got error saying that I can't do so, because the init is not a designated init. How can I subclass it but still setup the model object then?


Solution

  • I ended up using DFS to search for the geometry:

    import ModelIO
    import SceneKit.ModelIO
    
    // Main actor because accessing node.geometry is main isolated
    @MainActor
    public final class MyModelCache {
      
      public let modelCache: [String: SCNGeometry]
      
      // The model could contain non-geo nodes, such as light and camera.
      // We assume there's only 1 geo per model.
      private static func dfs(node: SCNNode) -> SCNGeometry? {
        if let geometry = node.geometry {
          return geometry
        }
        for child in node.childNodes {
          if let ans = dfs(node: child) {
            return ans
          }
        }
        return nil
      }
      
      public init(bundle: Bundle, models: [String]) {
        modelCache = models.reduce(into: [String:SCNGeometry]()) { result, model in
          let url = bundle.url(forResource: model, withExtension: nil)!
          let asset = MDLAsset(url: url)
          asset.loadTextures()
          let object = asset.object(at: 0)
          let node = SCNNode(mdlObject: object)
          guard let geometry = Self.dfs(node: node) else {
            fatalError("No geometry found in model file")
          }
          result[model] = geometry
        }
      }
      
      public func geometryNamed(_ fn: String) -> SCNGeometry {
        // From Apple's doc: The copy shares the underlying vertex data of the original, but can be assigned materials independently.
        // Since the model cache is shared, we want to keep it unchanged, so we return a copy.
        return modelCache[fn]!.copy() as! SCNGeometry
      }
      
    }