レンダラー開発のための測光学覚書Vol.2 レイトレースのための基本的な物理量からレンダリング方程式まで

 どうも、前回に引き続き、『レンダラー開発のための測光学覚書』シリーズ第2弾です。前回は立体角についての数学の話だったので、今回からは物理の話に入っていきましょう。

基本的な物理量

放射束

 光とは粒子であり、波である。これが今の物理の定説です。しかし未だに謎が多く、光とは何かというのは完璧に答えることはできません。しかし、光は私たちに世界を知覚させることができる、とても身近な存在ですよね。ゆえに経験からいろんなことがわかります。例えば、虫眼鏡で黒い紙に光を1点に集中させれば火が出るように、光はエネルギーを持っています。ここで紹介する「放射束」は単位時間あたりの光のエネルギーという定義で、測光の分野では最も基本的な物理量です。


 \displaystyle
\begin{align}
\Phi = \frac{dQ}{dt}
\end{align}
ここで、 Qは光のエネルギー、 tは時間です。

放射照度

単位面積当たりの放射束を放射照度といいます。


 \displaystyle
\begin{align}
E(x)=\frac{d\Phi}{dA}
\end{align}

放射強度

ここで、前回の内容である、立体角の概念を導入します。ラジアンの立体版でしたね。忘れてしまった方はこちらをどうぞ。
drumath.hatenablog.com
放射強度は単位立体角当たりの放射束として与えられる物理量です。


 \displaystyle
\begin{align}
I(\vec{\omega})=\frac{d\Phi}{d\vec{\omega}}
\end{align}
ですので、逆に半球について積分してやれば、放射束が求められます。

 \displaystyle
\begin{align}
\Phi = \int_\Omega I(\vec{\omega}) d\vec{\omega}
\end{align}

放射輝度

 放射輝度は、単位投影面積、単位立体角あたりの放射束として定義されます。先に式を出すと、


 \displaystyle
\begin{align}
L(x, \vec{\omega})=\frac{d^2\Phi}{\cos \theta dAd\vec{\omega}}
\end{align}
 \cos \thetaがいきなり出てきましたが、これは定義が単位面積ではなく、「単位投影面積」ということに起因します。
例えば、下の図のように、光が地面に向かって \thetaの角度で入射しているとしましょう。
f:id:drumath:20180228002606p:plain
このとき、光が当たっている面積を Aとします。このときの放射照度は \displaystyle E=\frac{\Phi}{A}ですね。
ここで、光の進行方向に垂直な面を考えます。すると、その面と光が当たっている面積は、図に示した通り、 A\cos \thetaとなります。これを、投影面積といいます。
放射輝度積分すれば、いろんな物理量が導けるので、シミュレーションでは大変重宝します。例えば放射束を出したい時には

 \displaystyle
\begin{align}
\Phi = \int_A \int_\Omega L(x,\vec{\omega})(\vec{n} \cdot \vec{\omega})d\vec{\omega}dx
\end{align}
上の式では、地面に対する法線ベクトルの単位ベクトルと、入射ベクトルの単位ベクトルの内積を取ることで入射角を求めることができます。この (\vec{n} \cdot \vec{\omega})の項をコサイン項と呼びます。

マテリアルの決定

 ここまで光について見てきました。ここでは実際にレンダリングするとき、もう一つ重要な「どんな材質に光が当たったか」を決めるための関数をご紹介します。

BSSRDF(英:bidirectional scattering surface reflectance distribution function、日:双方向散乱面反射率分布関数)

 光が物質の表面に当たったら、入射角と同じ大きさの反射角で反射し、屈折の法則に従って屈折することは高校生、いや中学生で習いました。しかし現実はそんなに甘くありません。なぜなら完全に平らな面など存在しないからです。実際にはすごく小さな凸凹があって、その凸凹に当たって光はあらゆる方向に反射していきます。これを散乱というのでしたね。また、光はどの物質でも通過します。つまり物質の表面で屈折した光はそのうち物質中で反射して出てくるかもしれないし、反射を繰り返して物質を通過するかもしれないのです。このように物質を通過した光が反射して出ていくことを「表面下散乱」といいます。このように表面化散乱の様子を示す関数が、BSSRDFと呼ばれる関数です。

f:id:drumath:20180228153043p:plain
https://news.mynavi.jp/article/graphics-59/ から引用
上の画像からもわかる通り、人の肌の質感などのレンダリングではこの技術が用いられます。しかし、ちょっと複雑ですね。なので普通はBSSRDFを簡略化したBSDFを用います。

