- 
							
							RealityKitによる空間描画アプリの構築空間描画アプリの構築プロセスで、RealityKitを使いこなしましょう。RealityKitとARKitおよびSwiftUIの統合により生まれる、印象的で魅力的な空間体験の構築について、RealityKitにおいてリソースが動作する仕組みや、ユーザーのブラシの線の更新を高速化する低レベルのメッシュおよびテクスチャのAPIなどの機能を使用する方法を解説します。 関連する章- 0:00 - Introduction
- 2:43 - Set up spatial tracking
- 5:42 - Build a spatial user interface
- 13:57 - Generate brush geometry
- 26:11 - Create a splash screen
 リソース関連ビデオWWDC24- iOS、macOS、visionOS向けRealityKit APIの紹介
- Reality Composer Proにおけるインタラクティブな3Dコンテンツの作成
- RealityKitオーディオで空間コンピューティングアプリの質を向上
- visionOSにおけるカスタムホバーエフェクトの作成
 WWDC23
- 
							このビデオを検索こんにちは Adrianです RealityKitチームのエンジニアです このセッションでは RealityKitの新機能を使用して visionOS用の空間描画アプリを 構築するプロセスについて説明します RealityKitは 高性能な 3Dシミュレーションとレンダリング機能を iOS macOS visionOSに提供する フレームワークです visionOSでは RealityKitは アプリの空間機能の基盤となります Apple Vision Proの発表以来 デベロッパの皆さんから 有益なフィードバックを多数受け取っており プラットフォームの機能を進化させながら そのフィードバックに対応するために 懸命に取り組んできました 本日は RealityKitで作成できるアプリの 限界を押し上げる 新しいAPIを紹介します 詳細を確認しましょう Appleの空間描画アプリは RealityKitのパワフルな3Dの機能を SwiftUIおよびARKitと統合し 優れたユーザー体験を実現します カスタマイズされたメッシュやテクスチャ シェーダを構築して 洗練されたビジュアルデザインを 実現するのは 楽しい作業です アプリを起動すると 目を引くスプラッシュ画面が表示されます 簡単な設定プロセスの後 作成の準備が整います 空中で指をつまむだけで すぐに描画を開始できます ブラシストロークの外観は パレットビューで変更できます このアプリは チューブのようなソリッドブラシタイプと きらめくスパークルブラシを サポートしています ブラシストロークはカスタマイズ可能で ストロークの色や太さを変更できます 他にもいろいろあります このアプリを一緒に構築していきましょう それでは始めましょう まず 空間トラッキングを設定して アプリが手と環境のデータを 認識できるようにします 次にUIを作成して ブラシとキャンバスを 制御できるようにします また パワフルな新機能を使って ホバー時のUIの外観をカスタマイズします RealityKitでのメッシュの 仕組みを詳しく確認し アプリで新しいRealityKit APIを使用して Metalでブラシジオメトリを 効率的に生成する方法を紹介します 動的なテクスチャと空間UI要素を使用して 魅力的なスプラッシュ画面を作成し アプリに最後の仕上げを施します 描画アプリには つまんで手を動かして描くときの 手のポーズを認識させる必要があります そのためには ハンドアンカーの 空間トラッキングを設定します visionOSでは アプリは SwiftUIまたはRealityKitコンテンツを ウインドウ ボリューム スペースに 配置できます アプリでイマーシブ空間を使用すると アンカーを使用して空間トラッキング情報を 受け取ることができます これには ワールドアンカーと プレーンアンカーを使用した シーンの理解に関する情報と ハンドアンカーを使用した ポーズに関する情報が含まれます visionOS 1.0では ARKitを使って このデータにアクセスできました visionOS 2.0ではRealityKitアプリで 空間トラッキングを使用するための より簡単な方法を導入しています このAPIを使って 描画アプリで空間ハンドトラッキングを設定しましょう RealityKitでは AnchorEntityを使って RealityKitエンティティを ARアンカーに固定できます この描画アプリでは それぞれの手に 2つのAnchorEntityが作成されます 1つは親指の先端に固定され もう1つは人差し指の先端に固定されます 空間トラッキングデータにアクセスするには アプリはユーザーからの許可を必要とします この描画アプリで ユーザーがをタップすると 関連する承認が要求されます 重要なのは ユーザーに許可を求めるタイミングです 承認はアプリが必要なときにのみ要求します ここでは ユーザーが 描画を開始するときです RealityKitでトラッキングデータの 承認を要求するには SpatialTrackingSessionを使用します これはvisionOS 2.0の新しいAPIです アプリに必要な トラッキング機能を宣言します この場合 アプリには手のデータが必要です 次にSpatialTrackingSessionで runを呼び出します この時点で このトラッキングを 承認するためのアラートが表示されます run関数は未承認のトラッキング機能の リストを返します このリストをチェックして 許可の有無を確認できます 空間トラッキングが承認された場合は AnchorEntityのtransformを介して トラッキングデータにアクセスできます 許可が拒否された場合 AnchorEntityのtransformは 更新されません ただし AnchorEntityは ポーズを視覚的に更新します 要点をまとめましょう ImmersiveSpaceを アプリで使用すると RealityKitコンテンツを 現実世界に固定できます AnchorEntityを使用すると RealityKitコンテンツで アンカーを設定できます そして今年からは アプリでAnchorEntityの transformにアクセスする必要がある場合 SpatialTrackingSessionを 使用できます さらにSpatialTrackingSessionを 使用すると AnchorEntitiesは RealityKitの物理システムと対話できます 次は このアプリのユーザー インターフェイスについて説明します スプラッシュ画面でをタップすると イマーシブ空間に移動し 描画キャンバスが表示されます キャンバスのサイズや位置は 球形のハンドルをドラッグすることで 変更できます 描画を開始する準備ができたら パレットビューが表示されます ここでブラシの形状と色を設定できます 描画の準備ができたら キャンバス内に足を踏み入れるだけで 開始できます このインターフェイスの構築方法を 詳しく見ていきましょう まず キャンバス配置インターフェイスから 始めます このインターフェイスを使用すると 描画領域を定義できます キャンバス配置中に イマーシブ空間を構成する要素は 2つあります 床には3D形状でキャンバスの端が描かれ ハンドルを使用すると キャンバスの位置を変更できます まず境界メッシュについて考えてみましょう このメッシュはリアルタイムで生成されます 境界のサイズはスライダをドラッグして 変更できるためです メッシュは2つの円で定義されます 左の図に示すように 外側の円が緑色 内側の円が赤色になります この形状をSwiftUIパスとして定義できます 円は360度広がる円弧です そのため半径が異なる円弧を2つ作成します 次に 正規化された偶奇の 塗りつぶしモードを指定して 作成する形状を定義しています RealityKitでメッシュを生成するには 今年の新しいAPIを使用できます MeshResource extrudingです MeshResource extrudingは パワフルなAPIで 2Dベクトルコンテンツを 3Dモデルに変換できます 必要なのは シェイプの奥行と 解像度を指定することだけです 覚えておくべき重要な考慮事項が 1つあります visionOSでは RealityKitは フォビエーションされたレンダラを使用します 周辺視野内の画像の領域は 低解像度でレンダリングされます これにより アプリのパフォーマンスが最適化されます シーンに高コントラストの 薄いジオメトリが含まれていると アーティファクトのちらつきに 気づくかもしれません この例では リングが薄すぎます このような薄いジオメトリック要素は 避けてください 高コントラストの領域では 特に注意が必要です この問題に対処するには ジオメトリの厚みを増やし 高コントラストの薄いエッジを削除します 左側ではアーチファクトのちらつきが 軽減されています 空間コンテンツの エイリアシングについて詳しくは WWDC23の「Explore rendering for spatial computing」 をご覧ください 次にキャンバスハンドルについて説明します 1つ指摘しておきたいことが あります このハンドルを見つめると 青いハイライト効果が生じます visionOSでは HoverEffectComponentは ユーザーがRealityKitのコンテンツを 見つめたときに視覚効果を追加します visionOS 1.0では HoverEffectComponentは デフォルトの スポットライト効果を使用します 今年はさらに2種類のホバー効果を HoverEffectComponentに導入します ハイライト効果はエンティティに 均一なハイライト色を適用します HoverEffectComponentを ShaderGraphシェーダと 併用できるようになりました シェーダベースのホバー効果は 非常に柔軟性が高いため ホバー時のエンティティの外観を 正確に制御できます ハンドルの青いハイライトを実現できるのは ハイライトホバー効果のおかげです ハイライト効果を使用するには ドットハイライトを使用して HoverEffectComponentを初期化し ハイライト色を指定します 強度の値を変更して ハイライトを より鮮やかにすることもできます キャンバス配置のUI要素が 背後の環境の上で 光っているように見えることに 気付いたかもしれません これは加算ブレンドモードで 設定されているためです 今年 RealityKitは UnlitMaterialや PhysicallyBasedMaterialなどの 組み込みマテリアルで 加算ブレンドモードを 新たにサポートするようになりました これを使うには まずProgramを作成し ブレンドモードをaddに設定します ユーザーが描画キャンバスを選択したら いよいよメインイベントです パレットビューが表示され ユーザーはブラシの設定を開始できます パレットビューはSwiftUIで構築されており ブラシのタイプとスタイルを カスタマイズできます パレットの下部には 選択できる プリセットブラシのセットがあります ブラシプリセットビューに 特に注目したいと思います 各ブラシプリセットサムネールは 実際に完全な3次元形状であることに 注意してください このメッシュは実際のブラシストロークと 同じ方法で生成されます SwiftUIとRealityKitは シームレスに統合されます ここでは各サムネールに RealityViewを使用します これにより RealityKitの すべての機能を活用できます ブラシプリセットを見つめると 目を引くホバー効果がアクティブになり ブラシに沿って紫色の光が流れます これは先ほど説明した シェーダベースのホバー効果です この効果をどのように実現したかを 詳しく見ていきましょう シェーダベースのホバー効果は シェーダグラフの Hover Stateノードで実現します このノードはシェーダに ホバー効果を統合するための 便利なツールを提供します 例えば Intensityはシステムが提供する値で 視線の状態に基づいて 0から1の間でアニメーション化されます Intensity値を使用すると キャンバスハンドルで先ほど説明したような ハイライト効果を再現できます ただしプリセットビューでは もっと高度な効果を作りたいと思っています グロー効果は ブラシストロークの最初から最後まで ブラシメッシュに沿って 流れる必要があります この複雑な効果を実現するために シェーダグラフマテリアルを使います シェーダグラフを一緒に見ていきましょう Hover Stateノードの Time Since Hover Start プロパティを使用します これはホバーイベントが始まってからの 秒単位での値です これを使って グローハイライトの 曲線に沿った位置を定義します ホバーイベントが始まると グローの位置が曲線に沿って移動し始めます ブラシストロークのメッシュを生成する際 アプリはCurveDistanceという 属性を提供します アプリはUV1チャネルを介して 各頂点のCurveDistance値を提供します これは ブラシストロークの曲線距離を 視覚化したものです この値はストロークの長さに応じて 増加します シェーダはグローハイライトの位置を 曲線距離と比較します こうすることでシェーダは 現在のジオメトリに対する グローの位置を把握できます 次に グロー効果のサイズを定義します 現在のジオメトリは グロー位置の範囲内にあるときに光ります これでイージング曲線を追加できます これはグローがジオメトリ上を 移動するときの ホバー効果のIntensityを定義します 最後の手順は 計算したIntensity値に応じて ホバー効果の色と 元のブラシストロークの色を 混合することです これは良くできています シェーダベースのホバー効果を使用するには ますシェーダ設定を使用して HoverEffectComponentを作成します 次にShaderGraphMaterialを使用します これがHover Stateノードの 更新を受け取ります ユーザーがブラシを設定する方法を 構築したので 次はアプリのコアについて説明します 各ブラシストロークの ジオメトリを生成します 大まかに言うと メッシュは頂点と それらを接続する三角形などの プリミティブの集合です 各頂点は その頂点の位置やテクスチャ座標など 様々な属性に関連付けられています これらの属性はデータによって記述されます 例えば 各頂点の位置は3次元ベクトルです 頂点データはGPUに送信できるように バッファに整理する必要があります ほとんどのRealityKitメッシュでは データはメモリ内で連続して整理されます したがってメモリ内では 頂点位置0の次に頂点位置1が続き その次に頂点2というように続きます 他のすべての頂点属性についても同様です インデックスバッファは別々に配置され これにはメッシュ内の各三角形の 頂点インデックスが含まれます RealityKitの標準メッシュレイアウトは 汎用性が高く 様々なユースケースに対応します ただし場合によっては ドメイン固有のアプローチの方が効率的です 描画アプリはカスタムビルドの ジオメトリ処理パイプラインを使用して ユーザーのブラシストロークの メッシュを作成します 例えば 各ブラシストロークを滑らかにして メッシュの曲率を改善します このアルゴリズムは最適化されているため ブラシストロークの末尾に 点を追加する処理が 可能な限り高速になります レイテンシを最小限に抑えることが重要です ブラシストロークメッシュでは 頂点のレイアウトに 1つのバッファが使用されます ただし標準的な メッシュレイアウトとは異なり 各頂点はその個々の全体が 順に記述されます そのため属性はインターリーブされます 最初の頂点の位置の次に その頂点の法線が続き 次に従接線が続くというように すべての属性が記述されるまで続きます その後で初めてバッファは 2番目以降の頂点の記述を開始します これとは対照的に 標準の頂点バッファは 各属性のすべてのデータを 連続してレイアウトします ブラシ頂点バッファのレイアウトは 描画アプリに特に便利です ブラシストロークを生成するとき アプリは常に頂点バッファの末尾に 頂点を追加します ブラシ頂点バッファは 古いデータの位置を変更せずに 新しい頂点を追加できることに 注意してください ただし標準の頂点バッファでこれを行うと バッファの増大に伴い ほとんどのデータを移動する必要があります ブラシ頂点には 標準レイアウトで表示されるものとは 異なる属性もあります 位置 法線 従接線などの 一部の属性は標準です 色 マテリアルプロパティ 曲線距離などはカスタム属性です アプリのコードでは ブラシの頂点は Metal Shading Languageの この構造体として表されます 構造体の各エントリは 頂点の属性に対応します そこで問題に直面します 一方では 高性能ジオメトリエンジンの 頂点レイアウトを保持し 不要な変換やコピーを 避けたいと考えています しかし一方で ジオメトリエンジンのレイアウトは RealityKitの標準レイアウトと 互換性がありません 必要なのは GPUバッファを そのままRealityKitに取り込み RealityKitにその読み取り方法を 指示する方法です そして今 LowLevelMeshという 新しいAPIでそれが可能になりました LowLevelMeshを使うと 様々な方法で頂点データを配置できます 頂点データには 4つの異なる Metalバッファを使用できます そのためRealityKitの標準レイアウトに似た レイアウトを使うことができます ただしバッファを複数用意すると 便利な場合もあります 例えば テクスチャ座標を 他の属性よりも頻繁に 更新する必要があるとします その場合 この動的データを 独自のバッファに移動する方が効率的です 頂点バッファをインターリーブするように 並べ替えることができます インターリーブと非インターリーブの 組み合わせも可能です また 三角形ストリップなどの Metalプリミティブタイプも使用できます LowLevelMeshと そのカスタムバッファレイアウトが アプリにどのようなメリットをもたらすか 考えてみてください メッシュデータは 独自のカスタムレイアウトを持つ バイナリファイルから 取得されているかもしれません これでそのデータを 変換のオーバーヘッドなしで 直接RealityKitに転送できます またはデジタルコンテンツ作成ツールや CADアプリケーションで見られるような バッファレイアウトが事前定義された 既存のメッシュ処理パイプラインを RealityKitにブリッジする場合もあります LowLevelMeshは ゲームエンジンから RealityKitにメッシュデータを効率的に ブリッジする方法としても使用できます LowLevelMeshにより メッシュデータをRealityKitに 提供する方法の可能性が広がります 皆さんのアプリで何が実現できるか 楽しみにしています では コードでLowLevelMeshを 作成する方法について見ていきましょう これでアプリは 余分な変換や不要なコピーなしで 頂点バッファをそのまま LowLevelMeshに提供できます LowLevelMesh属性を使用して 頂点のレイアウト方法を記述します 迅速な拡張機能で属性リストを SolidBrushVertex構造体に設定します まず位置の属性を宣言します 詳しく見ていきましょう 最初のステップはセマンティクスの定義です これはLowLevelMeshに 属性の解釈方法を指示します この場合 属性は位置なので そのセマンティクスを使います 次に この属性の Metal頂点形式を定義します この場合 SolidBrushVertexの 定義と一致するように float3を選択する必要があります 次に 属性のオフセットを バイト単位で指定します 最後にレイアウトの インデックスを指定します 頂点レイアウトのリストに インデックスを付けます これについては後で説明します 描画アプリは単一のレイアウトのみを 使うため インデックス0を使用します 次にその他のメッシュ属性を宣言します 法線属性と従接線属性は 異なるメモリオフセットと セマンティクスが使用されることを除いて 位置に似ています 色属性には半精度浮動小数点値を使用します 今年は任意のMetal頂点形式を LowLevelMeshで使用できます これには圧縮された頂点形式が含まれます 他の2つのパラメータには セマンティクスUV1とUV3を使用します また今年新たに 最大8つのUVチャネルをLowLevelMesh で使用できるようになりました シェーダグラフのマテリアルは これらの値にアクセスできます これでLowLevelMeshオブジェクト 自体を作成できます これを行うには LowLevelMesh記述子を作成します LowLevelMesh記述子は 概念的には Metalの MTLVertexDescriptorに似ていますが RealityKitがメッシュを取り込むために 必要な情報も含まれています まず頂点とインデックスバッファに 必要な容量を宣言します 次に頂点属性のリストを渡します これは前のスライドでまとめたリストです 次に 頂点レイアウトのリストを作成します 各頂点属性はレイアウトの1つを使用します LowLevelMeshは頂点データ用に 最大4つのMetalバッファを提供します バッファインデックスは どのバッファを使用するかを宣言します 次に バッファオフセットと 各頂点のストライドを指定します ほとんどの場合 ここで行ったように 1つのバッファのみを使用します これでLowLevelMeshを初期化できます 最後のステップは パーツのリストを入力することです 各パーツはインデックスバッファの 領域にまたがります 各メッシュパーツに 異なるRealityKit マテリアルインデックスを 割り当てることができます またここでは メモリ効率を向上させるため 三角形のストリップトポロジを使用します 最後に LowLevelMeshから MeshResourceを作成し それをエンティティの ModelComponentに割り当てます LowLevelMeshの頂点データを 更新するときは withUnsafeMutableBytes APIを使用できます このAPIを使用すると 実際のバッファにアクセスできます さらに GPUに渡されて レンダリングされます そのためメッシュデータを更新する際の オーバーヘッドは最小限です 例えば メッシュのメモリレイアウトが 事前にわかっているため bindMemoryを使用して 提供された生のポインタを バッファポインタに変換できます インデックスバッファデータについても 同様です LowLevelMeshインデックスバッファは withUnsafeMutableIndicesを使用して 更新できます LowLevelMeshが アプリのメッシュ処理パイプラインを 高速化する強力なツールであることは 既に説明しました LowLevelMeshを使用すると 頂点またはインデックスバッファの更新を GPU演算で バックアップすることもできます 例を見てみましょう これは描画アプリのスパークルブラシです ブラシストロークに追従する パーティクルフィールドを生成します このパーティクルフィールドは フレームごとに動的に更新されるため ソリッドブラシの場合とは異なる 更新スキームを使用します メッシュ更新の頻度と複雑さを考えると GPUを使うのは理にかなっています 詳しく見ていきましょう スパークルブラシには 位置や色などのパーティクルごとの 属性のリストが含まれています 前と同様に curveDistanceパラメータと パーティクルのサイズも含まれています GPUパーティクルシミュレーションでは SparkleBrushParticle型を使用して 各パーティクルの属性と速度を追跡します アプリはシミュレーションに SparkleBrushParticlesの 補助バッファを使用します SparkleBrushVertex構造体は メッシュの頂点データに使用されます これには各頂点のUV座標が含まれており シェーダは3D空間でパーティクルを 方向付ける方法を理解できます パーティクルごとに 4つの頂点を持つ平面が作成されます スパークルブラシメッシュの更新用に 2つのバッファを維持する必要があります SparkleBrushParticleで埋められた パーティクルシミュレーションバッファと SparkleBrushVerticesを含む LowLevelMesh頂点バッファです ソリッドブラシと同様に 頂点バッファの仕様を LowLevelMesh属性のリストと 共に提供します 属性のリストは SparkleBrushVertexの メンバーに対応します GPUでLowLevelMeshを 設定するときは Metalコマンドバッファと 演算コマンドエンコーダを使用します バッファが処理を終えると RealityKitは自動的に変更を適用します コードでは次のようになります 前に述べたように ここではパーティクルシミュレーションに Metalバッファを使用し 頂点バッファにLowLevelMeshを使用します Metalコマンドバッファと 演算コマンドエンコーダを設定します これでアプリは GPU演算カーネルを実行して メッシュを構築できるようになります LowLevelMeshでreplaceを呼び出して コマンドバッファを提供します Metalバッファが返されます この頂点バッファはRealityKitが レンダリングのために直接使用します シミュレーションをGPUに ディスパッチした後 コマンドバッファをコミットします コマンドバッファの処理が完了すると RealityKitは更新された頂点データの使用を 自動的に開始します 高速で応答性の高い ブラシストローク生成により アプリの見た目が向上します では魅力的なスプラッシュ画面で アプリの最後の仕上げをしましょう スプラッシュ画面は ユーザーをアプリの世界へ迎え入れる 最適な方法です 楽しみながらアプリのビジュアルスタイルを 披露する機会にもなります アプリのスプラッシュ画面には 4つの視覚的要素があります ロゴタイプには 2つの異なるフォントを使用した 「RealityKit Drawing App」という 3Dテキストが含まれています ロゴマークも3D形状です 下部には ユーザーに描画の開始を促す ボタンがあります そして背景には ユーザーの環境で光る 印象的なグラフィックがあります ロゴタイプの作成から始めましょう まずデフォルトのシステムフォントで 「RealityKit」のAttributed Stringを 作成します 今年新たに RealityKitのMeshResourceを MeshResource extrudingを使用して AttributedStringから 作れるようになりました AttributedStringを使用しているので 異なるプロパティを持つテキスト行を 簡単に追加できます 「Drawing App」というテキストを 別のフォントで サイズを大きくして描きましょう 次に段落スタイルを使用して テキストを中央揃えにします AttributedStringを使用して テキストのスタイルを 設定する方法については WWDC21の 「What’s new in Foundation」をご覧ください これまでに作成したテキストを 拡大してみましょう 現時点では3Dモデルが 少し平坦に見えるので カスタマイズしましょう これを行うには ShapeExtrusionOptions構造体を MeshResource extrudingに渡します より厚い3D形状を作成するために まず奥行きを大きくします 次に メッシュに 2つ目のマテリアルを追加します 前面 背面 側面に割り当てる マテリアルインデックスを指定できます 最後に テキストを正面から見たとき アウトラインマテリアルがより目立つように 微細な面取りを施します ここでは面取り半径を 0.1ポイントに指定します このアプリでは MeshResource extrudingを使用して ロゴマークも生成します SwiftUIパスを使用するので 形状の定義方法には 非常に柔軟性があります ロゴマークは 一連のベジエ曲線として設定されます SwiftUIパスの詳細については SwiftUIチュートリアル 「Drawing paths and shapes」 をご覧ください 次にスプラッシュ画面の 背景について説明します これはアプリの中でも 最も印象的な美的要素の1つです これを構築するために LowLevelTextureという 新しいAPIを使いました LowLevelTextureは LowLevelMeshと同じ 高速リソース更新セマンティクス を提供しますが テクスチャアセット用です スプラッシュ画面では LowLevelTextureを使用して ピル型シェイプが連なる 一種の形状記述を生成します この形状記述は テクスチャの赤チャネルに保存されます 各ピルの内部には暗い領域があり ピルの外側は明るい領域です テクスチャの緑チャネルには スプラッシュ画面の ビネットの記述が保存されます このテクスチャは Reality Composer Proの シェーダグラフシェーダを介して 最終画像に解釈されます LowLevelTextureは そのDescriptorから作成します LowLevelTexture記述子は Metalの MTLTextureDescriptorに相当します LowLevelMeshと同様に LowLevelTextureは ピクセル形式とテクスチャの使用を 詳細に制御します そしてRealityKitで圧縮ピクセル形式を 使用できるようになりました このスプラッシュ画面で必要なのは 赤と緑のチャネルだけなので ピクセル形式RG16Floatを使用します 記述子から LowLevelTextureを初期化できます 次に LowLevelTextureから RealityKitテクスチャリソースを作成します これでこのテクスチャを マテリアルで使用する準備ができました LowLevelMeshと同じように GPUでLowLevelTextureを更新します まず Metalコマンドバッファと 演算コマンドエンコーダを設定します 次にコマンドバッファを使って LowLevelTexture.replaceを 呼び出します 演算シェーダで書き込むことができる Metalテクスチャが返されます 最後に GPU演算をディスパッチし コマンドバッファをコミットします コマンドバッファの処理が完了すると Metalテクスチャが自動的に RealityKitに表示されます こうして完成したスプラッシュ画面の 見た目に非常に満足しています 目を引く背景とパーソナライズされた 3Dジオメトリを組み合わせることで 非常に独特な外観になっています このアプリに 最適な仕上げを施すことができました 以上で終わりです 今日は RealityKitでインタラクティブな 空間描画アプリを構築しました RealityKit 空間トラッキングAPIを使用して ユーザーが空間のどこに描画するかを アプリで検出できるようにしました SwiftUIと高度なホバー効果を使用して ブラシとスタイルをカスタマイズするための インタラクティブな空間UIを構築しました RealityKitでのリソース更新の仕組みを学び 高度な低レベルAPIを使用して メッシュとテクスチャを インタラクティブに生成しました 最後に新しいAPIを使って 空間体験向けに 2Dベクトルグラフィックスを インポートしました 今年のRealityKitの新機能の 詳細については 「Discover RealityKit APIs for iOS, macOS and visionOS」と 「Enhance your spatial computing app with RealityKit audio」をどうぞ 皆さんの成果に期待しています WWDC24の他のセッションも お楽しみください 
