Loading...
Loading...
Build 3D scenes and visualizations using SceneKit. Use when creating 3D views with SCNView and SCNScene, building node hierarchies with SCNNode, applying materials and lighting, animating with SCNAction, simulating physics with SCNPhysicsBody, loading 3D models (.usdz, .scn), adding particle effects, or embedding SceneKit in SwiftUI with SceneView. Note: SceneKit was deprecated at WWDC 2025 and is in maintenance mode; RealityKit is recommended for new projects.
npx skill4agent add dpearson2699/swift-ios-skills scenekitimport SceneKit
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)allowsCameraControllet scene = SCNScene() // Empty
guard let scene = SCNScene(named: "art.scnassets/ship.scn") // .scn asset catalog
else { fatalError("Missing scene asset") }
let scene = try SCNScene(url: Bundle.main.url( // .usdz from bundle
forResource: "spaceship", withExtension: "usdz")!)rootNodelet parentNode = SCNNode()
scene.rootNode.addChildNode(parentNode)
let childNode = SCNNode()
childNode.position = SCNVector3(0, 1, 0) // 1 unit above parent
parentNode.addChildNode(childNode)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 performanceSCNBoxSCNSphereSCNCylinderSCNConeSCNTorusSCNCapsuleSCNTubeSCNPlaneSCNFloorSCNTextSCNShapelet node = SCNNode(geometry: SCNSphere(radius: 0.5))let maxNode = scene.rootNode.childNode(withName: "Max", recursively: true)
let enemies = scene.rootNode.childNodes { node, _ in
node.name?.hasPrefix("enemy") == true
}SCNMaterialfirstMaterialmaterialslet 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 = materiallet 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").physicallyBased.blinn.phong.lambert.constant.shadowOnlySCNMaterialPropertyUIColorUIImageCGFloatSKTextureCALayerAVPlayermaterial.transparency = 0.5
material.transparencyMode = .dualLayer
material.isDoubleSided = trueSCNLight// 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 = 60let lightNode = SCNNode()
lightNode.light = directional
lightNode.eulerAngles = SCNVector3(-Float.pi / 3, 0, 0)
lightNode.position = SCNVector3(0, 10, 10)
scene.rootNode.addChildNode(lightNode)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)light.categoryBitMask = 1 << 1 // Category 2
node.categoryBitMask = 1 << 1 // Only lit by category-2 lightsattenuationEndDistanceSCNCameralet cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 15)
cameraNode.look(at: SCNVector3Zero)
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNodecamera.fieldOfView = 60 // Degrees
camera.zNear = 0.1
camera.zFar = 500
camera.automaticallyAdjustsZRange = true
// Orthographic
camera.usesOrthographicProjection = true
camera.orthographicScale = 10wantsDepthOfFieldfocusDistancefStopwantsHDRbloomIntensitybloomThresholdscreenSpaceAmbientOcclusionIntensitySCNCameralet 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.begin()
SCNTransaction.animationDuration = 1.0
node.position = SCNVector3(5, 0, 0)
node.opacity = 0.5
SCNTransaction.completionBlock = { print("Done") }
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")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-drivenshapenillet 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.3node.physicsBody?.applyForce(SCNVector3(0, 10, 0), asImpulse: false) // Continuous
node.physicsBody?.applyForce(SCNVector3(0, 5, 0), asImpulse: true) // Instant
node.physicsBody?.applyTorque(SCNVector4(0, 1, 0, 2), asImpulse: true)struct PhysicsCategory {
static let player: Int = 1 << 0
static let enemy: Int = 1 << 1
static let ground: Int = 1 << 2
}
playerNode.physicsBody?.categoryBitMask = PhysicsCategory.player
playerNode.physicsBody?.collisionBitMask = PhysicsCategory.ground | PhysicsCategory.enemy
playerNode.physicsBody?.contactTestBitMask = PhysicsCategory.enemy
scene.physicsWorld.contactDelegate = self
func physicsWorld(_ world: SCNPhysicsWorld, didBegin contact: SCNPhysicsContact) {
handleCollision(between: contact.nodeA, and: contact.nodeB)
}scene.physicsWorld.gravity = SCNVector3(0, -9.8, 0)
node.physicsBody?.isAffectedByGravity = falseSCNParticleSystemlet particles = SCNParticleSystem()
particles.birthRate = 100
particles.particleLifeSpan = 2
particles.particleSize = 0.1
particles.particleColor = .orange
particles.emitterShape = SCNSphere(radius: 0.5)
particles.particleVelocity = 2
particles.isAffectedByGravity = true
particles.blendMode = .additive
let emitterNode = SCNNode()
emitterNode.addParticleSystem(particles)
scene.rootNode.addChildNode(emitterNode)SCNParticleSystem(named: "fire.scnp", inDirectory: nil)colliderNodes.usdz.scn.dae.obj.abc.usdzguard let scene = SCNScene(named: "art.scnassets/ship.scn") else { return }
let scene = try SCNScene(url: Bundle.main.url(
forResource: "model", withExtension: "usdz")!)
guard let modelNode = scene.rootNode.childNode(withName: "mesh", recursively: true) else { return }SCNReferenceNode.onDemandSCNSceneSourceSceneViewimport SwiftUI
import SceneKit
struct SceneKitView: View {
let scene: SCNScene = {
let scene = SCNScene()
let sphere = SCNNode(geometry: SCNSphere(radius: 1))
sphere.geometry?.firstMaterial?.lightingModel = .physicallyBased
sphere.geometry?.firstMaterial?.diffuse.contents = UIColor.systemBlue
sphere.geometry?.firstMaterial?.metalness.contents = 0.8
scene.rootNode.addChildNode(sphere)
return scene
}()
var body: some View {
SceneView(scene: scene,
options: [.allowsCameraControl, .autoenablesDefaultLighting])
}
}.allowsCameraControl.autoenablesDefaultLighting.jitteringEnabled.temporalAntialiasingEnabledSCNViewUIViewRepresentableSCNSceneRendererDelegate// DON'T: Scene renders blank or black -- no camera, no lights
sceneView.scene = scene
// DO: Add camera + lights, or use convenience flags
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 15)
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNode
sceneView.autoenablesDefaultLighting = true// DON'T
node.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(geometry: complexMesh))
// DO: Simplified primitive
node.physicsBody = SCNPhysicsBody(type: .dynamic,
shape: SCNPhysicsShape(
geometry: SCNBox(width: 1, height: 2, length: 1, chamferRadius: 0),
options: nil))// DON'T: Resets physics simulation
dynamicNode.position = SCNVector3(5, 0, 0)
// DO: Use forces/impulses
dynamicNode.physicsBody?.applyForce(SCNVector3(10, 0, 0), asImpulse: true)// DON'T: 20 lights with no attenuation
for _ in 0..<20 {
let light = SCNNode()
light.light = SCNLight()
light.light?.type = .omni
scene.rootNode.addChildNode(light)
}
// DO: Set attenuationEndDistance so SceneKit skips distant lights
light.light?.attenuationEndDistance = 10pointOfViewautoenablesDefaultLightingcontactTestBitMaskSCNPhysicsContactDelegatescene.physicsWorld.contactDelegateattenuationEndDistance.physicallyBased.usdzSCNReferenceNodebirthRateparticleLifeSpancategoryBitMaskSceneViewUIViewRepresentableSCNView