物理ベースのレンダリングと画像ベースの照明の準備。 理論と実践。 ステップバイステップ

こんにちは。 2017年は庭にあります。 単純なモバイルおよびブラウザアプリケーションでさえ、物理的に正しい照明をゆっくりと描き始めています。 インターネットには、たくさんの記事と既製のシェーダーがたくさんあります。 そして、PBRを塗り付けるのも簡単なはずです...またはそうでないのですか?



実際、正直なPBRは、同様の結果を達成するのは簡単ですが、正しい結果を得るのは難しいため、実行するのは困難です。 また、インターネットには、正しい結果ではなく、まったく同じ結果をもたらす記事がたくさんあります。 このカオスでカツレツからハエを分離することは難しくなります。

したがって、この記事の目的は、PBRの概要とその機能を理解することだけでなく、PBRの作成方法を学ぶことでもあります。 通常、デバッグ方法、参照先、および間違いの原因。

この記事は、すでにhlslを十分に知っており、線形代数に精通している人を対象としています。最も単純な非PBR Phongライトを書くことができます。 一般的には、できるだけ簡単に説明しようとしますが、シェーダーの使用経験があることを願っています。



サンプルはDelphiで記述されます(FreePascalでビルドされます)が、メインコードはhlslのままです。 したがって、Delphiを知らなくても怖がらないでください。



どこで見たり感じたりできますか?
サンプルをビルドするには、 AvalancheProjectコードが必要です。 これはDX11 / OpenGLを中心とした私のフレームワークです。 まだVampyre Imaging Libraryが必要です。 これは写真を操作するためのライブラリです。テクスチャをロードするために使用します。 例のソースコードはこちらです。 バイナリを収集したくない/収集できないが、既に収集したものを使用したい場合は、 こちらにあります



シートベルトを締めたので、行こう。



1 sRGBサポートの重要性



通常、モニターはsRGB画像を表示します。 これはデスクトップについては95%真実であり、ラップトップについてはある程度真実です(そして、電話では全くの意性があります)。

これは、知覚が線形ではないという事実によるものであり、明るい領域での同じ絶対的な変化よりも暗い領域での光の方がより小さな変化に気づきます。 粗い場合は、4倍の明るさの増加で、2倍の明るさの増加として認識します。 私はあなたのために写真を用意しました:







画像を理解する前に、ブラウザまたはオペレーティングシステムが画像のサイズを変更していないことを確認してください。



中央には、水平方向の単一ピクセルの黒と白のストライプで構成される正方形があります。 この正方形からの光の量は、真っ白からの光の量のちょうど2倍です。 ここで、ストライプを同じ色の正方形にマージするように画面から離れると、キャリブレーションされたモニターでは、中央の正方形が右の正方形とマージし、左の正方形がはるかに暗くなります。 左側の正方形の色をピペットで見ると、128であり、右側の色が187であることがわかります。 187。



したがって、物理的に正しいレンダリングを行うには、画面上の白を0.5倍した値が187 187 187になることが重要です。このため、グラフィックAPI(DirectXおよびOpenGL)はsRGBのハードウェアサポートを備えています。 シェーダーでテクスチャを操作する瞬間に、テクスチャは線形空間に転送され、画面に表示されると、sRGBに転送されます。



DirectX / OpenGLでこれを実現する方法については詳しく説明しません。 グーグルは簡単です。 そして、あなたのsRGBが非常に単純に獲得したことを確認してください。 黒から白への線形グラデーションは次のように変更する必要があります。



画像



これ 、PBRに関する記事でインターネット上で発生する最初の間違いの1つであるため、線形空間で作業することの重要性を示す必要がありました。



2クックトーランス



物理的に正しいレンダリングでは、通常次のようなことが考慮されます。



  1. フレネル反射係数
  2. 省エネ法
  3. 反射光と再放射のマイクロファセット理論


このリストは、表面下散乱や物理的に正しい屈折などのマイクロファセット理論で拡張できますが、この記事では最初の3つのポイントについて説明します。



2.1マイクロファセット理論



現実世界のさまざまな材料の表面は、完全に滑らかではなく、非常に粗いです。 これらの不規則性は光の波長よりもはるかに長く、照明に大きく貢献します。 単一の法線ベクトルで大まかな平面を記述することは不可能であり、法線は通常、マクロ表面の平均値を記述します。



画像



現実には、反射光に主に貢献するのはミクロの顔です:







入射角は反射角に等しく、この図のベクトルhはマイクロフェイスの法線を正確に表し、照明に寄与することを皆が覚えています。 表面上のこのポイントからの光が表示されます。



さらに、世界の一部は、それを反映する可能性のあるマイクロフェイスに物理的に到達していません。







これは、セルフシャドウイングまたはセルフシャドウイングと呼ばれます。



そして、表面に飛んで反射した光は、常に飛び出すことができるとは限りません。







これは、自己重複またはマスキングと呼ばれます。 そして、最もunningな光は2回以上反射することができます:







そして、この効果は- 再帰反射と呼ばれます。 これらのすべてのマイクロフェースは、通常(常にではないが)範囲(0; 1)にある粗さ係数で表されます。0では、表面は完全に滑らかで、マイクロフェースはありません。粗さ係数は、1粗さに等しい平滑度係数に置き換えられる場合があります。



これは基本的に、反射光に対するサーフェスの動作に関するすべてです。



2.2双方向反射率分布関数



