'How to programmatically export 3D mesh as USDZ using ModelIO?

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.



Solution 1:[1]

June 24, 2021.

At the moment Apple developers can export .usd, .usda and .usdc files 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)

Solution 2:[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)
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 dang