Cube Map Reflectionを実装した

Cube Map Reflectionを実装した。

前回、スカイボックスを実装したが、そのスカイボックスのリソースであるキューブテクスチャを環境マップとして用いて、リフレクションを実装することにする。

といっても、一応、拡張性を持たせるために、シーン内に複数の環境マップが存在し、最も近く、小さく精度が高いと期待できる環境マップをメッシュごとに探索し、レンダリング時のリフレクションリソースとして使用する、という仕組みを作った。

その際に、オブジェクトにUUIDを付与する必要性が出てきたので、Boost Libraryにお世話になりオブジェクトのIDを一意に定められるようにした。

#include <boost/lexical_cast.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/random_generator.hpp>

//~~~

std::size_t uuid = std::hash<std::string>()(boost::lexical_cast<std::string>(boost::uuids::random_generator()()));

UUID -> std::string -> std::size_t

の変換を噛ませている。UUIDのフォーマットはXXXXX-XXXXXXX-XXXXXのような形式(桁数は適当)だが、これをそのまま使うにはC++はあまりにも文字列に対して無力なので、ハッシュ化することにする。SHAアルゴリズムを使っているはずなので、 (少なくともVC++はFNV-1アルゴリズムだった。)UUIDの一意性は失われ危険がある気はするが、まあ別にプロダクトを作っているわけではないのでこの辺はざっくり適当でいいだろう。問題が起こったらどうにかする。


さて、肝心のリフレクションだが、すでに実装しているスペキュラ反射の一部を流用しつつ実装する。

{ \displaystyle
F_r ≈ F_ 0 + (1 - F_0)(1 - cosθ)^5
}


これはSchlickの近似を使ったフレネルの公式である。
ここで {F_0} は垂直入射時の反射係数(物理的な正確さを求めるなら、RGBそれぞれの反射能の定数ベクトル)である。マテリアルのパラメータとして与える。cosθは正規化された法線・視線ベクトルの内積で求まる。

さて、ここでリフレクションが起きている頂点の色というのは、屈折方向の色と反射方向の色の合成で求まる。先ほど求めた反射係数と使うと

TextureCube EnviromentMap : register(t2);
SamplerState EnviromentSampler : register(s2);

float FrenselEquations(float reflectionCoef, float3 H, float3 V) {
    return (reflectionCoef + (1.0f - reflectionCoef) * pow(1.0 - saturate(dot(V, H)), 5.0));
}

float3 ReflectionFrensel(float4 posw, float4 norw, float4 eye, float eta)
{
    float3 N = norw;
    float3 I = normalize(posw.xyz - eye);
    float3 R = reflect(I, N);
    float3 T = refract(I, N, eta);
    float fresnel = FrenselEquations(pow(eta - 1 / eta + 1, 2), N, I);

    float3 reflecColor = EnviromentMap.Sample(EnviromentSampler, R);
    float3 refracColor = EnviromentMap.Sample(EnviromentSampler, T);

    float3 col = lerp(refracColor, reflecColor, fresnel);

    return col;
}

このように求まる。ここで、etaというのは物質の屈折率だ。大気の屈折率は1として反射係数を計算している。

さて、これで求まる反射の色だが、これをどのようにPBRに当てはめるか?
一応作っているのは、物理ベースであるので、レンダリング方程式に乗っ取って適用したい。

結論から言うと、現時点での実装では物理的な正しさをこのリフレクションにもたせるのは不可能だ。なぜなら、基本的に現実の物質は多少なりとも光を拡散するからである。

まず、環境マップによるリフレクションとは何か?という定義から考えなくてはならない。
反射が発生するとしている以上は、環境マップは光を放っているということにして、その光が物体に入射しているのだという前提で環境マップリフレクションは実装されている。つまり、環境マップは光源なのである。しかも、テクセル一つ一つが、光源として色をもって光を放っている。その数は横*縦*高さ。

このように、テクスチャを光源と定義してライティングをすることをImage Based Lightingという。

ここで一度原点に戻ってレンダリング方程式を掲載してみよう。

{ \displaystyle
L_{o}(x,\vec{\omega}) = L_{e}(x,\vec{\omega}) + \int_{\Omega} f_{r}(x,\vec{\omega}',\vec{\omega}) L_{i}(x,\vec{\omega}') (\vec{\omega}' \cdot \vec{n}) d\vec{\omega}
}

ここで、右辺第一項は自身が光源として放つ光のことなので今回は無視してよい。
右辺第二項は、(否透過材質であると仮定すれば)入射してくる光と、法線ベクトルと入射ベクトルの内積、そして反射を表すBSDF関数の積について、半球積分したものだ。そう、半球上のあらゆる入射方向についての総和だ。

この理論でいくならば、全テクセルを光源と定義した環境マップにおいては、その半球上に存在する全テクセルについて、ライティングの計算をしなくてはならない。喜ばしいことに、直接光だけの計算で考えるならば環境マップは離散データなので、モンテカルロ法とかそういう手法で頭を悩ませずとも数値積分することができる。

うむ。解決。

とはならない。

非現実的だからだ。

なぜなら、ライティング処理はピクセルシェーダーでピクセルごとに処理するのが最も一般的で精度が高くなるので、全ピクセルで行われる。つまり、メッシュ上の全てのピクセルについて、かつ環境マップの解像度に比例した膨大な量の光源についてライティングをしなくてはならない。そんなもの、今日のGPUでもFPS60/30を保って処理するのは不可能である。

そのため、今回は、屈折ベクトル及び反射ベクトルの先にあるテクセルの2つを光源として、しかもそのまま雑にピクセルカラーに加算するという、PBRもへったくれもない近似手法でごまかすことにしよう。

この近似は、完全鏡面でかつ、全ての光源が無限遠に存在しかつ減衰しないという仮定に即したものとなる。

というわけで、既存のディレクショナル・ポイントライトの拡散・鏡面反射の計算の後に大胆に以下のコードを挿入することにする。

specular += ReflectionFrensel(IN.posw, IN.norw, Eye, 0.2f);


実行すると以下のようになる。


f:id:riyaaaaasan:20180311013600p:plain



球のモデルが実はサッカーボールなせいで地味に凹凸がうまれている。

さて、次回の予定は未定だ。PRTか、ランタイムシェーダーコンパイルか....うーむ。何を実装しよう。