【ゲーム制作】頂点ライティングとピクセルライティングの違いとは?|グーローとフォンを画像で比較

頂点とピクセルのライティングの違いのサムネイル ゲーム制作

今回は、3D描画の基礎である「頂点ライティングとピクセルライティングの違い」について解説していきます。

「ライティングの計算って、頂点シェーダーとピクセルシェーダーのどっちに書けばいいの?」
「グーローシェーディング、フォンシェーディングって何が違うの?」
「ハイライトがなんかボヤけて汚いんだけど…」

こんな悩みはありませんか?

結論から言うと、両者の違いは「明るさをどこで計算するか」だけです。計算式そのものは同じでも、頂点で計算するか、ピクセルで計算するかで見た目が大きく変わります。

実際に自作のDirectXプロジェクトで、まったく同じライト・同じモデルで計算する場所だけを変えて描画してみました。

頂点ライティングとピクセルライティングの比較。左はハイライトが鋭く、右はぼやけている
左がピクセル単位、右が頂点単位です。左はハイライトが小さく鋭いのに対し、右はぼんやり広がって全体が明るくなってしまっています。

この記事では、2つの違い・なぜ差が出るのか・実際のコード・使い分け・ハマりどころまで、HLSLのコードつきで解説します。読み終えると、ライティング処理をどちらに書くべきか自分で判断できるようになりますので、ぜひ最後まで読んでください。

違いは「計算する場所」だけ

まず前提として、3Dモデルは頂点の集まりでできています。そして画面に映るピクセルの数は、頂点の数よりはるかに多いのが普通です。

  • 頂点ライティング…頂点シェーダーで明るさを計算する。頂点の数だけ計算し、間は自動で補間される
  • ピクセルライティング…ピクセルシェーダーで明るさを計算する。画面に映るピクセルの数だけ計算する

頂点ライティングはグーローシェーディング、ピクセルライティングはフォンシェーディングとも呼ばれます。名前は難しそうですが、計算式は同じで、実行する場所が違うだけです。

頂点ライティングのコード

頂点シェーダーで明るさを求め、結果をDiffuse(頂点の色)として出力します。

// 頂点シェーダー側で明るさを計算する
void main(in VS_IN In, out PS_IN Out) {
    // 座標変換
    matrix wvp = mul(mul(World, View), Projection);
    Out.Position = mul(In.Position, wvp);

    // 法線をワールド空間へ
    float4 worldNormal = normalize(mul(float4(In.Normal.xyz, 0.0f), World));

    // 拡散光(ランバート)
    float light = saturate(-dot(Light.Direction.xyz, worldNormal.xyz));

    // 明るさを「頂点の色」として出力する
    Out.Diffuse.rgb = light;
    Out.Diffuse.a = In.Diffuse.a;
}

ここで出力したOut.Diffuseは、ピクセルシェーダーに渡るときに三角形の内側で自動的に補間されます。つまり、頂点と頂点の間の色は「計算された結果ではなく、混ぜられた値」です。ここが後で効いてきます。

ピクセルライティングのコード

一方こちらは、頂点シェーダーでは法線を渡すだけにして、明るさの計算はピクセルシェーダーで行います。

// ピクセルシェーダー側で明るさを計算する
void main(in PS_IN In, out float4 outDiffuse : SV_Target) {
    float4 texColor = g_Texture.Sample(g_Sampler, In.TexCoord);

    // 補間されてきた法線は長さが変わっているので、
    // ピクセルごとに正規化し直すのが重要
    float3 normal = normalize(In.Normal.xyz);

    // 拡散光(ランバート)
    float light = saturate(-dot(Light.Direction.xyz, normal));

    outDiffuse.rgb = texColor.rgb * In.Diffuse.rgb * light;
    outDiffuse.a = texColor.a * In.Diffuse.a;
}

ポイントは補間されてきた法線をnormalizeし直しているところです。補間で長さが1でなくなっているため、これを忘れると明るさがおかしくなります。

なぜ見た目に差が出るのか

拡散光(じんわりした明るさ)だけなら、実は両者の差はあまり目立ちません。差が決定的になるのは鏡面反射(スペキュラ=ツヤのハイライト)を入れたときです。

ハイライトは狭い範囲だけが急に明るくなる性質があります。頂点ライティングでは、その「急な変化」を頂点でしか捉えられません。

  • ハイライトが頂点と頂点の間に落ちた場合…どの頂点も明るくならず、ハイライトが消える
  • ハイライトが頂点に当たった場合…その頂点から三角形全体へ色が引き伸ばされ、ぼやけて広がる

冒頭の画像で右側のハイライトがボヤッと大きく広がっているのは、まさにこれが起きているためです。ピクセル単位なら1ピクセルずつ計算するので、ハイライトは本来の小ささと鋭さを保てます。

使い分けの基準

  • 基本はピクセルライティング…現在のGPUなら十分な性能があり、見た目の品質が段違い
  • 頂点ライティングが有効な場面…頂点数が非常に少ない背景、遠景、大量に描く小物、性能が厳しい環境

負荷の目安は単純で、頂点数とピクセル数のどちらが多いかです。画面いっぱいに映る低ポリゴンのモデルならピクセルの方が圧倒的に多いので頂点ライティングが軽く、逆に画面の隅に小さく映る高ポリゴンのモデルなら頂点の方が多くなることもあります。

【重要】私がライティングでハマった体験談

個人制作で3Dモデルを表示したとき、キャラクターの体に三角形の継ぎ目がうっすら浮き出るという現象に悩まされました。モデルの法線がおかしいのかと、モデリングソフト側を何度も見直しましたが問題は見つかりません。

原因は頂点シェーダーで明るさを計算していたことでした。頂点で求めた明るさが三角形内で直線的に補間されるため、隣り合う三角形の境目で変化の傾きが変わり、それが筋のように見えていたんです。計算をピクセルシェーダーへ移したら、継ぎ目はきれいに消えました。

「モデルを疑う前にシェーダーを疑う」と覚えた出来事でした。

ライティング実装のよくある失敗例と対処法

①ピクセルシェーダーで法線を正規化していない

補間された法線は長さが1ではなくなっています。normalizeを忘れると明るさが不自然に暗くなるので、ピクセルシェーダーの先頭で必ず正規化しましょう。

②法線にワールド行列の平行移動が乗ってしまう

法線は「向き」なので移動の影響を受けてはいけませんfloat4(normal, 0.0f) のようにw成分を0にして掛けるのが定石です。ここを1にすると、モデルを動かすたびに陰の付き方が変わる不可解なバグになります。

③ライトの向きの符号を間違える

Light.Directionは「光が進む向き」なので、面から光源へ向かうベクトルとの内積を取るにはマイナスを付ける必要があります。符号を間違えると光が当たる側と影の側が逆になります。

注意点

  • 計算式は同じ。違うのは実行する場所だけ
  • ピクセル側では法線の再正規化を必ず行う
  • 差が出るのはスペキュラ。拡散光だけなら差は小さい

まとめ

  • 頂点ライティング=グーロー、ピクセルライティング=フォン
  • 頂点で計算した明るさは三角形の内側で補間される
  • そのため頂点単位ではハイライトが消えたり、ぼやけて広がったりする
  • 現在はピクセルライティングが基本。頂点単位は軽さが必要な場面で

ライティングは「同じ式をどこで実行するか」で見た目が変わる、シェーダーらしさが一番わかりやすいテーマです。まずはピクセル単位で実装して、負荷が問題になったときに頂点単位を検討する、という順番がおすすめです。

関連記事