過去の独りごち/独りごとはこちら
過去のJavaアプレットは
こちら

 

8/17 Rayleighさん

 まずは、日記ライクな文章から…

 お久しぶりです。約1年ぶりの更新になりましたが、みなさんいかがお過ごしでしょうか。

 僕は年甲斐も無く夏のイベントを物見遊山…いや、遠くに住む方々と会うにはこれが一番なものでして、ハイ。 で、今回も爆裂工房の皆さん、Ko-Taさん、UOXさんをはじめとする皆さんにお会いしてきました。

 うーん、マーズワース凄いですね。なんで雑誌のオマケにこんなのがついてくるご時世なのに、市販ゲームって(ry

ここから本題

 Rayleighってレイレイじゃなくてレイリーなんですね。

   

 というわけで、空の描画をやってみました。


sky00.zip
(984K ソース+バイナリ)
要1 DirectX9
要2 VertexShader1.1以降&PixelShader1.1以降が「HALで動作する事」
ちなみに、Radeon9600での動作しか確認してません

 最初に困ったことを言ってしまいますが…まだいい感じの係数が見つかってなくて、なんだかうまい具合に色が変化してくれません。悔しいので色の変化は適当にいじれるようにもしてあります。 うーん、邪道。実測値とか載せてるサイトとかありませんかねぇ。

 実に2002年、ATIのNaty氏らにより発表された手法で、国内でも鬼武者3などのPS2タイトルでも使用された、今更という感のあるテクニックですが、それほど実装が困難なものではなく、むしろシェーダも非常に短くコンパクトに作ることが出来、見栄えもするのでお勧めのテクニックです。

 以下は、ATIの論文「Rendering Outdoor Light Scattering in Real Time」より、僕が理解してる範囲での覚書のようなものです。

 8/18 昼 注 : 図は描き次第追加していきます…

基本的な考え方

 今回紹介する方法は、光の散乱をシミュレーションする事で、空の色合いの場所による変化や、太陽の高さによる色の変化までをリアルに再現しようという方法です。

 光の散乱が起きる原因というのは、言うまでも無く、大気中には細かな粒子が存在しているからです。雲の出ていない昼間の空は、なぜ青いのか?といえば、小学校の理科でも「大気中の細かい粒子が太陽の、特に青い光を強く散乱するから」と教えられたはずです。

 では、どうやって大気中に無数にあるような、粒子による散乱をシミュレーションしたものでしょうか。それには、ちょっとした積分と確率関数を導入するだけでいいんです。

 ところで、「どうして散乱されると空が明るくなるの?」という問いには大人は答えてくれないものでした。確かに明るくなる面積が広がるという点には合点がいくのですが、空全体に広がるような散乱なんて、本当にするものなんでしょうか?散乱されるうちに宇宙の外へでていったり、「細かい粒子」に光が吸い取られたりはしないんでしょうか? シミュレーションは、このことに明快な答えを与えてくれます。

天球体について

 まず、空を描く、というのは3Dゲームを作ったことのある方なら、空模様を描いたテクスチャを用意して、ゲームのステージを取り囲むのに充分な球体の内側に貼り付けることで、実現したのではないかと思います。この、ステージを取り囲む球体を天球体と呼ぶ事にします。

 今回の方法では、天球体にテクスチャは貼りません(雲を浮かべたいときには貼りますが)。代わりに、天球体の各頂点の色を計算する事で、空の色合いを表現する事を考えます。

 天球体の大きさですが、今回のデモでは本当に地球の半径(6358km)+大気圏の上までの高さ(100km)のサイズの天球体を作り、そのうちデモで使う範囲を切り取る事でポリゴン数を減らしたものを使っています。

光と粒子が出会うとき

 天球体メッシュを作ったところで、空の色付けについて考えます。

 まず、光が粒子に衝突したときの応答は、以下のどれかと考えられます。

  • 光は粒子に吸収されて、粒子を暖める(吸収)
  • 光は粒子によって飛ぶ方向を変えられる(散乱)

 実際には大気中での光の吸収はわずかにしか起こらないので、大気中での光は、粒子によってどんどん散乱されると見てよいそうです。ここで、散乱をさらに二通りに分けて考えます。

  • 散乱された光が、視線上から外れる(out-scattering)
  • 元々視線上には無かった光が、散乱によって視線上に割り込む(in-scattering)

 実は、後者のin-scatteringが今回の話のキモでして、散乱されることによって空全体が明るくなる、という不思議現象の謎を解く鍵を与えてくれます。はい、in-scatteringによって、めっさ明るくなりますよ。

 ま、しかし、out-scatteringの方が分かりやすく、馴染みのある式が出てくるので、そちらから先に説明しましょう。

out-scattering

 out-scatteringは、視線(今回の場合、目と、これから色を決めたい天球体の1頂点を結ぶ線)上に僅かでも屈折を起こすような粒子が置いてあれば、たちまち成立することになります。粒子の屈折角とかは考慮しなくてよいので、大変ラクチンです。

 この計算にはまず、粒子の密度ρ[m-3]と、粒子の散乱断面積(scattering section)σ[m2]を掛けて、β[m-1]とします。なんか難しい言葉ですが、なにぶん相手が微粒子なものでして、粒子の大きさそのものと、光に干渉する部分の面積はまた別物になるんですね。なので、光を屈折させる部分の面積の事を、散乱断面積と呼びます。

 つまり、βは単位長さを光が通る間に、散乱によって視線上から失われる割合になるわけです。よって、out-scatteringのみを考慮した場合、入射光の強さをL0とすると、xメートル離れた地点での明るさL(x)は、以下のように表現できます。

dL(x)/dx = -βL(x)
これを解いて、
L(x) = L0 ・ exp ( -βx) 
(Eq.1)

 簡単ですね。さて、先ほど「馴染みのある式」と言ったのは…そうです。EXPフォグの式に非常に似た式になるんですね。ただし、フォグの場合は、入射した光が霧の中を長く旅した場合、目に届く色はフォグの色に近づくのに対して、out-scatteringではどんどん暗くなるばかりです。

 さて、暗くなるばかりでは空は晴れるはずもありません。続いて、in-scatteringについて。

in-scattering

 in-scatteringは、視線上に置いてある粒子が、視線と平行でない方向に飛んでいた光をうまくキャッチして、視線と平行な方向に飛ばしてくれる場合に成立します。

 これを計算するには、先ほどの

  • 粒子の散乱断面積 σ
  • 粒子の密度 ρ
  • 以上2つの積 β

  のほかに、

  • 屈折角(θ,φ)で、屈折する確率を教えてくれるような関数 Φ(θ,φ)
  • 粒子に立体角(θ,φ)方向から入射する光の量 Li(θ,φ)

 が必要となります。この関数は、立体角を入力値として取るため、ニ変数関数となってていやらしいのですが、これから扱う散乱を起こす微粒子は光の波長より小さいと仮定できるので、この関数はφに依存しないと見做してよく、Φ(θ,φ)はΦ(θ)と簡略化して構わないようです。

 ここで、視線上に粒子が一個だけ置いてあり、それによってθ,φ方向にある視線に届く光の量はどれだけ増えるかというと、以下のようになります。

∫ Li(θ,φ)・Φ(θ) dω
(Eq.2)

 ちなみに、積分の範囲は全立体角です。角度で積分するという事に慣れていないと、いまひとつ掴みづらいかもしれませんが、この式では、ある立体角dω = (θ,φ) から入射する光がちょうどθ,φだけ屈折して出てくれる事によって、視線の上にうまく光を載せる確率を乗算した物を、全方向について合計しているわけです。

 粒子一個によるin-scatteringについてはOK。次に、短い距離 dx の空気を移動する間に、どれだけin-scatteringによって光量が増すか考えると、以下のようになります。

dL(x)/dx = β∫ Li(θ,φ)・Φ(θ) dω
(Eq.3)

 …β掛けただけ。

 尚、Li(θ,φ)は、太陽の角度と明るさによって決まり、位置による依存性はありません。太陽以外の光…つまり、周囲にあると考えられる、他の粒子からの散乱によってやってくる光については無視できるそうです。この理由についてはここでは説明しませんが、「rayleigh散乱 アインシュタイン」あたりをキーワードにぐぐって見ればヒントにたどり着けるかもしれません。

 というか僕も良く分かりませんよ。双極子モーメントとか言われても困ります(ぉーぃ

 ともあれ、「光とは何か?」というCGの根本的な原理について真面目に考える向きには非常に有意義ですし、そのうち大格闘を繰り広げてみたいテーマではありますね。

合体!

 さてさて、in-scatteringとout-scatteringとを両方加味すると、短い距離 dx あたりの光量の変化は以下のようになります。

dL(x)/dx = -βL(x) + β∫ Li(θ,φ)・Φ(θ) dω
(Eq.4)

 これを解くと、

L(x) = L0 exp(-βx) + ∫ Li(θ,φ)・Φ(θ) dω
(Eq.5)

 となります。∫ Li(θ,φ)・Φ(θ) dω は x に依存しないわけで、x について解いてもそんなに複雑怪奇な形にはならないんですね。

RayleighさんとMieさん

 ここまでの話では、「光量の変化」とは言ったものの、色あいの変化については何も話しませんでした。 「空全体が明るくなる」という事はin-scatteringによって説明でき、シミュレートのための式は分かりましたが、「昼間の空は青くなる」という説明はできません。

 実は、今までの式は、光の波長を加味していないのです。「青い(波長の短い)光ほど良く散乱される」という事は十分に考慮せねばならないので、これからは光の波長λに依存する変数を、(λ)付きで表記します。よって、

  • 波長λごとの光量 : L(λ)
  • 入射光L0のうち、波長λについての光量 : L0(λ)

 と表記する事にします。

 そして、ここが肝心なのですが、光の波長ごとに粒子の散乱断面積はσ(λ)として変化するので、βもλに依存して変化するβ(λ)という事になります。以上より、Eq.5は以下のように示せます、

L(λ)(x) = L0(λ) exp(-β(λ)x) + ∫ Li(λ)(θ,φ)・Φ(λ)(θ) dω
(Eq.6)

 ご存知のとおり、光の波長が違えば色相が変化するので、波長ごとに異なる散乱の度合いを示すなら、空の色は太陽の光をそのままスケーリングしたような色にはならないのです。

 粒子の散乱断面積と波長の関係についてですが、リアルなシミュレーションをしようとすると、二種類の散乱を扱う必要があるそうです。面倒だなぁ。

  • 粒子の直径が光の波長より充分小さいの場合に起こる、Rayleigh散乱
  • 粒子の直径が光の波長と同程度か、やや小さい場合に起きる、Mie散乱

 他にも、水滴などのように大きな粒子による非選択的散乱というのもあるのですが、それは雲のレンダリングのように、特殊なケースとして取り扱います。

 話を本筋に戻しまして、Rayleigh散乱の強さはλ-4に比例すると考えてよいのですが、Mie散乱はλによって非常に複雑な挙動を示すため、大気中に存在する粒子の大きさは、まちまちであるという事実から、λには、ほぼ依存しない物として実装します。このあたりのもう少し細かい経緯についてはATIの論文をどうぞ。

 Rayleigh散乱のみ考慮したβ値を、βR(λ)とし、屈折についての関数ΦもΦR(λ)と表すと、それぞれは以下のように求められています。

βR(λ) ∝ λ-4
ΦR(λ) = 3/(16π) * (1 + cosθ)
(Eq.7)

 次に、Mie散乱考慮したβ値を、βMとし、屈折についての関数ΦもΦM(λ)と表すと、それぞれは以下のようになるそうです。

βM は 一定
ΦM(λ) = (1 - g^2) / (4π * ((1+g^2 - 2g cosθ)^(3/2)))
但し、g は指向性についての係数であり、前方に強く散乱される場合には、0>g≧-1
(Eq.8)

入射光について

ここで、入射光である太陽の扱いについて、もう一度考えてみると、L0(λ)は大気圏に突入してくる太陽の光であるという事は分かるのですが、Li(λ)(θ,φ)も同じ太陽の光とはいえ、どう扱った物かと。

 まず、太陽の光には向きがあるので、特定のθ,φについてのみLi(λ)(θ,φ)は高い値を出すとみなせますし、視線との角度だけが問題になるので、実は独立変数として持っている角度は二つも要らないという事になります。ここで、視線ベクトルと、目と太陽を結ぶ線分との角度をθsunとすると、

Li(λ)(θ) = 
	Esun(λ) (θ=θsun)
	0 (otherwise)
(Eq.9)

 ここで、Esun(λ)は、太陽の明るさ・色合いによって決まる係数とみなせます。つまり、∫ Li(λ)(θ,φ)・Φ(λ)(θ) dω という式は、もとより積分なんかする必要は無くて、太陽光線と視線の角度のギャップである、視線とθsunだけずれている太陽の光が、Φによって直る割合を一回計算するだけで済むんです。ちなみに、θsun = 0の時、目は太陽を直接見ている事になります。

 ただ、空全体を照らせるような光源を、太陽のほかに複数求める場合などは光源の数だけ合計する必要がありますけど。エルガイムの舞台みたいに恒星を複数持つ惑星とか…ってここの読者さんなら分かりますよね??

 ともかく、in-scatteringによる光の増加を示す式は、以下のように書き換えられます

dL(λ)(x)/dx = β(λ)・ Esun(λ)Φ(λ)sun)
(Eq.10)

 次に、Rayleigh散乱とMie散乱の両方を考慮して、↑の式を書き換えると…

dL(λ)(x)/dx = Esun(λ) ( βR(λ)ΦR(λ)sun) + βM(λ)ΦM(λ)sun) )
(Eq.11)

 β,Φを変えて足し算してるだけです。

 out-scatteringも考慮すると、

