How to create ARQuickLookPreviewItem with a ModelEntity with RealityKit? - swift

I'm trying to Load a model from a usdz file, change the material colour of it and then display the updated model with AR QuickLook. I have got my code to the point that it loads the original model in the quick look preview. How do i use the updated ModelEntity?
guard let path = Bundle.main.path(forResource: "atm", ofType: "usdz") else {
fatalError("Couldn't find the supported asset file.")
}
let url = URL(fileURLWithPath: path)
guard let modelEntity = try? Entity.loadModel(contentsOf: url) else {
fatalError("Entity not found")
}
var newMaterial = SimpleMaterial()
newMaterial.color.tint = UIColor.cyan
for i in 0...(modelEntity.model?.materials.count ?? 1) - 1 {
modelEntity.model?.materials[i] = newMaterial
}
//let temporaryDirectory = FileManager.default.temporaryDirectory
//let temporaryFileURL = temporaryDirectory.appendingPathComponent("model.usdz")
let previewItem = ARQuickLookPreviewItem(fileAt: url) // how do i use modelEntity here?
previewItem.allowsContentScaling = allowsContentScaling
previewItem.canonicalWebPageURL = nil
return previewItem
}
I just need to change the colour of the model at runtime and show it. TIA

Related

How do I load a Data object into a SCNScene?

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?
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)
}
}
}
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 ?
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

RealityKit – Get a model entity from USDZ file

I haave a file (exists in main bundle with target membership checked) named matrix.usdz and need to load it with
do {
let path = Bundle.main.path(forResource: "Matrix", ofType: "usdz")!
let url = URL(fileURLWithPath: path)
let assetsLoader = try Entity.load(contentsOf: url)
}
catch {
print(error)
}
But it crashes with
Thread 1: signal SIGABRT
on this line
let assetsLoader = try Entity.load(contentsOf: url)
Preview
You have to create an anchor if you need to load an entity into your scene. In order to get a ModelEntity, you need to grab it from the scene hierarchy using .children[X] subscript.
import RealityKit
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
do {
let path = Bundle.main.path(forResource: "Matrix", ofType: "usdz")!
let url = URL(fileURLWithPath: path)
// Scene
let scene = try Entity.load(contentsOf: url)
print(scene)
// Entity
let entity = scene.children[0].........children[0] as! ModelEntity
entity.model?.materials[0] = UnlitMaterial(color: .red)
let anchor = AnchorEntity(plane: .any)
anchor.addChild(scene)
arView.scene.anchors.append(anchor)
} catch {
print(error)
}
}
}
You can also get a model this way:
let modelEntity = try Entity.loadModel(contentsOf: url)
modelEntity.model?.materials[0] = UnlitMaterial(color: .red)
P.S.
I should say that you have an obvious naming error – "Matrix" vs "matrix". Also Matrix.rcproject and Matrix.usdz are not the same. To load Matrix.rcproject (Reality Composer project) use the following approach:
// .rcproject
let scene = try! Matrix.loadCircle()
let circleEntity = scene.children[0]...........children[0] as! ModelEntity
to load a USDZ model use this one:
// .usdz
let model = try! Entity.loadModel(named: "Matrix", in: nil)
But as far as I know, you do not need an RC project, so export USDZ from Reality Composer.
To load .reality file use the following approach:
// .reality
let carModel = try! Entity.loadAnchor(named: "car")
print(carModel)
arView.scene.addAnchor(carModel)
Here's your USDZ model on iOS simulator:

Issue with adding node to scene view

I am having an issue with the way the model is being placed on the scene view.
The problem is that the model downloads successfully but for some reason it appears on top of the scene view.
here is a video of the problem:
https://www.youtube.com/watch?v=-pTPEZTF9zo
view did load:
override func viewDidLoad() {
super.viewDidLoad()
self.dowloadModel()
self.sceneView.debugOptions = [ARSCNDebugOptions.showWorldOrigin, ARSCNDebugOptions.showFeaturePoints]
self.configuration.planeDetection = .horizontal
self.sceneView.session.run(configuration)
self.registerGestureRecogniser()
}
Gesture recofniser:
func registerGestureRecogniser(){
let tapGestureRecogiser = UITapGestureRecognizer(target: self, action: #selector(tapped))
self.sceneView.addGestureRecognizer(tapGestureRecogiser)
}
Tapped function:
#objc func tapped(sender:UITapGestureRecognizer){
let sceneView = sender.view as! ARSCNView
let tapLocation = sender.location(in: sceneView)
let hitTest = sceneView.hitTest(tapLocation, types: .existingPlaneUsingExtent)
if !hitTest.isEmpty{
print("touched a horizontal surface")
self.addItem2(hitTestResult: hitTest.first!)
}
else{
print("no match")
}
}
Download model from AWS:
private func dowloadModel(){
let url = URL(string: "https://ENTER URL HERE")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error{
print(error.localizedDescription)
return
}
if let data = data{
print(data)
let documentDirectories = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
if let documentDirectory = documentDirectories.first{
let fileURL = documentDirectory.appendingPathComponent("Food.scn")
let dataNS : NSData? = data as NSData
try! dataNS?.write(to: fileURL, options: .atomic)
print("Saved!")
}
}
}.resume()
}
Add Item 2 function:
func addItem2(hitTestResult : ARHitTestResult){
let documentDirectories = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
if let documentDirectory = documentDirectories.first{
let fileURL = documentDirectory.appendingPathComponent("Food.scn")
do{
let scene = try SCNScene(url: fileURL, options: nil)
let node = scene.rootNode.childNode(withName: "Burger", recursively: true)!
let material = SCNMaterial()
material.diffuse.contents = UIImage(named: "Hamburger_BaseColor")
material.diffuse.wrapT = SCNWrapMode.repeat
material.diffuse.wrapS = SCNWrapMode.repeat
material.isDoubleSided = true
let transform = hitTestResult.worldTransform
let thirdColumn = transform.columns.3
node.position = SCNVector3(thirdColumn.x, thirdColumn.y, thirdColumn.z)
self.sceneView.scene.rootNode.addChildNode(node)
}
catch{
print(error)
}
}
}
It seems that this issue entirely depends on a pivot point's position and a scale of your model.
Pivot point meets ARAnchor and helps you control a model's offset, orientation and scale of model on a floor, a table-top, or a wall.
node.scale = SCNVector3(x: 0.2, y: 0.2, z: 0.2)
node.pivot = SCNMatrix4MakeTranslation(0, -0.5, 0)
...or for positioning pivot use this approach:
node.simdPivot.columns.3.y = -0.5
So if you want your model to precisely stand on an invisible detected plane move a pivot point in SceneKit toward desired place or set its position in 3D authoring tool (like 3dsMax or Blender).