BRDF(英:bidirectional reflectance distribution function、日:双方向反射率分布関数)

 BRDFはBSSRDFを簡略化したモデルです。正確に言うと、BSSRDFの簡略版は、BRDFとBTDFを足しあさせたBSDFというモデルなのですが、拡散反射や鏡面反射だけのモデルの場合、BRDFで事足りるということなんだと思います。BRDFは入射位置からでる反射光をモデル化したもので、先ほどの表面下散乱を考えないもので、入射光による放射照度と反射光による放射輝度の比で与えられます。
ここで、式の中の物理量に以下のものを使います。(なんか今更な感じもしますが)

物理量 意味
 \Phi 放射束
 E 放射照度
 L 放射輝度
 f_{r} BRDF
 \theta_{i} 入射角
 \vec{\omega} 入射光、及び反射光の方向
 \vec{n} 入射面に対する法線

添え字の i rはそれぞれ入射光関係(incidence)、反射光関係(reflectance)という意味です。


 \displaystyle
\begin{align}
BRDF: f_{r} (x, \vec { \omega }_{i}, \vec{\omega}_{r}) &= \frac{dL_{r}(x,\vec{\omega}_{r})}{dE_{i}(x, \vec{\omega}_{i})} \\\ &= \frac{dL_{r}(x, \vec{\omega}_{r})}{L_{i}(x, \vec{\omega}_{i})(\vec{n} \cdot \vec{\omega}_{i})d\vec{\omega}_{i}}
\end{align}
さて式変形についての説明ですが、

 \displaystyle
\begin{align}
L_{i}(x, \vec{\omega}_{i}) = \frac{d^{2}\Phi}{\cos \theta_{i} dAd\vec{\omega}_{i}} = \frac{dE(x, \vec{\omega}_{i})}{(\vec{n} \cdot \vec{\omega}_{i})d\vec{\omega}_{i}}
\end{align}
となるためですね。

BRDFの意味(高校生なりの解釈)

 私は最初BRDFの式を見て、なんでこうなるんだかわかりませんでしたw というのも、なぜ放射照度と放射輝度という二つの違う量の比で表すのかが意味不明でした。しかもこんなに複雑な式で、さも微積させる気しかないような雰囲気ですよね。頭の中ではグラサンかけてる厳ついおっさんが突然出てきたみたいなイメージでした。絡みづらいわ。
しかし、BRDFもこんな風に変形すれば、目的は一目瞭然です。


 \displaystyle
\begin{align}
 dL_{r} ( x, \vec { \omega }_{r} ) &= f_{r} ( x, \vec { \omega }_{i}, \vec{\omega}_{r} )  L_{i} (x, \vec { \omega }_{i})  (\vec{n} \cdot \vec { \omega }_{i})  d \vec { \omega }_{i} \\\
\Leftrightarrow L_{r} ( x, \vec { \omega }_{r} ) &= \int_{\Omega}  f_{r} ( x, \vec { \omega }_{i}, \vec{\omega}_{r} )  L_{i} (x, \vec { \omega }_{i})  (\vec{n} \cdot \vec { \omega }_{i})  d \vec { \omega }_{i} \tag{1}
\end{align}
つまり何が言いたいかというと、BRDFなんてものは関数というよりもよりも、係数みたいなもんじゃないのか。
このようにすると、後述するレンダリング方程式とほぼ形は一緒です。つまり、いま私たちが求めたいのは放射輝度である。前述のとおり放射輝度さえわかればほかの物理量は積分すれば求まりますからね。だから普通はBRDFを求めよう、なんてことはしないんじゃないかな。
もう一回式(1)を見てみましょう。

 \displaystyle
\begin{align}
 L_{r} ( x, \vec { \omega }_{r} ) &= \int_{\Omega}  f_{r} ( x, \vec { \omega }_{i}, \vec{\omega}_{r} )  L_{i} (x, \vec { \omega }_{i})  (\vec{n} \cdot \vec { \omega }_{i})  d \vec { \omega }_{i} \tag{1}
\end{align}
目的は L_{r}を求めることでした。被積分関数を見てみると、3つの項があります。左から順に、BRDF、入射光の放射輝度、コサイン項です。反射光の放射輝度を求めるのには、入射光の放射輝度が必要だということは、納得がいくと思います。またコサイン項があることによって放射輝度が一緒でも、入射角によって反射光の放射輝度が違うことも読み取れます。だから、BRDFというのは、入射光がどれだけ変化するかを表しているだけなのです。ここまでよろしいでしょうか。
半球で積分をしている意味は、面に対して、あらゆる方向から入射してくる光の和という意味ですね。このおかげで、グローバルイルミネーションが実現できます。

