WWDC Quick Look 💓 By SwiftGGTeam
Xcode で Reality Composer Pro 3 の機能を拡張する

Xcode で Reality Composer Pro 3 の機能を拡張する

元の動画を見る

ハイライト

Reality Composer Pro 3 は、Xcode で書いたプラグインを通じて、カスタム Component、System、アニメーション Action、Script Graph ノードをエディタへ公開できます。これによりアーティストやデザイナーは、App のコンパイルとデプロイを何度も繰り返さずに、エディタ内で直接パラメータを調整し、効果をプレビューできます。

主要内容

チーム協業の課題

空間コンピューティングプロジェクトでは、開発者とアーティストが別々に作業しがちです。エンジニアは Xcode でコードを書いて 3D シーンのロジックを制御し、アーティストは Reality Composer Pro でモデルを配置し、マテリアルを調整します。2つのワークフローは分断されています。アーティストがあるパラメータを調整して効果を見たい場合、エンジニアがコードを変更し、コンパイルし、パッケージ化し、デバイスへデプロイするのを待たなければなりません。このループは数分かかることもあり、1日の多くが待ち時間になります。

さらに大きな問題は、エンジニアが書いたカスタム Component や System が、エディタから見るとブラックボックスであることです。アーティストはそれらのコンポーネントの属性を見られず、エディタ内で直接変更できません。すべての調整がエンジニア経由になり、コミュニケーションコストが高く、反復速度が遅くなります。

Reality Composer Pro 3 のプラグイン機構

Apple の今年の解決策はプラグイン化です。エンジニアは Xcode で動的ライブラリ(Dynamic Library)を書き、プロジェクト固有の Component、System、アニメーション Action を Reality Composer Pro 3 に登録します。エディタがこのプラグインを読み込むと、これらのカスタム機能がエディタ UI に現れ、アーティストが直接使えるようになります。

(02:07) ワークフローは次のようになります。Xcode プロジェクトには2つの Scheme があります。1つは最終的なゲーム App をビルドする Scheme、もう1つは RCPCustomComponents.framework プラグインをビルドする Scheme です。Reality Composer Pro 3 プロジェクトは simulation bar を通じて Xcode プロジェクトと関連付けられ、エディタから直接 App を起動できます。2つのプロジェクトは同じ Git リポジトリに置き、チームメンバーはそれぞれ変更してコミットし、マージします。エディタにはカスタム merge tool もあり、JSON 形式の内部データを扱う時の衝突を減らします。

(03:34) データの流れも明確です。Xcode が App とプラグインをビルドし、Reality Composer Pro 3 がプラグインでカスタムコンポーネントとシステムを読み込みます。アーティストはエディタでシーンを調整し、最後に Reality File として書き出され、App がそれを読み込んで実行します。

Script Graph からコードへ切り替えるタイミング

(05:20) Reality Composer Pro 3 には Script Graph というビジュアルスクリプティングツールが組み込まれており、コードを書かずに多くのロジックを実装できます。しかし Script Graph にも限界があります。グラフが大きくなると保守が難しくなり、Script Graph の外にある他の Apple API(たとえば SwiftUI)を呼び出せません。

発表者は1つの例を挙げています。大釜(cauldron)の水位は、材料を入れると上下する必要があり、さらに渦の効果も必要です。このロジックをすべて Script Graph で作ると、グラフが非常に複雑になります。さらに後で食材の浮遊システムとも連携するため、コードで実装する方が合理的です。

詳細

カスタム Component と System

(06:07) 最初に、大釜の水位を保存する Component を定義します。

import RealityKit

public struct Cauldron: Component, Codable {
    public var waterLevel: Float

    enum CodingKeys: CodingKey {
        case waterLevel
    }
}

重要な点:

  • Component プロトコルにより、Cauldron を Entity へ付加できます。
  • Codable は必須です。Reality Composer Pro 3 がエディタ内でこのコンポーネントをシリアライズし、Reality File に保存できるようにするためです。
  • CodingKeys は、どの属性をシリアライズ対象にするかを制御します。単純な場面では省略できます。

