ボリューム レンダリングの簡潔なチュートリアル [NeRF の基本]

ボリューム レンダリングは、ハード サーフェス レンダリングとほぼ同じくらい大きくて複雑なトピックです。これには、光が硬質物質とどのように相互作用するかを説明するために使用される方程式を実際に一般化した独自の一連の方程式があります。このような複雑な数式に必ずしも慣れていない読者にとっては、圧倒されてしまうかもしれません。

ここに画像の説明を挿入

推奨事項: NSDT Designerを使用して、プログラム可能な 3D シーンを迅速に構築します。

Scratchapixel で教える方法と同様に、ボリューム レンダリングを教えるという課題に対して「ボトムアップ」アプローチを選択しました。言い換えれば、それは実践的なアプローチです。方程式から始めて詳しく調べるのではなく、単純な体積球をレンダリングするコードを作成し、できれば直感的な方法でプロセス全体を説明します。その後、コースの最後にこれまでに学んだすべてをまとめて正式にまとめます。

いくつかのレッスンでは、ボリューム レンダリング (これは大きなトピックです) について取り上げます。この入門コースでは、ボリューム レンダリングとレイ マーチングの基本を学びます。後続のコースでは、ボリュームをレンダリングする他の可能な方法、参加メディアに適用されるグローバル イルミネーション、多重散乱、ボリューム データを保存する形式 (OpenVDB など) について取り上げます。

1. ボリューム レンダリングの概要

このコースの最初の 2 章の目標は、均一な色の背景上に単一の光源で照らされた球形のボリュームをレンダリングする方法を学習することです。これは、最初にボリュームが何であるかを直観し、それらをレンダリングするために使用するレイ マーチング アルゴリズムを導入するのに役立ちます。

この章では、均一な密度で通常のボリュームをレンダリングするだけです。ボリュームの外側または内側からのオブジェクトによって投影されるシャドウ、およびボリュームがさまざまな密度でどのようにレンダリングされるかは無視します。これらについては次の章で学習します。

ボリュームとは何か、およびボリュームをレンダリングするために使用される方程式について多くの詳細な背景を提供する代わりに、実装に直接入り込み、そこからボリューム レンダリングについてより正式な理解を得ることができます。
ここに画像の説明を挿入

2. 内部透過率、吸収率、粒子密度とベールの法則

物体で反射したり、光源から発せられたりして私たちの目に届く光は、粒子で満たされた空間を通過するときに吸収される可能性があります。ボリューム内の粒子が多いほど、ボリュームの不透明度は高くなります。この単純な観察から、ボリューム レンダリングに関連するいくつかの基本概念、つまり吸収、透過、ボリュームの不透明度とボリュームに含まれる粒子の密度の関係を導き出すことができます。ここでは、ボリュームに含まれる粒子の密度が均一であると考えます。
ここに画像の説明を挿入

光がボリュームの中を目の方向に進むとき (これが、私たちが見る物体の像が目に形成される仕組みです)、この光の一部はボリュームを通過する際にボリュームに吸収されます。この現象を吸収といいます。(現時点では) 背景からボリュームを透過する光の量に興味があります。内部透過率 (ボリュームを通過するときに吸収される光の量) について説明します。内部透過率は、0 (体積がすべての光を遮断する) から 1 (真空であるため、すべての光が透過される) までの値として見ることができます。

このボリュームを透過する光の量は、ランベルト ベールの法則 (略してビールの法則) によって決まります。ランベルト ベールの法則では、密度の概念は吸収係数 (および散乱係数ですが、散乱係数についてはこの章で後ほど説明します) で表されます。これは、「体積の密度が高くなるほど、吸収係数が高くなる」と解釈できます。直感的に推測できるように、吸収係数が増加すると、体積はより不透明になります。ランベルト・ベールの法則の方程式は次のようになります。
ここに画像の説明を挿入

この法則は、内部透過率、体積を通過する光 (T) と体積吸収係数 (sigma_a) の積、および光が材料を通過する距離 (つまり、経路長) の間に指数関数的な依存性があることを示しています。

