Scenekit
Maintain and extend existing SceneKit 3D scenes and visualizations. Use when working with SCNView, SCNScene, SCNNode scene graphs, SceneKit geometry/materials/lights/cameras, SCNAction animation, SCNPhysicsBody physics, SCNParticleSystem effects, .scn/.dae/.abc SceneKit assets, shader modifiers, or SwiftUI SceneView. SceneKit is soft-deprecated and in maintenance mode; route new apps, significant updates, USD/USDZ pipelines, and migration planning toward RealityKit.
- Skill ID
- dpearson2699/swift-ios-skills/scenekit
- Publisher
- dpearson2699
- Repository
- swift-ios-skills
- Installs
- 0
- Files
- 3
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
dpearson2699/swift-ios-skills/scenekitInstalls these files- SKILL.md
- evals/evals.json
- references/scenekit-patterns.md
What this skill tells the agent
SceneKit
Maintain existing SceneKit scenes only. Apple deprecated SceneKit at WWDC 2025 and limits it to maintenance; route new projects, major modernization, and USD/USDZ pipelines to RealityKit. Existing apps continue to work.
Contents
- Scene Setup
- Nodes and Geometry
- Materials
- Lighting
- Cameras
- Animation
- Physics
- Particle Systems
- Loading Models
- SwiftUI Integration
- Common Mistakes
- Review Checklist
- References
Scene Setup
SCNView in UIKit
import SceneKit
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)allowsCameraControl adds built-in orbit, pan, and zoom gestures. Typically disabled in production where custom camera control is needed.
Creating an SCNScene
let scene = SCNScene() // Empty
guard let scene = SCNScene(named: "art.scnassets/ship.scn") // .scn in .scnassets
else { fatalError("Missing scene asset") }
let url = Bundle.main.url(forResource: "ship", withExtension: "dae")!
let scene = try SCNScene(url: url, options: [.checkConsistency: true])Nodes and Geometry
Every scene has a rootNode. All content exists as descendant nodes. Nodes define position, orientation, and scale in their parent's coordinate system. SceneKit uses a right-handed coordinate system: +X right, +Y up, +Z toward the camera.
let parentNode = SCNNode()
scene.rootNode.addChildNode(parentNode)
let childNode = SCNNode()
childNode.position = SCNVector3(0, 1, 0) // 1 unit above parent
parentNode.addChildNode(childNode)Transforms
node.position = SCNVector3(x: 0, y: 2, z: -5)
node.eulerAngles = SCNVector3(x: 0, y: .pi / 4, z: 0) // 45-degree Y rotation
node.scale = SCNVector3(2, 2, 2)
node.simdPosition = SIMD3<Float>(0, 2, -5) // Prefer simd for performanceBuilt-in Primitives
SCNBox, SCNSphere, SCNCylinder, SCNCone, SCNTorus, SCNCapsule, SCNTube, SCNPlane, SCNFloor, SCNText, SCNShape (extruded Bezier path).
let node = SCNNode(geometry: SCNSphere(radius: 0.5))Finding Nodes
let maxNode = scene.rootNode.childNode(withName: "Max", recursively: true)
let enemies = scene.rootNode.childNodes { node, _ in
node.name?.hasPrefix("enemy") == true
}Materials
SCNMaterial defines surface appearance. Use firstMaterial for single-material geometries or the materials array for multi-material.
Color and Texture
let material = SCNMaterial()
material.diffuse.contents = UIColor.systemBlue // Solid color
material.diffuse.contents = UIImage(named: "brick") // Texture
material.normal.contents = UIImage(named: "brick_normal")
sphere.firstMaterial = materialPhysically Based Rendering (PBR)
let pbr = SCNMaterial()
pbr.lightingModel = .physicallyBased
pbr.diffuse.contents = UIImage(named: "albedo")
pbr.metalness.contents = 0.8 // Scalar or texture
pbr.roughness.contents = 0.2 // Scalar or texture
pbr.normal.contents = UIImage(named: "normal")
pbr.ambientOcclusion.contents = UIImage(named: "ao")Lighting Models
.physicallyBased (metalness/roughness), .blinn (default), .phong, .lambert (diffuse-only), .constant (unlit), .shadowOnly.
Each material property is an SCNMaterialProperty accepting UIColor, UIImage, CGFloat scalar, SKTexture, CALayer, or AVPlayer.
Transparency
material.transparency = 0.5
material.transparencyMode = .dualLayer
material.isDoubleSided = trueLighting
Attach an SCNLight to a node. The light's direction follows the node's negative Z-axis.
Light Types
// Ambient: uniform, no direction
let ambient = SCNLight()
ambient.type = .ambient
ambient.color = UIColor(white: 0.3, alpha: 1)
// Directional: parallel rays (sunlight)
let directional = SCNLight()
directional.type = .directional
directional.castsShadow = true
// Omni: point light, all directions
let omni = SCNLight()
omni.type = .omni
omni.attenuationEndDistance = 20
// Spot: cone-shaped
let spot = SCNLight()
spot.type = .spot
spot.spotInnerAngle = 20
spot.spotOuterAngle = 60Attach to a node:
let lightNode = SCNNode()
lightNode.light = directional
lightNode.eulerAngles = SCNVector3(-Float.pi / 3, 0, 0)
lightNode.position = SCNVector3(0, 10, 10)
scene.rootNode.addChildNode(lightNode)Shadows
light.castsShadow = true
light.shadowMapSize = CGSize(width: 2048, height: 2048)
light.shadowSampleCount = 8
light.shadowRadius = 3.0
light.shadowColor = UIColor(white: 0, alpha: 0.5)Category Bit Masks
light.categoryBitMask = 1 << 1 // Category 2
node.categoryBitMask = 1 << 1 // Only lit by category-2 lightsSceneKit renders a maximum of 8 lights per node. Use attenuationEndDistance on point/spot lights so SceneKit skips them for distant nodes.
Cameras
Attach an SCNCamera to a node to define a viewpoint.
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 15)
cameraNode.look(at: SCNVector3Zero)
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNodeConfiguration
camera.fieldOfView = 60 // Degrees
camera.zNear = 0.1
camera.zFar = 500
camera.automaticallyAdjustsZRange = true
// Orthographic
camera.usesOrthographicProjection = true
camera.orthographicScale = 10Depth-of-field (wantsDepthOfField, focusDistance, fStop) and HDR effects (wantsHDR, bloomIntensity, bloomThreshold, screenSpaceAmbientOcclusionIntensity) are configured directly on SCNCamera.
Animation
SceneKit provides three animation approaches.
SCNAction (Declarative, Game-Oriented)
Reusable, composable animation objects attached to nodes.
let move = SCNAction.move(by: SCNVector3(0, 2, 0), duration: 1)
let rotate = SCNAction.rotateBy(x: 0, y: .pi, z: 0, duration: 1)
node.runAction(.group([move, rotate]))
// Sequential
node.runAction(.sequence([.fadeOut(duration: 0.3), .removeFromParentNode()]))
// Infinite loop
let pulse = SCNAction.sequence([
.scale(to: 1.2, duration: 0.5),
.scale(to: 1.0, duration: 0.5)
])
node.runAction(.repeatForever(pulse))SCNTransaction (Implicit Animation)
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.0
node.position = SCNVector3(5, 0, 0)
node.opacity = 0.5
SCNTransaction.completionBlock = { print("Done") }
SCNTransaction.commit()Explicit Animations (Core Animation)
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")Physics
Physics Bodies
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: nil) // Forces + collisions
floor.physicsBody = SCNPhysicsBody(type: .static, shape: nil) // Immovable
platform.physicsBody = SCNPhysicsBody(type: .kinematic, shape: nil) // Code-drivenWhen shape is nil, SceneKit derives it from geometry. For performance, use simplified shapes:
let shape = SCNPhysicsShape(
geometry: SCNBox(width: 1, height: 2, length: 1, chamferRadius: 0),
options: nil
)
node.physicsBody = SCNPhysicsBody(type: .dynamic, shape: shape)
node.physicsBody?.mass = 2.0
node.physicsBody?.restitution = 0.3Applying Forces
node.physicsBody?.applyForce(SCNVector3(0, 10, 0), asImpulse: false) // ContinuousMore from dpearson2699
Use this skill in a project
Open any RiverX project, search the Skills panel for dpearson2699/swift-ios-skills/scenekit, and install it in one click.
