axiom-scenekit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SceneKit 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
  • SceneView
    (SwiftUI) is formally deprecated in iOS 26
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
axiom-scenekit-ref
for the complete concept mapping table.

SceneKit自iOS 26(WWDC 2025)起被软弃用。这意味着:
  • 现有应用可继续正常运行
  • 不再添加新功能或常规Bug修复
  • 仅会提供关键安全补丁
  • SceneView
    (SwiftUI)在iOS 26中被正式弃用
苹果推荐的未来方向是RealityKit。所有新3D项目都应使用RealityKit。SceneKit的相关知识对于维护遗留代码以及在迁移过程中理解概念仍具有价值。
在RealityKit中:ECS架构取代了场景图。完整的概念映射表请参考
axiom-scenekit-ref

1. 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
        └── 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采用**节点树(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.
entity.position
is local,
entity.position(relativeTo: nil)
gives world position.

变换属性从父节点向下传递给子节点。子节点的世界变换 = 父节点的世界变换 × 子节点的局部变换。
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.position
为局部位置,
entity.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
RealityView
in SwiftUI — no UIViewRepresentable needed.

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) {}
}
在RealityKit中:SwiftUI中使用
RealityView
即可,无需UIViewRepresentable。

3. 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 = material
In RealityKit: Use
PhysicallyBasedMaterial
with similar properties but different API surface. See
axiom-scenekit-ref
Part 1 for the mapping.
swift
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中:使用
PhysicallyBasedMaterial
,其属性与SceneKit类似但API有所不同。详细映射关系请参考
axiom-scenekit-ref
第一部分。

Shader 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
,
.fragment
In RealityKit: Use
ShaderGraphMaterial
with Reality Composer Pro, or
CustomMaterial
with Metal functions.

SceneKit支持在特定入口点注入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的
ShaderGraphMaterial
,或结合Metal函数使用
CustomMaterial

4. Lighting

4. 光照

Light Types

光照类型

TypeDescriptionShadows
.omni
Point light, radiates in all directionsNo
.directional
Parallel rays (sun)Yes
.spot
Cone-shaped beamYes
.area
Rectangle emitter (soft shadows)Yes
.IES
Real-world light profileYes
.ambient
Uniform, no directionNo
.probe
Environment lighting from cubemapNo
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
DirectionalLightComponent
,
PointLightComponent
,
SpotLightComponent
as components on entities. Image-based lighting via
EnvironmentResource
.

类型描述阴影
.omni
点光源,向所有方向辐射
.directional
平行光线(如太阳光)
.spot
锥形光束
.area
矩形面光源(产生柔和阴影)
.IES
真实世界光照配置文件
.ambient
均匀光照,无方向
.probe
基于立方体贴图的环境光照
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中:将
DirectionalLightComponent
,
PointLightComponent
,
SpotLightComponent
作为组件附加到实体上。基于图像的光照通过
EnvironmentResource
实现。

5. 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
entity.playAnimation()
with animations loaded from USD files. Transform animations via
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中:使用
entity.playAnimation()
加载USD文件中的动画。变换动画可通过
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.projectile
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.projectile

Contact 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
PhysicsBodyComponent
,
CollisionComponent
, and collision event subscriptions via
scene.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中:使用
PhysicsBodyComponent
,
CollisionComponent
,并通过
scene.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
ManipulationComponent
for drag/rotate/scale gestures, or collision-based hit testing.

swift
// 在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
实现拖拽/旋转/缩放手势,或基于碰撞的命中测试。

8. Asset Pipeline

8. 资源管线

Supported Formats

支持的格式

FormatExtensionNotes
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
Alembic
.abc
Animation baking
格式扩展名说明
USD/USDZ
.usdz
,
.usda
,
.usdc
推荐格式,同时支持SceneKit和RealityKit
Collada
.dae
遗留格式,仍受支持
SceneKit归档
.scn
Xcode专用格式,无法移植到RealityKit
Wavefront OBJ
.obj
仅包含几何体,无动画
Alembic
.abc
动画烘焙格式

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
.scn
files to
.usdz
using
xcrun scntool --convert file.scn --format usdz
before migrating to RealityKit.

swift
// 从应用包中加载
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
格式。

9. 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
RealityView
with
AnchorEntity
types. ARSCNView is legacy — all new AR development should use RealityKit.

swift
// 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中:使用
RealityView
结合
AnchorEntity
类型。ARSCNView属于遗留方案——所有新AR开发都应使用RealityKit。

10. 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
.scn
files are SceneKit-specific and cannot be loaded in RealityKit. Convert early:
bash
xcrun scntool --convert model.scn --format usdz --output model.usdz
时间成本:迁移开始时需花费数小时处理
.scn
文件是SceneKit专用格式,无法在RealityKit中加载。应尽早转换:
bash
xcrun scntool --convert model.scn --format usdz --output model.usdz

Anti-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的
ShaderGraphMaterial

Anti-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
RealityRenderer
(RealityKit's Metal-level API).
时间成本:迁移时需重新设计架构
如果需要自定义渲染管线,请直接基于Metal构建,或使用RealityKit的Metal级API
RealityRenderer

Anti-Pattern 5: Ignoring Deprecation Warnings

反模式5:忽略弃用警告

Time cost: Surprise breakage when Apple removes APIs
Track
SceneView
deprecation warnings and plan UIViewRepresentable fallback or RealityKit migration.
时间成本:当苹果移除API时,应用会意外崩溃
跟踪
SceneView
的弃用警告,并规划使用UIViewRepresentable作为替代方案或直接迁移到RealityKit。

Anti-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
SCNNode.flattenedClone()
to reduce draw calls.

时间成本:花费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
  • [weak self]
    in completion handlers and closures
  • 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