720

# 中級グラフィックス入門　～シャドウマッピング総まとめ～

レイトレ合宿4で発表予定だったスライドです。

#### Pocol

September 10, 2017

## Transcript

2. ### はじめに… • レイトレ合宿ですが… レイトレの話を一切しません！ 予めご了承ください。 • CEDEC 2015公募の落選したものを改変したものです。 落選したものなので内容についてはお察しください。 •

Web公開版は内容を変更してあります。 • 本スライドは，不特定多数の目に触れぬよう取り扱いを注意してく ださい。参加者限定で閲覧をお願いします。

13. ### シャドウマッピング法の基本 • ライトへの距離 を 測って 遮蔽を判定する シャドウマップ 深度値を求める 深度値を比較 透視投影変換後のXYZ座標をW値で割ることで

射影テクスチャの座標が求まる。 これをSampleCompLevelZero()などを使って， シャドウマップに格納された深度値と比較する

21. ### 深度傾斜バイアスって？ typedef struct D3D11_RASTERIZER_DESC { D3D11_FILL_MODE FillMode; D3D11_CULL_MODE CullMode; BOOL

FrontCounterClockwise; INT DepthBias; FLOAT DepthBiasClamp; FLOAT SlopeScaledDepthBias; BOOL DepthClipEnable; BOOL ScissorEnable; BOOL MultisampleEnable; BOOL AntialiasedLineEnable; } D3D11_RASTERIZER_DESC; • 深度の傾斜に応じてバイアスを設定する方法。 • 大抵のAPIでサポートされている。 使いたい場合は数値を設定するだけ APIでサポートされていない場合 の計算例は次の通り Bias = (float)DepthBias + SlopeScaledDepthBias * MaxDepthSlope; float MaxDepthSlope = max( abs( ddx( Depth ) ), abs( ddy( Depth ) ) );

23. ### エイリアシング誤差 • 2次元での話 ライト シャドウ平面 視錘台 視点 ※3次元の視錘台の側面から見ていると想定 = d

d ∈ 0,1 ∈ [0,1] 画像のテクスチャ座標 テクスチャの解像度 > 1 エイリアシング発生 シャドウマップのテクスチャ座標 シャドウマップの解像度 ∆ ∆

25. ### エイリアシング誤差 • 各平面上の点を求める = 0 + = − − −

= − − − = ∙ − 0 = ⊺ − 0 はシャドウマップによって覆われる の幅の一部 ハット付きは正規化済みベクトル はイメージ画像によって覆われる の幅の一部 を法線ベクトル と原点から距離を使って 平面の方程式を用いて行ベクトルで表現すると = ⊺, − シャドウマップ上のに対応する 正規化したライト画像平面座標は ∈ 0,1 で， これを用いてライト画像平面上の点 を表すと ライト位置を通過して線分Lに沿って 平面上へと射影することで次を得る 視点を通過して線分Eに沿って 平面 上へと射影することで次を得る と同様に視画像平面はによってパラメータ化され から次のように計算できる
26. ### エイリアシング誤差 d d = d d = ⊺ = +

− − − 2 − = − − + − 2 − = d d = d d = ⊺ − 0 は単位行列 = − − − = − − − = 0 + 商の導関数を利用 d d を求めたいが，直接解くことができないので，連鎖律(Chain Rule)を適用する 微分法において連鎖律とは、複数の関数が合成された合成関数を微分するとき， その導関数がそれぞれの導関数の積で与えられるという関係式のこと。 ′ = ′ − ′ 2
27. ### エイリアシング誤差 = = d d − − − − 長さの比率と考えると

と のなす角を とすると = − sin − = − と書ける = d d − − sin − − − とする − = −cos = d d − sin cos ※図は，D.Bradon Lloyd, “Logarithmic Perspective Shadow Maps”より引用
28. ### エイリアシング誤差 , , を で展開する = cos − sin =

−sin − cos d d = d d cos cos + sin sin cos d d − sin cos 代入 = d d cos − cos − = d d = d d = − = − = − = ⊺ − ⊺ d d cos cos
29. ### エイリアシング誤差 d d = ⊺ − ⊺ d d cos

cos ⊺ = cos ⊺ = sin = − sin = cos = cos cos + sin sin cos d d cos cos = cos − cos cos cos − = = d d cos cos cos cos = に求まった式を代入すると，

31. ### シャドウマップを良くする = d d > 1 エイリアシング発生 [解決策] ・シャドウマップのサイズを大きくする ・シャドウマップの1テクセルあたりの品質を上げる

・適当にごまかす ・シャドウマップを諦める 要は… シャドウマップの テクセルの割合が， カメラ視点での テクセルの割合 より大きければよい

38. ### 対処方法 • ライトが後ろにある場合は， 順番が正しくなるようにカメラを後ろに下げてしまう カメラを後退するということは 無駄な領域が増えるということ。 シャドウマップの品質が落ちる ※図は，Simon Kozlov, “Perspective

Shadow Maps: Care and Feeding”, GPU Gems, https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch14.html より引用
39. ### Practical Perspective Shadow Maps • 特殊な透視変換行列を使用する nearClip = a, farClip

= -aを設定した透視変換行列を作成 この特殊な透視変換行列を使用することで， 仮想カメラ無しで順番が保たれるようになる。 ※図は，Simon Kozlov, “Perspective Shadow Maps: Care and Feeding”, GPU Gems, https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch14.html より引用
40. ### 単位キューブクリッピング シャドウマップに描画されない無駄な領域を無くせば， シャドウマップのテクセルが多く割り当てることが出来るので品質が向上する 視錘台にあるシャドウを落とすオブジェクトのAABBを求めて 描画範囲を最適化する。 ※図は，Simon Kozlov, “Perspective Shadow Maps:

Care and Feeding”, GPU Gems, https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch14.html より引用
41. ### 疑似コード float4x4 ComputeUnitCubeClipMatrix(float3 mini, float3 maxi) { // 単位キューブクリップ行列を求める. float4x4

