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.
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)
}
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)
Related
I am having trouble finding where to place files on my Mac so that my Xcode simulator sees them.
Working on a "file upload" section for my app. Before I call the UIDocumentPickerViewController, I do call my own function printSimDir which I use to open the proper folder on my Mac so I can throw my files in there.
And in there I have three files: "blank_inv, invoice_001.cvs, and example.mp3"
However, in my simulator, I don't see these files. I do however keep seeing one xls file that is not any of three above files. So at one point I did get this right. But not anymore.
I realize that my problem might also be in how I am calling the UIDocumentPickerViewController so am including that code as well.
case ButtType.file.rawValue:
printSimDir()
let supportedTypes: [UTType] = [UTType.spreadsheet, UTType.commaSeparatedText, .mp3]
let pickerViewController = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes)
pickerViewController.delegate = self
pickerViewController.allowsMultipleSelection = false
present(pickerViewController, animated: true, completion: nil)
...
extension UploadInv: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
for url in urls {
guard url.startAccessingSecurityScopedResource() else {
print ("error")
return
}
xFile = XFile(fileUrl: url, key: "filename")
myStartUPButt.isEnabled = true
do { url.stopAccessingSecurityScopedResource() }
myStatus.text = xFile?.filename
}
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
controller.dismiss(animated: true, completion: nil)
}
}
printSimDir()
func printSimDir(){
// tried the commented code as well
// let fManager = FileManager.default
// guard let url = fManager.urls(for: .documentDirectory, in: .userDomainMask).first else {return}
// print ("\(url)")
#if targetEnvironment(simulator)
if let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.path {
print("Documents Directory: \(documentsPath)")
}
#endif
}
For those who come across this. Not sure where I found the answer, but it's quite simple.
Simply drag and drop the files you want from your Mac to the Xcode
simulator's Home Screen. Currently have only tried them one at a time.
Is this the proper answer? I do not know. But it does work.
Haven't tried every way to do this, (one at a time, multiple files at once, multiple simulators, etc.) But even after a Mac restart the files are still there.
How can I use a NSFilePromiseProvider in combination with .onDrag in SwiftUI? It only allows me to return an NSItemProvider, but for this, the file must already exist on the computer as the callback of registerFileRepresentation is called eagerly.
I want to drag a remote file displayed in my application to my computer desktop. With AppKit I used file promises for this use case, but for SwiftUI I found no solution so far.
struct ContentView: View {
var body: some View {
Text("Draggable Text")
.onDrag {
let provider = NSItemProvider()
provider.registerFileRepresentation(
forTypeIdentifier: UTType.fileURL.identifier,
fileOptions: [],
visibility: .all
) {
// This file must already exists. How to provide a file promise here?
let fileUrl = URL(fileURLWithPath: "file:///path/to/file.txt")
$0(fileUrl, true, nil)
return nil
}
return provider
}
}
}
Here's a limited workaround. It uses a FileHandle to write to the created file even after the drop has finished.
It reliably works for drop targets that move the file (e.g. Finder).
It fails for drop targets that read from the file (e.g. Apple Mail) if the file hasn't been completely written when the drop event occurs. In this situation, the empty placeholder content is read by the target app.
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Drag me")
.onDrag {
let provider = NSItemProvider()
provider.registerDataRepresentation(for: .fileURL, visibility: .all, loadHandler: loadFileForItemProvider)
return provider
}
}
#Sendable
func loadFileForItemProvider(onComplete callback: #escaping (Data?, Error?) -> Void) -> Progress? {
let progress = Progress(totalUnitCount: 100)
Task.detached {
do {
let temporaryDirectory = try FileManager.default.url(for: .itemReplacementDirectory, in: .userDomainMask, appropriateFor: .documentsDirectory, create: true)
let url = temporaryDirectory.appendingPathComponent("some_file.pdf")
try Data().write(to: url) // create temporary, emptyfile
let handle = try FileHandle(forWritingTo: url)
callback(url.dataRepresentation, nil)
let fileData = try await ... // load/generate file data here
try handle.write(contentsOf: fileData)
try? handle.close()
progress.completedUnitCount = 100
} catch {
callback(nil, error)
}
}
return progress
}
}
Remember to clean up the temporary directory after a certain duration or when quittung/launching the app.
I have a Reality Composer scene and I want to extract it as usdz file or any files that can be used in ARQuickLook?
is it possible?
At build time, Xcode compiles your .rcproject into a .reality file, and AR Quick Look accepts preview items of type .reality. Here's an example that uses AR Quick Look to preview the Experience.rcproject taken from Apple's SwiftStrike TableTop sample code:
import UIKit
import QuickLook
import ARKit
class ViewController: UIViewController, QLPreviewControllerDataSource {
override func viewDidAppear(_ animated: Bool) {
let previewController = QLPreviewController()
previewController.dataSource = self
present(previewController, animated: true, completion: nil)
}
func numberOfPreviewItems(in controller: QLPreviewController) -> Int { return 1 }
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
guard let path = Bundle.main.path(forResource: "Experience", ofType: "reality") else { fatalError("couldn't find the rcproject file.") }
let url = URL(fileURLWithPath: path)
let item = ARQuickLookPreviewItem(fileAt: url)
return item
}
}
From Apple's Creating 3D Content with Reality Composer
document:
You can also save your composition to a .reality file for use as a
lightweight AR Quick Look experience in your app or on the web. This
allows users to place and preview content in the real world to get a
quick sense of what it’s like.
To create a Reality file, choose File > Export > Export Project in the
Reality Composer menu, and provide a name for the file. You use the
Reality file that gets stored to disk just like you would use a USDZ
file, as described in Previewing a Model with AR Quick Look.
I've created a JSON string file from an object containing multiple properties. This is the object:
RecipeFile : Codable {
var name: String
var theRecipeIngredients: [String]
var theRecipeSteps: [String]
var theRecipeRating: Int
var theRecipeCategory: String
var theRecipeIndexStrings: String
var theRecipeImage: String?
I create the JSON string file with this code:
let json_encoder = JSONEncoder()
let recipeFileName = recipeToDisplay.name! + UUID().uuidString + ".json"
let exportFilePath = getDocumentsDirectory().appendingPathComponent(recipeFileName)
do {
let jsonData = try json_encoder.encode(exportRecipeFile)
if let jsonString = String(data: jsonData, encoding: .utf8)
{
try jsonString.write(to: exportFilePath, atomically: false, encoding: .utf8)
}
} catch {
print(error.localizedDescription)
}
I upload it to iCloud Drive. I import the string file from iCloud Drive using UIDocumentPickerViewController. I can parse the imported file just fine. However, I get this message (edited to remove some path info) in the xCode debug area when func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) is called:
[DocumentManager] Failed to associate thumbnails for picked URL
file:///....Bourbon%20Chocolate%20Walnut%20Pie18D20181-DAFD-499C-9873-7D3E0794C37A.json
with the Inbox copy
file:///....Bourbon%20Chocolate%20Walnut%20Pie18D20181-DAFD-499C-9873-7D3E0794C37A.json:
Error Domain=QLThumbnail Code=2 "(null)"
UserInfo={NSUnderlyingError=0x149a042b0 {Error
Domain=GSLibraryErrorDomain Code=3 "Generation not found"
UserInfo={NSDescription=Generation not found}}}
Any idea what is causing this to be generated?
The didPickDocumentsAt code starts as follows:
let data = try? Data(contentsOf: urls[0]) as Data
let json_decoder = JSONDecoder()
do {
let importRecipeFile = try json_decoder.decode(RecipeFile.self, from: data!)
let importedRecipeToSave = Recipe(context: theMOC)
importedRecipeToSave.name = importRecipeFile.name
importedRecipeToSave.category = importRecipeFile.theRecipeCategory
importedRecipeToSave.rating = Int16(importRecipeFile.theRecipeRating)
importedRecipeToSave.terms = importRecipeFile.theRecipeIndexStrings
importedRecipeToSave.addedToGroceryList = false
You can safely ignore this message. When you import a file from iCloud, iOS tries to copy the thumbnail from iCloud to the imported copy, but for JSON files there's no thumbnail to copy and it logs this. This is not an error on your side.
I can reproduce this issue with SwiftUI with iOS 14. After Originally I present the UIDocumentPickerViewController after a ToolbarItem pressed and it works fine. Later I refactor the UI and the View is pushed from other parent View and this error occurs and the JSON is not received.
The same thing has happened to me, and I have not found a concrete solution in any forum. But, by testing and mixing code that I found in the forums finally worked for me. Still, I don't know exactly what's wrong.
I leave my code here in case it's useful for someone, although it's been many months since this question.
func importarCsv(sender: UIBarButtonItem) {
let types = [kUTTypePDF,kUTTypeUTF8PlainText]
let importMenu = UIDocumentPickerViewController(documentTypes: types as [String], in: .import)
if #available(iOS 11.0, *) {
importMenu.allowsMultipleSelection = false
}
importMenu.delegate = self
present(importMenu, animated: true, completion: nil)
}
extension MaterialViewController: UIDocumentPickerDelegate {
internal func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt urls: URL) {
print("urls : \(urls)")
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
print("close")
controller.dismiss(animated: true, completion: nil)
}
}
I had this problem when i presented the UIDocumentPicker from another UIViewController before adding it as a child view controller on its parent view controller.
I am making a SpriteKit framework using swift 4.2 and want to include some .sks files for scenes and actions. I have tried to load the scene from the bundle using the code below:
class func newGameScene() -> GameScene {
guard let gameScenePath = Bundle(for: self).path(forResource: "GameScene", ofType: "sks") else { assert(false) }
guard let gameSceneData = FileManager.default.contents(atPath: gameScenePath) else { assert(false) }
let gameSceneCoder = NSKeyedUnarchiver(forReadingWith: gameSceneData)
guard let scene = GameScene(coder: gameSceneCoder) else { assert(false) }
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
return scene
}
I load the scene and present it. (This code is mostly from Apple's template for SpriteKit as Im testing this issue.)
guard let view = view else {
return nil
}
let scene = GameScene.newGameScene()
view.presentScene(scene)
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
return nil
The GameScene.sks and the code is unchanged from Apples template in this case. This code and the .sks assets are in the dynamic framework and imported into another project.
When having the framework load the scene into a view I pass it, it shows the fps and node count but not the "Hello, World!" text.
In the code below, also copied from the template, a break point shows that these are not called when mousing down.
#if os(OSX)
// Mouse-based event handling
extension GameScene {
override func mouseDown(with event: NSEvent) {
if let label = self.label {
label.run(SKAction.init(named: "Pulse")!, withKey: "fadeInOut")
}
self.makeSpinny(at: event.location(in: self), color: SKColor.green)
}
override func mouseDragged(with event: NSEvent) {
self.makeSpinny(at: event.location(in: self), color: SKColor.blue)
}
override func mouseUp(with event: NSEvent) {
self.makeSpinny(at: event.location(in: self), color: SKColor.red)
}
}
#endif
I know it must have to do with how SpritKit loads the scene but cannot find a solution. I have to use an NSKeyedUnarchiver becuase SpritKit's built in file initializer:
GameScene(fileNamed: "GameScene")
Only loads from the Main Bundle.
Now in the above I assumed that the file can be loaded by using a coder but Tomato made the point that sks most likely was not saved using a coder. In that case, It may be impossible to load an sks file from another bundle in sprite-kit using the provided api from apple. The answer may not include coders.
I have compiled the above discussion/solution into a single extension function on SKScene. Hope this helps someone!
import SpriteKit
extension SKScene {
static func fromBundle(fileName: String, bundle: Bundle?) -> SKScene? {
guard let bundle = bundle else { return nil }
guard let path = bundle.path(forResource: fileName, ofType: "sks") else { return nil }
if let data = FileManager.default.contents(atPath: path) {
return NSKeyedUnarchiver.unarchiveObject(with: data) as? SKScene
}
return nil
}
}
Just as I thought let gameSceneCoder = NSKeyedUnarchiver(forReadingWith: gameSceneData) was not creating a proper coder for you.
Just do
guard let scene = NSKeyedUnarchiver.unarchiveObject(with: gameSceneData) as? SKScene
else{
assert(false)
}
This will unarchive the file properly for you.
Note, if you want to use GameScene, make sure GameScene is set in the custom class of the SKS file