[Unity] URP 対応のシェーダを書いて、軽やかで色鮮やかな世界を作る方法

2020-09-04

これは何

趣味で作ろうと思っているゲームがあって、最近はその絵作りのためのシェーダを書いたり、 レンダリングまわりを研究したりしている。

最近、コツコツ作っていたものがまとまってきて、自分のイメージしていたものが出来上がってきたので、 シェーダのコードとともに公開しておこうと思う。 このシェーダは Unity 向けで、比較的新しめの技術である URP (Universal Rendering Pipeline) で動作する。 URP の情報はまだ世に少ないので、URP に興味がある人の参考になれば幸いだ。

サンプルとして明るいイメージと暗めのイメージで 2 つの 3D マップを作って動画にしてみたので、 まずはこれを見てほしい:

動画で使っているシェーダのソースコードは GitHub に上げてある:

作ったシェーダの特徴

ローポリの 3D ゲームを 1 人で作ろうと思ったときに、 「手軽にそれっぽい見た目が得られて、負荷が軽くモバイルでも快適に動く」 という要件の絵作りが必要だった。そこで今回、以下のようなシェーダを作った:

  • Unity が今後新しい標準にしようとしている URP で動作する
    • (Unity 2019 LTS 版で標準で入っている URP 7.3.1 を使用)
  • シェーダを分けず、1 つのシェーダのパラメータで色々できるようにしている
    • ※ URP ではシェーダ単位でバッチングが行われるので、 シェーダを分けない(バリアントを作らない)方が描画効率が上がりやすい
  • シェーダのパラメータだけで各方向からの色味とグラデーションを当てられる
    • テクスチャや AO、ポイントライトを使わずに済むので描画負荷・メモリ消費が軽くなる


テクスチャやポイントライトを使わずにそれらしい効果を得る、というのがポイントだ。

以下の画像の 1 枚目は Unity デフォルトのシェーダを単に当てたものでちょっとつまらない見た目だが、 この画面でシェーダ「だけ」を今回作ったものに差し替えると 2 枚目のような見た目が得られる。

Unity デフォルトのシェーダを当てたもの

Unity デフォルトのシェーダを当てたもの

同じ画面でシェーダ(マテリアル)だけを差し替えたもの

同じ画面でシェーダ(マテリアル)だけを差し替えたもの

シェーダひとつでなかなか雰囲気が変わるものだ。

URP とは何か

ここで Unity URP の技術情報について整理しておこう:

  • まず、Unity 2018 あたりから Scriptable Render Pipeline (SRP) という機能が登場してきた
  • で、とは言え誰しもがイチから Render Pipeline を書いたりしないので、 Unity が公式で組み込みの SRP を 2 つ用意した
    • それがハイエンド向け SRP である HDRP と、モバイルをはじめ広い用途で使える URP
    • URP はかつては LWRP と呼ばれていたが、Unity 2019.3 から URP に名称が変更された

なぜ URP で作ったか

仕事で使うにはまだちょっと不安な URP だが、趣味開発ということでチャレンジしても良いかなと思い、 作るゲームは URP の Unity プロジェクトで開発することにした。 単純に描画負荷の軽減が見込めるというところが魅力だし、 どうせ今後標準になるなら先立って技術をキャッチアップしておこうというモチベーションがあった。

描画負荷に関しては

  • ポストプロセス (Bloom などの視覚効果) の処理がパイプラインに直接組み込まれている
  • ポイントライトの計算を 1 パスで行える

なども良さそうだったが、一番「おお、これは」と思ったのが 「シェーダ単位でバッチングが行われる」 という SRP Batcher の機能だった。

ゲームの描画処理においては、シェーダのパラメータ(マテリアルのプロパティ)を CPU から GPU に転送する処理 (SetPass Call) が必要で、これがそれなりのオーバヘッドとなる。 (※ そのパラメータを使って、じゃあ描画してねと命令するのが Draw Call)

従来の Built-in RP では、この SetPass Call のバッチングが同じマテリアル内でしか適用されなかった。 これが URP では同じシェーダバリアントであればバッチングして、SetPass Call を増やさずに描画してくれるというのである。

このあたりの技術的詳細を知るには以下の公式記事を読むのが良い:

上記の記事にも書かれているが、「シェーダは同じだがマテリアルが違う」 というケースはゲーム画面ではよく起こる。 同じメッシュを並べて、それぞれ色を変えたい、みたいな例がそうだ。 そのためシェーダの数を増やさないように気をつければ、URP では多くの場面で描画負荷の改善が見込めるというわけだ。

今回作ろうとしていたシェーダも、パラメータで色を多様に変えたいようなものだったので、都合がよかった。

自作シェーダは何をやっているか