clip; clip.11 = 2.0f / ( maxi.x - mini.x ); clip.12 = 0.0f; clip.13 = 0.0f; clip.14 = 0.0f; clip.21 = 0.0f; clip.22 = 2.0f / ( maxi.y - mini.y ); clip.23 = 0.0f; clip.24 = 0.0f; clip.31 = 0.0f; clip.32 = 0.0f; clip.33 = 1.0f / ( maxi.z - mini.z ); clip.34 = 0.0f; clip.41 = -( maxi.x + mini.x ) / ( maxi.x - mini.x ); clip.42 = -( maxi.y + mini.y ) / ( maxi.y - mini.y ); clip.43 = - mini.z / ( maxi.z - mini.z ); clip.44 = 1.0f; return clip; }
42. ### Light Space Perspective Shadow Map PSMの欠点がなくなるように，ライトが垂直になるようにすりゃいんじゃね？ …というのがアイデア。 元々の論文通りのPSMは色々な問題を抱えていた • 透視変換のため，ライトが変化する

• 変化しないようにカメラを後ろに下げると品質落ちる • 遠い所がいい加減すぎる

51. ### Light Space Perspective Shadow Maps • 論文中での透視エイリアシングの定義 = 1 cos

cos 最適解は = 1 になること = 1 cos cos 定数扱い = ＝ = log 0 1 = つまり， = となればよいので 対数 ※図は，Michal Wimmer, Daniel Schezer, Werner Purgathofer, “Light Space Perspective Shadow Maps”, Eurographics Symposium on Rendering 2004 より引用

54. ### エリアシング誤差 >10 7.75 3.25 1 1 1 1 3.25 7.75

10 < >10 7.75 3.25 1 1 1 1 3.25 7.75 10 < ※図は，D.Bradon Lloyd, “Logarithmic Perspective Shadow Maps”より引用

57. ### 実装してみた。 • コンピュートシェーダで実装。 Githubでソースコード公開中。 https://github.com/ProjectAsura/LinearRasterizer https://github.com/ProjectAsura/LogarithmicRasterizer 線形ラスタライザ 対数ラスタライザ 実装が良くなかったので，やっぱり画面を占めるピクセルの割合が多くなると， コンピュートシェーダでもゲロ重。

論文に記載されているストリーム出力を使う方法は まだ試していないので，後で試してみる。

59. ### シャドウマップテクセル • 2次元での話 ライト シャドウ平面 視錘台 視点 ※3次元の視錘台の側面から見ていると想定 実際の大きさが シーン上で見えると

調整しやすい グリッドシェーダ による描画

65. ### エイリアシング誤差の可視化 • 実装例 float3 VisualizeError( float2 shadowCoord, float2 shadowMapSize )

{ const float3 values[] = { float3( 0.0f, 1.0/7.75, 1.0f/7.75f ), float3( 1.0f/7.75f, 1.0f/3.25f, 1.0f/3.25f - 1.0f/7.75f ), float3( 1.0f/3.25f, 1.0f, 1.0f - 1.0f/3.25f ), float3( 1.0f, 3.25f, 2.25f ), float3( 3.25f, 7.75f, 4.5f ), float3( 7.75f, 10.0f, 7.75f ), }; const float3 colors[] = { float3( 0.2f, 0.0f, 0.0f ), float3( 1.0f, 0.2f, 0.0f ), float3( 1.0f, 1.0f, 0.0f ), float3( 0.0f, 1.0f, 0.0f ), float3( 0.3f, 0.8f, 1.0f ), float3( 0.0f, 0.0f, 1.0f ), float3( 0.0f, 0.0f, 0.2f ) }; float2 ds = shadowMapSize.x * ddx( shadowCoord ); float2 dt = shadowMapSize.y * ddy( shadowCoord ); float error = max( length( ds + dt ), length( ds - dt ) ); float3 result = (float3)1.0f; [unroll] for( int i=0; i<6; ++i ) { if ( error >= values[i].x && error < values[i].y ) { result = lerp( colors[i], colors[i+1], (error - values[i].x) / values[i].z ); break; } else { result = colors; } } return result; } 1次元テクスチャ化可能 1回のテクスチャフェッチに変更可能 誤差を求める計算
66. ### エイリアシング誤差の可視化 • 簡略化版の実装例 Texture1D ErrorTexture : register( t0 ); SamplerState

ErrorSampler : register( s0 ); float3 VisualizeError( float2 shadowCoord, float2 shadowMapSize ) { float2 ds = shadowMapSize.x * ddx( shadowCoord ); float2 dt = shadowMapSize.y * ddy( shadowCoord ); float error = max( length( ds + dt ), length( ds - dt ) ); return ErrorTexture.Sample( ErrorSampler, error ).rgb; }
67. ### 可視化について • デモ作成でかなり重宝した。 – どこが悪いかが一目瞭然。 – 可視化することで調整しやすくなる – 単なる目視だと見逃しがちな所も気を付けられる 実装も手軽なので

導入することを是非お勧めします！ その他の可視化方法については，以下を参照すると良いです。 Fan Zhang, Chong Zhao and Adrian Egli “Visualize Your Shadow Map Techiniques”, GPU Pro, pp.15-29

69. ### Parallel-Split Shadow Maps = log + 1 − uni 0

< < 1 対数分割スキーム 均一分割スキーム uni = + − log = : ファークリップ平面までの距離 ∶ ニアクリップ平面までの距離 : カスケードの最大段数 : 現在のカスケードの段数 を用いて カスケードの分割位置を求める

71. ### スイミング問題 • フリッカリングが発生するのは3パターン – パターン1. スケール 次のフレーム 次のフレームでシャドウマップのテクセルサイズが変わる。 その結果，シャドウテストの結果が前フレームと異なる。 関心がある点

シャドウマップテクセル ラスタライズされた状態の線 ライトのビューポイント

73. ### オフセットの分かりやすい例 ※動画は Silicon Studio, “Rendering Engine Mizuchi Tech Demo –

”Museum“” , https://youtu.be/sYX9I3ONHc4?t=53sより引用

76. ### 疑似コード // Step1 : ライトの基底を計算 Vector3 dir = Normalize(World.GetColumn3(2)); //