レンダリング方程式

さて、ついにレンダリング方程式です。とはいえ、先ほどの内容がわかっていれば余裕です。ようは出ていく放射輝度を求める方程式です。
出射する光というのは、表面から放射される光と反射される光の和で求まります。これは感覚的ですね。つまり、

 L_{r} 反射光(reflected)の放射輝度
 L_{e} 放射光(emitted)の放射輝度
 L_{o} 出射光(outgoing)の放射輝度

とすれば、求める L_{o}


 \displaystyle
\begin{align}
L_{o} = L_{e} + L_{r}
\end{align}
となります。また、式(1)より

 \displaystyle
\begin{align}
L_{o}(x, \vec{\omega}_{r}) = L_{e}(x, \vec{\omega}_{r}) + \int_{\Omega}  f_{r} ( x, \vec { \omega }_{i}, \vec{\omega}_{r} )  L_{i} (x, \vec { \omega }_{i})  (\vec{n} \cdot \vec { \omega }_{i})  d \vec { \omega }_{i} 
\end{align}
と書き換えられます。お疲れさまでした、レンダリング方程式の出来上がりです!

最後に

ここまで学習するのにはとても時間がかかりました。しかし、レンダリングの原理が少しわかったような気がして、自信が付きました。既存のレンダラーなど見ていると、いつもは何気なく見ている言葉も、「これ知ってる!」ってなってなんだか楽しいですw BlenderのCyclesレンダラーも、マテリアルの名前は「~~BSDF」という風になってますけど、材質決定の関数だなんて思いませんでした。これからも頻度は高くないかもしれないけど、このシリーズは続けていこうと思います。最後まで見てくださり、ありがとうございました。

参考文献

qiita.com
↑いつもお世話になっています。より詳しい内容が知りたい方はこのリンクへ飛んでみてください。
raytracing.hatenablog.com
↑教育用のレンダラーを作ってくださっています。日本語のコメントがあるって素敵
qiita.com
↑こちらもわかりやすく解説してくださっています。

Blenderで水の中の表現

 どうも、今回はYafarayレンダラーを使って海の中を作ってみます。あまり長くはならないと思うのでお付き合いください。

海のオブジェクトとその他の環境を作る

 では早速作っていきます。まず、画面に平面のオブジェクトを作成し、「海洋モディファイア」を付与します。f:id:drumath:20180220180251p:plain
すると、こんな感じになります。
f:id:drumath:20180220180313p:plain
そして海洋を囲むように空間を作ります。
私は上面を除去したCubeオブジェクトに「厚み付けモディファイア」を付与しました。別に上面が閉じていてもいいのですが、Lampでサンを使いたいので私は開けました。
今の画面はこんな感じ。中にカメラも入れてください。
f:id:drumath:20180220180633p:plainf:id:drumath:20180220180640p:plain

海洋の設定

 先ほど追加した「海洋もモディファイア」ですが、私の設定はこんな感じ
f:id:drumath:20180220180911p:plain
時間を変えると波が変化するので、いい感じの時間を選んでください。イイ感じの時間とは、波がまぁまぁ立っているときですかね。波がないと、海洋独特のコースティクスが生まれませんので。
 マテリアルも編集していきます。
f:id:drumath:20180220181212p:plain
IOR(屈折率)は水の値を設定しました。

レンダリングしてみる

 ではフォトンマッピングレンダリングします。主要な設定は以下の通り。

名前
Lighting Method Photon Mapping
深度 16
バウンス 9
サンプル 16

f:id:drumath:20180220182059p:plain

海洋モディファイアの解像度の値を7→10に変えてみましょう。
f:id:drumath:20180220182527p:plain

おわりに

今回はすごく早く書き終わりましたw とはいえCyclesでの再現で困っていたのでYafarayでできてよかったです。これからもYafarayの記事を書くかもしれないのでよろしくお願いします。
Yafarayについての他の記事↓
drumath.hatenablog.com

追記(2018/3/2)

動画で作ってみました。コンポジットし忘れたので、AviUtlでグロー効果をかけました。
youtu.be

レンダラー開発のための測光学覚書Vol.1 立体角について

 どうも。大学生の勉強の予習になればと、レンダラー制作に向けて少しずつ勉強を始めている、drumathです。実際わからないことだらけなので、『レンダラー開発のための測光学覚書』というシリーズで、学習してきたことを整理していきたいと思います。もしよろしければ、解釈や式の導出などが間違っていた場合に教えていただけるとありがたいです。
 今回は本格的に光学の分野には触れませんが、物理量の基本となる立体角についてまとめていきます。

