【ゲーム制作】トゥーンシェーダーとは?|輪郭線と色の境目の仕組みをDirectXで実装

トゥーンシェーダーのサムネイル ゲーム制作

今回は、アニメ調の見た目を作る「トゥーンシェーダー」とは何か、そしてDirectXでどう実装するかについて解説していきます。

「アニメみたいなベタ塗り+黒い輪郭線って、どういう仕組みなの?」
「セルシェーディングって聞くけど、トゥーンと何が違うの?」
「普通のライティングから、どこをどう変えればあの見た目になるの?」

こんな疑問はありませんか?

トゥーンシェーダー(セルシェーディング)は、ひとことで言うと「明るさをなめらかなグラデーションではなく、くっきりした段(バンド)で塗る」+「輪郭線を足す」ことで作られています。難しい理論は不要で、既存のライティング計算に一手間加えるだけで実現できます。

この記事では、トゥーンシェーダーの仕組み・段差を作るランプテクスチャ・輪郭線の作り方・DirectXでの実装コード・ハマりどころまで、実際にDirectXで動かした画像つきで解説します。読み終えると、自分のプロジェクトにトゥーン表現を組み込めるようになりますので、ぜひ最後まで読んでください。

トゥーンシェーダーの正体|「段差」と「輪郭線」の2点セット

通常のライティング(Lambert反射やBlinn-Phong)は、光の当たり具合(NdotL)に応じて明るさが連続的に変化します。これがなめらかな陰影=リアル寄りの見た目です。

トゥーンシェーダーは、この連続的な明るさを数段階の色にスパッと区切ることで、アニメのセル画のような「面で塗った」見た目を作ります。さらに輪郭線(アウトライン)を足すことで、線画っぽさが完成します。つまり必要なのは次の2つだけです。

  • ①陰影の段階化… 明るさを連続値ではなく、2〜3段のバンドで塗る
  • ②輪郭線… モデルのシルエットに沿って黒い線を足す

実際にDirectXで動かした結果がこちらです。光の当たる面が明るい水色、影になる面が濃紺、その中間が中間色の3段に分かれ、輪郭が黒線で縁取られています。

DirectXで実装したトゥーンシェーダー。球体が水色・青・濃紺の3段バンドで塗られ、黒い輪郭線で縁取られている

仕組み①|ランプテクスチャで陰影を段階化する

段差の作り方はシンプルです。通常のライティングと同じくNdotL(法線と光の内積)で明るさを求めたあと、その値をそのまま色として使わず、「ランプテクスチャ」という細長い画像から色を引くのがポイントです。

ランプテクスチャは横方向(U座標)に明るさ0〜1を割り当てた画像で、くっきり色が変わる境目を持たせておきます。今回使ったランプはこの1枚です(256×16ピクセル、左が暗い側・右が明るい側)。

トゥーンシェーダーで使うランプテクスチャ。左から濃紺・青・水色の3段にくっきり分かれた横長の画像

グラデーションではなく3色がはっきり切り替わる画像にしているのがミソです。これを「明るさ(NdotL)」をU座標として横方向にサンプリングすれば、その場でトゥーンの段差が手に入ります。

// 明るさ計算(ハーフランバート:影を潰れにくくする)
float NdotL = dot(N, L);
float light = NdotL * 0.5f + 0.5f;

// 明るさをランプテクスチャのU座標として使う
float u = clamp(light, 0.01f, 0.99f);
float4 rampColor = g_TextureRamp.Sample(g_Sampler, float2(u, 0.5f));

// ベースカラーにランプの色を掛ける
outColor.rgb = baseColor.rgb * Light.Diffuse.rgb * rampColor.rgb
             + baseColor.rgb * Light.Ambient.rgb;

普通のライティングならlightの値をそのまま明るさとして使いますが、トゥーンではランプ画像というルックアップテーブルを経由させることで、なめらかな変化を強制的に「段」に変換しています。ランプの色を変えれば、そのまま作品の色調(今回のような青系でまとめる、など)にもなります。

仕組み②|輪郭線は「裏返して膨らませる」だけ

輪郭線の作り方は色々ありますが、実装が一番シンプルなのはモデルをもう一度、少しだけ法線方向に膨らませて背面だけ描く方法です。手順はこうです。

  • ①頂点を法線方向にほんの少し(0.01〜0.02程度)押し出す
  • カリングを反転(表面ではなく裏面だけを描く)
  • ③その裏面を単色(黒)で塗る

一回り大きい黒いモデルを裏面だけ描くと、元のモデルからわずかにはみ出した部分が輪郭線として見える、という仕組みです。頂点シェーダーはこうなります。

// 法線方向に頂点を押し出す(EdgeScaleぶん膨らませる)
float3 normal = normalize(In.Normal.xyz);
float4 pos = In.Position;
pos.xyz += normal * EdgeScale;

Out.Position = mul(pos, WorldViewProjection);
Out.Diffuse = float4(EdgeColor, 1.0f); // 輪郭線の色(黒)