ライトのZ軸 Vector3 side = Normalize(World.GetColumn3(1)); // ライトのX軸 Vector3 up = Normalize(World.GetColumn3(0)); // ライトのY軸 // 全てのスプリットを更新 for(auto i=0; i<SplitCount; ++i) { // Step2 : 各スプリット'split[i]' のバウンディングスフィア 'bs' を計算する Sphere bs = CalculateBoundingSphere(split[i]); // ワールド空間におけるスフィアの中心座標を取得する Vector3 center = InvWorldView * bs.GetCenter(); // スフィアの半径を取得する float radius = bs.GetRadius(); // シャドウマップへの中心へとスフィアの中心を移動させ調整する float x = ceilf( Dot(center, up) * sizeSM / raidus ) * radius / sizeSM; // 'sizeSM' はテクスチャサイズ. float y = ceilf( Dot(center, side) * sizeSM / radius ) * radius / sizeSM; center = up * x * side * y + dir * Dot(center, dir); // Step3 : スプリットを指定するビュー行列'matrixView[i]'と射影行列'matrixProjection[i]'を更新する Vector3 origin = center - dir * far; // ライトの位置を取得する. matrixProjection[i] = MatrixOrthoProjection(-radius, radius, radius, -radius, near, far); matrixView[i] = MatrixLookAt(origin, center, up); }
77. ### 利点と欠点 • 利点 – フリッカリングを正確に排除できる • 欠点 – シャドウマップの解像度が無駄に使われる =シャドウの品質が落ちる

– 均一なテクセル分布にしか使えない =Projective Warping Algorithmと一緒に使えない
78. ### 近似による解決方法 • スフィアを使わず，オフセットとスケールを回避する – Fan Zhang, Alexander Zaprjagaev, Allan Bentham

“Practical Cascaded Shadow Maps”, ShaderX7 pp.305-329. – オフセットを取り除くために，視錘台に相対な位置の量子化を用いる。 – スケールを取り除くために，事前定義された離散化レベルの範囲へとスケール値を量子化。 中心 スプリット ライトの錘台 ライト 視錘台 スケールの問題 オフセットの問題 シャドウマップ シャドウマップ 回転 ライト サイドビュー サイドビュー スプリット
79. ### 疑似コード // スケール値を計算する. // Notes: ライトの透視変換後の空間において，ビューポートの幅と高さは両方とも2である。 // 指定のスプリットに対するシャドウマップの大きさは，この空間におけるスプリットの角点で構成される境界の座標となる。 // 'maxX'と'minX'はそれぞれX値の最大値と最小値。

// 'maxY'と'minY'はそれぞれY値の最大値と最小値。 float scaleX = 2.0f / (maxX - minX); // X方向に対するスケール値. float scaleY = 2.0f / (maxY - minY); // Y方向に対するスケール値. // スケール値は64の離散レベルへと量子化する.(ShaderX7では経験的に64が良いと分かったためとの記述があります). float scaleQuantizer = 64.0f; // 量子化スケール scaleX = 1.0f / ceilf(1.0f / scaleX * scaleQuantizer) * scaleQuantizer; scaleY = 1.0f / ceilf(1.0f / scaleY * scaleQuantizer) * scaleQuantizer; // オフセット値を計算 float offsetX = -0.5f * (maxX + minX) * scaleX; // X方向に対するオフセット. float offsetY = -0.5f * (maxY + minY) * scaleY; // Y方向に対するオフセット. // 量子化オフセット float halfTextureSize = 0.5f * sizeSM; offsetX = ceilf(offsetX * halfTextureSize) / halfTextureSize; offsetY = ceilf(offsetY * halfTextureSize) / halfTextureSize;
80. ### ストレージ戦略 • カスケードシャドウマップをどうやって持つ？ – 別個のテクスチャ – テクスチャアトラス – テクスチャ配列 –

キューブマップ 経験則として， ジオメトリシェーダを 使わない方法がお勧め v 0 1 2 3 (0, 1.0) (0, 0.5) (1, 0.5) (1, 0.5) (0.5, 1) (0, 0) (0, 1) u ジオメトリシェーダが走ることが遅い原因になる テクスチャアトラスがお勧め！
81. ### http://www.gamedev.net/topic/650183-cascaded-shadows-maps-texture-atlas-or-texture-array/ より引用 1) 全部一度描画し，GSで複製して，2048 ×2048のテクスチャに1024×1024の1/4ずつ描画する → FPS = 21.1 2)

全部一度描画し，GSで複製して，1024 × 1024のテクスチャ配列を使用 → FPS = 22.4 3) インスタンス数を4にして全部一度描画し，2048 × 2048のテクスチャに1024 × 1024の1/4ずつ描画する → FPS = 42.7 4) 各カスケードに対して，カスケードごとにドローコールをサブミットして，テクスチャ配列を使用する → FPS = 42.1 自分の経験則とも合致。最適化無しのやっぱりジオメトリシェーダを使うと遅い！
82. ### スプリットをまたぐ場合は？ テクスチャアトラスを使う場合で境界をまたいでしまう場合の対策 → Andrew Lauritzen, “Parallel-Split Variance Shadow Maps”, In

Proceedings of Graphics Interface 2007 でパフォーマンスを犠牲にせず，うまく解決する方法が記述されている。 スプリット境界 現在のフラグメント 隣のフラグメント スプリット番号の計算 if Split ≠ Split∗ ⇔ 2Split∗ − 2Split = 2Split > 0 ⇔ 2Split ∗ − 2Split > 0 ⇔ ddx 2 > 0 then output Split endif z 矩形 TextureMatrix[split]によって変換される u, v u∗, v∗ PSSM[split]についての テクスチャ空間 u x = u∗ − u PSSM[split]のミップマップチェイン 正しいLOD!
83. ### 疑似コード // 定数 const int SPLIT_COUNT = 3; //分割数. //