立体角とは?

ラジアンの定義を振り返る

 高校になっていきなり導入された弧度法ですが、なぜ\displaystyle 60^\circ = \frac{\pi}{3}となるのか、説明できますか?「簡単だよ!」という方は飛ばしていただいて構いません。
弧度法の定義は\displaystyle \theta = \frac{l}{r}で、\displaystyle \thetaは弧度法での角度、単位はラジアンです。\displaystyle l\displaystyle \thetaでの弧の長さ。\displaystyle rは半径です。つまり、円周の長さと半径の比であらわされる値なのです。とても直観的だと思いませんか?対して度数法の場合、一周を360個に分けた内、どれくらいを占めるかという、とてもあいまいな値です。一周が360°なんて一体だれが考えたのでしょうか。古代文明の60進法からですよね、たぶん。
 数学Ⅲを習うと、弧度法を使うメリットがわかります。詳しくはほかの記事に書いてあることを参照してほしいのですが、\displaystyle (\sin x)' = \cos xのような三角関数微分は弧度法を使うことによって、きれいな形で表されます。個人的な解釈なのですが、三角関数自体、直角三角形の辺の比で表される関数なので、弧と半径の比という定義はなじみやすいのではないかと考えます。

平面から立体へ

 少し話は外れましたが、要は円周の長さと半径の比がラジアンなわけです。平面角では長さを使ったので、立体角では面積を使ってはいかかでしょうか。ということで立体角の定義は

{ \displaystyle
\omega  = \frac{A}{r^2}
}
となります。 Aは面積で、 rは円の半径です。立体角の単位は「ステラジアン」といいます。
定義に沿うと、全球の立体角は

 \begin{align} \displaystyle
\omega &= \frac{A}{r^2} \\\ &=\frac{4 \pi r^2}{r^2} \\\ &= 4 \pi
\end{align}
となります。下のようなイメージです。
f:id:drumath:20180218152709p:plain

微小立体角を求める

設定

 光の物理量は微分積分のオンパレードです。ですので、ここで微小立体角、 d \omegaを決めたいと思います。まず先ほどの画像の領域について、横が a、縦が bの微小長方形で、立体角が \omegaと定めます。
f:id:drumath:20180218153603p:plain
当然、


{ \displaystyle
 \omega = \frac{ab}{r^2} \tag{1}
}
となりますね。

微分だ!

さて、では a bが微小な量だとします。そうすると立体角 \omegaも微小な量になり、長方形領域は点のように小さくなります。ここで領域の方向を以下のように定めます。
f:id:drumath:20180218155923p:plain
ではまず微小 aについて考えましょう。長さ aである横の辺を含む円周の長さは弧度法の定義から求めることができるでしょう。弧度法での円周の長さ l


{ \displaystyle
l = r\theta
}
と表わせます。また、今回求めようとしている円の半径は r\sin \thetaですので、円周の長さは 2\pi r \sin \thetaです。いったん図にして整理しましょう。
f:id:drumath:20180218220530p:plain
では、なぜ円周の長さは 2\pi r \sin \thetaなのでしょうか。それは中心角が \phiである扇形の弧の長さが r\phi \sin \thetaと表わされ、今は \phi = 2\piの場合を考えているからです。そして、微小 aは、この円周が微小 \phiの場合を考えていることがわかります。よって

 \begin{align} \displaystyle
a &= dl \\\ &= r\sin \theta d \phi \tag{2}
\end{align}
ということになります。
すみません、たぶんもっとうまい説明があるし、なにしろすごく遠回りな考えなのはわかっていますが、私が思考した結果がこれなのです。引き続き bについても同じような導出を行います。このやり方が嫌だという人は読み飛ばしてください。申し訳ございません。
さて、続きを見ていきましょう。長さ bである縦の辺を含む円は、半径 rですので、 2\pi rとなりますね。図にするとこんな感じ。
f:id:drumath:20180218222630p:plain
 aの長さを求めたときのように、この 2\pi rというのは r\thetaにおいて \theta = 2\piだからでした。よって

 \begin{align} \displaystyle
b &= dl \\\ &= rd \theta \tag{3}
\end{align}
やっと微小領域の面積が求まりますね。式(2)と式(3)を式(1)に代入します。すると、

 \begin{align} \displaystyle
d\omega &= \frac{ab}{r^2} \\\ &= \frac{r \sin \theta d \phi \times r d \theta}{r^2} \\\ &= \sin \theta d \theta d \phi
\end{align}
となり、無事、微小立体角を導くことができました!

終わりに

