2023年12月19日火曜日

BlenderでAmazon Lumberyard Bistro を読み込む

Blender で Amazon Lumberyard Bistro を読み込む

この記事は レイトレ(Raytracing) Advent Calendar 2023 の18日目の記事です。

はじめに

グラフィックス系の論文では実用的なレンダリングシーンの例として Amazon Lumberyard Bistro [1] がしばしば実験に用いられます。 自作のレンダラーでこのシーンを試す方法として Blender [2] でシーンを読み込み glTF などの形式に変換して使用するという方法がありますが、ただ Blender に読み込むだけでは期待した動作にならなかったため、シーンを修正するために行ったことをノートとして残します。但し、シーンを修正するにあたりオブジェクトを手作業で一つ一つ編集するといった時間のかかる作業は行わないようにしています。また、この記事では Blender 4.0.2 で作業を行っています。

Blender でのスクリプト実行について

Blender は Python Interpreter を内蔵しており Python スクリプトを実行してシーンを編集することができます (Blender 4.0.2 時での Python のバージョンは 3.10.13 です) 。 Python スクリプトを実行するにあたって、まずは UI 選択で Text Editor を選択しエディターを表示します。

エディター上で Python スクリプトを記述し実行ボタンを押すことでスクリプトを実行できます。

実行時に標準出力にテキストを出力したり、実行エラーメッセージを確認したい場合は Linux なら標準の端末 (端末からBlenderを実行している場合) 、 Windows なら Blender のメニューの "Windows > Toggle System Console" を選択してコンソールを表示することで確認することがきでます。

スクリプト実行でシーン編集した内容は Blender の Undo 操作で戻すことができるので、 試行錯誤しながら Python スクリプトを記述することもできます。 また、Blender のメニューの "Edit > Preference > Interface" にある Python Tooltips を有効にすることで GUI 上の各種メニューやボタンに相当する Python コマンドを確認できるようになるので、スクリプトを記述する上でとても便利です。

Blender での Python スクリプトについてのより詳しい情報は Python API Documentation を参照して下さい。

シーンの読み込み

読み込み前の準備

本記事ではオブジェクトがまったく無い状態からシーンの読み込みを行います (Blenderで新規シーンを作成してデフォルトで作成されたオブジェクトを全部削除した状態) 。IBL (Background) は無効にしています。また、レンダリングには Cycles レンダラーを用いてデノイザーをオフにして行っています (デバイスバックエンドは HIP を使用しています Cycles GPU Rendering) 。

Amazon Lumberyard Bistro (v5.2)

配布ページに載っている Bistro_Night.png のような画像をレンダリングできることを目標にします。 まずはダウンロードした BistroExterior.fbx を Blender にインポートします。但し、本記事ではアニメーションは考慮しないので fbx をインポート時にアニメーションは無効にします。

シーンをインポートしたら Bistro_Night.png と同じカメラアングルになるようにカメラを下の画像のように設定します。

また、指向性光源 (Sun Light) は使わないので directionalLight1 オブジェクトをシーンから削除します。 この状態でレンダリングを開始すると、

上画像のように光源以外ほとんど真っ暗な画像になります。 光源の Strength が足りないため以下のスクリプト (Emission.py) を実行して全ての光源マテリアルの Strength を上げます、


import bpy # Blenderのデータ構造にアクセスできるようにする

def strengthenEmission(material):
    # マテリアルのノードツリーが無かったり'Principled BSDF'の無いマテリアルは無視する
    if (not material.node_tree) or (material.node_tree.nodes.find("Principled BSDF") == -1):
        return

    # ノードツリーの中から'Principled BSDF'を取り出す
    bsdf = material.node_tree.nodes["Principled BSDF"]

    # Emissionカラーに黒以外が指定されていたりノードが接続されている場合はEmission Strengthを上げる
    emission = bsdf.inputs[26]
    if (len(emission.links) > 0) or ((emission.default_value[0] + emission.default_value[1] + emission.default_value[2]) > 0):
        emission_strength = bsdf.inputs[27]
        emission_strength.default_value *= 512

if __name__ == "__main__":
    # 全てのマテリアルをチェックする
    for material in bpy.data.materials:
        strengthenEmission(material)

このスクリプトでは光源の Strength を 512 倍しています。実行後、マテリアルは以下の画像のように変化しています、

レンダリング結果は、

