LIFE LOG(MochiMochi3D)

MochiMochi3D

3D関係の記事を書きます

薄レンズモデル(ThinLens model)

被写界深度

 ピンホールカメラモデルでは所謂「ボケ」というものが存在せずため、どこまで遠くの物体でもくっきりとした画像を出すことができますが、現実で見るような写真は背景や手前のものがボケていると思います。このようになるのは現実のカメラ(肉眼も含む)はピンホールカメラとは異なり、レンズを通して写真を撮っているためです。  

 ある点からのあっちこっちに飛ぶ光がレンズによって屈折し、カメラ内部に集められて、センサーに光が当たる形で写真を撮っており、うまいことセンサー部分の1点に光が集められないと他のセンサーにも光が当たることになり、いわゆるボケが生じてしまいます。  

 ピントが合い、はっきりと見える距離の範囲(深度)というのは被写界深度と呼ばれており、被写界深度から外れた位置にいる物体はカメラではボケたものとして表現されます。この被写界深度を作ることでピンホールカメラではなかった、ボケがある絵というものが作ることができます。

ja.wikipedia.org

 レイトレではこの被写界深度を作ることは簡単で、被写界深度の原因がレンズを通した光の動きなのですからレンズの光の屈折をレイで同様に行えばいいのです。カメラレンズの簡単なモデルとしては薄レンズモデル(ThinLens Model)というものがあります。ThreeJsでパストレを実装した記事でも実装方法を記載したのですが、その時の実装方法では足りないことや絞りの実装などいくつか後で追加したため一通りまとめるために今回記事にしました。この記事ではカメラレイの生成、ウェイトの計算、絞りの実装について載せていこうと思います。

薄レンズモデルとは

 薄レンズモデルはカメラのレンズが厚みのない1枚の凸レンズで構成されているものとするモデルです。イメージとしては虫メガネを使うカメラのようなものです。厚みがないので屈折をそのまま計算するわけではなく、レンズの法則から光が曲がる方向を計算していきます。薄レンズモデルは簡単なものの被写体深度が表現でき、ピンホールカメラに比べると結構リアルな画像を出すことができます。

 薄レンズモデルの処理の流れとしては

  1. レンズの位置、大きさなどの計算
  2. レンズ上の点をサンプリング
  3. 光が向かう方向の計算、カメラレイの生成

というような形で行います。また、サンプリングを行う関係でカメラのレイに重みを付ける必要が出てきます。それも併せてやっていくことになります。

カメラレイの生成

 まず、レンズの位置などについて求める必要があります。  一般的なレンズに成り立つ法則としてレンズの法則があります。焦点距離fのレンズからbの距離にある点が放つ光はレンズを通って反対側にレンズからの距離aの点に再び集まります。この時の各距離について理想的に

 \displaystyle \frac{1}{a} + \frac{1}{b} = \frac{1}{f}

という関係が成り立ちます。これがレンズの法則です。

f:id:kinakomoti321:20211027033509p:plain

 焦点距離fというのはレンズの特性を示すパラメータであり、こちらから与える量になります。そしてabかを与えれば、自動的にレンズの法則より計算することができます。センサーとレンズの距離 aをこちらで設定するとなると直感的にピントが合う距離というのがわかりづらいため、レンズから物体までの距離bを与えてそれにピントが合うようなaを作る方が良いと思います

 レンズの法則よりafbを与えて

\displaystyle a = \frac{bf}{b-f}

と求められます。従って、レンズのポジション\vec{C}はカメラの向き\vec{n}と位置\vec{Cam}とした時

\displaystyle \vec{C} = a\vec{n} + \vec{Cam}

と得ることができます。

 また、レンズのもう一つの特性としてレンズそのものの大きさがあります。レンズの有効な半径Rに対して焦点距離を用いて

\displaystyle F = \frac{f}{2R}

という値が作れます。これはF値(F number)と呼ばれているパラメータで、レンズの明るさを示す指標として用いられるものです。一般的にF値を与えてから、レンズがどの程度の半径を持つかを計算するため、レンズの半径Rについては

 \displaystyle R = \frac{f}{2F}

と求めます。以上によってb,f,Fの3つのパラメータを設定することでレンズの設定が完了します。

 次にカメラのレイを作っていくことになりますが、レンズを通した場合におけるセンサーに入る光はピンホールカメラと違いレンズから様々な方向から来ます。そのため、カメラから出るレイをサンプリングしてやる必要が出てきます。レイのサンプリング方法はレンズ上の点x_0をサンプリングして、センサー上の点x_pと繋ぎ、レンズに曲げられる方向へのレイをx_0から飛ばすといった形で行います。

 今回の実装では極座標について、一様乱数u,vを用いて

 \displaystyle r_0 = Ru\\
