Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

Unityの描画パイプラインをURPに移行しました

こんにちは、ミラティブUnityエンジニアの牧野です。

ミラティブアプリの3Dアバター(エモモ)の描画にはUnityを利用しており、その描画パイプラインをURPに移行した内容を紹介していきます。

はじめに

URP (Universal Render Pipeline) とは以下のような特徴を持った、描画パイプラインの公式テンプレートです。

  • モバイル端末や家庭用ゲーム機、PCなど幅広いプラットフォームに最適化されている
  • ShaderGraphやPost-processingなど、アーティスト向けのワークフローを提供している

learning.unity3d.jp

移行前はビルトインレンダーパイプラインでしたが、以下のような理由でURPに移行ことにしました。

  • Unity本体のバージョンと独立して管理できるため、本体更新によって見た目が影響を受けるのを抑えられる
  • 内部実装が確認でき、必要に応じてカスタムもできる
  • シェーダーの作り次第では別マテリアルでも同じSetPassになり描画関連のCPU負荷軽減になる
    • エモモの使用シェーダーは絞られているがマテリアル数が多いのでそれなりに効果が出そう
  • ShaderGraphによってデザイナ側がシェーダーを作る敷居が下がる

移行の流れ

  1. UniversalRenderPipelineAssetを作成しGraphics設定でそれを指定
  2. ビルトインシェーダーはメニューからコンバート
  3. 必要に応じてRendererFeatureを追加
  4. 内製シェーダーの対応
  5. Prefabやスクリプトの対応

RendererFeatureの追加

URPではビルトインレンダーパイプラインのようにマルチパスを記述することはできないため、RendererFeatureを追加してそこからパスを呼ぶ設定をする必要があります。
ミラティブのUnityプロジェクトではアウトラインの表示のために3つのRendererFeatureを追加しています。

  • 不透明アウトライン描画
  • 半透明アウトライン用ステンシル描画
  • 半透明アウトライン描画

f:id:bosakana:20220407172234p:plain:w400

内製シェーダーの対応

URPを含むSRP (Scriptable Render Pipeline) では、シェーダーを適切に書けば描画関連のCPU負荷の軽減を見込める SRP Batcher が利用できます。 ビルトインレンダーパイプラインでは設定値の異なるマテリアルはSetPassが別に呼ばれていましたが、SRP Batcher が効けば同一のシェーダーの設定値が異なるマテリアルはSetPassが一度で済みます。
DrawCallのバッチングとは別なのでDrawCallはそれぞれ発生しますが、近年のハードではDrawCallのオーバーヘッドよりもメッシュを動的に結合する方がコストが高くなったりするので現在のUnityバージョンではメッシュの動的バッチングはデフォルトでOFFになっています。

blog.unity.com

ミラティブアプリではギフト演出の背景などの一部でStandard等のビルトインシェーダーを利用していますが、それ以外のほとんどのマテリアルでは内製のシェーダーを使っているので、それらをバッチング処理が効くように書き換えます。

CGPROGRAM を HLSLPROGRAM に変え、Unity本体依存の UnityCG.cginc や Lighting.cgin などのinclude部分をURPパッケージ内の Core.hlsl や Lighting.hlsl を利用するように変更します。 それに伴って利用する関数を変更する必要がありますが、UnityObjectToClipPos() → TransformObjectToHClip() などほとんどのものは単純な置き換えで済みます。
しかし置き換えだけでは再現できなくなる部分もあり注意が必要です。
例えばライト情報取得で _WorldSpaceLightPos0 / _LightColor0 を使っていた部分は _MainLightPosition / _MainLightColor または GetMainLight()で取得するライトで代替可能と紹介されていたりするのですが、場合によっては挙動が異なります。オブジェクトに適用されるライトをレイヤーで分けている場合、ビルトインレンダーパイプラインでは各レイヤーを対象に設定しているライトが取得できたのですが、URPではレイヤーに関わらずSun Sourceで指定しているライト(指定していない場合は最も強いディレクショナルライト)が取得されます。そのため GetAdditionalLight()で目的のライト情報を取ってくるなどする必要がありました。

続いて、シェーダー内で使っているすべてのプロパティをPropertiesブロックに定義し、さらに UnityPerMaterial という名前のCBufferに含めます。

以上の対応で、下の画像のようにシェーダーのインスペクタの SRP Batcher が compatible になっていればバッチングが効きます。

f:id:bosakana:20220406132432p:plain:w300

Frame Debuggerで確認

Frame Debugger で実行時のフレームをキャプチャすることで実際にSetPassがまとまっているか確認できます。

