$npx -y skills add dpearson2699/swift-ios-skills --skill scenekitMaintain 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, s
| 1 | # SceneKit |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Contents |
| 6 | |
| 7 | - [Scene Setup](#scene-setup) |
| 8 | - [Nodes and Geometry](#nodes-and-geometry) |
| 9 | - [Materials](#materials) |
| 10 | - [Lighting](#lighting) |
| 11 | - [Cameras](#cameras) |
| 12 | - [Animation](#animation) |
| 13 | - [Physics](#physics) |
| 14 | - [Particle Systems](#particle-systems) |
| 15 | - [Loading Models](#loading-models) |
| 16 | - [SwiftUI Integration](#swiftui-integration) |
| 17 | - [Common Mistakes](#common-mistakes) |
| 18 | - [Review Checklist](#review-checklist) |
| 19 | - [References](#references) |
| 20 | |
| 21 | ## Scene Setup |
| 22 | |
| 23 | ### SCNView in UIKit |
| 24 | |
| 25 | ```swift |
| 26 | import SceneKit |
| 27 | |
| 28 | let sceneView = SCNView(frame: view.bounds) |
| 29 | sceneView.scene = SCNScene() |
| 30 | sceneView.allowsCameraControl = true |
| 31 | sceneView.autoenablesDefaultLighting = true |
| 32 | sceneView.backgroundColor = .black |
| 33 | view.addSubview(sceneView) |
| 34 | ``` |
| 35 | |
| 36 | `allowsCameraControl` adds built-in orbit, pan, and zoom gestures. Typically |
| 37 | disabled in production where custom camera control is needed. |
| 38 | |
| 39 | ### Creating an SCNScene |
| 40 | |
| 41 | ```swift |
| 42 | let scene = SCNScene() // Empty |
| 43 | guard let scene = SCNScene(named: "art.scnassets/ship.scn") // .scn in .scnassets |
| 44 | else { fatalError("Missing scene asset") } |
| 45 | let url = Bundle.main.url(forResource: "ship", withExtension: "dae")! |
| 46 | let scene = try SCNScene(url: url, options: [.checkConsistency: true]) |
| 47 | ``` |
| 48 | |
| 49 | ## Nodes and Geometry |
| 50 | |
| 51 | Every scene has a `rootNode`. All content exists as descendant nodes. Nodes |
| 52 | define position, orientation, and scale in their parent's coordinate system. |
| 53 | SceneKit uses a right-handed coordinate system: +X right, +Y up, +Z toward |
| 54 | the camera. |
| 55 | |
| 56 | ```swift |
| 57 | let parentNode = SCNNode() |
| 58 | scene.rootNode.addChildNode(parentNode) |
| 59 | |
| 60 | let childNode = SCNNode() |
| 61 | childNode.position = SCNVector3(0, 1, 0) // 1 unit above parent |
| 62 | parentNode.addChildNode(childNode) |
| 63 | ``` |
| 64 | |
| 65 | ### Transforms |
| 66 | |
| 67 | ```swift |
| 68 | node.position = SCNVector3(x: 0, y: 2, z: -5) |
| 69 | node.eulerAngles = SCNVector3(x: 0, y: .pi / 4, z: 0) // 45-degree Y rotation |
| 70 | node.scale = SCNVector3(2, 2, 2) |
| 71 | node.simdPosition = SIMD3<Float>(0, 2, -5) // Prefer simd for performance |
| 72 | ``` |
| 73 | |
| 74 | ### Built-in Primitives |
| 75 | |
| 76 | `SCNBox`, `SCNSphere`, `SCNCylinder`, `SCNCone`, `SCNTorus`, `SCNCapsule`, |
| 77 | `SCNTube`, `SCNPlane`, `SCNFloor`, `SCNText`, `SCNShape` (extruded Bezier path). |
| 78 | |
| 79 | ```swift |
| 80 | let node = SCNNode(geometry: SCNSphere(radius: 0.5)) |
| 81 | ``` |
| 82 | |
| 83 | ### Finding Nodes |
| 84 | |
| 85 | ```swift |
| 86 | let maxNode = scene.rootNode.childNode(withName: "Max", recursively: true) |
| 87 | let enemies = scene.rootNode.childNodes { node, _ in |
| 88 | node.name?.hasPrefix("enemy") == true |
| 89 | } |
| 90 | ``` |
| 91 | |
| 92 | ## Materials |
| 93 | |
| 94 | `SCNMaterial` defines surface appearance. Use `firstMaterial` for single-material |
| 95 | geometries or the `materials` array for multi-material. |
| 96 | |
| 97 | ### Color and Texture |
| 98 | |
| 99 | ```swift |
| 100 | let material = SCNMaterial() |
| 101 | material.diffuse.contents = UIColor.systemBlue // Solid color |
| 102 | material.diffuse.contents = UIImage(named: "brick") // Texture |
| 103 | material.normal.contents = UIImage(named: "brick_normal") |
| 104 | sphere.firstMaterial = material |
| 105 | ``` |
| 106 | |
| 107 | ### Physically Based Rendering (PBR) |
| 108 | |
| 109 | ```swift |
| 110 | let pbr = SCNMaterial() |
| 111 | pbr.lightingModel = .physicallyBased |
| 112 | pbr.diffuse.contents = UIImage(named: "albedo") |
| 113 | pbr.metalness.contents = 0.8 // Scalar or texture |
| 114 | pbr.roughness.contents = 0.2 // Scalar or texture |
| 115 | pbr.normal.contents = UIImage(named: "normal") |
| 116 | pbr.ambientOcclusion.contents = UIImage(named: "ao") |
| 117 | ``` |
| 118 | |
| 119 | ### Lighting Models |
| 120 | |
| 121 | `.physicallyBased` (metalness/roughness), `.blinn` (default), `.phong`, |
| 122 | `.lambert` (diffuse-only), `.constant` (unlit), `.shadowOnly`. |
| 123 | |
| 124 | Each material property is an `SCNMaterialProperty` accepting `UIColor`, |
| 125 | `UIImage`, `CGFloat` scalar, `SKTexture`, `CALayer`, or `AVPlayer`. |
| 126 | |
| 127 | ### Transparency |
| 128 | |
| 129 | ```swift |
| 130 | material.transparency = 0.5 |
| 131 | material.transparencyMode = .dualLayer |
| 132 | material.isDoubleSided = true |
| 133 | ``` |
| 134 | |
| 135 | ## Lighting |
| 136 | |
| 137 | Attach an `SCNLight` to a node. The light's direction follows the node's |
| 138 | negative Z-axis. |
| 139 | |
| 140 | ### Light Types |
| 141 | |
| 142 | ```swift |
| 143 | // Ambient: uniform, no direction |
| 144 | let ambient = SCNLight() |
| 145 | ambient.type = .ambient |
| 146 | ambient.color = UIColor(white: 0.3, alpha: 1) |
| 147 | |
| 148 | // Directional: parallel rays (sunlight) |
| 149 | let directional = SCNLight() |
| 150 | directional.type = .directional |
| 151 | directional.castsShadow = true |
| 152 | |
| 153 | // Omni: point light, all directions |
| 154 | let omni = SCNLight() |
| 155 | omni.type = .omni |
| 156 | omni.attenuationEndDistance = 20 |
| 157 | |
| 158 | // Spot: cone-shaped |
| 159 | let spot = SCNLight() |
| 160 | spot.type = .spot |
| 161 | spot.spotInnerAn |