「いけにえと雪のセツナ」グラフィック解説(第3回・シェーダ編)

投稿者: | 2016年8月17日

はじめに

前回に引き続き、「いけにえと雪のセツナ(I am Setsuna)」(Tokyo RPG Factory)でのグラフィック処理の解説をしていきます!(全4回予定で今回は3回目)

前回は「グラフィック効果編」で、本作で特徴的な処理(シャドウやリフレクションなど)について説明していきました。
そして今回ですが、グラフィックの下支えとなる部分として「シェーダ編」をお送りします。

※去る2016/4/5にもUnite2016にて一部公開致しましたが、本編はそれも含めつつの内容になっています。
そちらの資料動画も合わせてご覧いただくのをオススメします。


レンダリング方式

Unite2016でレンダリング設計の一部を説明しました。
それは以下の通りです。

setsuna_rendering_settings
・レンダリング方式(Rendering Path)はForward Renderingで行う。

・カラー空間(Color Space)はガンマ。

カラー空間のガンマ化はUnityはPS Vitaではリニア対応がされていなかったため、ガンマの方を利用することになりました。
(出来ればリニア対応は行いたかったですが・・・。)

一方のレンダリング方式は、ForwardもしくはDeferredの二択にはなりますが、Forwardにしました。

ForwardとDeferredの特徴

Forwardに至った経緯ですが、まずUnityのForwardとDeferredに関するドキュメントを掲載します。

Forward Rendering パスの詳細
Deferred Shading レンダリングパス

これらを本作で設定するにあたってのメリット&デメリットなどをまとめます。

メリット デメリット
Forward ・シェーダ設計がシンプル。
・合成負荷は考えなくて良い。
・VRAM消費が少ない。
・ライトの数が増えるにつれ負荷が上がる。
Deferred ・ライトを多数配置できる。 ・G-Bufferが必要なのでVRAM消費が多そう。
・合成負荷が掛かってしまう。
・半透明の扱いが大変。(そちらはForward用シェーダが必要)

本連載で何度も書いていますが、PS Vitaの事を考慮しなければなりません。
そうなるとやはり負荷とメモリが心配になってきます。

DeferredだとG-Buffer分のメモリが必要なのと、それの合成コストが掛かってしまうのが気になります。
ということでForwardに決定したいところですが、ライトの数次第というところがあります。

本作のライト数について

UnityのForwardでのライト数における処理の違いについては割愛します。(ドキュメント参照)
が、ライト数に比例して負荷が上がるというのは間違いないです。

少なくするための思考として、本作の特徴である「見下ろし型RPG」としてゲーム性からライト数を考えます。

・背景のライト(環境光とか灯篭とか)はLight Probesで処理してしまう。

→Light Probesは球面調和(SH)なので、ライト数とは関係ない。
→しかしそれらの光のアニメーションはできないが、仕様としてしまう。
setsuna_shader_light_probes

・コマンド式RPGなので、プレイヤーと敵が同時攻撃することはない。

→攻撃時のエフェクトっぽいライトが多数出ることはない。
→仕様として1つまではライトを受けられるようにする。
setsuna_shader_attack_enemy
↓↑
↓↑(交互に行動)
↓↑
setsuna_shader_attack_player

ということで、見下ろし型RPGとして考えると、動的なライトは攻撃などのエフェクト用ライト1つで良さそうというのが分かりました。(というより、そういう仕様にしました。)

それに関連し、Quality Settingsで「Pixel Light Count」は0にしました。
エフェクトライトは独自実装です。

setsuna_shader_quality_light

これよりForwardで設計してするのを決め、最後までそれを通しました。


シェーダの変遷

本作は当初Unity4で開発を開始し、数ヶ月後にUnity5に切り替えを行いました。
Unity4→Unity5でのシェーダ周りで、今回の話の要点となる部分の変更点は以下になります。

・ライトマップの方式がBeast→Enlightenに変更になった。(全く別物)
・Unity5からStandardシェーダが追加になった。

