mcpField
A
axiom-scenekit

axiom-scenekit

github.com/charleswiltgen开发1.2k136 次安装今天更新
代码仓库

关于 axiom-scenekit

面向维护 SceneKit 项目和规划迁移到 RealityKit 的开发指南,适用于调试渲染、物理、动画及 SwiftUI 集成。内容涵盖旧版 ARKit 用法,并说明 SceneKit 的弃用背景与迁移注意事项。

主要功能

  1. 01SceneKit 场景与渲染指南
  2. 02几何体、材质和灯光说明
  3. 03动画与物理处理
  4. 04SwiftUI 和 ARKit 集成
  5. 05RealityKit 迁移建议

适用场景

  1. 01维护现有 SceneKit 应用
  2. 02排查渲染或物理问题
  3. 03在 SwiftUI 中呈现场景
  4. 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 归档.scnXcode 专用,无法移植到 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

评论

支持 Markdown