dL(λ)(x)/dx = -(βR(λ)M(λ))・L(x) + Esun(λ) ( βR(λ)ΦR(λ)sun) + βM(λ)ΦM(λ)sun) )
(Eq.12)

 という事になります。

役者は揃った

 Eq.12を解いて、Rayleigh散乱、Mie散乱および波長λまで考慮した、in-scattering, out-scatteringによる、波長ごとの光の強さL(λ)(x)を、以下の通り得られます。

L(λ)(x) = L0(λ) exp(-x(βR(λ)+βM(λ))) + 
		 Esun(λ) ( βR(λ)ΦR(λ)sun) + βM(λ)ΦM(λ)sun) ) / (βR(λ)M(λ))

但し、
βR(λ) ∝ λ-4
ΦR(λ) = 3/(16π) * (1 + cosθ)
βM は 一定
ΦM(λ) = (1 - g^2) / (4π * ((1+g^2 - 2g cosθ)^(3/2)))

θsun 視線と太陽光線のなす角
x 視点から(これから描く天球体の頂点まで)の距離

(Eq.13)

  微分方程式いじりは以上で多分終わりです。手計算で充分できる範囲とはいえ、久々にやるとなかなかくたびれるものですねー。

色の決定

 次に、色の決定法について考えます。

 波長λとか言われても、人間の可視領域全体について事細かに変化させなければならないのかというと、そうでもありません。人間の視細胞のうち、色を感じる錐体細胞は3種類しかないのと、それぞれの細胞の興奮の大きさのみによって、目に見える色が決定されるために、色を特徴づけるパラメータというのは人間の目にとっては本当に3つしか無いからです。

 そこで、手っ取り早いアプローチとしてはR,G,Bそれぞれに対応する代表値λRGBを選び、三つのλについてのみEq.13を計算する事で、色についてもシミュレートすることができます。色の変化に対しては人間はそれほど敏感では無いので、大雑把な計算でもなんとかなる…といいなぁ。

 より正確な色のシミュレーション方法としては、λを複数選んでEq.13を通したあと、XYZ色空間での刺激値→RGB色空間での値と変換することで、色の変化を得るという方法があります。λを選ぶ数ですが、多ければ多いに越した事はないのですが、2度視野XYZ系に基づく当色関数のピークに相当する3つのλを選ぶだけでもそれなりのシミュレーションになるかな、と。

 錐体細胞はそれぞれが幅広い感度分布を持っているので、純粋に緑用の錐体だけ興奮する波長とか、赤用の錐体だけ興奮する波長、というのは無いのが面倒といえば面倒です。

以上ッ

 以上で、空のリアルな描画ができるようになりました…と言いたい所ですが、まだ考慮すべき事はあったりします。

 今回説明した方法では、

  • 大気の密度分布は一様ではない
  • 地形の描画の際もin-scattering, out-scatteringを考慮することで、フォグよりリアルな空気遠近法を演出できる。ただし、その場合は地形による自己遮蔽などを考慮する必要がある

 といった事が非常に大雑把か、全く考慮していないという状態でした。

 実装に当たってはこのことも考慮しないと今回紹介したデモのようにいい加減な事になるよ、という事で、落ちもついたところで逃げるとしますか!

 

 ええと、GDC2002年のアーカイブにNaty氏、Hoffman氏らによる発表スライドがPowerPoint形式で掲載されているので、ゲーム作成にバリバリ活用するぜ!という向きには、そちらの「Rendering Outdoor Light Scattering in Real-Time」を参考にされるとよいでしょう。実装時の注意点なども書いてありますし。

Taku Hayase(SANDMAN)

戻る