そもそもUnity(ゲームエンジン)を使ってのグラフィック設計が今回が初めてで、負荷やゲームに沿った機能拡張を考えてシェーダも一から設計することも考えましたが、一から作るのではゲームエンジンを利用する意義が薄れてしまうとも考えたため、既存機能との共存を考えつつ、シェーダ設計を行なっていきました。

Unity4での設計

プロジェクト開始当初で本作の絵作りの方向性を決めるため、Unity4でグラフィック処理の設計を行いました。

setsuna_shader_art_target_build

これはプロジェクト内では「アートターゲットビルド」という位置付けで、ここで基本技術や方向性を決めるためのものとしました。

検証を進めた結果、そこでのポイントは以下の通りです。

・ライトマップでの影などの出来栄えは非常に良いので使いたい!
・Light Probesも搭載されていたので、動くものはこれでライティングを行う。
・↑を活用するとなると独自のライティングのシェーダは書けないと判断。
・Surfaceシェーダを使うのが良いだろう。

ライトマップはその出来栄えに非常に感激したので、これを活かすことを軸に考えていきました。
そこから、モデル系に使われるシェーダはSurfaceベースで考えるのが良いという判断です。

Surfaceシェーダは非常にシンプルに記述でき、(当たり前ですが)Unityの既存機能(ライトマップ等)との親和性も高いため、この時点では設計に問題は無いと判断しました。

Unity5への移行

2015年に入って、Unity5への移行というのが作業として入りました。
他ゲームではスケジュールなどの関係でUnity5へ移行せずUnity4をそのまま使うというケースもありますが、本作ではこれは時期的には3月くらいで、ゲーム自体のリリースまで結構時間があったので、移行は必須作業となりました。

setsuna_shader_unity5

Unity4世代で技術の方向性を固めましたが、Unity5で変わったことや新規機能追加もあり、対応しなければいけないですし、何より新機能で面白そうなものがあれば積極利用したいと思い、再検証しました。

ライトマップの方式がBeast→Enlightenに変更になった。

→Unity4検証(アートターゲットビルド)でのデータは本編で使うことが無いので全く問題無し。

・Reflection Probesが追加になった。

→こちらは負荷などの観点もあるので様子見。(結局未使用)

・シェーダとしてStandardが追加になった。

→それでもSurfaceはあるので、ひとまず使わない。

ライトマップは以外と問題が発生しませんでした。
一番の大きな点が「Standardシェーダの登場」です。

Standardシェーダは端的に言うと、物理ベースレンダリングを行うために用意されたシェーダになります。
(現実の物体がリアルに表現出来るようになります。)

とはいえ、本作はイラストベースな世界観であり、そこに物理ベースの処理が必要か?というのが真っ先に出てきまして、そしてSurfaceシェーダで上手く回せるという判断も行っていましたので、この時点ではStandardは検証対象外としました。

Surfaceシェーダの問題点(1)

そうしてUnity5でもSurfaceシェーダで進めることを決めましたが、そんな折に問題が発生しました。

静的(動かない)オブジェクトはライトマップで、動的(動く)オブジェクトはLight Probesでライティングを行っていました。
Light ProbesもEnlightenで焼き付けが行われますが、その動的オブジェクトでSurfaceシェーダを使うと必要以上に明るくなってしまう状態になりました。

setsuna_shader_surface_lighting

Standardだときちんとした色合いで出るのに対し、Surfaceは過度に明るくなる印象があります。
ライトを暗くしての回避も考えますが、静的オブジェクトもそれでライティングが焼き付けるため、静的と動的オブジェクトで整合性が取れなくなりました。

これはUnity5リリース直後くらいの状態で、現在はもしかしたら修正されているのかもしれませんが、この時点でこの観点においては、Surfaceに少し暗雲が垂れてきました。

Surfaceシェーダの問題点(2)

ここで、Surfaceシェーダについて説明します。

