DXライブラリでトゥーンシェーダーを実装する方法について説明しています。アニメ調・セルルックな感じにしたい場合に。
トゥーンシェーダーってなんだ?
トゥーンシェーダーとは以下のように陰影やハイライトの境界がくっきりとした見た目にする技法のこと。セルシェーダーとも。アニメでよく見られる表現方法なのでアニメ調・セルルック調などとも言われる。

古くはゼルダの伝説風のタクト(2002年)にて採用されていて、近年だと原神あたりが有名だろうか。直近だとモンスターハンターストリーズ3とか。
Blenderだと実装が意外と難しかったりするトゥーンシェーダーだが、コードで書く場合はランバートシェーダーに処理をいくつか加えるだけで簡単に実装できる。
トゥーンシェーダーを採用しているゲームだと輪郭線がついているものが多いがここでは輪郭線なしのトゥーンシェーダーの実装について紹介する。輪郭線の表現は方法がいくつかあってどれも一長一短なため紹介が難しいので…
サンプルコード
それでは実装。今回は数式だけで全部やってしまうコードとグレースケールのテクスチャを読み込んでそこから陰影の色を計算するコードの2つを用意した。
まずはシェーダー側で数式で全部やってしまう方。ピクセルシェーダーのメイン関数を以下のようにする。
ピクセルシェーダー
// main関数
PS_OUTPUT main(PS_INPUT PSInput)
{
PS_OUTPUT PSOutput;
float4 TextureDiffuseColor;
float3 Normal;
float DiffuseAngleGen;
float3 TotalDiffuse;
float3 lLightDir;
// 法線の準備
Normal = normalize(PSInput.VNormal);
// ディフューズカラーの蓄積値を初期化
TotalDiffuse = float3(0.0f, 0.0f, 0.0f);
// ディレクショナルライトの処理 +++++++++++++++++++++++++++++++++++++++++++++++++++++( 開始 )
// ライト方向ベクトルのセット
lLightDir = g_Common.Light[0].Direction;
// ディフューズ色計算
// DiffuseAngleGen = ディフューズ角度減衰率計算
// トゥーンの場合閾値でDiffuseAngleGenを補正するためハーフランバート処理はする必要なし
DiffuseAngleGen = saturate(dot(Normal, -lLightDir));
// DiffuseAngleGenが閾値以上なら1.0f、閾値未満なら0.85fに補正する。ここでは閾値を0.5とした
// シェーダーで条件分岐式は好まれないのでstep関数で計算する
DiffuseAngleGen = step(0.5f, DiffuseAngleGen) + 0.85f * (1.0f - step(0.5f, DiffuseAngleGen));
// ディフューズカラー蓄積値 += ライトのディフューズカラー * マテリアルのディフューズカラー * ディフューズカラー角度減衰率 + ライトのアンビエントカラーとマテリアルのアンビエントカラーを乗算したもの
TotalDiffuse += g_Common.Light[0].Diffuse * g_Common.Material.Diffuse.xyz * DiffuseAngleGen + g_Common.Light[0].Ambient.xyz;
// ディレクショナルライトの処理 +++++++++++++++++++++++++++++++++++++++++++++++++++++( 終了 )
// 出力カラー計算 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++( 開始 )
// 出力カラー = TotalDiffuse * テクスチャカラー + SpecularColor
TextureDiffuseColor = g_DiffuseMapTexture.Sample(g_DiffuseMapSampler, PSInput.TexCoords0);
PSOutput.Color0.rgb = TextureDiffuseColor.rgb * TotalDiffuse + SpecularColor;
// アルファ値 = テクスチャアルファ * マテリアルのディフューズアルファ * 不透明度
PSOutput.Color0.a = TextureDiffuseColor.a * g_Common.Material.Diffuse.a * g_Base.FactorColor.a;
// 出力カラー計算 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++( 終了 )
// 出力パラメータを返す
return PSOutput;
}
ディレクショナルライトと面の法線の内積を求めるところまではランバートシェーダーと同じだが、内積の値が閾値以上かそうでないかで補正し、その結果をディフューズカラーに乗算している。
ここでポイントなのが閾値以上かそうでないかでの判定でif文を使っていないこと。シェーダーコードではif文などの条件分岐式は好まれない(基本処理が遅くなるため)のでstep関数を使って判定している。
| 宣言 | step(a, x) | |
| 概要 | ( x >= a) ? 1 : 0 を返す | |
| 引数 | float a | 値1 |
| float x | 値2 | |
| 戻り値 | 1 | xがa以上だった場合 |
| 0 | xがaより小さかった場合 | |
step関数の結果を逆にしたい(x< aなら1にする)場合は 1.0f – step(a, x) と書いて結果を逆にする。
後の処理はランバートシェーダーと同じ。
次はグレースケールのテクスチャを使って処理する方。DXライブラリ本家で採用されている方法で、3段階以上に色を変えたい場合はこっちの方がわかりやすい。
グレースケールのテクスチャだがクリスタなどのイラスト・画像編集ソフトで下のようなグレースケールのグラデーションマップを作成する。画像のサイズだが縦・横ともに2の乗数(2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, …)でないと正しい結果が得られないので注意。

