
リアルタイムレンダリング用に、画像のように水・煙・炎などを簡易的に表現可能なボリュームレンダリングのモデルを作りました。
モデルの導出過程と結果、計算例を書いておきます。
ボリュームレンダリング
ボリュームレンダリングとは、半透明な体積を持った物質を描画する技術全般のことを指します。
通常、ゲームなどで描画されるオブジェクトはほとんどが不透明であり、半透明であっても板ポリゴンによってブレンディングが施されたものがほとんどです。
板ポリゴンのブレンディングにおいては、体積という概念は登場しません。なぜなら、板ポリゴン自体が体積を持たないためです。板ポリゴンの不透明度はアルファ値によってパラメータ化され、アルファ値0で完全な透明、アルファ値1で完全な不透明を表します[1]。ちょうど色のついたシートをかざしてみるような感じです。
一方で、体積を持った半透明な物質をレンダリングしたい場合、前述のようにシート一枚で済ませるといったことはできません。
物理ベースレンダリング
きちんとした物理ベースレンダリングにおいては、こうした体積を持った物質は関与媒質として扱われます。関与媒質は空間中の光を拡散させ、あるいは吸収し、あるいは媒質自体が光を放ちながら周囲の物質の照光状態を左右します。
しかし、このような現象を正確に再現するためには大量の計算が必要になります。具体的には、媒質中のありとあらゆる場所でありとあらゆる方向への積分計算が必要になります。リアルタイム性を要求しないオフライン計算においてはモンテカルロ法を用いてこのような計算を実際に実行するのですが、リアルタイムレンダリングにおいてはそのような計算は(少なくとも1フレーム内で収束させるのは)無謀と言えるでしょう。
例えば、細かい粒子を溶かした水にレーザーを当てると、レーザーの軌跡が見えるようになる現象を知っているかと思います。あれはレーザー光を直接見ているのではなく、
レーザー光 → レーザー光が粒子に当たる → 粒子により光が拡散する → その光が目に届く
という過程を経ています。このような過程を再現するためには、ありとあらゆる場所で粒子に当たった光の総量を計算する必要があり、そのためあらゆる場所での積分計算が必要になります。