これらの係数の単位は、cm^-1 や m^-1 などの距離の逆数または長さの逆数です。これは、これらの係数に含まれる情報を直感的に理解するのに役立つため重要です。任意の点/距離でランダムなイベント (光子が吸収または散乱するなど) を発生させたい場合は、吸収係数 (および後ほど説明する散乱係数) を確率または尤度として考えることができます。

吸収係数と散乱係数は確率密度を表すと言われています (このトピックについてさらに詳しく調べたい場合は)。ただし、1を超えてはいけない確率なので、測定単位によって異なります。たとえば、ミリメートルを使用すると、特定の媒体に対して 0.2 が得られる可能性があります。ただし、センチメートルとメートルでは、それぞれ 2 と 20 です。したがって、実際には、1 より大きい値を使用することを妨げるものはありません。

係数と平均自由行程の関係

吸収係数と散乱係数の単位が長さの逆数であるという事実は、係数の逆数 (1 を散乱係数の吸収で割った値) を取ると距離が得られるため、重要です。この距離は平均自由行程と呼ばれ、ランダム イベントが発生する平均距離を表します。
ここに画像の説明を挿入

この値は、参加するメディアからの多重散乱をモデル化する際に重要な役割を果たします。これらの本当に素晴らしいトピックについて詳しく学ぶには、サブサーフェス スキャッタリングと高度なボリューム レンダリングに関するコースをチェックしてください。

ここに画像の説明を挿入

図 1: 距離が長くなるほど、または密度が高くなるほど、内部透過率の値は低くなります。

吸収係数または距離が大きいほど、T は小さくなります。ランバート ビール方程式は、0 ~ 1 の範囲の数値を返します。距離または吸収係数が 0 の場合、方程式は 1 を返します。距離または密度が非常に大きい場合、T は 0 に近づきます。一定の距離では、吸収係数が増加するにつれて T は減少します。吸収係数が固定されている場合、T は距離とともに減少します。光は体積内を遠くまで進むほど、より多く吸収されます。ボリューム内の粒子が多いほど、より多くの光が吸収されます。単純。この効果は図 1 で確認できます。

3. 均一な背景でボリュームをレンダリングする

ここから始めるのは簡単です。厚さと密度が既知の体積スラブがあると想像してください。それぞれ 10 と 0.1 と言います。したがって、背景色 (たとえば、見ている壁で反射した光) が (xr, xg, xb) の場合、ボリュームを通して見える背景色の量は次のようになります。

vec3 background_color {xr, xg, xb};
float sigma_a = 0.1; // absorption coefficient
float distance = 10;
float T = exp(-distance * sigma_a);
vec3 background_color_through_volume = T * background_color;

これ以上にシンプルなことはありません。

4. 散乱

これまでボリュームが黒であると仮定してきたことに注意してください。言い換えれば、ボードのどこにいても背景色を暗くするだけです。しかし、ボリュームはこのようにする必要はありません。固体のようなボリュームも光を反射します (より正確には散乱します)。晴れた日に雲を見ると、まるで立体的な雲の形が見えるのはこのためです。ブロックは光を発することもできます (ろうそくの炎を考えてください)。完全を期すためにこれについて言及しますが、この章では光りは無視します。

したがって、ボリューム プレートには特定の色 (yr、yg、yb) があると想定します。この色のソースについては、この章の後半で説明するので、今は無視しましょう。それまでは、ボリューム オブジェクトが光を「反射」し (実際には反射しませんが、固体オブジェクトと同様に「反射」の概念を使用します)、固体オブジェクトのように照明するため、ボリュームが特定の色を持つとしか言えません。コードは次のようになります。

vec3 background_color {xr, xg, xb}; 
float sigma_a= 0.1; 
float distance = 10; 
float T = exp(-distance * sigma_a); 
vec3 volume_color {yr, yg, yb}; 
vec3 background_color_through_volume = T * background_color + (1 - T) * volume_color;

これは、たとえばアルファ ブレンディングを使用して、Photoshop で (A+B) 画像をブレンドするプロセスと考えてください。画像 B を A の上に追加するとします。ここで、A は背景画像 (青い壁)、B は透明チャネルを持つ赤いディスクの画像です。これら 2 つの画像を結合する式は次のとおりです。

ここに画像の説明を挿入

