遠くの景色が、急にパッと消える。遠景の切れ目が気になるときに使うのが、フォグ(霧)です。リニア、exp、exp2と、何が違うのか。霧をかける処理は、結局、どういう計算なのか。
この記事では、遠くのものを霧に溶かして見せるフォグを、DirectXのピクセルシェーダーで実装します。3つの方式を並べるだけでなく、式が本当に画面の色になっているかを、画像のピクセルから逆算して確かめ、さらに、私が実際にやらかした失敗(霧の色を合わせ忘れる、頂点で計算してしまう)も、撮った画像で紹介します。
フォグとは?
フォグとは、
・カメラから遠いピクセルほど、元の色を「霧の色」に近づけて混ぜる処理
です。現実の空気は、遠くの景色をうっすら白く霞ませます。その見た目の再現に加えて、ゲームでは遠くで急にモデルが出現する「ポップイン」や、描画距離(far)の切れ目を隠す目的でもよく使われます。
計算はとてもシンプルです。距離から「元の色がどれだけ残るか」を表す係数f(1=霧なし、0=完全に霧の色)を作り、次のように混ぜるだけです。
float3 col = lerp(FogColor.rgb, lit, f);残るのは「距離からfをどう作るか」で、ここが方式の違いです。
3つの方式:リニア・exp・exp2
作例では、この関数1つで3方式を切り替えています。
// 戻り値 f : 1 = 霧なし(元の色のまま) / 0 = 完全にフォグ色
float FogFactor(float d) {
int mode = (int)FogParam.x;
float f = 1.0f;
if (mode == 1) {
f = saturate((FogParam.z - d) / (FogParam.z - FogParam.y));
} else if (mode == 2) {
f = exp(-FogParam.w * d);
} else if (mode == 3) {
float x = FogParam.w * d;
f = exp(-x * x);
}
return f;
}- リニア:
startからendにかけて、直線的に元の色が減る。endでぴったりゼロになるので「ここから先は霧だけ」と決めやすい - exp:
exp(-density × d)。近くから少しずつ霧が始まり、遠くにいくほどゆるやかに濃くなる。現実の霧に近い - exp2:
exp(-(density × d)^2)。近くではほぼ霧がなく、ある距離から一気に濃くなる。「霧の壁」に入る感じ
ただし、3つを同じ見た目で比べるには、「どの距離で霧がほぼ隠れるか」をそろえる必要があります。パラメーターの数値をそのまま比べても意味がないからです。そこで、100m先で元の色が5%しか残らないように調整しました。
| 方式 | 設定 | 5%になる距離(実測) |
|---|---|---|
| リニア | start=10, end=100 | 95.6 m |
| exp | density=0.03 | 98.0 m |
| exp2 | density=0.0173 | 100.7 m |

見た目の差が出るのは近距離です。expは、足元に近い床からすでにうっすら霧がかかり、全体が白っぽく霞みます。リニアとexp2は、近くの床や柱はクリアなまま、遠くで一気に霞みます。ゲームの雰囲気で選ぶと、「霧の深い森」ならexp、「遠景だけ隠したい」ならリニアかexp2が使いやすいと感じました。
式は本当に画面の色になっているのか?
シェーダーの式を書いて、見た目が「それっぽい」からOK、としがちです。しかし今回は、画面のピクセルの色から、元の色が何%残っているかを逆算して、式と一致するかを確かめました。
やり方は、地面の1列のピクセルを取り出し、「フォグなしの画像」と「フォグありの画像」、そして霧の色から、f = (フォグあり - 霧の色) ÷ (フォグなし - 霧の色)で残り具合を出すだけです。行の位置から、そのピクセルが指す地面の距離も計算できます。

| 方式 | 最大の誤差 | 平均の誤差 |
|---|---|---|
| リニア | 0.68 / 255 | 0.16 / 255 |
| exp | 0.67 / 255 | 0.42 / 255 |
| exp2 | 0.69 / 255 | 0.41 / 255 |
3方式とも、誤差は最大でも0.7/255以内、つまり8bitの丸めの範囲に収まっていました。式どおりに描画されていることが、数字で確認できました。
【重要】実際に踏んだ罠:霧の色と背景色が違うと、遠景が幽霊になる
フォグでいちばん最初にやらかしたのがこれです。フォグの計算は正しいのに、遠くのビルだけが青空に浮かぶ白っぽい幽霊のように見えました。

