この記事はUnreal Engine 4(UE4) Advent Calendar 2019の1日目の投稿記事です。
さて、今年のAdvent Calendar 1発目のネタはみんな大好きなセルシェーダーについてです。と、言っても今更なネタでもありますので、普段仕事でも散々NPR系のシェーダーを作っており、本当は高度なネタも沢山知っていますが、今回はシェーダー初心者に向けたセルシェーダーの原理についての解説です。タイトルにもある通り誰でもわかる程度の内容を想定しています。
もう散々解説されているネタではありますが、意外なくらい原理を解説しているくらい記事は見かけないので、今回はあくまでもその原理を解説するという趣旨です。
セルシェーダー実装の種類
セルシェーダーの実装には種類があり、大きく分けて以下の2タイプです。
1. ポストプロセスマテリアルによるG-Bufferを使った加工処理
2. サーフェスマテリアルによる擬似的な陰影計算処理
1のポストプロセスマテリアルによるセルシェーダー実装は私自身が以前に開発した、PPCelShaderというものがあり、こちらは既にGitHub上で誰でも使える形で公開しています。
ポストプロセスによるセルシェーダーはとにかく使い方が楽で、ほとんど調整の必要がありません、しかしモデルに対して細かい調整ができないので、細かい制御が必要な場合には向いていません。
それに対して、サーフェスマテリアル、つまり通常のマテリアルに対してのセルシェーダーは柔軟にカスタマイズが可能なので個別に調整を繰り返したい場合には必然とサーフェスマテリアルを利用して作ることになります。今回はこちらを使った解説ということになります。
セルシェーダーと内積
セルシェーダーと線形代数学に出てくる内積は切っても切れない関係です。内積ってわかりますか?おそらくそんなに難しい概念ではないのですが、内積の解説を探しにいくと、どこを見ても難しそうな数式の話がでてくるので、わかりやすく画像で解説します。
画像内にある、光源方向と、物体のもつ法線を計算し、そのなす角度が内積でθ(シータ)として求められます。θが1~0の場合、光に当たっていることとなり、-1~0の場合には光が当たっていないこととなるので、この境界を利用することで陰影をハッキリとさせてメリハリをつけることでセルシェーダーが可能となります。
基本的なセルシェーダーはこれが全てと言っても過言ではありませんが、世の中の解説はこれを難しく解説しすぎているので、数学が苦手な人にとっては理解する気も起きないわけです。ではこれをUE4で再現すると、どうなるのでしょうか?
事前準備
実際にセルシェーダーを作成する前に、モデルやライト用のBP、パラメーター格納用のマテリアルパラメーターコレクションを用意します。
今回モデルはバンダイナムコスタジオさんが配布している公式モデルのミライ小町を利用させていただきました。モデルのインポート方法は全く別の話となるので、ここでは解説していません。
次にライトベクトルを得るためのブループリントを作成します。ライトベクトルに関しては後ほど解説します。新規BP作成を行い、親クラスに"DirectionalLight"を選択します。名前は"BP_DirectionalLight"にでもしておきましょう。
そして次にライトベクトルを格納する新規のマテリアルパラメーターコレクションを作成します。名前は"MPC_CelShader"にしておきます。"Vector Parameters"に"LightVector"という名前のパラメーターを新規で追加します。
最後に、"BP_DirectionalLight"にノードを追加します。"Construction Script"と"Event Graph"の"Event Tick"に以下のようにノードを作成します。これにより、ディレクショナルライトから直接光源方向を得ることができるようになります。
最後に既に配置してあるディレクショナルライトがあれば、それを"BP_DirectionalLight"と差し替えを行っておきましょう。レベルに配置済みのディレクショナルライトがあればそれを右クリックすることで、コンテンツブラウザ上で選択している"BP_DirectionalLight"をそのまま置換することも可能です。
これで事前準備が全て整いました!
セルシェーダー用マテリアル作成
ここからやっとセルシェーダー用のマテリアル作成に入ります。まずは通常のマテリアルを作成し、そのマテリアルをメッシュ上に全て割り当てておきましょう。このマテリアルを"M_CelShader"という名前にしておきます。
当然何も作っていないので真っ黒な状態でスタートになります。そして以下のようなノードを作成します。
まずは左のノードから解説していきます。マテリアルパラメーターコレクションからライトベクトルを抽出します。"Collection Parameter"というノードで、詳細の中から"MPC_CelShader"とParameter Nameで"LightVector"を選択します。このライトベクトルが正しく光源方向となり、光の向きとなります。これを"Component Mask"ノードで"RGB"だけ抽出します。本来はなくてもいいのですが、"Collection Parameter"ノードは4 Vectorを返すノードなので、ここでMaskして 3 Vectorに変換しておく必要があります。
そして遂に出てくるのが内積です。内積は"Dot"という名前のノードとなっており、これを"VertexNormalWS"ノードでメッシュのワールド空間上の頂点法線を取得し、ライトベクトルと法線方向を内積として計算させます。これの結果が以下です。
何か怖い結果ですね。ちなみにややこしいですが、白い部分が陰影となる部分となっています。なぜ白い部分が陰影となるのかは、また後程解説します。このまま次に以下のようにノードを組みます。
"Multiply"ノードで100をかけています。それを"Clamp"ノードで0~1に収まるようにしています。これにより、陰影となる部分の境界をパキっとさせることができます。もしグラデーションをかけたい場合にはMultiplyの値を小さくすることで、多少陰影のグラデーションを残すこともできます。
次にテクスチャーと影となる部分の色を決めます。更に以下のようにノードを組みます。
Clampした結果は常に0~1の結果になるので、これを"Lerp"ノードの"Alpha"に渡すことで、2つの結果を合成することができます。ここで、陰影が白くなっていた理由がわかります。LerpのAにはベースとなるテクスチャーの色がそのまま入り、LerpのBにはテクスチャーの色と陰影の色をかけ算した結果が入ります。つまり、陰影が白い部分はBの結果を得ることができるので、陰影色をそのまま受け取ることができるようになるのです!
おお!なんかそれっぽい感じになりましたね。これをもう少し改造すると、ベースカラーの色も変更できるようになります。
ベースカラーを弄って、自由自在に色を変更できるように。
では、全てマテリアルインスタンス化して、それぞれのテクスチャーをパラメーターで設定してみましょう!
うん、ばっちりですね!ひとまずセルシェーダーとして必要最低限の情報が揃いました。意外と簡単だなということがわかったと思います!