You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在ARKit/RealityKit应用中加载GLB及实现格式转换

问题描述

我开发了一个模块,用来处理3D .glb文件的URL:先下载到本地文件系统,再加载为Entity并锚定。目前代码完成了大部分,但.glb和.gltf文件加载失败,.usdz文件可以正常加载;把同一份.glb转成.usdz后能正常加载操作,但从OpenSea的GLB文件URL加载就失败。想咨询两个问题:

  1. 有没有加载GLB的正确方法?我是不是遗漏了步骤?
  2. 能不能在应用内把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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 13:04:55