\theta_0 = 2\pi v

というようにレンズ状の点をサンプリングしました。ワールド空間上でのサンプル点はカメラのローカル基底\vec{n_x},\vec{n_y}を用いれば、

 \displaystyle \vec{x_0} = C + r_0 \cos{\theta_0} \vec{n_x} + r_0 \sin{\theta_0}\vec{n_y}

と求めることができます。

 次にこのレンズ上の点に向かう光はレンズを通ってどの方向に飛ぶのかを考えなくてはなりません。レンズの性質として、ある点からの光はレンズを通って1つの収束点へと向かいます。即ち、その収束点に向かうような方向を計算することになります。

 レンズの法則より収束点\vec{P}はレンズからbの距離にあります。また、レンズの中心を通る光というのは曲げられることがなく直進します。この時の位置関係は図のようになります。

 レンズを通る光の方向ベクトル\vec{e}はセンサーの点\vec{x_p}とレンズの位置\vec{C}を結ぶ単位ベクトルであるため

\displaystyle \vec{e} = \mathrm{Normalize}(\vec{C}-\vec{x_p})

  と求めることができます。これを用いると幾何的な関係から収束点$\vec{P}$が以下のように求められます。

\displaystyle \vec{P} = \vec{C} + \frac{\cos{\theta}}{L}\vec{e}

以上によってレイの方向ベクトル\vec{d}

 \displaystyle \vec{d} = \mathrm{Normalize(\vec{P} - \vec{x_0})}

と求めることが出来ます。結果としてカメラのレイとしては

\displaystyle \mathrm{Ray}(\vec{x_0},\vec{d})

となります。このようなレイを作ることで薄レンズモデルのカメラを作ることができます。

f:id:kinakomoti321:20211027034513p:plain

ウェイトの計算

 カメラレイの生成には乱数を使用しており、カメラレイはパストレーサーとしてパスのウェイトを持つ必要があります。カメラウェイトの導出に関しては@shockerさんの記事に非常に細かく解説されております。

被写界深度 (Depth of Field)

こちらの記事によるとカメラのウェイト$W_{camera}$は

 \displaystyle W_{camera} = \frac{|\vec{\omega}_{x_0\rightarrow x_1} \cdot \vec{n}|}{P_{lens}(x_0) P_\sigma(x_0 \rightarrow x_1)}

それぞれについては

  • \vec{\omega}_{x_0\rightarrow x_1} :  x_0 から [tex : x_1]への方向ベクトル
  • \vec{n} : レンズの法線
  • P_{lens}(x_0) : レンズ上の点x_0を取る確率密度
  • P_\sigma(x_0\rightarrow x_1) :  x_0 から  x_1の方向へサンプルする確率密度

となっている。

 まず、P_{lens}(x_0)についてはr_0\theta_0極座標のサンプリングを

 \displaystyle r_0 = r_{lens}u\\
\theta_0 = 2\pi v

で行っていたため、

 \displaystyle P_{lens}(x_0) = P_{lens}(r_0,\theta_0) = \frac{2\pi}{r}

という形になる(一様サンプリングをしても良かったかも)。

 次に、P_\sigma(x_0\rightarrow x_1)についてだがこちらは結構複雑になっている。x_0が決まれば一義的に決まるから単に1になるのではないかと考えたが、実際はそうではなく以下のような式で表される。

 \displaystyle P_\sigma(x_0\rightarrow x_1) = \frac{a^2}{|\vec{\omega}_{x_0\rightarrow x_1}|^3} P_A(x_p)

aは画素からレンズまでの距離です(レンズの法則でのa)。この導出は@shockerさんの記事をご覧ください。

 P_A(x_p)とは何かというとセンサーの点$x_p$を選ぶ確率密度であり、このカメラウェイトを計算する際は、カメラの画素の点をサンプリングしたものとして考える必要があります。しかしながら、カメラの画素上のサンプリングは固定されているか、アンチエイリアスによって一様サンプリングされていることが大半です。

そのため、P_A(x_p)は一般的には定数で表されることになると考えられます。

 \displaystyle P_A(x_p) = P_A = const.

後述の理由でP_Aの値は特に気にする必要はありません。

以上をまとめるとカメラウェイトは

 \displaystyle W = \frac{|\vec{\omega}_{x_0\rightarrow x_1} \cdot \vec{n}|^4r}{2\pi a^2} P_A

