OpenGLを学びます。 レッスン6.3-画像ベースの照明。 拡散照射

OGL3 イメージまたはIBLImage Based Lighting )に基づく照明は、分析光源( 前のレッスンで説明)を考慮せずに、照明オブジェクトの環境全体を1つの連続した光源と見なす照明方法のカテゴリです。 一般に、このような方法の技術的基礎は、環境の立方体マップ(実世界で準備された、または3次元シーンに基づいて作成された)の処理にあり、マップに格納されたデータを照明計算で直接使用できるようにします。実際、立方体マップのすべてのテクセルは光源と見なされます。 一般に、これにより、シーンのグローバルライティングの効果をキャプチャできます。これは、現在のシーンの全体的な「トーン」を伝え、照らされたオブジェクトをより適切に「埋め込む」のに役立つ重要なコンポーネントです。



IBLアルゴリズムは特定の「グローバル」環境からの照明を考慮するため、その結果は、背景照明のより正確な模倣またはグローバル照明の非常に大まかな近似であると見なされます。 照明モデルでアンビエントライトを使用すると、オブジェクトが物理的にはるかに正確に見えるため、PBRをモデルに組み込むという点で、IBLメソッドはこの側面を興味深いものにします。



内容
パート1.はじめに



  1. Opengl
  2. ウィンドウ作成
  3. こんにちはウィンドウ
  4. こんにちはトライアングル
  5. シェーダー
  6. テクスチャー
  7. 変換
  8. 座標系
  9. カメラ


パート2.基本的な照明



  1. 照明の基本
  2. 素材
  3. テクスチャマップ
  4. 光源
  5. 複数の光源


パート3. 3Dモデルをダウンロードする



  1. Assimpライブラリ
  2. メッシュポリゴンクラス
  3. 3Dモデルクラス


パート4.高度なOpenGL機能



  1. 深度テスト
  2. ステンシルテスト
  3. 色混合
  4. 顔のクリッピング
  5. フレームバッファ
  6. キュービックカード
  7. 高度なデータ処理
  8. 高度なGLSL
  9. 幾何学シェーダー
  10. インスタンス化
  11. スムージング


パート5.高度な照明



  1. 高度な照明。 Blinn-Fongモデル。
  2. ガンマ補正
  3. シャドウカード
  4. 全方向シャドウマップ
  5. 法線マッピング
  6. 視差マッピング
  7. HDR
  8. ブルーム
  9. 遅延レンダリング
  10. SSAO


パート6. PBR



  1. 理論
  2. 分析光源
  3. IBL 拡散照射。
  4. IBL ミラー露出。




IBLの影響を既に説明したPBRシステムに組み込むために、よく知られた反射率の方程式に戻りましょう。





Lop omegao= int limits Omegakd fracc pi+ks fracDFG4 omegao cdotn omegai cdotnLip omegain cdot omegaid omegai







前述のように、主な目標は、すべての入射放射方向の積分を計算することです wi 半球 \オ前回のレッスンでは、積分計算は面倒ではありませんでした。なぜなら、光源の数、したがってそれらに対応するいくつかの光入射の方向すべてを事前に知っていたからです。 同時に、積分をスナップで解決することはできません: 任意の下降ベクトル wi 環境から非ゼロのエネルギー輝度を運ぶことができます。 結果として、この方法を実用的に適用するには、次の要件を満たす必要があります。



さて、最初のポイントはそれ自体で解決されます。 解決策のヒントは、すでにここに記載されていません。シーンまたは環境の照射を表す方法の1つは、特別な処理が行われた3次マップです。 このようなマップ内の各テクセルは、個別の放射源と見なすことができます。 任意のベクトルに従ってこのようなマップからサンプリングすることにより wi この方向のシーンのエネルギーの明るさを簡単に取得できます。



したがって、任意のベクトルのシーンのエネルギーの明るさを取得します wi



vec3 radiance = texture(_cubemapEnvironment, w_i).rgb;
      
      





