This is the documentation for Enlighten.

7.5. ローカル IBL 反射


高解像度の空の反射

Enlighten では、キューブマップのエミッシブ環境解像度を指定すると、キューブマップに低解像度の空のライティングが出力されます。また、キューブマップのアルファ値を使用して、反射のために高解像度で空をレンダリングすることもできます。

Enlighten キューブマップでアルファ値を保持するには、Enlighten キューブマップ データを更新マネージャーに追加する際に BaseCubeMap::SetEmissiveEnvironment() を呼び出さないでください。この変更により、IUpdateManager::EnqueueUpdateEmissiveEnvironment() を呼び出しても、Enlighten ではキューブマップに空のライティングが出力されません。

視差とローカル反射

キューブマップは、単純にキューブマップの中心から見たワールドのビューを記録します。それ自体では、サーフェスがどの程度離れているかについては何も判断できません。この情報は、正確にワールドに反射を配置する際に重要です。追加の深度情報を指定せず、単純に反射ベクトルを持つカラーを参照した場合、キューブマップのビューを無限遠とみなすことになります。ワールド空間のオブジェクトを移動した際、反射に映り込んでいるオブジェクトが近い場合、同じ角度からオブジェクトに投影される反射は変化するはずです。これを可能にするには、なんらかの追加の深度情報を指定する必要があります。

レイ トレーシングによりスペキュラー反射を計算する場合、反射ベクトルに沿ってワールド空間のサンプルの位置からの光線を追跡し、その光線に沿った最初のサーフェスを見つけます。これは近似のプロセスです。簡単で効果的なアプローチの 1 つは、ボックスまたは類似のプリミティブを使用してキューブマップで見えるすべてのサーフェスの深度を近似させることです。プリミティブはシーンの実際のジオメトリに適切に対応しても、光線と交差させるコストを十分に低くする必要があります。シェーダーで、反射光線とボックスとの間の光線の交差を実行し、光線がぶつかる点を見つけます。次に、サンプルの位置からぶつかる点のベクトルをキューブマップのルックアップ ベクトルとして使用します。これによってボックスのサーフェスのすべての反射を配置し、ボックスが実際のシーンのジオメトリと一致すれば、正確な反射が得られます。

この手法は、過去にさまざまな名前で呼ばれてきましたが、最もよく知られている名前としては「ボックス プロジェクション」があります。重要な参考文献は以下のとおりです。

以下の例で、深度を含めることによる違いがわかります。

深度なし

ボックス プロジェクションを使用した深度あり

ボックス プロジェクション

ボックス プロジェクションの実装の例は、GeoRender ライブラリの Resources フォルダにある SpecularCubeMap.cg で確認できます。同じライブラリの Deferred.cgPS_Radiosity_Body() 関数に使用されています。この実装では、方向づけられたボックスをサポートしており、キューブマップの中心がボックスの中心とは異なっていても構いません。この柔軟性により、手順は少し増えますが、シーンの深度の近似が向上します。

プロジェクションそのものは、 CubeMapSpecular 関数により実装されています。

// ボックス プロジェクションを使ってキューブマップ空間のすべてのキューブマップの値を計算します。
float4 CubeMapSpecular(samplerCUBE cubeMap, float3 sampleCentre, float3 boxHalfExtents, float3 cubeMapCentre, float3 reflectionVec, float roughness, int numMips)
{
    // 簡単に言うと、6 つの光線と平面の交差を実施します。
    // 一部は (光線と平面が平行になり) 正しく交差せずに inf が生成されますが、これらは比較で除外されます。
    const float3 sampleCentreToBoxMax   = (+boxHalfExtents) - sampleCentre;
    const float3 sampleCentreToBoxMin   = (-boxHalfExtents) - sampleCentre;
    const float3 rRecip                 = (1.0 / reflectionVec);
    const float3 rayBoxMax              = sampleCentreToBoxMax * rRecip;
    const float3 rayBoxMin              = sampleCentreToBoxMin * rRecip;
    // ここで、平面の 3 つにバックフェース カリングを行います
    const float3 rayBoxFront            = (reflectionVec > 0.0) ? rayBoxMax : rayBoxMin;
    // そして最も近い交差点を選びます
    const float rayBoxClosest           = min(min(rayBoxFront.x, rayBoxFront.y), rayBoxFront.z);
    // キューブマップ空間の交差点を再構築し、キューブマップ原点を再度中心とします
    const float3 collisionPoint         = sampleCentre + reflectionVec * float3(rayBoxClosest);
    const float3 vecFromBoxCentre       = collisionPoint - cubeMapCentre;
 
    // 場合によっては HDR 形式をデコードした後、キューブマップ ルックアップを返します
    const float mip = ComputeMipFromRoughness( roughness, numMips );
    return DecodeIrradianceValue( texCUBElod( cubeMap, float4(reflectionVec, mip) ) );
}