今回は以上になります。立体角の概念は光学において基本的なものになりますが、私は学習当初あまりイメージできなかったので、数学Ⅲを習っている高校生にわかるように記事を書いたつもりです。最後まで読んでいただき。ありがとうございました。

参考文献

qiita.com

OpenGLでobjファイルの3Dデータを表示してみる

 こんにちは。今回は入門したてのOpenGLBlender出力のobjファイルビューワを作りたいと思います!

作るものについて

 今回は下の画像のような3Dビューワを作ります。↓

f:id:drumath:20180205232453p:plain
自作モデルを読み込んだ3Dビューワ
ほんとにシンプルなビューワですね(笑) 見て分かる通り、テクスチャやマテリアルなどは複雑化しそうなので今回は実装しません。今回使用するのは頂点、辺、面のデータのみです。

実装プラン

 OpenGLをメインのライブラリとして使っていきますが、objファイル解析をRubyで実装しようと思います。その理由は、単純にRubyのほうがC++よりも文字列の処理がやりやすいからです。本当はオールC++でも全然できると思いますが、私が純粋なルビイストということもあり、Rubyを使用します。つまり、

  1. objファイルをRubyで解析
  2. 無駄なデータを省き、簡単な形式にしてファイルを吐き出す
  3. C++でファイル読み込み
  4. OpenGLで描画

という流れで実行していきます。また、objファイルはBlenderによって出力された3Dモデルデータを使用します。

開発環境など

 使用する環境はこちらです。

項目 内容
OS Windows10
ライブラリなど OpenGL、freeglut
コンパイラ Ruby2.2.4p230、bcc32
エディタ Visual Studio Code 1.19.3
Blenderバージョン Blender2.79

いざ、Rubyサイドを実装だ

Blenderでobjファイルにエクスポート

 例えば、BlenderでCubeを追加したときのデフォルトのオブジェクトをobjファイル出力してみましょう。

f:id:drumath:20180205235638p:plain
一辺2の立方体

まず、[ファイル]→[エクスポート]→[Wavefront(.obj)]とクリックしていきます。そうすると、下の画面のようになります。
f:id:drumath:20180206000141p:plain
普通にエクスポートボタンを押したくなると思いますが、ここで「三角面化」を忘れないでください!今回は面が三角形であることを前提に頂点を結んでいきます。もし多角形の面があったらちょっと厄介なことになりますので、必ず設定してください。

objファイルの形式を知る

 先ほどの要領でobjファイルをエクスポートすると、このような内容が吐き出されます。

# Blender v2.79 (sub 0) OBJ File: ''
# www.blender.org
mtllib cube_obj_test.mtl
o Cube_Cube.001
v -1.000000 -1.000000 1.000000
v -1.000000 1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v -1.000000 1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v 1.000000 1.000000 1.000000
v 1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -1.000000
vn -1.0000 0.0000 0.0000
vn 0.0000 0.0000 -1.0000
vn 1.0000 0.0000 0.0000
vn 0.0000 0.0000 1.0000
vn 0.0000 -1.0000 0.0000
vn 0.0000 1.0000 0.0000
usemtl None
s off
f 2//1 3//1 1//1
f 4//2 7//2 3//2
f 8//3 5//3 7//3
f 6//4 1//4 5//4
f 7//5 1//5 3//5
f 4//6 6//6 8//6
f 2//1 4//1 3//1
f 4//2 8//2 7//2
f 8//3 6//3 5//3
f 6//4 2//4 1//4
f 7//5 5//5 1//5
f 4//6 2//6 6//6

これは何を示しているのでしょうか。
 まず、

v -1.000000 -1.000000 1.000000

のような「v ~」ではじまる行は、頂点の座標を記録しています。Vはおそらく頂点の英語Vertexを意味しています。「v」につづく小数が左からx,y,z座標を表わしています。
そして、

f 2//1 3//1 1//1

のように「f~」ではじまる行は面のデータを記録しています。「f」のあとに「2//1」のような数字をスラッシュで区切ったような記述がみられますが、それは「頂点/テクスチャ座標/法線ベクトル」というデータを表わしています。今回使うのはい一つ目の数字だけですので、あとは無視してください。この1つ目の数字は、どの頂点を使うかを示しています。つまり「2//1」だった場合、一番左の「2」という数が、「2番目の頂点を使うぞ」と言っています。何番目の頂点かは「v」から始まる行の上から何番目かで決まります。
面の情報はどの三つの頂点を使うかを表しています。つまり

f 2//1 3//1 1//1

ならば、この面は2番目の頂点と3番目の頂点と1番目の頂点で構成されていることを示します。

