0

Is it possible to programmatically export 3D mesh as .usdz file format using ModelIO and MetalKit frameworks?

Here's a code:

import ARKit
import RealityKit
import MetalKit
import ModelIO

let asset = MDLAsset(bufferAllocator: allocator)
asset.add(mesh)

let filePath = FileManager.default.urls(for: .documentDirectory, 
                                         in: .userDomainMask).first!
    
let usdz: URL = filePath.appendingPathComponent("model.usdz")

do {
    try asset.export(to: usdz)               
    let controller = UIActivityViewController(activityItems: [usdz], 
                                      applicationActivities: nil)
    controller.popoverPresentationController?.sourceView = sender
    self.present(controller, animated: true, completion: nil)
} catch let error {
    fatalError(error.localizedDescription)
}

When I press a Save button I get an error.

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220

2 Answers2

2

The Andy Jazz answer is correct, but needs modification in order to work in a SwiftUI Sandboxed app:

First, the SCNScene needs to be rendered in order to export correctly. You can't create a bunch of nodes, stuff them into the scene's root node and call write() and get a correctly rendered usdz. It must first be put on screen in a SwiftUI SceneView, which causes all the assets to load, etc. I suppose you could instantiate a SCNRenderer and call prepare() on the root node, but that has some extra complications.

Second, the Sandbox prevents a direct export to a URL provided by .fileExporter(). This is because Scene.write() works in two steps: it first creates a .usdc export, and zips the resulting files into a single .usdz. The intermediate files don't have the write privileges the URL provided by .fileExporter() does (assuming you've set the Sandbox "User Selected File" privilege to "Read/Write"), so Scene.write() fails, even if the target URL is writeable, if the target directory is outside the Sandbox.

My solution was to write a custom FileWrapper, which I return if the WriteConfiguration UTType is .usdz:

public class USDZExportFileWrapper: FileWrapper {
    var exportScene: SCNScene

    public init(scene: SCNScene) {
        exportScene = scene
        super.init(regularFileWithContents: Data())
    }

    required init?(coder inCoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override public func write(to url: URL,
                               options: FileWrapper.WritingOptions = [],
                               originalContentsURL: URL?) throws {
        let tempFilePath = NSTemporaryDirectory() + UUID().uuidString + ".usdz"
        let tempURL = URL(fileURLWithPath: tempFilePath)
        exportScene.write(to: tempURL, delegate: nil)
        try FileManager.default.moveItem(at: tempURL, to: url)
    }
}

Usage in a ReferenceFileDocument:

public func fileWrapper(snapshot: Data, configuration: WriteConfiguration) throws -> FileWrapper {
    if configuration.contentType == .usdz {
        return USDZExportFileWrapper(scene: scene)
    }

    return .init(regularFileWithContents: snapshot)
}
dang
  • 587
  • 1
  • 5
  • 10
  • This did the trick for me. I had an issue exporting a textured mesh to .usdz. Using `SCNScene.write()` worked fine on iOS 16 but the texture wasn't exported on iOS 15. Using `SCNRenderer.prepare()` and then calling `SCNScene.write()` in the completion handler successfully exported a .usdz file with a texture. – MasDennis Feb 13 '23 at 08:27
1

08th January 2023

At the moment iOS developers still can export only .usd, .usda and .usdc files; you can check this using canExportFileExtension(_:) type method:

let usd = MDLAsset.canExportFileExtension("usd")
let usda = MDLAsset.canExportFileExtension("usda")
let usdc = MDLAsset.canExportFileExtension("usdc")
let usdz = MDLAsset.canExportFileExtension("usdz")
    
print(usd, usda, usdc, usdz)

It prints:

true true true false

However, you can easily export SceneKit's scenes as .usdz files using instance method called: write(to:options:delegate:progressHandler:).

let path = FileManager.default.urls(for: .documentDirectory,
                                     in: .userDomainMask)[0]
                                         .appendingPathComponent("file.usdz")
    
sceneKitScene.write(to: path, 
               options: nil, 
              delegate: nil, 
       progressHandler: nil)
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
  • 1
    This works great - once. The first time I call scene.write, I get a 14MB file that looks good in quick look. Any subsequent calls write 1.5-2k of garbage. Quitting & restarting the app re-enables the functionality. Any ideas? This is a macOS version of a universal SwiftUI app, BTW. – dang Apr 20 '22 at 01:14
  • Hi @dang, Write a check: if the file already exists in the given directory, then there is no need to write it. – Andy Jazz Apr 20 '22 at 04:39
  • 1
    Yes, of course. What I am saying is that after the first call, the function ceases to work *at all*: garbage is output no matter if the file exists or not, or if the scene has changed (or not). And although sometimes quitting and restarting the app restores functionality, it can get to where I have to reboot before it produces a valid usdz again. More context: I am calling the function from within a sandboxed macOS swiftui app. I'm wondering if the sandbox is screwing things up, but if that is the case, why would it ever work? – dang Apr 21 '22 at 16:46
  • I don't know why it's happening... – Andy Jazz Apr 21 '22 at 16:52
  • After much experimentation and console gazing, I found out what was happening, which I have posted below. – dang Apr 27 '22 at 00:05