この関数はワールド空間ではなくキューブマップ空間の座標を受け入れます。この空間では、ボックスの中心が原点であり、座標軸がボックスの軸と整合します。これにより、ボックスを単一の半分の大きさのベクトル (ボックスの中心からキューブマップ空間の最大の角までのベクトル) で表すことができます。サンプルの位置と反射ベクトルは、事前にキューブマップ空間に変換する必要があります。実装は、ボックスの 6 つの光線と平面の交差を指定して簡素化することにより導出されます。平面と平行な光線は、逆数計算時に inf を生成しますが、バックフェース カリングと最も近い交差のテストの際に取り除く必要があります。しかし、一部のデバイスでは inf に対する動作が標準的でないことがあります。これが問題になる場合、0 で除算しないように逆数を調整できます。

ブレンディング

実務では、複数のキューブマップからの寄与を組み合わせる必要があります。これは単純なフォールオフや、さらに高度なブレンディングで実施できます。実装例では、両面的な重みづけを使用しています。N の最も近いキューブマップをシェーダーにバインドし、それぞれを評価して、それぞれに重みを割り当てます。各キューブマップの寄与は重みによってスケーリングされ、最終的な合計は重みの合計で再び正規化されます。重みを計算するには、以下のようにサンプル ポイントから最も近いボックスの点の距離の 2 乗を計算します。

float CalcDistanceSqToClosestPointInHalfExtents(float3 p, float3 halfExtents)
{
    // 半分の大きさで表される p から AABB の最も近い点の距離の 2 乗を返します
    const float3 pInBox = min(halfExtents, max(-halfExtents, p));
    const float3 d = pInBox - p;
    return max(0, dot(d, d));
}
 
...
// 重みを計算します
float epsilon = 0.000001;
const float cubeWeight = 1.0 / (epsilon + CalcDistanceSqToClosestPointInHalfExtents(cubeSpaceSamplePos, g_CubeMapHalfExtents));

計算を単純化するため、ボックスが軸と整合したキューブマップ空間で計算を行います。この重みづけのアプローチにより、サンプル ポイントがキューブマップの中にある場合、結果はそのキューブマップから最も影響を受けますが、それでもキューブマップ間で円滑にブレンドします。その他の詳細情報については、SpecularCubeMap.cg. の実装をご覧ください。

実装ではドロー コールごとにキューブマップをシェーダーにバインドしますが、プリミティブへの依存はなく、ブレンディングはフルスクリーン コンピュート シェーダー パスでも実行できます。

PBR 反射

キューブマップは、物理ベースのシェーディング モデルの一部として、スペキュラー イメージベースド ライティング (IBL) を提供するために使われることがよくあります。通常、キューブマップの最も詳細なミップは、シェーディング モデルで使用される法線分配関数 (NDF) に基づきプリフィルタリングされます。増加するさまざまなマテリアルのラフネス値が使用され、結果はキューブマップのミップチェーンに保存されます。次に、シェーダーにスペキュラー IBL を適用する際、シェーディングされるポイントのマテリアルのラフネス値が、プレフィルタリングされた適切なキューブマップのミップのサンプリングに使用されます。

GGX がキューブマップをプレフィルタリングする際に最もよく使われる法線分配関数 (NDF) ですが、非常にコストが高く、オフラインで実施されるか、実行時に低頻度で実施されます。Enlighten キューブマップ ソルバーは、非常に速い単純なダウンサンプリング動作を使用して、完全なミップマップチェーンを生成することもできます。ただし、以下の関数を使用してマテリアルのラフネス値とキューブマップのミップの数を基に適切なミップを選択すると、GGX でフィルタリングされたキューブマップを要求するシェーディング モデルで、許容範囲の結果を実現できることが判明しました。

float ComputeMipFromRoughness(float roughness, int numMips)
{
    return numMips + 2.0f * log2( roughness );
}

また、Enlighten キューブマップ ソルバーにより生成され、ダウンサンプリングされたキューブマップのミップチェーンを、GPU で実行されるフィルタリングされた重点サンプリング GGX フィルター動作への入力として使用することもできます。