- 
							- 
										
										4:18 - Using SpatialTrackingSession // Retain the SpatialTrackingSession while your app needs access let session = SpatialTrackingSession() // Declare needed tracking capabilities let configuration = SpatialTrackingSession.Configuration(tracking: [.hand]) // Request authorization for spatial tracking let unapprovedCapabilities = await session.run(configuration) if let unapprovedCapabilities, unapprovedCapabilities.anchor.contains(.hand) { // User has rejected hand data for your app. // AnchorEntities will continue to remain anchored and update visually // However, AnchorEntity.transform will not receive updates } else { // User has approved hand data for your app. // AnchorEntity.transform will report hand anchor pose }
- 
										
										7:07 - Use MeshResource extrusion // Use MeshResource(extruding:) to generate the canvas edge let path = SwiftUI.Path { path in // Generate two concentric circles as a SwiftUI.Path path.addArc(center: .zero, radius: outerRadius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true) path.addArc(center: .zero, radius: innerRadius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true) }.normalized(eoFill: true) var options = MeshResource.ShapeExtrusionOptions() options.boundaryResolution = .uniformSegmentsPerSpan(segmentCount: 64) options.extrusionMethod = .linear(depth: extrusionDepth) return try MeshResource(extruding: path, extrusionOptions: extrusionOptions)
