WWDC Quick Look 💓 By SwiftGGTeam
RealityKit の進化を探る

RealityKit の進化を探る

元の動画を見る

ハイライト

RealityKit は今年、ソフトシャドウ、投影テクスチャ、物理空間ライティング、ナビゲーションメッシュ、布シミュレーション、LOD 切り替え、3D Gaussian Splat レンダリング、カスタムリバーブメッシュを追加します。これにより visionOS 上の 3D ゲームや没入型アプリは、「動く」段階から「本物らしく感じる」段階へ進みます。

主要内容

ライティングと影:仮想世界を実環境へなじませる

以前 visionOS で室内シーンを作ると、隅が暗いままになりがちでした。間接光は開発者が自分で工夫する必要があり、動的光源の影の端は刃物で切ったように硬く見えました。

Apple は今年、この問題を3方向から解決しています。

Lightmap(ライトマップ)は、静的シーンの間接光を簡単にします。Reality Composer Pro 3 で間接光、環境遮蔽、beauty マップをベイクし、そのままシーンへ貼ると、隅がすぐ明るくなります。(02:24)

ただし Lightmap が解決するのは静的シーンだけです。動的光源はどうするのでしょうか。RealityKit は新たに Soft Shadows(ソフトシャドウ)を追加しました。現実世界の光源には面積があり、影の端には半影(penumbra)が生まれます。lightSizequality を設定すると、スポットライトの影は硬い端から自然なグラデーションへ変わります。(04:02)

最も印象的なのは Physical Space Lighting(物理空間ライティング)です。以前、仮想光源は仮想物体だけを照らせました。今は仮想のスポットライトや点光源が、実際の部屋を直接照らせます。Projective Textures(投影テクスチャ)と組み合わせれば、仮想プラネタリウム投影機を作り、星空や星雲を寝室の壁や家具へ直接投影できます。(05:16)

ナビゲーションメッシュ:NPC がようやく歩き方を知る

3D ゲームで最も面倒なことの1つは経路探索です。以前の visionOS 開発者は A* アルゴリズムを自前で書き、障害物、地形コスト、はしご、ジャンプ地点も扱う必要がありました。

RealityKit は今年、ナビゲーションメッシュをネイティブにサポートしました。Reality Composer Pro 3 で歩行可能領域を描き、森には高い traversal cost を設定し、橋には off-mesh connection を追加します。その上でコードから NavigationController.computePath を使い、非同期で経路を計算できます。(07:44)

非同期であることが重要です。visionOS はフレームレートに非常に敏感です。経路探索の計算をバックグラウンドスレッドへ渡し、メインスレッドは描画に集中します。(09:46)

布シミュレーション:カーテン、マント、シーツが動く

RealityKit は完全な布シミュレーションシステムを追加しました。メッシュの頂点を粒子、辺をバネとして扱うことで、布のしわや流れをリアルタイムにシミュレートできます。(11:01)

より実用的なのは「アンカー」機能です。布の一部の頂点を kinematic に設定すると、それらはエンティティの transform に追従しつつ、布シミュレーションの影響は受けません。これによりカーテンをドア枠に掛けたり、マントをキャラクターの肩に固定したりできます。(12:51)

性能最適化:LOD と熱状態管理

ソフトシャドウ、布シミュレーション、Gaussian Splat はどれも性能コストが高い機能です。RealityKit はそれに対応する2つのツールを提供します。

LOD(Level of Detail)は、遠くの物体を自動的に低ポリゴンモデルへ切り替えます。RealityKit はカメラ距離と画面面積という2つの切り替え戦略をサポートします。visionOS では画面面積に基づく方式の方が向いています。視野の端にある物体は、距離が近くても占めるピクセル数が少ないことがあるためです。(14:42)

熱状態の監視により、App は品質を能動的に下げられます。デバイスが .serious または .critical に達したら、LOD 切り替えしきい値を上げたり、影品質を下げたりして、システムの強制的なクロック低下によるカクつきを避けられます。(16:26)

3D Gaussian Splat:現実世界を仮想空間へ持ち込む

3D Gaussian Splat は、3D ガウス分布でシーンを表現する技術です。従来のフォトグラメトリより軽量で、より豊かなディテールを保持できます。RealityKit は今年、Gaussian Splat のネイティブレンダリングをサポートしました。位置、スケール、回転、不透明度、球面調和関数のバッファを渡すだけで、高忠実度な現実世界のスキャンデータを visionOS 内で表示できます。(17:13)

空間オーディオ:カスタムリバーブで音をより本物らしくする

空間オーディオの没入感は、反射と残響の正確さに左右されます。RealityKit は Custom Reverb Mesh を追加しました。ReverbMeshResource で部屋の形状を定義し、各表面に吸収係数と散乱係数を指定できます。仮想バンドが博物館で演奏すると、音は壁の間で現実的に反射してからユーザーの耳に届きます。(19:08)

詳細

ソフトシャドウの設定

(04:02)