ピクセルシェーダ int powerSplitIndex = pow(2, splitIndex); int dx = abs(ddx(powerSplitIndex)); int dy = abs(ddy(powerSplitIndex)); int dxy = abs(ddx(dy)); int split = max(dxy, max(dx, dy)); //微分値の最大値を得る if (powerIndex > 0) //ミスマッチが発生した場合 splitIndex = log(powerSplitIndex); //スプリット番号を更新 // 2SplitIndex // 2SplitIndex / // 2SplitIndex / // 2 2SplitIndex /

85. ### Sample Distribution Shadow Maps 適用方法： 1.深度バッファを得る （深度プリパス or G-Bufferパスから） 2.

コンピュートシェーダを用いて 深度バッファの最大・最小値を削減 (描画されない無駄な領域を削減するため) 3.削減された深度の最大・最小値から対数分割による領域を求める。 4.PSSMと同じようにライトのビュー射影行列を生成する。 5.生成した行列をシャドウマップに適用する。
86. ### Sample Distribution Shadow Maps • メリット – 影が凄く綺麗になる。 – カスケードのつなぎ目が滑らかになる

– 最悪のケースでもPSSMと同じ品質 – シーン依存 • デメリット – ライト空間の分割計算をGPUでやる必要がある – 他の手法に比べるとトリッキーな実装 – CPUによる視錐台カリングが適用できない (GPU上で分割データが生成されるため) • ゲームでの採用事例あり – The Order : 1886 – Destiny
87. ### 比較 SDSM off ※図は Matte Pettineo, “Rendering the Alternate History

of The Order: 1886”, SIGGRAPH 2005 Advances in Real-Time Rendering in Games course より引用
88. ### 比較 SDSM on ※図は Matte Pettineo, “Rendering the Alternate History

of The Order: 1886”, SIGGRAPH 2005 Advances in Real-Time Rendering in Games course より引用

on Graphics Hardware Rectilinear Texture Warping for Fast Adaptive Shadow Maps ※図は，Paul Rosen, “Rectilinear texture warping for fast adaptive shadow maps”, i3D 2012 より引用

91. ### Rectilinear Texture Warping for Fast Adaptive Shadow Maps 大事な所だけピクセルを多く割り当てりゃよくね？ ・・・というアイデア

(a) 重要度マップを構築 (b) 2次元重要度マップを1次元のワーピングマップに変換 (1) XY方向を1次元の重要度マップへと落とし込む (2) 重要度マップにブラーを掛ける (3) 重要度マップからワーピングマップを構築 (c) RTWシャドウマップを描画する (d) 要求された視点からの出力画像を描画する ※図は，Paul Rosen, “Rectilinear texture warping for fast adaptive shadow maps”, i3D 2012 より引用
92. ### 重要度マップの生成 • 3つのシーンの解析手法 – Forward Analysis ：ライトからみた視点で解析。 – Backward Analysis

：カメラからみた視点で解析。 – Hybrid Analysis ：上記２つの組み合わせ。 • 解析には重要度関数を使用 – Desired View Function • レシーバーが見えたら1を返却，見えなかったら0を返却。 • Forward Analysis, Backward Analysisの両方で使用。 – Distance to Eye Function • Forward Analysis ⇒ 1.0 – (ビュー空間での深度値) • Backward Analysis ⇒ 1.0 – (深度値) – Shadow Edge Function • 隣接する8ピクセルに対して，2ピクセル間の差分を求めて不連続テストを行う。 • Forward Analysisに対して使用。 – Surface Normal Function • 1 + β・saturate( dot( -normal, viewDirection ) ) ※βは付加重要度で，論文中では2.0を使用。 • 3つの解析手法すべてについて使用できる。
93. ### ワーピングマップの構築 (1) 2次元重要度マップの縦・横方向それぞれについて，最大重要度を求める。 (2) ガウシアンブラーを用いて，隣接するセルと重要度値をブレンドする。 ⇒ ブラーによる2つの重要な効力 ① 一貫性を保証するための，サンプリング上でのスムーズな変化を生成。 ②非線形ラスタライズ化によるエラーを低減。

(3) 下記の式を用いて，変位を求めてワープマップを生成。 【不要】黒 【必要】ダークブルー = max (0 , 1 , 2 , ⋯ ) = max (0 , 1 , 2 , ⋯ ) = −1 =1 =1 − ※図は，Paul Rosen, “Rectilinear texture warping for fast adaptive shadow maps”, i3D 2012 より引用
94. ### シャドウマップの描画 • 各頂点に対する出力領域を計算する • ワープを適用することによって 出力画像平面上の位置 を求める • 最終的に三角形が ライスタライズされる

V M P L   L M ：ビュー射影行列 V ：頂点 ' P     y y y x x x P GetWarpY P P P GetWarpX P P     ' ' はクリップ空間なので(-1.0 ～ +1.0 ) y x P P , GetWarp メソッドはテクスチャ空間なので(0.0 ～1.0) GetWarpY GetWarpX, はクリップ空間に変換する必要あり ※図は，Paul Rosen, “Rectilinear texture warping for fast adaptive shadow maps”, i3D 2012 より引用
95. ### 出力フレーム画像を生成 • Forward Analysisの場合 ライトの視点で解析しているので，通常のシャドウマップと同じ。 シーンのジオメトリをもう一度描画する必要があり， RTWシャドウマップで陰影づけをする。 • Backward, Hybrid

Analysisの場合 カメラからの視点で解析しているので，描画イメージがある状態。 シャドウのみの計算が必要で，画像の上に合成する。 • シャドウマップのテクスチャ座標は，ワーピングによって求める。     t GetWarpT t t s GetWarpS s s     ' ' ※図は，Paul Rosen, “Rectilinear texture warping for fast adaptive shadow maps”, i3D 2012 より引用
96. ### 制限事項 • Forward Analysisの場合 – 初期の深度バッファの解像度があまりに小さいときに， 重要なディテールが失われる可能性があり， 重要度マップから除外されてしまう。 ⇒ 高解像度で深度バッファを描画することによって処理することが可能だが，