How to set google map zoom level according to path & markers in swift

I am trying to show path in google maps from one place to another place I am getting something like this. but I need to show like this. Which means the whole path need to show & according to path on map, the zooming level should adjust
Here is the code which I tried to draw path from API. and here in let settingCam am setting camera to adjust to one of the location
func showingPathFromPickupLocToDropLoc(dropLat: Double, dropLong: Double){
let origin = "\(dropLat),\(dropLong)"
let destination = "\(dropLatitude),\(dropLongitude)"
let settingCam: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: CLLocationDegrees(dropLat), longitude: CLLocationDegrees(dropLong))
let url = URL(string: "https://maps.googleapis.com/maps/api/directions/json?origin=\(origin)&destination=\(destination)&key=\(NEWAPI.GOOGLE_APIKEY)")
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
if(error != nil){
print("error")
}else{
do{
let json = try JSONSerialization.jsonObject(with: data!, options:.allowFragments) as! [String : AnyObject]
if json["status"] as! String == "OK"{
let routes = json["routes"] as! [[String:AnyObject]]
OperationQueue.main.addOperation({
for route in routes{
let routeOverviewPolyline = route["overview_polyline"] as! [String:String]
let points = routeOverviewPolyline["points"]
let path = GMSPath.init(fromEncodedPath: points!)
self.PathFromPickupLocToDropLoc = GMSPolyline(path: path)
self.PathFromPickupLocToDropLoc.strokeColor = .gray
self.PathFromPickupLocToDropLoc.strokeWidth = 3.0
self.PathFromPickupLocToDropLoc.map = self.mapView
let camera = GMSCameraPosition.camera(withTarget: settingCam, zoom: 16.0)
self.mapView.animate(toLocation: settingCam)
self.mapView.animate(to: camera)
self.insertingMarkersFromPickupLocToDropLoc(dropLat: dropLat, dropLong: dropLong)
}
})
}
}catch let error as NSError{
print(error)
}
}
}).resume()
}
You need to do like this
DispatchQueue.main.async
{
if self.googleMap != nil
{
let bounds = GMSCoordinateBounds(path: path!)
self.googleMap!.animate(with: GMSCameraUpdate.fit(bounds, withPadding: 50.0))
}
}

ios - Could not open OBJ file when convert MDLAsset to MDLMesh

I'm working with demonstrating loading and texturing a .OBJ file using ModelIO.
This code bellow works fine when I use local file.
guard let url = Bundle.main.url(forResource: "myVase", withExtension: "obj") else {
fatalError("Failed to find model file.")
}
let asset = MDLAsset(url:url)
guard let object = asset.object(at: 0) as? MDLMesh else {
fatalError("Failed to get mesh from asset.")
}
But, when I change my code to use file from my Amazon S3 instead of local file. I got errors: "Could not open OBJ file" & "Failed to get mesh from asset."
Here is my code:
let url = URL.init(string: "https://s3.amazonaws.com/myObject/.../object.obj")
let asset = MDLAsset(url:url!)
guard let object = asset.object(at: 0) as? MDLMesh else {
fatalError("Failed to get mesh from asset.")
}
Note: I made the link public and free to download.
I fixed my issue. My issue is that I converted the file before the downloading is finished. Therefore, the local path is created but data is empty because download process hasn't finished yet.
To solve it, I use async to finish downloading first then converting it.
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("myVase.obj")
return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
Alamofire.download(urlString, to: destination).response { response in
if response.error == nil, let filePath = response.destinationURL?.path {
print(imagePath)
let myUrl = "file://" + filePath
let asset = MDLAsset(url:URL(string:myUrl)!)
guard let object = asset.object(at: 0) as? MDLMesh else {
fatalError("Failed to get mesh from asset.")
}
...
}
}