こんにちは。株式会社スパーククリエイティブのクライアントエンジニア、細井です。
当社では毎月社内ブログを投稿していますが、このたびCGWORLD.jpへの寄稿の機会をいただき、社内のエンジニアスタッフが数回に分けてUEにおけるセルシェーディングについての記事を書いていきます。
今回はセルシェーディングにおける基本をおさらいしつつ、エディタ上で完結できる表現をしていきます。初めはUEエディタ内のみで完結できる方法ですが、連載の後半ではエンジン改造を含めた表現を記事にまとめていきたいと思います。
<1>セルシェーディングとは?
この記事を読んでいる方は既に知ってるよと思われるかもしれませんが、初めてシェーダというものに触れる方のために、簡単にではありますが、セルシェーディングとは何かを確認しておきましょう。
「セルルック」「トゥーンシェーダ」と呼ばれることもあるセルシェーダですが、セルやトゥーンという名前からイメージできる通り、アニメのセル画っぽい、漫画っぽい見た目にするための技法です。
それってどんな見た目なの? と思う方もいるかと思います。
文章でお伝えするのは難しいですが、あえて色数を抑えることでモデルの立体感をなくしたり、リアルには存在しないアウトライン(主線)を付けたりすることで、「3Dなのに手描きのような見た目」になります。
<2>UEでセルシェーダを実装するには?
UEでセルシェーダを実装するにはいくつかの手法が存在します。その中でも、本連載では以下の3つに焦点を当てます。
1:Unlitシェーダ
2:ポストエフェクト
3:エンジン改造

今回は、比較的難易度の低い「Unlitシェーダ」を用いた表現を行なっていきます。
作業環境
今回は以下の環境で実装を進めています。
・Windows 10
・Visual Studio 2019
・Unrea Engine 5.1.1
<3>内積による陰影の表現
さて、セルシェーダを作成する上で外せないのが「内積」です。
モデルの法線とライトベクトルの内積をとることで、-1~0、0~1の2つの範囲に分けることができます。

この2つの範囲はそれぞれ光が当たっている場所と当たっていない場所を指し、そのラインを基準に影をつけることでよりセルシェーダらしいはっきりとした見た目になるのです。
図解するとこんな感じです。

-1~0の間は光が当たっていないので黒、0~1の間は光が当たっているので白というように綺麗に2つに分けることができています!
<4>実際につくってみましょう!
STEP 1:モデルの準備
今回は以下のモデルを使用します。画像は「Opaqueシェーダ」を使った見た目です。このモデルに、「Unlitシェーダ」を適用していきます。

まず、[基本アセットの作成]から[マテリアル]を選択しましょう。コンテンツ ブラウザ内にマテリアルが作成できたら右クリック→[マテリアルインスタンスを作成]を選択し、モデルのマテリアルの数だけマテリアルインスタンスを作成しましょう。
作成できたら、モデルのマテリアルにそれぞれセットしてみます。
※モデルによって、ここのマテリアル数は変わります。

そうすると、モデルが真っ黒になったと思います。

ここがスタート地点です。
STEP 2:ノード作成
親マテリアルを開き、セルシェーダをつくっていきます。
初めに説明した通り、今回はUnlitシェーダを使いますので、ShadingModelは[Unlit]としておきます。

それでは、ライトのベクトルを取得してみましょう。
UEのライトには、ディレクショナル、ポイント、スポット、スカイの4種類があります。
今回はその中の1つ、ディレクショナルライトを用いてセルシェーディングの表現をしていきます。つまり、ディレクショナルライトが当たっている箇所は白、当たっていない箇所は黒になるということです。
早速ノードを作っていきます。「SkyAtomosphereLightDirection」と検索してみてください。以下のノードが作成できたと思います。

続いて、モデルの法線情報を取得します。「VertexNormalWS」と検索しましょう。

これで内積をとるための素材が2つとも揃いました。
では内積の計算をしていきましょう! Dotノードを使うこともできますが、今回は下記の式をBluePrintで再現してみます。

ベクトルの成分を取得するため、ComponentMaskノードを使用します。
Rには上記式のa1、b1をGにはa2、b2をBにはa3、b3が渡ります。
それぞれを乗算した後、足すことで内積を求めることができます。