しかし、驚くべきことに、積分を解決するには、環境マップから一方向からではなく、半球内のすべての可能性から選択する必要があります。 そして-影付きのフラグメントごとに。 明らかに、リアルタイムタスクの場合、これは実際には実行不可能です。 より効果的な方法は、アプリケーションの外部であっても、事前に被積分演算の一部を計算することです。 ただし、このためには、袖をまくり上げて、反射率の表現の本質に深く入り込む必要があります。







Lop omegao= int limits Omegakd fracc pi+ks fracDFG4 omegao cdotn omegai cdotnLip omegain cdot omegaid omegai









拡散に関連する式の部分が kd そして鏡 ks BRDFコンポーネントは独立しています。 積分を2つの部分に分割できます。







Lop omegao= int limits Omegakd fracc piLip omegain cdot omegaid omegai+ int limits Omegaks fracDFG4 omegao cdotn omegai cdotnLip omegain cdot omegaid omegai









このようなパーツへの分割により、各パーツを個別に扱うことができます。このレッスンでは、拡散照明を担当するパーツを扱います。



拡散成分の積分の形式を分析すると、ランバート拡散成分は本質的に定数であると結論付けることができます(色 s 屈折率 kd そして  pi 被積分関数の条件下では定数であり、他の変数から独立しています。 この事実を考えると、積分の符号を超えて定数を置くことができます。







Lop omegao=kd fracc pi int limits OmegaLip omegain cdot omegaid omegai









だから、私たちは wi (それは p 環境の3次マップの中心に対応します)。 この式に基づいて、サンプル(またはマップテクセル)の各方向の拡散成分の積分を計算した結果を格納する新しいキュービックマップを計算するか、さらによく事前に計算できます。 wo 畳み込み演算を使用します。



畳み込みは、セット内の他のすべての要素のデータを考慮して、データセット内の各要素に何らかの計算を適用する操作です。 この場合、そのようなデータは、シーンまたは環境マップのエネルギー輝度です。 したがって、キュービックマップのサンプルの各方向で1つの値を計算するには、サンプルポイントの周囲にある半球のサンプルの他のすべての可能な方向から取得した値を考慮する必要があります。



環境マップの畳み込みのために、結果の各サンプル方向の積分を解く必要があります wo 方向に沿って複数の離散サンプルを実行することにより wi 半球に属する \オ 、合計エネルギー輝度の平均化。 サンプリング方向の基準となる半球 wi ベクトルに沿った向き wo 現在の畳み込みが計算されている目的地の方向を表します。 より良い理解のために写真を見てください:











サンプルの各方向の積分結果を保存するこのような事前計算された立方体マップ wo また、シーン内のすべての間接拡散照明を合計した結果を保存し、方向に沿った特定の表面に入射すると考えることもできます wo 。 つまり、このようなキュービックマップは放射照度マップと呼ばれます。これは、事前畳み込みキュービック環境マップを使用すると、任意の方向からのシーンの照射量を直接サンプリングできるためです。 wo 、追加の計算なし。

エネルギーの明るさを決定する式は、サンプリングポイントの位置にも依存します。 p 照射マップの真ん中に横たわって撮影しました。 このような仮定は、すべての間接拡散照明の光源も単一の環境マップになるという意味で制限を課します。 照明が不均一なシーンでは、これは現実の錯覚を破壊する可能性があります(特に屋内のシーン)。 最新のレンダリングエンジンは、特殊な補助オブジェクトをシーン反射プローブに配置することにより、この問題を解決します。 そのようなオブジェクトはそれぞれ1つのタスクに関与します。それは、直接の環境のための独自の照射マップを形成します。 この手法では、任意のポイントでの照射(およびエネルギー輝度) p は、最も近い反射サンプル間の単純な補間によって決定されます。 ただし、現在のタスクについては、環境マップが中心からサンプリングされることに同意し、さらにレッスンで反射サンプルを分析します。
以下は、環境の立方体マップと、それに基づいて取得された各出力方向の環境のエネルギー輝度を平均する照射マップ( 波動エンジンに基づく)の例です wo







したがって、このカードは畳み込みの結果を各テクセルに保存します(方向に対応 wo )、および外見上、このようなマップは環境マップの平均色を格納するように見えます。 そのようなマップから任意の方向にサンプリングすると、この方向から発せられる照射の値が返されます。



PBRおよびHDR



前のレッスンでは、PBRライティングモデルを正しく動作させるために、存在する光源のHDR輝度範囲を考慮することが非常に重要であることがすでに簡単に指摘されました。 入力のPBRモデルは、非常に特定の物理量と特性に基づいて何らかの方法でパラメーターを受け入れるため、光源のエネルギー輝度が実際のプロトタイプと一致することを要求することは論理的です。 各線源の放射束の特定の値をどのように正当化するかは問題ではありません。大まかな工学的推定を行うか、 物理量を使用します。ルームランプと太陽の特性の違いは、いずれにしても非常に大きくなります。 HDR範囲を使用しないと、さまざまな光源の相対的な明るさを正確に決定することは不可能になります。



それで、PBRとHDRは永遠に友達です、これは理解できますが、この事実は画像ベースの照明方法とどのように関係していますか? 前回のレッスンでは、PBRをHDRレンダリング範囲に変換するのが簡単であることが示されました。 ただし、環境からの間接照明は環境の立方体マップに基づいているため、この背景照明のHDR特性を環境マップに保持する方法が必要です。



これまで、LDR形式( skyboxesなど)で作成された環境マップを使用していました。 私たちはそれらの色サンプルをそのままレンダリングに使用しましたが、これはオブジェクトの直接シェーディングには非常に受け入れられます。 また、物理的に信頼できる測定のソースとして環境マップを使用する場合は、まったく不適切です。



RGBE-HDR画像形式



RGBE画像ファイル形式に精通してください。 拡張子が「 .hdr 」のファイルは、広いダイナミックレンジの画像を保存するために使用され、カラートライアドの各要素に1バイトを割り当て、共通の指数に別のバイトを割り当てます。 この形式では、LDR範囲[0.、1.]を超える色強度範囲を持つキュービック環境マップを保存することもできます。 これは、光源がそのような環境マップで表されて、実際の強度を維持できることを意味します。



ネットワークには、さまざまな実際の条件で撮影されたRGBE形式の非常に多くの無料の環境マップがあります。 sIBLアーカイブサイトの例を次に示します









あなたが見たことに驚くかもしれません:結局のところ、この歪んだ画像は、6つの面にはっきりと分解された通常の立方体の地図のようには見えません。 説明は簡単です。環境のこのマップは、球体から平面に投影されました- 長方形のスキャンが適用されました。 これは、キュービックカードのストレージモードをそのままサポートしない形式で保存できるようにするために行われます。 もちろん、この投影方法には欠点があります。水平解像度は垂直解像度よりもはるかに高いです。 通常、レンダリングにおけるアプリケーションのほとんどの場合、これは許容できる比率です。これは、通常、環境と照明の興味深い詳細が垂直面ではなく水平面に正確に配置されるためです。 さて、すべてに加えて、立方体マップに戻す変換コードが必要です。



stb_image.hでのRGBE形式のサポート



この画像フォーマットを自分でダウンロードするには、フォーマット仕様に関する知識が必要です 。これは難しくはありませんが、依然として面倒です。 幸いなことに、単一のヘッダーファイルに実装されたstb_image.hイメージロードライブラリは、RGBEファイルのロードをサポートし、浮動小数点数の配列を返します-目的に必要なもの! ライブラリをプロジェクトに追加して、画像データをロードするのは非常に簡単です。



 #include "stb_image.h" [...] stbi_set_flip_vertically_on_load(true); int width, height, nrComponents; float *data = stbi_loadf("newport_loft.hdr", &width, &height, &nrComponents, 0); unsigned int hdrTexture; if (data) { glGenTextures(1, &hdrTexture); glBindTexture(GL_TEXTURE_2D, hdrTexture); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, width, height, 0, GL_RGB, GL_FLOAT, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout << "Failed to load HDR image." << std::endl; }
      
      





ライブラリは、内部HDR形式の値を通常の32ビットの実数値に自動的に変換します。デフォルトでは3つのカラーチャネルがあります。 元のHDR画像のデータを通常の2D浮動小数点テクスチャに保存するだけで十分です。



等角スキャンを立方体マップに変換する



同様に長方形のスキャンを使用して環境マップからサンプルを直接選択することもできますが、通常のキュービックマップからのフェッチはパフォーマンスが実質的に無料になる一方で、高価な数学演算が必要になります。 これらの考慮事項から、このレッスンでは、同じように長方形の画像をキュービックマップに変換し、それを後で使用します。 ただし、3次元ベクトルを使用した長方形のマップからの直接サンプリング方法もここに表示されるため、適切な作業方法を選択できます。



変換するには、ユニットサイズの立方体を描画し、その立方体を内側から観察し、その面に等しい長方形の地図を投影し、立方体地図の面として面から6つの画像を抽出する必要があります。 このステージの頂点シェーダーは非常にシンプルです。キューブの頂点をそのまま処理するだけでなく、未修正の位置をフラグメントシェーダーに渡して、3次元サンプルベクトルとして使用します。



 #version 330 core layout (location = 0) in vec3 aPos; out vec3 localPos; uniform mat4 projection; uniform mat4 view; void main() { localPos = aPos; gl_Position = projection * view * vec4(localPos, 1.0); }
      
      





フラグメントシェーダーでは、キューブを等しい長方形のマップを持つシートで慎重にラップしようとして、キューブの各面をシェーディングします。 これを行うために、フラグメントシェーダーに転送されたサンプルの方向が取得され、特別な三角法のマジックによって処理され、最終的に、選択は実際の立方体マップであるかのように等長方形マップから行われます。 選択結果は、立方体面のフラグメントの色として直接保存されます。



 #version 330 core out vec4 FragColor; in vec3 localPos; uniform sampler2D equirectangularMap; const vec2 invAtan = vec2(0.1591, 0.3183); vec2 SampleSphericalMap(vec3 v) { vec2 uv = vec2(atan(vz, vx), asin(vy)); uv *= invAtan; uv += 0.5; return uv; } void main() { // localPos   vec2 uv = SampleSphericalMap(normalize(localPos)); vec3 color = texture(equirectangularMap, uv).rgb; FragColor = vec4(color, 1.0); }
      
      





このシェーダーと関連するHDR環境マップを使用して実際にキューブを描画すると、次のような結果が得られます。









つまり 実際、長方形のテクスチャを立方体に投影したことがわかります。 素晴らしいですが、これは実際の立方体マップの作成にどのように役立ちますか? このタスクを終了するには、各キューブをカメラで6回レンダリングして、出力を別のフレームバッファーオブジェクトに書き込む必要があります。



 unsigned int captureFBO, captureRBO; glGenFramebuffers(1, &captureFBO); glGenRenderbuffers(1, &captureRBO); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 512, 512); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, captureRBO);
      
      