ディレクショナルライトと面の法線の内積をUV座標のUの値とみなしてこのグレースケールのテクスチャから色を取得し、ディフューズカラーに加算or乗算するという流れ。
実際のシェーダーコードはこちら。こちらもピクセルシェーダーのメイン関数を変更している。
ピクセルシェーダー
// C++ 側で設定するテクスチャや定数の定義に以下を追加
SamplerState g_ToonDiffuseGradSampler : register(s1); // トゥーンレンダリング用ディフューズカラーグラデーションテクスチャ
Texture2D g_ToonDiffuseGradTexture : register(t1); // トゥーンレンダリング用ディフューズカラーグラデーションテクスチャ
// main関数を以下のようにする
PS_OUTPUT main(PS_INPUT PSInput)
{
PS_OUTPUT PSOutput;
float4 TextureDiffuseColor;
float3 Normal;
float DiffuseAngleGen;
float3 TotalDiffuse;
float3 lLightDir;
float3 TotalAmbient;
float TotalLightGen;
//トゥーンテクスチャカラー関連
float4 TextureToonColor;
// 法線の準備
Normal = normalize(PSInput.VNormal);
// ディフューズカラーの蓄積値を初期化
TotalDiffuse = float3(0.0f, 0.0f, 0.0f);
// アンビエントカラーの累積値を初期化
TotalAmbient = float3(0.0f, 0.0f, 0.0f);
// ライトの減衰率合計値の初期化
TotalLightGen = 0.0f;
// ディレクショナルライトの処理 +++++++++++++++++++++++++++++++++++++++++++++++++++++( 開始 )
// ライト方向ベクトルのセット
lLightDir = g_Common.Light[0].Direction;
// ディフューズ色計算
// DiffuseAngleGen = ディフューズ角度減衰率計算
// こちらの場合もハーフランバート処理はしなくてよい
DiffuseAngleGen = saturate(dot(Normal, -lLightDir));
// ディフューズカラー蓄積値に減衰率を加算
TotalLightGen += DiffuseAngleGen;
// アンビエントカラー累積値にライトのアンビエントカラーを加算
TotalAmbient += g_Common.Light[0].Ambient.xyz;
// ディレクショナルライトの処理 +++++++++++++++++++++++++++++++++++++++++++++++++++++( 終了 )
// 出力カラー計算 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++( 開始 )
// テクスチャカラーを取得
TextureDiffuseColor = g_DiffuseMapTexture.Sample(g_DiffuseMapSampler, PSInput.TexCoords0);
//トゥーンテクスチャカラーをライトのディフューズ減衰率から取得
TextureToonColor = g_ToonDiffuseGradTexture.Sample(g_ToonDiffuseGradSampler, TotalLightGen);
// ディフューズカラー = ライトのディフューズカラー * マテリアルノディフューズカラー
TotalDiffuse += g_Common.Light[0].Diffuse * g_Common.Material.Diffuse.xyz;
// 出力 = saturate( saturate( ディフューズカラー * アンビエントカラーの蓄積値 ) * トゥーンテクスチャカラー + スペキュラカラー ) * テクスチャカラー
// 今回はスペキュラを計算していないのでそこは省いている
PSOutput.Color0.rgb = saturate(saturate(TotalDiffuse + TotalAmbient) * TextureToonColor.rgb) * TextureDiffuseColor.rgb;
// アルファ値 = テクスチャアルファ * マテリアルのディフューズアルファ * 不透明度
PSOutput.Color0.a = TextureDiffuseColor.a * g_Common.Material.Diffuse.a * g_Base.FactorColor.a;
// 出力カラー計算 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++( 終了 )
// 出力パラメータを返す
return PSOutput;
}
この方法を使う場合はC/C++側で SetUseTextureToShader() でグレースケールのテクスチャをシェーダーに渡してあげる必要がある。
| 宣言 | int SetUseTextureToShader(int StageIndex, int GraphHandle) | |
| 概要 | シェーダーで使用するテクスチャを設定する | |
| 引数 | int StageIndex | 設定するテクスチャのレジストリ番号 |
| int GraphHandle | 設定するテクスチャの画像ハンドル | |
| 戻り値 | 0 | 成功 |
| -1 | エラー発生 | |
C/C++側のコードはこちら。
ShaderMng.cpp
// 以下の変数を追加
static int GradTextureHandle; //トゥーンシェーダーで使うテクスチャハンドル
//初期化
void ShaderMng_Initialize() {
// 略
// トゥーンテクスチャ用画像ハンドルをセット
GradTextureHandle = LoadGraph("dat/shader/ToonGradTexture.png");
// トゥーンテクスチャ用画像を正しく読み込めたか
if (GradTextureHandle == -1) ShaderLoadSuccess = false;
//オリジナルシェーダーを使うか分岐
if (ShaderLoadSuccess) {
// オリジナルシェーダーを正しく読み込めた場合
// 略
// 使用するテクスチャ1にグラデーションテクスチャをセットする
SetUseTextureToShader(1, GradTextureHandle);
}
else {
// シェーダー関連ファイルで読み込みに1つでも失敗した場合
// ライティング処理をOFF
SetUseLighting(FALSE);
}
}
シェーダーについてきちんと学びたい人は以下の本がおすすめ。なおこの本はトゥーンシェーダーは扱っていないので注意。

コメント