(06:42) 次に、水位変化に反応する System を書きます。

import RealityKit

public struct CauldronSystem: System {
    let query = EntityComponentQuery(Cauldron.self)
    public init(scene: Scene) {}

    public func update(context: SceneUpdateContext) {
        for (entity, cauldron) in context.entities(matching: query) {
            guard let water = entity.findEntity(named: "Cauldron_Water_mesh")
                else { continue }
            water.setPosition(SIMD3<Float>(0, 1, 0) * cauldron.waterLevel, relativeTo: entity)
        }
    }
}

重要な点:

  • EntityComponentQuery(Cauldron.self) は、この system が関心を持つエンティティ集合を定義します。
  • update(context:) は毎フレーム実行され、Cauldron コンポーネントを持つすべてのエンティティを走査します。
  • findEntity(named:)"Cauldron_Water_mesh" という名前の子エンティティ、つまり水面メッシュを探します。
  • setPosition(_:relativeTo:) は水位の値に応じて水面の高さを調整します。

(07:00) 3つ目に、エディタがこれらのコンポーネントとシステムを認識できるよう、プラグインを作ります。

import RealityComposerPro

final class RCPCustomComponentsPlugin: RealityComposerProPlugin {
    public func setup(context: any RealityComposerProContext) {
        context.registerComponent(Cauldron.self)
        context.registerSystem(CauldronSystem.self)
    }
}

@_cdecl("createRealityComposerProPlugin")
public func createRealityComposerProPlugin() -> UnsafeMutableRawPointer {
    return RCPCustomComponentsPlugin().passRetained()
}

重要な点:

  • RealityComposerProPlugin プロトコルは RealityComposerPro Swift Package に含まれます。エディタで “Run With Xcode” を使ってプロジェクトをリンクすると自動的に追加されます。
  • setup(context:) でコンポーネントとシステムを登録します。エディタがプラグインを読み込むと認識できるようになります。
  • @_cdecl("createRealityComposerProPlugin") は関数を C シンボルとしてエクスポートし、プラグインローダーが入口を見つけられるようにします。
  • passRetained() は raw pointer を返します。プラグインローダーが動的ライブラリインターフェイス経由で呼び出すためです。

プラグイン Scheme をビルドした後、Reality Composer Pro 3 でプロジェクトを開くと、エディタはプラグインを信頼するか尋ねます。信頼すると、Cauldron コンポーネントが Custom Components フォルダに表示されます。これを大釜エンティティへ付加し、waterLevel 属性を調整すると、水面がリアルタイムに反応します。

(10:12) さらに便利なのは、Xcode で System の update メソッドにブレークポイントを置き、エディタプロセスへ attach してデバッグできることです。コードはエディタ内部で動き、通常の App をデバッグする時と同じ体験になります。

Shader Graph との連携

(10:35) 基本の水位調整ができたら、次は水面に渦効果を加えます。テクニカルアーティストは Shader Graph で渦のマテリアルをすでに作っており、rotationSpeedvortexCoeff などのパラメータがあります。これらのパラメータをコードから駆動する必要があります。

まず Cauldron コンポーネントを拡張し、渦に関する属性を追加します。

public struct Cauldron: Component, Codable {
    public var waterLevel: Float
    public var rotationSpeed: Float
    public var minWaterLevel: Float
    public var maxWaterLevel: Float
    public var vortexCoeff: Float
}

(11:05) その後 CauldronSystem を変更し、コンポーネント属性を Shader Graph のマテリアルパラメータへ同期します。

public func update(context: SceneUpdateContext) {
    for (entity, cauldron) in context.entities(matching: query) {
        guard let water = entity.findEntity(named: "Cauldron_Water_mesh") else { continue }
        water.setPosition(SIMD3<Float>(0, 1, 0) * cauldron.waterLevel, relativeTo: entity)

        guard var model = water.components[ModelComponent.self] else { continue }
        guard var mat = model.materials.first as? ShaderGraphMaterial else { continue }
        let surface = computeSurface(cauldron: cauldron)
        try? mat.setParameter(name: "Level Radius", value: .float(surface.levelRadius))
        try? mat.setParameter(name: "Lowest Point",
            value: .float(cauldron.waterLevel - surface.lowestPoint))
        try? mat.setParameter(name: "Height Change", value: .float(surface.heightChange))
        try? mat.setParameter(name: "Level Coeff", value: .float(surface.levelCoeff))
        try? mat.setParameter(name: "Is Level", value: .bool(surface.isLevel))
        model.materials[0] = mat
        water.components.set(model)
    }
}

