如何在ARKit/RealityKit应用中加载GLB及实现格式转换
问题描述
我开发了一个模块,用来处理3D .glb文件的URL:先下载到本地文件系统,再加载为Entity并锚定。目前代码完成了大部分,但.glb和.gltf文件加载失败,.usdz文件可以正常加载;把同一份.glb转成.usdz后能正常加载操作,但从OpenSea的GLB文件URL加载就失败。想咨询两个问题:
- 有没有加载GLB的正确方法?我是不是遗漏了步骤?
- 能不能在应用内把GLB转换为USDZ?目前只找到命令行工具。
相关代码
RealityViewController.swift
import UIKit import ARKit import RealityKit import Combine class RealityViewController: UIViewController { var arView: ARView = ARView(frame: .zero) override func viewDidLoad() { super.viewDidLoad() // Set up the ARView's session configuration for LiDAR scanning let config = ARWorldTrackingConfiguration() if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { config.sceneReconstruction = .mesh } // Enable horizontal and vertical plane detection config.planeDetection = [.horizontal, .vertical] // Enable automatic environment texturing config.environmentTexturing = .automatic arView.environment.sceneUnderstanding.options = [] arView.environment.sceneUnderstanding.options.insert(.receivesLighting) arView.environment.sceneUnderstanding.options.insert(.occlusion) arView.environment.sceneUnderstanding.options.insert(.collision) arView.environment.sceneUnderstanding.options.insert(.physics) arView.renderOptions = [.disableFaceMesh, .disableHDR, .disableMotionBlur, .disableCameraGrain, .disableDepthOfField] // Run the session with the configuration arView.session.run(config) // Set the frame of your ARView to cover the entire screen arView.frame = view.bounds // Add your ARView to the view hierarchy view.addSubview(arView) // Make sure ARView's autoresizingMask is set so it resizes with its parent view arView.autoresizingMask = [.flexibleWidth, .flexibleHeight] } func loadModel(from url: URL) { print("Checking file at \(url.absoluteString)") if FileManager.default.fileExists(atPath: url.path) { print("File exists, loading model") DispatchQueue.main.async { do { let entity = try Entity.load(contentsOf: URL(string: url.absoluteString)!) if let anchorEntity = entity as? AnchorEntity { DispatchQueue.main.async { self.arView.scene.addAnchor(anchorEntity) print("AnchorEntity anchored") } }else{ let anchor = AnchorEntity() anchor.addChild(entity) DispatchQueue.main.async { self.arView.scene.addAnchor(anchor) print("ModelEntity anchored") } } print("Finished loading") } catch { print("Unable to load model: \(error)") } } } else { print("File does not exist, GLB download have failed") } } }
RealityKitView.swift
import SwiftUI struct RealityKitView: UIViewControllerRepresentable { let url: URL var realityViewController = RealityViewController() func makeUIViewController(context: Context) -> RealityViewController { print("makeUIViewController - realityViewController") return realityViewController } func updateUIViewController(_ uiViewController: RealityViewController, context: Context) {} func loadModel() { print("loadModel") GLBManager.shared.downloadAndStoreGLB(from: url) { savedURL in if let savedURL = savedURL { DispatchQueue.main.async { self.realityViewController.loadModel(from: savedURL) } } } } }
GLBManager.swift
import Foundation class GLBManager { static let shared = GLBManager() func downloadAndStoreGLB(from url: URL, completion: @escaping (URL?) -> Void) { let task = URLSession.shared.downloadTask(with: url) { localURL, urlResponse, error in guard let localURL = localURL else { completion(nil) return } do { let fileName = url.lastPathComponent let cleanedFileName = fileName.replacingOccurrences(of: " ", with: "_") let applicationSupportDirectory = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let savedURL = applicationSupportDirectory.appendingPathComponent(cleanedFileName) try FileManager.default.moveItem(at: localURL, to: savedURL) completion(savedURL) } catch { print("File error: \(error)") completion(nil) } } task.resume() } }
RealityContentView.swift
//https://openseauserdata.com/files/1e23b556e07fb653bb538f8ffc2ba42d.glb //https://openseauserdata.com/files/b1e1e80a7a2006598b20e3f01097c359.glb //https://openseauserdata.com/files/f886842eb950813ba2f7ba775de327fc.glb //WHEELS //https://openseauserdata.com/files/afb49aede036bcb4f37f5785b88b9732.glb //sample //https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/2.0/2CylinderEngine/glTF-Binary/2CylinderEngine.glb //https://github.com/KhronosGroup/glTF-Sample-Models/blob/master/1.0/Avocado/glTF-Embedded/Avocado.gltf //https://developer.apple.com/ar/photogrammetry/AirForce.usdz struct RealityContentView: View { var body: some View { if let url = URL(string: "https://developer.apple.com/augmented-reality/quick-look/models/gramophone/gramophone.usdz") { let realityKitView = RealityKitView(url: url) realityKitView .onAppear { realityKitView.loadModel() } .edgesIgnoringSafeArea(.all) } } }
解决方案
1. 修复GLB加载失败的问题
你的代码里存在一个明显错误,导致GLB加载失败:
在loadModel(from:)方法中,你把本地文件URL转成字符串后再重新创建URL:
let entity = try Entity.load(contentsOf: URL(string: url.absoluteString)!)
这是多余且危险的操作——本地文件URL的absoluteString以file://开头,虽然能转成URL,但强制解包(!)会在转换失败时崩溃。直接使用传入的url即可:
let entity = try Entity.load(contentsOf: url)
除此之外,还要检查以下几点:
- 文件完整性:确保下载的GLB文件没有损坏。可以在下载完成后验证文件大小是否和服务器上的一致,或者用MD5校验。
- 格式兼容性:RealityKit仅支持glTF 2.0格式的GLB/GLTF文件,不支持glTF 1.0。OpenSea的部分GLB可能使用了RealityKit不支持的自定义扩展(比如某些非标准材质、动画扩展),这种情况下可以尝试用ModelIO先预处理文件。
- 加载选项:尝试指定加载选项,关闭一些可能导致兼容问题的特性:
let options = Entity.LoadOptions() options.loadingPolicy = .loadAll options.shouldGenerateCollisionShapes = false let entity = try Entity.load(contentsOf: url, options: options)
2. 应用内将GLB转换为USDZ
苹果没有提供官方的应用内转换API,但可以通过ModelIO框架实现GLB到USDZ的转换:
ModelIO支持加载glTF/GLB文件,然后导出为USDZ格式。以下是实现代码:
转换方法示例
import ModelIO import SceneKit func convertGLBtoUSDZ(glbURL: URL, outputURL: URL) -> Bool { do { // 加载GLB文件为MDLAsset let asset = MDLAsset(url: glbURL) // 导出为USDZ格式 try asset.export(to: outputURL, preserveTopLevelHierarchy: true) return true } catch { print("转换失败:\(error)") return false } }
在你的代码中集成
修改GLBManager的下载方法,在保存文件后自动转换为USDZ:
func downloadAndConvertGLBtoUSDZ(from url: URL, completion: @escaping (URL?) -> Void) { let task = URLSession.shared.downloadTask(with: url) { localURL, _, error in guard let localURL = localURL else { completion(nil) return } do { let fileName = url.lastPathComponent.replacingOccurrences(of: " ", with: "_") let usdzFileName = fileName.replacingOccurrences(of: ".glb", with: ".usdz").replacingOccurrences(of: ".gltf", with: ".usdz") let appSupportDir = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) let usdzURL = appSupportDir.appendingPathComponent(usdzFileName) // 转换GLB到USDZ if convertGLBtoUSDZ(glbURL: localURL, outputURL: usdzURL) { completion(usdzURL) } else { completion(nil) } } catch { print("文件处理失败:\(error)") completion(nil) } } task.resume() }
注意:
- ModelIO对glTF的支持依赖于系统版本,iOS 13及以上支持加载glTF 2.0。
- 某些复杂的GLB文件可能转换后丢失部分特性(比如自定义动画、材质),需要测试验证。
内容的提问来源于stack exchange,提问作者Csabi
相关产品推荐
相关产品推荐