色々な軽めのシェーディング効果を、パラメータで組み合わせて使えるようにしてある。 1 個 1 個の処理はよくありそうなもので、大したことはやっていないと思う。

  • 面の方向に応じた色の指定
  • 陰影のコントラストの強さ
  • テクスチャとのブレンド
  • リムライティング
  • リアルタイムシャドウに色味をつける
  • 距離に応じた多色のフォグ
  • 高さに応じたフォグ(Height Fog)
  • HSV (色相 / 彩度 / 明度)の調整

中でも絵作りのキモになっているのが、「面の法線方向に応じて色をつける」 という処理だ。 左右・前後・上下の 6 方向に色を割り当てることができる。 この手法に確立された名前があるかは知らないので、個人的には Cubic Color と呼んでいる。

6 方向の色をグラデーションで指定可能

6 方向の色をグラデーションで指定可能

実装は以下のような感じ:

面の方向(法線ベクトル)というのはシェーダで扱うごく基本的な要素なので、発想自体は珍しくないかと思う。 例えば KAYAC 社が開発した Kamakura Shaders にも Cube Color という似たようなオプションがある。


「面の方向に色をつける」表現の効用

自分のこれまでの記憶を辿って、自分が「綺麗だな」「幻想的だな」「整っているな」「雰囲気あるな」 と感じた絵や風景を思い返すと、

  • 相性の良い色どうしのグラデーションがある
  • 床と壁でコントラストのある色が組み合わさっている
  • 壁が色味を持った間接照明で照らされている
  • アクセントカラーや差し色が効果的に使われている
  • 影が単なる黒ではなく補色などの色を持っている
  • まわりの色が反射する / ふもとや隅が暗くなっているなど、物理的な説得力や一貫性がある

など色々な要素が思い起こされるが、こうした要素を表現するのに 「面の方向に応じた色をつける」 という処理は結構使える道具だな、 というのがシェーダを実装していて思ったことだ。

そもそも現実世界でも

  • 横から夕焼けの光が当たっている
  • 光の当たらない面にコケが生えている

など、特定の面に特定の色がつく、という現象はあるもので、 面の方向で色をつけるだけでも意外と説得力のある絵が作れるものなのだ。

各面の色はそれぞれ 2 色のグラデーションが指定可能で、 グラデーションの位置や傾きもパラメータである程度は調整できるようにしてあるので、 パラメータの組み合わせ方次第では思ったよりもイイ感じの絵が作れたりする。

以下は、山のモデルに自作シェーダを当てて色味を調整した例。 1 枚目が Unity デフォルトのシェーダを単に当てたもので、2 枚目がシェーダだけを差し替えたものだ:

Unity デフォルトのシェーダ

Unity デフォルトのシェーダ

自作シェーダ

自作シェーダ

1 枚目はいかにもプロトタイプという見た目だが、2 枚目はそのまま実用に耐えそうな山に見えるのではなかろうか。 こうした見た目がテクスチャやポイントライトを使わずに作れるというのは、なかなか良い。

テクスチャをいじらなくて良いことの楽さ

当然ながらこのような見た目を作るにはパラメータの細かい調整が必要にはなるが、 それがシェーダのパラメータだけで調整できるというところに優位性がある。

ちなみに実際に色味を調整している様子は、以下のような感じ:

これをテクスチャでやろうとすると結構大変そうに思える。
(少なくとも Unity の標準機能だけで完結する作業ではなくなる)

まあグラデーションの組み合わせだけでの表現になるので実現できるアートスタイルは限られてくるが、 パラメータだけで色を気軽に調整できるのは開発効率の観点でメリットが大きそうだ。

URP のシェーダをどう実装したか

URP に関して Unity 公式のドキュメントは用意されているが、 内容は基本的な概念や使い方の範囲に留まっている:

そのため初めは 「URP 向けのシェーダを自作するにはどうしたら良いか」 がよくわからなかった。 新しい技術を学ぼうとすると、まず学び方を探すところから始めなければいけない。

2020-09 の現在では、前よりも URP に関する記事が検索でヒットするようになってきたが、 自分が URP に取り組み始めた半年前くらいには、かなり情報が少なかった。

※ これから URP のシェーダを書き始める人には、このあたりの解説記事が役に立ちそうだ:

また、以下の記事では既存のシェーダから URP 向けへの移行の仕方をまとめてくれている。こちらも素晴らしい記事だ:


ネットの海を漂っていると大なり小なり URP に関する記事やサンプルコードを見つけることもできたが、 過渡期の技術なので、できるだけ一次情報に近いものから知識を得たかった。 一番良いのは、URP のプロジェクトに実際に組み込まれているビルトインのシェーダのソースコードを読み、 それをいじって動作を確認しながら理解することだろうなと考えた。

これまでのビルトインシェーダのように、URP 向けのシェーダのソースコードもどこかからダウンロードできるのかな? と探してみると、そもそも SRP 自体のソースコードが GitHub 上にあった

これで足がかりが得られたので、ここから始めていくことにした。