Rubyでobjファイルコンバータを作る

 Rubyでは、objファイルを読み込み、あとでC++側でscanfするだけで簡単にデータが格納できるようなファイルを新たに作ります。というわけでコードはこんな感じ。

# vars
vertex = []
face = []

# const regexp
Face_regexp = /^f (\d+)\/\d*\/\d* (\d+)\/\d*\/\d* (\d+)\/\d*\/\d*/
Vertex_regexp = /^v (.*) (.*) (.*)/

# conversion obj file
File.open(ARGV[0], 'r') do |file|
  file.read.split("\n").each do |line|
    if line =~ Face_regexp
      face << [$1, $2, $3].map { |m| m.to_i - 1 }
    elsif line =~ Vertex_regexp
      vertex << [$1, $2, $3].map(&:to_f)
    end
  end
end

# output vertex data
File.open('vData.txt', 'w') do |file|
  vertex.each do |e|
    file.puts e.join(', ')
  end
end

# output face data
File.open('fData.txt', 'w') do |file|
  face.each do |e|
    file.puts e.join(', ')
  end
end

たとえば、さきほどの立方体のデータを読み込むと、

ruby obj_convert.rb cube.obj

vData.txtとfData.txtという二つのファイルが作成され、vData.txtには、

-1.0, -1.0, 1.0
-1.0, 1.0, 1.0
-1.0, -1.0, -1.0
-1.0, 1.0, -1.0
1.0, -1.0, 1.0
1.0, 1.0, 1.0
1.0, -1.0, -1.0
1.0, 1.0, -1.0

fData.txtには、

1, 2, 0
3, 6, 2
7, 4, 6
5, 0, 4
6, 0, 2
3, 5, 7
1, 3, 2
3, 7, 6
7, 5, 4
5, 1, 0
6, 4, 0
3, 1, 5

という内容が出力されていることがわかります。vDataのほうは、単純に座標のリストです。fDataのほうは、三角面を構成する座標のインデックスを表わしていますが、実際コードを見るとわかりますが、インデックスを-1しています。それは、objファイルは上から1,2,3...と数えますが、C++の配列では0,1,2...と数えるので、C++の仕様に合わせるためです。

いざ、OpenGLで描画

ファイル階層

 ファイル階層はこんな感じになります。

  • \GL
  • \lib
    • \x86
      • freeglut.dll
      • freeglut.exp
      • freeglut.iobj
      • freeglut.lib
      • freeglut.pdb
  • ObjTest.cpp
  • obj_trans.rb

ObjTest.cppを書いていく

 最初にコードを出します。私はこんな感じで書きました。

#include <windows.h>
#include <stdio.h>
#include "GL\freeglut.h"

#pragma comment (lib, "lib\x86\freeglut.lib")

#define ARRAY_MAX 10000000

float vertex[ARRAY_MAX];
int lines[ARRAY_MAX];
int vertexDataSize=0, lineDataSize=0;

void disp()
{
  glClearColor(0,0,0,1);
  glClear(GL_COLOR_BUFFER_BIT);

  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(3,GL_FLOAT,0,vertex);

  // 頂点の描画
  glPointSize(3);
  glBegin(GL_POINTS);
  {
    for(int i=0;i<vertexDataSize;i++)
      glArrayElement(i);
  }
  glEnd();

  glColor3f(0.6,0.35,0);
  glBegin(GL_TRIANGLES);
  {
    for(int i=0;i<lineDataSize;i++){
      glArrayElement(lines[i*3]);
      glArrayElement(lines[i*3+1]);
      glArrayElement(lines[i*3+2]);
    }
  }
  glEnd();

  glColor3f(1.0,0.75,0);
  glBegin(GL_LINES);
  {
    for(int i=0;i<lineDataSize;i++){
      glArrayElement(lines[i*3]);
      glArrayElement(lines[i*3+1]);

      glArrayElement(lines[i*3+1]);      
      glArrayElement(lines[i*3+2]);

      glArrayElement(lines[i*3+2]);
      glArrayElement(lines[i*3]);
    }
  }
  glEnd();
  glFlush();
}

void timer(int x)
{
  glRotatef(1,0,1.0,0);
  glutPostRedisplay();
  glutTimerFunc(50,timer,0);
}

