Use when working with SceneKit 3D scenes, migrating SceneKit to RealityKit, or maintaining legacy SceneKit code. Covers scene graph, materials, physics, animation, SwiftUI bridge, migration decision tree.
Maintains legacy SceneKit code and guides migration to RealityKit.
npx claudepluginhub charleswiltgen/axiomThis skill inherits all available tools. When active, it can use any tool Claude has access to.
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 |
.ambient | Uniform, no direction | No |
.probe | Environment lighting from cubemap | No |
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 |
| Alembic | .abc | Animation baking |
// 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
Activates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.