ShaderGraphでToonShaderを作ってみよう①
目次
はじめに
こんにちは! 3D課リーダー兼コーディング課3回生のともちん(小笠原)です!時間の流れとは早いもので、あっという間に2025年も残りわずかとなってきました。アドカレも半分が終了してしまいましたね….どの記事も読みごたえがあり今後の制作に役立つものばかりです!ぜひ、いろんな記事を読んで自分の技術や見識を広げましょう!!!!さて、お得意の文字数稼ぎが終わったところで、本題に入ろうと思います!!!!
今回はUnityのShaderGraphを使ってToonShaderを作るお話です。以前のLT会も話したものに少し改良を加えて説明しますので、よければ最後まで読んでください!
Shaderってナニ?
そもそも、Shaderってなんぞやという話ですよね。Shaderとは”ゲーム画面の見た目(色・光・影・質感など)を決める特別なプログラム”です。スクリーンに3D空間を表示するとき、パソコンはカメラに映る3D空間の情報を受け取り、スクリーンにどう映すのかをピクセルごとに計算して決定します。カメラが写すピクセル一つ一つが何色を描画するのか”計算”で決定し、対応するピクセルの色を描画しています。つまり、スクリーンは画用紙で、”どこ”を”何色”で塗るのかを計算で決めて色を塗っているみたいな感じです。この時、色を決める”計算方法” を定義するのが Shaderの役割です。
Shaderでは、以下のようなことを計算します。
- 色は何色か
- ライト(光源)が当たった時の明るさ・影のつき方(拡散反射・鏡面反射・フレネル反射など)
- 質感
- オブジェクトからカメラまでの距離(深度値)による描画範囲
しかし、Shader言語(HLSL, GLSL)はコードを書くために必要な知識量が他言語と比べて多く、初心者には不向きです。
そこで今回はUnityが用意しているShaderGraphというノードを繋げるだけでShaderをいじることができる機能を使います。ShaderGraphを使ってアニメ調のToonShaderを作成しながら、Shaderの基本的な考え方をおさえてみましょう!
完成形
今回作るToonShaderの完成形はこんな感じになります。

可愛くていいね👍
Lv0. セットアップ
まずはShaderGraphを作成してみましょう。Unityプロジェクト作成時Universal3DのCoreを選択し、プロジェクトを新規作成します。Projectウインドウ上で [Create => ShaderGraph => URP => UnlitShaderGraph] で名前をつけてShaderGraphを作成します。

作成された青色のアイコンをクリックするとShaderGraphのWindowが立ち上がり、以下のような画面になると思います。
最初からある中央のノードは最終的な出力値を決定するノードです。今回はFragmentのBaseColorに最終的な色の出力をつなげていきます。

ではノードを作成してみましょう。右クリックから[Create Node => Math => Basic => Add]を新規作成すると、以下のノードが出てきます。ノードはプログラミングにおける関数・メソッドのようなものです。左側(A,B)が入力、右側が出力(Out)です。Addノードは想像できる通り、入力A,Bの足し算を行います。(Out = A+B)
このほかにも線形補完をするLerpや取りうる値を制限するClampなど様々あり、これらを組み合わせでShaderを作成していきます。

Lv1. アニメ調の”ベタ塗り感”の表現 (拡散反射の3値化)
3DCGの世界でどのように光による陰影を決定しているのでしょうか。光が当たっているところ・光源から隠れて暗くなっているところをどのように判定しているのでしょうか。
CGの陰影は光源の方向ベクトルと3Dオブジェクトの面ごとの法線ベクトルの内積計算で求めます。法線ベクトルとは面に対して垂直な単位ベクトルです。内積は同じ方向を向く単位ベクトル同士の内積は1になり、二つのベクトルの向きが異なるほど値は小さくなっていき、逆向きになると−1になるという性質があります。この性質を活かし、面の法線ベクトルと光源の方向ベクトルが逆向きの関係を持つ(= 面が光源の方を向いている)時は明るく、反対に同じ方向を持つ時(= 面が光源の方向を向いていない)は暗いと判定することで3Dオブジェクトの陰影を表現しています。通常、CG分野では光の当たり具合を1~0(1に近いほど明るい)で表現するので、内積計算の結果が-1に近いものを1に対応させてあげれば良いわけですね。

では早速、ShaderGraphで拡散反射を作ってみましょう。使用するNodeは以下の通りです。
- Normal Vector(法線ベクトル)
- MainLight Direction(光の方向ベクトル)
- Normalize(入力を正規化する。Lightベクトルの正規化用)
- Dot Product(内積計算)
これらを組み合わせて拡散反射を作っていきます!(さっきの話が理解できた人は自分で作ってみましょう) 内積計算ノードの入力に法線ベクトルノードと正規化したMainLightノードを繋げます。組んでみると以下の図のようになります。