ここでの透明度は 1 - 透過率 (別名不透明度)、B はボリューム オブジェクトの色 (ボリュームによって「反射」され、目/カメラに向かって進む光) です。これについては、レイ マーチング アルゴリズムについて説明するときに戻りますが、今のところは覚えておいてください。

5. 最初の体積ボールをレンダリングします。

最初の 3D 画像をレンダリングするために必要なものはすべて揃っています。これまでに学んだことを使用して、いくつかのパーティクルで満たされていると想定される球をレンダリングします。何らかの背景上に球をレンダリングしていると仮定しましょう。

原理は簡単です。まず、カメラ光線と球体の交差を確認します。交差がない場合は、単純に背景色を返します。交差がある場合は、光線が球に出入りする球の表面上の点を計算します。そこから、光が球を通過する距離を計算し、ビールの法則を適用して球を通過する光の量を計算します。ここで、球によって「反射」(散乱)された光は均一であると仮定します。照明については後ほどお話します。
ここに画像の説明を挿入

図 2: ボリュームを通過するカメラ光線。
ここに画像の説明を挿入

図 3: カメラ レイとボリューム オブジェクトの交点を使用して、カメラ レイに沿ったボリューム オブジェクトの不透明度を計算します。

class Sphere : public Object 
{ 
public: 
    bool intersect(vec3, vec3, float, float) const { /* compute ray-sphere intersection */ } 
    float sigma_a{ 0.1 }; 
    vec3 scatter{ 0.8, 0.1, 0.5 }; 
    vec3 center{ 0, 0, -4 }; 
    float radius{ 1 }; 
}; 
 
void traceScene(vec3 ray_origin, vec3d ray_direction, const Sphere *sphere) 
{ 
    float t0, t1; 
    vec3 background_color { 0.572, 0.772, 0.921 }; 
    if (sphere->intersect(rayOrigin, rayDirection, t0, t1)) { 
        vec3 p1 = ray_origin + ray_direction * t0; 
        vec3 p2 = ray_origin + ray_direction * t1; 
        float distance = (p2 - p1).length();  // though you could simply do t1 - t0 
        float tranmission = exp(-distance * sphere->sigma_a); 
        return background_color * transmission + sphere->scatter * (1 - transmission); 
    } 
    else 
       return background_color; 
} 
 
void renderImage() 
{ 
    Sphere *sphere = new Sphere; 
    for (each row in the image) 
        for (each column in the image) 
            vec3 ray_dir = computeRay(col, row); 
            pixel_color = traceScene(ray_orig, ray_dir, sphere); 
            image_buffer[...] = pixel_color;  // store pixel color in image buffer 
 
    saveImage(image_buffer); 
    ... 
} 

論理的には、濃度が増加するにつれて、透過率は徐々に 0 に近づきます。これは、ボリューム ボールの色が背景の色よりも支配的であることを意味します。
ここに画像の説明を挿入