もちろん、将来のキュービックマップの6つの面のそれぞれを保存するためのメモリを整理することを忘れないでください。



 unsigned int envCubemap; glGenTextures(1, &envCubemap); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); for (unsigned int i = 0; i < 6; ++i) { //  ,     // 16     glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 512, 512, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      
      





この準備の後、立方体マップの寸前で等しい長方形のマップのパーツの転送を直接実行するだけです。



特にフレームバッファ無指向性シャドウのレッスンでコードが頻繁に見られるため、あまり詳細に説明しません。 原則として、キューブの各面に厳密にカメラを向ける6つの個別のビューマトリックスと、キューブの全面をキャプチャするための90度の角度を持つ特別な投影マトリックスを準備することになります。 次に、わずか6回、レンダリングが実行され、結果が浮動小数点フレームバッファーに保存されます。



 glm::mat4 captureProjection = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 10.0f); glm::mat4 captureViews[] = { glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(-1.0f, 0.0f, 0.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 1.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, -1.0f, 0.0f), glm::vec3(0.0f, 0.0f, -1.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, 1.0f), glm::vec3(0.0f, -1.0f, 0.0f)), glm::lookAt(glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3( 0.0f, 0.0f, -1.0f), glm::vec3(0.0f, -1.0f, 0.0f)) }; //  HDR        equirectangularToCubemapShader.use(); equirectangularToCubemapShader.setInt("equirectangularMap", 0); equirectangularToCubemapShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrTexture); //         glViewport(0, 0, 512, 512); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); for (unsigned int i = 0; i < 6; ++i) { equirectangularToCubemapShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, envCubemap, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); //    } glBindFramebuffer(GL_FRAMEBUFFER, 0);
      
      