そうした計算を行うと、このようなリアルな画像がレンダリングできるようになります[2]。しかし、当然ながら大量の計算時間を消費することになります。
近似を入れる
そこで、リアルタイムに計算を行うために、「あらゆる媒質はあらゆる場所において均一に照らされている」という仮定を置きます[3]。これは大域照明を否定するものであり、前述のような「レーザー光により局所的に媒質が照らされている」といった現象が再現不可能になります。
その代わり、どの方向からどれだけ光を浴びているか・どの方向にどれだけ光を反射するかを(均一な媒質内部では)定数として扱うことができるので、あらゆる場所でのあらゆる方向への積分計算は不要になります。
これはある種かなりの大胆な近似になるのですが、それでも冒頭で示したような立体感のあるシーンをレンダリングすることができます。
個別の近似モデルを導出する
上記の近似の下で、体積のある半透明な物質を見たときにどのように見えるか考えてみます。
なお、ここでの媒質はひとつの塊として均質であるとします。つまり、場所によって色や透明度のムラがあるような媒質は考えません。
RGBで分離して考える
最終的な出力はRGBの3次元の値になりますが、ここでは1次元、つまりモノクロのモデルを考えます。理由は単純で、色を付けたければ赤用・緑用・青用の3種類のパラメータを用意してそれぞれで計算した結果を合成すればいいからです。
目標
最終的に欲しい情報は、ある場所と方向から媒質を見たときの
- 媒質の影響で目に到達する光の量
- 媒質を通り抜ける光の割合
の2つです。
媒質の影響で目に到達する光の量 は、媒質によって拡散された光、あるいは媒質自身から出てくる光がどれだけ目に届くかを表します。これを f_R で表すことにします。
一方、媒質を通り抜ける光の割合 は、媒質の後ろにあるものがどれだけ見えるか、言い換えると、媒質全体を通した透明度を表します。これを f_T で表すことにします。この値が1のとき、媒質の後ろの色はそのまま最終的な色に加算されます。一方、この値が0のとき、媒質の後ろにあるものは最終的な色に一切影響を与えません。完全に媒質によって隠れている状態です。
引数は深さだけ
前述の仮定によって、媒質は場所・方向などの影響を受けないため、媒質の影響は視線が媒質を通過する長さ(=見えている深さ)の関数になります。この深さを d とします。すると、媒質の影響で目に到達する光の量 f_R と、媒質を通り抜ける光の割合 f_T は(媒質自身のパラメータを除くと)いずれも d のみの関数となることが分かります。以降、f_R(d) や f_T(d) などと書くことにします。
拡散・吸光・発光
媒質は大きく分けて、拡散・吸光・発光の3つの性質を持ちます。それぞれの性質の特徴は次の通りです。
拡散
媒質が受けた光を拡散する能力です。煙や濁った物質などに特徴的です。
吸光
媒質が受けた光を吸収する能力です。色のついた液体やガラスなどに特徴的です。
発光
媒質自身が光を発する能力です。炎などのプラズマに特徴的です。
拡散モデル
とりあえず拡散モデルだけ考えてみることにします。媒質は均一な光を受けているので、見えている深さ d が大きくなるほど「一定の色」に近付いていくことになります。霧がかかったような感じですね。この一定の色を C_D とすると[4]、十分大きな d で f_R(d)\approx C_D, f_T(d)\approx0 が成り立つことになります。これは通常のアルファブレンディングを無数に細かく重ねていくことで表現できそうです。
通常のアルファブレンディングのおさらいです。背景色が C のところにアルファ値 \alpha で色 C_D を混ぜ込むと、得られる色は単純な線形補間で (1-\alpha)C+\alpha C_D になります。
いま、媒質の非常に薄いレイヤ(厚さ h\ll1)を考え、このレイヤによって変化する色を求めてみます。非常に薄いレイヤなので、不透明度はレイヤの厚さ h に比例すると考えていいでしょう。よって、アルファ値を h\alpha としたブレンディングを考えます。ブレンディングにより得られる色は
(1-h\alpha)C+h\alpha C_D = C+(C_D-C)h\alpha
つまり、元の色 C からの変化が (C_D-C)h\alpha で表されることになります。これを微小量 h で割ると、色 C の変化の微分係数 (C_D-C)\alpha が得られます。
求めたい色は非常に薄いレイヤを無数に重ねて厚さ d まで達したときの色 C なので、厚さを x とおいて次の微分方程式を解き、x に d を代入すればよいです。
\frac{dC}{dx} = (C_D-C)\alpha
この微分方程式は変数分離型であり、次のように簡単に解くことができます。
\begin{gathered}
\frac{dC}{dx}=\frac{d(C-C_D)}{dx}=(C_D-C)\alpha\\
\frac{d(C-C_D)}{C-C_D} = -\alpha dx\\
\int\frac{d(C-C_D)}{C-C_D} = \int-\alpha dx\\
\log|C-C_D| = -\alpha x+c\\
|C-C_D| = e^{-\alpha x+c_1}\\
C = C_D+c_2e^{-\alpha x}
\end{gathered}
ここで、c_1, c_2 は任意の定数で、境界条件を満たすように決定します。今は厚さが0、すなわち x=0 の場合に C が元々の背景色(媒質の後ろの色)C_B になるとして計算すると、
C_B=C_D+c_2e^0=C_D+c_2
より c_2=C_B-C_D と分かります。結局のところ、深さ d のときの全体の色は次のように表されます。
C_D+(C_B-C_D)e^{-\alpha d}
なんということでしょう、アルファ値が指数の肩に乗っただけの結果になりました。ともあれ、これで拡散だけする媒質の色は計算できたことになります。この結果より、媒質の不透明度を表すパラメータを \alpha_D、照光後の色を C_D として f_R, f_T を求めると、
f_R(d) &= C_D(1-e^{-\alpha_Dd})\\
f_T(d) &= e^{-\alpha_Dd}
となります。ここで、不透明度 \alpha_D はもはや0~1の範囲に収まっている必要がないことに注意してください。値が大きければ大きいほど不透明な物質に近くなります。
吸光モデル
拡散モデルと同じ考えで、d が十分大きいときの挙動を考えてみます。光を拡散することなく吸収し続けるので、最終的には f_R(d)\approx0, f_T(d)\approx0 と真っ黒になります。
しばらく考えると、この場合には乗算合成を無数に適用すればいいことが分かります。元の色 C に対し、アルファ値 \alpha で色 C_A の乗算合成を行うと[5]色 (1-\alpha)C+\alpha C_AC が得られます。
よって、厚みが h\ll1 のときにアルファ値 h\alpha で色 C_A の乗算合成を行うと、得られる色は
(1-h\alpha)C+h\alpha C_AC = C - h\alpha C(1-C_A)
であり、C の微分係数は -\alpha C(1-C_A) となります。拡散モデルと同様に微分方程式を解いてやります。
\begin{gathered}
\frac{dC}{dx}=-\alpha C(1-C_A)\\
\int\frac{dC}C=-\int\alpha(1-C_A)dx\\
C=ce^{-\alpha(1-C_A)x}
\end{gathered}
ここで c は任意定数で、x=0 で C=c が成り立つことから c=C_B であり、最終的に深さ d の場合の色は C_Be^{(1-C_A)d} になります。よって、媒質の吸光に関する不透明度 \alpha_A と色 C_A をパラメータとして、次の吸光モデルが得られます。
f_R(d) &= 0\\
f_T(d) &= e^{-\alpha_A(1-C_A)d}
発光モデル
次に発光モデルを求めます。実はこれは非常に簡単に求まります。単位距離あたりに媒質が発光する光の量を C_E とすると[6]、全体として dC_E の光が目に届くことになりますから、発光モデルは
f_R(d) &= dC_E \\
f_T(d) &= 1
です。
全部を統合したモデルを求める
さて、今までは拡散・吸光・発光をそれぞれ別々に扱ってきましたが、実際のところ拡散を一切しない、あるいは吸光を一切しない媒質は稀であり、実用上は拡散・吸光・発光をすべて同時に扱えるモデルが求められます。
実は導出に微分方程式を利用した理由がここにあります。それぞれ導出したモデルは足したり掛けたりしても有効な意味を持ちませんが、微分係数であれば足し合わせることに意味があります。
色 C の深さ x に対する微分係数は
- 拡散モデル:
(C_D-C)\alpha_D - 吸光モデル:
-\alpha_A C(1-C_A) - 発光モデル:
C_E
でした。これらを足し合わせた次の微分方程式を解きます。
\frac{dC}{dx} &= (C_D-C)\alpha_D-\alpha_A C(1-C_A)+C_E\\
&= C_D\alpha_D+C_E-(\alpha_D+\alpha_A(1-C_A))C
複雑そうに見えますが、A=C_D\alpha_D+C_E, B=\alpha_D+\alpha_A(1-C_A) とおくとシンプルな式になります。
\begin{gathered}
\frac{dC}{dx} = A-BC\\
\frac{d(A/B-C)}{dx} = -(A-BC)\\
\int\frac{d(A/B-C)}{A/B-C} = -\int B dx\\
A/B-C = ce^{-Bx}\\
C = A/B-ce^{-Bx}
\end{gathered}
今までと同様に c は任意定数であり、x=0 で C=C_B になることより、c=A/B-C_B と定まります。結局、深さ d における色は
&A/B(1-e^{-Bd})+C_Be^{-Bd}\\
&=\frac{C_D\alpha_D+C_E}{\alpha_D+\alpha_A(1-C_A)}(1-e^{-(\alpha_D+\alpha_A(1-C_A))d})+C_Be^{-(\alpha_D+\alpha_A(1-C_A))d}
となります。これより、最終的な f_R と f_T が求まります。
f_R(d) &= \frac{C_D\alpha_D+C_E}{\alpha_D+\alpha_A(1-C_A)}(1-e^{-(\alpha_D+\alpha_A(1-C_A))d})\\
f_T(d) &= e^{-(\alpha_D+\alpha_A(1-C_A))d}
お疲れ様でした。
ちなみに、B=0 のときは拡散も吸光もしないことを意味するので、発光モデルの式を使ってください。
結果まとめ
媒質のパラメータ
\alpha_D: 拡散しやすさC_D: 拡散する光の色\alpha_A: 吸光しやすさC_A: 吸光に使う色[7]C_E: 発光の色と強度
奥行き d の媒質を見たときの
- 媒質から目に届く光:
f_R(d) - 媒質を通り抜ける光の割合:
f_T(d)
f_R(d) &= \frac{C_D\alpha_D+C_E}{\alpha_D+\alpha_A(1-C_A)}(1-e^{-(\alpha_D+\alpha_A(1-C_A))d})\\
f_T(d) &= e^{-(\alpha_D+\alpha_A(1-C_A))d}
ただし、\alpha_D+\alpha_A(1-C_A)=0 の場合は代わりに次の式を使います。
f_R(d) &= dC_E\\
f_T(d) &= 1
たとえば、背景色が C_B のときは合成後の色を f_R(d) + f_T(d) C_B のように計算できます。媒質が複数存在する場合は順に合成を行えばよいです。
計算例
折角なのでモデルを使用した計算例をいくつか載せておきます。
何もなし

何もない空間だとあれなので不透明な物体を置いてあります。
拡散だけ

霧やガスがかかったような、曇った感じになります。
吸光だけ

青色1号の中に沈められるとこんな感じでしょうか。
拡散+吸光

暗くなりつつもモヤがかかったような雰囲気になります。色のついた濁った水なんかに似ています。
発光だけ

見るからに炎。熱そうです。
拡散+発光

光るガスに包まれました。
吸光+発光

光を吸いつつ自らは光を発するという謎の物質です。なんだか不思議な感じになりました。使いどころは謎です。
おわり
汎用性が高そうな簡易モデルを導出することができました。パラメータの調節次第で水から炎、煙まで様々な雰囲気を出せるので、いろんなシェーダで使えるのではないでしょうか。
何かあったら Twitter でフィードバックをもらえると作者が大変喜びます。