SurfaceシェーダとはUnityでの独自の記述方法で、普段のシェーダでは頂点シェーダ、フラグメントシェーダを記述するのに対し、フラグメント処理での一部情報(アルベドや法線、スペキュラなど)を返すような処理のみを記述することで簡単にシェーダが書けるものです。

しかしこれは「Unityが内部でSurfaceから頂点&フラグメントシェーダに変換」しています。
これは、シェーダを選択して「Show generated code」を押すことでその様が確認できます。

setsuna_shader_surface_inspector

それを踏まえて、Surfaceには以下の欠点があると考えます。

・トリッキーな処理を盛り込みづらい。
・いざという時の最適化が行いづらい。
・内部で何が行われているか把握しづらい。

本作では「ライティングが明るくなりすぎる」という面からの調査ですが、上記を鑑み、Standardへの移行を考え始めました。


シェーダの改造

ということで、Standardシェーダの改造ベースで進めることにしました。

setsuna_shader_standard_info

やはりライトマップとの親和性は非常に高く絵的には問題無いのですが、Standardをそのまま使うことはなく、以下の作業が必要となります。

・シェーダサイズを極力小さくする。
・負荷を軽くする。
・ゲーム向けの機能拡張も行う。

非常に困難な作業でしたが、それらについて書いていきます。

ビルトインシェーダ

Unityにはシェーダが内蔵されていますが、それはダウンロードが可能です。
こちらから落とせます。

「Standard.shader」がStandardシェーダで、一見すると割とシンプルそうに見えますが、#includeを追うと、非常に複雑だというのが分かります。
(※今回はStandardに関する説明のみですが、本当に全シェーダが載っているはずですので、興味を持たれた方はUIやSpriteなども読まれると面白いと思います。)

ですが、頂点&フラグメントシェーダで構成されているので、いざという時の最適化は非常に行いやすいというのはこの時点で分かり安心しました。


無駄を削ぎ落とす(サイズ編)

Standard.shaderを見ていると、非常に物量が多いように見えます。
シェーダもリソースですので、コードが長かったりするとデータサイズも大きくなります。

本作ではサイズ削減に以下のことを行いました。

・不要なPassの削減。
・Fallbackは使わない。
・シェーダバリアント数を極限まで削減。

それぞれについて具体的に説明します。

不要Passの削除

Unityのシェーダは以下のような構成になっています。

setsuna_shader_shader_file

SubShaderで分かれ、その中に必要に応じてPassという単位が複数あるというものです。
それぞれの詳細は割愛しますが、ここで重要なのはPassに設定されている「LightMode」というタグです。

どんなLightModeがあるかは、Unityのドキュメントに書かれています。
Passは本来は設定しない場合はSubShader内のを全て処理しますが、Unityの内部処理で、特定のLightModeだけ処理する、というためのものです。

が、本作ではForwardレンダリングのみだったりライト数を制限していたりする関係で、不要なLightModeがかなり存在していました。
以下、LightModeごとの対応表になります。(Standardで用意されていたものを記載)

LightMode名 残す? 内容
ForwardBase Forwardでの基本シェーダなので残す。
ForwardAdd Forwardでの追加ライト。削除。(後述)
ShadowCaster 影落としだが、本作は独自影なので削除。
Deferred 本作はDeferredではないので削除。
Meta 用途不明だが、削除しても問題なかった。

本作はForwardレンダリングでの運営なので、ForwardBaseだけ残して全て消しました。
それによりシンプルに考えやすくなりました。

ポイントはForwardAddというPassです。
UnityでのForwardレンダリングでのライトですが、ForwardBaseとForwardAddで以下の役割を果たします。

★ForwardBase
・物体本体を描画。
・それと同時に、LightProbes(SH)と平行光を1つのみライティングを行う。

★ForwardAdd
・物体におけるライトの光の部分の描画を行う。
追加ライト1つごとにこのPassが処理される。

端的に言えばライトの数が増えれば増えるほど、ForwardAddでの処理回数が増えます。
試しに球に複数のライトを当てた時の絵とFrameDebuggerを載せます。

setsuna_shader_forward_add

