シャドウアクネ

Shadow Acne

概要

シャドウマップで影を付けるとき、光が当たっているはずの面に、縞や斑点の模様の影が出てしまう現象。面が自分自身の影を誤って受けることで起きる。深度を比べるときに少しずらす(バイアスをかける)ことで抑える。

デモ

デモを読み込んでいます
白い線を左右にドラッグすると、バイアスなしとバイアスありを見比べられます。光源を低くするほど左側の縞が強くなり、十分に低くすると右側にも縞が戻ります。バイアスを大きくしすぎると、影が物体の根元から離れて浮きます。

使われる場面

  • リアルタイム 3D の影全般

    光源から見た深度を画像に記録するシャドウマップは、ゲームで広く使われている影の手法で、ほぼ必ずバイアスの調整が必要になる。

  • 朝や夕方の低い太陽

    光が地面をかすめるように当たると、面の傾きが深度の差として大きく現れ、縞が出やすい。

  • 広い屋外の場面

    一枚のシャドウマップで広い範囲を覆うと、1 画素が受け持つ範囲が広がり、縞が粗くなる。距離ごとに範囲を分けて複数のシャドウマップを使う手法(カスケードシャドウ)がある。

  • キャラクターの顔や服

    自分自身に影を落とす入り組んだ形では、細かな縞や、首元の影の浮きが目立ちやすい。

判断基準

こういうときこうする
光が当たっている面に、縞や斑点の模様が出るシャドウアクネである。深度のバイアスを少しずつ上げ、模様が消える最小の値にする。
光源の角度によって縞が出たり消えたりする面の傾きに応じてずらす量を変える(傾きに応じたバイアス、法線方向へのオフセット)。一定のバイアスを浅い角度に合わせると、他の場所で影が浮く。
影が物体の接地点から離れて浮いて見えるバイアスが大きすぎる(ピーターパンニング)。値を下げるか、法線方向へのオフセットに切り替える。
縞は消えたが、影の輪郭がギザギザしているバイアスでは直らない。シャドウマップの解像度を上げるか、影を落とす範囲を狭めるか、輪郭をぼかす処理を使う。
中身の詰まった閉じた形状が多い場面光源から見た裏の面を深度に書き込むと、表の面にアクネが出にくくなる。ただし薄い板のような形状では影が抜けることがある。

こう見えたら

  • 光が当たっている面に、細かい縞模様の影が出る光と影
  • 影が物体の足元から離れて、浮いて見える光と影

指示の例

  • 光が当たっている床や壁に、細かい縞模様の影が出ている。シャドウアクネだと思うので、影のバイアスを少し上げて

  • バイアスを上げたら、影が物体の根元から浮いてしまった。バイアスを戻して、代わりに法線方向のオフセットで縞を消して

  • 太陽が低い夕方の場面だけ縞が出る。面の傾きに応じてバイアスを変える設定にして

詳細

影は深度の比較で決まる

シャドウマップは、光源の位置から場面を描き、各画素に「光源からいちばん近い面までの距離(深度)」を記録した画像である。ある点が影かどうかは、その点の光源からの深度 dd と、シャドウマップの同じ位置に記録された深度 zz を比べて決める。d≤zd \le z なら光源との間に何もないので照らされ、d>zd > z なら手前に別の面があるので影になる。

面が自分の影を受ける理由

シャドウマップの 1 画素は、画素の中心で測った深度を一つだけ記録するが、実際には面のある広さを受け持っている。光に対して傾いた面では、その広さの中で深度が少しずつ変わる。記録された値より少しでも奥にある部分は d>zd > z と判定され、自分自身の影に入ったことになる。この誤判定が画素ごとに繰り返されるので、光の当たる面に縞や斑点の模様が並ぶ。これがシャドウアクネである。光が面をかすめるほど画素の中での深度の変化が大きくなるので、デモで光源を低くすると縞が強くなる。

比べるときに少しずらす

対策は、比べる前に深度を光源側へ少しずらすことである。ずらす量 bb をバイアスと呼び、d−b≤zd - b \le z なら照らされていると判定する。bb が小さすぎるとアクネが残り、大きすぎると本来の影まで消えて、影が物体の根元から離れる(ピーターパンニング)。

画素の中での深度の変化は、法線と光の方向のなす角 θ\theta が大きいほど大きい。そこで、傾きに応じてバイアスを増やす方法がよく使われる。

b=b0+ktan⁡θb = b_0 + k \tan\theta

比べる点そのものを法線の方向へ少し押し出す方法(法線方向へのオフセット)もあり、傾いた面でも影の浮きが出にくい。

float bias = max(0.0005, 0.005 * (1.0 - dot(n, l))); // 傾いた面ほど大きく
vec3 p = worldPos + n * normalOffset;                 // 法線方向へ押し出す方法
vec4 lp = lightViewProj * vec4(p, 1.0);
vec3 s = lp.xyz / lp.w * 0.5 + 0.5;                   // シャドウマップ上の位置と深度
float lit = (s.z - bias <= texture(shadowMap, s.xy).r) ? 1.0 : 0.0;

見分け方

シャドウアクネは、光が当たっている面の上に出る細かい縞で、光源やカメラが動くとうねるように動く。影の輪郭がギザギザになる現象とは別物で、こちらはシャドウマップの解像度が足りないことが原因なので、バイアスでは直らない。なお、このデモでは違いを見やすくするため、光源から見た表の面を深度に書き込む素朴な設定にしている。

関連