リンク
- 物理ベースレンダリングを柔らかく説明してみる(1)
- 物理ベースレンダリングを柔らかく説明してみる(2)
- 物理ベースレンダリングを柔らかく説明してみる(3)
- 物理ベースレンダリングを柔らかく説明してみる(4)
- 物理ベースレンダリングを柔らかく説明してみる(5)
- 物理ベースレンダリングを柔らかく説明してみる(6) <--今回
今回はIBL(イメージベースド・ライティング)について
第6回のテーマを何にしようかと思案していたのですが、IBLについてのリクエストが多かったので今回取り上げます。
物理ベースレンダリングと一言に言っても、ディレクショナルライトやポイントライト、スポットライトといった、いわゆるパンクチュアル・ライト(Punctual Light)のみでは良い絵にならないことが多くあります。
こうしたPunctual Lightは面積がゼロという、現実の世界ではあり得ない仮想的な光源ですので、現実には起こり得ない反射を生み出します。現実的な豊かなライティングを生成するには、より現実に即した光源データが必要です。その表現・実装方法の1つがIBL(イメージベースド・ライティング)です。
この記事では、現在のリアルタイムレンダリングにおけるIBLの最もベースとなる技術資料である「Real Shading in Unreal Engine 4」の内容を解説しながら、IBLの仕組みと取り扱いについて学びます。
IBL(イメージベースド・ライティング)とは
IBL(イメージベースド・ライティング)とは、カメラ写真などから作られたHDRI(HDR画像)をもとに、それらのピクセル値を輝度とみなしてライティングに用いるライティング手法です。
いくつかのWebサイトが、ライセンスフリーのHDR画像を配布しています。
現行主流なIBLの方式
結論から言ってしまうと、現在のリアルタイムIBLで主流の方式では、物体の拡散反射(Diffuse反射)と鏡面反射(Specular反射)を分けて処理します。その際の光の入力もそれぞれ別に分けます。
大元のHDR画像
に対し、これを処理して生成されるIBLテクスチャ画像は、それぞれ次のようなものです。
ディフューズ用IBLテクスチャ
スペキュラ用IBLテクスチャ
ディフューズ用IBLテクスチャはなんだかとてもボケていますね。スペキュラ用IBLテクスチャはミップマップレベルごとに分かれていますが、ミップマップレベルが高い(小さいミップマップ)ほど、内容はよりボケたものになっていることがわかるかと思います。
これらディフューズ用IBLテクスチャとスペキュラ用IBLテクスチャは、共に大元の素材となるHDRI(HDR画像)から加工されて作られるものです。大元のHDR画像に「ボカす加工」をされて作られているように見えますが、この「ボカす」処理は「プレフィルタリング処理」と言います。ボカすといっても、実際はなんとなくぼかしているのではなく、その裏にきちんとした理論的背景があって、そのアルゴリズムに従って処理しています。その結果、見た目上ボケたような感じの画像に変わるのです。
今回は、このプレフィルタリング処理に重点を置いてお話ししようと思います。そもそも、リアルタイムIBLにおいて、なぜこのプレフィルタリング処理が必要なのでしょうか。
レンダリング方程式を思い出そう
以前の回で見たレンダリング方程式を振り返りましょう(ここではエミッション項をあえて除いています)。
L_r(x, \vec \omega) = \int_\Omega f_r(x, \vec \omega', \vec \omega)(\vec n \cdot \vec \omega') L_i(x, \vec \omega') d \vec \omega'
$f_r$ はBRDF、$Li$ は入射光、$(\vec n \cdot \vec \omega')$はコサイン項で、これらを乗算したものを半球積分しています。この結果が、物体表面から反射して目に届く光量になるわけです。このレンダリング方程式はレイトレーシングなどのオフラインレンダリングで必ず使われます。
一方のリアルタイムレンダリングで、特にポイントライトやディレクショナルライトなどのパンクチュアルライトを光源に使う場合は、次の式を使います。
L_r(x) = f_r(x,\vec\omega', \vec\omega) E(x)
ちなみに$E(x)$は入射光なのですが、単位としては照度または放射照度です。
シェーダーで式を書くとき、実際には$E(x)=(\vec n \cdot \vec \omega') L_{ie}$であり、 $L_{ie}$はパンクチュアルライトから放たれた光です。
先のオフラインレンダリングにおける積分式での入射光$L_i$の単位は輝度または放射輝度であり、リアルタイムレンダリングにおける$L_{ie}$とは光の単位が異なります。
ここまではおさらいです。
さて、冒頭でお話したIBL(イメージベースドライティング)は、写真などから作成されたハイダイナミックレンジの画像を用いることで、360度あらゆる方向から入射する光を再現できます。
この「あらゆる方向から入射する」がポイントで、これを反射計算で処理するには想像できるかもしれませんが、パンクチュアルライトと異なり、物体表面上の点に降り注ぐ入射光を半球状に考慮(積分する)必要が出てくるのです。
でもリアルタイムで積分はやりたくない
半球面上で積分するためには、半球面上にサンプリングポイントを大量に設定し、そのポイントごとにBRDFを計算しなくてはなりません。当然重たい処理ですので、リアルタイムレンダリングでこれを直接やるとなると、現行のGPUを持ってしてもゲームのような60FPSはおろか、数FPSのインタラクティブなフレームレートを実現することも難しいでしょう。
プレフィルタリング処理は、一言で言うとこの積分処理を前もってオフラインでやってしまおうというものです。
前もって積分をやっておくのがプレフィルタリング処理
プレフィルタリング処理によって積分が行われた光源情報が収められたのが、前述のディフューズ用IBLテクスチャ、スペキュラ用IBLテクスチャになります。
これらのプレフィルタリング(積分)済みIBLテクスチャをリアルタイムレンダリング時に参照することで、レンダリング時には積分をせずに、レンダリング方程式と同等(厳密には近似ですが)の計算を行って反射光を求めることができます。
このプレフィルタリング処理の考え自体はだいぶ前からあるのですが、具体的な処理の仕方や前計算したデータをどういうデータフォーマットに収めるか、といったことは技術の進展によって少しずつ変遷してきました。2013年に登場したEpic Gamesによる技術資料「Real Shading in Unreal Engine 4」で提案された手法でGGXを考慮したIBLの取り扱いがしやすくなり、それ以降現在のリアルタイムIBLの基礎を成しています。
「Real Shading in Unreal Engine 4」の内容
さて、ここからは技術資料「Real Shading in Unreal Engine 4」の内容をもとに解説を行っていきますが、皆様にもできれば「Real Shading in Unreal Engine 4」の原書を読みながら理解を進めてもらえればと思うので、これより先は物理量の表記などを本シリーズの表記から「Real Shading in Unreal Engine 4」の表記に合わせたいと思います。
「Real Shading in Unreal Engine 4」はこのタイトル名でGoogle検索すればすぐに入手できます。
例えば、これまでこの記事シリーズにおけるレンダリング方程式の表記は以下でしたが、
L_r(x, \vec \omega) = \int_\Omega f_r(x, \vec \omega', \vec \omega)(\vec n \cdot \vec \omega') L_i(x, \vec \omega') d \vec \omega'
ここから先は、原書に合わせて次のようにします。
L_r(v) = \int_H L_i(l) f(l,v)cos \theta_l dl
この式を実際にレンダリングするために離散式に置き換えると、次のようになります。
L_r(v) = \int_H L_i(l) f(l,v)cos \theta_l dl \approx \frac{1}{N} \sum_{k=1}^N \frac{L_i(l_k)f(l_k,v)cos\theta_{l_k}}{p(l_k,v)}
ここで、$p(l_k,v)$ はpdf(probability density function:確率密度関数)です。後述するインポータンスサンプリングを行う際の各サンプリングポイントの寄与率の違いを、このpdfで割ることで考慮していると考えてください。
この離散式ですが、ここでいうNがサンプリングポイントの数になります。結構な数が必要ですので、シェーダーでリアルタイムで計算するには当然重たい処理になります。
サンプリングポイントについては、Holger Dammertz氏によるこちらのページ「Hammersley Points on the Hemisphere」がわかりやすいです。
(上記Holger Dammertz氏のページのデモのキャプチャ。ポイント数256、Cosine Weightedのインポータンスサンプリングの様子)
近似テクニック「Split Sum Approximation」
さて、この重たい処理を事前に計算しテクスチャなどに格納してしまいたいのですが、「Real Shading in Unreal Engine 4」ではここで「Split Sum Approximation」という近似テクニックを使って、この式を大きく2つに分離します。
\frac{1}{N} \sum_{k=1}^N \frac{L_i(l_k)f(l_k,v)cos\theta_{l_k}}{p(l_k,v)} \approx \left(\frac{1}{N} \sum_{k=1}^N L_i(l_k)\right)\left(\frac{1}{N} \sum_{k=1}^N \frac{f(l_k,v)cos\theta_{l_k}}{p(l_k,v)}\right)
記号$\approx$が示すとおり、左辺と右辺は等しいわけではありませんが、似た結果になります。近似とはいえ、原書中の記述によれば、定数$L_i$については正確であり,一般的な環境についてもほとんど正確であるとしています。
この近似を行うことで、前計算すべき対象を、2種類に大きく分離することができます。1つは(左側)は光源情報、もう1つ(右側)はBRDFです。言い換えると、この近似はライトの情報とマテリアルの情報を分離して取り扱うことを可能にするものです。
では次に、左側の光源情報の積分$\left(\frac{1}{N} \sum_{k=1}^N L_i(l_k)\right)$を見ていきましょう。「Real Shading in Unreal Engine 4」では、この光源情報の積分を「Pre-Filtered Environment Map」と呼んでいます。
Pre-Filtered Environment Map
Pre-Filtered Environment Mapには2種類あります。冒頭でお話ししたディフューズIBLテクスチャとスペキュラIBLテクスチャです。この2つの名称は私が説明の都合上、今は便宜上そう呼んでいるだけで、CG界隈での共通用語ではありません。もう少し普及している呼称として、前者は照度マップ(イラディアンスマップ)、後者は放射輝度マップと呼ばれることがあります。
どちらもオフラインでGPUを使って(CPUでも別に良いですが遅いです)積分を計算します。
放射輝度マップの生成
スペキュラIBLに関わる放射輝度マップから説明します。
「Real Shading in Unreal Engine 4」では後者の放射輝度マップを生成するHLSLコードが掲載されています。
コメントについては私が付加したものです。
float3 PrefilterEnvMap( float Roughness, float3 R ) { // Rは反射ベクトル
float3 N = R;
float3 V = R; // 観測者が物体表面の真上から見下ろしていることを前提とする(V = N = R)
float3 PrefilteredColor = 0;
const uint NumSamples = 1024; // サンプリング数
for( uint i = 0; i < NumSamples; i++ )
{
float2 Xi = Hammersley( i, NumSamples ); // Hammersley関数による2D上のサンプル分布を生成
float3 H = ImportanceSampleGGX( Xi, Roughness, N ); // GGXを想定したインポータンスサンプリングを行う。
float3 L = 2 * dot( V, H ) * H - V; // ライトベクトルを計算
float NoL = saturate( dot( N, L ) );
if( NoL > 0 )
{
PrefilteredColor += EnvMap.SampleLevel( EnvMapSampler, L, 0 ).rgb * NoL; // HDRキューブマップ画像からライトベクトルの方向へピクセル値(輝度)をフェッチ、NoLと乗算
TotalWeight += NoL; // トータルの重みを蓄積
}
}
return PrefilteredColor / TotalWeight; // 重みで割る(加重平均)
}
float3 ImportanceSampleGGX( float2 Xi, float Roughness, float3 N ) {
float a = Roughness * Roughness;
float Phi = 2 * PI * Xi.x;
float CosTheta = sqrt( (1 - Xi.y) / ( 1 + (a*a - 1) * Xi.y ) );
float SinTheta = sqrt( 1 - CosTheta * CosTheta );
float3 H;
H.x = SinTheta * cos( Phi );
H.y = SinTheta * sin( Phi );
H.z = CosTheta;
float3 UpVector = abs(N.z) < 0.999 ? float3(0,0,1) : float3(1,0,0);
float3 TangentX = normalize( cross(UpVector, N) );
float3 TangentY = cross ( N, TangentX );
// Tangent to world space
return TangentX * H.x + TangentY * H.y + N * H.z;
}
放射輝度マップはスペキュラ反射の項で用いられるIBLテクスチャですので、想定されるBRDFはスペキュラ反射のBRDFになります。スペキュラ系BRDFではラフネス(Roughness)がパラメータとして含まれています。ラフネス(表面上のマイクロスケールでの粗さ)が大きいと、その場所での光の反射は鈍いものになります。放射輝度マップはPre-Filtered Environment Mapの積分$\left(\frac{1}{N} \sum_{k=1}^N L_i(l_k)\right)$に相当しますが、生成に際しては対応するBRDFの反射特性を考慮したインポータンスサンプリングを行う必要があります。
サンプリングについて
ここで、Hammersley関数について紹介しておきましょう。
float2 Xi = Hammersley( i, NumSamples ); // Hammersley関数による2D上のサンプル分布を生成
Hammersleyは昇順に並んでいく数列から、二次元に満遍なくサンプルが分布するような分布を作ってくれる関数です。
Holger Dammertz氏のページ「Hammersley Points on the Hemisphere」に実際に試せるデモがあります。
続いてのImportanceSampleGGX関数ですが、ここがインポータンスサンプリングです。ここではGGXの特性を考慮したものになっています。
float3 H = ImportanceSampleGGX( Xi, Roughness, N ); // GGXを想定したインポータンスサンプリングを行う。
この関数を経ることで、Hammersleyによる満遍なく散らばった2D上の一様分布は、マテリアルのBRDFの特性を考慮した(重要な箇所に偏った)3D上のサンプルになります。
いわばこのような感じです(以下のキャプチャはGGXではなくコサイン項でインポータンスサンプリングしたものですが)
放射輝度マップはRoughnessごとにミップマップに分けて格納する
float3 PrefilterEnvMap( float Roughness, float3 R ) { // Rは反射ベクトル
とある通り、引数にはRoughnessが存在します。引数Rは反射ベクトルですから、リアルタイムレンダリング時に放射輝度マップをフェッチする際に反射ベクトルを使えば良いことを意味します。
放射輝度マップでは、異なるRoughnessの値ごとに、異なるミップマップレベルに値を格納します。従って、リアルタイムレンダリング時に放射輝度マップをフェッチする際には、マテリアルのRoughness値によってテクスチャアクセス時のミップマップLOD指定を切り替えれば良いことになります。
このやり方自体は「Real Shading in Unreal Engine 4」以前にも、いくつかの商業ケースで行われていたようです。「Real Shading in Unreal Engine 4」での新規部分は、前述のコードのように、GGXによる畳み込みを行ったことでした。
しかしこのGGX、以前の回でも見たと思いますが、定義を見れば分かる通り、視線ベクトルに依存しています(マイクロファセットベースのBRDFはハーフベクトル(ということは視線ベクトル)に依存している)。
光源情報の積分にもかかわらず、視線ベクトルが入ってくるのです。やむを得ず、「Real Shading in Unreal Engine 4」ではV = N = Rという限定条件を入れています(これはグレージング角からかすめて反射を観察する際に、GroundTruthとは異なる結果を生み出すというデメリットがあります)。
照度マップの生成
次に、ディフューズIBLに関わる照度マップの生成です。
前節「放射輝度マップの生成」におけるImportanceSampleGGX関数の部分が次のImportanceSampleCosineWeighted関数(この関数は私が作成したもので、「Real Shading in Unreal Engine 4」には記載されていません)に置き換わっている他、呼び出し側のPrefilterLambertEnvMap関数にも「放射輝度マップの生成」時のPrefilterEnvMap関数とは若干の差異があります。
float3 PrefilterLambertEnvMap( float Roughness, float3 N ) { // Nは法線ベクトル
float3 LambertColor = 0;
const uint NumSamples = 1024; // サンプリング数
for( uint i = 0; i < NumSamples; i++ )
{
float2 Xi = Hammersley( i, NumSamples ); // Hammersley関数による2D上のサンプル分布を生成
float3 H = ImportanceSampleCosineWeighted( Xi, N ); // CosineWeightedインポータンスサンプリングを行う。
LambertColor += EnvMap.SampleLevel( EnvMapSampler, H, 0 ).rgb; // HDRキューブマップ画像からピクセル値(輝度)をフェッチ
}
return LambertColor / float(NumSamples);
}
float3 ImportanceSampleCosineWeighted(float2 Xi, float3 N)
{
float r = sqrt(Xi.x);
float phi = 2 * PI * Xi.y;
float3 H;
H.x = r * cos(phi);
H.y = r * sin(phi);
H.z = sqrt(1-Xi.x);
float3 UpVector = abs(N.z) < 0.999 ? float3(0,0,1) : float3(1,0,0);
float3 TangentX = normalize( cross(UpVector, N) );
float3 TangentY = cross ( N, TangentX );
// Tangent to world space
return TangentX * H.x + TangentY * H.y + N * H.z;
}
Environment BRDF
さて、ここでSplit Sum Approximationの式に戻りましょう。
\frac{1}{N} \sum_{k=1}^N \frac{L_i(l_k)f(l_k,v)cos\theta_{l_k}}{p(l_k,v)} \approx \left(\frac{1}{N} \sum_{k=1}^N L_i(l_k)\right)\left(\frac{1}{N} \sum_{k=1}^N \frac{f(l_k,v)cos\theta_{l_k}}{p(l_k,v)}\right)
次は近似の右の部分$\left(\frac{1}{N} \sum_{k=1}^N \frac{f(l_k,v)cos\theta_{l_k}}{p(l_k,v)}\right)$の生成です。この部分は「Real Shading in Unreal Engine 4」ではEnvironment BRDFと呼ばれています(私の肌感ですが、この呼び方はリアルタイムCG界隈ではかなり一般的な名称のように思います)
順を追って説明しましょう。
\left(\frac{1}{N} \sum_{k=1}^N \frac{f(l_k,v)cos\theta_{l_k}}{p(l_k,v)}\right)
Environment BRDFの項、ここでは離散化された表記になっていますが、元の連続的な式で表すと、次のようになります。
\int_H f(l,v)cos\theta_l dl
ここで、以前習ったSchlickのフレネル式$F(v,h)=F_0+(1-F_0)(1-{v \cdot h})^5$を思い出してください。BRDF $f(l,v)$ にはフレネル項が含まれています。ここで $F_0$について括り出してみると、次の式を得ます。
\int_H f(l,v)cos\theta_l dl = F_0 \int_H \frac{f(l,v)}{F(v,h)} \left( 1 - (1 - v \cdot h)^5 \right)cos\theta_l dl + \int_H \frac{f(l,v)}{F(v,h)} (1 - v \cdot h)^5 cos\theta_l dl
この式の第1項の$F_0$を除いた部分をテクスチャの赤成分、第2項をテクスチャの緑成分に入れることで、次のような特徴的な色合いのルックアップ2Dテクスチャが出来上がります。
これはいわば積分されたBRDF、俗にEnvironment BRDF 2D LUTと呼ばれるものです。
このLUT画像を生成するためのコードを以下に示します。
float2 IntegrateBRDF( float Roughness, float NoV ) {
float3 V;
V.x = sqrt( 1.0f - NoV * NoV ); // sin
V.y = 0;
V.z = NoV; // cos
float A = 0;
float B = 0;
const uint NumSamples = 1024;
for( uint i = 0; i < NumSamples; i++ ) {
float2 Xi = Hammersley( i, NumSamples );
float3 H = ImportanceSampleGGX( Xi, Roughness, N );
float3 L = 2 * dot( V, H ) * H - V;
float NoL = saturate( L.z );
float NoH = saturate( H.z );
float VoH = saturate( dot( V, H ) );
if( NoL > 0 ) {
float G = G_Smith( Roughness, NoV, NoL );
float G_Vis = G * VoH / (NoH * NoV);
float Fc = pow( 1 - VoH, 5 );
A += (1 - Fc) * G_Vis;
B += Fc * G_Vis;
}
}
return float2( A, B ) / NumSamples;
}
これはBRDFに対応するものなので、GGXにはGGXのEnvironment BRDF、他のBRDF(Sheen BRDFなど)にはそれらに応じたEnvironment BRDFを生成する必要があります。逆にいえば、一度生成してしまえば、同じBRDFを使うマテリアル間では同じものをそのまま継続して使うことができます。
リアルタイムレンダリング時
さて、以上を持って前計算部分は終わりです。次に、これまでに構築したディフューズIBLテクスチャ(照度マップ)、スペキュラIBLテクスチャ(放射輝度マップ)、Enbironment BRDFを使ってリアルタイムレンダリングを行うことができます。
スペキュラ反射の場合
Split Sum Approximationの計算をシェーダー上で行います。Split Sum Approximationは、レンダリング方程式の近似でしたね。
ほぼスペキュラIBLテクスチャ(放射輝度マップ)とEnvironment BRDF 2D LUTを参照するだけで、簡単にできてしまいます。
float3 ApproximateSpecularIBL( float3 SpecularColor, float Roughness, float3 N, float3 V )
{
float NoV = saturate( dot( N, V ) );
float3 R = 2 * dot( V, N ) * N - V;
float3 PrefilteredColor = PrefilterEnvMap( Roughness, R ); // プレフィルタリングされた入射光(スペキュラIBLテクスチャ)(Split Sum Approximationの左側)
float2 EnvBRDF = IntegrateBRDF( Roughness, NoV ); // Environment BRDF 2D LUT
return PrefilteredColor * ( SpecularColor * EnvBRDF.x + EnvBRDF.y ); // Split Sum Approximationの計算。ここでSpecularColorはF_0に相当
}
このApproximateSpecularIBL関数はReal Shading in Unreal Engine 4
に掲載されている関数です。コード中のPrefilterEnvMap
ですが、前述のこの関数を実際に実行するのではなく、実際にはPrefilterEnvMap
で前処理して生成したスペキュラIBLテクスチャ(放射輝度マップ)を参照するという意味で捉えてください。それがPrefilterEnvMap
を実行するのと同じ結果になるという意味です。IntegrateBRDF
関数についても同様で、IntegrateBRDF関数を実行するという意味ではなく、実際にはここではIntegrateBRDF関数で前生成しておいたIntegrate BRDF 2D LUTテクスチャを参照する、という意味です。
このApproximateSpecularIBL関数を持って、リアルタイムレンダリングにおけるIBLを用いたスペキュラ反射ができたことになります。
ディフューズ反射の場合
ディフューズ反射の場合はさらに簡単です。
ディフューズIBLテクスチャ(照度マップ)は照度であるため、
L_r(x) = f_r(x,\vec\omega', \vec\omega) E(x)
の公式がそのまま使えます。つまり、次のようにすれば良いです(ApproximateDiffuseIBL関数は私の自作の関数です。「Real Shading in Unreal Engine 4」には載っていません)。
float3 ApproximateDiffuseIBL( float3 DiffuseColor, float3 N )
{
float3 IrradianceColor = IrradianceEnvMap( Roughness, N ); // IrradianceEnvMap関数はPrefilterLambertEnvMapで生成済みのディフューズIBLテクスチャ(照度マップ)を参照する
return DiffuseColor * IrradianceColor;
}
あとは、ディフューズIBLとスペキュラIBLの結果を足せばIBLによるライティングは完了です。
float3 IBL(float3 DiffuseColor, float3 SpecularColor, float Roughness, float3 N, float3 V)
{
return ApproximateDiffuseIBL(DiffuseColor, N) + ApproximateSpecularIBL(SpecularColor, Roughness, N, V);
}
IBLテクスチャ向きのピクセルフォーマット
プレフィルタリングした照度マップや放射輝度マップも、元のHDR画像と同じく非常に広いダイナミックレンジを持ちます。そのため、値の格納およびリアルタイムレンダリング時の参照にあたっては、フォーマットの選択が重要です。
どのフォーマットを選ぶべきかは、動作させるプラットフォームの性能・機能上の制約とクオリティのバランスによって変わります。
少し前であればdLDRやRGBMやRGBDといった、アルファチャンネルにダイナミックレンジを広げる情報を入れるピクセルフォーマットが使われていました。これは各色8ビットの古典的なピクセルフォーマットしかサポートされないプラットフォームにも対応できる利点があります。
しかし最近のプラットフォームであれば、RG11FB10FやBC6Hなどの優れたフォーマットを利用できる可能性があります。
整数フォーマット
dLDR
[0,x]
(x>1)の値域を[0,1]にマッピングして各チャンネルの輝度を保存する形式です。最も低スペックな環境でも動作させることができます。
精度的には2倍([0,2])がせいぜい限度で、この程度ではあまりリアルな表現はできないと考えた方が良いでしょう。
RGBM
RGBM エンコードは、アルファチャンネルにRGB最大値のスケールを、RGBチャンネルにカラー/スケール値を保存するHDRエンコーディング形式です。
テクスチャフィルタリングされる値は不正(スケール値を何の工夫もなく線形補間するので)なため、部位によっては視覚上のアーティファクトが発生する可能性があります。
RGBD
RGBDエンコードは、アルファチャンネルにRGBにおける最大値の逆数を格納します。
こちらもテクスチャフィルタリング時の問題があります。
RGBE
アルファチャンネルにRGB共通の指数を格納するフォーマットです。
ソフトウェア的にエンコード・デコードしてもいいですし、WebGL2/OpenGL3以降では、RGB9_E5という専用フォーマットがGPUネイティブでサポートされています。こちらの場合はテクスチャフィルタリングも破綻がないように補間してくれるかもしれません(未確認)
浮動小数点フォーマット
RG11FB10F
赤と緑のチャンネルに11ビットの浮動小数点、青のチャンネルに10ビットの浮動小数点で輝度を保存するフォーマットです。おそらくですがRGB9_E5よりも高精度と思われます。
圧縮フォーマット
BC6H
HDR対応の圧縮フォーマットです。RGB9_E5やRG11FB10Fなどと近い品質でデータサイズを1/4に抑えることができます。
太陽光の扱い
リアルタイムレンダリングとオフラインレンダリングで運用を分けることが多いです。
リアルタイムレンダリングではテクスチャアクセス性能やデータサイズの制約上、せいぜい16bit浮動小数点でダイナミックレンジを表現することが現世代では現実的です。しかし16bit HDRでは暗闇の薄暗さから太陽付近の明るさまでを十分な量子化解像度で表現することはできません(無理にやろうとすれば、人間生活上の領域の輝度段階はガタガタになるでしょう)。
そのため、リアルタイムレンダリング時でのHDR画像には太陽とその付近の極端に高い輝度は取り除く処理を行い、太陽光を平行光源で擬似表現することが通常です。
大体、地球に直接降り注ぐ太陽光の強さ(直射日光の強さ)は、晴天時の天空光を半球状に集めた量の大まかに5倍程度と覚えておくと良いでしょう。
特に野外のシーンではこのようにIBLによる天空光と平行光源による太陽の直接光の合わせ技でライティングします。
オフラインレンダリングの場合は、32bit浮動小数点のデータが問題になることは少ないため、暗闇から太陽光までのダイナミックレンジを十分な量子化解像度で収めることができます。
最後に
今回は、リアルタイムIBLについてお話ししました。全体的な流れを掴むことに限定したため、途中で色々と疑問が浮かんだと思います。
ImportanceSampleGGX関数やImportanceSampleCosineWeighted関数はどうやって導出するのか。PrefilterEnvMap関数とPrefilterLambertEnvMap関数はどうして実装に細部の差異があるのか。これらはいずれ補講のような形で別の機会に説明できたらと思います。
また、今回のサンプリング方法ではよほどサンプル数を増やさないと、精度的に「fireflies」と呼ばれるアーティファクトが出ると思います。これを少ないサンプル数で大幅に改善する「Mipmap Filtered Sampling」という手法もあるのですが、長くなるので今回は説明を省いています。こちらもいずれやれたらと思います。
いずれにしても「Real Shading in Unreal Engine 4」におけるSplit Sum Approximationの考え方は現在のリアルタイムIBLの基礎を成しています。全体的な流れ・考え方を理解していただければ幸いです。
参考文献
- Brian Karis, Epic Games, "Real Shading in Unreal Engine 4", 2013
- Holger Dammertz, Hammersley Points on the Hemisphere, http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
- 「レンダリングにおけるimportance samplingの基礎」, shikihuiku – 色不異空 – Real-time rendering topics in Japanese.
- https://www.pbr-book.org/3ed-2018/Monte_Carlo_Integration/2D_Sampling_with_Multidimensional_Transformations#Cosine-WeightedHemisphereSampling
- Sébastien Lagarde, PI or not to PI in game lighting equation, https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/
- Bruno Opsenica, Image Based Lighting with Multiple Scattering, https://bruop.github.io/ibl/
- Julien Guertault (Zavie), Gamma correct and HDR rendering in a 32 bits buffer, http://lousodrome.net/blog/light/tag/rgbd/