如何用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) } }
关键改进点说明
- 层级结构处理:通过节点栈
currentNodeStack追踪SDF中的<model>、<link>、<visual>层级,确保节点正确添加到父节点下。 - 多几何体支持:新增了
<sphere>、<cylinder>的解析逻辑,可根据需要扩展到mesh等其他几何体。 - 变换解析:处理SDF中的
<pose>属性,将位置和旋转转换为SceneKit的矩阵变换。 - 错误处理优化:增加了更明确的空值判断和日志输出,便于排查问题。
后续扩展建议
- 如果需要支持网格模型(
<mesh>元素),可以解析其中的<uri>字段,加载对应的DAE/OBJ文件并转换成SCNGeometry。 - 可以扩展材质解析逻辑,读取SDF中的
<material>属性来设置几何体的材质参数。 - 加入错误处理机制,捕获XML解析过程中的具体错误(比如调用
parser.parserError获取错误信息)。
内容的提问来源于stack exchange,提问作者thantzin win
相关产品推荐
相关产品推荐

