BRDF(GGX)実装してみた
本記事について
本記事はわたくしが実装したBRDFについて解説するものです。
実装内容の概要紹介であり、BRDF自体についての解説や実装方法の詳細については深く触れていません。そのため、読者の方がBRDFやPBRといったことをある程度知っていることを前提に書いています。
作ったもの
こちらが作成したものです。
ただし、シャドウについてはリアルタイム生成ではなく事前にベイクしたテクスチャを使用しています。
こちらがシャドウをオフにしたものです。こちらの方がBRDFの効果がわかりやすいかもしれません。
今回作成したBRDFについて
物理ベースでのBRDFになります。ディフューズはランバート、スペキュラはマイクロファセット理論に基づく実装となっています。
開発にはDirectX12を使用し、本記事内で記述しているコードはhlslになっています。
ディフューズBRDF
ディフューズ(拡散反射)を表すBRDFについて解説します。
ランバートBRDF
今回、ディフューズBRDFにはランバートモデルを採用しています。
BRDF(
RGBごとの拡散反射率。これは材質によって固有の値になる。 |
実装
float3 Lambert(
float3 reflection_rate // 反射率
)
{
return reflection_rate / F_PI; // F_PI は事前に定義された円周率
}
スペキュラBRDF
マイクロファセットモデルにおけるスペキュラBRDFは以下の式で表されます。
記号 | 説明 |
---|---|
BRDF(Specular) | |
法線分布関数 | |
幾何関数 | |
フレネル関数 | |
マイクロサーフェイスの粗さを表すスカラー値 | |
視線ベクトル | |
光源ベクトル | |
ハーフベクトル( |
|
ジオメトリの法線ベクトル | |
ジオメトリ法線ベクトルと視線ベクトルのなす角が0度のときのフレネル反射率 |
法線分布関数(GGX)
法線分布関数にはGGXを採用します。
実装は以下
float D_GGX(
float a, // alpha, 通常はパラメータ値 roughness の二乗となる
float NoH // ジオメトリ法線とハーフベクトルの内積
)
{
// 変数名は適当なので深く考えないでほしい
NoH = saturate(NoH);
float a2 = a * a;
float b = 1 + (NoH * NoH) * (a2 - 1);
return a2 / (b * b * F_PI);
}
幾何関数(SmithCorrelated_GGX)
幾何減衰関数はSmithモデルを使用します。式としては以下のようになります。
左辺のうち、右側がシャドウイング(ライトから見えない)、左側がマスキング(カメラから見えない)になります。
↑の
これを整理すると
実装は以下
float G2_SmithCorrelated_GGX
(
float NoV,
float NoL,
float a
)
{
NoV = abs(NoV) + 1e-5;
NoL = saturate(NoL);
float a2 = a * a;
float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
return 0.5 / (GGXV + GGXL);
}
フレネル反射関数
フレネル反射関数にはシュリックの近似を使用します。
実装は以下
float3 Schlick(
float3 f0,
float NoL
)
{
float a = 1 - saturate(NoL);
return f0 + (float3(1, 1, 1) - f0) * (a * a * a * a * a);
}
f_0 の導出
だいたい次のような感じ
- 導体(金属)と誘電体(非金属)を分けて考える
- 導体の場合はベースカラーをそのまま
値として使用f_0 - 誘電体の場合は反射率(refrectance)から計算
誘電体の場合
今回の実装では、誘電体の
Physically Based Rendering in Filament より
一般的な誘電体材質のフレネル反射率はダイヤモンドの0.16を超えることはありません。そのため、
導体の場合
導体(金属)の場合はベースカラーをそのままフレネル反射率として用います。導体の場合は拡散反射光はないので、拡散反射率を保持する必要はありません。
スペキュラBRDF のまとめ
これまでの情報をまとめてスペキュラBRDFを実装します。
float3 BRDF_Specular_GGX
(
float3 N, // マクロ法線
float3 L, // ライト
float3 V, // 視線
float3 baseColor, // ベースカラー
float roughness, // ラフネス値
float3 f0 // f0
)
{
float3 H = normalize(L + V);
float NoV = dot(N, V);
float NoL = dot(N, L);
float NoH = dot(N, H);
float LoH = dot(L, H);
float a = roughness * roughness;
// 法線分布
float D = D_GGX(a, NoH);
// 幾何減衰
float G = G2_SmithCorrelated_GGX(NoV, NoL, a);
// フレネル
float3 F = Schlick(f0, NoL);
// スペキュラBRDF
return (D * G) * F;
}
導体と誘電体の振り分け
導体と誘電体の振り分けにはmetallic
という値を使用します。metallic
は
基本的には、非金属であればmetallic=0
となり、金属であればmetallic=1
となります。metallic=0.5
のような材質はあまり存在しません。鉄錆などはmetallic
が中途半端な値になるかも。
拡散反射光
導体(金属)ではディフューズ反射光はなく、誘電体のみディフューズ反射光が存在します。そのため、線形補間を使ってディフューズ反射光の強さを調整します。
実装は以下のとおりです。
float3 BRDF_Diffuse_Lambert
(
float3 baseColor, // ベースカラー
float metallic // メタリック
)
{
//拡散反射(ランバート)
return Lambert((1.0 - metallic) * baseColor.rgb);
}
スペキュラ反射光
スペキュラ反射光でmetallic
に依存するのはフレネル反射成分です。フレネル反射を計算するときに使用するmetallic
を使用します。
f_0 の値
metallic
をつかって導体と誘電体を線形補間します。
実装は以下のとおり
float3 Fresnel0
(
float3 baseColor,
float metallic,
float reflectance
)
{
// 誘電体(Dielectrics)のf0値
// 誘電体の場合はマテリアルベースカラーに依存しない(無彩色)
// また、自然界にある材質だとせいぜい0.16(ダイヤモンド)が最大値である
// よって、0.16を最大値としてreflectanceをリマッピングする
float3 f0_D = reflectance * reflectance * 0.16;
// 導体(Conductors)のf0値
// 導体≒金属の場合はベースカラーがそのままf0値となる
float3 f0_C = baseColor;
// あとは、metallic値によって線形補間する
return lerp(f0_D, f0_C, metallic);
}
BRDFを使ったライティング実装
これまでの実装をまとめて、ライティングを実装したものが以下になります。
/*
// 以下はマテリアルの基本情報である。設定は適当に
float3 N; // ジオメトリ法線
float3 L; // ライト方向ベクトル
float3 V; // 視線方向ベクトル
float3 H = normalize(V + L); // ハーフベクトル
float3 baseColor; // ベースカラー
float roughness; // ラフネス
float metallic; // メタリック
float reflectance; // 反射率
*/
// ディフューズ反射成分
float3 Fd = BRDF_Diffuse_Lambert(baseColor, metallic);
// スペキュラ反射成分
float3 f0 = Fresnel0(baseColor, metallic, reflectance);
float3 Fr = BRDF_Specular_GGX(
N,
L,
V,
baseColor,
roughness,
f0
);
// BRDF
float3 BRDF = (Fd + Fr);
以上でBRDFの実装は完了です。
最終的に出力するカラー
BRDFの計算はすでに完了していますが、ここまできたら最終的な出力カラーまで計算してしまいましょう。
以下のコードは直前のコードの続きです。
// ライト。値は適当
float3 lit = float3(1.0, 1.0, 1.0);
// 出力カラー
float3 outputColor = saturate(dot(N, L)) * BRDF * lit;
簡単化のためライトを定数として設定しています。これでもBRDFの確認はできますが、絵的に面白い画面を作ろうと思ったらライトの値も複雑になっていきます。
補足
以下は補足的な内容となっています。
各関数の導出など、調べたことを書いてあります。
Kajiyaのレンダリング方程式
Kajiyaのレンダリング方程式は、Hanes T.Kajiya氏が考案した積分方程式で、物理ベースのレンダリングの計算・実装を行う際によく利用されます。
Real Time Rendering(第4版) 11.1式・11.2式より(ちょっとわかりやすく変えてあります)
出射光 | |
発光 | |
BRDF | |
入射光 | |
サーフェイス上の位置ベクトル | |
視線ベクトル | |
ライトベクトル | |
半球立体角(積分範囲) |
BRDFを考える上では発光を表す
正規化ランバートの導出
ランバートモデルは以下のように表されます。
レンダリング方程式を使って、エネルギー保存則を満たすよう変形したものが正規化ランバートモデルとなります。
以下、その導出です。
前提として、今回考える拡散反射は入射した光が半球上に一様に反射する完全拡散面での反射とします。
すると、反射光の比率は入力値に関わらず一定となるため、定数で表すことができます。
入力値は関係なくなるので、省略します。
これをレンダリング方程式に当てはめます。(
入射光(
積分範囲は立体角
参考:https://youtu.be/aNoEzONgIYo?t=454
こちらを使って積分範囲を修正します。半球であるため、
先程の式にあてはめると
よって、先程の式は次のようになります。
二倍角の公式から
よって
以上をまとめると以下になります。
これは、反射率が方向によらず一定(
入射光
これを数式で表すと以下のようになります。
以上から、エネルギー保存則を満たす完全拡散面の反射率は最大でも
材質固有の反射率を
以上が正規化ランバートモデルの導出になります。
SmithモデルとGGXモデル
(ちょっと厳密でない表現になりますが)幾何関数Smithモデルは法線分布関数のモデルから算出されます。つまり、法線分布関数にGGXモデルを採用した場合、SmithモデルもGGXに合わせたモデルにする必要があります。実装する際は参考資料をもとに行う場合が多いと思われますが、幾何関数のモデルと法線分布関数のモデルがちゃんと対応しているか確認する必要があります。
このことについて、RealTimeRendering(第4版)を参考に少しだけ詳しく書いてみます。
まず、Smithモデルの
step(0, x)
と同じようなものといったらわかりやすいかもしれません。(x==0
のときだけ値が違うのに注意してください)
Discussion