そのため、表面に入る光は部分的に反射され、部分的に材料に浸透します。 したがって、反射光の量は最初に全光束から分離されます。 さらに、反射光の量だけでなく、目に入る光の量にも関心があります。 そして、これはさまざまな双方向反射率分布関数(BRDF)によって記述されます。

最も人気のあるモデルの1つ、 クックトーランスモデルを検討します。



画像



この関数では:



Vは、観測者の表面から目までのベクトルです。

N-表面のマクロ法線

L-表面から光源への方向

Dは、微小面を考慮した反射光の分布関数です。 私たちの目に光を反射するために私たちに向けられたマイクロフェースの数を説明します。

Gは、自己シャドウイングと自己重複の分布関数です。 残念ながら、この関数で数回反射した光は考慮されず、失われます。 この点については、記事の後半で説明します。

F-フレネル反射係数。 すべての光が反射されるわけではありません。 光の一部は屈折し、マテリアルに入ります。 この関数では、Fは反射光の量を表します。



式でも、 Hベクトルなどのパラメーターは表示されませんが、DおよびG分布関数で積極的に使用されます。 Hベクトルの意味は、反射光に寄与するマイクロフェースの法線を記述することです。 つまり 通常のHを持つマイクロファセットに入射する光線は、常に私たちの目に反射されます。 入射角は反射角に等しいため、 H正規化(V + L)として常に計算できます。 このようなもの:



画像



分布関数DGは近似解であり、多くの異なる関数があります。 記事の最後に、そのようなディストリビューションのリストへの参照を残します。 Bruce Walterが開発したGGX分布関数を使用します。



2.3 G.オーバーラップジオメトリ



セルフシャドーイングとオーバーラップから始めましょう。 これは関数Gです。 この関数のGGX分布は、スミス法を使用しています(Smith 1967、「ランダムな粗い表面の幾何学的な影」)。 この方法の主な点は、光から表面へ、および表面から観察者へと失われる光の量が、損傷のマクロノーマルに対して対称になることです。 したがって、関数Gを2つの部分に分割し、 NLの間の角度から損失光の前半を計算し、次に同じ関数を使用して、 VNの間の角度から損失光を計算できます このような関数Gの半分を次に示し、 GGXの分布を説明します。







この機能では

αg-正方形の表面粗さ(粗さ*粗さ)。

θvは、マクロ法線Nと、ある場合には光Lと、他の場合には観測者V上のベクトルとの間の角度です

Χ-テスト対象のビームが法線の反対側から来た場合にゼロを返す関数、それ以外の場合は1を返します。 HLSLシェーダーでは、これを数式から削除します。 早い段階で確認し、そのようなピクセルをまったく点灯させません。 元の式では接線がありますが、レンダリングには角度の余弦を使用すると便利です。 スカラー積を介して取得します。 したがって、式を少し変換し、HLSLコードで記述しました。



float GGX_PartialGeometry(float cosThetaN, float alpha) { float cosTheta_sqr = saturate(cosThetaN*cosThetaN); float tan2 = ( 1 - cosTheta_sqr ) / cosTheta_sqr; float GP = 2 / ( 1 + sqrt( 1 + alpha * alpha * tan2 ) ); return GP; }
      
      





そして、光ベクトルと観測ベクトルからの合計Gを次のように考慮します。



 float roug_sqr = roughness*roughness; float G = GGX_PartialGeometry(dot(N,V), roug_sqr) * GGX_PartialGeometry(dot(N,L), roug_sqr);
      
      





ボールをレンダリングしてこのGを導出すると、次の図が得られます。







光源は左側にあります。 左から右へのボールの粗さは0.05から1.0です。



チェック :ピクセルは複数であってはなりません。 この条件を入れてください:



 Out.Color = G; if (Out.Color.r > 1.000001) Out.Color.gb = 0.0;
      
      





少なくとも1つの出力ピクセルが複数ある場合、赤に変わります。 すべてが正しく行われると、すべてのピクセルが白のままになります。



2.4 D.反射マイクロファセットの分布



そのためマクロノーマルラフネスHベクトルなどのパラメーターがあります。 これらのパラメーターから、指定されたピクセルのマイクロフェイスのどの%がHに一致する法線を持っているかを確立することができます GGXでは、この関数がこれを担当します。







Gの場合と同じ関数です 同じ理由で破棄します。

αg-正方形の表面粗さ

θmは、マクロ法線NHベクトルの間の角度です。



再び、私はいくつかの小さな変換を行い、接線を角度のコサインに置き換えました。 結果として、まさにそのようなHLSL関数があります。



 float GGX_Distribution(float cosThetaNH, float alpha) { float alpha2 = alpha * alpha; float NH_sqr = saturate(cosThetaNH * cosThetaNH); float den = NH_sqr * alpha2 + (1.0 - NH_sqr); return alpha2 / ( PI * den * den ); }
      
      





そして、次のように呼び出します:



 float D = GGX_Distribution(dot(N,H), roughness*roughness);
      
      





Dの値を画面に表示すると、次のようになります。







粗さは、左の0.05から右の1.0まで変化します。



チェック :1.0の粗さでは、すべての光が半球全体に均等に分配されることに注意してください。 これは、最後のボールが固体でなければならないことを意味します。 その色は153 153 153(丸めによる+ -1)である必要があります。sRGB から線形空間に変換すると、0.318546778125092になります。 この数値にPIを掛けると、約1になります。これは、半球での反射に相当します。 PIを選ぶ理由 半球積分cos(x)sin(x)はPIを与えるためです。