適切なサイズの見つけ方が難しい問題。 • Backward Analysisの場合 – 視点が異なる場合は，同じ重要度マップの位置に異なる重要度 で射影される可能性がある。 ⇒ 高い視点を保持することで対応。（それでいいのか？） • テッセレーションが不十分な場合にアーティファクトが発生する。 – 非線形ラスタライゼーションがサポートされていないため ⇒ 適度に細かくしようね。

101. ### サンプリングを変える 各社みんな頑張っているみたいです…。 ※ 岩崎純一, 山口裕也, “FINAL FANTASY 零式HD にみる新しいHDリマスター”, Kansai

CEDEC 2015, p.75 より引用
102. ### 高速化している一方で… • とあるデモプロジェクトでは品質が最優先。 – でも，シャドウに割り当てるレンダリングバジェットがない。 – フィルタリングは入れたい。 – でも，ブラーパスを入れるバジェットが無い。 •

少し重めのフィルタリング手法をデモで採用(?) – Holger Gruen, “Fast Conventional Shadow Filtering”, GPU Pro – 多項式を用いて，8×8のPCFフィルタリングを16PCF操作で済ますような方法。 （くそまじめに計算すると49PCF操作が必要） – GatherCmp命令を使うため，ShaderModel 5.0以上が必須。 – 現在のMuzuchiでは完全に無効化(#if 0 - #endif)され使えない状態に… – 興味がある人はGPU Pro1 pp.415-445を参照されたし。 非公開スライド
103. ### ちなみに，シェーダはこんな感じ。 float ConventionalSample9x9 ( Texture2DArray shadowMap, SamplerComparisonState shadowSmp, float2 texcoord,

float compareZ ) { float2 mapSize; float elements; float numMipMapLevels; shadowMap.GetDimensions(0, mapSize.x, mapSize.y, elements, numMipMapLevels ); const int GS = 9; const int GS2 = GS / 2; float4 s = 0.0f; float2 stc = ( mapSize * texcoord.xy ) + float2( 0.5, 0.5 ); float2 tcs = floor( stc ); float2 tc = tcs / mapSize; float2 fc = stc - tcs; float w = 0.0f; float4 v1[ GS2 + 1 ]; float2 v0[ GS2 + 1 ]; int row; int col; const float arrayIdx = texcoord.z; for( row = 0; row < GS; ++row ) { for( int col = 0; col < GS; ++col ) { w += W9x9[ row ][ col ]; } } [unroll] for( row = -GS2; row <= GS2; row +=2 ) { [unroll] for( int col = -GS2; col <= GS2; col += 2 ) { float sumOfWeights = W9x9[ row + GS2 ][ col + GS2 ]; if ( col > -GS2 ) { sumOfWeights += W9x9[ row + GS2 ][ col + GS2 - 1 ]; } if ( col < GS2 ) { sumOfWeights += W9x9[ row + GS2 ][ col + GS2 + 1 ]; } if ( row > -GS2 ) { sumOfWeights += W9x9[ row + GS2 - 1 ][ col + GS2 ]; if ( col < GS2 ) { sumOfWeights += W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ]; } if ( col < -GS2 ) { sumOfWeights += W9x9[ row + GS2 - 1 ][ col + GS2 - 1 ]; } } if ( sumOfWeights != 0.0f ) { // ShaderModel 5.0 でコンパイルする必要あり. v1[ ( col + GS2 ) / 2 ] = shadowMap.GatherCmp( shadowSmp, float3(tc, arrayIdx), compareZ, int2( col, row ) ); } else { v1[ ( col + GS2 ) / 2 ] = 0.0f; } if ( col == -GS2 ) { s.x += ( 1 - fc.y ) * ( v1.w * ( W9x9[ row + GS2 ][ col + GS2 ] - W9x9[ row + GS2 ][ col + GS2 ] * fc.x ) + v1.z * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 ] - W9x9[ row + GS2 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 ][ col + GS2 + 1 ] ) ); s.y += ( fc.y ) * ( v1.x * ( W9x9[ row + GS2 ][ col + GS2 ] - W9x9[ row + GS2 ][ col + GS2 ] * fc.x ) + v1.y * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 ] - W9x9[ row + GS2 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 ][ col + GS2 + 1 ] ) ); if ( row > -GS2 ) { s.z += ( 1 - fc.y ) * ( v0.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 ] - W9x9[ row + GS2 - 1 ][ col + GS2 ] * fc.x ) + v0.y * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 ] - W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) ); s.w += ( fc.y ) * ( v1.w * ( W9x9[ row + GS2 - 1 ][ col + GS2 ] - W9x9[ row + GS2 - 1 ][ col + GS2 ] * fc.x ) + v1.z * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 ] - W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) ); } } else if ( col == GS2 ) { s.x += ( 1 - fc.y ) * ( v1[ GS2 ].w * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 - 1 ] - W9x9[ row + GS2 ][ col + GS2 ] ) + W9x9[ row + GS2 ][ col + GS2 ] ) + v1[ GS2 ].z * fc.x * W9x9[ row + GS2 ][ col + GS2 ] ); s.y += ( fc.y ) * ( v1[ GS2 ].x * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 - 1 ] - W9x9[ row + GS2 ][ col + GS2 ] ) + W9x9[ row + GS2 ][ col + GS2 ] ) + v1[ GS2 ].y * fc.x * W9x9[ row + GS2 ][ col + GS2 ] ); if ( row > -GS2 ) { s.z += ( 1 - fc.y ) * ( v0[ GS2 ].x * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 - 1 ] - W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + v0[ GS2 ].y * fc.x * W9x9[ row + GS2 - 1 ][ col + GS2 ] ); s.w += ( fc.y ) * ( v1[ GS2 ].w * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 - 1 ] - W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + v1[ GS2 ].z * fc.x * W9x9[ row + GS2 - 1 ][ col + GS2 ] ); } } else { s.x += ( 1 - fc.y ) * ( v1[ ( col + GS2 ) / 2 ].w * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 - 1 ] - W9x9[ row + GS2 ][ col + GS2 ] ) + W9x9[ row + GS2 ][ col + GS2 ] ) + v1[ ( col + GS2 ) / 2 ].z * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 ] - W9x9[ row + GS2 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 ][ col + GS2 + 1 ] ) ); s.y += ( fc.y ) * ( v1[ ( col + GS2 ) / 2 ].x * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 - 1 ] - W9x9[ row + GS2 ][ col + GS2 ] ) + W9x9[ row + GS2 ][ col + GS2 ] ) + v1[ ( col + GS2 ) / 2 ].y * ( fc.x * ( W9x9[ row + GS2 ][ col + GS2 ] - W9x9[ row + GS2 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 ][ col + GS2 + 1 ] ) ); if ( row > -GS2 ) { s.z += ( 1 - fc.y ) * ( v0[ ( col + GS2 ) / 2 ].x * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 - 1 ] - W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + v0[ ( col + GS2 ) / 2 ].y * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 ] - W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) ); s.w += ( fc.y ) * ( v1[ ( col + GS2 ) / 2 ].w * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 - 1 ] - W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 ] ) + v1[ ( col + GS2 ) / 2 ].z * ( fc.x * ( W9x9[ row + GS2 - 1 ][ col + GS2 ] - W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) + W9x9[ row + GS2 - 1 ][ col + GS2 + 1 ] ) ); } } if ( row != GS2 ) { v0[ ( col + GS2 ) / 2 ] = v1[ ( col + GS2 ) / 2 ].xy; } } } return dot( s, 1.0f ) / w; } ※全部で1関数です。 非公開スライド
104. ### 最終的には… Fast Conventional Shadow Filtering も重たいため入れられず。 ハードウェアでPCFを掛けたサンプルを何点かとり， その結果をシェーダ上で，更にバイリニア補間。 ものすご～く力技。 自前で計算するほうがちょっとだけ綺麗になった。

あんまり，イケてないやり方なので 真似するのはやめましょう。 最終版は4Tap
105. ### Variance Shadow Map 影かどうかをバイナリではなくて確率的に求められればいいんじゃね？ …というアイデア チェビシェフの不等式 ≥ ≤ ≡ 2

2 + − 2 = = 1 2 = 2 − 2 = 2 − 1 2 平均値 分散 深度値 1 = = ∞ −∞ 2 = 2 = 2 ∞ −∞ フィルタ領域について のモーメント 任意の確率分布について ある値以上または以下である 確率がどれぐらいであるのか？ 大体の見当をつけるのに用いる
106. ### 疑似コード float ChebyshevUpperBound(float2 Moments, float t) { // 片側不等式が有効なのは， t

> Moments.xの場合 float p = (t <= Moments.x); // 分散を計算 float Variance = Moments.y – (Moments.x * Moments.x); Variance = max(Variance, g_MinVariance); // 確率的上限の計算 float d = t – Moments.x; float p_max = Variance / (Variance + d * d); return max(p, p_max); } float ShadowContribution(float2 LightTexCoord, float DistanceToLight) { // 分散シャドウマップからモーメントを読み込む. float Moments = texShadow.Sample(ShadowSampler, LightTexCoord).xy; // チェビシェフ上限の計算 return ChebyshevUpperBound(Momemtns, DistanceToLight); }
107. ### バイアス • PCFはフィルタカーネルが大きくなるとシャドウアクネが出ることがある。 • VSMの第2モーメントを使ったバイアス問題の解決手法が提案されている。 , = + + 2

= 2 = 2 + 2 2 + 2 2 2 = 2 2 = 2 = 2 = 1 2 2 = 1 4 2 = 2 + 1 4 2 + 2 局所的に平面的な分布と考え，次の式で表現する。 次に，2を計算する。期待値演算の線形性と = = = 0という事実より ハーフピクセルの標準偏差を持つ対称性のあるガウス分布としてピクセルを表現するので，次を得る これより 第2モーメントを使って1ピクセルを表現することを考える
108. ### 疑似コード // 分散シャドウマップ描画時のモーメント計算 float ComputeMoments(float Depth) { float2 Moments; //

1つの目のモーメントは深度そのもの Moments.x = Depth; // 深度の偏微分を計算 float dx = ddx(Depth); float dy = ddy(Depth); // 2つ目のモーメントをピクセル範囲で計算 Moments.y = Depth * Depth + 0.25 * (dx * dx + dy * dy); return Moments; }

111. ### なぜ発生する？ オブジェクトA, B, Cの深度値をそれぞれ , , とする。 1 = +

2 2 = 2 + 2 2 = + 2 2 = − 2 4 ∆ = − ∆ = − を不等式の右辺に代入 ∆ = 1 4 ∆2 1 4 ∆2 + ∆ + 1 2 ∆ 2 = 1 4 ∆2 1 2 ∆2 + ∆∆ + ∆2 ※図は，Andrew Timothy Lauritzen, “Rendering Antialiased Shadows using Warped Variance Shadow Maps”, より引用
112. ### なぜ発生する？ ∆が与えられると， ∆ は 1 ∆2 のようなフォールオフになる ∆ = 1

4 ∆2 1 2 ∆2 + ∆∆ + ∆2 一方，オブジェクトBよりも， ライトから遠いオブジェクトCは Bによって完全に遮蔽されるので， 正しい可視性は全ての∆ > 0についてゼロ この相違がライトブリーディング
113. ### じゃ，どうする？(1) • 単純な解決方法 ある最小強度未満の値を0にクランプして， 残りの値を0（最小強度）から1にマップされるようにmax を修正する。 再マッピングする float linstep(float mini,

float maxi, float v) { return clamp((v – mini) / (maxi – mini), 0, 1); } float ReduceLightBleeding(float p_max, float Amount) { return linstep(Amount, 1, p_max); } ※図は，Andrew Lauritzen, “Summed-Area Variance Shadow Maps”, GPU Gems3, https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch08.html より引用
114. ### じゃ，どうする？(2) • もうちょっと違う解決方法 Wojciech Sterna, “Variance Shadow Maps Light-Bleeding Reduction

Tricks”, GPU Pro2 シャドウのエッジを検出して， シャドウのエッジ境界部分にだけVSMを適用する。 通常のVSM エッジにVSMを適用
115. ### 疑似コード(1) float shadowEdgeDetection(float2 projTexCoord, float depth, float bias) { float

halfTexelSize = 0.5f / shadowMapSize; float2 offsets = { float2(-halfTexelSize, -halfTexelSize), float2(-halfTexelSize, +halfTexelSize), float2(+halfTexelSize, -halfTexelSize), float2(+halfTexelSize, +halfTexelSize) }; float samplesSum = 0.0f; for (int i = 0; i < 4; i++) { if (tex2D(shadowMapSampler_point, projTexCoord.xy + offsets[i]).r > depth - bias) samplesSum += 1.0f; } return samplesSum; }
116. ### 疑似コード(2) PS_OUTPUT main(PS_INPUT input) { PS_OUTPUT output; input.normal = normalize(input.normal);

input.projTexCoord.x = +0.5f * input.projTexCoord.x + 0.5f; input.projTexCoord.y = -0.5f * input.projTexCoord.y + 0.5f; float depth = input.projTexCoord.z; float4 ambient = g_Ambient; float4 diffuse = max(dot(input.normal, -lightDirection), 0.0f); float bias = g_Bias; float samplesSum = shadowEdgeDetection(input.projTexCoord.xy, depth, bias); if (samplesSum < g_Threshold1) output.color = ambient; else if (samplesSum > g_Threshold2) output.color = ambient + diffuse; else output.color = ambient + diffuse * VSM(input.projTexCoord.xy, depth, bias, VSM_tailCutOff); return output; }
117. ### Layered Variance Shadow Map VSM LVSM 深度を複数のレイヤーに分割してしまって 異なるレイヤー間でのライトブリーディングを排除すればよくね？ …というアイデア。 標準のVSMよりは良い結果が得られるが，