のようになり、明るさは Bistro_Night.png と比較してそれっぽくなりました。 しかし、床や手前のバイクを見てみるとスペキュラーな反射の具合がおかしく見えます。 Blender上でバイクのマテリアルを見てみると、

スペキュラーテクスチャーが Principled BSDF の Specular のパラメータに接続されています。 一方で、ダウンロードした Bistro ファイル内の README.txt を読むと、

Textures (compressed .DDS) designed for GGX-based metal-rough PBR material system with the following convention:   
- BaseColor  
    RGB channels: BaseColor value  
    Alpha channel: Opacity  
- Specular  
    Red channel: Occlusion  
    Green channel: Roughness  
    Blue channel: Metalness  
- Normal (DirectX)  
- Emissive  
    RGB channels: Emissive color

と記述されており、スペキュラーテクスチャーの緑成分は Roughness 、青成分は Metalness として使われる事が想定されています。 そこで、全てのスペキュラーテクスチャーの接続を修正するために以下のスクリプト (MetallicRoughness.py) を実行します、


import bpy # Blenderのデータ構造にアクセスできるようにする

def linkMetallicRoughnessTexture(material):
    # マテリアルのノードツリーが無かったり'Principled BSDF'の無いマテリアルは無視する
    if (not material.node_tree) or (material.node_tree.nodes.find("Principled BSDF") == -1):
        return

    # ノードツリーの中から'Principled BSDF'を取り出す
    bsdf = material.node_tree.nodes["Principled BSDF"]

    # Specularパラメータにノードが接続されている場合、MetallicとRoughnessパラメータに繋ぎかえる
    specular = bsdf.inputs[12]
    if len(specular.links) > 0:
        specular_texture = specular.links[0].from_node
        metallic = bsdf.inputs[1]
        roughness = bsdf.inputs[2]
        # テクスチャーの緑成分と青成分を取り出すために'SeparateColor'ノードを作成する
        rgb = material.node_tree.nodes.new("ShaderNodeSeparateColor")
        # スペキュラーテクスチャーノードのリンクを一度解除する
        material.node_tree.links.remove(specular.links[0])
        # スペキュラーテクスチャーノードを'SeparateColor'ノードに接続し、緑成分をRoughnessに青成分をMetallicパラメータに接続する
        material.node_tree.links.new(specular_texture.outputs[0], rgb.inputs[0])
        material.node_tree.links.new(rgb.outputs[1], roughness)
        material.node_tree.links.new(rgb.outputs[2], metallic)
        # スペキュラーパラメータはデフォルト値に戻す
        specular.default_value = 0.5

if __name__ == "__main__":
    # 全てのマテリアルをチェックする
    for material in bpy.data.materials:
        linkMetallicRoughnessTexture(material)

実行後は以下のようにスペキュラーテクスチャーが Metallic と Roughness に繋がっている事が確認できます、

この状態でのレンダリング結果は、

となり Bistro_Night.png と似た画像をレンダリングすることができました。

終わりに

本記事では Amazon Lumberyard Bistro を Blender で読み込み Bistro_Night.png と似たレンダリングができるように Python スクリプトを用いてシーンの修正を行いました。 一度 Blender 上で期待したレンダリングが出来るか確認する作業は、その後で glTF 形式などで自作のレンダラーに読み込ませレンダリングする際の基準としても使えるので確認しておくのは良いと思います。

参考

  1. Amazon Lumberyard, Amazon Lumberyard Bistro, Open Research Content Archive (ORCA), url=http://developer.nvidia.com/orca/amazon-lumberyard-bistro
  2. Blender Online Community, Blender - a 3D modelling and rendering package, url=http://www.blender.org

2021年12月25日土曜日

Godot4 に実装されるグローバルイルミネーションについて調べてみる

Godot4 に実装されるグローバルイルミネーションについて調べてみる

このページは レイトレーシング(レイトレ) Advent Calendar 2021 の14日目の記事です。

はじめに

Godot は2D及び3Dゲームを製作できるクロスプラットフォームなゲームエンジンです。 Godot はオープンソースであり MIT ライセンスの下で開発・配布されており無料で利用することができます。 現在は Godot3 が最新のメジャーバージョンですが、既に次期メジャーバージョンである Godot4 の計画は発表されており開発も進んでいます。 Godot4 ではレンダリングバックエンドとして新しく Vulkan サポートが追加されるなど様々なアップデートが予定されており (マイルストーン)、 その中の一つにリアルタイムグローバルイルミネーション (以降 GI と表記) の追加があります。 本記事ではそのリアルタイムGIの実装である Signed Distance Field Global Illumination (以降は SDFGI と表記) について簡単に紹介します。
最新の Godot4 の実装は github の master ブランチで見ることができます。 また、この記事は commit e53e357 の時点での Godot4 を使用して書いています。