guard var shadow = hearthSpotlight.components[SpotLightComponent.Shadow.self] else {
    // エラー処理
}
shadow.lightSize = 0.7 // メートル
shadow.quality = .medium // または .high
// shadow.quality = .low // 硬い影になります
hearthSpotlight.components.set(shadow)

重要な点:

  • lightSize は光源の直径(メートル)を表します。値が大きいほど影の端が柔らかくなります。
  • quality はサンプル数を制御します。.medium.high だけがソフトシャドウを生成し、.low は硬い影へ退化します。
  • .high は見た目が良い一方で性能コストが高くなります。中距離や遠景の物体には .medium が最もバランスに優れます。

投影テクスチャと物理空間ライティング

(06:13)

let spotLightEntity = Entity()
spotLightEntity.components.set(SpotLightComponent(
    color: .white,
    intensity: intensity,
    innerAngleInDegrees: innerAngle,
    outerAngleInDegrees: outerAngle,
    attenuationRadius: attenuationRadius,
))

let projectiveTexture: TextureResource = generateStarsAndNebulaeTexture()
spotLightEntity.components.set(SpotLightComponent.ProjectiveTexture(
    texture: projectiveTexture
))

(07:13)

spotLightEntity.components.set(SpotLightComponent.SurroundingsLight())

重要な点:

  • SpotLightComponent.ProjectiveTexture はテクスチャをスライドのように投射します。窓から差す光、海中の caustics、星空投影の表現に向いています。
  • SurroundingsLight は仮想光源で実環境を照らせます。現在は SpotLight と PointLight のみ対応しています。
  • 色は白に設定し、投影テクスチャへ余計な色調を重ねないようにします。

ナビゲーションメッシュの経路クエリ

(09:46)

extension Entity {
    public func navigate(/* ... */) async {
        let navigator = try! NavigationController(entity: self)
        guard let result = await navigator.computePath(from: fromPosition, to: toPosition)
        else {
            return
        }
        if result.isEmpty {
            return
        }
        for node in result {
            switch node.category {
                case .meshPoint:
                    finalPath.append(node.position)
                case .offMeshConnection:
                    // はしごを処理
            }
        }
    }
}

重要な点:

  • NavigationControllerNavigationComponent を持つエンティティで初期化する必要があります。
  • computePath は非同期メソッドで、メインスレッドをブロックしません。
  • 返される path nodes は2種類です。.meshPoint はメッシュ上の通常の経路点、.offMeshConnection ははしごやジャンプ地点などの特殊な接続です。
  • nil は有効な経路が見つからないことを意味し、空配列はすでに目的地に到達していることを意味します。

布アンカーの固定

(12:51)

for (pin, pinComponent) in pins {
    let position = pin.position(relativeTo: event.entity)
    let selectionSphere = ClothSphereShape(radius: pinComponent.radius)

    let vertices = clothMesh.vertices(in: .sphere(selectionSphere),
                                center: position)
    clothBody.motionTypes.set(vertexIndices: vertices, value: .kinematic)
}

重要な点:

  • ClothSphereShape は、固定する頂点を選ぶための球形領域を定義します。
  • clothMesh.vertices(in:center:) は球形領域内のすべての頂点インデックスを返します。
  • .kinematic は、それらの頂点がエンティティの transform だけに追従し、布シミュレーションの影響を受けないことを意味します。
  • カーテンのフック、マントの肩紐などの表現に向いています。

カメラ距離による LOD 切り替え

(14:42)

let lod0 = [ModelEntity(mesh: lodMesh0)]
let lod1 = [ModelEntity(mesh: lodMesh1)]
let lod2 = [ModelEntity(mesh: lodMesh2)]

let entity = Entity()

LevelOfDetailComponent.addByCameraDistance(to: entity, levels: [
    (entities: lod0, maxDistance: 1.0 /* メートル */), // 最高精度
    (entities: lod1, maxDistance: 5.0),              // 中精度
    (entities: lod2, maxDistance: .infinity),        // 低精度
])

重要な点:

  • lod0 は最高精度で、1メートル以内で使われます。
  • 1メートルを超えると lod1、5メートルを超えると lod2 に切り替わります。
  • 最後の LOD の maxDistance.infinity に設定し、遠距離でも常に描画可能なモデルがあるようにします。

画面面積による LOD 切り替え

(15:58)

let lod0 = [ModelEntity(mesh: lodMesh0)]
let lod1 = [ModelEntity(mesh: lodMesh1)]
let lod2 = [ModelEntity(mesh: lodMesh2)]

let entity = Entity()

LevelOfDetailComponent.addByScreenArea(to: entity, levels: [
    (entities: lod0, minArea: 0.2 /* 画面面積に対する割合 */),  // 最高精度
    (entities: lod1, minArea: 0.1),                                // 中精度
    (entities: lod2, minArea: 0.01),                               // 低精度
])

重要な点:

  • minArea は、物体が画面全体に占める割合です。
  • 画面の20%以上を占める場合は最高精度、10%-20% は中精度、1%-10% は低精度を使います。
  • 画面面積による切り替えは距離による切り替えより正確です。特に視野の端にある物体に向いています。