ここでは、フレームバッファーの色のアタッチメントを使用し、キュービックマップの接続面を交互に変更します。これにより、環境マップの面の1つにレンダーが直接出力されます。 このコードを実行する必要があるのは1回だけです。その後、HDR環境マップの元の長方形のバージョンを変換した結果を含む本格的なenvCubemap環境マップが残ります。



最も単純なスカイボックスシェーダーをスケッチして、結果のキュービックマップをテストします。



 #version 330 core layout (location = 0) in vec3 aPos; uniform mat4 projection; uniform mat4 view; out vec3 localPos; void main() { localPos = aPos; //         mat4 rotView = mat4(mat3(view)); vec4 clipPos = projection * rotView * vec4(localPos, 1.0); gl_Position = clipPos.xyww; }
      
      





clipPosベクトルのコンポーネントのトリックに注意してください :頂点の変換された座標を記録するときにxyww tetradを使用して、スカイボックスのすべてのフラグメントの最大深度が1.0になるようにします(アプローチは対応するレッスンで既に使用されています )。 比較関数をGL_LEQUALに変更することを忘れないでください:



 glDepthFunc(GL_LEQUAL);
      
      





フラグメントシェーダーは、キュービックマップから選択するだけです。



 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; void main() { vec3 envColor = texture(environmentMap, localPos).rgb; envColor = envColor / (envColor + vec3(1.0)); envColor = pow(envColor, vec3(1.0/2.2)); FragColor = vec4(envColor, 1.0); }
      
      