となります。これをパストレーサーで計算した寄与に掛けて上げればウェイトの実装は完成です。

 コサイン項が入ることで現実のカメラで現れるエフェクトの1つ、口径食(vignetting)が現れます。これは画面端辺りが暗くなる現象で、より現実に近い表現を作ることができます。

ja.wikipedia.org

 しかしながら、これをそのままやると実は真っ暗になります(場合によっては極端に明るくなるかも)。この式が間違ってるというわけではなく、真っ暗になることが現実的に正しいためです。現実のカメラはセンサーに感度がつけられており、かなり暗い光に大して良く見えるように感度を高める形で画像を作っています。このため、ウェイトにさらに感度(sensitivity)を追加して調整する必要があるそうです。

 感度の追加自体は単なる係数を付けてあげればいいので、

 \displaystyle W = \frac{|\vec{\omega}_{x_0\rightarrow x_1} \cdot \vec{n}|^4r}{2\pi a^2} P_A \times Sensitivity

という形で簡単に実装できます。この時、P_Aは定数であるため、Sensitivityの値に含めることができて最終的には

 \displaystyle W = \frac{|\vec{\omega}_{x_0\rightarrow x_1} \cdot \vec{n}|^4r}{2\pi a^2} Sensitivity

という計算式を用いれば実装ができます。 一応,サンプリングによって確率密度は変わるので、確率密度を書けば

\displaystyle W = \frac{|\vec{\omega}_{x_0\rightarrow x_1} \cdot \vec{n}|^4}{P{lens}(x_0) a^2} Sensitivity

ともなります。これをパスの重みとして最後にかけてあげることでレンズのウェイトが実装できます。

絞りの実装

 カメラの絞りはカメラのレンズの前に覆いかぶさる形で光を遮蔽することで、カメラに入る光の量を調整します。なので、レイトレでの実装方法は単に絞りに当たらないレンズ上の点を選択するだけで実装が行えます。

 よくあるカメラの絞りのイメージは六角形や円形だったりします。今回は六角形の絞りの実装を行いましたが、単に絞りの形状さえ変えれば他の形の絞りも作ることができます。

 私は2つの実装方法を試してみましたので、両方の実装方法を記載します。

距離関数での判定

 レンズ上の点のサンプリングは2次元上で行ってましたが、この時距離関数を使ってサンプル点が絞りにあるかどうかを判定することで行う方法です。

 六角形の距離関数はIQさんの以下のサイトに載っており、点が六角形の外にある時は正を、中にある時は負を返すような関数になっています。この関数を絞りの形状として、レンズ上のサンプルした点で距離関数に入れた時、正を返せば即ち絞りに遮られる点として寄与を取らないようにすれば絞りを作ることができます。

 しかしながら、この方法では寄与が全くない点をサンプルするため、収束が遅くなります。再びサンプリングして絞りに当たらない点を取るまでやるということも考えられますがどちらにしろ無駄な処理が増えてしまいます(利点を挙げるとすればどんな形状でもサンプルできることぐらい)。そのため、寄与がある点だけをサンプリングするようにした方法として次の方法を取りました。

六角形のサンプリング

 こちらの方法は直接レンズのサンプリングを六角形のサンプリングにしてしまう方法です。方法についてはこちらのサイトを参考にさせて頂きました。

3次元空間、複数三角形内に均一に、点をばらまく

 n角形のサンプリングはn個の三角形のサンプリングという形で実装することができ、六角形も同様に6つの三角形を用意し、その中から一つを選び三角形のサンプリングをするという形でサンプリングが可能です。

 具体的な手順としては 1. 一様乱数で0,1,2,3,4,5の番号を作る 2. 番号に応じた三角形で一様サンプリングを行う 3. pdfを計算する

という形で行う。

私の実装では一様乱数で手順1を行い、事前に作った三角形を番号分回転させることで番号に応じた三角形を作りました。

三角形の一様サンプリングはそれぞれの頂点が\vec{A},\vec{B},\vec{C}として、一様乱数を$u,v$とした時、

\displaystyle \vec{P} = \vec{A}(1 - \sqrt{u}) + \vec{B}(\sqrt{u}(1 - v)) + Cv\sqrt{u}

と行うことでできる。この時の確率密度$P_{tri}$は三角形の面積の逆数になるため、外積

\displaystyle P_{tri} =\frac{2}{|(\vec{B} - \vec{A})\times (\vec{C} - \vec{A})|}

と求めることができます。また、六角形のサンプリングであったため、これにさらに1/6をかけて

\displaystyle P_{hex} = \frac{1}{3|(\vec{B} - \vec{A})\times (\vec{C} - \vec{A})|}

