axiom-scenekit by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-scenekit目的:安全维护现有 SceneKit 代码并规划迁移至 RealityKit iOS 版本:iOS 8+ (SceneKit),iOS 26+ 起已弃用 Xcode:Xcode 15+
在以下情况下使用此技能:
请勿在以下情况下使用此技能:
axiom-realitykit)axiom-realitykit)axiom-realitykit)axiom-spritekit)axiom-metal-migration-ref)广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
SceneKit 自 iOS 26 (WWDC 2025) 起已软弃用。这意味着:
SceneView (SwiftUI) 在 iOS 26 中正式弃用Apple 的前进方向是 RealityKit。 所有新的 3D 项目都应使用 RealityKit。SceneKit 知识对于维护遗留代码和在迁移过程中理解概念仍然有价值。
在 RealityKit 中:ECS 架构取代了场景图。完整的概念映射表请参见 axiom-scenekit-ref。
SceneKit 使用一个节点树(SCNNode),这些节点附加在 SCNScene 中的根节点上。每个节点相对于其父节点都有一个变换(位置、旋转、缩放)。
SCNScene
└── rootNode
├── cameraNode (SCNCamera)
├── lightNode (SCNLight)
├── playerNode (SCNGeometry + SCNPhysicsBody)
│ ├── weaponNode
│ └── particleNode (SCNParticleSystem)
└── environmentNode
├── groundNode
└── wallNodes
在 RealityKit 中:实体(Entities)取代节点。组件(Components)取代节点属性。层次结构概念仍然存在,但行为由系统(Systems)驱动,而非节点回调。
SceneKit 使用右手 Y 轴向上坐标系:
+Y (上)
|
|
+──── +X (右)
/
/
+Z (朝向观察者)
这与 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 (局部) = (0, 5, 0)
在 RealityKit 中:概念相同。entity.position 是局部的,entity.position(relativeTo: nil) 给出世界位置。
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene(named: "scene.scn")
sceneView.allowsCameraControl = true
sceneView.showsStatistics = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)
// 仍然有效但已弃用。新代码请使用 SCNViewRepresentable。
import SceneKit
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.allowsCameraControl, .autoenablesDefaultLighting]
)
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。
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)
let material = SCNMaterial()
material.lightingModel = .physicallyBased
material.diffuse.contents = UIColor.red // 或 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 中:使用具有类似属性但不同 API 表面的 PhysicallyBasedMaterial。映射关系请参见 axiom-scenekit-ref 第一部分。
SceneKit 支持在特定入口点注入 GLSL/Metal 着色器片段:
// 片段修改器 — 表面上的自定义效果
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。
| 类型 | 描述 | 阴影 |
|---|---|---|
.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 实现。
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.begin()
SCNTransaction.animationDuration = 0.5
node.position = SCNVector3(0, 5, 0)
node.opacity = 0.5
SCNTransaction.commit()
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:) 实现。
// 动态体 — 模拟控制位置
node.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(geometry: node.geometry!, options: nil))
// 静态体 — 不可移动的碰撞表面
ground.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
// 运动体 — 代码控制位置,参与碰撞
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
// 处理碰撞
}
}
在 RealityKit 中:使用 PhysicsBodyComponent、CollisionComponent,并通过 scene.subscribe(to: CollisionEvents.Began.self) 订阅碰撞事件。
// 在 SCNView 点击处理器中
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 进行拖拽/旋转/缩放手势,或使用基于碰撞的命中测试。
| 格式 | 扩展名 | 备注 |
|---|---|---|
| USD/USDZ | .usdz, .usda, .usdc | 首选格式,在 SceneKit 和 RealityKit 中均可工作 |
| Collada | .dae | 遗留格式,仍受支持 |
| SceneKit 归档 | .scn | Xcode 特定格式,不可移植到 RealityKit |
| Wavefront OBJ | .obj | 仅几何体,无动画 |
| Alembic | .abc | 动画烘焙 |
// 从 bundle 加载
let scene = SCNScene(named: "model.usdz")!
// 从 URL 加载
let scene = try SCNScene(url: modelURL, options: nil)
// 通过 Model I/O 加载(用于格式转换)
let asset = MDLAsset(url: modelURL)
let scene = SCNScene(mdlAsset: asset)
迁移提示:在迁移到 RealityKit 之前,使用 xcrun scntool --convert file.scn --format usdz 将 .scn 文件转换为 .usdz。
// ARSCNView — SceneKit + ARKit(遗留方法)
let arView = ARSCNView(frame: view.bounds)
arView.delegate = self
arView.session.run(ARWorldTrackingConfiguration())
// 在锚点处添加虚拟内容
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 中:使用带有 AnchorEntity 类型的 RealityView。ARSCNView 是遗留的 — 所有新的 AR 开发都应使用 RealityKit。
时间成本:最终必须迁移时,需要数周的重构工作
SceneKit 已弃用。新项目应从一开始就使用 RealityKit,即使最初的学习曲线更陡峭。
时间成本:迁移开始时需要数小时
.scn 文件是 SceneKit 特定的,无法在 RealityKit 中加载。尽早转换:
xcrun scntool --convert model.scn --format usdz --output model.usdz
时间成本:迁移期间完全重写
SceneKit 着色器修改器使用专有的入口点系统。在此方面的深度投入对 RealityKit 的 ShaderGraphMaterial 完全没有可移植性。
时间成本:迁移期间需要重新设计架构
如果需要自定义渲染管线,请直接基于 Metal 构建,或使用 RealityRenderer(RealityKit 的 Metal 级 API)。
时间成本:当 Apple 移除 API 时出现意外崩溃
跟踪 SceneView 弃用警告,并规划 UIViewRepresentable 回退方案或 RealityKit 迁移。
时间成本:花费 2-4 小时调试帧率下降,常被误诊为 GPU 问题
// ❌ 错误:每个 SCNNode 都有开销(变换、包围盒、命中测试)
for i in 0..<500 {
let node = SCNNode(geometry: SCNSphere(radius: 0.05))
node.position = randomPosition()
scene.rootNode.addChildNode(node) // 500 个节点 = 极差的帧率
}
// ✅ 正确:对于类似粒子的效果,使用 SCNParticleSystem
let particles = SCNParticleSystem()
particles.birthRate = 500
particles.particleSize = 0.05
particles.emitterShape = SCNBox(width: 5, height: 5, length: 5, chamferRadius: 0)
particleNode.addParticleSystem(particles)
// ✅ 正确:对于相同的对象,使用几何体实例化
let source = SCNGeometrySource(/* 实例变换 */)
geometry.levelsOfDetail = [SCNLevelOfDetail(geometry: lowPoly, screenSpaceRadius: 20)]
规则:如果 >50 个相同对象,使用 SCNParticleSystem 或展平几何体。如果对象不同,使用 SCNNode.flattenedClone() 来减少绘制调用。
是否应该迁移到 RealityKit?
│
├─ 这是新项目吗?
│ └─ 是 → 从一开始就使用 RealityKit。无需犹豫。
│
├─ 应用需要 AR 功能吗?
│ └─ 是 → 迁移。ARSCNView 是遗留的,RealityKit 是唯一的前进方向。
│
├─ 应用目标平台是 visionOS 吗?
│ └─ 是 → 必须迁移。SceneKit 不支持 visionOS 的空间功能。
│
├─ 代码库是否深度依赖 SceneKit?
│ ├─ 是,且应用稳定 → 暂时在 SceneKit 中维护,规划分阶段迁移。
│ └─ 是,但需要新功能 → 增量迁移(新功能在 RealityKit 中实现)。
│
├─ 性能是关注点吗?
│ └─ 是 → RealityKit 针对 Apple Silicon 和 Metal 优先渲染进行了优化。
│
└─ 应用处于维护模式吗?
└─ 是 → 保持 SceneKit 直到关键时期。安全补丁将继续提供。
压力:团队熟悉 SceneKit,有交付期限
错误做法:因为团队熟悉而启动新的 SceneKit 项目。
正确做法:投入时间学习 RealityKit。SceneKit 将不会获得新功能。等待越久,迁移债务就越大。
反驳模板:"SceneKit 自 iOS 26 起已弃用。在其中启动新工作会随着我们添加的每个功能而增加迁移债务。RealityKit 的 ECS 模型不同但可以学习 — 让我们现在就投入时间。"
压力:截止日期紧张,团队不熟悉 ECS
错误做法:为了满足截止日期,所有内容都用 SceneKit 构建。
正确做法:如果必要,用 SceneKit 构建原型,但记录每个 SceneKit 依赖项并规划迁移。从一开始就使用 USDZ 资产,以便它们可以移植。
反驳模板:"让我们使用 USDZ 资产,并保持 SceneKit 层尽可能薄。当我们迁移时,资产可以直接转移,只有代码层需要改变。"
压力:希望进行干净的迁移
错误做法:尝试一次性将整个 SceneKit 代码库重写为 RealityKit。
正确做法:增量迁移。新功能在 RealityKit 中实现。现有的 SceneKit 代码保持不变,直到需要修改。使用 Swift 包进行模块化(遵循 Apple 的迁移指南)。
反驳模板:"Apple 自己的迁移指南建议将代码模块化为 Swift 包,并按系统迁移。大爆炸式的重写有在整个应用中引入新错误的风险。"
[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
每周安装数
71
代码仓库
GitHub 星标数
601
首次出现
2026年2月5日
安全审计
安装于
opencode67
gemini-cli64
codex63
github-copilot62
kimi-cli60
cursor60
Purpose : Maintain existing SceneKit code safely and plan migration to RealityKit iOS Version : iOS 8+ (SceneKit), deprecated iOS 26+ Xcode : Xcode 15+
Use this skill when:
Do NOT use this skill for:
axiom-realitykit)axiom-realitykit)axiom-realitykit)axiom-spritekit)axiom-metal-migration-ref)SceneKit is soft-deprecated as of iOS 26 (WWDC 2025). This means:
SceneView (SwiftUI) is formally deprecated in iOS 26Apple's forward path is RealityKit. All new 3D projects should use RealityKit. SceneKit knowledge remains valuable for maintaining legacy code and understanding concepts during migration.
In RealityKit : ECS architecture replaces scene graph. See axiom-scenekit-ref for the complete concept mapping table.
SceneKit uses a tree of nodes (SCNNode) attached to a root node in an SCNScene. Each node has a transform (position, rotation, scale) relative to its parent.
SCNScene
└── rootNode
├── cameraNode (SCNCamera)
├── lightNode (SCNLight)
├── playerNode (SCNGeometry + SCNPhysicsBody)
│ ├── weaponNode
│ └── particleNode (SCNParticleSystem)
└── environmentNode
├── groundNode
└── wallNodes
In RealityKit : Entities replace nodes. Components replace node properties. The hierarchy concept persists, but behavior is driven by Systems rather than node callbacks.
SceneKit uses a right-handed Y-up coordinate system:
+Y (up)
|
|
+──── +X (right)
/
/
+Z (toward viewer)
This matches RealityKit's coordinate system, so spatial concepts transfer directly during migration.
Transforms cascade parent → child. A child's world transform = parent's world transform × child's local transform.
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)
In RealityKit : Same concept. entity.position is local, entity.position(relativeTo: nil) gives world position.
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene(named: "scene.scn")
sceneView.allowsCameraControl = true
sceneView.showsStatistics = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)
// Still works but deprecated. Use SCNViewRepresentable for new code.
import SceneKit
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.allowsCameraControl, .autoenablesDefaultLighting]
)
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) {}
}
In RealityKit : Use RealityView in SwiftUI — no UIViewRepresentable needed.
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)
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
In RealityKit : Use PhysicallyBasedMaterial with similar properties but different API surface. See axiom-scenekit-ref Part 1 for the mapping.
SceneKit supports GLSL/Metal shader snippets injected at specific entry points:
// Fragment modifier — custom effect on surface
material.shaderModifiers = [
.fragment: """
float stripe = sin(_surface.position.x * 20.0);
_output.color.rgb *= step(0.0, stripe);
"""
]
Entry points: .geometry, .surface, .lightingModel, .fragment
In RealityKit : Use ShaderGraphMaterial with Reality Composer Pro, or CustomMaterial with Metal functions.
| Type | Description | Shadows |
|---|---|---|
.omni | Point light, radiates in all directions | No |
.directional | Parallel rays (sun) | Yes |
.spot | Cone-shaped beam | Yes |
.area | Rectangle emitter (soft shadows) | Yes |
.IES | Real-world light profile | Yes |
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)
In RealityKit : Use DirectionalLightComponent, PointLightComponent, SpotLightComponent as components on entities. Image-based lighting via EnvironmentResource.
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.begin()
SCNTransaction.animationDuration = 0.5
node.position = SCNVector3(0, 5, 0)
node.opacity = 0.5
SCNTransaction.commit()
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()
In RealityKit : Use entity.playAnimation() with animations loaded from USD files. Transform animations via entity.move(to:relativeTo:duration:).
// 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
}
}
In RealityKit : Use PhysicsBodyComponent, CollisionComponent, and collision event subscriptions via scene.subscribe(to: CollisionEvents.Began.self).
// 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
}
In RealityKit : Use ManipulationComponent for drag/rotate/scale gestures, or collision-based hit testing.
| Format | Extension | Notes |
|---|---|---|
| USD/USDZ | .usdz, .usda, .usdc | Preferred format, works in both SceneKit and RealityKit |
| Collada | .dae | Legacy, still supported |
| SceneKit Archive | .scn | Xcode-specific, not portable to RealityKit |
| Wavefront OBJ | .obj | Geometry only, no animations |
// 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)
Migration tip : Convert .scn files to .usdz using xcrun scntool --convert file.scn --format usdz before migrating to RealityKit.
// 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))
}
In RealityKit : Use RealityView with AnchorEntity types. ARSCNView is legacy — all new AR development should use RealityKit.
Time cost : Weeks of rework when you eventually must migrate
SceneKit is deprecated. New projects should use RealityKit from the start, even if the learning curve is steeper initially.
Time cost : Hours when migration begins
.scn files are SceneKit-specific and cannot be loaded in RealityKit. Convert early:
xcrun scntool --convert model.scn --format usdz --output model.usdz
Time cost : Complete rewrite during migration
SceneKit shader modifiers use a proprietary entry-point system. Heavy investment here has zero portability to RealityKit's ShaderGraphMaterial.
Time cost : Architecture redesign during migration
If you need custom render pipelines, build on Metal directly or use RealityRenderer (RealityKit's Metal-level API).
Time cost : Surprise breakage when Apple removes APIs
Track SceneView deprecation warnings and plan UIViewRepresentable fallback or RealityKit migration.
Time cost : 2-4 hours debugging frame drops, often misdiagnosed as GPU issue
// ❌ 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)]
Rule : If >50 identical objects, use SCNParticleSystem or flatten geometry. If different objects, use SCNNode.flattenedClone() to reduce draw calls.
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.
Pressure : Team familiarity with SceneKit, deadline to ship
Wrong approach : Start new project in SceneKit because the team knows it.
Correct approach : Invest in RealityKit learning. SceneKit will receive no new features. The longer you wait, the larger the migration debt.
Push-back template : "SceneKit is deprecated as of iOS 26. Starting new work in it creates migration debt that grows with every feature we add. RealityKit's ECS model is different but learnable — let's invest the time now."
Pressure : Tight deadline, team unfamiliar with ECS
Wrong approach : Build everything in SceneKit to meet the deadline.
Correct approach : Build the prototype in SceneKit if necessary, but document every SceneKit dependency and plan the migration. Use USDZ assets from the start so they're portable.
Push-back template : "Let's use USDZ assets and keep the SceneKit layer thin. When we migrate, the assets transfer directly and only the code layer changes."
Pressure : Desire for a clean migration
Wrong approach : Attempt to rewrite the entire SceneKit codebase in RealityKit at once.
Correct approach : Migrate incrementally. New features in RealityKit. Existing SceneKit code stays until it needs changes. Modularize with Swift packages (per Apple's migration guide).
Push-back template : "Apple's own migration guide recommends modularizing into Swift packages and migrating system by system. A big-bang rewrite risks introducing new bugs across the entire app."
[weak self] in completion handlers and closuresshowsStatistics = true)WWDC : 2014-609, 2014-610, 2017-604, 2019-612
Docs : /scenekit, /scenekit/scnscene, /scenekit/scnnode, /scenekit/scnmaterial, /scenekit/scnphysicsbody
Skills : axiom-scenekit-ref, axiom-realitykit, axiom-realitykit-ref
Weekly Installs
71
Repository
GitHub Stars
601
First Seen
Feb 5, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode67
gemini-cli64
codex63
github-copilot62
kimi-cli60
cursor60
.ambient | Uniform, no direction | No |
.probe | Environment lighting from cubemap | No |
| Alembic | .abc | Animation baking |