マップからの選択は、立方体の頂点の補間されたローカル座標に基づいています。これは、この場合の選択の正しい方向です(再び、スカイボックスのレッスンで約Perで説明します )。 ビューマトリックス内のトランスポートコンポーネントは無視されたため、スカイボックスのレンダリングは観測者の位置に依存せず、無限遠の背景の錯覚を作成します。 ここでは、HDRカードからLDRレシーバーであるデフォルトのフレームバッファーにデータを直接出力するため、トーン圧縮をリコールする必要があります。 最後に、ほとんどすべてのHDRカードは線形空間に保存されます。つまり、最終処理コードとしてガンマ補正を適用する必要があります。



そのため、取得したスカイボックスを既におなじみの球体の配列と一緒に出力すると、同様のことが得られます。









さて、多くの努力が費やされましたが、最終的には、HDR環境マップを読み取り、それを同じように長方形から立方体のマップに変換し、HDRの立方体マップをシーンのスカイボックスとして出力することに慣れました。 さらに、立方体マップの6つの面にレンダリングすることで立方体マップに変換するコードは、環境マップの畳み込みのタスクでさらに役立ちます 。 変換プロセス全体のコードはこちらです。



キュービックカードの畳み込み



レッスンの冒頭で述べたように、私たちの主な目標は、環境の立方体マップの形で与えられたシーンの照射を考慮に入れて、間接拡散照明のあらゆる方向の積分を解くことです。 シーンのエネルギー輝度の値を取得できることが知られています Lpwi 任意の方向 wi HDRからその方向の環境の3次マップをサンプリングすることにより。 積分を解くには、半球内のあらゆる方向からシーンのエネルギー輝度をサンプリングする必要があります \オ 各レビュー済みフラグメント。