ライトブリーディングを完全に避けるためには，多くレイヤーが必要。 ※図は，Andrew Timothy Lauritzen, “Rendering Antialiased Shadows using Warped Variance Shadow Maps”, より引用
118. ### Convolution Shadow Maps 田村尚希, “レンダリスト養成講座 2.0 ～Real-Time, All-Frequency Shadows in

Dynamic Scenes～”, CEDEC 2008 を参照されたし。
119. ### Exponential Shadow Map 影かどうかを0か1ではなく， 指数関数で滑らかにつなげばいいんじゃね？ …というアイデア ※図は，Louis Bavoil, “Advanced Soft

Shadow Mapping Techniques”, Game Developers Conference 2008 より引用
120. ### 疑似コード // ESMキャスタ描画 float4 ESM_Caster( const VS_OUTPUT v ) :

COLOR { float4 ret = exp( (v.vTexPos.w + fOffset) * fLargeConstant ); return ret; } // ESMレシーバ描画 float4 ESM_Receiver( const VS_OUTPUT v ) : COLOR { float ez = tex2Dproj( s1, v.vTexPos ).x; float ed = exp( -v.vTexPos.z * fLargeConstant ); float4 col = v.vColor; col.rgb *= saturate( ed * ez - fThreshold ); return col; }
121. ### アーティファクト • シャドウが欠損する問題が発生する ※図は，Louis Bavoil, “Advanced Soft Shadow Mapping Techniques”,

