关于 axiom-scenekit
面向维护 SceneKit 项目和规划迁移到 RealityKit 的开发指南,适用于调试渲染、物理、动画及 SwiftUI 集成。内容涵盖旧版 ARKit 用法,并说明 SceneKit 的弃用背景与迁移注意事项。
主要功能
- 01SceneKit 场景与渲染指南
- 02几何体、材质和灯光说明
- 03动画与物理处理
- 04SwiftUI 和 ARKit 集成
- 05RealityKit 迁移建议
适用场景
- 01维护现有 SceneKit 应用
- 02排查渲染或物理问题
- 03在 SwiftUI 中呈现场景
- 04规划向 RealityKit 迁移
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-scenekit安装后在 Claude Code 中输入/axiom-scenekit确认已加载。
详细介绍
SceneKit 开发指南
目的:安全地维护现有 SceneKit 代码,并规划迁移到 RealityKit iOS 版本:iOS 8+(SceneKit),iOS 26+ 起弃用 Xcode:Xcode 15+
何时使用此技能
在以下情况下使用此技能:
- 维护现有 SceneKit 代码
- 构建 SceneKit 原型(同时注意其弃用状态)
- 规划从 SceneKit 迁移到 RealityKit
- 调试 SceneKit 渲染、物理或动画问题
- 将 SceneKit 内容集成到 SwiftUI
- 通过 Model I/O 或 SCNSceneSource 加载 3D 模型
以下情况请勿使用此技能:
- 新建 3D 项目(使用
axiom-realitykit) - AR 体验(使用
axiom-realitykit) - visionOS 开发(使用
axiom-realitykit) - SpriteKit 2D 游戏(
axiom-spritekit) - Metal 着色器编程(
axiom-metal-migration-ref)
弃用背景
SceneKit 自 iOS 26 起进入软弃用状态(WWDC 2025)。这意味着:
- 现有应用仍可继续运行
- 不再新增功能,也不再提供常规错误修复
- 仅提供关键安全补丁
SceneView(SwiftUI)已在 iOS 26 正式弃用
Apple 后续推荐使用 RealityKit。 所有新的 3D 项目都应使用 RealityKit。SceneKit 知识对于维护旧代码以及在迁移过程中理解相关概念仍然很有价值。
在 RealityKit 中:ECS 架构取代场景图。关于完整的概念映射表,请参阅 axiom-scenekit-ref。
1. 心智模型
场景图架构
SceneKit 使用一棵节点树(SCNNode),附加到 SCNScene 中的根节点上。每个节点都有一个相对于其父节点的变换(位置、旋转、缩放)。
SCNScene
└── rootNode
├── cameraNode (SCNCamera)
├── lightNode (SCNLight)
├── playerNode (SCNGeometry + SCNPhysicsBody)
│ ├── weaponNode
│ └── particleNode (SCNParticleSystem)
└── environmentNode
├── groundNode
└── wallNodes
在 RealityKit 中:实体取代节点,组件取代节点属性。层级概念仍然存在,但行为由 Systems 驱动,而非节点回调。
坐标系
SceneKit 使用右手 Y 轴朝上的坐标系:
+Y (up)
|
|
+──── +X (right)
/
/
+Z (toward viewer)
这与 RealityKit 的坐标系一致,因此迁移时空间概念可以直接沿用。
变换层级
变换会沿父节点 → 子节点级联。子节点的世界变换 = 父节点的世界变换 × 子节点的局部变换。
let parent = SCNNode()
parent.position = SCNVector3(10, 0, 0)
let child = SCNNode()
child.position = SCNVector3(0, 5, 0)
parent.addChildNode(child)
// child.worldPosition = (10, 5, 0)
// child.position (local) = (0, 5, 0)
在 RealityKit 中:概念相同。entity.position 是局部位置,entity.position(relativeTo: nil) 返回世界位置。
2. 场景设置与渲染
SCNView(UIKit)
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene(named: "scene.scn")
sceneView.allowsCameraControl = true
sceneView.showsStatistics = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)
SceneView(SwiftUI)——iOS 26 起弃用
// Still works but deprecated. Use SCNViewRepresentable for new code.
import SceneKit
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.allowsCameraControl, .autoenablesDefaultLighting]
)
SCNViewRepresentable(SwiftUI 替代方案)
struct SceneKitView: UIViewRepresentable {
let scene: SCNScene
func makeUIView(context: Context) -> SCNView {
let view = SCNView()
view.scene = scene
view.allowsCameraControl = true
view.autoenablesDefaultLighting = true
return view
}
func updateUIView(_ view: SCNView, context: Context) {}
}
在 RealityKit 中:在 SwiftUI 中使用 RealityView,无需 UIViewRepresentable。
3. 几何体与材质
内置几何体
let box = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.1)
let sphere = SCNSphere(radius: 0.5)
let cylinder = SCNCylinder(radius: 0.3, height: 1)
let plane = SCNPlane(width: 2, height: 2)
let torus = SCNTorus(ringRadius: 1, pipeRadius: 0.3)
let capsule = SCNCapsule(capRadius: 0.3, height: 1)
let cone = SCNCone(topRadius: 0, bottomRadius: 0.5, height: 1)
let tube = SCNTube(innerRadius: 0.3, outerRadius: 0.5, height: 1)
let text = SCNText(string: "Hello", extrusionDepth: 0.2)
PBR 材质
let material = SCNMaterial()
material.lightingModel = .physicallyBased
material.diffuse.contents = UIColor.red // or UIImage
material.metalness.contents = 0.8
material.roughness.contents = 0.2
material.normal.contents = UIImage(named: "normal_map")
material.ambientOcclusion.contents = UIImage(named: "ao_map")
let node = SCNNode(geometry: sphere)
node.geometry?.firstMaterial = material
在 RealityKit 中:使用 PhysicallyBasedMaterial,其属性类似,但 API 不同。映射关系请参阅 axiom-scenekit-ref 第 1 部分。
着色器修改器
SceneKit 支持在特定入口点注入 GLSL/Metal 着色器片段:
// Fragment modifier — custom effect on surface
material.shaderModifiers = [
.fragment: """
float stripe = sin(_surface.position.x * 20.0);
_output.color.rgb *= step(0.0, stripe);
"""
]
入口点:.geometry、.surface、.lightingModel、.fragment
在 RealityKit 中:使用 Reality Composer Pro 中的 ShaderGraphMaterial,或使用 Metal 函数创建 CustomMaterial。
4. 光照
光源类型
| 类型 | 说明 | 阴影 |
|---|---|---|
.omni | 点光源,向所有方向发光 | 否 |
.directional | 平行光线(太阳光) | 是 |
.spot | 锥形光束 | 是 |
.area | 矩形发光体(柔和阴影) | 是 |
.IES | 真实世界光照配置文件 | 是 |
.ambient | 均匀、无方向 | 否 |
.probe | 来自立方体贴图的环境光照 | 否 |
let light = SCNLight()
light.type = .directional
light.intensity = 1000
light.castsShadow = true
light.shadowRadius = 3
light.shadowSampleCount = 8
let lightNode = SCNNode()
lightNode.light = light
lightNode.eulerAngles = SCNVector3(-Float.pi / 4, 0, 0)
scene.rootNode.addChildNode(lightNode)
在 RealityKit 中:将 DirectionalLightComponent、PointLightComponent、SpotLightComponent 用作实体上的组件。基于图像的光照通过 EnvironmentResource 实现。
5. 动画
SCNAction(声明式)
let moveUp = SCNAction.moveBy(x: 0, y: 2, z: 0, duration: 1)
let fadeOut = SCNAction.fadeOut(duration: 0.5)
let sequence = SCNAction.sequence([moveUp, fadeOut])
let forever = SCNAction.repeatForever(moveUp.reversed())
node.runAction(sequence)
隐式动画(SCNTransaction)
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
node.position = SCNVector3(0, 5, 0)
node.opacity = 0.5
SCNTransaction.commit()
显式动画(CAAnimation 桥接)
let animation = CABasicAnimation(keyPath: "rotation")
animation.toValue = NSValue(scnVector4: SCNVector4(0, 1, 0, Float.pi * 2))
animation.duration = 2
animation.repeatCount = .infinity
node.addAnimation(animation, forKey: "spin")
从文件加载动画
let scene = SCNScene(named: "character.dae")!
let animationPlayer = scene.rootNode
.childNode(withName: "mixamorig:Hips", recursively: true)!
.animationPlayer(forKey: nil)!
characterNode.addAnimationPlayer(animationPlayer, forKey: "walk")
animationPlayer.play()
在 RealityKit 中:使用从 USD 文件加载的动画调用 entity.playAnimation()。使用 entity.move(to:relativeTo:duration:) 播放变换动画。
6. 物理
物理体
// Dynamic — simulation controls position
node.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(geometry: node.geometry!, options: nil))
// Static — immovable collision surface
ground.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
// Kinematic — code controls position, participates in collisions
platform.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil)
碰撞类别
struct PhysicsCategory {
static let player: Int = 1 << 0 // 1
static let enemy: Int = 1 << 1 // 2
static let projectile: Int = 1 << 2 // 4
static let wall: Int = 1 << 3 // 8
}
playerNode.physicsBody?.categoryBitMask = PhysicsCategory.player
playerNode.physicsBody?.collisionBitMask = PhysicsCategory.wall | PhysicsCategory.enemy
playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.enemy | PhysicsCategory.projectile
接触代理
class GameScene: SCNScene, SCNPhysicsContactDelegate {
func setupPhysics() {
physicsWorld.contactDelegate = self
}
func physicsWorld(_ world: SCNPhysicsWorld,
didBegin contact: SCNPhysicsContact) {
let nodeA = contact.nodeA
let nodeB = contact.nodeB
// Handle collision
}
}
在 RealityKit 中:使用 PhysicsBodyComponent、CollisionComponent,并通过 scene.subscribe(to: CollisionEvents.Began.self) 订阅碰撞事件。
7. 命中测试与交互
// In SCNView tap handler
let results = sceneView.hitTest(tapLocation, options: [
.searchMode: SCNHitTestSearchMode.closest.rawValue,
.boundingBoxOnly: false
])
if let hit = results.first {
let tappedNode = hit.node
let worldPosition = hit.worldCoordinates
}
在 RealityKit 中:使用 ManipulationComponent 实现拖动、旋转和缩放手势,或使用基于碰撞的命中测试。
8. 资源管线
支持的格式
| 格式 | 扩展名 | 说明 |
|---|---|---|
| USD/USDZ | .usdz、.usda、.usdc | 首选格式,SceneKit 和 RealityKit 均支持 |
| Collada | .dae | 旧格式,仍受支持 |
| SceneKit 归档 | .scn | Xcode 专用,无法移植到 RealityKit |
| Wavefront OBJ | .obj | 仅包含几何体,不含动画 |
| Alembic | .abc | 动画烘焙 |
加载模型
// From bundle
let scene = SCNScene(named: "model.usdz")!
// From URL
let scene = try SCNScene(url: modelURL, options: nil)
// Via Model I/O (for format conversion)
let asset = MDLAsset(url: modelURL)
let scene = SCNScene(mdlAsset: asset)
迁移提示:迁移到 RealityKit 之前,使用 xcrun scntool --convert file.scn --format usdz 将 .scn 文件转换为 .usdz。
9. ARKit 集成(旧版)
// ARSCNView — SceneKit + ARKit (legacy approach)
let arView = ARSCNView(frame: view.bounds)
arView.delegate = self
arView.session.run(ARWorldTrackingConfiguration())
// Adding virtual content at anchors
func renderer(_ renderer: SCNSceneRenderer,
didAdd node: SCNNode, for anchor: ARAnchor) {
let box = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0)
node.addChildNode(SCNNode(geometry: box))
}
在 RealityKit 中:使用 RealityView 和 AnchorEntity 类型。ARSCNView 已属旧版——所有新的 AR 开发都应使用 RealityKit。
10. 反模式
反模式 1:用 SceneKit 启动新项目
时间成本:最终必须迁移时,需要数周返工
SceneKit 已弃用。即使初期学习曲线更陡,新项目也应从一开始就使用 RealityKit。
反模式 2:不转换 USDZ 就使用 .scn 文件
时间成本:开始迁移时需要数小时
.scn 文件是 SceneKit 专用的,无法在 RealityKit 中加载。请尽早转换:
xcrun scntool --convert model.scn --format usdz --output model.usdz
反模式 3:深度定制着色器修改器
时间成本:迁移期间需要彻底重写
SceneKit 着色器修改器使用专有的入口点系统。在这方面投入大量精力,无法移植到 RealityKit 的 ShaderGraphMaterial。
反模式 4:依赖 SCNRenderer 构建自定义管线
时间成本:迁移期间需要重新设计架构
如果需要自定义渲染管线,请直接基于 Metal 构建,或使用 RealityRenderer(RealityKit 的 Metal 级 API)。
反模式 5:忽略弃用警告
时间成本:Apple 移除 API 时可能出现意外故障
跟踪 SceneView 的弃用警告,并规划 UIViewRepresentable 后备方案或迁移到 RealityKit。
反模式 6:在循环中创建数百个节点
时间成本:调试帧率下降需要 2-4 小时,且常被误诊为 GPU 问题
// ❌ WRONG: Each SCNNode has overhead (transform, bounding box, hit test)
for i in 0..<500 {
let node = SCNNode(geometry: SCNSphere(radius: 0.05))
node.position = randomPosition()
scene.rootNode.addChildNode(node) // 500 nodes = terrible frame rate
}
// ✅ RIGHT: Use SCNParticleSystem for particle-like effects
let particles = SCNParticleSystem()
particles.birthRate = 500
particles.particleSize = 0.05
particles.emitterShape = SCNBox(width: 5, height: 5, length: 5, chamferRadius: 0)
particleNode.addParticleSystem(particles)
// ✅ RIGHT: Use geometry instancing for identical objects
let source = SCNGeometrySource(/* instance transforms */)
geometry.levelsOfDetail = [SCNLevelOfDetail(geometry: lowPoly, screenSpaceRadius: 20)]
规则:如果有超过 50 个相同对象,请使用 SCNParticleSystem 或展平几何体。如果对象各不相同,请使用 SCNNode.flattenedClone() 减少绘制调用。
11. 迁移决策树
Should you migrate to RealityKit?
│
├─ Is this a new project?
│ └─ YES → Use RealityKit from the start. No question.
│
├─ Does the app need AR features?
│ └─ YES → Migrate. ARSCNView is legacy, RealityKit is the only forward path.
│
├─ Does the app target visionOS?
│ └─ YES → Must migrate. SceneKit doesn't support visionOS spatial features.
│
├─ Is the codebase heavily invested in SceneKit?
│ ├─ YES, and app is stable → Maintain in SceneKit for now, plan phased migration.
│ └─ YES, but needs new features → Migrate incrementally (new features in RealityKit).
│
├─ Is performance a concern?
│ └─ YES → RealityKit is optimized for Apple Silicon with Metal-first rendering.
│
└─ Is the app in maintenance mode?
└─ YES → Keep SceneKit until critical. Security patches will continue.
12. 压力场景
场景 1:“就用 SceneKit 吧,它运行得很好”
压力:团队熟悉 SceneKit,且有交付期限
错误做法:因为团队熟悉 SceneKit,就用它启动新项目。
正确做法:投入时间学习 RealityKit。SceneKit 不会再获得新功能。等待越久,迁移负担就越大。
回应模板:“SceneKit 自 iOS 26 起已弃用。用它开始新工作会产生迁移负担,而且会随着每项新增功能不断增加。RealityKit 的 ECS 模型有所不同,但可以学会——现在就投入时间吧。”
场景 2:“我们没时间学习 RealityKit”
压力:工期紧迫,团队不熟悉 ECS
错误做法:为了赶上工期,所有内容都用 SceneKit 构建。
正确做法:必要时用 SceneKit 构建原型,但要记录每一项 SceneKit 依赖,并规划迁移。从一开始就使用 USDZ 资源,确保它们可移植。
反驳模板:“我们使用 USDZ 资源,并尽量精简 SceneKit 层。迁移时,资源可以直接转移,只需更改代码层。”
场景 3:“一次性迁移所有内容”
压力:希望干净利落地完成迁移
错误做法:试图一次性将整个 SceneKit 代码库重写为 RealityKit。
正确做法:逐步迁移。新功能使用 RealityKit。现有 SceneKit 代码保留,直到需要修改为止。使用 Swift 包进行模块化(参见 Apple 的迁移指南)。
反驳模板:“Apple 自己的迁移指南建议将代码模块化为 Swift 包,并逐个系统进行迁移。大爆炸式重写可能会在整个应用中引入新的错误。”
代码审查清单
- 面向 iOS 26+ 的项目中,不添加新的 SceneKit 代码,除非有迁移计划
- 资源采用 USDZ 格式(而非 .scn),以确保可移植性
- 未明确对应的 RealityKit 替代方案时,不进行深度着色器修改器定制
- 使用 SCNTransaction 实现隐式动画(不要在没有动画上下文时直接更改属性)
- 显式设置物理 categoryBitMask(不要依赖默认值)
- 设置接触代理并添加协议遵循
- 在完成处理程序和闭包中使用
[weak self] - 开发期间启用调试叠加层(
showsStatistics = true)
资源
WWDC:2014-609、2014-610、2017-604、2019-612
文档:/scenekit、/scenekit/scnscene、/scenekit/scnnode、/scenekit/scnmaterial、/scenekit/scnphysicsbody
技能:axiom-scenekit-ref、axiom-realitykit、axiom-realitykit-ref