axiom-scenekit について
SceneKit プロジェクトの保守と RealityKit への移行計画を支援する開発ガイドです。レンダリング、物理演算、アニメーション、SwiftUI 連携、旧式の ARKit 利用、非推奨化の注意点を扱います。
主な機能
- 01SceneKit のシーンとレンダリングのガイド
- 02ジオメトリ、マテリアル、ライティング
- 03アニメーションと物理演算
- 04SwiftUI と ARKit の連携
- 05RealityKit への移行ガイダンス
活用シーン
- 01既存の SceneKit アプリの保守
- 02レンダリングや物理演算の問題調査
- 03SwiftUI でのシーン表示
- 04RealityKit への移行計画
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-scenekitインストール後、Claude Code で/axiom-scenekitを入力して読み込まれたか確認します。
詳細
SceneKit 開発ガイド
目的:既存の SceneKit コードを安全に保守し、RealityKit への移行を計画します iOS バージョン:iOS 8 以降(SceneKit)、iOS 26 以降で非推奨 Xcode:Xcode 15 以降
このスキルを使用する場面
次の場合にこのスキルを使用してください:
- 既存の SceneKit コードを保守するとき
- SceneKit のプロトタイプを作成するとき(非推奨化を考慮)
- SceneKit から RealityKit への移行を計画するとき
- SceneKit のレンダリング、物理演算、アニメーションの問題をデバッグするとき
- SceneKit コンテンツを SwiftUI と統合するとき
- Model I/O または SCNSceneSource を使用して 3D モデルを読み込むとき
次の場合はこのスキルを使用しないでください:
- 新しい 3D プロジェクト(
axiom-realitykitを使用) - AR エクスペリエンス(
axiom-realitykitを使用) - visionOS 開発(
axiom-realitykitを使用) - SpriteKit 2D ゲーム(
axiom-spritekit) - Metal シェーダープログラミング(
axiom-metal-migration-ref)
非推奨化の背景
SceneKit は iOS 26 からソフト非推奨です(WWDC 2025)。これは次のことを意味します:
- 既存のアプリは引き続き動作します
- 新機能や一般的なバグ修正は提供されません
- 重大なセキュリティパッチのみが提供されます
SceneView(SwiftUI)は iOS 26 で正式に非推奨になります
Apple が今後推奨するのは RealityKit です。 新しい 3D プロジェクトにはすべて RealityKit を使用してください。SceneKit の知識は、レガシーコードの保守や移行時の概念の理解に引き続き役立ちます。
RealityKit では:ECS アーキテクチャがシーングラフに置き換わります。概念の対応表全体については axiom-scenekit-ref を参照してください。
1. メンタルモデル
シーングラフアーキテクチャ
SceneKit では、SCNScene 内のルートノードに接続された ノードのツリー(SCNNode)を使用します。各ノードには、親に対するトランスフォーム(位置、回転、スケール)があります。
SCNScene
└── rootNode
├── cameraNode (SCNCamera)
├── lightNode (SCNLight)
├── playerNode (SCNGeometry + SCNPhysicsBody)
│ ├── weaponNode
│ └── particleNode (SCNParticleSystem)
└── environmentNode
├── groundNode
└── wallNodes
RealityKit では:ノードがエンティティに置き換わります。ノードのプロパティはコンポーネントに置き換わります。階層の概念はそのまま維持されますが、動作はノードのコールバックではなくシステムによって制御されます。
座標系
SceneKit では 右手系の Y アップ座標系を使用します:
+Y (up)
|
|
+──── +X (right)
/
/
+Z (toward viewer)
RealityKit の座標系と一致するため、移行時に空間に関する概念をそのまま適用できます。
トランスフォーム階層
トランスフォームは親から子へと連鎖します。子のワールドトランスフォーム = 親のワールドトランスフォーム × 子のローカルトランスフォームです。
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)
RealityKit では:同じ概念です。entity.position はローカル座標を表し、entity.position(relativeTo: nil) はワールド座標を返します。
2. シーンのセットアップとレンダリング
SCNView(UIKit)
let sceneView = SCNView(frame: view.bounds)
sceneView.scene = SCNScene(named: "scene.scn")
sceneView.allowsCameraControl = true
sceneView.showsStatistics = true
sceneView.backgroundColor = .black
view.addSubview(sceneView)
SceneView(SwiftUI)— iOS 26 で非推奨
// Still works but deprecated. Use SCNViewRepresentable for new code.
import SceneKit
SceneView(
scene: scene,
pointOfView: cameraNode,
options: [.allowsCameraControl, .autoenablesDefaultLighting]
)
SCNViewRepresentable(SwiftUI の代替)
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) {}
}
RealityKit では:SwiftUI では RealityView を使用します。UIViewRepresentable は必要ありません。
3. ジオメトリとマテリアル
組み込みジオメトリ
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)
PBR マテリアル
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
RealityKit では:類似のプロパティを持ちながら API の構成が異なる PhysicallyBasedMaterial を使用します。対応関係については axiom-scenekit-ref の Part 1 を参照してください。
シェーダーモディファイア
SceneKit は、特定のエントリーポイントに挿入する GLSL/Metal シェーダーのスニペットをサポートしています:
// Fragment modifier — custom effect on surface
material.shaderModifiers = [
.fragment: """
float stripe = sin(_surface.position.x * 20.0);
_output.color.rgb *= step(0.0, stripe);
"""
]
エントリーポイント:.geometry、.surface、.lightingModel、.fragment
RealityKit では:Reality Composer Pro の ShaderGraphMaterial、または Metal 関数を使用する CustomMaterial を使用します。
4. ライティング
ライトの種類
| 種類 | 説明 | シャドウ |
|---|---|---|
.omni | 全方向に光を放つポイントライト | なし |
.directional | 平行光線(太陽) | あり |
.spot | 円錐状のビーム | あり |
.area | 長方形の発光体(ソフトシャドウ) | あり |
.IES | 実世界の光源プロファイル | あり |
.ambient | 均一で方向性がない光 | なし |
.probe | キューブマップによる環境ライティング | なし |
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)
RealityKit では:エンティティのコンポーネントとして DirectionalLightComponent、PointLightComponent、SpotLightComponent を使用します。イメージベースライティングには EnvironmentResource を使用します。
5. アニメーション
SCNAction(宣言型)
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)
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
node.position = SCNVector3(0, 5, 0)
node.opacity = 0.5
SCNTransaction.commit()
明示的なアニメーション(CAAnimation ブリッジ)
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()
RealityKit の場合:USD ファイルから読み込んだアニメーションには、entity.playAnimation() を使用します。トランスフォームアニメーションには、entity.move(to:relativeTo:duration:) を使用します。
6. 物理演算
物理ボディ
// 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
}
}
RealityKit の場合:PhysicsBodyComponent、CollisionComponent、および scene.subscribe(to: CollisionEvents.Began.self) による衝突イベントのサブスクリプションを使用します。
7. ヒットテストとインタラクション
// 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
}
RealityKit の場合:ドラッグ、回転、拡大縮小のジェスチャーには ManipulationComponent を使用するか、衝突ベースのヒットテストを使用します。
8. アセットパイプライン
対応フォーマット
| フォーマット | 拡張子 | 備考 |
|---|---|---|
| USD/USDZ | .usdz、.usda、.usdc | 推奨フォーマット。SceneKit と RealityKit の両方で動作します |
| Collada | .dae | レガシー形式ですが、引き続きサポートされています |
| SceneKit Archive | .scn | Xcode 固有で、RealityKit には移植できません |
| Wavefront OBJ | .obj | ジオメトリのみで、アニメーションはありません |
| Alembic | .abc | アニメーションのベイク処理 |
モデルの読み込み
// 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)
移行のヒント:RealityKit に移行する前に、xcrun scntool --convert file.scn --format usdz を使用して .scn ファイルを .usdz に変換します。
9. ARKit との統合(レガシー)
// 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))
}
RealityKit の場合:AnchorEntity タイプとともに RealityView を使用します。ARSCNView はレガシーです。新しい AR 開発ではすべて RealityKit を使用してください。
10. アンチパターン
アンチパターン 1:新規プロジェクトを SceneKit で始める
時間的コスト:最終的に移行が必要になった際、数週間の手戻りが発生します
SceneKit は非推奨です。最初は学習曲線が急でも、新規プロジェクトでは最初から RealityKit を使用してください。
アンチパターン 2:USDZ に変換せずに .scn ファイルを使用する
時間的コスト:移行開始時に数時間かかります
.scn ファイルは SceneKit 固有で、RealityKit では読み込めません。早めに変換してください:
xcrun scntool --convert model.scn --format usdz --output model.usdz
アンチパターン 3:シェーダーモディファイアを過度にカスタマイズする
時間的コスト:移行時に全面的な書き換えが必要です
SceneKit のシェーダーモディファイアは、独自のエントリーポイントシステムを使用します。ここに多大な投資をしても、RealityKit の ShaderGraphMaterial にはまったく移植できません。
アンチパターン 4:カスタムパイプラインに SCNRenderer を頼る
時間的コスト:移行時にアーキテクチャの再設計が必要です
カスタムレンダーパイプラインが必要な場合は、Metal を直接使用するか、RealityRenderer(RealityKit の Metal レベル API)を使用してください。
アンチパターン 5:非推奨の警告を無視する
時間的コスト:Apple が API を削除した際に、予期せぬ不具合が発生します
SceneView の非推奨警告を追跡し、UIViewRepresentable によるフォールバック、または RealityKit への移行を計画してください。
アンチパターン 6:ループ内で何百ものノードを作成する
時間的コスト:フレーム落ちのデバッグに 2~4 時間かかり、多くの場合 GPU の問題と誤診されます
// ❌ 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)]
ルール:同一オブジェクトが 50 個を超える場合は、SCNParticleSystem を使用するか、ジオメトリを統合してください。異なるオブジェクトの場合は、SCNNode.flattenedClone() を使用してドローコールを削減してください。
11. 移行の判断ツリー
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.
12. プレッシャーのかかる状況
シナリオ 1:「SceneKit を使えばいい。問題なく動いている」
プレッシャー:チームが SceneKit に慣れており、リリース期限が迫っています
誤ったアプローチ:チームが使い慣れているという理由で、新規プロジェクトを SceneKit で始めます。
正しいアプローチ:RealityKit の学習に投資します。SceneKit に新機能が追加されることはありません。先延ばしにするほど、移行の負債が大きくなります。
反論のテンプレート:「SceneKit は iOS 26 で非推奨になりました。新しい開発を SceneKit で始めると、機能を追加するたびに移行の負債が増えます。RealityKit の ECS モデルは異なりますが、習得できます。今のうちに時間を投資しましょう。」
シナリオ 2:「RealityKit を学ぶ時間がありません」
プレッシャー:厳しい納期、チームに ECS の経験がない
誤ったアプローチ:納期に間に合わせるため、すべてを SceneKit で構築します。
正しいアプローチ:必要であれば SceneKit でプロトタイプを構築しますが、SceneKit への依存箇所をすべて記録し、移行を計画します。移植性を確保するため、最初から USDZ アセットを使用します。
異論を伝える際のテンプレート:「USDZ アセットを使い、SceneKit レイヤーは薄く保ちましょう。移行時にはアセットをそのまま移行でき、変更が必要なのはコードレイヤーだけです。」
シナリオ 3:「一度にすべてを移行する」
プレッシャー:すっきりとした移行を実現したい
誤ったアプローチ:SceneKit のコードベース全体を一度に RealityKit で書き直そうとします。
正しいアプローチ:段階的に移行します。新機能は RealityKit で実装します。既存の SceneKit コードは、変更が必要になるまでそのままにします。Swift パッケージでモジュール化します(Apple の移行ガイドに従います)。
異論を伝える際のテンプレート:「Apple 自身の移行ガイドでは、Swift パッケージにモジュール化し、システムごとに移行することが推奨されています。一括での書き直しは、アプリ全体に新たなバグを持ち込むリスクがあります。」
コードレビューのチェックリスト
- 移行計画なしで、iOS 26 以降を対象とするプロジェクトに SceneKit のコードを新たに追加していない
- 移植性を確保するため、アセットは USDZ 形式(.scn ではない)になっている
- RealityKit での代替手段を特定せずに、シェーダーモディファイアを深くカスタマイズしていない
- 暗黙的なアニメーションに SCNTransaction を使用している(アニメーションコンテキストなしでプロパティを直接変更していない)
- 物理演算の categoryBitMask を明示的に設定している(デフォルトに依存していない)
- コンタクトデリゲートを設定し、プロトコルへの準拠を追加している
- 完了ハンドラーとクロージャーで
[weak self]を使用している - 開発中にデバッグオーバーレイを有効にしている(
showsStatistics = true)
リソース
WWDC:2014-609、2014-610、2017-604、2019-612
ドキュメント:/scenekit、/scenekit/scnscene、/scenekit/scnnode、/scenekit/scnmaterial、/scenekit/scnphysicsbody
スキル:axiom-scenekit-ref、axiom-realitykit、axiom-realitykit-ref