Signed Distance Field について

Signed Distance Field (以降 SDF と表記) はスカラー場で、ここでは空間上のある点からシーン内の最も近いジオメトリの表面までの距離を表します。また、 SDF が正の値の時は自分がジオメトリの外側にいる、負の値の時はジオメトリの内側に位置していることを意味します。
レイキャスト時はこの SDF を用いてスフィアトレーシングを行います。下の図はレイがカメラ (点 p0p_0) から出て表面 (点 p4p_4) にヒットするまでを表しています。始めに、レイは開始地点である点 p0p_0 の SDF の値を見ます。 SDF が示している距離の間には何も無いことが判明しているのでレイは点 p1p_1 まで進む事ができます。これを繰り返す事で表面にヒットする点 p4p_4 (点から表面までの距離が閾値以下) までレイを進めます。

SDFGI

SDFGI は名前の通り SDF を利用した GI 実装です。Godot4 の SDFGI 実装はまだ完成ではないそうですが機能としては既に利用できます。また、Godot公式サイトの記事でデモを見ることができます。以下は SDFGI を無効/有効にした時のレンダリング画像です。
SDFGI無効 SDFGI有効
このシーンでは 道路や建物がディレクショナルライトで照らされていますが SDFGI を有効にした場合は影になっている部分も間接光によって明るくなっているのがわかります。
以下の画像はもう少し単純なシーンで SDFGI を試した例です。このシーンでは箱の中を天井にあるエリアライトで照らしています。真ん中に立っている小さな箱の側面を見ると GI 効果 (Color Bleeding) が出ているのがわかります (ただし現在エリアライトからの影はうまく出ないようです) 。
CornellBox
SDFGI は RayTracing Core を使っていないので比較的古い GPU でも動きます。そして、古い GPU でもパフォーマンスが出て程良いクオリティの GI レンダリングができるように設計されています。

ソースコード中では、SDFGI 関連の実装は C++側は servers/rendering/renderer_rd/ 内の renderer_scene_render_rd.cpprenderer_scene_gi_rd.cppforward_clustered/render_forward_clustered.cpp に記述されており、Shaderコードは servers/rendering/renderer_rd/shaders/ 内に glsl で実装されています。 Godot4 でレンダリングを開始すると renderer_scene_render_rd.cpp 内の RendererSceneRenderRD::render_scene() が実行されているのでこの関数から見ていきます。
残念ながら私が実装の詳細まで理解することができなかったため、ここでは処理の概要のみ説明します

SDFGI に関して render_scene() 内の処理は大まかに以下のようになっています、

  1. SDF の構築
  2. ライトプローブの更新
  3. 各ピクセルの GI の計算

まずは SDF の構築を行い、シーン上に一定間隔毎で設置された各ライトプローブ上の放射照度を計算します。 最後に各ピクセルから見える点の GI を周囲にあるライトプローブから計算します。

SDF の構築

シーン中の空間をグリッドに区切り各セルの SDF を計算します。Godot4 のデフォルトでは 128 x 128 x 128 のグリッドに区切っています。 各セルはまず自分のセル内にジオメトリがあるか確認します。この時オリジナルのジオメトリでは処理が重たくなってしまうので、下図のように簡易化されたジオメトリを用いています。

ジオメトリが見つかった場合はその表面までの距離をセルに格納します。 ジオメトリが見つからなかった場合は、次はセルの周囲にある別のセルを参照します。周囲のセルのどれかにジオメトリがある (SDFの値がセルに格納されている) 場合は自身のセルからジオメトリの表面までの距離をセルに格納します。周囲のセルを参照する処理を何度か繰り返す事でシーン全体の SDF の構築を完成させます。

ライトプローブの更新

ここでは GI を計算するために、シーン中の各ライトプローブ上の放射照度を計算します。ライトプローブは下の画像のようにシーン中に一定間隔で配置されています。