Game Developers Conference 2008 より引用

123. ### Exponential Variance Shadow Maps • 深度値と深度値の2乗を格納する代わりに， 指数深度と指数深度値の2乗を格納する。 • 指数関数は∆ ∆

の比率を減らす効果がある。 従って，VSMのようなライトブリーディングを低減できる。 • EVSMはVSMとESMの両方が失敗する場合のみ， ライトブリーディングの影響を受ける。 • VSMの再マッピングとESMの定数の2つの要素による ライトブリーディング制御ができる。
124. ### 疑似コード float EVSM ( float2 projTexCoord, float depth, float bias,

float tailCutoff, float k ) { float2 moments = tex2D(shadowMapSampler_linear, projTexCoord).rg; if (moments.x >= exp(k * (depth - bias))) return 1.0f; float variance = moments.y - moments.x*moments.x; float delta = exp(ESM_k * depth) - moments.x; float p_max = variance / (variance + delta*delta) - tailCutoff; return saturate(p_max); }
125. ### Exponentially Warped Gaussian Filtering • Jesus Gumbau, Mateu Sbert, et.al,

“Smooth shadow boundaries with exponentially warped Gaussian filtering”, Computers and Graphics, 37(3):214–224, 2013. In this paper we propose a new statistical filtering method that approximates the cumulative distribution function (CDF) of depth values by a Gaussian CDF instead of bounding it with Chebyshev Inequality. This approximation significantly reduces ‘‘light leaks’’ and has similar performance and storage requirements compared to the original variance shadow map method. We also show that the combination of this technique with an exponential warp allows us to further reduce the remaining shadowing artifacts from the rendered image. ・累積分布関数を使うらしい… ・まだちゃんと論文読めていないです。 ・きちんと調査して，BlogにUpしたいな。 （希望的観測） ・ライトブリーディングが削除される。 ・速度はEVSMよりも遅い。
126. ### Moment Shadow Map VSMは2次までのモーメントを使用。 じゃ，もっと高次のモーメント使えばいいんじゃね？ …というアイデア。 ※理論が難しくて良く理解できなかったので，詳細は論文(+Appendix)を参照してください。 モーメント問題に対して仮説を立てて条件付けをして，うまく解いているようです。 ※図は，Christoph Peter

and Reinhard Klein, “Moment Shadow Mapping”, i3D 2015 より引用
127. ### Moment Shadow Map • 4つのモーメントを使用（, 2, 3, 4) • VSMのように2段階で生成