このこともあり、本作はLightProbesベースのみにしています。
そしてエフェクトライト1つを実装していますが、それはForwardBaseを改造して実現しています。
(このライト自体も、Unityのライトではなく独自スクリプトで作成しています。)

setsuna_shader_effect_light

Fallbackは使わない

Pass整理と少し近いのですが、Fallbackも削除しました。

Unityではシェーダファイル内でSubShader単位を複数持っていますが、それは特定機種でシェーダがコンパイルできない時に、保険として複数持っておくというためのものです。
(主にスマホなどの機種が多数にあるプラットフォーム向け)

本作は(開発時点では)PS4とPS Vitaのみで、その2機種で問題なければ構いませんので、Fallbackは全てオフにしてあります。
(SubShaderもほぼ単一にしています。)

シェーダバリアント数の削減

そしてサイズ削減で一番大きなポイントが、「シェーダバリアント数の削減」です。

シェーダバリアント自体の詳細は割愛が、端的に言えばシェーダで機能切り替えのため、シェーダ内に複数コードを抱えることです。
非常に便利な機能ですが、その切り替え数が多いとコードサイズも大きくなります。

その数はシェーダを選択した際の「Compile and show code」右の「▼」を押すことで確認できます。
そしてStandardシェーダでPassの整理などを行った時点では「6」もありました。
(ちなみにPassを整理しなかった時は61もあります。)

setsuna_shader_variants_6

さらに「Show」ボタンで内訳を見られます。
以下、その時の実際のバリアント数です。

見てみると「SHADOW」系だったり「VERTEXLIGHT_ON」でバリアントが増えている印象です。
これらはStandardシェーダ内で記載されていたり、#pragma multi_compile_fwdbaseで設定されたりしますが、本作では機能を絞りに絞っているので、この切り替えは不要です。

ですのでこのキーワードを無効化する処理も追加しました。
それには「#pragma skip_variants」を利用しますが、本作では具体的に以下のようにしました。

これを行うことでほぼ全てのシェーダでバリアント数を1にすることが可能となり、シェーダサイズも削減することができました。

setsuna_shader_variants_1

これらの流れでStandardベースのシェーダの削減を行いました。


無駄を削ぎ落とす(処理編)

サイズと同時に、処理負荷も削減しないといけません。
特にPS Vitaを考えると、その点は非常に心配です。

Standardシェーダは高機能な分、削減できる箇所も多数あるので極端に削りました。
今回はその一部を紹介します。

テクスチャ参照枚数を減らす

Standardシェーダは物理ベースレンダリング用のシェーダで、パラメータもそれに応じたものが複数存在します。
それが標準の状態でどのように管理されているかを調査したところ、単一の値だとしてもテクスチャを保持して参照する形になっていました。

setsuna_shader_standard_texture
(※Unity5.3.4のFrameDebuggeerで見た時のStandardシェーダのパラメータ)

例えばオクルージョンマップというパラメータがあります。
これを指定しない場合でもテクスチャとして「_OcclusionMap」という単色のテクスチャを利用し、それを値としていますが、利用しないのであればここは定数にしたいところです。

本作では厳密な物理レンダリングは行わず、主にライティングを正確に当てるという用途でStandardをベースにさせてもらっているので、この辺はバッサリとカットしました。
各パラメータをどのように対処したかは以下の通りです。

内部テクスチャ名 対処方法
_DetailAlbedoMap
_DetailMask
_DetailNormalMap
キーワード「_DETAIL」を有効にしないことで対処。
_BumpMap キーワード「_NORMALMAP」を有効にしないことで対処。
_SpecGlossMap 対処なし。(Standard (Specular setup)の方なので。)
_MetallicGlossMap キーワード「_METALLICGLOSSMAP」を有効にしないことで対処。
_OcclusionMap 強引に定数1として処理。
_ParallaxMap キーワード「_PARALLAXMAP」を有効にしないことで対処。
_EmissionMap キーワード「_EMISSION」を有効にしないことで対処。
unity_SpecCube0 (後述)
unity_SpecCube1