以下は、ビルトインの SimpleLit シェーダの主要コードを抜き出して、 自分で修正可能なようにライティングの関数名を置き換えたものだ:

これはそのままカスタムの URP シェーダを自作する時のテンプレートとして利用することができるので、 URP シェーダを自作する人は参考にしてもらえればと思う。

この大枠を利用してしまえば、あとは実装方法や文法などは従来のシェーダの書き方と変わらなかった。 (余談だが、URP のシェーダコードは実装が綺麗で、読んで理解しやすく勉強になった)

SRP Batcher の対応方法について

SRP Batcher が良さそうという話を上のほうで書いたが、 自分が作るシェーダもちゃんと SRP Batcher に対応したものにしなければならない。

これについては、具体的な対応方法や有効性の確認方法・プロファイリング方法を詳しくまとめてくれている素敵な記事があったので、 僕のほうでわざわざ書くことが無い。以下の記事を読んでもらえたらよい:

SRP Batcher をできるだけ効かせるには、できるだけ描画を同じシェーダで統一すればよい。 ここで注意すべきは、シェーダで使用されるキーワードが変わる (例えばリアルタイムシャドウの有無が変わる) と、 別のシェーダとしてビルドされるために、同じシェーダとは見なされなくなるということである。 これは避けたい。

具体例を示そう。シェーダの処理をパラメータで分岐する際、 従来では以下のようなプリプロセッサで分岐する (このコードがあるもの / 無いものとしてそれぞれ別々のシェーダにビルドする) という方法がよくとられていたと思う:

#ifdef _RimLightingOn
    finalColor += RimLight(inputData, rimColor);
#endif

URP においては、以下のように if 文で分岐してしまってシェーダ自体は同じものとしておいた方が SRP Batcher を効かせるという点では有利になる:

UNITY_BRANCH
if (_RimLightingOn > 0)
{
    finalColor += RimLight(inputData, rimColor);
}

そのため今回は後者のような書き方でシェーダの実装を統一した。

シェーダにおいては、if 文による分岐はパフォーマンスを落としやすいと言われている。 これはシェーダが GPU 上で「同じ処理を並列で一斉にやる」ような性質のものであるため、 処理の内容が変わると処理を切り替えるためのオーバヘッドが生じるということだろう。

自分の理解では、上記のようにシェーダのパラメータで処理の ON / OFF を切り替えるもの (各フラグメントで ON / OFF が個別に変わらないもの) であれば、if 文によるパフォーマンスの悪化は大きくなさそうだと思っている。 そのため、総合的に URP シェーダでは if 文で機能の ON / OFF をするのが良いと判断した。

カスタムのシェーダ GUI の実装

今回、インスペクタに表示されるシェーダの GUI も自前で実装している。

自作のシェーダ GUI

自作のシェーダ GUI

シェーダのエディタは、以下のような記述でクラス名を指定することで差し替えられる:

Shader "Altotascal/URP 7.3.1/Cubic Color"
{
    Properties
    {
       ...
    }
    SubShader
    {
       ...
    }
    CustomEditor "AltoLib.ShaderGUI.CubicColorGUI"
}

Properties に並べるだけでも Unity は自動でエディタを表示してくれるのだが、 今回テンプレートとした URP の SimpleLit シェーダは、もともとカスタムエディタで何やかや処理をしていたので、 エディタに関してもそれをベースにして拡張するのが良さそう (というかそうしないと逆に面倒そう) だと考え、横着せずに書いた。

僕が書いた GUI のコードはこのあたり:

  • CubicColorGUI.cs
    • Unity のエディタ実装はわりと上から下に素朴に書く感じの仕事になるが、 結構しんどかったのでリフレクションを使って記述量を減らすユーティリティを書いて楽をしたりなどしている:
    • ShaderGUIUtil.cs

おわりに

ということで何やらマニアックな技術記事になってしまったが、 試行錯誤の末に自分の欲しかったものがひとつ作れて、一歩前進した感じがして嬉しい。

実際にパフォーマンスも良好で、冒頭の動画で示したくらいの画面なら中堅 Android でも滑らかに動いていた。 ちゃんとベンチマークしたわけではないが、これまでの経験上、 これくらいの描画をやると中堅 Android だとすぐ FPS 落ちてたよな… といった絵が FPS 60 で動いていたりしたので、 手応えを感じている。

引き続き趣味のゲーム開発で使いながら、シェーダを整えたり拡張したりしていく所存。 次のステップとしては水や宝石のような質感の表現、草の揺れやエフェクト的な揺らめきなどを研究して、 自作シェーダに落とし込んでいきたい。

おまけ : カラフルフォグ

今回作ったシェーダには、通常の Unity フォグに加えて、距離に応じて複数の色に変わるような多色のフォグを実装している。

遠景の雰囲気を味付け・調整するための機能だが、極端な値を入れたりするとちょっと面白い視覚効果が得られたりする:

なんかのゲームの一場面で使えるかもしれない。