3

I want to load a 3d usdz blob into a view, but since I only have the data object, I'm trying to initialize the scene with that with no luck.

To that, I initialize the SCNSceneSource() and then open it using .scene().

Now what I don't understand: If I use a URL and load the scene directly - it works. If I use a Data object on the same URL it doesn't.

Apple docs says, the data should be of type NSData but that seems wrong.

import SceneKit

let url = URL(string: "file:///Users/thilo/Desktop/Input/UU2.usdz")!
// working
let src_ok = SCNSceneSource(url: url)
let scn_ok = src_ok?.scene(options: nil, statusHandler: {
    a,b,c,d in print("OK: \(a) \(b) \(String(describing: c)) \(d) ")
})
print("Ok: \(scn_ok)")

// Not working?
let data    = try! Data(contentsOf: url)
let src_bad = SCNSceneSource(data: data)
let scn_bad = src_bad?.scene(options: nil, status handler: {
    a,b,c,d in print("BAD: \(a) \(b) \(String(describing: c)) \(d) ")
})
print("Failed: \(scn_bad)")

running on Playground says:

Ok: Optional(<SCNScene: 0x6000038e1200>)
BAD: 0.0 SCNSceneSourceStatus(rawValue: 4) nil 0x000000016fa948bf 
BAD: 0.0 SCNSceneSourceStatus(rawValue: 4) nil 0x000000016fa942af 
BAD: 0.0 SCNSceneSourceStatus(rawValue: -1) Optional(Error Domain=NSCocoaErrorDomain Code=260 "Could not load the scene" UserInfo={NSLocalizedDescription=Could not load the scene, NSLocalizedRecoverySuggestion=An error occurred while parsing the COLLADA file. Please check that it has not been corrupted.}) 0x000000016fa942af 
Failed: nil

What am I missing?

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
thilo
  • 181
  • 1
  • 7

3 Answers3

1

SCNSceneSource doesn't support .usdz in Data context

Official documentation says that SCNSceneSource object supports only .scn, .dae and .abc file formats. But it turns out that SceneKit doesn't support URL-loading of .usdz only in the context of working with Data. Thus, when working with Data, use files in the .scn format.

import SceneKit
import Cocoa

class GameViewController : NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let url = URL(string: "file:///Users/swift/Desktop/ship.scn") {
    
            let data = try! Data(contentsOf: url)
            let source = SCNSceneSource(data: data)
    
            let sceneView = self.view as! SCNView
            sceneView.scene = source?.scene()
        }
    }
}

To load .usdz using URL, try SCNSceneSource.init?(url: URL)

class GameViewController : NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if let url = URL(string: "file:///Users/swift/Desktop/ship.usdz") {
    
            let source = SCNSceneSource(url: url)
    
            let sceneView = self.view as! SCNView
            sceneView.scene = source?.scene()
        }
    }
}

Or use SCNScene object to load .usdz model

class GameViewController : NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let url = URL(fileURLWithPath: "/Users/swift/Desktop/ship.usdz")

        do {
            let scene = try SCNScene(url: url)
            let sceneView = self.view as! SCNView
            sceneView.scene = scene
            sceneView.autoenablesDefaultLighting = true
        } catch {
            print(error.localizedDescription)
        }
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
1

Gathering from the comment "does not support usdz" my solution is: to create a temporary file ( .usdz) seems to be required by the API... and then manually remove the temporary file after loading.

First extend FileManager with the below code:

public extension FileManager {

    func temporaryFileURL(fileName: String = UUID().uuidString,ext: String) -> URL? {
        
        return URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
            .appendingPathComponent(fileName + ext)
    }
}

For a limited hard-coded use case:

let fm = FileManager.default
let tempusdz = fm.temporaryFileURL(ext:".usdz")!
fm.createFile(atPath: tempusdz.path(), contents: sceneData)
let src = SCNSceneSource(url: tempusdz)
if let scene =  src?.scene(options: nil)  {
        ....
}
try? fm.removeItem(at: tempusdz)

of course this is a hack, because it will only work if the data is in usdz format.

Since usdz is a ZIP archive, maybe testing for a zip and then just doing the below is a better option:

let sceneData:Data? = data
var sceneSrc: SCNSceneSource? = nil
var tempURL:URL? = nil
if let dataStart = sceneData?.subdata(in: 0..<4),
   let dataMagic = String(data: dataStart, encoding: String.Encoding.utf8) as String?,
   dataMagic == "PK\u{3}\u{4}" {
    let fm = FileManager.default
    tempURL = fm.temporaryFileURL(ext: ".usdz")
    if let tempURL {
        fm.createFile(atPath: tempURL.path(), contents: sceneData)
        sceneSrc = SCNSceneSource(url: tempURL)
    }
} else {
    sceneSrc = SCNSceneSource(data: sceneData!)
}

let scene = sceneSrc?.scene()

if let tempURL {
    try? FileManager.default.removeItem(at: tempURL)
}

Does anyone knows a better solution? Is there an easy way to check the type of the Data ?

thilo
  • 181
  • 1
  • 7
-1

potential solution could be to verify the format of the data object and ensure that it is a valid COLLADA file.

import Foundation

let url = URL(string: "file:///Users/thilo/Desktop/Input/UU2.usdz")!

let data = try! Data(contentsOf: url)
print("Data size: \(data.count)")
print("Data format: \(data.description)")

you usually get these types of errors when the data wasn't properly formatted

  • Does this mean the only solution is write the blob into a temporary file and use the generated URL for this to import into a scene kit scene? ( like shown here https://stackoverflow.com/questions/32657533/temporary-file-path-using-swift/56033291#56033291 ) – thilo Feb 05 '23 at 10:15