となる。サンプリングそのものが変わるため、ウェイトの方にこの確率密度を与えることで実装ができます。

 このように六角形のサンプリングが可能なため、光が通る点のみをサンプリングするできるようになります。これを使用した絞りは距離関数に比べ収束が目に見えて早くなります。n角形や円形のサンプリングもできるため、絞りの実装はこちらのようなサンプリングによる実装の方が良いと思います。

実装

GLSLでの実装を行いました。

Ray thinLensCamera(vec2 uv,vec3 atlook,vec3 camerapos,inout bool ap,inout float weight){

 //カメラの各パラメータ (外部から受け取っている)
    float f = cameraLens.x;
    float F = cameraLens.y;
    float b = cameraLens.z;

    float a = b * f / (b - f);
    
    //カメラのローカル基底
    vec3 up = vec3(0,1,0);
    vec3 cw = normalize(atlook);
    vec3 cu = normalize(cross(cw,up));
    vec3 cv = normalize(cross(cu,cw));
    
    //カメラのポジション
    vec3 X0 = camerapos + uv.x * cu + uv.y * cv;
    vec3 C = camerapos + cw * a;
    vec3 e = normalize(C - X0);

    //レンズ上の位置サンプリング
    //ランダム関数
    vec2 xi = hash23(vec3(iTime * uv.x, iTime * iTime * uv.y , iTime * iTime * iTime));
    xi = hash22(xi);
    vec3 S;
    vec3 P;
    float pdf = 1.;
    if(!TestCheck){
      float phi = 2.0 * M_PI * xi.x;
      float r = xi.y * f / (2.0 * F);
      S = C + r * cos(phi) * cu + r * sin(phi) * cv;
      P = C + e * b / dot(e,cw);
      pdf = 2.0 * M_PI / r;
      
      ap = cameraAperture(vec2(r * cos(phi),r * sin(phi)),f/(2.0 * F));
    }
    else{
        float LensR = f/(2.0 * F) * cameraAp;
        float TriN = float(floor(xi * 6.0));
        //三角形の点
        vec2 TriA = vec2(0.0);
        vec2 TriB = rotate(vec2(0.0,LensR),TriN * 2.0 * M_PI / 6.0);
        vec2 TriC = rotate(vec2(0.0,LensR),(TriN + 1.0) * 2.0 *M_PI/6.0);
        
        //三角形の一様サンプリング
        xi = hash22(xi);
        float sw = sqrt(xi.x);
        vec2 TriP = TriA * (1.0 - sw) + TriB * (sw*(1.0 - xi.y)) + TriC * sw * xi.y;
        S = C + TriP.x * cu + TriP.y * cv;
        P = C + e * b / dot(e,cw);
        
        //pdf
        pdf = 2.0 * 1.0 / (abs(length(cross(vec3(TriB,0),vec3(TriC,0))))*6.0);
        ap = false;
    }
    //カメラレイの生成
    Ray camera;
    camera.origin = S;
    camera.direction = normalize(P - S);
    
    //ウェイト
    float cosine = abs(dot(camera.direction,cw)); 
    weight = cosine * cosine *cosine * cosine / (pdf * a*a);
    return camera;
}

終わりに

 薄レンズモデルは実装そのものはそこまで難しいものではなく、何なら難しいウェイトや絞りは別段なくてもそれっぽい絵が出ます。意外とボケさえあればかなりリアリティが増して、一瞬写真かと思うような画像が作れて中々簡単な実装の割にはいい表現ができます。なのでパストレができるようになった次の段階にやるべきこととして実装するのが良い気がします。

 現実のカメラは光を屈折などさせることによって被写界深度など様々なエフェクトが物理的に生じます。ポストエフェクトでこれらを表現しようとなるとやはり正確性に欠けてしまうことが多いと思います。そうした中、光の動きさえシミュレーションできればこうしたエフェクトが正確に出せるというのはやはりレイトレの大きな強みだと感じます。薄レンズモデルはそうした強みを簡単に感じさせられるモデルでした。

 しかしながら、実際の一眼カメラなどは幾つものレンズが組み合わさっており、薄レンズモデルのように1枚のレンズだけを使うカメラはありません。レンズフレアなどはそうした複数レンズ特有のエフェクトであり、より写実的な絵を出したいとなると現実のレンズ設計をシミュレーションすることとなってきます。

 より発展的なカメラモデルを知りたいとなれば、yumcyawizさんの記事が複数レンズのカメラモデルについてご解説なさっていますのでそちらをご覧ください。

blog.teastat.uk @yumcyawiz

参考文献

レイトレース:薄いレンズのカメラ

3次元空間、複数三角形内に均一に、点をばらまく by Ushio

被写界深度 (Depth of Field) by shocker