- 
										
										9:33 - Highlight HoverEffectComponent // Use HoverEffectComponent with .highlight let placementEntity: Entity = // ... let hover = HoverEffectComponent( .highlight(.init( color: UIColor(/* ... */), strength: 5.0) ) ) placementEntity.components.set(hover)
- 
										
										9:54 - Using Blend Modes // Create an UnlitMaterial with Additive Blend Mode var descriptor = UnlitMaterial.Program.Descriptor() descriptor.blendMode = .add let prog = await UnlitMaterial.Program(descriptor: descriptor) var material = UnlitMaterial(program: prog) material.color = UnlitMaterial.BaseColor(tint: UIColor(/* ... */))
- 
										
										13:45 - Shader based hover effects // Use shader-based hover effects let hoverEffectComponent = HoverEffectComponent(.shader(.default)) entity.components.set(hoverEffectComponent) let material = try await ShaderGraphMaterial(named: "/Root/SolidPresetBrushMaterial", from: "PresetBrushMaterial", in: realityKitContentBundle) entity.components.set(ModelComponent(mesh: /* ... */, materials: [material]))
- 
										
										16:56 - Defining a vertex buffer struct for the solid brush struct SolidBrushVertex { packed_float3 position; packed_float3 normal; packed_float3 bitangent; packed_float2 materialProperties; float curveDistance; packed_half3 color; };
