ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
RealityKit 2を使用した高度なレンダリング
RealityKitの最先端のレンダリング機能を使用して、AR体験のための魅力的なビジュアルを作成しましょう。カスタムシェーダの書き方、リリアルタイムでのダイナミックメッシュの描き方、ARシーンのスタイリングに役立つクリエイティブなポストプロセス効果について解説します。
リソース
- Building an Immersive Experience with RealityKit
- Creating a Fog Effect Using Scene Depth
- Displaying a Point Cloud Using Scene Depth
- Explore the RealityKit Developer Forums
- RealityKit
関連ビデオ
WWDC22
WWDC21
WWDC20
-
ダウンロード
♪ (RealityKit 2を使った 高度なレンダリングを探求) こんにちは 私は コートランド・アイドルトラムです RealityKitチームの エンジニアです このビデオでは RealityKit 2の新しい レンダリング機能の使い方を ご紹介します RealityKitは ARアプリケーションを シンプルかつ直感的に構築するため に設計されたフレームワークです レンダリングは リアリティの高い 物理ベースのレンダリング を中心とした RealityKitのキーとなる部分です 2019年の 最初のリリース以来 私たちは皆さんの フィードバックに取り組み続け RealityKitのメジャーアップデートを お届けすることとなりました 「RealityKit 2の詳細」 セッションでは RealityKitの進化について 説明しました ECSシステムのアップデートから 多くの改良点 より進化したマテリアルおよび アニメーション機のを提供 実行時のオーディオおよび テクスチャリソース生成などです これらの改良点を 紹介するために リビングルームを 海中水族館にする Appを作りました 本セッションでは このAppに搭載された 新しいレンダリング機能の 一部をご紹介します RealityKit 2は オブジェクトの レンダリングにおける 制御と柔軟性を 提供し より優れたAR体験を 生み出すことができます 今年は マテリアルシステムが進化し カスタムMetalシェーダーを オーサリングして 独自のマテリアルを追加 できるようになりました カスタムポストエフェクトは RealityKitのポストエフェクトを 独自の物で補強することができます 新しいメッシュAPIにより 実行時に メッシュの作成 検査 修正が 可能になりました RealityKit 2で最もリクエストの 多かった機能から始めましょう これはカスタムシェーダーの サポートです RealityKitのレンダリングは 物理ベースのレンダリングモデルを 中心として行われます 内蔵されたシェーダーにより さまざまな 照明条件のもとで 実物に近い自然なモデルを 簡単に作成することができます 今年はこれらの物理ベースの シェーダーに基づいて構築し シェーダーを使用して モデルのジオメトリと サーフェスをカスタマイズする 機能を公開します シェーダーAPIの第一弾 は ジオメトリモディファイヤです ジオメトリモディファイヤは Metal Shading言語で開発された プログラムであり GPUでレンダリングされるときに フレームごとに オブジェクトの頂点を 変更する機会を提供します これには頂点の移動や カラー 法線 UVのような 頂点属性の カスタマイズが含まれます RealityKitの頂点シェーダーの 中で実行され アンビエントアニメーション デフォメーション パーティクルシステムおよび ビルボードに最適です 海藻が周囲の水の動き に合わせて ゆっくりと動いています もっと近くで 見てみましょう ここでは アーティストが作成した海藻の ワイヤーフレームを 見ることができます これは網の目を構成する 頂点と三角形を表示しています これから 各頂点で実行される シェーダープログラムを書いて 動きを作ります 単純な周期関数である 正弦波を使って 動きを表現します 水流をシミュレート しているので モデルのスケールや 向きに関係なく 近くの頂点が同じように動く ようにしたいのです そのため 頂点のワールド位置を 正弦関数の入力データ として使います 時間の経過とともに動くように 時間値も入れています 最初の正弦波は 上下の動きを表現するために Y次元になっています 動きの周期を コントロールするために 空間スケールを 追加します そして その動きの大きさを振幅で コントロールすることができます X軸とZ軸にも 同じ機能を適用して 3つの軸すべてで 動くようにします では モデル全体を 見てみましょう 1つだけ まだ計算していない ことがあります 茎の根元に近いところにある 頂点は ほとんど動く余地がなく 上の方にある頂点は 最も自由に動くことができます これをシミュレートするには オブジェクトの原点に対する 頂点のY座標を 3つの軸に対する倍率 として使用すれば 最終的な計算式 が得られます さて シェーダーの計画が できたところで これらのパラメータがどこに あるのかを見てみましょう ジオメトリパラメータは いくつか のカテゴリーに分かれています 1つ目はユニフォーム つまり1フレーム内の オブジェクトのすべての頂点に 同じ値を設定することです 海藻には 時間が必要です テクスチャにはモデルの一部として 描かれたすべてが含まれており さらにカスタムスロット が追加されています これは必要に応じて 使用することができます マテリアル定数には 色合いや 不透明度などの パラメータがあり オブジェクトで描くか コードを用いて設定します ジオメトリには 現在の頂点のモデルポジションや 頂点IDなど 一部の読み取り専用 の値が含まれています 私たちの 海藻の動きには モデルとワールドの 両方のポジションが必要です また ジオメトリは読み書き可能な 値を持っています 法線 UV モデルポジションの オフセットなどです オフセットを計算したら それをここに保存して 頂点を移動させます それでは Metal shaderを 見ていきましょう まず RealityKit.hを インクルードします ここでvisible function属性を 持つ関数を宣言します これは 他の機能とは 別に利用できるようにすることを コンパイラに指示するものです この関数は RealityKitの geometry_parametersという 1つのパラメータを受け取ります すべての値をこのオブジェクト で取得します paramsのジオメトリ メンバーを使って ワールドポジションとモデル ポジションの両方を要求します 次に 頂点での ワールドポジションと 時間に基づいて 位相オフセットを計算します そして計算式を適用して この頂点のオフセットを算出します このオフセットは ジオメトリに保存され 頂点のモデルポジションに 追加されます ジオメトリモディファイアに ありますが まだ私たちの海藻には つながっていません Swiftで書かれたARViewの サブクラスに切り替えてみましょう まず Appのデフォルトの Metalライブラリをロードします このライブラリには シェーダーが含まれています 次に シェーダーの名前と ライブラリを使って geometryModifierを 作成します 海藻の素材ごとに 新しいカスタム素材を 作ります 既存の素材を CustomMaterialの 第一パラメータとして渡し ベース素材からテクスチャや 素材のプロパティを継承しつつ ジオメトリモディファイアを 追加します なかなかいい感じですね 水中なので アニメーションはかなり ゆっくりとした動きになっています 振幅と位相を調整することで 草や木などの葉にも 同じ効果を 与えることができます ジオメトリの変更方法 を紹介しましたが 次にシェーディングについて 説明します これは水中シーンの タコです 内蔵されたシェーダーで きれいになっています 本物のように タコは様々な表情を 見せてくれます 2つ目は赤みを帯びた 色をしています 当社のアーティストは 各ルック用に2つの ベースカラーテクスチャを 作成しました また 色の変化だけでなく 赤いタコは粗さの値が 大きいため 反射しにくく なっています そして このタコをより特別な ものにするために 見た目の変化を 楽しめるようにしました ここでは その変遷を ご覧いただけます 魅了されます それぞれの見た目は 物質的に マテリアルベースで表現されていますが トランジション自体は サーフェスシェーダーを 書く必要があります では サーフェスシェーダーとは 何でしょうか? サーフェスシェーダーは オブジェクトの外観を 定義する ことができます オブジェクトの 可視ピクセルごとに フラグメントシェーダー内で 実行されます 色だけではなく 表面の 法線 反射 粗さなどの 表面特性も含まれます シェーダーを書くことで オブジェクトの外観を向上させたり 完全に置き換えて新しい効果を 生み出すことができます タコのベースカラーとなる2つの テクスチャを見てきました トランジション効果のために アーティストが特別なテクスチャを エンコードしてくれました このテクスチャーは 実は3つの異なる 層の組み合わせです 一番上には ノイズレイヤーがあり 局所的なトランジションパターンを 作り出しています トランジション層があり 全体の動きを決定し 頭から始まり 触手に向かって移動していきます また 目や触手の下側など 色を変えたくない部分には マスクレイヤーがあります この3つのレイヤー を組み合わせて テクスチャの赤 緑 青の チャネルを作り カスタムテクスチャスロットに 割り当てます テクスチャの設定が できたところで シェーダーからテクスチャへの アクセス方法を見てみましょう ジオメトリモディファイアと 同様に サーフェスシェーダーは ユニフォーム テクスチャ マテリアルの定数に アクセスできます 時間は私たちのオクトパス トランジションの入力データです モデルで描かれたテクスチャ をサンプリングし マテリアルの定数を 読み取ることで アーティストはモデル全体の 調整を行うことができます ジオメトリ 位置 法線 UVなどは ジオメトリ構造体に 表示されます これらはバーテックスシェーダー からの補間された出力です ここではUV0をテクスチャ座標 として使用します サーフェスシェーダーは サーフェス構造を書き込みます プロパティは デフォルトの値で始まり その値を自由に 計算することができます ベースカラーと法線を 計算することになります そして 4つの表面パラメータ: 粗さ メタリック アンビエントオクルージョン 反射 値の場所が わかったところで シェーダーの作成を 始めましょう これを3つのステップで 行います まず 0が完全に紫のタコ 1が完全に赤のタコと なる遷移値を計算します トランジション値を使って 色と法線を計算し マテリアルのプロパティを 割り当てて微調整します では 始めましょう 最初のステップ:トランジション タコの表面関数を 構築しています これはsurface_parameters という引数を取ります テクスチャを使っているので サンプラーを宣言します 右側にはサーフェイスシェーダーが 何もない状態の タコの様子があります グレーで 少し光沢があります RealityKitでは モデルの外観に 影響を与えるものと 与えないものを完全にコントロール することができます 色を計算するためには 最初にいくつかのことを しなければなりません いくつかの便利な変数を 保存しておきます テクスチャーの座標 としてしようする UV0にアクセスします MetalとUSDではテクスチャの 座標系が異なるので USDから読み込んだ テクスチャに合わせて Y座標を反転させます では トランジションテクスチャを アーティストが作成した3層構造の テクスチャを試してみましょう アーティストは マスクの値と時間を受け取り blendとcolorBlendに 0から1の値を返す 小さな関数を設定しました 第2のステップ:カラーと法線 先に計算した ブレンド変数を使って タコの色を計算し その推移を 見ることができます そのために2つのテクスチャを サンプリングします: ベースカラーと セカンダリーベースカラーで emissive_color に 格納しています そして 先に計算された colorBlendを使って 2つの色をブレンドします マテリアルの値である base_color_tintを 掛け合わせて サーフェスの ベースカラーを設定します 次に 法線マップを 適用します 法線マップは 頭と触手に顕著な サーフェス偏差を 追加します 法線マップのテクスチャを サンプリングし その値をアンパックし サーフェスオブジェクトに セットします カラーと法線を用いた これまでのところのタコです サーフェスの特性が どう見た目に 影響するかを見てみましょう 下半身に見られるような粗さ: アンビエントオクルージョンで 下の部分が暗くなります そしてスペキュラを使用することで 目には美しい反射を 体にはさらなる立体感を 与えることができます これらをシェーダーに 追加してみましょう モデルでは 各特性ごとに 4つのテクスチャーを サンプリングしています 次にこれらの値をマテリアルの 設定でスケーリングします さらに 紫から赤への 移行に合わせて 粗さを大きくしています そして サーフェスに 4つの値を設定します 先ほどと同様にモデルにシェーダー を適用する必要があります ARViewのサブクラスで この素材をモデルに割り当てます まず 2つの追加テクスチャを 読み込み 次にサーフェスシェーダーを 読み込みます 前回と同様に 今回はサーフェスシェーダーと 2つの追加テクスチャを 使って オブジェクトのベースマテリアル から新しいマテリアルを構築します 以上が私たちの することです おさらいすると ジオメトリモディファイヤを使った 海藻のアニメーションと サーフェスシェーダーを使った タコのトランジションの 作り方を紹介しました ここまでは別々に 紹介してきましたが この2つを組み合わせることで より面白い効果を得られるのです 次に 要望の多かった機能を 紹介します カスタムポストプロセッシングエフェクトの 追加をサポートします RealityKitには モーションブラー カメラノイズ 被写界深度など カメラに合わせた ポストエフェクトが 豊富に用意されています これらの効果はバーチャルなものと リアルなものが 同じ環境に存在するかのように 感じさせるためのものです これらはARViewでカスタマイズ することができます 今年はまた 独自のフルスクリーン効果を 作ることが できるようになります これによりRealityKitを活用して 写真をリアルに再現したり 新しいエフェクトを追加してApp に合わせて仕上げることができます ではポストプロセスとは 何でしょうか? ポストプロセスとはオブジェクトが レンダリングされて命を 吹き込まれた後に実行される シェーダーや一連のシェーダーです どんなRealityKitのポスト エフェクトの後にも発生します その入力は2つのテクスチャ: カラーと深度バッファです 深度バッファはグレースケールで 表示されます このバッファには 各ピクセルのカメラに対する 距離値が含まれています ポストプロセスは その結果を ターゲットのカラーテクスチャに 書き込みます 最もシンプルな ポストエフェクトは ソースカラーをターゲットカラーに コピーするものです これらを構築するには いくつかの方法があります Appleプラットフォームには ポストエフェクトとの 連携に優れた技術が 数多く搭載されています 例えば Core Image Metal Performance Shaders そしてSpriteKitなどです またMetal Shading Languageで 自分で書くこともできます まずはCore Imageのエフェクトを ご紹介します Core Imageは画像処理のための Appleのフレームワークです 画像やビデオに適用できる 数百種類のカラー処理 スタイル化 変形効果を備えています サーマルは素晴らしい効果 水中の魚群探知機のように あなたを惹きつけるかもしれません それでは RealityKitとの連携が どれほど簡単か見てみましょう 全てのポストエフェクトは 同じパターンで行われます レンダリングコールバックを設定し デバイスの準備に応じて ポストプロセスが フレームごとに呼び出されます RealityKitのARViewには レンダーコールバックが存在します prepareWithDevice コールバックと postProcessコールバック の両方が必要です prepareWithDeviceは MTLDeviceで 一度だけ呼び出されます これはテクスチャを 作成したり コンピュートやレンダリング パイプラインをロードしたり デバイスの性能をチェック したりする良い機会です ここでCore Imageコンテキストを 作成します postProcessコールバックは フレームごとに呼び出されます ソースカラーの テクスチャを参照して CIImageを作成します 次にサーマルフィルターを 作成します 別のCore Image 使用している場合は ここで 他の パラメータを設定します 次に出力カラーの テクスチャをターゲットにして コンテキストのコマンド バッファを利用する レンダリングデスティネーションを 作成します Core Imageに画像の向きを 保持するように依頼し タスクを開始します それです! Core Imageでは あらかじめ用意されている 何百ものエフェクトを 使用することができます それでは Metal Performance Shadersを使い 新しいエフェクトを構築する 方法を見てみましょう ブルームについて お話しましょう ブルームとは 明るく照らされた 物体の周りに 輝きを与え 現実のレンズ効果をシミュレート するscreen spaceの手法です Core Imageにはブルーム効果が 搭載されていますが ここではすべての工程を コントロールできるように 独自に構築してみましょう Metal Performance Shadersを 使って効果を作ります 高度に最適化された計算と画像の シェーダーのコレクションです このシェーダーを 構築するために カラーをソースとして使い フィルタのグラフを 構築していきます まず 明るい部分を 分離したいと思います そのために「Threshold to Zero」 という操作を行います 色を輝度に変換し ある明度以下の画素を すべて0にする その結果をガウシアンブラー でぼかします そして隣接するエリアに 光を拡散させます 効率的なブラーを実現するには 複数のステージが 必要になることがあります Metal Performance Shadersが これを処理してくれます そしてこのぼかしたテクスチャーを 元の色に加え 明るい部分には輝きを加えます このグラフをポストエフェクト として実装してみましょう まずは 中間的な bloomTextureを作成します それから ThresholdToZero操作を実行し sourceColorから読み込み bloomTextureに書き込みます そして 適所に gaussianBlurを行います 最後に 元の色と このコーティングした色を 合わせます それです! さてポストエフェクトを作る方法 をいくつか見てきましたが 今度はSpriteKitを使って 出力の上にエフェクトを乗せる 方法をご紹介します SpriteKitはハイパフォーマンスで バッテリー効率の良い 2Dゲームのための Appleのフレームワークです 3Dビューの上にエフェクトを 加えるのに最適です 同じprepareWithDeviceと postProcessコールバックを使って ポストエフェクトとして 画面上に泡を追加します 先ほどと同じ 2つのステップがあります prepareWithDeviceでは SpriteKitのレンダラーを作成し 泡を含むシーンを ロードします そして postProcess コールバックでは ソースカラーを ターゲットカラーにコピーし SpriteKitシーンを更新し 3Dコンテンツの上に レンダリングします prepareWithDeviceは 非常に簡単で レンダラーを作成し ファイルから シーンを読み込みます これをARシーンの上に 描くことになるので SpriteKitの背景を 透明にする必要があります postProcessでは まずソースカラーを targetColorTextureに 転送します これがSpriteKitが レンダリング する際の背景になります そして SpriteKitのシーンを 新しい時間に進めることで 泡が上に移動します RenderPassDescriptorを設定し その上でレンダリングを行います そして これで終わりです 既存のフレームワークを利用して ポストエフェクトを 作る方法を紹介しましたが 実際にはゼロから作る 必要がある場合もあります またコンピュートシェーダを 書くことで フルスクリーン効果を 加えることもできます 水中のデモではバーチャル オブジェクトに適用される フォグ効果とカメラの パススルーが必要でした フォグは媒体を通して 光の散乱を表現するもので その明暗度は 距離に比例します この効果を生み出すためには 各ピクセルが デバイスからどれだけ 離れているかを知る必要があります 幸いなことに ARKitとRealityKitは どちらも奥行き情報への アクセスが可能です LiDARを搭載した デバイスに対して ARKitはメートルでの カメラからの距離を 表すsceneDepthに アクセスできます これらの値は フルスクリーンよりも 低い解像度では 極めて正確です この深度をそのまま 使うこともできますが バーチャルオブジェクトが 含まれていないため 正しくフォグが かかりません ポストプロセスでは RealityKitがバーチャルコンテンツ の深度へのアクセスと scene understandingが 有効な場合は 実在するオブジェクトに対する近似 メッシュへのアクセスを提供します 動きに合わせて徐々にメッシュ が構築されていくので 現在スキャンしていない 穴も含まれています これらの穴はまるで非常に遠くに あるかのように霧を見せています この相違を解決するために この2つの深度テクスチャーの データを組み合わせます ARKitでは深度値をテクスチャー として提供しています 各ピクセルは サンプリングされた点の メートルでの距離です iPhoneやiPadでは センサーの向きが 固定されているため ARKitに センサーの向きから現在の 画面の向きへの変換を行い その結果を反転させるように 指示します バーチャルコンテンツの深度を 読み取るためには RealityKitが どのように深度をパックしているか という情報が少し必要です ARKitのsceneDepthとは異なり 明るい値の方が カメラに近いことが わかると思います 値は0~1の範囲で格納され インフィニットReverse-Zプロジェクション を使用します これは 0が非常に遠く 1がカメラの近い次元にある ことを意味しています この変換を逆にするには 近景の深度を サンプリングされた深度で割る ことで簡単にできます これを行うためのヘルパー関数を 書いてみましょう サンプルの深度と projection matrixを取る Metal関数があります バーチャルコンテンツを持たない ピクセルはちょうど0です ゼロ除算を防ぐために 小文字のイプシロンに固定します 遠近法の分割を元に戻すには 最後の列のz値を取得し サンプリングされた深さで 除算します 素晴らしいです! 2つの深度値が得られたので 2つのうちの最小値を フォグ関数の入力 として使用できます 私たちのフォグには最大距離 その距離での最大強度 パワーカーブの指数という いくつかの パラメータがあります 正確な値は 実験的に選ばれたものです 望ましいフォグ濃度を実現する ために深度の値を調整します これでパーツを組み立てる 準備が整いました ARKitからの深度値 RealityKitからの 線形化された深度値 そしてフォグ用の 関数があります それではコンピュートシェーダーを 書いてみましょう 各ピクセルでは まず両方の直線的な 深度値をサンプリングします そしてチューニングパラメータを 使ってフォグ関数を適用し 直線的な深度を 0~1の値に変えます そしてfogBlendの値に応じて ソースカラーと フォグカラーをブレンドし その結果をoutColorに格納します おさらいするとRealityKitの 新しいポストプロセスAPIは 様々なポストエフェクトを 可能にします Core Imageでは 数百種類もの既存エフェクトの ロックを解除しました。 Metal Performance Shadersで 新しいものを作ったり SpriteKitでスクリーン オーバーレイを追加したり Metalで1から自分で書いたりする ことが簡単にできます Core Imageや Metal Performance Shadersの 詳細については 掲載されている セッションをご覧ください レンダリング効果について 説明しましたが 次のテーマであるダイナミック メッシュについて説明します RealityKitではメッシュリソースが メッシュデータを格納します 従来この不透明なタイプでは 実在する物にメッシュを 割り当てることができました 今年は ランタイムで メッシュを検査したり メッシュを作成 更新する機能を提供します ここではダイバーに特殊効果を 加える方法を見てみましょう 今回のデモでは ダイバーの周りに螺旋状の輪郭を 描くスパイラル効果を 表現したいと思います またスパイラルがその動きを アニメーション化するために時間の 経過とともにメッシュをどのように 変化させているかを確認できます それでは 新しいメッシュAPIを使って どのように作成するかを 見てみましょう その効果は3つのステップに 集約されます メッシュ検査では モデルの頂点を調べて 測定します そしてその寸法を参考にしながら スパイラルを作っていきます そして最後に時間の経過とともに スパイラルを更新できます まずはメッシュ検査から 始めます メッシュがどのように 保存されるかを説明するために ダイバーモデルを 見てみましょう RealityKitでは Diverのメッシュは メッシュリソースとして 表現されます 今回のリリースで MeshResourceにはContents というメンバーが追加されました ここには処理されたメッシュ ジオメトリが全部格納されています Contentsには インスタンスと モデルのリストが含まれています モデルは未加工の 頂点データを含み インスタンスはそれらを参照し 変換を加えます インスタンスではデータを コピーすることなく 同じジオメトリを複数回表示 することができます モデルは複数のパーツを 持つことができます パーツとは 1つの素材をもつ ジオメトリのグループです 最後に各パートには 私たちが関心を持つ頂点データ 位置 法線 テクスチャ座標インデックスなどの データが含まれます まずコードでこのデータに アクセスする方法を見てみましょう MeshResource.Contentsを 拡張して 各頂点の位置を持つクロージャを 呼び出すようにします まずはすべてのインスタンスを 確認することから始めます これらのインスタンスは それぞれモデルに対応しています 各インスタンスについて 実在物に対する トランスフォームを求めます そしてモデルの 各パーツを調べ パーツの属性にアクセス することができます この関数ではポジションにのみ 興味があります そしてその頂点を 実在空間の位置に変換し コールバックを 呼び出します 頂点を見に行くことが できたので このデータをどのように利用するか を考えてみましょう ダイバーを水平方向に 分割します 各スライスについて モデルの外接半径を求め これをすべてのスライス について行います これを実装するには まず numSlicesの要素を持つ ゼロ埋めの配列を作成します そしてY軸に沿って メッシュの境界を把握し スライスを作成します 先ほど作成した関数を使って モデルの各頂点に対して どのスライスに入るかを把握し そのスライスの最大半径で 半径を更新します 最後に 半径と境界線を含む スライスオブジェクトを 返します さてメッシュの大きさを 分析したところで スパイラルメッシュの作成方法を 見てみましょう スパイラルは 同時に生成されるメッシュです このメッシュを作るためには RealityKitにデータを 記述する必要があります これにはメッシュディスクリプター を使用します メッシュディスクリプターには 位置 法線 テクスチャ座標 プリミティブ マテリアルインデックスがあります メッシュディスクリプターが あれば メッシュリソースを 生成することができます これによりRealityKitの メッシュプロセッサが起動し メッシュが最適化されます 重複する頂点を結合し 四角形や多角形を三角化し レンダリングに 最も適した形式で メッシュを表現します この処理の結果 メッシュリソースが得られ これを実在物に割り当てることが できるようになります なお 法線 テクスチャ座標 マテリアルは オプションであることを 注意してください 私たちの メッシュプロセッサは 自動的に正しい法線を生成し それを入力します 最適化プロセスの一環として RealityKitはメッシュの トポロジーを再生成します 特定のトポロジーが 必要な場合には MeshResource.Contentsを 直接使用することができます ここまでで メッシュの 作成方法がわかったので スパイラルを 作る方法を見てみましょう スパイラルのモデルとして セクションを詳しく見てみましょう
スパイラルは ヘリックスとも呼ばれています これを等間隔に分割して 作っていきます 螺旋の数学的定義と 解析したメッシュから得られる 半径を用いて 各ポイントを 計算することができます 螺旋上の各部分に この関数を使用すると 4つの頂点を 定義することができます P0とP1はまさに p()が返す値です P2とP3を計算するには P0とP1を与えられた厚さで 垂直にオフセット すればいいのです 三角形を作るわけですから 対角線が必要です これらの点を利用して 2つの三角形を作ります まとめてみましょう generateSpiral関数は 位置とインデックスを 格納する必要があります 位置の基準値を示します 各部分について 4つの位置を計算し そのインデックスを保存します i0は配列に追加されたときの p0 のインデックスです そして 2つの三角形のための 4つの位置と 6つのインデックスを それぞれの配列に追加します ジオメトリがあれば メッシュの作成は簡単です まず 新しい MeshDescriptorを作成します そして ポジションと プリミティブを割り当てます ここでは三角形のプリミティブを 使用していますが 四角形や多角形でも 構いません この2つのフィールドが 入力されると MeshResourceを生成するのに 十分な情報が得られます また法線やtextureCoordinates マテリアルの割り当てなど 他の頂点属性を 指定することもできます ここまではメッシュの作成方法 について説明しました スパイラルの例で 最後に 紹介するのはメッシュの更新です スパイラルがダイバーの周りを 動くようにするために メッシュアップデートを使用します メッシュを更新するには 2つの方法があります MeshDescriptors APIを使って フレームごとに 新しいMeshResourceを 作成することができます しかしこれはフレームごとに メッシュオプティマイザを 実行することになるので 効率的なルートではありません より効率的な方法は MeshResourceの コンテンツを 更新することです 新しい MeshContentsを生成し それを使ってメッシュを 置き換えることができます ただし 1つだけ 注意点があります MeshDescriptorを使ってオリジナル のメッシュを作成した場合は RealityKitのメッシュプロセッサが データを最適化します トポロジーも三角形に 還元されます そのためアップデートを適用する 前に自分のメッシュが どのような影響を受けるかを 確認してください スパイラルを更新する方法について コードを見てみましょう まず既存のスパイラルの内容を 保存することから始めます 既存のモデルから 新しいモデルを作成します そして 各パーツごとに triangleIndicesをインデックスの サブセットで置き換えます 最後に 新しいコンテンツで 既存のMeshResourceでreplaceを 呼び出すことができます 以上でダイナミックメッシュの 作成は終了です ダイナミックメッシュの 重要な点をまとめるために MeshResourceに新しい Contentsフィールドを紹介しました このコンテナでは メッシュの未加工のデータを 検査 修正することができます MeshDescriptorを使って新しい メッシュを作ることができます この柔軟なルートでは 三角形 四角形あるいは 多角形さえ使うことができ RealityKitは レンダリング用に最適化された メッシュを生成します 最後にメッシュを更新するために MeshResourceのコンテンツを 更新する機能を用意しました これは頻繁に更新する場合に 理想的です まとめます 本日は RealityKit 2に搭載された 新しいレンダリング機能を いくつかご紹介しました ジオメトリモディファイヤは 頂点の移動や修正を行います サーフェスシェーダーは モデルの外観を 定義することができます ポストエフェクトを使って 最終フレームに エフェクトをかけることができるし ダイナミックメッシュを使えば ランタイムにメッシュの作成や 修正が簡単にできます 今年の機能の詳細については 「RealityKit 2の詳細」をぜひ ご確認してください また RealityKitの 更なる情報については 「RealityKitでAppを構築する」を ご覧ください 私たちは 今年のリリースを とても楽しみにしています 皆さんがどのような体験を構築されるか 楽しみでしかないです ご清聴 ありがとうございました ♪
-
-
4:52 - Seaweed Shader
#include <RealityKit/RealityKit.h> [[visible]] void seaweedGeometry(realitykit::geometry_parameters params) { float spatialScale = 8.0; float amplitude = 0.05; float3 worldPos = params.geometry().world_position(); float3 modelPos = params.geometry().model_position(); float phaseOffset = 3.0 * dot(worldPos, float3(1.0, 0.5, 0.7)); float time = 0.1 * params.uniforms().time() + phaseOffset; float3 maxOffset = float3(sin(spatialScale * 1.1 * (worldPos.x + time)), sin(spatialScale * 1.2 * (worldPos.y + time)), sin(spatialScale * 1.2 * (worldPos.z + time))); float3 offset = maxOffset * amplitude * max(0.0, modelPos.y); params.geometry().set_model_position_offset(offset); }
-
5:43 - Assign Seaweed Shader
// Assign seaweed shader to model. func assignSeaweedShader(to seaweed: ModelEntity) { let library = MTLCreateSystemDefaultDevice()!.makeDefaultLibrary()! let geometryModifier = CustomMaterial.GeometryModifier(named: "seaweedGeometry", in: library) seaweed.model!.materials = seaweed.model!.materials.map { baseMaterial in try! CustomMaterial(from: baseMaterial, geometryModifier: geometryModifier) } }
-
9:21 - Octopus Shader
#include <RealityKit/RealityKit.h> void transitionBlend(float time, half3 masks, thread half &blend, thread half &colorBlend) { half noise = masks.r; half gradient = masks.g; half mask = masks.b; half transition = (sin(time * 1.0) + 1) / 2; transition = saturate(transition); blend = 2 * transition - (noise + gradient) / 2; blend = 0.5 + 4.0 * (blend - 0.5); // more contrast blend = saturate(blend); blend = max(blend, mask); blend = 1 - blend; colorBlend = min(blend, mix(blend, 1 - transition, 0.8h)); } [[visible]] void octopusSurface(realitykit::surface_parameters params) { constexpr sampler bilinear(filter::linear); auto tex = params.textures(); auto surface = params.surface(); auto material = params.material_constants(); // USD textures have an inverse y orientation. float2 uv = params.geometry().uv0(); uv.y = 1.0 - uv.y; half3 mask = tex.custom().sample(bilinear, uv).rgb; half blend, colorBlend; transitionBlend(params.uniforms().time(), mask, blend, colorBlend); // Sample both color textures. half3 baseColor1, baseColor2; baseColor1 = tex.base_color().sample(bilinear, uv).rgb; baseColor2 = tex.emissive_color().sample(bilinear, uv).rgb; // Blend colors and multiply by the tint. half3 blendedColor = mix(baseColor1, baseColor2, colorBlend); blendedColor *= half3(material.base_color_tint()); // Set on the surface. surface.set_base_color(blendedColor); // Sample the normal and unpack. half3 texNormal = tex.normal().sample(bilinear, uv).rgb; half3 normal = realitykit::unpack_normal(texNormal); // Set on the surface. surface.set_normal(float3(normal)); // Sample material textures. half roughness = tex.roughness().sample(bilinear, uv).r; half metallic = tex.metallic().sample(bilinear, uv).r; half ao = tex.ambient_occlusion().sample(bilinear, uv).r; half specular = tex.roughness().sample(bilinear, uv).r; // Apply material scaling factors. roughness *= material.roughness_scale(); metallic *= material.metallic_scale(); specular *= material.specular_scale(); // Increase roughness for the red octopus. roughness *= (1 + blend); // Set material properties on the surface. surface.set_roughness(roughness); surface.set_metallic(metallic); surface.set_ambient_occlusion(ao); surface.set_specular(specular); }
-
11:41 - Assign Octopus Shader
// Apply the surface shader to the Octopus. func assignOctopusShader(to octopus: ModelEntity) { // Load additional textures. let color2 = try! TextureResource.load(named: "Octopus/Octopus_bc2") let mask = try! TextureResource.load(named: "Octopus/Octopus_mask") // Load the surface shader. let surfaceShader = CustomMaterial.SurfaceShader(named: "octopusSurface", in: library) // Construct a new material with the contents of an existing material. octopus.model!.materials = octopus.model!.materials.map { baseMaterial in let material = try! CustomMaterial(from: baseMaterial surfaceShader: surfaceShader) // Assign additional textures. material.emissiveColor.texture = .init(color2) material.custom.texture = .init(mask) return material } }
-
14:13 - CoreImage PostEffect
// Add RenderCallbacks to the ARView. var ciContext: CIContext? func initPostEffect(arView: ARView) { arView.renderCallbacks.prepareWithDevice = { [weak self] device in self?.prepareWithDevice(device) } arView.renderCallbacks.postProcess = { [weak self] context in self?.postProcess(context) } } func prepareWithDevice(_ device: MTLDevice) { self.ciContext = CIContext(mtlDevice: device) } // The CoreImage thermal filter. func postProcess(_ context: ARView.PostProcessContext) { // Create a CIImage for the input color. let sourceColor = CIImage(mtlTexture: context.sourceColorTexture)! // Create the thermal filter. let thermal = CIFilter.thermal() thermal.inputImage = sourceColor // Create the CIRenderDestination. let destination = CIRenderDestination(mtlTexture: context.targetColorTexture, commandBuffer: context.commandBuffer) // Preserve the image orientation. destination.isFlipped = false // Instruct CoreImage to start our render task. _ = try? self.ciContext?.startTask(toRender: thermal.outputImage!, to: destination) }
-
16:15 - Bloom Post Effect
var device: MTLDevice! var bloomTexture: MTLTexture! func initPostEffect(arView: ARView) { arView.renderCallbacks.prepareWithDevice = { [weak self] device in self?.prepareWithDevice(device) } arView.renderCallbacks.postProcess = { [weak self] context in self?.postProcess(context) } } func prepareWithDevice(_ device: MTLDevice) { self.device = device } func makeTexture(matching texture: MTLTexture) -> MTLTexture { let descriptor = MTLTextureDescriptor() descriptor.width = texture.width descriptor.height = texture.height descriptor.pixelFormat = texture.pixelFormat descriptor.usage = [.shaderRead, .shaderWrite] return device.makeTexture(descriptor: descriptor)! } func postProcess(_ context: ARView.PostProcessContext) { if self.bloomTexture == nil { self.bloomTexture = self.makeTexture(matching: context.sourceColorTexture) } // Reduce areas of 20% brightness or less to zero. let brightness = MPSImageThresholdToZero(device: context.device, thresholdValue: 0.2, linearGrayColorTransform: nil) brightness.encode(commandBuffer: context.commandBuffer, sourceTexture: context.sourceColorTexture, destinationTexture: bloomTexture!) // Blur the remaining areas. let gaussianBlur = MPSImageGaussianBlur(device: context.device, sigma: 9.0) gaussianBlur.encode(commandBuffer: context.commandBuffer, inPlaceTexture: &bloomTexture!) // Add color plus bloom, writing the result to targetColorTexture. let add = MPSImageAdd(device: context.device) add.encode(commandBuffer: context.commandBuffer, primaryTexture: context.sourceColorTexture, secondaryTexture: bloomTexture!, destinationTexture: context.targetColorTexture) }
-
17:15 - SpriteKit Post Effect
// Initialize the SpriteKit renderer. var skRenderer: SKRenderer! func initPostEffect(arView: ARView) { arView.renderCallbacks.prepareWithDevice = { [weak self] device in self?.prepareWithDevice(device) } arView.renderCallbacks.postProcess = { [weak self] context in self?.postProcess(context) } } func prepareWithDevice(_ device: MTLDevice) self.skRenderer = SKRenderer(device: device) self.skRenderer.scene = SKScene(fileNamed: "GameScene") self.skRenderer.scene!.scaleMode = .aspectFill // Make the background transparent. self.skRenderer.scene!.backgroundColor = .clear } func postProcess(context: ARView.PostProcessContext) { // Blit (Copy) sourceColorTexture onto targetColorTexture. let blitEncoder = context.commandBuffer.makeBlitCommandEncoder() blitEncoder?.copy(from: context.sourceColorTexture, to: context.targetColorTexture) blitEncoder?.endEncoding() // Advance the scene to the new time. self.skRenderer.update(atTime: context.time) // Create a RenderPass writing to the targetColorTexture. let desc = MTLRenderPassDescriptor() desc.colorAttachments[0].loadAction = .load desc.colorAttachments[0].storeAction = .store desc.colorAttachments[0].texture = context.targetColorTexture // Render! self.skRenderer.render(withViewport: CGRect(x: 0, y: 0, width: context.targetColorTexture.width, height: context.targetColorTexture.height), commandBuffer: context.commandBuffer, renderPassDescriptor: desc) }
-
19:08 - ARKit AR Depth
let width = context.sourceColorTexture.width let height = context.sourceColorTexture.height let transform = arView.session.currentFrame!.displayTransform( for: self.orientation, viewportSize: CGSize(width: width, height: height) ).inverted()
-
20:01 - Depth Fog Shader
typedef struct { simd_float4x4 viewMatrixInverse; simd_float4x4 viewMatrix; simd_float2x2 arTransform; simd_float2 arOffset; float fogMaxDistance; float fogMaxIntensity; float fogExponent; } DepthFogParams; float linearizeDepth(float sampleDepth, float4x4 viewMatrix) { constexpr float kDepthEpsilon = 1e-5f; float d = max(kDepthEpsilon, sampleDepth); // linearize (we have reverse infinite projection); d = abs(-viewMatrix[3].z / d); return d; } constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); float getDepth(uint2 gid, constant DepthFogParams &args, texture2d<float, access::sample> inDepth, depth2d<float, access::sample> arDepth) { // normalized coordinates float2 coords = float2(gid) / float2(inDepth.get_width(), inDepth.get_height()); float2 arDepthCoords = args.arTransform * coords + args.arOffset; float realDepth = arDepth.sample(textureSampler, arDepthCoords); float virtualDepth = linearizeDepth(inDepth.sample(textureSampler, coords)[0], args.viewMatrix); return min(virtualDepth, realDepth); } [[kernel]] void depthFog(uint2 gid [[thread_position_in_grid]], constant DepthFogParams& args [[buffer(0)]], texture2d<half, access::sample> inColor [[texture(0)]], texture2d<float, access::sample> inDepth [[texture(1)]], texture2d<half, access::write> outColor [[texture(2)]], depth2d<float, access::sample> arDepth [[texture(3)]] ) { const half4 fogColor = half4(0.5, 0.5, 0.5, 1.0); float depth = getDepth(gid, args, inDepth, arDepth); // Ignore depth values greater than the maximum fog distance. float fogAmount = saturate(depth / args.fogMaxDistance); float fogBlend = pow(fogAmount, args.fogExponent) * args.fogMaxIntensity; half4 nearColor = inColor.read(gid); half4 color = mix(nearColor, fogColor, fogBlend); outColor.write(color, gid); }
-
23:32 - MeshResource.Contents extension
// Examine each vertex in a mesh. extension MeshResource.Contents { func forEachVertex(_ callback: (SIMD3<Float>) -> Void) { for instance in self.instances { guard let model = self.models[instance.model] else { continue } let instanceToModel = instance.transform for part in model.parts { for position in part.positions { let vertex = instanceToModel * SIMD4<Float>(position, 1.0) callback([vertex.x, vertex.y, vertex.z]) } } } } }
-
24:20 - Mesh Radii
struct Slices { var radii : [Float] = [] var range : ClosedRange<Float> = 0...0 var sliceHeight: Float { return (range.upperBound - range.lowerBound) / Float(sliceCount) } var sliceCount: Int { return radii.count } func heightAt(index: Int) -> Float { return range.lowerBound + Float(index) * self.sliceHeight + self.sliceHeight * 0.5 } func radiusAt(y: Float) -> Float { let relativeY = y - heightAt(index: 0) if relativeY < 0 { return radii.first! } let slice = relativeY / sliceHeight let sliceIndex = Int(slice) if sliceIndex+1 >= sliceCount { return radii.last! } // 0 to 1 let t = (slice - floor(slice)) // linearly interpolate between two closest values let prev = radii[sliceIndex] let next = radii[sliceIndex+1] return mix(prev, next, t) } func radiusAtIndex(i: Float) -> Float { let asFloat = i * Float(radii.count) var prevIndex = Int(asFloat.rounded(.down)) var nextIndex = Int(asFloat.rounded(.up)) if prevIndex < 0 { prevIndex = 0 } if nextIndex >= radii.count { nextIndex = radii.count - 1 } let prev = radii[prevIndex] let next = radii[nextIndex] let remainder = asFloat - Float(prevIndex) let lerped = mix(prev, next, remainder) return lerped + 0.5 } } func meshRadii(for mesh: MeshResource, numSlices: Int) -> Slices { var radiusForSlice: [Float] = .init(repeating: 0, count: numSlices) let (minY, maxY) = (mesh.bounds.min.y, mesh.bounds.max.y) mesh.contents.forEachVertex { modelPos in let normalizedY = (modelPos.y - minY) / (maxY - minY) let sliceY = min(Int(normalizedY * Float(numSlices)), numSlices - 1) let radius = length(SIMD2<Float>(modelPos.x, modelPos.z)) radiusForSlice[sliceY] = max(radiusForSlice[sliceY], radius) } return Slices(radii: radiusForSlice, range: minY...maxY) }
-
25:58 - Spiral Point
// The angle between two consecutive segments. let theta = (2 * .pi) / Float(segmentsPerRevolution) // How far to step in the y direction per segment. let yStep = height / Float(totalSegments) func p(_ i: Int, radius: Float = 1.0) -> SIMD3<Float> { let y = yStep * Float(i) let x = radius * cos(Float(i) * theta) let z = radius * sin(Float(i) * theta) return SIMD3<Float>(x, y, z) }
-
26:37 - Generate Spiral
extension MeshResource { static func generateSpiral( radiusAt: (Float)->Float, radiusAtIndex: (Float)->Float, thickness: Float, height: Float, revolutions: Int, segmentsPerRevolution: Int) -> MeshResource { let totalSegments = revolutions * segmentsPerRevolution let totalVertices = (totalSegments + 1) * 2 var positions: [SIMD3<Float>] = [] var normals: [SIMD3<Float>] = [] var indices: [UInt32] = [] var uvs: [SIMD2<Float>] = [] positions.reserveCapacity(totalVertices) normals.reserveCapacity(totalVertices) uvs.reserveCapacity(totalVertices) indices.reserveCapacity(totalSegments * 4) for i in 0..<totalSegments { let theta = Float(i) / Float(segmentsPerRevolution) * 2 * .pi let t = Float(i) / Float(totalSegments) let segmentY = t * height if i > 0 { let base = UInt32(positions.count - 2) let prevInner = base let prevOuter = base + 1 let newInner = base + 2 let newOuter = base + 3 indices.append(contentsOf: [ prevInner, newOuter, prevOuter, // first triangle prevInner, newInner, newOuter // second triangle ]) } let radialDirection = SIMD3<Float>(cos(theta), 0, sin(theta)) let radius = radiusAtIndex(t) var position = radialDirection * radius position.y = segmentY positions.append(position) positions.append(position + [0, thickness, 0]) normals.append(-radialDirection) normals.append(-radialDirection) // U = in/out // V = distance along spiral uvs.append(.init(0.0, t)) uvs.append(.init(1.0, t)) } var mesh = MeshDescriptor() mesh.positions = .init(positions) mesh.normals = .init(normals) mesh.primitives = .triangles(indices) mesh.textureCoordinates = .init(uvs) return try! MeshResource.generate(from: [mesh]) } }
-
28:17 - Update Spiral
if var contents = spiralEntity?.model?.mesh.contents { contents.models = .init(contents.models.map { model in var newModel = model newModel.parts = .init(model.parts.map { part in let start = min(self.allIndices.count, max(0, numIndices - stripeSize)) let end = max(0, min(self.allIndices.count, numIndices)) var newPart = part newPart.triangleIndices = .init(self.allIndices[start..<end]) return newPart }) return newModel }) try? spiralEntity?.model?.mesh.replace(with: contents) }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。