殆どの場合で「UnityStandardInput.cginc」を編集しています。
そしてStandardシェーダでのキーワードを設定する「StandardShaderGUI.cs」も利用していません。
(キーワードを設定しないだけで大体は回避できます。)
定数化が主ですが、これで過度にテクスチャを参照することは無くなりました。

リフレクション処理のカット

前回でも書きましたが、本作ではリフレクションを独自手法で表現しています。
ですので、Standardシェーダ内で処理している方のリフレクションも不要です。

具体的には「UnityGlobalIllumination.cginc」での「if (reflections)」のくだりは丸ごとカットしました。
(シェーダ内でのif分岐はコスト高なので、削減には意味があります。)

ちなみにこれで内部テクスチャ「unity_SpecCube0」や「unity_SpecCube1」の参照も回避できます。

頂点ライトマップ化

Unite2016でもお話ししましたが、ライトマップを頂点単位で行う仕組みも作成しました。

setsuna_shader_vertex_lightmap

今までピクセル単位で行っていた処理が頂点単位で済むので、リソースサイズの面もそうですが高速化にも寄与しました。

今回はシェーダのどの辺りを具体的に修正したかを書いていきます。
Unite2016のスライドにも書いていますが、「ビルド時最適化前」と「ビルド時最適化後」で処理が分かれます。

★ビルド時最適化前
・モデルのMesh.uv2はライトマップUV。(つまりそのまま)
・「UnityStandardCore.cginc」のvertForwardBase関数内の「o.ambientOrLightmapUV」を以下のようにする。

★ビルド時最適化後
・モデルのMesh.uv2とMesh.uv3に該当位置のライトマップカラーを予め入れておく。(Unityのuvはx,y構成なので2つのuv)
・「UnityStandardCore.cginc」のvertForwardBase関数内の「o.ambientOrLightmapUV」を以下のようにする。
(v.uv1とv.uv2は上記Mesh.uv2とMesh.uv3)

★共通処理
・「UnityGlobalIllumination.cginc」のUnityGlobalIllumination関数の「fixed4 bakedColorTex」を以下の通りにする。

これで頂点シェーダで処理を行って利用する形に書き換えることが出来ました。

今回紹介したのは負荷削減の一端ではありますが、Standardシェーダをよく眺めることで無駄になりそうなところを削減していきました。


機能の追加

今まではStandardシェーダをいかに削るかという話をしましたが、ゲームに適した機能の追加も行わなければなりません。

Unite2016にて「シェーダ自動生成」の話をしましたが、具体的にどのように運営したかを説明していきます。

シェーダ自動生成の生い立ち

シェーダ自動生成とは、UnityでGUI上で必要な機能を指定し、出力ボタンを押すことで該当機能が含まれたシェーダを作成するシステムで、本作のために自作しました。

setsuna_shader_generator

機能の切り替えという観点ではUnityの場合「multi_compile」が真っ先に想像されると思います。
なぜそれを活用せずシェーダ自動生成の仕組みを作ったか、というお話をします。

結論から言いますとmulti_compileは「CGPROGRAM〜ENDCG間」もしくは「CGINCLUDE〜ENDCG間」でしか利用できません
つまりPassでのTagやBlend設定などに関しては活用できないことになります。

setsuna_shader_multi_compile

Unity5からの「ShaderGUI」を活用することも考えましたが、該当ソースを修正して運営ルールが変わった際にマテリアルを全更新かけないといけないというのがあり、渋々諦めました。

シェーダ自動生成の運営

シェーダ自動生成で実際にシェーダを生成する流れを見てみます。

setsuna_shader_generator

まずカテゴリを選択します。
「Character」「Map」「Effect」「UI」の4種類存在し、それぞれの用途に向いた機能が提供されます。
(例えば「リフレクション(動的写り込み)はMapのみ」とか。)

setsuna_shader_generator_category