(1) シーンを描画し，4つのモーメントをシャドウマップに格納。 (2) フィルタを掛ける（ガウスブラーまたはミップマップ生成） • 次の手順によりシャドウ強度を求める。 1. ′ ≔ 1 − ∙ + ∙ 0.5, 0.5, 0.5, 0.5 ⊺ 2. ∈ ℝ3について解くためにコレスキー分解を使用する： 1 1 ′ 2 ′ 1 ′ 2 ′ 3 ′ 2 ′ 3 ′ 4 ′ ∙ = 1 2 3. 2次方程式の解の公式を用いてについて3 ∙ 2 + 2 ∙ + 1 = 0を解く。 2 ≤ 3 である2 , 3 ∈ ℝも同様にして解く。 4. if ≤ 3 ：return ≔ 0 5. else if ≤ 3 : return ≔ ∙3−1 ′∙ +3 +2 ′ 3−2 ∙ −2 6. else : return ≔ 1 − 2∙3−1 ′∙ 2+3 +2 ′ −2 ∙ −3 ：フィルタされたシャドウマップのサンプル ∈ ℝ4 ：ピクセルの深度値 : バイアス 例： = 3 ∙ 10−5 , : シャドウ強度 入力 出力 ′ ≔ 1 − ∙ + ∙ ∗ ∗ ：定数ベクトル 1 2 , 1 2 , 1 2 , 1 2 ⊺
128. ### 疑似コード float ComputeMSMHamburger(float4 moments, float fragmentDepth , float depthBias, float

momentBias) { float4 b = lerp(moments, float4(0.5f, 0.5f, 0.5f, 0.5f), momentBias); float3 z; z = fragmentDepth - depthBias; float L32D22 = mad(-b, b, b); float D22 = mad(-b, b, b); float squaredDepthVariance = mad(-b, b, b); float D33D22 = dot(float2(squaredDepthVariance, -L32D22), float2(D22, L32D22)); float InvD22 = 1.0f / D22; float L32 = L32D22 * InvD22; float3 c = float3(1.0f, z, z * z); c -= b.x; c -= b.y + L32 * c; c *= InvD22; c *= D22 / D33D22; c -= L32 * c; c -= dot(c.yz, b.xy); float p = c / c; float q = c / c; float D = (p * p * 0.25f) - q; float r = sqrt(D); z =- p * 0.5f - r; z =- p * 0.5f + r; float4 switchVal = (z < z) ? float4(z, z, 1.0f, 1.0f) : ((z < z) ? float4(z, z, 0.0f, 1.0f) : float4(0.0f,0.0f,0.0f,0.0f)); float quotient = (switchVal * z - b * (switchVal + z) + b)/((z - switchVal) * (z - z)); float shadowIntensity = switchVal + switchVal * quotient; return 1.0f - saturate(shadowIntensity); }
129. ### 比較結果 https://youtu.be/ThyWHCrYniA?t=59s ※図は，Christoph Peter and Reinhard Klein, “Moment Shadow Mapping”,

http://cg.cs.uni-bonn.de/en/publications/paper-details/peters-2015-msm/より引用
130. ### 比較結果 https://youtu.be/ThyWHCrYniA?t=59s ※図は，Christoph Peter and Reinhard Klein, “Moment Shadow Mapping”,

http://cg.cs.uni-bonn.de/en/publications/paper-details/peters-2015-msm/より引用
131. ### 比較結果 https://youtu.be/ThyWHCrYniA?t=59s ※図は，Christoph Peter and Reinhard Klein, “Moment Shadow Mapping”,

http://cg.cs.uni-bonn.de/en/publications/paper-details/peters-2015-msm/より引用

133. ### Percentage Closer Soft Shadow • 3ステップでシャドウイングする – Step1.ブロッカー探索 • シャドウマップを探索し，シェーディング点よりライトに近い方向に

ブロッカーがある場合は，ブロッカーの深度値の合計値を計算し， ブロッカー数で合計値を割り，深度平均を求める。 – Step2.半影推定 • ブロッカー／レシーバーのライトからの距離とライトサイズに基づき， 次式により半影を推定する。 = − ∙ ℎ – Step3.フィルタリング • Step2.で計算した半影推定に比例したカーネルサイズを用いて， PCFフィルタリングを実行する。 [ゲームでの採用事例あり] FAR CRY4 / Assassin’s Creed Unity / GTA5

135. ### Subpixel Shadow Mapping 境界付近が汚くなるので， 境界部分だけレイトレしよう …というアイデア • Conservative Rasterizationと128bitRGBAに格納したジオメトリデータを使って， レイと三角形の交差判定を行う。

※図は，Pascal Lecocq, Jean-Ecudes Marvie et. al, “Sub-Pixel Shadow Mapping”, i3D 2014 より引用

137. ### Efficient Virtual Shadow Maps for Many Lights • お餅さんの解説を参照。 “輪読発表資料：Efficient

Virtual Shadow Maps for Many Lights” http://www.slideshare.net/omochi64/ss-47145338?qid=bd52bda4-2f71-4290-90ae-51d5ce6cab94&v=&b=&from_search=1 ※図は，Ola Olsson, Erik Sintorn, et.al, “Efficient Virtual Shadow Maps for Many Lights”, i3D 2014より引用
138. ### Adaptive Depth Bias for Shadow Map ℎ = ℎ +

= ′ ∆ ∆ = × Depth compression = − ℎ × − 2 × × − × × OpenGL 論文では 0.0001を設定 AABBの対角線 の長さ ※図は，Hang Dou, Yajie Yan, et.al, “Adaptive depth bias for shadow maps”, Journal of Compute Graphics Technique, Vol3, No.4, 2014より引用
139. ### アルゴリズム ※図は，Hang Dou, Yajie Yan, et.al, “Adaptive depth bias for

shadow maps”, Journal of Compute Graphics Technique, Vol3, No.4, 2014より引用
140. ### 適用例 ※図は，Hang Dou, Yajie Yan, et.al, “Adaptive depth bias for

shadow maps”, Journal of Compute Graphics Technique, Vol3, No.4, 2014より引用
141. ### 実行時間 ※DualはWEISKOPF, D., AND ERTL, T. 2003. “Shadow mapping based

on dual depth layers”. In Proceedings of Eurographics, vol. 3, 53–60.の手法 ※図は，Hang Dou, Yajie Yan, et.al, “Adaptive depth bias for shadow maps”, Journal of Compute Graphics Technique, Vol3, No.4, 2014より引用