axiom-scenekit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSceneKit Development Guide
SceneKit 开发指南
Purpose: Maintain existing SceneKit code safely and plan migration to RealityKit
iOS Version: iOS 8+ (SceneKit), deprecated iOS 26+
Xcode: Xcode 15+
用途:安全维护现有SceneKit代码并规划向RealityKit的迁移
iOS版本要求:iOS 8+(SceneKit),iOS 26+已被弃用
Xcode版本要求:Xcode 15+
When to Use This Skill
何时使用此技能
Use this skill when:
- Maintaining existing SceneKit code
- Building a SceneKit prototype (with awareness of deprecation)
- Planning migration from SceneKit to RealityKit
- Debugging SceneKit rendering, physics, or animation issues
- Integrating SceneKit content with SwiftUI
- Loading 3D models via Model I/O or SCNSceneSource
Do NOT use this skill for:
- New 3D projects (use )
axiom-realitykit - AR experiences (use )
axiom-realitykit - visionOS development (use )
axiom-realitykit - SpriteKit 2D games ()
axiom-spritekit - Metal shader programming ()
axiom-metal-migration-ref
在以下场景中使用此技能:
- 维护现有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
Deprecation Context
弃用背景
SceneKit is soft-deprecated as of iOS 26 (WWDC 2025). This means:
- Existing apps continue to work
- No new features or general bug fixes
- Only critical security patches
- (SwiftUI) is formally deprecated in iOS 26
SceneView
Apple'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 for the complete concept mapping table.
axiom-scenekit-refSceneKit自iOS 26(WWDC 2025)起被软弃用。这意味着:
- 现有应用可继续正常运行
- 不再添加新功能或常规Bug修复
- 仅会提供关键安全补丁
- (SwiftUI)在iOS 26中被正式弃用
SceneView
苹果推荐的未来方向是RealityKit。所有新3D项目都应使用RealityKit。SceneKit的相关知识对于维护遗留代码以及在迁移过程中理解概念仍具有价值。
在RealityKit中:ECS架构取代了场景图。完整的概念映射表请参考。
axiom-scenekit-ref1. Mental Model
1. 核心模型
Scene Graph Architecture
场景图架构
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
└── wallNodesIn RealityKit: Entities replace nodes. Components replace node properties. The hierarchy concept persists, but behavior is driven by Systems rather than node callbacks.
SceneKit采用**节点树(SCNNode)**结构,所有节点都附加到SCNScene的根节点上。每个节点都有相对于其父节点的变换属性(位置、旋转、缩放)。
SCNScene
└── rootNode
├── cameraNode (SCNCamera)
├── lightNode (SCNLight)
├── playerNode (SCNGeometry + SCNPhysicsBody)
│ ├── weaponNode
│ └── particleNode (SCNParticleSystem)
└── environmentNode
├── groundNode
└── wallNodes在RealityKit中:实体(Entity)取代了节点,组件(Component)取代了节点属性。层级结构的概念仍然存在,但行为由系统(Systems)而非节点回调驱动。
Coordinate System
坐标系
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.
SceneKit采用右手Y轴向上的坐标系:
+Y (向上)
|
|
+──── +X (向右)
/
/
+Z (朝向观察者)这与RealityKit的坐标系一致,因此空间概念在迁移时可直接沿用。
Transform Hierarchy
变换层级
Transforms cascade parent → child. A child's world transform = parent's world transform × child's local transform.
swift
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. is local, gives world position.
entity.positionentity.position(relativeTo: nil)变换属性从父节点向下传递给子节点。子节点的世界变换 = 父节点的世界变换 × 子节点的局部变换。
swift
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.positionentity.position(relativeTo: nil)2. Scene Setup and Rendering
2. 场景设置与渲染
SCNView (UIKit)
SCNView(UIKit)
swift
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene(named: "scene.scn")
sceneView.allowsCameraControl = true
sceneView.showsStatistics = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)swift
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) — Deprecated iOS 26
SceneView(SwiftUI)—— iOS 26已弃用
swift
// Still works but deprecated. Use SCNViewRepresentable for new code.
import SceneKit
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.allowsCameraControl, .autoenablesDefaultLighting]
)swift
// 仍可运行但已被弃用。新代码请使用SCNViewRepresentable。
import SceneKit
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.allowsCameraControl, .autoenablesDefaultLighting]
)SCNViewRepresentable (SwiftUI replacement)
SCNViewRepresentable(SwiftUI替代方案)
swift
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 in SwiftUI — no UIViewRepresentable needed.
RealityViewswift
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中使用即可,无需UIViewRepresentable。
RealityView3. Geometry and Materials
3. 几何体与材质
Built-in Geometries
内置几何体
swift
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)swift
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 Materials
PBR材质
swift
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 = materialIn RealityKit: Use with similar properties but different API surface. See Part 1 for the mapping.
PhysicallyBasedMaterialaxiom-scenekit-refswift
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中:使用,其属性与SceneKit类似但API有所不同。详细映射关系请参考第一部分。
PhysicallyBasedMaterialaxiom-scenekit-refShader Modifiers
着色器修改器
SceneKit supports GLSL/Metal shader snippets injected at specific entry points:
swift
// 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.fragmentIn RealityKit: Use with Reality Composer Pro, or with Metal functions.
ShaderGraphMaterialCustomMaterialSceneKit支持在特定入口点注入GLSL/Metal着色器代码片段:
swift
// 片段着色器修改器——为表面添加自定义效果
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的,或结合Metal函数使用。
ShaderGraphMaterialCustomMaterial4. Lighting
4. 光照
Light Types
光照类型
| Type | Description | Shadows |
|---|---|---|
| Point light, radiates in all directions | No |
| Parallel rays (sun) | Yes |
| Cone-shaped beam | Yes |
| Rectangle emitter (soft shadows) | Yes |
| Real-world light profile | Yes |
| Uniform, no direction | No |
| Environment lighting from cubemap | No |
swift
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 , , as components on entities. Image-based lighting via .
DirectionalLightComponentPointLightComponentSpotLightComponentEnvironmentResource| 类型 | 描述 | 阴影 |
|---|---|---|
| 点光源,向所有方向辐射 | 无 |
| 平行光线(如太阳光) | 有 |
| 锥形光束 | 有 |
| 矩形面光源(产生柔和阴影) | 有 |
| 真实世界光照配置文件 | 有 |
| 均匀光照,无方向 | 无 |
| 基于立方体贴图的环境光照 | 无 |
swift
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中:将, , 作为组件附加到实体上。基于图像的光照通过实现。
DirectionalLightComponentPointLightComponentSpotLightComponentEnvironmentResource5. Animation
5. 动画
SCNAction (Declarative)
SCNAction(声明式)
swift
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)swift
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)Implicit Animation (SCNTransaction)
隐式动画(SCNTransaction)
swift
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
node.position = SCNVector3(0, 5, 0)
node.opacity = 0.5
SCNTransaction.commit()swift
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
node.position = SCNVector3(0, 5, 0)
node.opacity = 0.5
SCNTransaction.commit()Explicit Animation (CAAnimation bridge)
显式动画(CAAnimation桥接)
swift
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")swift
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")Loading Animations from Files
从文件加载动画
swift
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 with animations loaded from USD files. Transform animations via .
entity.playAnimation()entity.move(to:relativeTo:duration:)swift
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. Physics
6. 物理系统
Physics Bodies
物理体
swift
// 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)swift
// 动态物理体——由物理模拟控制位置
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)Collision Categories
碰撞分类
swift
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.projectileswift
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.projectileContact Delegate
碰撞代理
swift
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 , , and collision event subscriptions via .
PhysicsBodyComponentCollisionComponentscene.subscribe(to: CollisionEvents.Began.self)swift
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中:使用, ,并通过订阅碰撞事件。
PhysicsBodyComponentCollisionComponentscene.subscribe(to: CollisionEvents.Began.self)7. Hit Testing and Interaction
7. 命中测试与交互
swift
// 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 for drag/rotate/scale gestures, or collision-based hit testing.
ManipulationComponentswift
// 在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中:使用实现拖拽/旋转/缩放手势,或基于碰撞的命中测试。
ManipulationComponent8. Asset Pipeline
8. 资源管线
Supported Formats
支持的格式
| Format | Extension | Notes |
|---|---|---|
| USD/USDZ | | Preferred format, works in both SceneKit and RealityKit |
| Collada | | Legacy, still supported |
| SceneKit Archive | | Xcode-specific, not portable to RealityKit |
| Wavefront OBJ | | Geometry only, no animations |
| Alembic | | Animation baking |
| 格式 | 扩展名 | 说明 |
|---|---|---|
| USD/USDZ | | 推荐格式,同时支持SceneKit和RealityKit |
| Collada | | 遗留格式,仍受支持 |
| SceneKit归档 | | Xcode专用格式,无法移植到RealityKit |
| Wavefront OBJ | | 仅包含几何体,无动画 |
| Alembic | | 动画烘焙格式 |
Loading Models
加载模型
swift
// 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 files to using before migrating to RealityKit.
.scn.usdzxcrun scntool --convert file.scn --format usdzswift
// 从应用包中加载
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.usdz9. ARKit Integration (Legacy)
9. ARKit集成(遗留方案)
swift
// 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 with types. ARSCNView is legacy — all new AR development should use RealityKit.
RealityViewAnchorEntityswift
// 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中:使用结合类型。ARSCNView属于遗留方案——所有新AR开发都应使用RealityKit。
RealityViewAnchorEntity10. Anti-Patterns
10. 反模式
Anti-Pattern 1: Starting New Projects in SceneKit
反模式1:在新项目中使用SceneKit
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.
时间成本:后续迁移时需花费数周时间返工
SceneKit已被弃用。所有新3D项目都应从一开始就使用RealityKit。
Anti-Pattern 2: Using .scn Files Without USDZ Conversion
反模式2:使用.scn文件而不转换为USDZ
Time cost: Hours when migration begins
.scnbash
xcrun scntool --convert model.scn --format usdz --output model.usdz时间成本:迁移开始时需花费数小时处理
.scnbash
xcrun scntool --convert model.scn --format usdz --output model.usdzAnti-Pattern 3: Deep Shader Modifier Customization
反模式3:深度自定义着色器修改器
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时间成本:迁移时需完全重写
SceneKit的着色器修改器使用专有入口点系统。在这方面的大量投入无法移植到RealityKit的。
ShaderGraphMaterialAnti-Pattern 4: Relying on SCNRenderer for Custom Pipelines
反模式4:依赖SCNRenderer实现自定义渲染管线
Time cost: Architecture redesign during migration
If you need custom render pipelines, build on Metal directly or use (RealityKit's Metal-level API).
RealityRenderer时间成本:迁移时需重新设计架构
如果需要自定义渲染管线,请直接基于Metal构建,或使用RealityKit的Metal级API 。
RealityRendererAnti-Pattern 5: Ignoring Deprecation Warnings
反模式5:忽略弃用警告
Time cost: Surprise breakage when Apple removes APIs
Track deprecation warnings and plan UIViewRepresentable fallback or RealityKit migration.
SceneView时间成本:当苹果移除API时,应用会意外崩溃
跟踪的弃用警告,并规划使用UIViewRepresentable作为替代方案或直接迁移到RealityKit。
SceneViewAnti-Pattern 6: Creating Hundreds of Nodes in a Loop
反模式6:在循环中创建数百个节点
Time cost: 2-4 hours debugging frame drops, often misdiagnosed as GPU issue
swift
// ❌ 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 to reduce draw calls.
SCNNode.flattenedClone()时间成本:花费2-4小时调试帧率下降问题,常被误判为GPU问题
swift
// ❌ 错误:每个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()11. Migration Decision Tree
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.是否应迁移到RealityKit?
│
├─ 这是新项目吗?
│ └─ 是 → 从一开始就使用RealityKit,无需犹豫。
│
├─ 应用需要AR功能吗?
│ └─ 是 → 必须迁移。ARSCNView是遗留方案,RealityKit是唯一的未来方向。
│
├─ 应用目标平台是visionOS吗?
│ └─ 是 → 必须迁移。SceneKit不支持visionOS的空间特性。
│
├─ 代码库大量依赖SceneKit吗?
│ ├─ 是,且应用已稳定 → 暂时继续维护SceneKit代码,规划分阶段迁移。
│ └─ 是,但需要添加新功能 → 增量迁移(新功能使用RealityKit开发)。
│
├─ 性能是关注点吗?
│ └─ 是 → RealityKit针对Apple Silicon进行了优化,采用Metal优先的渲染方式。
│
└─ 应用处于维护模式吗?
└─ 是 → 继续使用SceneKit,直到出现关键问题。苹果仍会提供安全补丁。12. Pressure Scenarios
12. 压力场景应对
Scenario 1: "Just Use SceneKit, It Works Fine"
场景1:“就用SceneKit吧,它能用就行”
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."
压力:团队熟悉SceneKit,交付期限紧张
错误做法:因为团队熟悉就在新项目中使用SceneKit。
正确做法:投入时间学习RealityKit。SceneKit不会再添加新功能,等待时间越长,迁移债务越大。
反驳模板:“SceneKit自iOS 26起已被弃用。在新项目中使用它会产生迁移债务,且每添加一个功能,债务就会增加。RealityKit的ECS模型虽然不同但易于学习——我们现在就投入时间学习它。”
Scenario 2: "We Don't Have Time to Learn RealityKit"
场景2:“我们没时间学习RealityKit”
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."
压力:期限紧张,团队不熟悉ECS架构
错误做法:为了赶期限完全使用SceneKit构建.
正确做法:如有必要,可使用SceneKit构建原型,但需记录所有SceneKit依赖并规划迁移。从一开始就使用USDZ格式的资源,确保其可移植。
反驳模板:“我们使用USDZ资源并尽量简化SceneKit层。迁移时,资源可直接复用,仅需修改代码层。”
Scenario 3: "Port Everything At Once"
场景3:“一次性迁移所有内容”
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."
压力:希望实现彻底的迁移
错误做法:尝试一次性将整个SceneKit代码库重写为RealityKit代码。
正确做法:增量迁移。新功能使用RealityKit开发,现有SceneKit代码保持不变,直到需要修改时再迁移。按照苹果的迁移指南,使用Swift包进行模块化。
反驳模板:“苹果官方的迁移指南建议将代码模块化为Swift包,逐个系统进行迁移。一次性重写会导致整个应用引入新Bug的风险。”
Code Review Checklist
代码审查检查清单
- No new SceneKit code in projects targeting iOS 26+ without migration plan
- Assets in USDZ format (not .scn) for portability
- No deep shader modifier customization without RealityKit equivalent identified
- SCNTransaction used for implicit animations (not direct property changes without animation context)
- Physics categoryBitMask explicitly set (not relying on defaults)
- Contact delegate set and protocol conformance added
- in completion handlers and closures
[weak self] - Debug overlays enabled during development ()
showsStatistics = true
- 针对iOS 26+的项目中,若无迁移计划则不得新增SceneKit代码
- 资源使用USDZ格式(而非.scn)以保证可移植性
- 若深度自定义着色器修改器,需先确认RealityKit中有对应的替代方案
- 使用SCNTransaction实现隐式动画(而非直接修改属性而不使用动画上下文)
- 显式设置物理体的categoryBitMask(不依赖默认值)
- 设置了碰撞代理并遵循了协议
- 完成处理程序和闭包中使用避免循环引用
[weak self] - 开发过程中启用调试覆盖层()
showsStatistics = true
Resources
资源
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
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