void InitialProc()
{
  FILE *fpVData, *fpFData;
  fpVData = fopen("vData.txt","r");
  fpFData = fopen("fData.txt", "r");

  if((fpVData==NULL)||(fpFData==NULL)){
    printf("file error!!\n");
    return;
  }

  while (fscanf(fpVData,"%f, %f, %f",&vertex[vertexDataSize*3],&vertex[vertexDataSize*3+1],&vertex[vertexDataSize*3+2])!=EOF)
    vertexDataSize+=1;

  while(fscanf(fpFData,"%d, %d, %d",&lines[lineDataSize*3],&lines[lineDataSize*3+1],&lines[lineDataSize*3+2])!=EOF)
    lineDataSize+=1;
}

int main(int argc, char **argv)
{
  InitialProc();

  glutInit(&argc, argv);
  glutInitWindowPosition(50,100);
  glutInitWindowSize(500,500);
  glutInitDisplayMode(GLUT_SINGLE|GLUT_RGBA);

  glutCreateWindow("Obj Test");

  glRotatef(30,1.0,0,0);
  glRotatef(30,0,1.0,0);
  glScalef(0.8,0.8,0.8);

  glutDisplayFunc(disp);
  glutTimerFunc(100,timer,0);

  glutMainLoop();

  return 0;
}

InitialProc()関数内ではvData.txtとfData.txtからデータを入力して配列に格納しています。こうすることで、あとでglArrayElementsで簡単に頂点を描画したり、辺を作ったりすることができます。
また、timer()関数で0.05秒ごとに回転させています。

最後に

今回はOpenGLの覚書ということで記事を書きました。まだまだ拙いコードではありますが、objファイルビューワが無事開発できてよかったです。最終的にはBlenderのような3DCGアプリをいつか作ってみたいです。

【自己紹介もかねて】Yafarayでコースティクスと光の分散(dispersion)をレンダリングしてみる

 初めまして。初投稿になります、drumathです。タイトルにある通り、自己紹介もかねまして、先日Blenderに導入したYafarayレンダラーについて少し記事を書こうと思います。

 

自己紹介

 PCいじりが好きな高校生です。最近はOpenGLで3Dソフトを作っていて、前はXamarinでスマホアプリを作ったり、Unityでゲームを作ったりしていました。どれも中途半端に初心者なのでどれか一つに絞って極めたい!と思いつつ、ほかの方面にも興味が出てしまう、なんていう贅沢な悩みを抱えています。他にも3DCGをやったり、アコギを弾いたり、ドラムを叩いたり、クトルゥフTRPGをやったり・・・と飽きっぽい性格なので趣味がどんどん増えていきます。はてなブログはプログラミングするときにいつもお世話になっていたので、いつか自分でも書いてみたいなぁなんて思っていました。文才がないので、まとまりのない文章になってしまうかもしれませんが、ぜひ興味のある記事にはコメント、シェア等してくださるとありがたいです。これからどうぞよろしくお願いします。

 

最近のマイブーム「レンダリング

 前述したとおり飽きっぽい性格なので、マイブームはちょくちょく変わります。次の記事を書くころにはまた変わってしまっているかもしれませんし、この記事を書くきっかけにもなったことなので、少しご紹介します。

 レンダリングとは、3Dで作られたオブジェクトを光の反射、屈折や陰影をコンピュータ内で計算、シミュレートし、2D画像に変換することを言います。3DCGの作品を作る時にはだいたいレンダリングし、ポストプロセス(コンポジットなど)を踏んで完成となります。レンダリングという作業は日常的にやっていたのですが、今使っているノートパソコンにグラボを積んでいないのでレンダリングはすべてCPUで行っている状態です。ゆえにレンダリング速度が遅くてあまり好きな作業ではありませんでした。

 しかし、最近見た『宝石の国』というアニメを見て、その美麗なCGに感銘を受けました。正確に言うと、

主題歌いいなぁ→ほう、オールCGなのか→内容めっちゃいい→(『アニメCGの現場』をみて)制作陣天才かよ・・・

という具合に自分の創作意欲を掻き立てるような、新鮮な感覚でした。

 こんなCGを作ってみたい!と思いました。宝石のようなモデルならBlenderで作り、Glass BSDFマテリアルで再現できます。ということでQiitaで記事を書きました。

qiita.com

 

Yafaray導入までの流れ

 Qiita執筆時はよくできたと思ったのですが、本物の宝石とはちょっと違うなぁって感じがしました。そんな疑問は、前述した『アニメCGの現場』で解決しました。

 自然の透明な材質に光が入るとその光は屈折し、ほかの表面に到達するときに特有の模様を描きます。これをコースティクスというのですが、BlenderのマニュアルによるとCyclesレンダラーはコースティクスをサポートしていない、というような記述が見れます。はて、どうしたものか。

entry.cgworld.jp

f:id:drumath:20180131133328p:plain

