scenekit について
scenekitは、既存のSceneKitシーンを保守する開発者向けのスキルです。シーン構築、レンダリング、アニメーション、物理演算、アセット読み込みを扱い、新規開発やUSD/USDZの処理にはRealityKitを案内します。
主な機能
- 01シーン、ノード、ジオメトリの設定
- 02マテリアル、照明、カメラの構成
- 03アクションと明示的なアニメーションの作成
- 04物理ボディと衝突の設定
- 05パーティクル効果の作成
- 06モデル読み込みとSwiftUI連携
活用シーン
- 01既存のSceneKitアプリを保守する
- 02シーンの照明、視点、マテリアルを設定する
- 03ゲームシーンにアニメーションと物理演算を追加する
- 04.scnまたは.daeアセットを検証して読み込む
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill scenekitインストール後、Claude Code で/scenekitを入力して読み込まれたか確認します。
同梱ファイルevals/evals.jsonreferences/scenekit-patterns.md
詳細
SceneKit
既存の SceneKit シーンのみを保守してください。Apple は WWDC 2025 で SceneKit を非推奨とし、保守のみを対象としています。新規プロジェクト、大規模なモダナイズ、USD/USDZ パイプラインには RealityKit を使用してください。既存のアプリは引き続き動作します。
目次
- シーンのセットアップ
- ノードとジオメトリ
- マテリアル
- ライティング
- カメラ
- アニメーション
- 物理演算
- パーティクルシステム
- モデルの読み込み
- SwiftUI との統合
- よくある間違い
- レビュー用チェックリスト
- 参考資料
シーンのセットアップ
UIKit での SCNView
import SceneKit
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene()
sceneView.allowsCameraControl = true
sceneView.autoenablesDefaultLighting = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)
allowsCameraControl を有効にすると、組み込みのオービット、パン、ズームジェスチャが追加されます。カスタムのカメラ制御が必要な本番環境では、通常は無効にします。
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])
ノードとジオメトリ
すべてのシーンには rootNode があります。すべてのコンテンツは子孫ノードとして存在します。ノードは、親の座標系での位置、向き、スケールを定義します。SceneKit は右手座標系を使用します。+X は右、+Y は上、+Z はカメラ方向です。
let 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 performance
組み込みプリミティブ
SCNBox、SCNSphere、SCNCylinder、SCNCone、SCNTorus、SCNCapsule、SCNTube、SCNPlane、SCNFloor、SCNText、SCNShape(押し出しベジェパス)。
let 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
}
マテリアル
SCNMaterial は表面の見た目を定義します。単一マテリアルのジオメトリには firstMaterial を、複数マテリアルの場合は materials 配列を使用します。
色とテクスチャ
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 = material
物理ベースレンダリング(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")
ライティングモデル
.physicallyBased(メタリック/ラフネス)、.blinn(デフォルト)、.phong、.lambert(拡散反射のみ)、.constant(ライティングなし)、.shadowOnly。
各マテリアルプロパティは SCNMaterialProperty で、UIColor、UIImage、CGFloat スカラー、SKTexture、CALayer、AVPlayer を受け取ります。
透明度
material.transparency = 0.5
material.transparencyMode = .dualLayer
material.isDoubleSided = true
ライティング
SCNLight をノードにアタッチします。ライトの方向は、ノードの負の Z 軸に沿います。
ライトの種類
// 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 = 60
ノードにアタッチします。
let 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 lights
SceneKit がノードごとに描画するライトは最大 8 個です。ポイントライト/スポットライトには attenuationEndDistance を設定し、遠くのノードでは SceneKit がそれらをスキップするようにします。
カメラ
SCNCamera をノードにアタッチして、視点を定義します。
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(0, 5, 15)
cameraNode.look(at: SCNVector3Zero)
scene.rootNode.addChildNode(cameraNode)
sceneView.pointOfView = cameraNode
設定
camera.fieldOfView = 60 // Degrees
camera.zNear = 0.1
camera.zFar = 500
camera.automaticallyAdjustsZRange = true
// Orthographic
camera.usesOrthographicProjection = true
camera.orthographicScale = 10
被写界深度(wantsDepthOfField、focusDistance、fStop)と HDR エフェクト(wantsHDR、bloomIntensity、bloomThreshold、screenSpaceAmbientOcclusionIntensity)は、SCNCamera で直接設定します。
アニメーション
SceneKit には 3 つのアニメーション手法があります。
SCNAction(宣言型、ゲーム向け)
ノードにアタッチする、再利用可能で組み合わせ可能なアニメーションオブジェクトです。
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(暗黙的アニメーション)
SCNTransaction.begin()
SCNTransaction.animationDuration = 1.0
node.position = SCNVector3(5, 0, 0)
node.opacity = 0.5
SCNTransaction.completionBlock = { print("Done") }
SCNTransaction.commit()
明示的なアニメーション(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")
物理演算
物理ボディ
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-driven
shape が nil の場合、SceneKit はジオメトリから形状を導き出します。パフォーマンス向上のため、簡略化した形状を使用します。
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.3
力の適用
node.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 = false
パーティクルシステム
SCNParticleSystem は、炎、煙、雨、火花などのエフェクトを作成します。
let 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)
Xcode のパーティクルエディタから SCNParticleSystem(named: "fire.scnp", inDirectory: nil) を使って読み込みます。パーティクルは colliderNodes を介してジオメトリと衝突できます。
モデルの読み込み
SceneKit でドキュメント化されているシーンソース形式は、.scn、.dae、.abc です。
同梱アセットの場合、シーンファイルは .scnassets フォルダーに配置し、テクスチャ画像はアセットカタログに配置してください。そうすることで、Xcode が対象デバイス向けに最適化できます。
USD/USDZ は RealityKit への移行パスであり、SceneKit のデフォルトの読み込みパスではありません。新規プロジェクト、大幅な更新、または SCN から USD へのアセット変換については、RealityKit スキルに引き継いでください。
enum SceneAssetError: Error { case missingResource, missingNode(String) }
func loadCheckedScene() throws -> SCNScene {
guard let url = Bundle.main.url(forResource: "model", withExtension: "dae")
else { throw SceneAssetError.missingResource }
let scene = try SCNScene(url: url, options: [.checkConsistency: true])
guard scene.rootNode.childNode(withName: "mesh", recursively: true) != nil
else { throw SceneAssetError.missingNode("mesh") }
return scene
}
これを作成/インポート時のゲートとして使用してください。一貫性チェックまたは必須ノードのチェックに失敗したら処理を止め、ソースアセットまたはインポートオプションを修正してから、同じチェックを再度実行します。生成した .scn ファイルについては、シーンのシリアライズを読み込み、コミット前にエクスポートの成功と、再読み込みの確認の両方を必須としてください。
大きなモデルには、読み込みポリシーを .onDemand に設定した SCNReferenceNode を使用してください。インポート時の単位変換には、SCNSceneSource.LoadingOption を使用します。
let source = SCNSceneSource(url: url, options: nil)!
let scene = try source.scene(options: [.convertUnitsToMeters: 1.0])
SCNScene.Attribute.unit や UnitMetersPerUnit は使用しないでください。SCNScene.Attribute はメタデータ専用で、.startTime、.endTime、.frameRate、.upAxis を指定できます。
SwiftUI との統合
SceneView は SceneKit を SwiftUI に埋め込みます。
import 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、.temporalAntialiasingEnabled。
レンダーループを制御するには、SCNView を UIViewRepresentable でラップし、SCNSceneRendererDelegate コーディネーターを使用します。references/scenekit-patterns.md を参照してください。
よくある間違い
カメラやライトを追加しない
// 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, options: nil))
// 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)
レビューチェックリスト
- シーンに
pointOfViewとして設定されたカメラノードが少なくとも 1 つある - シーンに適切なライティングがある(またはプロトタイピングでは
autoenablesDefaultLightingを使用する) - 物理形状には、メッシュの詳細全体ではなく、簡略化したジオメトリを使用している
- 衝突コールバックが必要なボディに
contactTestBitMaskが設定されている -
SCNPhysicsContactDelegateがscene.physicsWorld.contactDelegateに割り当てられている - ダイナミックボディのトランスフォームは、位置を直接変更するのではなく、力/インパルスを介して変更している
- ライトはノードごとに 8 個以下に制限されている。ポイントライト/スポットライトには
attenuationEndDistanceが設定されている - リアリスティックなレンダリングでは、マテリアルに
.physicallyBasedライティングモデルを使用している - SceneKit アセットには、ドキュメント化された
.scn、.dae、または.abcのシーンソース形式を使用している - インポートおよびエクスポートしたアセットは、コミット前に一貫性チェックと必須ノードのチェックに合格している
- 同梱する SceneKit のテクスチャ/画像には、アセットカタログまたは Xcode 最適化済みのリソースを使用している
- シーンのメタデータ/インポートオプションには、ドキュメント化された API を使用している。存在しない
SCNScene.Attribute.unitは使用していない - 新しい USD/USDZ パイプラインや大幅な更新は RealityKit に引き継いでいる
- Game Center の認証、リーダーボード、実績、またはマルチプレイヤー機能は GameKit に引き継いでいる
- 大きなモデルには
SCNReferenceNodeを使用し、遅延読み込みを有効にしている - パーティクル数を制御するため、パーティクルの
birthRateとparticleLifeSpanのバランスを取っている - ライトやカメラの対象を関連ノードに絞るため、
categoryBitMaskを使用している - SwiftUI のシーンには
SceneView、またはUIViewRepresentableでラップしたSCNViewを使用している - 非推奨であることを認識し、新規プロジェクトでは RealityKit を評価している
参考資料
- カスタムジオメトリ、シェーダーモディファイア、制約、モーフターゲット、ヒットテスト、シーンのシリアライズ、レンダーループデリゲート、パフォーマンス、SpriteKit オーバーレイ、LOD、Metal シェーダーについては、references/scenekit-patterns.md を参照してください。
- SceneKit のドキュメント、SCNSceneSource、SCNView、SceneView
- SCNPhysicsShape、SCNShadable
- WWDC 2025 セッション 288:SceneKit プロジェクトを RealityKit に移行する