出力結果をエミッシブカラーに繋いでみましょう。
下のような見た目になっているかと思います。ちょっと怖いですが、モデルに陰影がつきましたね。

よりセルシェーダらしくはっきりした陰影を表現するため、内積結果を100倍し、その結果をClampで0と1の範囲に制限します。

これによって、内積の結果-1~1が渡されていたところに、0か1が渡されるようになりました。
陰影がはっきりしているのがわかりますね。

STEP 3:テクスチャの設定
そろそろテクスチャを設定できるようにしましょう。「TextureSampleParameter2d」と検索しノードを追加します。
実際に使用するテクスチャは最初に作成したマテリアルインスタンスでそれぞれ設定するので、いったん白い画像を入れておきます。
また、影の色を調整するためのノードも追加します。「VectorParameter」と検索しましょう。影はテクスチャの色と乗算しないとただの単色になってしまいますので、Multiplyを追加しそれぞれつないでおきましょう。

最後に「Linear interpolate」と検索し、ノードを追加しましょう。
このLerpというノードは、線形補完をしてくれるものになります。A、Bにテクスチャと影色をつなぎ、Alphaには先ほどまでエミッシブカラーにつないでいた内積の結果をつなぎます。
内積の結果は0か1が返るようになっており、線形補完の結果0であれば影色、1であればテクスチャが返るようになります。

最終的なBluePrintはこのようになります。

実際に、マテリアルインスタンスにテクスチャを設定してみましょう。
マテリアルインスタンスを開き、[BaseTexture]にチェックを入れて使用するテクスチャを設定します。

結果は……

影が濃すぎましたね。影色を少し調整してみましょう。

灰色くらいにしてみました。
マテリアルインスタンスから個別に影色を設定することもできるので、見た目を向上させたい場合はいろんな色を試してみましょう!

STEP 4:結果
最終結果はこのようになりました。

少しのっぺりしすぎましたね。
特にスカートは「Unlitシェーダ」によってプリーツ感がなくなってしまいました。これは、頂点の法線とディレクショナルライトの内積の結果のみとなっているためです。
ここに、さらに法線マップを加えることで、頂点の法線を利用した陰影より細かい陰影を表現できるようにしていきます。
<5>法線マップによるディテールの追加
先ほどまではVertexNormalWSノードを使用し、モデルの法線情報を取得していました。ただ、その場合スカートなどの細かい箇所を表現するのは難しいです。
法線マップとは、ポリゴンに貼ることでポリゴンの法線とは別に内側の細かい起伏の向きをテクスチャで表現できるようにしたもので、1枚の板ポリゴンでさえ表面に凹凸があるように見せることができます。
実際どのように起伏の向きを判定しているかというと、法線とは別のベクトルを用意する必要があります。
ベクトルを作るには、XYZの3つの成分が必要になります。テクスチャで3成分を用意するとなると……「RGB」が使えそうです! 法線マップではX成分をR、Y成分をGそしてZ成分をBで表現するのが一般的です。このようにして法線とは別のベクトルを用意することができました。
こちらを先ほどのVertexNormalWSのところに置き換えてみると……


スカートがプリーツ感が生まれてきました。
また、髪の毛に対して法線マップを当てはめてみるとこのようになります。



<6>最後に
実際に作ってみると、基本的な陰影によるセルシェーダの実装手順は多くないことがわかります。
しかし出来上がったモデルを見て、自分の知っているあのゲームのキャラクターはもっと格好良かったとか、可愛かったとか思ったかもしれません。
今回実装したものは、あくまでキホンのキです。次回以降は、当社のグラフィックスエンジニアによる様々な技法を用いて、よりオシャレなセルシェーディングの紹介ができればと思います。

細井洸葉 / Koyo Hosoi
2020年SPARK CREATIVE入社。フロントエンジニアとして多数のアプリ制作に携わる。主としてUnity、Unreal Engineを用いた開発を担当し、VFX作成ツール「SPARK GEAR」のサポート業務やTechブログ執筆なども手がけている。
●SPARK CREATIVE
www.spark-creative.jp
●SPARK GEAR
sparkfx.jp
●SPARK CREATIVE Techブログ
tech.spark-creative.co.jp
TEXT_細井洸葉 / Koyo Hosoi(SPARK CREATIVE)
EDIT_小村仁美 / Hitomi Komura(CGWORLD)