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

如何用Swift、UIKit和SceneKit加载并显示.sdf格式3D模型?

在Swift(UIKit/SceneKit)中加载SDF模型的实现方案

核心问题分析

SceneKit原生不支持SDF(Simulation Description Format)格式,必须通过解析SDF的XML结构,将其中的几何体、变换、层级信息转换成SceneKit的SCNNode和SCNGeometry对象。你的现有代码只实现了最基础的<box>元素解析,缺少对SDF层级结构、变换属性以及其他几何体的处理,这是解析失败的主要原因。

改进后的实现代码

下面是完善后的解析逻辑,支持常见几何体(box、sphere、cylinder)、节点变换(位置、旋转)以及层级结构:

import UIKit
import SceneKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        
        guard let sdfURL = Bundle.main.url(forResource: "model", withExtension: "sdf") else {
            print("SDF文件未找到")
            return
        }
        loadAndDisplaySDFModel(from: sdfURL)
    }
    
    func loadAndDisplaySDFModel(from url: URL) {
        let scene = SCNScene()
        
        guard let sdfNodes = parseSDFFile(at: url) else {
            print("解析SDF文件失败")
            return
        }
        
        sdfNodes.forEach { scene.rootNode.addChildNode($0) }
        
        let scnView = SCNView(frame: view.bounds)
        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.autoenablesDefaultLighting = true
        scnView.backgroundColor = .white
        view.addSubview(scnView)
    }
    
    func parseSDFFile(at url: URL) -> [SCNNode]? {
        guard let data = try? Data(contentsOf: url) else {
            print("读取SDF文件失败")
            return nil
        }
        
        let parser = XMLParser(data: data)
        let delegate = SDFParserDelegate()
        parser.delegate = delegate
        
        return parser.parse() ? delegate.rootNodes : nil
    }
}

class SDFParserDelegate: NSObject, XMLParserDelegate {
    var rootNodes: [SCNNode] = []
    private var currentNodeStack: [SCNNode] = []
    private var currentElement: String = ""
    private var currentTransform: SCNMatrix4 = .identity
    
    // 解析元素开始
    func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) {
        currentElement = elementName
        
        switch elementName {
        case "model", "link", "visual":
            // 创建节点并加入栈(处理层级)
            let node = SCNNode()
            if currentNodeStack.isEmpty {
                rootNodes.append(node)
            } else {
                currentNodeStack.last?.addChildNode(node)
            }
            currentNodeStack.append(node)
        case "box":
            createBoxNode(from: attributeDict)
        case "sphere":
            createSphereNode(from: attributeDict)
        case "cylinder":
            createCylinderNode(from: attributeDict)
        case "pose":
            parsePose(from: attributeDict)
        default:
            break
        }
    }
    
    // 解析元素结束
    func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) {
        if elementName == "model" || elementName == "link" || elementName == "visual" {
            // 节点结束,弹出栈
            if !currentNodeStack.isEmpty {
                currentNodeStack.removeLast()
            }
        }
        currentElement = ""
    }
    
    // 创建Box几何体节点
    private func createBoxNode(from attributes: [String: String]) {
        guard let sizeStr = attributes["size"],
              let currentNode = currentNodeStack.last else { return }
        
        let sizeComponents = sizeStr.split(separator: " ").compactMap { Double($0) }
        let width = CGFloat(sizeComponents[0] ?? 1.0)
        let height = CGFloat(sizeComponents[1] ?? 1.0)
        let length = CGFloat(sizeComponents[2] ?? 1.0)
        
        let box = SCNBox(width: width, height: height, length: length, chamferRadius: 0)
        box.firstMaterial?.diffuse.contents = UIColor.red
        currentNode.geometry = box
        // 应用变换
        currentNode.transform = currentTransform
        currentTransform = .identity
    }
    
    // 创建Sphere几何体节点
    private func createSphereNode(from attributes: [String: String]) {
        guard let radiusStr = attributes["radius"],
              let radius = Double(radiusStr),
              let currentNode = currentNodeStack.last else { return }
        
        let sphere = SCNSphere(radius: CGFloat(radius))
        sphere.firstMaterial?.diffuse.contents = UIColor.blue
        currentNode.geometry = sphere
        currentNode.transform = currentTransform
        currentTransform = .identity
    }
    
    // 创建Cylinder几何体节点
    private func createCylinderNode(from attributes: [String: String]) {
        guard let radiusStr = attributes["radius"],
              let heightStr = attributes["length"],
              let radius = Double(radiusStr),
              let height = Double(heightStr),
              let currentNode = currentNodeStack.last else { return }
        
        let cylinder = SCNCylinder(radius: CGFloat(radius), height: CGFloat(height))
        cylinder.firstMaterial?.diffuse.contents = UIColor.green
        currentNode.geometry = cylinder
        currentNode.transform = currentTransform
        currentTransform = .identity
    }
    
    // 解析Pose(位置+旋转)
    private func parsePose(from attributes: [String: String]) {
        guard let poseStr = attributes["pose"] else { return }
        let components = poseStr.split(separator: " ").compactMap { Double($0) }
        
        guard components.count >= 6 else { return }
        // SDF pose格式: x y z roll pitch yaw
        let position = SCNVector3(CGFloat(components[0]), CGFloat(components[1]), CGFloat(components[2]))
        let rotation = SCNVector4(CGFloat(components[3]), CGFloat(components[4]), CGFloat(components[5]), .pi/180)
        
        currentTransform = SCNMatrix4MakeRotation(rotation.w, rotation.x, rotation.y, rotation.z)
        currentTransform = SCNMatrix4Translate(currentTransform, position.x, position.y, position.z)
    }
}

关键改进点说明

  1. 层级结构处理:通过节点栈currentNodeStack追踪SDF中的<model>、<link>、<visual>层级,确保节点正确添加到父节点下。
  2. 多几何体支持:新增了<sphere>、<cylinder>的解析逻辑,可根据需要扩展到mesh等其他几何体。
  3. 变换解析:处理SDF中的<pose>属性,将位置和旋转转换为SceneKit的矩阵变换。
  4. 错误处理优化:增加了更明确的空值判断和日志输出,便于排查问题。

后续扩展建议

  • 如果需要支持网格模型(<mesh>元素),可以解析其中的<uri>字段,加载对应的DAE/OBJ文件并转换成SCNGeometry。
  • 可以扩展材质解析逻辑,读取SDF中的<material>属性来设置几何体的材质参数。
  • 加入错误处理机制,捕获XML解析过程中的具体错误(比如调用parser.parserError获取错误信息)。

内容的提问来源于stack exchange,提问作者thantzin win

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 07:32:17