ピクセルシェーダー側は色を出すだけなので、頂点色をそのまま返す1行で済みます。試しに、この輪郭線パスを描かずに①のバンドだけにするとどうなるか比較してみましょう。

輪郭線を描かない場合のトゥーンシェーダー。色の段差はあるがシルエットが背景に溶け込み、輪郭がぼやけて見える

色のバンドだけでも立体感は出ていますが、シルエットが背景に溶け込み、輪郭がぼんやりします。最初の画像と見比べると、黒い輪郭線が「絵として一枚締まる」ことに一役買っているのが分かると思います。

C++側の実装|同じモデルを2回描く

C++側でやることは、同じモデルを2パス描くだけです。1回目が通常のトゥーン塗り、2回目が輪郭線です。

// 1) 通常描画(背面カリング)
SetCullMode(D3D11_CULL_BACK);
GetDeviceContext()->VSSetShader(VertexShader, NULL, 0); // トゥーン用VS
GetDeviceContext()->PSSetShader(PixelShader, NULL, 0);  // ランプ参照PS
ModelDraw(Model);

// 2) 輪郭線描画(表面カリング=裏面だけ描く)
SetCullMode(D3D11_CULL_FRONT);
GetDeviceContext()->VSSetShader(EdgeVertexShader, NULL, 0); // 押し出しVS
GetDeviceContext()->PSSetShader(EdgePixelShader, NULL, 0);  // 単色PS
ModelDraw(Model);

// 3) カリングを元に戻す
SetCullMode(D3D11_CULL_BACK);

カリングモードの切り替えが肝です。1回目は普段どおり背面を消して表面を描き、2回目はあえてカリングを反転して裏面だけを描く。この順番と設定を間違えると、輪郭線が出なかったり、逆にモデル全体が黒く潰れたりします。

【重要】私が実装中にハマった体験談

自主制作でトゥーンシェーダーの検証用に新しいモデルを読み込ませたところ、読み込んだ瞬間にアプリが即クラッシュしました。エラーメッセージも出ず、ただ落ちるだけです。

原因は、モデル読み込み処理が「法線情報は必ずある」前提mesh->mNormals[v] にノーチェックでアクセスしていたことでした。エクスポート元によっては法線を持たないモデルデータもあり、その場合 mNormalsnullptrになります。そこへ普通に配列アクセスしてアクセス違反、という単純な事故でした。mNormalsmTextureCoords[0]nullptrかどうかを確認し、無ければ仮の値(法線なら上向き)を入れるガードを足したら解決しました。

もう一つ、薄い板状の武器モデルをトゥーン化したときは正面から見ると陰影の段差がほとんど見えないことがありました。原因は単純で、板状のモデルは表側の面がほぼ全部同じ方向を向いているため、光をどう動かしてもNdotLがモデル全体でほぼ同じ値になり、バンドの境目が生まれないのです。回転させて複数の面が見える角度にしたところ、ちゃんと段差が出ました。「陰影が変化しない」と思ったら、まずモデルの向きと面の形状を疑うとよいです。

トゥーンシェーダーのよくある失敗例と対処法

①モデルの読み込みでクラッシュする

体験談のとおり、法線やUV座標が存在しないモデルにノーガードでアクセスしているのが原因です。読み込み処理側でnullptrチェックを入れ、無ければデフォルト値で埋めましょう。

②輪郭線がまったく出ない・逆にモデルが真っ黒になる

カリングモードの設定ミスがほとんどです。輪郭線パスはD3D11_CULL_FRONT(表面を消して裏面を描く)にする必要があります。ここがBACKのままだと膨らませた分がそのまま前面に出て、モデル全体が黒くなります。

③色の境目が滲んでグラデーションになる

ランプテクスチャのサンプラーがリニア(線形)補間だと、境目付近がぼやけて中間色が出ます。くっきりした段差にしたいなら、ランプ画像自体を「同じ色のベタ塗りが連続する」形にする(今回のように境目をはっきり作る)か、サンプラーをポイント(最近傍)補間にすると解決します。

注意点

  • ランプテクスチャは境目をくっきり作る(グラデーションにしない)
  • 輪郭線パスはカリング反転+法線押し出しで作る
  • 薄い・平らなモデルは陰影の段差が見えにくいことがある

まとめ

  • トゥーンシェーダーの正体は「陰影の段階化」+「輪郭線」の2点セット
  • 段階化はNdotLをランプテクスチャのU座標として使うだけで作れる
  • 輪郭線はモデルを法線方向に膨らませて裏面だけ描く2パス目で作る
  • モデルによっては法線・UVが無い/段差が見えにくいことがあるので要注意

トゥーンシェーダーは、既存のライティング計算に「ランプテクスチャ」と「もう1パスの輪郭線描画」を足すだけで実現できます。仕組みが分かれば応用も効くので、まずは今回のようにシンプルな球体で色の段差と輪郭線を確認してから、自分のモデルに広げてみてください。

関連記事