ToonShaderでは独特のアニメ調のベタ塗り感を出すためにこの『拡散反射』を2段階で表現します。どういうことかというと、拡散反射では明るい部分から暗い部分に行くまで徐々に少しずつ暗くなっていましたが、これを明るい・暗いのどちらかに白黒ハッキリさせるということです。こうすることでアニメっぽい色の陰影を表現できます。今回は陰影を3段階(白・灰色・黒)に分けてみました。使用するノードは以下の通りです。
- Step (閾値処理を行うノード. Inの値以上の値を全て1とし, それ以下は0とする)
- Lerp (線形補完を処理するノード. out = (1-t) * A + t*B, 白黒の出力に特定の色をつけるときによく使う)
- Add (入力値 A,Bの加算結果を出力する)
これらノードを組み合わせてアニメ調の陰影をつけていきます。DotProductの出力をStepで2値化します。Stepノードは入力値Edgeが閾値 In以上の場合は1を返し、それ以下の場合は0を出力するノードです。これにより、値を明るい or 暗いの2段階に分けられます。これにlerpノードで白黒部文の色を挿入していきます。Lerpは線形補完ノードで出力値 Outは入力値A,Bに対し、 Out = A(1-t) + tBで求められます。今回はtの値に内積結果が入り、Stepで値が1と0のどちらかになっているので入力値Aに暗い部分の色を、入力値Bには明るい部分のColorノードを繋げると白と黒にそれぞれ色をつけることができます。 閾値の異なるStep処理を施した出力を2つ用意し、Addで加算することで陰影が3段階に分かれたアニメっぽいShader出力が得られました。ノードを組んでみると以下のようになります。

ノードを組んだら、Game画面で確認してみましょう。こんな感じになるはずです。

油絵?のようなアニメ調のベタ塗り感のある陰影ができましたね。
Lv2. アニメ調のハイライト(Phong鏡面反射の閾値処理)
しかし、これでは少し物足りないような気がしませんか?気がしますね(断定)
そこで次は、ハイライトのような光沢をアニメ調で表現しようと思います。
そもそも光沢とはカメラに入ってくる物体表面の反射光です。カメラが多くの反射光を捉える条件は、反射面から視点方向に向かうベクトルVと反射光ベクトルRが同じ方向を向く(= カメラに反射光がたくさん入ってくる)状態の時になります。つまり、計算の観点で言うと、反射光ベクトルRと視点方向に向かうベクトルVの内積が 1 (=なす角度が0度)に近づくものを光沢として表現すれば良いんです。なお、反射光R は 2N(N・L) -L の式で求められます。

では早速ノードを組んでPhong鏡面反射を実装してみましょう。一旦反射光Rまでの計算をノードで組み立てます。使用するノードは以下の通りです。
- Normal Vector(法線ベクトル)
- MainLight Direction(光の方向ベクトル)
- Normalize(入力を正規化する。Lightベクトルの正規化用)
- Dot Product(内積計算)
- Multiply(乗算処理 A*B)

計算した反射光R を視点方向に向かうベクトル Vと内積を取り、視点に入ってくる反射光を計算します。
使うノードは以下の通りになります。
- Dot Product(内積計算)
- View Direction(視点方向に向かうベクトル)
- Maximum(最小値制限. 負の値は0としている)
- Power(累乗計算. 1に近い値を炙り出せる. よく使う手法)

正規化した反射光Rと視点に向かうベクトルの内積を求め、カメラに入る反射光の強さを物体表面ごと計算します。Maximumで最小値を0に設定し、Powerの累乗計算で1に近い値(=反射光となる部分)をあぶり出しています。拡散反射光でのStepを用いた閾値処理を同様に行ってToonShaderの漫画調の光沢を表現します。Stepで白と黒(1 or 0)のどちらかに分類したのち、Lerpで色を挿入します。

最後に拡散反射のShaderと鏡面反射のShaderを合成して、出力をFragmentのBaseColorに接続すれば完成です。

おわり..
いかがだったでしょうか?拡散反射と鏡面反射の理論的な話を理解できましたか。(自分なりに頑張って説明したつもりです…👀)
情報理工学部の学生はコンピュータグラフィックスの授業を履修するとなお理解が進むと思います。もしShaderに興味を持ってくれる方が一人でもいたら、嬉しい限りです。
次回はスクリーン上でSobelフィルタを用いたEdge検出をToonShaderに適用していきたいと思いますので、そちらも興味のある方は読んでみてください!