このプローブ上からランダムな方向にレイを飛ばし当たった光源からの放射輝度を計算します。この時のレイキャストには SDF を用いたスフィアトレーシングを行います。デフォルトの設定では 1 プローブ当たり 16 本のレイを飛ばしています。また、ディレクショナルライトがある場合はその方向にもレイを飛ばし、ディレクショナルライトからの放射輝度も計算します。
次に間接照明に関しては、今度は周囲にあるライトプローブを参照し、自身のライトプローブ方向への放射輝度を計算します。

各ピクセルの GI の計算

ライトプローブの更新が終了したら、次は各ピクセルからの GI を計算します。ピクセルから見える頂点をデプスバッファから計算します。頂点を見つけたら、ライトプローブで間接照明を計算したときと同様に頂点の周囲にあるライトプローブから頂点への放射輝度を計算します。

終わりに

本記事では Godot4 に実装された SDFGI について調べたことを簡単にまとめました。 ここで紹介した内容は SDFGI の概要程度で、実際の実装では色々な高速化が施されていますが私が解説できるほどの理解ができなかったためスキップしています。また、似た手法として SDFDDGI が発表されているのでこちらを読んでみるのも良いかもしれません。 法線の八面体へのエンコード (codepaper) やRGB9995エンコード (code) など手軽に自分のレンダラーでも使えそうな圧縮手法もあったので、ここにコードへのリンクだけ貼っておきます。
大規模なソースコードから使われている技術を読み解いていくのは慣れていない事ですが、自分の知らない技術が使われているのを見つけられるので今後も他のレンダラーのコードも読んでいこうと思います。

参考文献

  1. Amazon Lumberyard, Amazon Lumberyard Bistro, Open Research Content Archive (ORCA), url=http://developer.nvidia.com/orca/amazon-lumberyard-bistro
  2. Jinkai H, Milo K. Y, Guillermo E. A, Shihao G, Xiangjun T, Xiaogang J Efficient real-time dynamic diffuse global illumination using signed distance fields (2021)

2020年12月20日日曜日

Resampled Importance Sampling & Weighted Reservoir Sampling

RIS and WRS

Resampled Importance Sampling & Weighted Reservoir Sampling

このページは レイトレ Advent Calendar 2020 の20日目の記事です。

Spatiotemporal Reservoir Resampling for Real-Time Ray Tracing with Dynamic Direct Lighting を読んでみて Resampled Importance SamplingWeighted Reservoir Sampling が面白いと思ったので少しまとめてみます。
Resampled Importance Sampling は通常の Importance Sampling では用いられない pdf に従ったサンプリングも行うことができます。また、 Resampled Importance Sampling は通常の Importance Sampling よりも計算に必要なメモリ容量が多くなるので、 Weighted Reservoir Sampling を用いて計算に必要なメモリ容量を削減します。

Importance Sampling

Direct Lighting では、ある点 yy から方向 ω\omega への反射輝度 LL は次のような積分で表されます、

L(y,ω)=Aρ(y,xyω)Le(xy)G(xy)V(xy)dAx(1) L \left( y, \omega \right) = \int_A \rho \left( y, \overrightarrow{xy} \leftrightarrow \omega \right) L_e \left( x \rightarrow y \right) G \left( x \leftrightarrow y \right) V \left( x \leftrightarrow y \right) dA_x \tag{1}

ρ\rho は BSDF 、 LeL_e は光源の放射輝度、 GG は幾何項、 VV は可視度項になります。簡単のために yyω\omega の表記を省略すると式 (1) は、

L=Af(x)dx,wheref(x)ρ(x)Le(x)G(x)V(x)(2) L = \int_A f \left( x \right) dx, \qquad where \quad f(x) \equiv \rho \left( x \right ) L_e \left( x \right) G \left( x \right) V \left( x \right) \tag{2}

と表せます。

(2) の積分は分析的に解くことは難しいため、通常 Monte Carlo 法を用いて解析的に解きます。 Monte Carlo 法は確率的手法であるため分散によるノイズが発生します。 分散を低減するために Importance Sampling (以降 IS と表記) がよく用いられます。
IS では NN 個のサンプル xix_i を pdf p(xi)p \left( x_i \right) に従ってサンプリングします。その推定量は以下のようになります、

<L>isN=1Ni=1Nf(xi)p(xi)L(3) \left< L \right>_{is}^{N} = \frac{1}{N} \sum_{i=1}^{N} \frac{f \left( x_i \right)}{p \left( x_i \right)} \approx L \tag{3}