上の画像では、球の中心に向かってボリュームが不透明になることがわかります (光が球を通って最も遠くまで進む場所です。また、密度が増加するにつれて (sigma_a が増加するにつれて)、球全体がより不透明になることがわかります。

6.光を加えてみよう!内部散乱

これまでのところ、素晴らしい体積球体イメージができましたが、照明はどうなるでしょうか? ボリューム オブジェクトにライトを当てると、ライトに直接さらされているボリュームの部分が、影になっている部分よりも明るいことがわかります。ボリュームもライトで照らされます。これをどう説明すればよいでしょうか?

原理は簡単です。光源から発せられた光がボリュームを通過する運命を想像してみましょう。体積を通過すると、吸収により強度が低下します。当然のことですが、ボリューム内で特定の距離を移動した後にどのくらいの光エネルギーが残るかは、ビールの法則によって決まります。言い換えれば、光がボリュームを通過する距離がわかっている場合、その距離での強度は次のようになります。

float light_intensity = 10; // just a number, it could be anything 
float T = exp(-distance_travelled_by_light * volume->absorption_coefficient); 
light_intensity_attenuation = T * ligth_intensity;

まず、ビールの法則によれば、光エネルギーは体積を通過するにつれて減少します。これは非常に論理的です。しかし、別のことが起こります。光源からの光は最初は目に当たりませんが、散乱効果と呼ばれるものにより、目にリダイレクトされることもあります(少なくとも目に見えるものの一部)。

私たちは散乱の特殊なケースについて話しています。非散乱とは、光がボリュームを通過し、散乱イベントにより目にリダイレクトされることです。この効果を図 4 に示します。散乱イベントは、光子と媒体/体積を構成する粒子/原子の間の相互作用の結果です。原子は吸収も反射もせず(これも起こり得る)、入射方向とは異なる方向に光子を単に「吐き出す」だけです。この現象については次の章で詳しく見ていきます。

ここに画像の説明を挿入

図 4: ボリュームを通して見える光は、光源だけでなく背景オブジェクト (ここでは青) からもたらされます。光源からの光線は目には届きませんが、散乱効果により、ボリュームを通過する際にある程度の量の光が目の方向に向けられます。

図 4 を見ると、目に到達する光 (図で青色で描かれた特定の目/カメラ光線に沿って) は、背景からの光 (青色の背景) と内部散乱によって目に向かって散乱する光源からの光 (黄色の光) の組み合わせであることに注意してください。

では、光源の寄与をどのように考慮すればよいのでしょうか? 不散乱の効果として目に向かって散乱する光 (およびカメラの光) を「測定」する必要があります。問題は、球と交差するカメラ光線の部分全体に沿ってこの効果を考慮する必要があることです (図 5)。t0 ~ t1 の範囲でカメラ光線に沿って散乱した光を「統合」する必要があります。

ここに画像の説明を挿入

図 5: ボリュームを通過する光の部分に沿った散乱により目にリダイレクトされた光を統合する必要があります。

これを解決するには、ボリュームを通過するカメラ光線の部分を特定の数のフラグメント (必要に応じて例) に分割し、次の手順を使用して各フラグメント (例) の中心に到達する光の量を計算します (概念についての直観については、図 6 を参照してください)。

  • このサンプル ポイント (X とします) から光源に向かって光線を発射し、サンプル ポイントから球の境界 (ポイント P とします) までの距離を計算します。X は常に球 (ボリューム) の内側にあり、P は常に球の表面上の点であることに注意してください。
  • 次に、ベールの法則を適用して、光エネルギーが P (光線が球に入る点) から X (光線が観察者に向かって散乱する目の光線に沿った点) に移動するときにどれだけ減衰するかを確認します。
    ここに画像の説明を挿入

図 6: リーマン和は、従来の手順で光線に続く積分を推定するために使用されます。
ここに画像の説明を挿入

図 7: リーマン和を使用して、カメラに沿って散乱する光の量を表す曲線の下の面積を推定できます。アイデアは、曲線の下の領域を小さな長方形の合計に分解することです。各長方形の高さは Li(x) によって与えられ、幅 dx はユーザーによって定義されます。

ここで扱っている問題の種類を理解するには、図 6 と 7 を参照する必要があります。図 6 は、カメラ光線に沿って到達する入射光を示しています。図の下部でわかるように、これは連続関数です。この関数を Li(x) と呼びます。ここで、x は t0 ~ t1 の範囲に含まれるカメラ光線に沿った任意の点です。計算する必要があるのは、曲線の下の「面積」です。数学では、これは積分であり、次のように書くことができます。

ここに画像の説明を挿入

先ほど述べたように、積分の結果 (数値) は、図 6 に示すように、曲線下の (正味符号付き) 面積 (関数 Li(x)) として定義されます。この場合の問題は、解析ソリューションを使用してこの領域を計算できないことです。ただし、長方形の面積がわかっている場合は、この面積をより単純な形状に分解することで、この面積を近似するトリックを使用できます (図 7 を参照)。曲線に沿って一定の間隔で Li(x) をサンプリングし、(dx) の幅がわかったら、結果として得られる長方形の面積を Li(x) と dx の積として計算できます (x は間隔の中央にあります)。すべての長方形の面積を加算することで、曲線の下の面積の近似値を取得できます。成り行きを見守る!この手法はリーマン加算として知られています (既知の領域を使用して未知の領域の形状を近似するという考えは、ギリシャ人にまで遡ります)。

では、それはどのようにコードに変換されるのでしょうか? 次回紹介します。


元のリンク:ボリューム レンダリングの簡潔なチュートリアル — BimAnt

おすすめ

転載: blog.csdn.net/shebao3333/article/details/131888400