ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
iOS/iPadOSゲームのvisionOSへの展開
iOS/iPadOS向けゲームをvisionOSに展開し、比類のない体験を実現する方法をご紹介します。3Dフレームやイマーシブな背景を追加すると、臨場感やワクワク感が高まります。立体視機能やヘッドトラッキングを利用してウインドウに奥行きを持たせ、プレイヤーをゲームの世界にいざないましょう。
関連する章
- 0:00 - Introduction
- 1:42 - Render on visionOS
- 3:48 - Compatible to native
- 6:41 - Add a frame and a background
- 8:00 - Enhance the rendering
リソース
関連ビデオ
WWDC24
- 魅力的な空間写真と空間ビデオの作成
- iOS、macOS、visionOS向けRealityKit APIの紹介
- RealityKitによる空間描画アプリの構築
- visionOSでのパススルーによるMetalのレンダリング
- visionOSにおけるゲームでの入力の詳細
WWDC23
-
ダウンロード
こんにちは RealityKitとvisionOSの ソフトウェアエンジニアのOlivierです このビデオでは iOSやiPadOS向けのゲームを visionOSでよりイマーシブなゲームへと 変身させる方法を紹介します
例えば Wylde Flowersというゲームは 居心地のよい暮らしと農業を体験できます これはiPadバージョンです
これも同じくiPadバージョンを 互換性のあるアプリとして visionOSのウインドウで実行したものです
visionOS用のApp Storeから 入手可能な最新バージョンの Wylde Flowersには このプラットフォームに固有の 多くの機能強化が行われています
例えば ウインドウが 3Dフレームで囲まれています
このフレームは ゲームプレイに 基づいて変化します 例えば プレイヤーが庭を操作すると 庭の3Dモデルと追加のUIが フレームに表示されます
ゲームの周囲に 舞い落ちてくるタンポポの種や 周囲を飛び回るハチドリなど イマーシブな背景が表示されます
ゲームビューはステレオスコピック(3次元) でレンダリングされ シーンに奥行きを加えています
これらの機能強化により visionOSでの Wylde Flowersはさらに良くなりました このビデオでは 同じことを皆さんのiOSおよびiPadOS向け ゲームでも行う方法を紹介します
まず visionOSで利用可能な レンダリング技術について説明します 次に iOSアプリをネイティブのvisionOS アプリに変換する方法を紹介します さらに ゲームに RealityKitフレームや背景を 追加する方法を紹介します 最後に ゲームのMetalレンダリングを ステレオスコピック ヘッドトラッキング VRRを使用して強化する方法を紹介します
まず visionOSで利用可能な レンダリング技術を見てみましょう visionOSでは RealityKitとMetalを 使用した3Dレンダリングを実行できます
RealityKitは Swiftで直接使用するか Unity PolySpatialなどの テクノロジーを介して間接的に 使用できるフレームワークです これによりvisionOSでは 複数のアプリを 同じ共有スペースで実行できます 例えば RealityKitを使用して Game Roomや LEGO Builder’s Journeyのような ボリュメトリックウインドウ内の コンテンツをレンダリングできます または Super Fruit Ninjaや Synth Ridersのように イマーシブな空間に コンテンツをレンダリングできます
Metalを使ってvisionOSで 直接レンダリングすることもできます この方法はカスタムレンダリング パイプラインが必要な場合や ゲームをRealityKitに移植することが 実用的ではない場合に選択できます
visionOSでのMetalを使った レンダリングには2つの方法があります
ゲームは互換性のあるアプリとして ウインドウで実行できます その場合 アプリの動作は iPadでの動作と非常によく似ています
これは Wylde FlowersのiPadバージョンが visionOS上で互換性のあるアプリとして 動作している例です
アプリをウインドウに表示することの 大きな利点は 共有スペース内でほかのデバイスと 並行して実行できることです 例えば Safariを使ったり 友達にメッセージを送ったりしながら ゲームをプレイすることができます
また CompositionorServicesを使用して フルイマーシブアプリとして実行できます その場合 ゲームのカメラは プレイヤーの頭部の動きによって制御されます 例えば 「Render Metal with passthrough in visionOS」のビデオにあるサンプルは CompositorServicesを使用して レンダリングされています 詳しくは ビデオをご覧ください
iPadアプリをvisionOSで互換性のある アプリとして実行するのはとても簡単です 一方 CompositionorServicesを使用して ゲームをイマーシブなアプリにすることで アプリはより鮮やかになりますが 大幅な再設計や やり直しが必要になるかもしれません なぜなら プレイヤーがカメラを完全に制御し シーン内のどこでも見ることができるからです
このビデオでは この2つの方法の中間にある 一連のテクニックを紹介します 互換性のあるアプリから始めて 徐々に機能を追加して イマーシブ感を向上させていき Vision Proの機能を 活用する方法を紹介します
最初の最も簡単なステップは ゲームをvisionOSで 互換性のあるアプリとして実行することです
Metal Deferred Lightingのサンプルを iOSゲームの例として使用します このビデオにはiPadで実行した サンプルが示されています
このサンプルのiOSバージョンを デベロッパWebサイトの developer.apple.com/jp からダウンロードできます
最初に iOS SDKを使って アプリをコンパイルし visionOSで互換性のある アプリとして実行しましょう
互換性のあるアプリは visionOSのウインドウ内で実行されます
visionOSでは タッチコントロールと ゲームコントローラの両方が 利用できるため すべてのプラットフォームで一貫した体験を すぐに提供できます
ゲーム入力についての詳細は 「Explore game input in visionOS」 というビデオをご覧ください
互換性のあるアプリでも正常に動作しますが アプリをネイティブアプリに変換して visionOS SDKを使ってみましょう アプリのに移動し iOSターゲットを選択します 現在は visionOSでiOS SDKが 使用されているため と表示されています
サポートする出力先として Apple Visionを追加して visionOS SDKでコンパイルしましょう
エラーが発生することもありますが アプリがiOS向けに作られている場合 ほとんどのコードはコンパイルされます
例えば Metal iOSゲームのコンテンツを visionOSに表示するには オプションがいくつかあります
CAMetalLayerにレンダリングして UIビューに簡単に統合できます または 新しいLowLevelTexture APIを 使用してRealityKitテクスチャに 直接レンダリングすることができます
もし使いやすければ CAMetalLayerから始めてもよいですが LowLevelTextureに移行して 制御性を最大にすることをお勧めします
CAMetalLayerにレンダリングする場合 それを含むビューを作成します
CADisplayLinkを作成して フレームごとにコールバックを取得します
コードはこのようになります UIViewはCAMetalLayerを layerClassとして宣言しています 次に CAMetalDisplayLinkを作成して レンダリングコールバックを取得します
最後に フレームごとにコールバックで CAMetalLayerをレンダリングします
LowLevelTexturesも同様に使用できます 指定されたピクセル形式と解像度から LowLevelTextureを作成できます 次に LowLevelTextureから TextureResourceを作成し RealityKitシーンの任意の場所で使います CommandQueueを使用して MTLTextureを介して LowLevelTextureに描画できます
コードではこのようになります LowLevelTextureを作成します それからTextureResourceを作成し RealityKitシーンの任意の場所で使用します
次に フレームごとに テクスチャに描画します
LowLevelTextureについて詳しくは 「Build a spatial drawing app with RealityKit」というビデオをご覧ください これでゲームをネイティブの visionOSアプリに変換できたので visionOS固有の機能を追加できます 例えば ImmersiveSpaceで ゲームビューの周りにフレームや 背景を追加することで アプリの イマーシブ感を向上させることができます
Cut The Rope 3ではウインドウが 動的なフレームで囲まれています
フレームはRealityKitで またゲームは Metalでレンダリングされています
少し前に見たように ZStackを使用してMetalビューで ゲームをテクスチャにレンダリングして 実現できます RealityViewを使用してフレームを作成し ゲームの周囲に3Dモデルを読み込みます
@State変数を使用して フレームを動的することができます 例えば Cut The Rope 3では 盤面に応じてフレームが変化します ゲームの背後に イマーシブな背景を追加することもできます Void-Xは その良い例です ゲームプレイの大部分は ウインドウ内で行われますが Void-Xでは 背景で 雨を降らせたり 稲妻を光らせたり 3Dで部屋中に銃弾が飛び交うようにして イマーシブ感を高めています
背景は SwiftUIで ImmersiveSpaceを使って作成します
そして iOSゲームをWindowGroupに 入れます
SwiftUIの@Stateオブジェクトを使って ウインドウとImmersiveSpaceの間で @Stateを共有します
ここまでゲームの周囲に要素を 追加する方法を説明しました 次に ゲームのMetalレンダリングの 向上に役立つ 様々なテクニックを紹介します まず ステレオスコピックを追加して ゲームに奥行きを 追加する方法を紹介します 次に ヘッドトラッキングを追加して ゲームを別世界につながる 物理的な窓のように見せる方法を紹介します 最後に VRRをゲームに追加して パフォーマンスを向上させる 方法を紹介します
ゲームにステレオスコピックを追加して シーンに奥行きを追加することができます これは3D映画の仕組みと同じです
これはステレオスコピックでレンダリングした Deferred Lightingサンプルの 1シーンです 説明のため 赤とシアンの色合いを使用したアナグリフで ステレオスコピック画像を示していますが Vision Proでは 左右の目に異なる画像が表示されます
ステレオスコピックは 基本的に左右の目に 異なる画像を見せることで機能します visionOSでは RealityKitのShaderGraphを使って 具体的に言うと CameraIndexノードを使って
左右の目に異なる画像を提供して ステレオスコピックを実現します
ステレオスコピックによる奥行き効果は 各画像におけるオブジェクトの ビュー間の距離に由来し これを視差と呼びます
程度の差はありますが この視差によって オブジェクトまでの距離に応じて 目が収斂します 遠くを見るときは目が平行になり 近く見るときに目は収斂します 人間の脳は これを1つの手がかりとして 距離を判断しています このようにして ステレオスコピックにより 物理的なミニチュアが 目の前にあるかのように また別の世界につながる物理的な 窓のように見せることで シーンに奥行きを出します
負の視差を持つオブジェクトは 画像の前側にあるように見えます 視差のないオブジェクトは 2つの画像が重なり合っていることを意味し 2D画像と同じように 画像平面にあるように見えます 正の視差を持つオブジェクトは 画像平面の後ろ側にあるように見えます
これは正面から見た時に ステレオスコピックがどのように感じるかを 示したものです
実際には 横から見ると ウインドウから外に 何も出ていないように見えます これは単純にコンテンツが 長方形の上に表示されているからです
長方形の境界からオブジェクトが 飛び出てくるようにしたい場合は 新しいポータルクロッシングAPIなどの APIとRealityKitを使用して レンダリングします ポータルクロッシングの例については 「Discover RealityKit APIs for iOS, macOS and visionOS」をご覧ください
プレイヤーの頭部の位置を使用しない場合 横から見た時に シーンは投影されたように見えます 頭部の位置の使い方については 後ほど説明します これはステレオスコピックの仕組みを示した図です オブジェクトは2本の光線の 交点にあるように見えます 2つの画像間の視差によって 奥行きの見え方は変化します 視差が変化すると 交点の位置も変化します
このステレオスコピック画像について 画像が変化しなくても ウインドウのサイズや位置が変化すると 奥行きの見え方が変化することにも 注目してください
「Build compelling spatial photo and video experiences」ビデオで Vision Pro向けにステレオスコピック コンテンツを作成する方法をご覧ください
特に避けてもらいたい 主な状況の1つは コンテンツを無限遠を超えて レンダリングすることです コンテンツを見る時は 目が収斂するか平行であるべきです プレイヤーの両目の間の距離よりも 視差を大きくすると コンテンツが無限遠を超えてしまいます 光線が発散し 交点がなくなります 実際のコンテンツを見ている時は このようなことは決して起こらないため プレイヤーは非常に不快に感じます これを解決する1つの方法は 無限遠の平面に ステレオスコピック画像を表示することです 例えば この画像はウインドウ平面に レンダリングされています 一部のコンテンツは画像平面の 後ろ側にあるように見え 一部のコンテンツは 前側にあるように見えます 例えば空間写真のように ポータルを通じて コンテンツを無限遠の 画像平面にレンダリングすることにより 視差がゼロのコンテンツが 無限遠点に見えるようになります その他のすべてが 負の視差となり それより前側に表示されるようになります こうすることで すべてのコンテンツが無限遠点の 前側に表示されます
また ゲームの設定にスライダを追加する ことをお勧めします プレイヤーがステレオスコピックの強さを 快適なレベルに調整できるようにするためです スライダを実装するには 2つのバーチャルカメラの距離を変化させます ステレオスコピック画像を生成するには ゲームループを更新して 左右の目に対して レンダリングする必要があります iOS上のDeferred Lightingサンプルの ゲームループは このようになっています
このサンプルはゲームの状態と アニメーションを更新します 次に シャドウマップなどの オフスクリーンレンダリングを行ったら 画面にレンダリングします 最後にレンダリングの結果を表示します
ステレオスコピックでは 画面を複製して 左右の目に対して レンダリングする必要があります パフォーマンスを最大限に引き出すには Vertex Amplificationを使って 同じ描画関数の呼び出しで 左右の目に対してレンダリングします
デベロッパ向けドキュメントで Vertex Amplificationに 関する記事をご覧ください
例えば Deferred Lightingサンプルのコードを このように調整しました 最初にシャドウパスを 一度エンコードします 次に 各ビューについて記述し 適切なカメラ行列を設定します 最後に 該当するビューの色や 奥行きテクスチャに レンダリングコマンドをエンコードします
ステレオスコピックは 3D映画や空間写真のように シーンに奥行きを追加します ゲームをもっと別世界につながる 物理的な窓のように見せるために ヘッドトラッキングも追加できます これは ヘッドトラッキングを追加した Deferred Lightingサンプルです 頭部の動きに合わせてカメラが動きます
プレイヤーの頭部の位置を取得するには ImmersiveSpaceを開いて ARKitを使用します フレームごとにARKitから 頭部の位置を取得し それをレンダラに渡して カメラを操作します
コードはこのようになります 最初に ARKitをインポートします 次に ARKitSessionと WorldTrackingProviderを作成し フレームごとに ヘッド変換を問い合わせます
visionOSでは ウインドウとImmersiveSpacesは それぞれ独自の座標空間を持っています ARKitからのヘッド変換は ImmersiveSpaceの座標空間にあります それをウインドウで使うには 位置をウインドウの座標空間に変換します
コードで行うとこのようになります 頭部の位置をARKitから ImmersiveSpaceの座標空間に取得します
visionOS 2.0のこの新しいAPIを使って ImmersiveSpaceに対する ウインドウでのエンティティの transformMatrixを取得します
この逆行列を求めて 頭部の位置を ウインドウ空間に変換します 最後に このカメラ位置を レンダラに設定します
最良の結果を得るために 必ず頭部の位置の予測を行ってください レンダリングが実行され それが表示されるまでに 時間がかかるため その推定時間を使用して 頭部の位置を予測し レンダリングと最終的な 頭部の位置が できるだけ一致するようにします アプリの推定レンダリング時間を指定すると ARKitが頭部の位置の予測を行います サンプルでは 33ミリ秒を presentationTimeの推定値に使用しました これは90fpsで3フレームに相当します 物理的な窓からレンダリングされたように ゲームを見せるには 非対称の投影行列を 作成する必要もあります 固定の投影行列を使用すると ウインドウの形状に適合しません カメラの視円錐がウインドウを通過するように する必要があります 例えば ウインドウの左辺と右辺を 指すベクトルを使用して 投影行列を作成します このようにして投影行列を 作成する利点の1つは ウインドウに沿った 近クリッピング面を使用して オブジェクトがウインドウの左右の辺と 交差するのを防げることです
コードはこのようになります 最初に カメラの位置と ビューポートの3D境界から始めます カメラは-Zを向いており 指定された位置にあります ビューポートの各辺までの 距離を計算します 次に その距離を使って 非対称の投影行列を作成します これがヘッドトラッキングを使用して ゲームを別世界につながる 物理的な窓のように見せる方法です ステレオスコピックを使うと ゲームのイマーシブ感が高まりますが 2倍のフラグメントを レンダリングする必要があるため レンダリングコストも高くなります 可変ラスタ化レートを使用して ゲームのレンダリング効率を 向上させることで そのコストをいくらか相殺できます 可変ラスタ化レートとは Metalの機能の1つで 画面全体を 可変解像度でレンダリングできます
この機能を使用することで 周辺部分の解像度を下げ 中央部分の解像度を上げることができます ヘッドトラッキングを使用している場合は ピクセルが視野の中央にあるか それとも周辺にあるかわかるため ヘッド変換からVRRマップを作成できます 共有スペースを使用している場合 頭部の位置に アクセスすることはできませんが AdaptiveResolutionComponentを使って ゲームビューポートに広げた2Dグリッドに コンポーネントを配置することで VRRマップを作成できます
AdaptiveResolutionComponentは その3D位置において 1メートルの立方体が占めるおおよその 画面上のサイズをピクセル単位で返します 例えば この画面では値が1024ピクセルから 2048ピクセルへと変化しています
このビデオはカメラが離れたり 近づいたりする時に AdaptiveResolutionComponentの値が どのように変化するかを示しています
2Dグリッドから水平方向と垂直方向の VRRマップを抽出します よりスムーズな結果を得るために 各VRRマップを補間します 最終的に それらをMetalレンダラに渡します 最後に コンテンツVRRで レンダリングされたら VRRマップを反転させて ディスプレイに再マップする必要があります このように 可変ラスタ化レートを使用し カメラの変換に合わせて レンダリング解像度を調整することで ゲームのパフォーマンスを 向上させることができます
これらすべての機能強化により visionOSでは ゲームをさらに 面白くすることができます ちょうどWylde Flowersが visionOS向けに変身したのと同じです
このビデオでは 皆さんのiOSゲームを visionOSに展開する方法を見てきました フレームやImmersiveSpaceを追加して ゲームのイマーシブ感を 高める方法も紹介しました ステレオスコピックとヘッドトラッキングを Metalレンダラに追加して ゲームを別世界につながる窓のように 見せる方法も見ました また VRRを使ってパフォーマンスを 最適化する方法も見ました visionOSでiOSゲームを さらに魅力的にするために 役立てていただければ幸いです Vision Proで皆さんのゲームを プレイするのを楽しみにしています
-
-
5:44 - Render with Metal in a UIView
// Render with Metal in a UIView. class CAMetalLayerBackedView: UIView, CAMetalDisplayLinkDelegate { var displayLink: CAMetalDisplayLink! override class var layerClass : AnyClass { return CAMetalLayer.self } func setup(device: MTLDevice) { let displayLink = CAMetalDisplayLink(metalLayer: self.layer as! CAMetalLayer) displayLink.add(to: .current, forMode: .default) self.displayLink.delegate = self } func metalDisplayLink(_ link: CAMetalDisplayLink, needsUpdate update: CAMetalDisplayLink.Update) { let drawable = update.drawable renderFunction?(drawable) } }
-
6:20 - Render with Metal to a RealityKit LowLevelTexture
// Render Metal to a RealityKit LowLevelTexture. let lowLevelTexture = try! LowLevelTexture(descriptor: .init( pixelFormat: .rgba8Unorm, width: resolutionX, height: resolutionY, depth: 1, mipmapLevelCount: 1, textureUsage: [.renderTarget] )) let textureResource = try! TextureResource( from: lowLevelTexture ) // assign textureResource to a material let commandBuffer: MTLCommandBuffer = queue.makeCommandBuffer()! let mtlTexture: MTLTexture = texture.replace(using: commandBuffer) // Draw into the mtlTexture
-
7:06 - Metal viewport with a 3D RealityKit frame around it
// Metal viewport with a 3D RealityKit frame // around it. struct ContentView: View { @State var game = Game() var body: some View { ZStack { CAMetalLayerView { drawable in game.render(drawable) } RealityView { content in content.add(try! await Entity(named: "Frame")) }.frame(depth: 0) } } }
-
7:45 - Windowed game with an immersive background
// Windowed game with an immersive background @main struct TestApp: App { @State private var appModel = AppModel() var body: some Scene { WindowGroup { // Metal render ContentView(appModel) } ImmersiveSpace(id: "ImmersiveSpace") { // RealityKit background ImmersiveView(appModel) }.immersionStyle(selection: .constant(.progressive), in: .progressive) } }
-
13:11 - Render to multiple views for stereoscopy
// Render to multiple views for stereoscopy. override func draw(provider: DrawableProviding) { encodeShadowMapPass() for viewIndex in 0..<provider.viewCount { scene.update(viewMatrix: provider.viewMatrix(viewIndex: viewIndex), projectionMatrix: provider.projectionMatrix(viewIndex: viewIndex)) var commandBuffer = beginDrawableCommands() if let color = provider.colorTexture(viewIndex: viewIndex, for: commandBuffer), let depthStencil = provider.depthStencilTexture(viewIndex: viewIndex, for: commandBuffer) { encodePass(into: commandBuffer, color: color, depth: depth) } endFrame(commandBuffer) } }
-
13:55 - Query the head position from ARKit every frame
// Query the head position from ARKit every frame. import ARKit let arSession = ARKitSession() let worldTracking = WorldTrackingProvider() try await arSession.run([worldTracking]) // Every frame guard let deviceAnchor = worldTracking.queryDeviceAnchor( atTimestamp: CACurrentMediaTime() + presentationTime ) else { return } let transform: simd_float4x4 = deviceAnchor .originFromAnchorTransform
-
14:22 - Convert the head position from the ImmersiveSpace to a window
// Convert the head position from the ImmersiveSpace to a window. let headPositionInImmersiveSpace: SIMD3<Float> = deviceAnchor .originFromAnchorTransform .position let windowInImmersiveSpace: float4x4 = windowEntity .transformMatrix(relativeTo: .immersiveSpace) let headPositionInWindow: SIMD3<Float> = windowInImmersiveSpace .inverse .transform(headPositionInImmersiveSpace) renderer.setCameraPosition(headPositionInWindow)
-
15:05 - Query the head position from ARKit every frame
// Query the head position from ARKit every frame. import ARKit let arSession = ARKitSession() let worldTracking = WorldTrackingProvider() try await arSession.run([worldTracking]) // Every frame guard let deviceAnchor = worldTracking.queryDeviceAnchor( atTimestamp: CACurrentMediaTime() + presentationTime ) else { return } let transform: simd_float4x4 = deviceAnchor .originFromAnchorTransform
-
15:47 - Build the camera and projection matrices
// Build the camera and projection matrices. let cameraPosition: SIMD3<Float> let viewportBounds: BoundingBox // Camera facing -Z let cameraTransform = simd_float4x4(AffineTransform3D(translation: Size3D(cameraPosition))) let zNear: Float = viewportBounds.max.z - cameraPosition.z let l /* left */: Float = viewportBounds.min.x - cameraPosition.x let r /* right */: Float = viewportBounds.max.x - cameraPosition.x let b /* bottom */: Float = viewportBounds.min.y - cameraPosition.y let t /* top */: Float = viewportBounds.max.y - cameraPosition.y let cameraProjection = simd_float4x4(rows: [ [2*zNear/(r-l), 0, (r+l)/(r-l), 0], [ 0, 2*zNear/(t-b), (t+b)/(t-b), 0], [ 0, 0, 1, -zNear], [ 0, 0, 1, 0] ])
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。