ISf(x)0f \left( x \right) \neq 0 となる xx に対して常に p(x)>0p \left( x \right) \gt 0 となる場合に unbiased となります。 また、 p(x)p \left( x \right)f(x)f \left( x \right) に対して相関が強いほど分散を低減することができます。
IS を用いるためには pdf で定義される分布に従ってサンプルを生成できる必要があります。一般的にサンプリングの方法として inverse cumulative distribution function (逆CDF)rejection sampling が用いられます。

Resampled Importance Sampling

Direct Lighting を IS を用いて計算する場合は ρLeGV\rho \cdot L_e \cdot G \cdot V に比例した pdf に従ってサンプリングするのが理想ですが、このような pdf は closed-form で表現できず複雑なため 逆CDF によるサンプリングは難しくなります。
Resampled Importance Sampling (以降 RIS と表記) はこのような複雑な pdf を用いて IS を行うことができます。 RIS では2つの pdf を用います。1つはソース pdf ppff との相関は強くないが 逆CDF ができる pdf です (例えば pLep \propto L_e など)。もう1つは ターゲット pdf p^\hat{p}ff との相関は強いが正規化が難しく 逆CDF ができない pdf です (例えば p^ρLeG\hat{p} \propto \rho \cdot L_e \cdot G など)。

先ず、 MM 個 (M1M \ge 1) の候補サンプル x={x1,,xM}\boldsymbol{x} = \left\{ x_1, \ldots, x_M \right\} を ソース pdf pp に従ってサンプリングします。その候補サンプルの中から zz 番目、 xzx_z (z{1,,M}z \in \left\{1, \ldots, M \right\}) を重み w(x)=p^(x)p(x)w \left( x \right) = \frac{\hat{p} \left( x \right)}{p \left( x \right)} に従ってランダムに選びます。候補の中から xzx_z が選択される確率は、

p(zx)=w(xz)i=1Mw(xi)(4) p \left( z | \boldsymbol{x} \right) = \frac{w \left( x_z \right)}{\sum_{i=1}^{M} w \left( x_i \right)} \tag{4}

となります。上記の手順でサンプリングされた xzx_z はターゲット pdf p^\hat{p} に近似した分布になります。この xzx_z を使った推定量は以下のようになります、

<L>risN,M=1Ni=1N(f(xiz)p^(xiz)(1Mj=1Mw(xij)))(5) \left< L \right>_{ris}^{N,M} = \frac{1}{N} \sum_{i=1}^{N} \left( \frac{f \left( x_{iz} \right)}{\hat{p} \left( x_{iz} \right)} \cdot \left( \frac{1}{M} \sum_{j=1}^{M} w \left( x_{ij} \right) \right) \right) \tag{5}

(5) の直感的な理解は、この推定量は pdf p^(xz)\hat{p} \left( x_z \right) に従って xzx_z をサンプリングしているように振る舞いますが、 実際には p^\hat{p} は正規化されておらず xzx_zp^\hat{p} では無く p^\hat{p} に近似した分布に従ってサンプリングされているため、 1Mj=1Mw(xj)\frac{1}{M} \sum_{j=1}^{M} w \left( x_j \right) の項でその補正をかけている形になります。

(5)M=1M=1 の時は pp を使って IS しているのと同じになり、 M=M=\infty の時は p^\hat{p} を使って IS しているのと同じになります。 MM が大きくなるほど p^\hat{p} に近い分布で IS できますが計算量は多くなります。
以下の図は[1]からの引用ですが、ソース pdf pp を一様分布、ターゲット pdf p^\hat{p}p^cos(θ)+sin4(6θ)\hat{p} \propto cos \left( \theta \right) + sin^4 \left( 6 \theta \right) として MM を変化させた時の xzx_z の分布を表しています、

RISM,N1M,N \ge 1 かつ f(x)0f \left( x \right) \neq 0 となる xx に対して常に p(x)>0,p^(x)>0p \left( x \right) \gt 0, \hat{p} \left( x \right) \gt 0 となる場合に unbiased となります。

Weighted Reservoir Sampling