明らかに、半球のすべての可能な方向から環境から照明をサンプリングするタスク \オ 計算上実​​行不可能です-そのような方向は無限にあります。 ただし、ランダムに選択された、または半球内に均一に配置された有限数の方向をとることにより、近似を適用することができます。 これにより、真の照射に対してかなり良好な近似を得ることができ、本質的には対象の積分を有限和の形で解くことができます。



しかし、リアルタイムタスクの場合、フラグメントごとにサンプルが取得され、許容できる結果を得るためにサンプルの数が十分に多くなければならないため、このようなアプローチでも非常に強いられます。したがって、レンダリングプロセス以外で、このステップデータを事前準備しておくと便利です半球の向きによって、どの空間領域から照射をキャプチャするかが決まるため、考えられるすべての出射方向に基づいて、半球の考えられる向きごとに照射を事前に計算できます。wo







Lo(p,ωo)=kdcπΩLi(p,ωi)nωidωi









結果として、与えられた任意のベクトルに対して wi、計算された放射照度マップからサンプリングして、この方向の拡散放射照度を取得できます。現在のフラグメントのポイントでの間接拡散放射の大きさを決定するために、フラグメント表面の法線に沿って方向付けられた半球からの全照射を行います。言い換えれば、シーンの照射を取得することは簡単な選択になります。



  vec3 irradiance = texture(irradianceMap, N);
      
      





さらに、照射マップを作成するには、3次マップに変換された環境マップを畳み込む必要があります。各フラグメントについて、その半球が表面の法線に沿って方向付けられていると考えられていることを知っていますN この場合、立方体マップの畳み込みは、すべての方向からのエネルギー輝度の平均量を計算するために削減されます wi半球の内側\オ法線に沿って、オリエントN









幸いなことに、レッスンの最初に行った時間のかかる予備作業により、環境マップを特別なフラグメントシェーダーでキュービックマップに非常に簡単に変換できるようになり、その出力は新しいキュービックマップの形成に使用されます。このためには、等長方形の環境マップを立方体マップに変換するために使用されたコードの一部が役立ちます。



別の処理シェーダーを使用するだけです。



 #version 330 core out vec4 FragColor; in vec3 localPos; uniform samplerCube environmentMap; const float PI = 3.14159265359; void main() { //       vec3 normal = normalize(localPos); vec3 irradiance = vec3(0.0); [...] //   FragColor = vec4(irradiance, 1.0); }
      
      





ここで、environmentMapサンプラーは、以前に正三角形から派生した環境のHDRキュービックマップです。



環境マップを畳み込むには多くの方法があります。この場合、キュービックマップの各テクセルに対して、いくつかの半球サンプルベクトルを生成します。\オサンプルの方向に沿って、配向、及び結果を平均。サンプルベクトルの数は固定され、ベクトル自体は半球内に均等に分散されます。被積分関数は連続関数であり、この関数の離散推定は近似に過ぎないことに注意してください。そして、より多くのサンプリングベクトルを取得するほど、積分の分析解に近づきます。



反射率の式の被積分関数は立体角に依存しますdw使用するのにあまり便利ではない値。立体角にわたって積分する代わりにdw式を変更し、球面座標上の積分を導きます\シ そして  phi









角度Phiは、半球の底面の方位角を表し、0から 2π 角度 \シ 0からに至るまで、仰角を表します12π このような用語での反射率の修正表現は次のとおりです。







Lo(p,ϕo,θo)=kdcπϕ=02πθ=012πLi(p,ϕi,θi)cos(θ)sin(θ)dϕdθ