- 
										
										19:27 - Defining LowLevelMesh Attributes for solid brush extension SolidBrushVertex { static var vertexAttributes: [LowLevelMesh.Attribute] { typealias Attribute = LowLevelMesh.Attribute return [ Attribute(semantic: .position, format: MTLVertexFormat.float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.position)!), Attribute(semantic: .normal, format: MTLVertexFormat.float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.normal)!), Attribute(semantic: .bitangent, format: MTLVertexFormat.float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.bitangent)!), Attribute(semantic: .color, format: MTLVertexFormat.half3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.color)!), Attribute(semantic: .uv1, format: MTLVertexFormat.float, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.curveDistance)!), Attribute(semantic: .uv3, format: MTLVertexFormat.float2, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.materialProperties)!) ] } }
- 
										
										21:14 - Make LowLevelMesh private static func makeLowLevelMesh(vertexBufferSize: Int, indexBufferSize: Int, meshBounds: BoundingBox) throws -> LowLevelMesh { var descriptor = LowLevelMesh.Descriptor() // Similar to MTLVertexDescriptor descriptor.vertexCapacity = vertexBufferSize descriptor.indexCapacity = indexBufferSize descriptor.vertexAttributes = SolidBrushVertex.vertexAttributes let stride = MemoryLayout<SolidBrushVertex>.stride descriptor.vertexLayouts = [LowLevelMesh.Layout(bufferIndex: 0, bufferOffset: 0, bufferStride: stride)] let mesh = try LowLevelMesh(descriptor: descriptor) mesh.parts.append(LowLevelMesh.Part(indexOffset: 0, indexCount: indexBufferSize, topology: .triangleStrip, materialIndex: 0, bounds: meshBounds)) return mesh }
- 
										
										22:28 - Creating a MeshResource let mesh: LowLevelMesh let resource = try MeshResource(from: mesh) entity.components[ModelComponent.self] = ModelComponent(mesh: resource, materials: [...])
- 
										
										22:37 - Updating vertex data of LowLevelMesh using withUnsafeMutableBytes API let mesh: LowLevelMesh mesh.withUnsafeMutableBytes(bufferIndex: 0) { buffer in let vertices: UnsafeMutableBufferPointer<SolidBrushVertex> = buffer.bindMemory(to: SolidBrushVertex.self) // Write to vertex buffer `vertices` }
- 
										
										23:07 - Updating LowLevelMesh index buffers using withUnsafeMutableBytes API let mesh: LowLevelMesh mesh.withUnsafeMutableIndices { buffer in let indices: UnsafeMutableBufferPointer<UInt32> = buffer.bindMemory(to: UInt32.self) // Write to index buffer `indices` }
- 
										
										23:58 - Creating a particle brush using LowLevelMesh struct SparkleBrushAttributes { packed_float3 position; packed_half3 color; float curveDistance; float size; }; // Describes a particle in the simulation struct SparkleBrushParticle { struct SparkleBrushAttributes attributes; packed_float3 velocity; }; // One quad (4 vertices) is created per particle struct SparkleBrushVertex { struct SparkleBrushAttributes attributes; simd_half2 uv; };
- 
										
										24:58 - Defining LowLevelMesh Attributes for sparkle brush extension SparkleBrushVertex { static var vertexAttributes: [LowLevelMesh.Attribute] { typealias Attribute = LowLevelMesh.Attribute return [ Attribute(semantic: .position, format: .float3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.position)!), Attribute(semantic: .color, format: .half3, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.color)!), Attribute(semantic: .uv0, format: .half2, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.uv)!), Attribute(semantic: .uv1, format: .float, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.curveDistance)!), Attribute(semantic: .uv2, format: .float, layoutIndex: 0, offset: MemoryLayout.offset(of: \Self.attributes.size)!) ] } }