重要な点:

  • water.components[ModelComponent.self] はエンティティのモデルコンポーネントを取り出します。
  • model.materials.first as? ShaderGraphMaterial は Shader Graph マテリアルを取得します。
  • setParameter(name:value:) はマテリアルパラメータを設定し、.float.bool などの型に対応します。
  • 変更後のマテリアルは model.materials[0] へ再代入し、その後 water.components.set(model) でエンティティへ書き戻す必要があります。

プラグインを再ビルドするたびに Reality Composer Pro 3 の再起動が必要です。再起動時にはコンポーネント変更の diff ダイアログが表示されます。確認すると新しい属性がエディタに表示されます。rotationSpeed を調整すると、渦の深さがリアルタイムに変化します。

カスタムアニメーション Action

(13:25) Reality Composer Pro 3 の animation sequencer はカスタム animation Action に対応します。発表者は、水位を 0.3 から 0.5 へ変化させるアニメーションを実演しました。

まず Action を定義します。

import RealityKit

public struct SetWaterLevelAction: EntityAction, Codable {
    public let startWaterLevel: Float
    public let endWaterLevel: Float

    public var animatedValueType: (any AnimatableData.Type)? { Transform.self }
}

重要な点:

  • EntityAction プロトコルにより、Action を sequencer timeline に配置できます。
  • Codable により、Action を Reality File へシリアライズできます。
  • animatedValueTypeTransform.self を返します。これは EntityAction プロトコルの要件で、アニメーション実行器が対象エンティティへアクセスするために使います。

(14:05) 次に実行ロジックを書きます。

extension SetWaterLevelAction {
    static func subscribe() {
        Task { @MainActor in
            SetWaterLevelAction.subscribe(to: .updated) { event in
                let normalizedTime = (event.playbackController.time - event.startTime) /
                    event.duration
                let action = event.action
                let currentLevel = action.startWaterLevel +
                    Float(normalizedTime) * (action.endWaterLevel - action.startWaterLevel)
                guard let entity = event.targetEntity else { return }
                guard var cauldron = entity.components[Cauldron.self] else { return }
                cauldron.waterLevel = currentLevel
                entity.components.set(cauldron)
            }
        }
    }
}

重要な点:

  • subscribe(to: .updated) はアニメーション更新イベントを監視します。
  • normalizedTime は現在のアニメーション進捗(0 から 1)を計算します。
  • 線形補間で現在の水位を計算します:start + progress * (end - start)
  • event.targetEntity から対象エンティティを取得し、その Cauldron コンポーネントを更新します。

(14:56) 最後にプラグインで Action を登録し、subscribe() を呼びます。

final class RCPCustomComponentsPlugin: RealityComposerProPlugin {
    public func setup(context: any RealityComposerProContext) {
        context.registerComponent(Cauldron.self)
        context.registerSystem(CauldronSystem.self)

        context.registerAction(SetWaterLevelAction.self)
        SetWaterLevelAction.subscribe()
    }
}

登録後、エディタで Sequence を作成し、アニメーショントラックを追加します。SetWaterLevelAction を timeline へドラッグし、開始水位と終了水位を設定して再生すると、水位が滑らかに変化します。

カスタム Script Graph ノード

(17:32) Script Graph は Reality Composer Pro 3 のビジュアルスクリプティングシステムで、デザイナーがコードを書かずにインタラクションを作れるようにします。プラグインを通じて、カスタムコンポーネントを Script Graph ノードとして公開できます。

最も簡単な方法は @Scriptable マクロを使うことです。