コースティクス



 「Blender コースティクス」みたいな感じで検索をかけると、それっぽい画像が出てきたり疑似的に再現していたりとデモはあるのですが、やはり新しいレンダラーを導入するのが得策だなと思い、Yafarayを導入することになりました。

 導入方法は、まず下記のYafarayの公式サイトからYafarayレンダラーのソースなどが入った.zipファイルをダウンロードし、Blenderで「ファイル」→「ユーザー設定」→「アドオン」の画面の下にある「ファイルからアドオンをインストール」をクリックして先ほどDLしたzipファイルを指定すれば完了です。

Download | YafaRay

レンダリングシステムについての話

 レンダリングとは光の計算をして3DCGを2Dの作品に変換する処理だということは前に話しました。光の計算とはレンダリング方程式というものをコンピュータが解くことにより行われるのですが、その方式にはいくつか種類あります。有名なものとしては、

などがあります。

 詳しい話は数学の知識が必要なのでしませんが、レイトレーシングは光源から出た光をカメラから逆にトレースすることで計算をします。逆に、フォトンマッピング方式は光源からの光線をカメラ、光線両方からトレースすることで大域証明(GI)やコースティクスを再現できます。まさに、フォトリアルな作品を作るにはもってこいのレンダリング方式です。また、透明な材質(例えばガラスや水)を通る光の屈折や散乱には適しているが、金属による鏡面反射は計算に時間がかかる、といった得意不得意があります。今回はコースティクスを再現したいので、Yafarayレンダラーでフォトンマッピング方式のレンダリングをしたいと思います。

 

シーンを作る

 それではシーンを作っていきます。こんな感じ。↓

f:id:drumath:20180131141013p:plain

設定としてはまずCubeで空間を作って、なかをポイントライトで照らします。そしてそれとはまた別にスポットライトでダイヤモンドのマテリアルを持ったオブジェクトを照らしてコースティクスを作ります。

f:id:drumath:20180131141425p:plain

部屋の中とダイヤモンドのかけらのオブジェクトです。ダイヤモンドのマテリアルは下記のような設定で作りました。

項目
Material Type Glass

IOR

2.42

 

IORには実際のダイヤモンドの屈折率を入力します。

次に空間を照らすポイントライトと、ダイヤモンドを照らすスポットライトの設定です。

項目
ランプのタイプ ポイント
カラー #ffffff
パワー 5

ポイントライトの設定です。

項目
ライトのタイプ Spot
パワー 30
Soft Shadows あり
サンプル 128
Shadow fuzzyness 1
サイズ 15.2°

スポットライトの設定です。

実際当てているスポットライトはめっちゃ明るいです。できるだけはっきりとコースティクスを描こうと思うのでこうしました。

さて、これでレンダリングしてみましょう。レンダリングの設定は以下の通りです。

項目
Ray depth 8
Shadow depth 8
Transparent Shadows TRUE
lighting Method Photon Mapping
diffuse photons TRUE
photons 1000000
caustic photons TRUE
1000000
バウンス 9
サンプル 128
AAーフィルタ Mitchell
パス 3
AAーサンプル 6
Color nois detection TRUE

 

f:id:drumath:20180131145142p:plain

レンダリング結果

見事にコースティクスが描かれていますね。ちなみに同じような設定でCyclesレンダラーでレンダリングすると、こうなります。

f:id:drumath:20180131150030p:plain

Cyclesレンダラーの場合


これでもサンプル数は4096なのですが、ノイズが走っていますね。この結果から、どうやらCyclesでもコースティクスは再現できるようですが、かなりのサンプル数が必要だということがわかります。もしこのノイズを消したい場合には、「間接値を制限」のところに「1.0」を入力すると消えますよ。

光の分散をレンダリング

 さて、次は光の分散をレンダリングしましょう。といっても簡単で、先ほどのダイヤモンドのマテリアルの設定に「dispersion」という項目があるのですが、そこに任意の値を入れれば分散をシミュレートできるみたいです。下の画像は「0.3」を代入したものになります。

f:id:drumath:20180131150637p:plain

dispersionを0.3にしたレンダリング結果

きれいですねぇ。宝石らしくなったのではないでしょうか。

まとめ

 Yafarayレンダラーを使うことによりフォトリアルなレンダリングをすることができました。またCyclesよりもYafarayのほうがレンダリング時間もはるかに速いので、今後重宝したいと思います。

 今回は以上になります。こんな感じの記事をたまに投稿しようと思うので、よろしくお願いします。

 

追記

 アニメーションレンダリングしてみました。よかったら見てください。↓

youtu.be