- 
										
										25:28 - Populate LowLevelMesh on GPU let inputParticleBuffer: MTLBuffer let lowLevelMesh: LowLevelMesh let commandBuffer: MTLCommandBuffer let encoder: MTLComputeCommandEncoder let populatePipeline: MTLComputePipelineState commandBuffer.enqueue() encoder.setComputePipelineState(populatePipeline) let vertexBuffer: MTLBuffer = lowLevelMesh.replace(bufferIndex: 0, using: commandBuffer) encoder.setBuffer(inputParticleBuffer, offset: 0, index: 0) encoder.setBuffer(vertexBuffer, offset: 0, index: 1) encoder.dispatchThreadgroups(/* ... */) // ... encoder.endEncoding() commandBuffer.commit()
- 
										
										27:01 - Use MeshResource extrusion to generate 3D text // Use MeshResource(extruding:) to generate 3D text var textString = AttributedString("RealityKit") textString.font = .systemFont(ofSize: 8.0) let secondLineFont = UIFont(name: "ArialRoundedMTBold", size: 14.0) let attributes = AttributeContainer([.font: secondLineFont]) textString.append(AttributedString("\nDrawing App", attributes: attributes)) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .center let centerAttributes = AttributeContainer([.paragraphStyle: paragraphStyle]) textString.mergeAttributes(centerAttributes) var extrusionOptions = MeshResource.ShapeExtrusionOptions() extrusionOptions.extrusionMethod = .linear(depth: 2) extrusionOptions.materialAssignment = .init(front: 0, back: 0, extrusion: 1, frontChamfer: 1, backChamfer: 1) extrusionOptions.chamferRadius = 0.1 let textMesh = try await MeshResource(extruding: textString extrusionOptions: extrusionOptions)
