CA.unityはサイバーエージェントが運営するUnityをテーマにした勉強会です。サイバーエージェントのサービス開発者と社外の開発者を交えて、Unityに関する知見を共有する場となります。CA.unity #9では、ユニティ・テクノロジーズ・ジャパン株式会社様とサイバーエージェントが登壇いたします。
※CA.unityは、Unity Technologiesまたはその関連会社がスポンサーとなっているものでも、提携しているものでもありません。Unityは、米国およびその他の国におけるUnity Technologiesまたはその関連会社の商標または登録商標です。
本記事は、2025年01月08(水)に開催した「CA.unity #9」において発表された「Unity6の新機能 STPについての話」に対して、社内の生成AI議事録ツール「コエログ」を活用して書き起こし、登壇者本人が監修役として加筆修正しました。
張煜冰(株式会社サイバーエージェント グラフィックエンジニア)
2022年株式会社サイバーエージェントに中途入社し、コア技術本部に所属。主にUnityに使うグラフィックライブラリの開発を行い、各開発プロジェクトにサポーターを行う仕事を担当しています。
株式会社サイバーエージェントの張煜冰と申します。本日はUnity6からの新機能、STPという機能についてお話しさせていただきます。
今日はSTPについて、上記の順番でご紹介していきたいと思います。まずは機能紹介から始めさせていただきます。
STPはUnity6から正式に実装された新しいUpScaling機能で、モバイル端末向けに設計されています。正式名称はSpatial-Temporal Post-processingです。Spatial Temporalを直訳すると「空間的、時間的」となり、どういう機能なのかが分かないと思うので、周辺のピクセル情報と前のフレームのピクセル情報を用いてUpScalingをする機能だと解釈すればわかりやすくなったのではないでしょうか
そして、読み取る情報としては、ポストエフェクトの作りでおなじみのColorBuffer、DepthBuffer、NormalBuffer、およびMotionVectorが利用されます。また、STPについては右下に公式の紹介動画とE-bookもありますので、STPやUnity6の他の新機能にご興味のある方は、ぜひそちらもご覧ください。
次は、STPを有効にする手順をご紹介します。まず、大前提としてRenderGraphを有効にする必要があります。Unity6で新規プロジェクトを作成する場合、RenderGraphはデフォルトで有効になっていますが、旧Unityバージョンからアップデートしてきた場合は互換性モードになっている可能性があるため、そのチェックを外してRenderGraphに切り替える必要があります。
その設定場所は、Project SettingsのGraphics ページの一番下にあります。
次は、RenderPipelineAssetのQuality項目にあるRenderScaleを1以下に設定し、UpScalingフィルターをSTPに指定すると、STPが有効になります。
ここで注意すべきことがあります。STPは、RenderScaleが1より大きい数字に設定されてると他のUpScaling機能と同様に無効になりますが、RenderScaleがちょうど1の時には有効のままになります。STPをきちんと無効にしたい場合は、UpScalingFilterの項目を変える必要があります。
次は、STPの特徴についてご紹介します。まず一番の特徴といえば、その素晴らしい効果だと思います。まず、動かない画面をご覧ください。
左側は4Kフル解像度でレンダリングしたものです。右側は、4KでRenderScaleを0.25に設定し、ピクセル数で言えば16分の1に下げた状態から、STPによってUpScalingされたものです。細かく比較してみると、右側が多少ボケているように見えますが、細かく比較しなければ、これがRenderScale 0.25からUpScalingされたものだと分からないと思います。
そして、同じ場面でUnityに実装されていたFSR 1.0と比較してみると、右側に表示されているFSRでは明らかにジグザグが見えているのが分かります。一方で、STPの品質は圧倒的に優れていると言っても過言ではないでしょう。
さらに、オリジナルの解像度を1080pまで下げて比較してみます。1080pのRenderScaleを0.25に設定し、描画解像度がかなり小さくなったにも関わらず、STP処理後の結果はまだまだ綺麗です。
次に、画面が動く際の効果をご覧いただきましょう。
画面が動くというのは、カメラ自身が動く場合とオブジェクトが動く場合の2パターンがあります。STPはこれらの2パターンに対して、効果に若干の差があります。まずは、カメラ自身が動く場合の動画をご覧ください。
左上の数字がRenderScaleです。ご覧の通り、RenderScaleを0.25まで下げても、カメラが激しく動いている状況下で画質の劣化が特に感じられなかったはずです。
先ほどの動画から各RenderScaleの1フレームを切り出して比較してみます。最初にお見せした静止状態と比べると、画面が多少ボケているように感じられますが、テンポラルな処理でよく見られるゴースティング現象がほぼ発生しておらず、カメラが激しく動いている場合でも、STPの効果は依然として素晴らしいといえるでしょう。
次に、オブジェクトが動く場合の動画をご覧いただきます。ここでは動くボールを表示しています。
はい、いかがでしょうか。RenderScaleが0.25になっても、画質の劣化はそれほど目立ないが、先ほどのカメラが動く動画と比べると多少劣化が見えやすくなった印象はあります。
同じく1フレームを切り出して比較してみます。右側の画像をご覧いただければ分かると思いますが、特にオブジェクトのエッジ部分や落ち影のエッジ部分などの劣化が目立ちます。しかし、非常に低い解像度からUpScalingされた結果としては、まだまだきれいだとも言えるでしょう。
最後に、シーンに配置されるUIに対する効果を見ていきます。
オーバーレイのキャンバスUIは、STPの効果を受けないため、ワールドスペースおよびスクリーンスペースのキャンバスでUIを表示します。結果は非常にわかりやすく、カメラが動く際のUIにおいて、かなりのゴースティングが発生しているのが確認できます。
それは、STPがUI向けに設計されていないためです。UIを描画する際、DepthBuffer、NormalBuffer、MotionVectorに何も書き込まれないので、STPの処理に必要な4つの情報のうち3つも揃ってないのがSTP効果の低下の原因となっています。STPがUI描画に向いてない弱点を克服するための策を後の節で改めてご紹介します。
ここで一旦、STPの効果についてまとめます。静的な画面およびカメラのみが動く場合の結果は、非常に綺麗に仕上がっています。オブジェクトが動く場合は、前述の状況よりは劣化するものの、まだまだそこそこ綺麗です。一方、UIに対する効果はかなり悪く、動く際にゴースティングが顕著に発生しているため、UIにSTPを適用するべきではないと考えられます。
はい、次はSTPによる性能向上についてお話しします。この表をご覧ください。まず1列目は、Macで4Kで撮ったフレームレートのデータになります。RenderScaleを0.5にすると、フレームレートが50ほど上がり、0.25にするとフレームレートが倍ほどに跳ね上がりました。性能向上の効果は結構素晴らしいといえるでしょう。
2列目は、スペックが低いPixel 4aで取ったデータになります。スペックが低いAndroid端末でも、RenderScaleを0.3ぐらいまで下げれば性能向上が確認できています。ここで注意する点ですが、STP自体にも一定の負荷がかかっているため、RenderScaleを少し下げた場合、パフォーマンスが下がってしまうこともあります。特にPixel 4aのようなロースペック端末では、RenderScaleを0.3ぐらいまで下げないと性能向上は見込めません。
というわけで、とりあえずRenderScaleは0.5に設定するのが無難かと思います。特にロースペックの端末では、さらに下げるべきでしょう。もしRenderScaleを0.5以上に設定する場合、STPを使った性能向上の効果がそもそも薄くなり、場合によっては逆効果になることもあります。その場合はSTPを使わない方が良いかと思います。
次は、STPを使う際の注意事項についてお話しさせていただきます。
まず、STPの実行タイミングについてです。STPはURPのポストプロセッシングの一つで、before-post-processingとafter-post-processingの間に実行されます。その中で他のポストプロセッシングも実行されていまして、STPはDepth of Fieldなど、DepthやNormal情報が必要なポストプロセッシングの後に実行されます。そして、STPが実行された直後にモーションブラーの処理が走ります。
その後、Bloomなど、DepthやNormal情報を必要としないポストプロセッシングが実行されます。STPの実行後のタイミングにRenderScaleが1に戻ります。STPの後に実行される処理は、性能向上の恩恵を受けないため、STPの効果を発揮させたい場合は、重い処理をできるだけSTPの前に実行させるべきです。逆に、STPの効果を受けたくない処理は、STPの後に実行されるようにしてください。
次は STP の使用制限について話していきたいと思います。
STPは非常に強力な機能ですが、いくつかの使用制限があります。まず、端末がCompute Shaderをサポートする必要があります。STPは全てCompute Shaderで実行されるため、Compute Shaderがサポートされていない端末では動作しません。
また、他の各種ポスト系アンチエイリアス機能との併用はできません。STPを有効にした場合、他のポスト系アンチエイリアス機能は自動的に無効となります。この点については、STPの最後にTAAの処理が入っているため、特に問題にはなっていません。
さらに、STPにはTAAの処理が含まれているため、TAAが使えない場合はSTPも使用できません。例えば、MSAAとの併用はできず、CameraStackとの併用もできません。CameraStackとの併用ができないことで、STPが活用できる場面がかなり制限されるという課題があります。
この課題をなんとか解決する方法もあるので、後ほどその解決策についてもご紹介いたします。
では早速その実装の事例に入りたいと思います。
まずは、その実装事例での要望についてご紹介します。実装事例では、UIを描画する際に、UIがRenderScaleの影響を受けず、フル解像度で描画されることが求められています。また、UIにポストプロセッシングを適用したくないという要望もあります。ここまでは、オーバーレイのCanvasを使用することで解決可能です。
さらに、次の2点についても要望があります。まず、UIをGammaSpaceで描画することです。GammaSpaceで描画する自体はSTPとは関係ありませんが、それを自作のRendererFeatureで動作させる必要があるため、オーバーレイのCanvasが利用できなくなります。最後に、UIが3Dオブジェクトに遮蔽される、つまり深度テストが必要になるという要望です。
その後ろの2つを含めると、オーバーレイのCanvasが使えなくなり、スクリーンスペースやカメラのCanvasを使うしかなくなります。本来であればCameraStackを用いればそれが実装可能ですが、STPを使用するとCameraStackが無効になるため、自作のRendererFeatureで描画することで解決する必要があります。
この自作のRendererの役割の一つは、オブジェクトの描画タイミングを制御することです。UnityにはRender ObjectsというRendererFeaturesがあり、それも描画タイミングを制御することが可能ですが、STPと併用する場合、Render Objectsには2つの問題があり、残念ながらそのままでは使うことができません。
まず、UIにポストプロセッシングを適用しないという要望を実現するためには、UIをSTPの後に描画させる必要がありますね。
そこで、STPが有効になっている場合にRender Objectsを使おうとすると、画面が真っ黒になり、エラーが出てきます。そのエラーの原因は、描画を実行する際に描画ターゲットに設定されているColorBufferとDepthBufferのサイズが異なるためです。それが一つ目の問題です。
そこで、なぜColor BufferとDepth Bufferのサイズが異なるかというと、STP実行前後でRenderScaleが変わるからです。カメラのDepth Bufferは、STP実行前にRenderScaleが下がっている状態で生成されるため、フル解像度ではありません。STPが実行された後にRenderScaleが1に戻り、その時のカメラのColorBufferがフル解像度のため、ColorBufferとDepth Bufferのサイズが一致しなくなります。
この問題を解決するために、フル解像度でもう一度Depth Bufferを自前で作成する必要があります。自前でDepth Bufferを作ることで、この問題を解決しました。
この動画のようにUIを描画することはできたのですが、自前で描画したUIがかなり揺れているのが分かります。これはもう一つの問題です。STPの処理は、周辺のピクセル情報を効率的に取得するために、カメラのプロジェクションマトリックスにジッターを入れて画面を揺らしています。STPが処理を行う際にはその揺れが修正されますが、STP処理後に描画されるUIはその修正を受けないため、揺れるわけです。
その問題を解決するために、自前でUIを描画する前に、カメラのプロジェクションマトリックスからジッターを除去しました。ジッターを除去した結果、この動画のようにUIが正しく描画されるようになりました。
こうすることで、深度テストが有効な状態で、なおかつポストプロセッシングを受けず、フル解像度でUIを描画することができました。
また、UIに限らず、同じ方法で任意のオブジェクトをSTPの実行後に描画することも可能です。例えば、STPの効果を活かしつつ、重要なオブジェクトだけをフル解像度で描画したい場合に有用です。
最後にまとめです。STPの強みを申し上げますと、一番の強みはもちろんその素晴らしいUpScalingの品質です。そして、ハード依存度が低い点も魅力です。Compute Shaderさえサポートしていれば、どのハードでも動作させることができます。
また、処理負荷が比較的軽い点もポイントです。DLSSやFSRなどは、もともとPCやコンソール向けに設計されており、モバイル端末では負荷が大きくなる場合があります。それに対して、STPはモバイル向けに設計されているため、処理負荷が比較的軽くなっています。
一方、弱みとしては、Compute Shaderがサポートされていない古いAndroid端末では使用できない点が挙げられます。
また、UIの描画には向いていないため、UIの描画はSTP実行後に行うことを強くお勧めします。最後に、CameraStackとの併用ができないという点もありますが、先ほどご紹介したように自作のRendererFeatureを作成することで、この問題は解決可能です。
最後になります。今回は時間の関係上、実装の詳細には踏み込めませんでしたが、自作のRendererFeatureを実装する際に必要な要素をまとめました。
1つ目は描画タイミングの制御、2つ目はDepthBufferの作成、3つ目はカメラマトリックスからジッターを除去することです。この3つの機能を実装すれば、STPの弱点を克服できます。
なお、Unity6からのRenderGraphを用いたRendererFeatureおよびRenderPassの実装方法については、コアテクブログにて自分が執筆した3つの記事がありますので、ぜひご参考になさってください。
以上、STPについてのご紹介でした。STPは非常に強力な機能ですので、実際の開発でUnity6をご利用の際には、ぜひSTPもお試しください。ご清聴ありがとうございました。