このような積分の解決には、半球で有限数のサンプルを取得する必要があります \オ、結果を平均します。サンプル数を知るn1 そして n2の各球面座標について、積分をリーマン和に変換できます





Lo(p,ϕo,θo)=kdcπ1n1n2ϕ=0n1θ=0n2Li(p,ϕi,θi)cos(θ)sin(θ)dϕdθ







両方の球座標が離散的に変化するため、上の図に見られるように、各瞬間にサンプリングが半球の特定の平均領域で実行されます。球面の性質により、個別のサンプリング領域のサイズは、仰角の増加とともに必然的に減少します\シと天頂へのアプローチ。この面積削減の効果を補うために、式に重み係数を追加しましたsinθ



その結果、コードの形式での各フラグメントの球面座標に基づく半球での離散サンプリングの実装は次のとおりです。



 vec3 irradiance = vec3(0.0); vec3 up = vec3(0.0, 1.0, 0.0); vec3 right = cross(up, normal); up = cross(normal, right); float sampleDelta = 0.025; float nrSamples = 0.0; for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) { for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) { //   .   (  -) vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); //      vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); nrSamples++; } } irradiance = PI * irradiance * (1.0 / float(nrSamples));
      
      





sampleDelta変数は、半球の表面に沿った離散ステップのサイズを決定します。この値を変更することにより、結果の精度を増減できます。



両方のサイクル内で、通常の3次元サンプルベクトルが球面座標から形成され、接線からワールド空間に転送され、HDRから立方体環境マップをサンプリングするために使用されます。サンプルの結果は放射照度変数に蓄積され、処理の終了時に、照射の平均値を取得するために作成されたサンプルの数で除算されます。テクスチャからのサンプリングの結果は、cos(theta) -大きな角度での光の減衰を考慮するための2つの量、およびsin(theta)によって変調されることに注意してください。-天頂に近づく際のサンプル面積の減少を補うため。envCubemap



環境マップの畳み込みの結果をレンダリングおよびキャプチャするコードを処理するためだけに残ります最初に、照射を保存するための立方体マップを作成します(メインレンダリングサイクルに入る前に1回行う必要があります)。



 unsigned int irradianceMap; glGenTextures(1, &irradianceMap); glBindTexture(GL_TEXTURE_CUBE_MAP, irradianceMap); for (unsigned int i = 0; i < 6; ++i) { glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, 32, 32, 0, GL_RGB, GL_FLOAT, nullptr); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
      
      





照射マップは、環境マップのエネルギー輝度の均一に分布したサンプルを平均化することで取得されるため、実際には高周波の部品や要素は含まれていません-かなり小さな解像度のテクスチャ(ここでは32x32)と有効な線形フィルタリングで十分に保存できます。



次に、キャプチャフレームバッファをこの解像度に設定します。



 glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); glBindRenderbuffer(GL_RENDERBUFFER, captureRBO); glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, 32, 32);
      
      





畳み込み結果をキャプチャするコードは、環境マップを等辺から立方体に転送するコードに似ています。畳み込みシェーダーのみが使用されます。



 irradianceShader.use(); irradianceShader.setInt("environmentMap", 0); irradianceShader.setMat4("projection", captureProjection); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_CUBE_MAP, envCubemap); //        glViewport(0, 0, 32, 32); glBindFramebuffer(GL_FRAMEBUFFER, captureFBO); for (unsigned int i = 0; i < 6; ++i) { irradianceShader.setMat4("view", captureViews[i]); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, irradianceMap, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); renderCube(); } glBindFramebuffer(GL_FRAMEBUFFER, 0);
      
      





この段階を完了すると、間接拡散照明の計算に直接使用できる事前計算された照射マップが手にあります。畳み込みがどのように行われたかを確認するために、環境マップのスカイボックステクスチャを照射マップで置き換えようとします。









その結果、環境の非常にぼやけたマップのように見えるものを見た場合、おそらく、畳み込みは成功しました。