その後、適当に利用したい機能を選択し、「シェーダ生成」を押すと、該当のシェーダを生成します。
「シェーダ総更新」を押すと、今まで作成したシェーダが全て出力し直されるので、もしシェーダ処理を根本から変えたい時などに対応が可能です。

setsuna_shader_generator_create

このように、非常にシンプルにシェーダが作成される仕組みです。
そしてシェーダ自体はアーティストが作成を行います

シェーダファイルへの組み込み

シェーダ自動生成でシェーダを生成するのですが、選択した機能がStandardをベースにしたシェーダに組み込まれないといけません。
どの様に組み込まれるかのフローを簡単に書きます。

今回は、前回紹介した「頂点アニメーション」を行うシェーダを作ってみます。

setsuna_shader_vertex_animation

シェーダ自動生成ツールに機能を選択するのですが、下のようなテンプレートファイルを読み込み、設定に応じて一部だけ書き出すなどの処理を行います。(@if 〜 @endifなどがそうです。)

setsuna_shader_generator_template

頂点アニメーションを有効にすると「Map_VertexAnimation」という定義が有効になります。
例えば頂点操作を行う関数を作っているのですが、@if Map_VertexAnimation 〜 @endifで括り、昨日のオンオフ出力を可能にしています。

最終的にテンプレートファイルが非常に複雑になってしまいましたが、シェーダ量産の手間を考えると、まだテンプレートファイルを編集した方が楽だったので、ツールを作る意義があったと思います。


テッセレーション対応

本作はPS4でもリリースされていて、俗に言うDirectX11世代の機能は使えるのですが、シェーダ的にはそんなに違いを持たせていません。
(強いて言えば頂点ライトマップを取りやめて通常のライトマップにしたりという程度です。)

そんな中、一番大きな対応は「雪面のテッセレーション対応」です。

setsuna_VTF_snow_field

Unityでも条件を満たせばDirectX11世代の処理も問題なく行えるのですが、Standardシェーダ自体にはテッセレーションのコードが書かれていません!
ですので、新規に追加する必要が出てきます。

本作で行った対応を書きます。

Unityのドキュメントを参照して、サンプルコードをそのままコピーしシェーダを作る。
・そのシェーダを選択し、「Show generated code」でシェーダを確認し参考にする。
・本作でのStandardシェーダの方に組み込む。

つまりSurfaceでは容易にテッセレーション出来るようになっているのですが、Standardシェーダに組み込むのはSurfaceシェーダの形式では不可なので、「Show generated code」でどう変わるかを調べつつ組み込むという形で進めました。(具体的な内容は割愛します。)

これで雪面にもテッセレーションが組み込めました。


まとめ

今回は「いけにえと雪のセツナ」でのシェーダについて書いていきました。

Unity自体には標準でもシェーダが用意されていて、それらを用いて気軽に絵作りやゲーム開発が行えます。
それはそれで素晴らしいことですが、普段使っているシェーダがどういう処理を行っているかを把握しておくだけでも悪くないと考えます。
そうすることで拡張や最適化が行え、例えば低スペックのマシンでもクオリティの高い表現ができる可能性が出てきます。

まず、ビルトインを落としてみて読んでみると面白いと思います。

次回はいよいよ最終回です。「ツール編」をお送りする予定です。
お楽しみに!


※お知らせ

株式会社ロジカルビートでは、一緒に働いてくれる仲間を募集しています!
興味を持たれた方は採用ページを是非ご覧下さい!


 

 

【免責事項】
本サイトでの情報を利用することによる損害等に対し、株式会社ロジカルビートならびに株式会社Tokyo RPG Factoryは一切の責任を負いません。

※「いけにえと雪のセツナ」:©2016 Tokyo RPG Factory Co., Ltd. All rights reserved.

「いけにえと雪のセツナ」グラフィック解説(第3回・シェーダ編)」への2件のフィードバック

  1. ピンバック: スウィンぐるん開発裏話(6) | 蒼玉亭

  2. ピンバック: 「いけにえと雪のセツナ」グラフィック解説(第4回・ツール編) – 株式会社ロジカルビート

コメントは停止中です。