ミラティブアプリのUnity上のフレームキャプチャでもこの時点でSetPassが減っていることが確認できました。
しかし、同じシェーダーのマテリアルでも全てがバッチングされているわけではなく、いくつかに分かれている状態になっていました。

バッチングされなかった理由は Frame Debugger が教えてくれます。

f:id:bosakana:20220406132518p:plain:w400

SRP: Node use different shader keywords

そう、異なるシェーダーキーワードが定義されているとバッチングされないのです。

インスペクタをデバッグモードに変更することで、マテリアルが持っているシェーダーキーワードを確認することができます。

f:id:bosakana:20220406132536p:plain:w400

シェーダーで処理を分けるために以下のようにプロパティに [Toggle] 属性を使っていたりします。

Properties {
    [Toggle] _UseSpecular("Use Outline Control",Float) = 0
    ...

しかし プロパティに [Toggle] をつけているとシェーダーキーワードが自動でセットされてしまうので [Enum] に変更してセットされないようにします。

Properties {
    [Enum(Off, 0, On, 1)] _UseSpecular("Use Outline Control",Float) = 0
    ...

また、シェーダーを変更して使わなくなったシェーダーキーワードもマテリアルに残り続けます。昔のシェーダーキーワードが残っていてバッチングを阻害するマテリアルも多かったのでそれらも整理しました。 長く運用をしているUnityプロジェクトだと発生しうると思うので移行時には注意するポイントですね。

これらの変更によって ミラティブのUnityプロジェクトでは下の画像のようなアイテムもりもりエモモの場合、SetPassが 104 → 38 になりました。背景オブジェクトが多いギフト演出や 複数エモモが表示されるグループショットではさらに効果を発揮してくれます。

f:id:bosakana:20220406180555p:plain:w400

負荷の確認

UnityEngin.Profiling.Recorderを利用してスクリプト内で測定することができます。

SRP Batcher OFF
f:id:bosakana:20220406184135p:plain:w500
SRP Batcher ON
f:id:bosakana:20220405144232p:plain:w500

実機での確認は、ミラティブアプリはUnity as a Libraryを使っている ためUnityのプロファイラを使えず、XcodeやAndroid Studioのプロファイラで確認しました。XcodeのTimeProfilerでは UnityFramework内の各処理にかかる時間が以下のように細かく確認できます。

f:id:bosakana:20220406185619p:plain

端末の発熱等の状態によって結果が変動するので比較対象を交互に測定したり長めの時間の平均を測定します。 結果としては、アイテムを多くつけたバッチングの効果が出やすい場合のエモモでは、その描画関連のCPU負荷を40%近く削減できました

Prefabやスクリプトの対応

URP移行したことで、シェーダーだけでなくPrefabやスクリプトを修正しなければならない部分もあります。

複数のカメラを重ねたい場合、ビルトインレンダーパイプラインではカメラコンポーネントのDepthの値で重ねる順番を制御していましたが、URPではひとつのカメラのRenderTypeをBaseにして重ねるカメラをOverlayにしてBaseカメラにStackすることで実現します。

OnPreRender()やOnPostRender()が使えなくなっています。スクリーンキャプチャなどを実装する時に OnPostRender()でTexture2D.ReadPixels()をしてキャプチャしていた場合はできなくなるので、URPでは RenderPipelineManager.endCameraRendering に呼び出したい関数を追加することで実現できます。

OnRenderImage() も使えなくなっており、ビルトインレンダーパイプラインで独自のポストエフェクトを実装する時にはこれを利用しましたが、URPでは RendererFeatureを追加して実装する必要があります。

遭遇した細かい問題

アセットバンドルにしたFBXモデルをロードしてくると2番目以降のUVデータが消えている、という問題に遭遇しました。原因はFBXのSerializedVersionが古いためで、これを更新することで解決しました。Unityバージョン更新ではなくURP移行でこのような問題が発生したのは意外でした。

問題が発生したFBXのSerializedVersion 更新後のSerializedVersion
23 2020

おわりに

運用中で既存のリソースが膨大だと確認するコストもあり、レンダーパイプラインを切り替えるような影響範囲の大きい変更はなかなか気を使います。今回の移行作業中も新規アセット開発が並行で進んでいるため非対応シェーダーが使われてQAに乗ってしまったりが起こりました。それでも負荷軽減ができたり、シェーダーを作る敷居が下がって表現の幅を広げられたりと確実によくなったと思います。

  

We are hiring!

ミラティブでは一緒にアプリを作ってくれるエンジニアを募集しております!

www.mirrativ.co.jp