PBRおよび間接照明



結果の照射マップは、反射率の分割式の拡散部分で使用され、間接照明のすべての可能な方向からの累積寄与を表します。この場合、ライトは特定のソースからではなく、環境全体から来ているため、拡散およびミラー間接照明を背景(アンビエントとして考慮し、以前に使用した定数値を置き換えます。



まず、照射マップを備えた新しいサンプラーを追加することを忘れないでください。



 uniform samplerCube irradianceMap;
      
      





シーンからの間接拡散放射および表面の法線に関するすべての情報を保存する照射マップを使用して、特定のフラグメントの照射に関するデータを取得することは、テクスチャから1つのサンプルを作成するのと同じくらい簡単です。



 // vec3 ambient = vec3(0.03); vec3 ambient = texture(irradianceMap, N).rgb;
      
      





ただし、間接放射には拡散成分とミラー成分の両方のデータが含まれるため(反射率の表現のコンポーネントバージョンで見たように)、特殊な方法で拡散成分を変調する必要があります。前のレッスンのように、フレネル式を使用して、特定の表面の光の反射の程度を決定します。そこから、光の屈折の程度または拡散係数を取得します。



 vec3 kS = fresnelSchlick(max(dot(N, V), 0.0), F0); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
      
      





背景照明は、表面の法線に基づいて半球のすべての方向から落ちます N、中央値のみを決定することは不可能です(中間)フレネル係数を計算するためのベクトル。このような条件下でフレネル効果をシミュレートするには、法線と観測ベクトルの間の角度に基づいて係数を計算する必要があります。ただし、以前は、フレネル係数を計算するためのパラメーターとして、マイクロサーフェスのモデルに基づいて、表面粗さに依存して得られた中央ベクトルを使用していました。この場合、粗さは計算パラメーターに含まれないため、表面による光の反射の程度は常に過大評価されます。間接照明は全体として、直接照明と同じように動作する必要があります。粗い表面からは、エッジでの反射の程度が低くなることが予想されます。しかし、粗さは考慮されていないため、間接照明のフレネル鏡面反射は、粗い非金属表面では非現実的に見えます(下の画像では、説明を明確にするために説明した効果を誇張しています)。









この迷惑を回避するには、SébastienLagardeが説明するプロセスであるFremlin-Schlick表現に粗さを導入します



 vec3 fresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) { return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0); }
      
      





フレネルセットを計算するときに表面の粗さが与えられると、背景成分を計算するためのコードは次の形式を取ります。



 vec3 kS = fresnelSchlickRoughness(max(dot(N, V), 0.0), F0, roughness); vec3 kD = 1.0 - kS; vec3 irradiance = texture(irradianceMap, N).rgb; vec3 diffuse = irradiance * albedo; vec3 ambient = (kD * diffuse) * ao;
      
      





結局のところ、画像ベースの照明の使用は、本質的に3次マップからの1つのサンプルに要約されます。すべての困難は、主に環境マップの事前準備と照射マップへの転送に関連しています。さまざまな金属性と粗さの球体の配列を含む分析光源の



レッスンからおなじみのシーンを取り、環境からの拡散背景照明を追加すると、次のようになります。









メタリック度の高い素材は、本当にうーん、金属を見るために反射を必要とするため、やはり奇妙に見えます(金属は拡散照明を反射しません)。また、この場合、点分析光源からの反射のみが取得されます。それでも、表面はシーン環境からの背景照明に正しく応答するようになったため、球体は環境に特に没頭しているように見えます(特に環境マップを切り替えると顕著になります)。



レッスンの完全なソースコードはこちらです。次のレッスンでは、間接鏡面反射照明の原因となる反射率の表現の後半を最終的に扱います。このステップの後、照明におけるPBRアプローチの威力を実感できます。



追加資料





PS転送を調整するための電報confがあります。翻訳を手伝いたいという真剣な願望があれば、大歓迎です!



All Articles