mcpField
A
axiom-scenekit

axiom-scenekit

github.com/charleswiltgen開発1.2k136 回インストール今日更新
リポジトリ

axiom-scenekit について

SceneKit プロジェクトの保守と RealityKit への移行計画を支援する開発ガイドです。レンダリング、物理演算、アニメーション、SwiftUI 連携、旧式の ARKit 利用、非推奨化の注意点を扱います。

主な機能

  1. 01SceneKit のシーンとレンダリングのガイド
  2. 02ジオメトリ、マテリアル、ライティング
  3. 03アニメーションと物理演算
  4. 04SwiftUI と ARKit の連携
  5. 05RealityKit への移行ガイダンス

活用シーン

  1. 01既存の SceneKit アプリの保守
  2. 02レンダリングや物理演算の問題調査
  3. 03SwiftUI でのシーン表示
  4. 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.scnXcode 固有で、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

コメント