2.5 F.フレネル反射係数



2つの異なるメディアの境界に当たる光線は、反射および屈折します。



画像



フレネル式は、これが起こる法則を正確に説明していますが、ウィキに行ってこれらの複数階の式を見ると、それらが重いことがわかります。 幸いなことに、PBRレンダリングでほとんどの場合に使用される適切な近似があります。これはシュリック近似です。



画像



ここで、 R0は屈折率の比として計算されます。

式のcosθは、光の入射と法線の間の角度の余弦です。 cosθ= 1の場合、式はR0に縮退することがわかります 。これは、 R0の物理的意味は、ビームが表面に垂直に落ちる場合の反射光の量であることを意味します これをhlslコードに直接入れましょう:



 float3 FresnelSchlick(float3 F0, float cosTheta) { return F0 + (1.0 - F0) * pow(1.0 - saturate(cosTheta), 5.0); }
      
      





F0float3です 。 これは、反射係数がチャネルごとに異なる場合があるためです。 異なる材料は、波長に応じて異なる量の光を反射します。



画像



また、目にはRGBコーンがあるため、float3で十分です。



2.6一緒に折ります



じゃあ 反射色全体を返す関数を組み立てましょう:



 float3 CookTorrance_GGX(float3 n, float3 l, float3 v, Material_pbr m) { n = normalize(n); v = normalize(v); l = normalize(l); float3 h = normalize(v+l); //precompute dots float NL = dot(n, l); if (NL <= 0.0) return 0.0; float NV = dot(n, v); if (NV <= 0.0) return 0.0; float NH = dot(n, h); float HV = dot(h, v); //precompute roughness square float roug_sqr = m.roughness*m.roughness; //calc coefficients float G = GGX_PartialGeometry(NV, roug_sqr) * GGX_PartialGeometry(NL, roug_sqr); float D = GGX_Distribution(NH, roug_sqr); float3 F = FresnelSchlick(m.f0, HV); //mix float3 specK = G*D*F*0.25/NV; return max(0.0, specK); }
      
      





最初に、表面に到達しない光をフィルタリングします。



 if (NL <= 0.0) return 0.0;
      
      



表示されていない領域と同様に:

 if (NV <= 0.0) return 0.0;
      
      



さまざまなスカラー製品を準備し、それらの関数にGGX_PartialGeometry()GGX_Distribution()FresnelSchlick()を提供します。 次に、前述の式に従ってすべてを乗算します。



画像



NLに分割しなかったことに注意してください。



 float3 specK = G*D*F*0.25/NV;
      
      





とにかくNLを掛けるとNLが減るからです。 出力で、私はこの写真を得ました:







左から右に、粗さは0.05から1.0に増加します

上から下へ、異なるフレネル係数F0

1.(0.04、0.04、0.04)

2.(0.24、0.24、0.24)

3.(1.0、0.86、0.56)



2.7ランバートのアンビエントライトモデル



したがって、 FresnelSchlick関数は反射光の量を返します。 残りのライトは1.0-FresnelSchlick()に等しくなります。



ここで余談をさせてください。
多くの人々は、このユニットからフレネルを引いたものを異なって考えています。 たとえば、UE4では、FresnelSchlickはドット(V、H)からカウントされます。 どこかで2つの係数(ドット(L、N)とドット(V、N)から)を取ります。 私に関しては、ドット(L、N)から取得する方が論理的です。 今、私はそれがより正確で、どのように現実に近づくかを正確に知らないと言うことができます。 この問題を研究するとき、この記事のこのギャップを補いますが、今のところはUE4のように、つまりドット(V、H)を使用します。


この光は表面を通過し、吸収/再放射/別の点で表面を離れるまで、その内部をランダムにさまよいます。 表面下の散乱にまだ影響を与えていないため、この光は半球で吸収または再放射されるとおおよそ仮定します。



画像



最初の近似では、これは私たちに適しています。 この分散は、 Lambertライティングモデルによって記述されます。 これは、最も単純な式LightColor * dot(N、L)/ PIで記述されます。 つまり、誰もが知っているのは、表面に入射する光束の密度を表すドット(N、L)と、 積分半球の形で以前に出会ったPIによる除算です。 吸収/再放射される光の量は、 albedoと呼ばれるfloat3パラメーターによって記述されます。 ここのすべては、 F0パラメーターと非常に似ています。 被験者は特定の波長のみを再放射します。



Lambertは照明モデルについて何も言うことがないので、 CookTorrance_GGXに追加しています(ただし、別の関数に入れる方が正しいかもしれませんが、私はこれまでFパラメーターを引き出すのが面倒です)。



  float3 specK = G*D*F*0.25/(NV+0.001); float3 diffK = saturate(1.0-F); return max(0.0, m.albedo*diffK*NL/PI + specK);
      
      





しかし、一般的に、関数は次のようになりました
 float3 CookTorrance_GGX(float3 n, float3 l, float3 v, Material_pbr m) { n = normalize(n); v = normalize(v); l = normalize(l); float3 h = normalize(v+l); //precompute dots float NL = dot(n, l); if (NL <= 0.0) return 0.0; float NV = dot(n, v); if (NV <= 0.0) return 0.0; float NH = dot(n, h); float HV = dot(h, v); //precompute roughness square float roug_sqr = m.roughness*m.roughness; //calc coefficients float G = GGX_PartialGeometry(NV, roug_sqr) * GGX_PartialGeometry(NL, roug_sqr); float D = GGX_Distribution(NH, roug_sqr); float3 F = FresnelSchlick(m.f0, HV); //mix float3 specK = G*D*F*0.25/(NV+0.001); float3 diffK = saturate(1.0-F); return max(0.0, m.albedo*diffK*NL/PI + specK); }
      
      







以下は、拡散コンポーネントを追加した後に得られたものです。







上から下に(線形空間で)3つの材料のアルベド:



1.(0.47、0.78、0.73)

2.(0.86、0.176、0)

3.(0.01、0.01、0.01)



さて、ここで2番目の光源を右側に配置し、光源の強度を3倍に増やしました。







現在の例は、リポジトリのこちらからダウンロードできます。 また、例の収集されたバージョンがここにあることを思い出させます。



3.0画像ベースの照明







ここで、点光源が画像の各ピクセルの照明にどのように寄与するかを調べました。 もちろん、光源が距離の2乗から減衰しなければならないという事実、および光源が点のようになり得ないという事実を無視しました。これはすべて考慮することができますが、照明への別のアプローチを見てみましょう。 事実は、周囲のすべてのオブジェクトが光を反射/再放射し、それによって環境を照らすということです。 あなたは鏡に行き、自分を見ます。 鏡で反射された光は、最近あなたの体によって再放射/反射されたので、あなたはその中に自分自身を見ます。 滑らかなオブジェクトで美しい反射を取得する場合は、ピクセルごとに、このピクセルを囲む半球からの光を計算する必要があります。 ゲームでは、この偽物がこの場合に使用されます。 環境に合わせて大きなテクスチャを準備します(実際には360度の写真または立体地図)。 そのような写真の各ピクセルは小さなエミッタです。 次に、「何らかの魔法」を使用して、そのようなテクスチャからピクセルを選択し、ポイントソース用に上記で記述したコードを使用して描画されているピクセルを照らします。 そのため、この手法は画像ベースの照明と呼ばれます(つまり、環境のテクスチャが光源として使用されます)。



3.1モンテカルロ



簡単なものから始めましょう。 モンテカルロ法を使用してカバレッジを計算します。 怖くて「モンテカルロ法」という怖い言葉がわからない人のために、指で説明しようと思います。 各ポイントの照明は、マップのすべてのポイントの影響を受けます。 それらの半分を取り除くことができます。 これらは、ライティングにまったく寄与しないため、サーフェスの反対側にあるものです。 まだ半球があります。 これで、この半球にランダムに均等に分布した光線を出し、照明をヒープに積み重ねてから、放射された光線の数で除算し、2πを掛けることができます。 2πでは、これは半径1の半球の領域です。数学者は、モンテカルロ法を使用して半球全体に照明を統合したと言います。



これは実際にどのように機能しますか? 加算ブレンドにより、浮動小数点テクスチャのレンダリングを使用してヒープに追加します。 このテクスチャのアルファチャンネルでは、レイの数を記録し、rgbでは実際の照明を記録します。 これにより、color.rgbをcolor.aに分割し、最終的な画像を取得できます。



ただし、加法ブレンドは、他のオブジェクトで覆われたオブジェクトが、描画されるにつれて他のオブジェクトを通して輝き始めることを意味します。 この問題を回避するために、 深度プリパス手法を使用します。 このテクニックの本質は、最初にオブジェクトを深度バッファにのみ描画し、次に深度テストを同等に切り替えて 、オブジェクトをカラーバッファに描画することです。



したがって、球全体に均等に分散した光線の束を生成します。



 function RandomRay(): TVec3; var theta, cosphi, sinphi: Single; begin theta := 2 * Pi * Random; cosphi := 1 - 2 * Random; sinphi := sqrt(1 - min(1.0, sqr(cosphi))); Result.x := sinphi * cos(theta); Result.y := sinphi * sin(theta); Result.z := cosphi; end; SetLength(Result, ACount); for i := 0 to ACount - 1 do Result[i] := Vec(RandomRay(), 1.0);
      
      





このものを定数としてこのシェーダーに送信します。



 float3 m_albedo; float3 m_f0; float m_roughness; static const float LightInt = 1.0; #define SamplesCount 1024 #define MaxSamplesCount 1024 float4 uLightDirections[MaxSamplesCount]; TextureCube uEnviroment; SamplerState uEnviromentSampler; PS_Output PS(VS_Output In) { PS_Output Out; Material_pbr m; m.albedo = m_albedo; m.f0 = m_f0; m.roughness = m_roughness; float3 MacroNormal = normalize(In.vNorm); float3 ViewDir = normalize(-In.vCoord); Out.Color.rgb = 0.0; [fastopt] for (uint i=0; i<SamplesCount; i++){ //      float3 LightDir = dot(uLightDirections[i].xyz, In.vNorm) < 0 ? -uLightDirections[i].xyz : uLightDirections[i].xyz; //        180,      float3 LightColor = uEnviroment.SampleLevel(uEnviromentSampler, mul(LightDir, (float3x3)V_InverseMatrix), 0.0).rgb*LightInt; //       ( ) Out.Color.rgb += CookTorrance_GGX(MacroNormal, LightDir, ViewDir, m)*LightColor; //          } Out.Color.rgb *= 2*PI; //       Out.Color.a = SamplesCount; //      return Out; }
      
      





開始し、同じボールの画像が徐々に収束するのを確認します。 レンダリングには2つの立方体マップを使用しました。 このためにここに:







RenderMonkeyに付属している立方体の地図を取りました。 Snow.ddsと呼ばれます。 これはLDRテクスチャであり、退屈です。 ボールは美しく照らされるよりも汚れているように見えます。



そしてこれのために:







私はここからHDRプローブを取りました: www.pauldebevec.com/Probesと呼ばれるサンフランシスコのグレース大聖堂。 彼女のダイナミックレンジは最大200,000です.1。 違いは何ですか? したがって、このような照明を自分で作成する場合は、すぐにHDRテクスチャを使用してください。

ところで、エネルギー保存の法則が満たされていることを見てみましょう。 これを行うには、シェーダーのアルベドを強制的に1.0に設定します。



m.albedo = 1.0;

1.0でライトを設定します。

LightColor = 1.0;



理想的には、ボールの各ピクセルは1に等しいはずです。 したがって、ユニットを超えるものはすべて赤でマークされます。 次のようになりました。







実際、現在存在する赤はエラーです。 収束し始めますが、ある時点でフロートの精度が十分ではなく、収束が消えます。 右下のボールが青に変わっていることに注意してください。 これは、Lambertモデルでは表面粗さが考慮されておらず、Cook-Torensモデルでは表面粗さが完全に考慮されていないためです。 実際、 F0に行く黄色は失われました。 F0を試して、すべてのボールを1.0に設定してみましょう。







粗さが大きいため、右側のボールはずっと暗くなりました。 これは実際、私たちが失った再帰反射です。 クックトレンズはこのエネルギーを失います。 Oren-Nayarモデルは、このエネルギーを部分的に復元できます。 しかし、私たちは今のところ延期します。 非常にラフなモデルでは、再帰反射のエネルギーの最大70%が失われるという事実に我慢する必要があります。

ソースコードはこちらです。 収集したバイナリについては既に言及しましたが、ここあります



3.2重要度サンプリング



もちろん、ゲーマーは画像の光が収束するのを待ちません。 各ピクセルの数千および数千の光線をカウントしないように、何かを行う必要があります。 そして、トリックはこれです。 モンテカルロについては、半球内の均一な分布を考慮しました。 これがサンプリング方法です。



画像



しかし、実際の部分はピンク色で囲まれた部分によって与えられます。 主にレッドゾーンからの光線を選択できれば、はるかに早い段階で許容可能な画像が得られます。 だからこれが欲しい:



画像



幸いなことに、このための重要なサンプリング(または重要度によるサンプリング)と呼ばれる数学的な方法があります。 式の重要性は、パラメーターDによって導入されます これを使用して、いくつかのPDF関数(確率密度関数)からCDF (分布関数)を構築できます。 PDFの場合、正規分布に対するマイクロ正規分布を使用します。半球の合計は単一性を与えるため、このような積分を書くことができます。







被積分関数はこちら-PDF

球面角全体の積分を取ると、次のCDFが得られます。







このステップをステップごとに実行したい場合は、 こちらをご覧ください



望んでいない/深く行くことができない人のために私は言います。 均等に分散されたξを代入するCDFがあり、出力ではPDFを反映する分布が得られます。 これは私たちにとって何を意味するのでしょうか?



1.これにより、 Cook-Torrens関数が変更されること。 PDF関数に分割する必要があります。



2. PDF関数は、マクロ正規に対するマイクロ正規の分布を反映しています。 以前は、モンテカルロの場合、ランダムなベクトルを取得し、光源へのベクトルとして使用していました。 ここで、 CDFを使用して、ランダムなベクトルHを選択します 次に、このランダムHに関して視線ベクトルを反映し、光ベクトルを取得します。



3. PDFはフォームのスペースに転送する必要があります(ベクトルNに沿った分布を反映しているため)。 翻訳するには、 PDF4 *ドット(H、V)に分割する必要があります。さらに深く掘り下げたい場合は、 ここにアクセスしてください



何も明確ではないようです。 これらすべてをコードでダイジェストしてみましょう。



まず、 CDFからベクトルHを生成する関数を作成します。 HLSLでは、次のようになります。



 float3 GGX_Sample(float2 E, float alpha) { float Phi = 2.0*PI*Ex; float cosThetha = saturate(sqrt( (1.0 - Ey) / (1.0 + alpha*alpha * Ey - Ey) )); float sinThetha = sqrt( 1.0 - cosThetha*cosThetha); return float3(sinThetha*cos(Phi), sinThetha*sin(Phi), cosThetha); }
      
      





ここでEでは、両方の球面角に対して均一な分布[0; 1)を与えます。 アルファ では、材料の粗さの 2乗を持ちます。



出力では、半球でHベクトルを取得します。 しかし、この半球は表面に沿って方向付けられる必要があります。 これを行うために、サーフェス上の方向マトリックスを返す別の関数を作成します。



 float3x3 GetSampleTransform(float3 Normal) { float3x3 w; float3 up = abs(Normal.y) < 0.999 ? float3(0,1,0) : float3(1,0,0); w[0] = normalize ( cross( up, Normal ) ); w[1] = cross( Normal, w[0] ); w[2] = Normal; return w; }
      
      





この行列により、生成されたすべてのベクトルHを乗算し、それらの接線空間をフォームの空間に変換します。 この原理は、TBNの基礎に非常に似ています。



今、クック・トレンを分割することは残っています: G * D * F * 0.25 /(NV)PDFにPDF = D * NH /(4 * HV) 。 したがって、修正されたクック・トレンは次のようになります



G * F * HV /(NV * NH)



HLSLでは、次のようになります。



 float3 CookTorrance_GGX_sample(float3 n, float3 l, float3 v, Material_pbr m, out float3 FK) { pdf = 0.0; FK = 0.0; n = normalize(n); v = normalize(v); l = normalize(l); float3 h = normalize(v+l); //precompute dots float NL = dot(n, l); if (NL <= 0.0) return 0.0; float NV = dot(n, v); if (NV <= 0.0) return 0.0; float NH = dot(n, h); float HV = dot(h, v); //precompute roughness square float roug_sqr = m.roughness*m.roughness; //calc coefficients float G = GGX_PartialGeometry(NV, roug_sqr) * GGX_PartialGeometry(NL, roug_sqr); float3 F = FresnelSchlick(m.f0, HV); FK = F; float3 specK = G*F*HV/(NV*NH); return max(0.0, specK); }
      
      





この関数からのLambert照明から拡散部分を捨て、 FKパラメーターを返すことに注意してください。 事実は、 Importance Samplingを介して拡散コンポーネントをカウントできないことです。 PDFは、目の中で光を反射する顔用です。 そして、ランバートの分布はこれに依存していません。 どうする? うーん...今は拡散部分を黒のままにして、鏡面反射に焦点を当てましょう。



 PS_Output PS(VS_Output In) { PS_Output Out; Material_pbr m; m.albedo = m_albedo; m.f0 = m_f0; m.roughness = m_roughness; float3 MacroNormal = normalize(In.vNorm); float3 ViewDir = normalize(-In.vCoord); float3x3 HTransform = GetSampleTransform(MacroNormal); Out.Color.rgb = 0.0; float3 specColor = 0.0; float3 FK_summ = 0.0; for (uint i=0; i<(uint)uSamplesCount; i++){ float3 H = GGX_Sample(uHammersleyPts[i].xy, m.roughness*m.roughness); // H  H = mul(H, HTransform); //       float3 LightDir = reflect(-ViewDir, H); //       float3 specK; float3 FK; specK = CookTorrance_GGX_sample(MacroNormal, LightDir, ViewDir, m, FK); //   FK_summ += FK; float3 LightColor = uRadiance.SampleLevel(uRadianceSampler, mul(LightDir.xyz, (float3x3)V_InverseMatrix), 0).rgb*LightInt;//        specColor += specK * LightColor; //   } specColor /= uSamplesCount; FK_summ /= uSamplesCount; Out.Color.rgb = specColor; Out.Color.a = 1.0; return Out; }
      
      





1024個のサンプルを設定して(モンテカルロのように蓄積せずに)、結果を見てみましょう:







多くのサンプルがありますが、ノイズが多いことが判明しました。 特に大きな粗さで。



3.3 LODの選択



これは、 LODがゼロの非常に詳細なマップからサンプルを取得するためです。 そして、偏差の大きい光線がより小さなロッドを取るのが良いでしょう。 この写真はこの写真でよく示されています。



画像



NVidiaのこの記事から。 青い領域は平均値を示しておりテクスチャのLODから取得すると良いでしょう。 より重要な場合はLODを小さくし、重要でない場合はLODを大きくすることがわかります。 地域全体の平均値を取得します。 半球全体をlodaで覆うと理想的です。 幸いなことに、NVidiaは既製の(そして簡単に言うと)公式をすでに提供してくれました。



画像



この式は違いから成ります:







左側の部分は、テクスチャのサイズとサンプルの数に依存します。 そしてこれは、すべてのサンプルについて、一度カウントできることを意味します。右側には、私たちは機能していたp当社以外の何ものでもありません、PDFファイル、および関数D彼らが呼ぶ、歪みのを、実際に観察者にサンプルの角依存性があります。以下のためにD彼らはここに持っている式は次のとおりです。



画像



ネタバレ見出し
(, Dual-Paraboloid . , , b , . b=1.2 )


まあ、NVidia はlodsにバイアスをかけて、追加することも推奨しています。hlslコードでどのように見えるか見てみましょう。方程式の左側は次のとおりです。



 float ComputeLOD_AParam(){ float w, h; uRadiance.GetDimensions(w, h); return 0.5*log2(w*h/uSamplesCount); }
      
      





ここでは右側を考慮し、すぐに左側から減算します。



 float ComputeLOD(float AParam, float pdf, float3 l) { float du = 2.0*1.2*(abs(lz)+1.0); return max(0.0, AParam-0.5*log2(pdf*du*du)+1.0); }
      
      





ComputeLODでpdflの値を渡す必要があることがわかりましたlは光サンプルのベクトル、pdfは上を見るとpdf = D * dot(N、H)/(4 * dot(H、V))です。



したがって、CookTorrance_GGX_sample関数にreturn pdfパラメーター追加しましょう



 float3 CookTorrance_GGX_sample(float3 n, float3 l, float3 v, Material_pbr m, out float3 FK, out float pdf) { pdf = 0.0; FK = 0.0; n = normalize(n); v = normalize(v); l = normalize(l); float3 h = normalize(v+l); //precompute dots float NL = dot(n, l); if (NL <= 0.0) return 0.0; float NV = dot(n, v); if (NV <= 0.0) return 0.0; float NH = dot(n, h); float HV = dot(h, v); //precompute roughness square float roug_sqr = m.roughness*m.roughness; //calc coefficients float G = GGX_PartialGeometry(NV, roug_sqr) * GGX_PartialGeometry(NL, roug_sqr); float3 F = FresnelSchlick(m.f0, HV); FK = F; float D = GGX_Distribution(NH, roug_sqr); //      D pdf = D*NH/(4.0*HV); //   pdf float3 specK = G*F*HV/(NV*NH); return max(0.0, specK); }
      
      





そして、サンプルループ自体がLODを計算するようになりました



  float LOD_Aparam = ComputeLOD_AParam(); for (uint i=0; i<(uint)uSamplesCount; i++){ float3 H = GGX_Sample(uHammersleyPts[i].xy, m.roughness*m.roughness); H = mul(H, HTransform); float3 LightDir = reflect(-ViewDir, H); float3 specK; float pdf; float3 FK; specK = CookTorrance_GGX_sample(MacroNormal, LightDir, ViewDir, m, FK, pdf); FK_summ += FK; float LOD = ComputeLOD(LOD_Aparam, pdf, LightDir); float3 LightColor = uRadiance.SampleLevel(uRadianceSampler, mul(LightDir.xyz, (float3x3)V_InverseMatrix), LOD).rgb*LightInt; specColor += specK * LightColor; }
      
      





lodを使用した1024サンプルで得られたものを見てみましょう







。完璧に見えます。16に下げ、そして...







はい、もちろん完璧ではありません。粗さが非常に大きいボールでは品質が低下していることがわかりますが、それでも原則としてこの品質は許容できると考えています。分布に基づいてテクスチャのmipを作成すれば、品質をさらに向上させることができます。このことは、Epicのでプレゼンテーションで読み取ることができ、ここで(。セクションあらかじめフィルタ処理環境マップを参照してください)。それまでの間、この記事の一部として、古典的なピラミッド型ミペスについて説明することを提案します。



3.4 Hammersleyポイントセット



ランダムな光線を見ると、欠陥は明らかです。それらはランダムです。そして、私たちはすでにlodから読むことを学んでおり、できるだけ大きな正方形を光線でキャプチャしたいと考えています。これを行うには、光線を「スプレー」する必要がありますが、サンプリングの重要性を考慮してください。なぜなら CDFは均一な分布を取るため、区間[0; 1)でポイントを均等に分布させるだけで十分ですが、均一な分布は2次元でなければなりません。したがって、ギャップ内のポイントを均等に配置するだけでなく、ポイント間のデカルト座標の距離をできるだけ大きくする必要もあります。Hammersleyポイントセットは、この役割に適しています。ここで、この多くのポイントについてもう少し読むことができます



分布の写真のみを表示







します。別のポイントを生成する関数も提供します。



 function HammersleyPoint(const I, N: Integer): TVec2; function radicalInverse_VdC(bits: Cardinal): Single; begin bits := (bits shl 16) or (bits shr 16); bits := ((bits and $55555555) shl 1) or ((bits and $AAAAAAAA) shr 1); bits := ((bits and $33333333) shl 2) or ((bits and $CCCCCCCC) shr 2); bits := ((bits and $0F0F0F0F) shl 4) or ((bits and $F0F0F0F0) shr 4); bits := ((bits and $00FF00FF) shl 8) or ((bits and $FF00FF00) shr 8); Result := bits * 2.3283064365386963e-10; end; begin Result.x := I/N; Result.y := radicalInverse_VdC(I); end;
      
      





このドットのセットでボールはどのように見えますか?実際、私は偽造し、重要度サンプリングの上記のすべての画像は、これらのポイントのセットで生成されました。ランダムセットでは、写真が少し悪く見えます(言葉を信じますか?)



3.5放射照度マップ



拡散色なしで残された方法を覚えていますか?重要度サンプリングでは投機家にとって最も価値のある光線が選択されるため、重要度サンプリングでは拡散色をサンプリングできません。拡散のために、最も価値のあるものはマクロ法線の表面の真向かいにあります。幸いなことに、Lambertの拡散成分は観測者から完全に独立しています。したがって、立方体マップで照明を計算できます。また、角度を変更すると照明にわずかに影響するため、計算されたマップの解像度は非常に低くなる可能性があります(たとえば、16 x 16ピクセル/辺)。今回は幸運でした。放射照度マップの構築を扱うコードは作成しませんが、プログラムを使用します



CubeMapGencubmap(Load Cubemap(.dds))を開き、Irradiance cubemapチェックボックスを設定し、Output Cube Size 16を選択して







、結果のキュービックマップを保存します(HDRテクスチャの場合、目的のテクスチャ出力形式を設定することを忘れないでください)。



さて、Irradianceマップを計算したので、サンプリング後にこのマップから1つのサンプルを追加するだけです。HLSLコードは次のようになります。



  Out.Color.rgb = 0.0; float3 specColor = 0.0; float3 FK_summ = 0.0; for (uint i=0; i<(uint)uSamplesCount; i++){ float3 H = GGX_Sample(uHammersleyPts[i].xy, m.roughness*m.roughness); H = mul(H, HTransform); float3 LightDir = reflect(-ViewDir, H); float3 specK; float pdf; float3 FK; specK = CookTorrance_GGX_sample(MacroNormal, LightDir, ViewDir, m, FK, pdf); FK_summ += FK; float LOD = ComputeLOD(LOD_Aparam, pdf, LightDir); float3 LightColor = uRadiance.SampleLevel(uRadianceSampler, mul(LightDir.xyz, (float3x3)V_InverseMatrix), LOD).rgb*LightInt; specColor += specK * LightColor; } specColor /= uSamplesCount; FK_summ /= uSamplesCount; float3 LightColor = uIrradiance.Sample(uIrradianceSampler, mul(MacroNormal, (float3x3)V_InverseMatrix)).rgb; Out.Color.rgb = m.albedo*saturate(1.0-FK_summ)*LightColor + specColor;
      
      





出力には、16サンプルのこの画像があります。





そして、1024


拡散光にドット(N、L)を掛けないことに注意してください。これは一種の論理です。結局、このライティングを計算して、それをcubmapに焼き付けました。この場合、NLは一般に同じベクトルです。



私たちが省エネで何を持っているか見てみましょうか?いつものように、立方体カードからの光を単一に設定し、マテリアルのアルベドを単一に設定し、1を超える領域を赤で強調表示します。おおよそ1024個のサンプルについてこれを取得します。







そして、ここでは16 個のサンプルについてです。







ご覧のとおり、過剰なエネルギーの明らかな「放出」がありますが、それらはわずかです。これは、拡散エネルギーから光量を正確に計算できないためです。結局、フレネル係数は鏡面反射光の特定のサンプルに対してのみ考慮され、放射マップで計算されるすべてのサンプルに使用されます悲しいかな、私はこれをどうするかわかりません、インターネットは私に何も伝えることができませんでした。したがって、私は今のところ、これに同意することを提案しますが、それでもやはり、過剰エネルギーの排出は重要ではありません。



4材料についてもう少し。



ボールをいじりながら、float3の2つのカラーオプションがあることに気づいたはずです。これはアルベドf0です。どちらも物理的な意味がありますが、ゲームでは、原則として、材料は金属と非金属に分けられます。実際、非金属は常にグレースケール範囲の光を反射しますが、同時に色の光を再放射します。反対に、金属は色のついた光を反射しますが、同時に残りを吸収します。



つまり、金属の場合:

albedo = {0、0、0}

f0 = {R、G、B}

誘電体の場合:

albedo = {R、G、B}

f0 = {X、X、X}



そして...特定の係数[0,1]を導入できることがわかります。これにより、表面がどれだけ金属であるかを示し、単純に線形補間によって材料を考慮します。これは実際には多くのアーティストが行います。この 3Dモデルをダウンロードしました



剣のテクスチャの例を次に示します。



カラーテクスチャ:







粗さのテクスチャ:







そして最後に、金属性のテクスチャ(上記で言及したもの):







インターネット上の素材についてもう少し読むことができます。例えばここに



さまざまなエンジンやスタジオがさまざまな方法でパラメーターをパックできますが、原則として、すべてが色、粗さ、金属性を中心に展開します。



そして最後に、モデル全体でどのように見えるか:







左側には、ボールでテストしたのと同じ大聖堂があります。右側では、私たちのArtoriasが自然になりました(ここからの環境マップはwww.pauldebevec.com/ProbesはCampusと呼ばれます)





Artoriasモデルのソースコードは、私のフレームワークのデモであり、ここにありますまた、あなたのためにバージョンをまとめて、ここにアップロードしました



5結論



, . :



1.

2.

3. , -

4.

5.



, … — . , . - .



- , PBR. .



PBR
[1] blog.tobias-franke.eu/2014/03/30/notes_on_importance_sampling.html

PDF CDF, .



[2] hal.inria.fr/hal-00942452v1/document

PBR (+ )



[3] disney-animation.s3.amazonaws.com/library/s2012_pbs_disney_brdf_notes_v2.pdf

Disney, . ,

[4] blog.selfshadow.com/publications/s2015-shading-course/#course_content

Disney



[5] www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf

GGX, Bruce Walter-



[6] de45xmedrsdbp.cloudfront.net/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf

PBR Unreal Engine 4



[7] holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html

Hammersley Point Set



[8] www.jordanstevenstechart.com/physically-based-rendering

. .



[9] graphicrants.blogspot.nl/2013/08/specular-brdf-reference.html





[10] developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch20.html

NVidia Importance Sampling LOD-.



[11] cgg.mff.cuni.cz/~jaroslav/papers/2008-egsr-fis/2008-egsr-fis-final-embedded.pdf

Importance Sampling



[12] gdcvault.com/play/1024478/PBR-Diffuse-Lighting-for-GGX

PBR.



[13] www.codinglabs.net/article_physically_based_rendering.aspx

[14] www.codinglabs.net/article_physically_based_rendering_cook_torrance.aspx

( - )



[15] www.rorydriscoll.com/2009/01/25/energy-conservation-in-games





[16] www.gamedev.net/topic/625981-lambert-and-the-division-by-pi

www.wolframalpha.com/input/?i=integrate+cos+x+ *+sin+x+dx+dy+from+x+%3D+0+to+pi+%2F+2+y+%3D+0+to+pi+*+2





[17]https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf



[18] www.pauldebevec.com/Probes

HDR 360 . .



[19] seblagarde.wordpress.com/2012/06/10/amd-cubemapgen-for-physically-based-rendering

code.google.com/archive/p/cubemapgen/downloads

. radiance irradiance .



[20] eheitzresearch.wordpress.com/415-2

. .



All Articles