import RealityKit
import RealityKitScripting
import RealityKitScriptingMacros

@Scriptable
public struct Cauldron: Component, Codable {
    public var waterLevel: Float
    public var rotationSpeed: Float
    public var minWaterLevel: Float
    public var maxWaterLevel: Float
    public var vortexCoeff: Float
}

重要な点:

  • @Scriptable マクロは SchemaProvider.schema を自動生成し、コンポーネントの属性構造を記述します。
  • RealityKitScriptingRealityKitScriptingMacros を import する必要があります。
  • これら2つのモジュールは RealityComposerPro Swift Package と同じく、エディタから Xcode プロジェクトを作成する時に自動設定されます。

(18:08) 次に、プラグインでスクリプトモジュールを登録します。

public func setup(context: any RealityComposerProContext) {
    context.registerComponent(Cauldron.self)
    context.registerSystem(CauldronSystem.self)

    context.registerAction(SetWaterLevelAction.self)
    SetWaterLevelAction.subscribe()

    Task { @MainActor in
        let config = RKS.Configuration(id: "ChaparralVillage")
            .onInitialize { _ in
            [
                Module("ChaparralVillage") {
                    Cauldron.SchemaProvider.schema
                }
            ]
        }
        try! RKS.addConfiguration(config)
    }
}

重要な点:

  • スクリプトモジュールの登録はメインスレッドで行う必要があるため、Task { @MainActor in ... } で包みます。
  • RKS.Configuration(id:) はプロジェクト専用の設定を作成します。
  • onInitialize クロージャはモジュールリストを返し、各モジュールにはコンポーネントの schema が含まれます。
  • RKS.addConfiguration(config) は設定を RealityKitScripting システムへ追加します。

登録後、大釜エンティティに Scripting コンポーネントを追加し、Script Graph エディタを開きます。Update ノードと If ノードを追加してキー入力を検出します。If の true 分岐を Set Water Level ノードへ接続し、キー “a” を水位 0.25、キー “z” を水位 0.5 に設定します。simulation view を開いて “a” と “z” を押すと、水位がリアルタイムに上下します。

重要な示唆

1. リアルタイム調整可能なパーティクルシステムエディタを作る

カスタム Component にパーティクルパラメータ(発生率、寿命、色グラデーション)を保存し、System でパーティクル挙動を駆動します。アーティストは Reality Composer Pro 3 内でパラメータを調整し、パーティクル効果の変化をリアルタイムに確認できます。入口 API:ComponentSystemRealityComposerProPlugin

2. 物理インタラクションロジックをビジュアルノード化する

@Scriptable マクロを使い、カスタム物理コンポーネントを Script Graph へ公開します。デザイナーはノードをドラッグして衝突応答や重力場の強さを設定でき、コードを書く必要がありません。入口 API:@ScriptableRKS.Configuration

3. キャラクターアニメーション状態機械を作る

Idle、Walk、Jump など複数の EntityAction を定義し、それぞれが骨格アニメーションと移動を制御します。sequencer で状態遷移の timeline を編成し、デザイナーがエディタ内で遷移時間やブレンドウェイトを直接調整できるようにします。入口 API:EntityActionsubscribe(to: .updated)

4. マテリアルパラメータのリアルタイムプレビューツール

PBR マテリアルの各パラメータ(roughness、metallic、emissive)をカスタム Component にし、System が毎フレーム Shader Graph マテリアルへ同期します。アーティストはスライダーを調整するだけで異なるマテリアル効果を確認でき、何度も書き出してテストするよりはるかに速くなります。入口 API:ShaderGraphMaterial.setParameter(name:value:)

5. 複数人で共同作業する空間シーンのバージョン管理

Reality Composer Pro 3 のカスタム merge tool と Git 統合を使い、シーンデータ(JSON)とコードを同じリポジトリに置きます。エンジニアはプラグインを変更し、アーティストはシーンを変更し、それぞれコミットした後に自動マージして衝突を減らせます。入口:エディタ組み込みの merge tool + simulation bar による Xcode プロジェクト連携。

関連 Session

コメント

GitHub Issues · utterances