原因は単純で、画面のクリア色(背景色)と、シェーダーの霧の色が別の値だったことです。遠くのものは霧の色に溶けて消えていく設計なので、背景がその色でないと、遠景だけ「霧の色のシルエット」として背景から浮いてしまいます。左上がその状態、右上が背景を霧の色に合わせた状態です。空(スカイボックス)を使う場合は、地平線付近の空の色を霧の色にするのが基本です。
この原因を切り分けるコツは、密度を極端に上げて「遠景が背景に溶けるか」を見ることです。溶けずにシルエットが残るなら、霧の色か背景色のどちらかがずれています。
もう一つの罠:頂点でフォグを計算すると、地面が全部霧の色になる
計算コストを下げようと、フォグ係数を頂点シェーダーで計算して、ピクセルへ補間する方式を試しました(昔の固定機能パイプラインの頂点フォグがこの方式です)。すると、地面が丸ごと霧の色になりました(左下の画像)。
作例の地面は、たった4頂点の大きな四角形です。頂点は四隅にしかなく、その四隅はどれもカメラから十分遠く、係数がほぼ0(完全に霧)になります。そのあいだのピクセルは4点の値を補間するだけなので、手前の床まで全部霧の色になったのです。柱や球のように小さいモデルでは問題が出にくいのが、なおさら気づきにくい点でした。
フォグは必ずピクセルシェーダーで計算するのが安全です。頂点の少ない大きな地面がある場面ほど、差が出ます。
広角で差が出る:奥行きZと直線距離
距離dの測り方にも2通りあります。カメラの向いている方向の奥行き(ビュー空間のZ)を使うか、カメラからその点までの直線距離を使うかです。
奥行きZを使うと、画面の左右の端にあるものも、中央と同じ奥行きなら同じ濃さの霧になります。しかし、実際の目から見た距離は端のほうが遠いはずです。視野角を100度に広げて、同じ行(奥行き約61.6m)のフォグの濃さを比べました。
| 画面のx位置 | カメラからの直線距離 | 奥行きZの霧の濃さ | 直線距離の霧の濃さ |
|---|---|---|---|
| 640(中央) | 61.6 m | 84% | 84% |
| 400 | 78.6 m | 84% | 90% |
| 200 | 108.7 m | 84% | 96% |
| 60(端) | 133.3 m | 84% | 98% |

奥行きZだと、画面の端まで同じ84%のままです。直線距離では端に行くほど濃くなり、98%まで上がりました。広角のカメラで、カメラを左右に振ったときに画面の端で霧の濃さが変わってしまうのが気になるなら、直線距離(length(worldPos - cameraPos))で測ります。作例では、この切り替えをパラメーター1つで行えるようにしています。
フォグのよくある失敗例と対処法
①霧の色と背景色(クリア色)が違う
上で実演したとおりです。遠景がシルエットで残るときは、まず背景色を疑います。
②方式を変えたのに、密度の数値をそのまま流用する
expとexp2は式が違うので、同じ密度でも見え方が全く違います。右下の画像は、exp2に、exp用の密度0.03を入れたものです。exp2は距離の2乗で効くので、霧がずっと濃くなり、遠くの柱や球がほとんど消えてしまいました。方式を切り替えたら、「何m先で隠したいか」から密度を計算し直します(5%残る距離なら、expは3.0 ÷ 距離、exp2は1.73 ÷ 距離が目安です)。
③頂点フォグで済ませてしまう
大きなポリゴンほど破綻します。必ずピクセル単位で計算します。
注意点
- 霧の色はクリア色・空の地平線の色と必ず合わせる
- 方式を比べるときは、「何m先で見えなくなるか」をそろえてから見た目を比べる
- フォグはピクセルシェーダーで計算する(頂点の少ない大きな面が壊れる)
- 広角のカメラでは、直線距離のほうが自然。奥行きZは端が薄くなる
- 霧を混ぜる対象はライティング済みの色(作例では
lit)。光の計算のあとに混ぜる
まとめ
- フォグは、距離から作った係数
fで「元の色」と「霧の色」を混ぜるだけ - リニア・exp・exp2は距離から
fを作る式が違うだけ。式どおりの色が出ていることを、画素から逆算して誤差0.7/255以内で確認した - 踏みやすい罠は背景色の不一致と頂点フォグ。どちらも画像にすれば一目で分かる
- 広角では直線距離を使うと、画面端の霧の濃さがそろう
リニア・exp・exp2は、距離から係数fを作る式が違うだけです。式どおりの色が出ていることを、画素から逆算して、誤差0.7/255以内で確認しました。踏みやすい罠は、背景色の不一致と、頂点フォグです。広角では、直線距離を使うと、画面端の霧の濃さがそろいます。