RIS を行う際に候補サンプルを一度に全て保持しようとすると MM 個分の候補サンプルのメモリ容量が必要となります。 Weighted Reservoir Sampling (以降 WRS と表記) を RIS と組み合わせることで MM 個の候補の中から nn 個を選択する場合 n(+1)n (+1) 個分のメモリ消費のみで行うことができます。ここでは n=1n=1 とした場合、つまり候補サンプルの中から1個を選択する場合について説明します。
WRS は事前に候補数 MM を知っておく必要は無く、 {x1,x2,,xM}\left\{ x_1, x_2, \ldots, x_M \right\} を全て保持しておく必要もありません。例えば候補サンプルが x1x_1 から順にストリーミング入力されて総数 MM がわからない場合でも 1 個を選択することができます。

WRS では候補サンプルを x1x_1 から順に処理していき、 resevoir と呼ぶ入れ物 (ここでは候補サンプル 1 個分のメモリ容量) に確率的に挿入 (既に他の候補が入っている場合は交換)していきます。 x1x_1 から処理を始め mm 番目の xmx_m までの処理を行っている時、 xix_ireservoir に入っている確率は w(xi)j=1mw(xj)\frac{w \left( x_i \right)}{\sum_{j=1}^{m} w \left( x_j \right)} です。ここで用いる w(x)w \left( x \right)RIS で用いる重みと同じものです。次の xm+1x_{m+1} の処理では、reservoir に入っているものと以下の確率で交換します、

w(xm+1)j=1m+1w(xj)(6) \frac{w \left( x_{m+1} \right)}{\sum_{j=1}^{m+1} w \left( x_j \right)} \tag{6}

(6) から逆に xm+1x_{m+1}reservoir に入らず xix_i が 残っている確率は、

w(xi)j=1mw(xj)(1w(xm+1)j=1m+1w(xj))=w(xi)j=1m+1w(xj)(7) \frac{w \left( x_i \right)}{\sum_{j=1}^{m} w \left( x_j \right)} \left( 1 - \frac{w \left( x_{m+1} \right)}{\sum_{j=1}^{m+1} w \left( x_j \right)} \right) = \frac{w \left( x_i \right)}{\sum_{j=1}^{m+1} w \left( x_j \right)} \tag{7}

となります。m+1=Mm+1=M まで処理が終了した段階で xix_ireservoir に入っている確率は、

w(xi)j=1Mw(xj)(8) \frac{w \left( x_i \right)}{\sum_{j=1}^{M} w \left( x_j \right)} \tag{8}

となり式 (4) と同じ形となります。このことから RISWRS を組み合わせても xzx_z の分布を変えずにサンプリングできることがわかります。また、必要なメモリ容量を reservoir 分と重みの和 j=1mw(xj)\sum_{j=1}^{m} w \left( x_j \right) を保持する分のみに削減できます。

終わりに

RISWRS について調べましたが実はまだ試していません。以下の事についてまだ疑問が解決していないのでもう少し調べてみようと思っています。

  1. RIS + WRS では乱数が多く必要になるが必要な個数を削減する方法はあるか
  2. RIS と他のサンプリング戦略の Multiple Importance Sampling (MIS) を行う際の RIS 戦略の pdf を決定論的に計算する方法はあるか

1 については、通常の IS逆CDF を用いてサンプリングする際に乱数を 1 個使うのに対し、 RIS + WRS では候補サンプルのサンプリングや reservoir への確率的交換で多くの乱数を必要とします。何か必要な乱数の数を減らす方法はないでしょうか。
2 については、式 (5) から RIS で用いる pdf を pris(xz)=p^(xz)1Mj=1Mw(xj)p_{ris} \left( x_z \right) = \frac{\hat{p} \left( x_{z} \right)}{\frac{1}{M} \sum_{j=1}^{M} w \left( x_{j} \right)} とすると、例えば MIS 時に xzx_z から pris(xz)p_{ris} \left( x_z \right) を評価する際に j=1Mw(xj)\sum_{j=1}^{M} w \left( x_{j} \right) を計算するために乱数を用いて他の候補をサンプルをサンプリングする事になると思います。もっと決定論的に prisp_{ris} を評価する方法はないでしょうか。

参考文献

  1. Talbot J, Cline D, Egbert P: Importance Resampling for Global Illumination (2005)
  2. Efraimidis PS, Spirakis PG: Weighted Random Sampling with a Reservoir (2006)
  3. Efraimidis PS: Weighted Random Sampling over Data Streams (2015)
  4. Bitterli B, Wyman C, Pharr M, Shirley P, Lefohn A, Jarosz W: Spatiotemporal Reservoir Resampling for Real-Time Ray Tracing with Dynamic Direct Lighting (2020)