3月24日、サイバーエージェントのエンジニア・クリエイターによる技術カンファレンス「CyberAgent Developer Conference 2022」を開催しました。本記事では矢野春樹による「Unity用エフェクトシェーダ「NOVA Shader」のOSS化事例と技術的特徴」の模様をお届けします。
目次
■NOVA Shaderについて
■NOVA Shaderの特徴的な機能および技術的特徴
■Custom Vertex Streamsとの連携
■テクスチャ配列と3Dテクスチャ
■ポストプロセスディストーション
■インラインサンプラーステート
■GPUインスタンシング
■まとめ
■NOVA Shaderについて
NOVA Shader – GitHub
https://github.com/CyberAgentGameEntertainment/NovaShader
NOVA Shaderは、無料で利用できるOSSとして公開しています。ライセンスはMITです。
NOVA Shaderの概要
NOVA Shaderは、UnityのParticle System用のシェーダで、エフェクト制作でよく使う機能をまとめた多機能な汎用シェーダとなっています。社内のエフェクトのクオリティの向上と、開発効率の向上を目的として作成しました。
エフェクトサンプル
下図は、実際にNOVA Shaderを使い、弊社のアーティストが作ったサンプルのエフェクトです。
例えばこのエフェクトでは、氷の透明感を出すために、モデルの法線の角度が手前を向いているような部分だけ透過させる機能や、削れるように消えていく部分、削れたエッジが光りながら消えていくといった部分などに今回のシェーダの機能が使われています。また、地面に近いところが透過するソフトパーティクルといった機能も使われています。
サンプルの2つ目は炎のエフェクトです。
火花がキラキラ発光している部分や、炎が流れている流体表現にこのシェーダが使われています。このエフェクトは、サンプルとしてGitHubにも上げているので是非ご覧ください。
■NOVA Shaderの特徴的な機能および技術的特徴
ここからは、NOVA Shaderの各機能および技術的特徴を解説します。
■Custom Vertex Streamsとの連携
一般的にエフェクトでは、色々なシェーダのプロパティに対して、複雑なアニメーションを自由につける必要があります。アーティストが、自分の好きなプロパティを自由にアニメーションをさせられる仕組みが必要となります。Unityではこれを実現するための機能として、Custom Vertex Streamsという機能があります。
これはParticle Systemの各モジュールで作成した値を頂点データとしてシェーダに送るという機能です。例えば、Custom Dataモジュールと連携すると、アニメーションカーブを使って作った値をシェーダに渡すことができます。これによって、アニメーションカーブで、アニメーションの仕方を指定できるようになります。
Custom Data以外との連携
Custom Data以外のモジュールと組み合わせることも可能です。例えば、各パーティクルの粒子ごとにランダムな値を渡すことが可能です。
また、Particle Systemのノイズモジュールを使うと、不規則に連続的な値を作ることができます。その連続的な値を、その回転の角度として変換するプロパティとして渡すことで不規則な動きを表現することができます。他にも、各パーティクルの現在の速度を渡したり、スケール値を渡したりといった使い方もできます。
シェーダを値で受け取る
このCustom Vertex Streamsでプロパティをアニメーションさせるためには、この値を反映するシェーダを書く必要があります。
シェーダでの受け取り方に関しては、下図のコードのように、基本的には頂点データにアクセスするだけで受け取ることができます。
ただ、このままだと頂点データに対して、それを適用するプロパティが固定化されてしまうという問題があります。この例でいうと、texcoord1はcolor1にしか適用できず、texcoord2はcolor2しか適用できないということになります。
汎用的に使うための工夫
これでは困るのでNOVA Shaderでは、各プロパティに対して、そのプロパティのアニメーションのために使用するtexcoordのインデックスと、Swizzleの値を指定できるようにしています。
この図は、texcoord1のZを使うという設定で選択しているところです。この仕組みにより、アーティストが、好きなプロパティを好きなようにアニメーションできるようになりました。
汎用的な実装
実装としてはまず、texcoordのインデックスとSwizzleの値を持ったプロパティを定義します。この図では、_Fooというプロパティを定義しています。それをシェーダでデコードし、texcoordの値をインデックスとSwizzleに変換して取得するという実装になっています。このようなプロパティを、アニメーション対象のプロパティに対して一つずつ定義します。
■テクスチャ配列と3Dテクスチャ
これらの機能はNOVA Shaderでは、さまざまな箇所で使用していますが、意外と使う機会がないので、今回は実例として紹介します。
テクスチャ配列とは何か
テクスチャ配列とは、複数枚の同じサイズのテクスチャが配列になって、ひとつのアセットにまとまったものと言えます。サンプリングをする際には、普通のテクスチャと同様にUV座標でサンプリングしますが、そこに配列のインデックスが加わり、何番目のテクスチャという値を指定してサンプリングするという仕組みになっています。これにより、指定したインデックスのテクスチャがサンプリングできます。
その結果、いわゆるパラパラアニメ、フリップブックと呼ばれていますが、この機能が実現します。
3Dテクスチャ
3Dテクスチャは、複数枚の同じサイズのテクスチャの配列という意味では、テクスチャ配列と同じですが、こちらは、テクスチャ配列のようにUV座標に加えてインデックスで指定するのではなくて、三次元の座標を使ってサンプリングします。
この座標のz値によって、何枚目のテクスチャと何枚目のテクスチャが使われるかということが決まります。さらにz値に応じて、その2枚のテクスチャが、どのぐらいの率でブレンドされるかが決まります。これを利用してCustom Vertex Streamsでz値をアニメーションさせると、滑らかに補完されたパラパラアニメ、フリップブックブレンディングという表現ができるようになります。
作成方法
次に、これらのアセットをUnityで作成する方法を説明します。
まず複数のテクスチャを、ひとつのテクスチャにまとめたテクスチャシートがあります。この図の右上にあるのは、4枚のテクスチャがひとつの画像に結合されたものです。こうした画像を用意し、そのインポート設定で、ColumnsとRowsの数を指定します。今回は2:2で指定しています。上図ではTexture Shapeは3Dとして指定しているので、3DテクスチャとしてImportされます。一方、テクスチャ配列にしたい場合には、Texture Shapeを2D Arrayに設定すると、テクスチャ配列としてインポートされます。
なお、この方法を使えるのはUnity2020.2以降ですので、それ以前はスクリプトで作る必要があります。
テクスチャ配列のサンプリング
次は、実際のコードでどのように、これらをサンプリングするのかを説明します。
テクスチャ配列については、インデックスでサンプリングするので、そのままだとテクスチャの枚数に応じてどんどんインデックスが増えていきます。
NOVA Shaderでは0から1の値を取るプロパティでフリップブックの進捗を制御したかったため、適切なインデックス値をシェーダで計算しています。コード内では、テクスチャの枚数を表す_SliceCountというプロパティを使って計算しています。
この_SliceCountに関しては、マテリアルにテクスチャ配列をアサインするときに、自動的にテクスチャのインポート設定から取ってきて設定されるという仕組みにしています。
3Dテクスチャのサンプリング
次に3Dテクスチャの場合です。
3Dテクスチャのz値は、0が最初のテクスチャを示して、1が最後のテクスチャ、ブレンディングが終わった最後のテクスチャを示します。
こちらも、このソースコードのような処理によって、進捗を0から1で表せる形で算出しています。
_SliceCountに関してはテクスチャ配列の時と同様にマテリアルに3Dテクスチャをアサインするときに自動的に設定される仕組みにしています。
使用箇所
このテクスチャ配列や3Dテクスチャは、基本はベーステクスチャのフリップブックアニメーションをさせるのに使っています。
他には、乗算用のテクスチャや、Tintカラーを決めるテクスチャ、フェード用のテクスチャにも使われています。あとは、ディゾルブやエミッションのテクスチャにも使われています。
■ポストプロセスディストーション
ディストーションとは何か
この図のように、空間を歪ませるような表現に使うエフェクトがディストーションです。熱波や水、氷といった屈折表現をするときに使うことを想定しています。これは、前身となったシェーダから、大きく実装を変更して改善した機能であり、今回工夫したポイントとなっています。
前身のディストーション
前身のディストーションシェーダはシンプルな実装で、GrabPassによって今のフレームバッファの値を取得した後、ディストーション用のオブジェクトを描画する際にフレームバッファの値を歪めるという実装でした。
しかし、この実装には、重ねがけができないという問題がありました。
これはどういうことかというと、一回目のディストーションをかけるときに、フレームバッファの値を使いますが、二回目の時も同じものを参照してしまうので、一回目の歪み効果が、その時点で二回目の効果によって上書きされてしまい、二回目の効果しか見れなくなってしまうということです。
実は、GrabPassを複数回行えば重ねがけ自体は可能ではありますが、処理負荷を考えると現実的ではなく、スケールする設計とは言えません。
なお、URPに実装されているパーティクルシェーダにもディストーションが実装されていますが、こちらは工夫されているため、多少軽減できる方法はあるものの、本質的には同じ理由で重ねがけが難しいという実装になっています。
ポストプロセスディストーションとは
この重ねがけの問題を解決するために、NOVA Shaderではポストエフェクトとしてディストーションをかけるという実装にしました。
手順としては、まずディストーション用のオブジェクトを描画する際に、それをフレームバッファに書き込むのではなく、歪みの情報を持つ専用のレンダーテクスチャに書き込みます。このレンダーテクスチャをディストーションマップと呼びます。ポストプロセスでこのディストーションマップをサンプリングして、ここに書き込まれた情報を基にフレームバッファを最後に歪ませるという仕組みになっています。
ディストーションマップ作成パス
URPはGrabPassを使用できないので、その代わりに半透明描画の後にディストーションマップを描画するためのパスを差し込みました。
ディストーションマップのシェーダ
ディストーションマップは、下図のようなシェーダによって描画されています。
ディストーション用のオブジェクトに設定されているテクスチャのR値とG値をUVのオフセット値として、ディストーションマップに書き込んでいます。ポイントとしては、ディストーションマップがあらかじめ0.5の値で初期化されていることと、ブレンドモードはオフセット値を加算で書き込んでいくという実装になっていることです。なお実際はさまざまな処理をしているので、フラグメントシェーダ自体は、もっと複雑なものになります。
ポストエフェクトパス
次にディストーションマップに書かれた情報を使って、ここまでのレンダリング結果を歪めていくポストエフェクトをかけていきます。URPでは、まだカスタムホストエフェクトが公式に実装はされていないので、専用のパスを作成する必要があります。
ポストエフェクトのシェーダのコード自体は、かなりシンプルです。ディストーションマップの値をデコードしてUVにオフセットをかけつつ、メインテクスチャをサンプリングするだけとなっています。
■インラインサンプラーステート
意外と使う機会がない機能ではありますが、実例として紹介します。
インラインサンプラーステートとは
インラインサンプラーステートとは、テクスチャをサンプリングする方法を指定するためのものです。具体的には、この図の赤枠の部分、つまりテクスチャインポート設定のWrap Mode、Filter Mode、Aniso Levelが、これにあたります。
インラインサンプラーステートを使うと、この設定をシェーダで上書きができるようになります。例えばこのコードのように指定すると、Wrap ModeをPointにして、Filter ModeをMirrorにしてテクスチャをサンプリングすることができます。
ミラーサンプリング
NOVA Shaderでは、この機能をテクスチャのミラーサンプリングの設定のために使っています。
ミラーサンプリングというチェックボックスにチェックを入れると、インラインサンプラーステートが使用されてFilterModeがMirrorに上書きされます。
■GPUインスタンシング
GPUインスタンシングとは、同じメッシュを持つオブジェクトを、1回のドローコールで描画することにより処理負荷を下げるという機能です。Particle SystemではRender ModeをMeshにし、Enable Mesh GPU Instancingにチェックを入れることで有効にできます。もちろん、シェーダがGPUインスタンシングに対応しているという必要があります。
インスタンスごとのデータ
GPUインスタンシングは複数のオブジェクトを描画する際に、共通するパラメータを使い回すことで描画を高速化するという手法です。
インスタンスごとの固有のデータに関しては、このコードで示しているようなインスタンス固有の構造体に定義する必要があります。コードの後半のように書くことで、この構造体から取り出すことができます。
Custom Vertex Streamsの場合
NOVA ShaderではCustom Vertex Streamsを使っています。これによって頂点データが渡されるという実装になっています。
このデータは、インスタンスごとに固有なものなので、customCoord1、customCoord1、といった形で定義する必要があります。GPUインスタンシングは、有効/無効を設定できるので、これが有効になっている場合のみ、こちらを使用するという実装に分離させる必要があります。
■まとめ
以上、NOVA Shader の開発の経緯とOSS化の狙いについて紹介しました。また、技術的特徴として、Custom Vertex Streams、3Dテクスチャのサンプリング、ポストプロセスディストーション、インラインサンプラーステート、GPUインスタンシングといった、実際にNOVA Shaderの実装に使われている技術の解説をお届けしました。
「CyberAgent Developer Conference 2022」のアーカイブ動画・登壇資料は公式サイトにて公開しています。ぜひご覧ください。
https://cadc.cyberagent.co.jp/2022/
■採用情報
新卒採用:https://www.cyberagent.co.jp/careers/special/students/tech/?ver=2023-1.0.0