熱状態の監視

(16:26)

NotificationCenter.default.addObserver(of: ProcessInfo.self,
                                       for: .thermalStateDidChange) { _ in
    switch ProcessInfo.processInfo.thermalState {
        case .nominal, .fair:
            // 現状維持
        case .serious, .critical:
            // 性能を改善するために:
            // LOD 切り替えをより積極的にする
            // 影品質を下げる
    }
}

重要な点:

  • .thermalStateDidChange 通知を監視し、デバイスが熱くなった時に能動的に品質を下げます。
  • .nominal.fair は温度が正常であることを示し、現在の画質を維持できます。
  • .serious.critical では、LOD しきい値を上げたり、影品質を下げたり、布シミュレーション精度を落としたりする必要があります。

3D Gaussian Splat レンダリング

(18:44)

let resource = try GaussianSplatResource.BufferResource(
    count: splatCount,
    position: positionBuffer,
    scale: scaleBuffer,
    rotation: rotationBuffer,
    opacity: opacityBuffer,
    sphericalHarmonics: (sphericalHarmonicsBuffer, degree)
)

let splatResource = GaussianSplatResource(resource)
let splatComponent = GaussianSplatComponent(splatResource)
splatEntity.components.set(splatComponent)

重要な点:

  • BufferResource は位置、スケール、回転、不透明度、球面調和関数の5つのバッファを受け取ります。
  • degree は視点による色変化の度合いを制御します。0 は単色を意味し、値が高いほど豊かな色変化を表現できます。
  • RealityKit は特定のファイル形式に縛りません。開発者は .ply などの形式を自分で解析し、バッファを埋める必要があります。
  • レンダリング最適化は RealityKit 内部で処理されます。開発者が手動でソートやブレンドを行う必要はありません。

カスタムリバーブメッシュ

(20:49)

let mesh: ReverbMeshResource = .shoebox(size: [5, 4, 6])
let reverb: Reverb = .simulated(mesh: mesh, materials: [.dryWall])
entity.components.set(ReverbComponent(reverb: reverb))

(21:33)

let thickCarpet: Audio.Material = .carpet.scalingAbsorption { freq in 0.1 }

let bookshelfAbsorption = Audio.Absorption(
    [0.10, 0.15, 0.28, 0.20, 0.15, 0.10, 0.10, 0.07, 0.07, 0.05])

let bookshelfScattering = Audio.Scattering([500: 0.5, 1000: 0.6, 4000: 0.7])

let bookshelf = Audio.Material(
    absorption: bookshelfAbsorption,
    scattering: bookshelfScattering
)

重要な点:

  • .shoebox(size:) は最も簡単な入口です。内向きの箱を作ります。
  • 組み込みのプリセット材質には .dryWall.carpet などがあります。
  • カスタム材質では、31.5Hz から 16kHz までの10個の周波数帯に対する吸収係数と、500Hz、1kHz、4kHz の散乱係数を定義します。
  • Immersive Space のみ対応します。Shared Space では、実際の部屋からシステムが構築したリバーブが自動的に使われます。

重要な示唆

1. 「仮想と現実の光が混ざる」室内デザイン App を作る

仮想家具をユーザーの実際の部屋に置き、SurroundingsLight で仮想デスクライトが実際の壁や床を照らすようにします。ユーザーがライトを回すと、実際の壁面上で光と影がリアルタイムに変化します。入口 API は SpotLightComponent.SurroundingsLight() です。

2. visionOS 上の脱出ゲームを作る

Reality Composer Pro 3 の NavigationMeshResource で部屋の歩行可能領域を描き、狭い通路には高い traversal cost を設定して、プレイヤーキャラクターが障害物を自動的に避けるようにします。NPC は NavigationController.computePath でプレイヤーを非同期追跡します。投影テクスチャと組み合わせて、暗号の手がかりを壁へ投影できます。

3. 没入型の博物館ガイドを作る

3D Gaussian Splat で博物館の実際の展示品をスキャンし、ユーザーが visionOS 内で高忠実度なスキャンを自由に回り込んで見られるようにします。各展示品の横に仮想解説プレートを置き、カスタムリバーブメッシュで解説音声が展示室内を現実的に反射するようにします。入口は GaussianSplatComponentReverbMeshResource です。

4. 物理的な手応えのある魔法 RPG を作る

キャラクターのマントを ClothBodyComponent でシミュレートし、戦闘時に風で揺れるようにします。詠唱時の光効果は SpotLightComponent.ProjectiveTexture でルーン模様を床や壁へ投影します。熱状態の監視により、長時間プレイでもカクつきを抑えます。

5. 仮想プラネタリウムを作る

星空をユーザーの実際の寝室の天井へ投影し、SurroundingsLight で仮想投影機が実環境を照らすようにします。ユーザーが頭を動かすと、星空の模様が視点に合わせて変化します。SpotLightComponentinnerAngleInDegreesouterAngleInDegrees を組み合わせ、投影範囲を調整します。

関連 Session

コメント

GitHub Issues · utterances