- 
										
										28:25 - Use MeshResource extrusion to turn a SwiftUI Path into 3D mesh // Use MeshResource(extruding:) to bring SwiftUI.Path to 3D let graphic = SwiftUI.Path { path in path.move(to: CGPoint(x: -0.7, y: 0.135413)) path.addCurve(to: CGPoint(x: -0.7, y: 0.042066), control1: CGPoint(x: -0.85, y: 0.067707), control2: CGPoint(x: -0.85, y: 0.021033)) // ... } var options = MeshResource.ShapeExtrusionOptions() // ... let graphicMesh = try await MeshResource(extruding: graphic extrusionOptions: options)
- 
										
										29:44 - Defining a LowLevelTexture let descriptor = LowLevelTexture.Descriptor(pixelFormat: .rg16Float, width: textureResolution, height: textureResolution, textureUsage: [.shaderWrite, .shaderRead]) let lowLevelTexture = try LowLevelTexture(descriptor: descriptor) var textureResource = try TextureResource(from: lowLevelTexture) var material = UnlitMaterial() material.color = .init(tint: .white, texture: .init(textureResource))
- 
										
										30:27 - Update a LowLevelTexture on the GPU let lowLevelTexture: LowLevelTexture let commandBuffer: MTLCommandBuffer let encoder: MTLComputeCommandEncoder let computePipeline: MTLComputePipelineState commandBuffer.enqueue() encoder.setComputePipelineState(computePipeline) let writeTexture: MTLTexture = lowLevelTexture.replace(using: commandBuffer) encoder.setTexture(writeTexture, index: 0) // ... encoder.dispatchThreadgroups(/* ... */) encoder.endEncoding() commandBuffer.commit()
 
-